1 /* 2 * Copyright 2010-2011, Haiku, Inc. All Rights Reserved. 3 * Copyright 2008-2009, Pier Luigi Fiorini. All Rights Reserved. 4 * Copyright 2004-2008, Michael Davidson. All Rights Reserved. 5 * Copyright 2004-2007, Mikael Eiman. All Rights Reserved. 6 * Distributed under the terms of the MIT License. 7 * 8 * Authors: 9 * Michael Davidson, slaad@bong.com.au 10 * Mikael Eiman, mikael@eiman.tv 11 * Pier Luigi Fiorini, pierluigi.fiorini@gmail.com 12 * Stephan Aßmus <superstippi@gmx.de> 13 * Adrien Destugues <pulkomandy@pulkomandy.ath.cx> 14 */ 15 16 17 #include "NotificationView.h" 18 19 20 #include <Bitmap.h> 21 #include <ControlLook.h> 22 #include <GroupLayout.h> 23 #include <LayoutUtils.h> 24 #include <MessageRunner.h> 25 #include <Messenger.h> 26 #include <Notification.h> 27 #include <Path.h> 28 #include <PropertyInfo.h> 29 #include <Roster.h> 30 #include <StatusBar.h> 31 32 #include <Notifications.h> 33 34 #include "AppGroupView.h" 35 #include "NotificationWindow.h" 36 37 38 static const int kIconStripeWidth = 32; 39 40 property_info message_prop_list[] = { 41 { "type", {B_GET_PROPERTY, B_SET_PROPERTY, 0}, 42 {B_DIRECT_SPECIFIER, 0}, "get the notification type"}, 43 { "app", {B_GET_PROPERTY, B_SET_PROPERTY, 0}, 44 {B_DIRECT_SPECIFIER, 0}, "get notification's app"}, 45 { "title", {B_GET_PROPERTY, B_SET_PROPERTY, 0}, 46 {B_DIRECT_SPECIFIER, 0}, "get notification's title"}, 47 { "content", {B_GET_PROPERTY, B_SET_PROPERTY, 0}, 48 {B_DIRECT_SPECIFIER, 0}, "get notification's contents"}, 49 { "icon", {B_GET_PROPERTY, 0}, 50 {B_DIRECT_SPECIFIER, 0}, "get icon as an archived bitmap"}, 51 { "progress", {B_GET_PROPERTY, B_SET_PROPERTY, 0}, 52 {B_DIRECT_SPECIFIER, 0}, "get the progress (between 0.0 and 1.0)"}, 53 { NULL } 54 }; 55 56 57 NotificationView::NotificationView(NotificationWindow* win, 58 BNotification* notification, bigtime_t timeout) 59 : 60 BView("NotificationView", B_WILL_DRAW), 61 fParent(win), 62 fNotification(notification), 63 fTimeout(timeout), 64 fRunner(NULL), 65 fBitmap(NULL), 66 fCloseClicked(false) 67 { 68 if (fNotification->Icon() != NULL) 69 fBitmap = new BBitmap(fNotification->Icon()); 70 71 if (fTimeout <= 0) 72 fTimeout = fParent->Timeout() * 1000000; 73 74 BGroupLayout* layout = new BGroupLayout(B_VERTICAL); 75 SetLayout(layout); 76 77 SetViewColor(ui_color(B_PANEL_BACKGROUND_COLOR)); 78 SetLowColor(ui_color(B_PANEL_BACKGROUND_COLOR)); 79 80 switch (fNotification->Type()) { 81 case B_IMPORTANT_NOTIFICATION: 82 fStripeColor = ui_color(B_CONTROL_HIGHLIGHT_COLOR); 83 break; 84 case B_ERROR_NOTIFICATION: 85 fStripeColor = ui_color(B_FAILURE_COLOR); 86 break; 87 case B_PROGRESS_NOTIFICATION: 88 { 89 BStatusBar* progress = new BStatusBar("progress"); 90 progress->SetBarHeight(12.0f); 91 progress->SetMaxValue(1.0f); 92 progress->Update(fNotification->Progress()); 93 94 BString label = ""; 95 label << (int)(fNotification->Progress() * 100) << " %"; 96 progress->SetTrailingText(label); 97 98 layout->AddView(progress); 99 } 100 // fall through. 101 case B_INFORMATION_NOTIFICATION: 102 fStripeColor = tint_color(ui_color(B_PANEL_BACKGROUND_COLOR), 103 B_DARKEN_1_TINT); 104 break; 105 } 106 } 107 108 109 NotificationView::~NotificationView() 110 { 111 delete fRunner; 112 delete fBitmap; 113 delete fNotification; 114 115 LineInfoList::iterator lIt; 116 for (lIt = fLines.begin(); lIt != fLines.end(); lIt++) 117 delete (*lIt); 118 } 119 120 121 void 122 NotificationView::AttachedToWindow() 123 { 124 SetText(); 125 126 BMessage msg(kRemoveView); 127 msg.AddPointer("view", this); 128 129 fRunner = new BMessageRunner(BMessenger(Parent()), &msg, fTimeout, 1); 130 } 131 132 133 void 134 NotificationView::MessageReceived(BMessage* msg) 135 { 136 switch (msg->what) { 137 case B_GET_PROPERTY: 138 { 139 BMessage specifier; 140 const char* property; 141 BMessage reply(B_REPLY); 142 bool msgOkay = true; 143 144 if (msg->FindMessage("specifiers", 0, &specifier) != B_OK) 145 msgOkay = false; 146 if (specifier.FindString("property", &property) != B_OK) 147 msgOkay = false; 148 149 if (msgOkay) { 150 if (strcmp(property, "type") == 0) 151 reply.AddInt32("result", fNotification->Type()); 152 153 if (strcmp(property, "group") == 0) 154 reply.AddString("result", fNotification->Group()); 155 156 if (strcmp(property, "title") == 0) 157 reply.AddString("result", fNotification->Title()); 158 159 if (strcmp(property, "content") == 0) 160 reply.AddString("result", fNotification->Content()); 161 162 if (strcmp(property, "progress") == 0) 163 reply.AddFloat("result", fNotification->Progress()); 164 165 if ((strcmp(property, "icon") == 0) && fBitmap) { 166 BMessage archive; 167 if (fBitmap->Archive(&archive) == B_OK) 168 reply.AddMessage("result", &archive); 169 } 170 171 reply.AddInt32("error", B_OK); 172 } else { 173 reply.what = B_MESSAGE_NOT_UNDERSTOOD; 174 reply.AddInt32("error", B_ERROR); 175 } 176 177 msg->SendReply(&reply); 178 break; 179 } 180 case B_SET_PROPERTY: 181 { 182 BMessage specifier; 183 const char* property; 184 BMessage reply(B_REPLY); 185 bool msgOkay = true; 186 187 if (msg->FindMessage("specifiers", 0, &specifier) != B_OK) 188 msgOkay = false; 189 if (specifier.FindString("property", &property) != B_OK) 190 msgOkay = false; 191 192 if (msgOkay) { 193 const char* value = NULL; 194 195 if (strcmp(property, "group") == 0) 196 if (msg->FindString("data", &value) == B_OK) 197 fNotification->SetGroup(value); 198 199 if (strcmp(property, "title") == 0) 200 if (msg->FindString("data", &value) == B_OK) 201 fNotification->SetTitle(value); 202 203 if (strcmp(property, "content") == 0) 204 if (msg->FindString("data", &value) == B_OK) 205 fNotification->SetContent(value); 206 207 if (strcmp(property, "icon") == 0) { 208 BMessage archive; 209 if (msg->FindMessage("data", &archive) == B_OK) { 210 delete fBitmap; 211 fBitmap = new BBitmap(&archive); 212 } 213 } 214 215 SetText(); 216 Invalidate(); 217 218 reply.AddInt32("error", B_OK); 219 } else { 220 reply.what = B_MESSAGE_NOT_UNDERSTOOD; 221 reply.AddInt32("error", B_ERROR); 222 } 223 224 msg->SendReply(&reply); 225 break; 226 } 227 default: 228 BView::MessageReceived(msg); 229 } 230 } 231 232 233 void 234 NotificationView::Draw(BRect updateRect) 235 { 236 BRect progRect; 237 238 SetDrawingMode(B_OP_ALPHA); 239 SetBlendingMode(B_PIXEL_ALPHA, B_ALPHA_OVERLAY); 240 241 // Icon size 242 float iconSize = (float)fParent->IconSize(); 243 244 BRect stripeRect = Bounds(); 245 stripeRect.right = kIconStripeWidth; 246 SetHighColor(tint_color(ui_color(B_PANEL_BACKGROUND_COLOR), 247 B_DARKEN_1_TINT)); 248 FillRect(stripeRect); 249 250 SetHighColor(fStripeColor); 251 stripeRect.right = 2; 252 FillRect(stripeRect); 253 254 SetHighColor(ui_color(B_PANEL_TEXT_COLOR)); 255 // Rectangle for icon and overlay icon 256 BRect iconRect(0, 0, 0, 0); 257 258 // Draw icon 259 if (fBitmap) { 260 float ix = 18; 261 float iy = (Bounds().Height() - iconSize) / 4.0; 262 // Icon is vertically centered in view 263 264 if (fNotification->Type() == B_PROGRESS_NOTIFICATION) 265 { 266 // Move icon up by half progress bar height if it's present 267 iy -= (progRect.Height() + kEdgePadding); 268 } 269 270 iconRect.Set(ix, iy, ix + iconSize - 1.0, iy + iconSize - 1.0); 271 DrawBitmapAsync(fBitmap, fBitmap->Bounds(), iconRect); 272 } 273 274 // Draw content 275 LineInfoList::iterator lIt; 276 for (lIt = fLines.begin(); lIt != fLines.end(); lIt++) { 277 LineInfo *l = (*lIt); 278 279 SetFont(&l->font); 280 // Truncate the string. We have already line-wrapped the text but if 281 // there is a very long 'word' we can only truncate it. 282 TruncateString(&(l->text), B_TRUNCATE_END, 283 Bounds().Width() - l->location.x); 284 DrawString(l->text.String(), l->text.Length(), l->location); 285 } 286 287 rgb_color detailCol = ui_color(B_CONTROL_BORDER_COLOR); 288 detailCol = tint_color(detailCol, B_LIGHTEN_2_TINT); 289 290 AppGroupView* groupView = dynamic_cast<AppGroupView*>(Parent()); 291 if (groupView != NULL && groupView->ChildrenCount() > 1) 292 _DrawCloseButton(updateRect); 293 294 SetHighColor(tint_color(ViewColor(), B_DARKEN_1_TINT)); 295 BPoint left(Bounds().left, Bounds().top); 296 BPoint right(Bounds().right, Bounds().top); 297 StrokeLine(left, right); 298 299 Sync(); 300 } 301 302 303 void 304 NotificationView::_DrawCloseButton(const BRect& updateRect) 305 { 306 PushState(); 307 BRect closeRect = Bounds(); 308 309 closeRect.InsetBy(3 * kEdgePadding, 3 * kEdgePadding); 310 closeRect.left = closeRect.right - kCloseSize; 311 closeRect.bottom = closeRect.top + kCloseSize; 312 313 rgb_color base = ui_color(B_PANEL_BACKGROUND_COLOR); 314 float tint = B_DARKEN_2_TINT; 315 316 if (fCloseClicked) { 317 BRect buttonRect(closeRect.InsetByCopy(-4, -4)); 318 be_control_look->DrawButtonFrame(this, buttonRect, updateRect, 319 base, base, 320 BControlLook::B_ACTIVATED | BControlLook::B_BLEND_FRAME); 321 be_control_look->DrawButtonBackground(this, buttonRect, updateRect, 322 base, BControlLook::B_ACTIVATED); 323 tint *= 1.2; 324 closeRect.OffsetBy(1, 1); 325 } 326 327 base = tint_color(base, tint); 328 SetHighColor(base); 329 SetPenSize(2); 330 StrokeLine(closeRect.LeftTop(), closeRect.RightBottom()); 331 StrokeLine(closeRect.LeftBottom(), closeRect.RightTop()); 332 PopState(); 333 } 334 335 336 void 337 NotificationView::MouseDown(BPoint point) 338 { 339 int32 buttons; 340 Window()->CurrentMessage()->FindInt32("buttons", &buttons); 341 342 switch (buttons) { 343 case B_PRIMARY_MOUSE_BUTTON: 344 { 345 BRect closeRect = Bounds().InsetByCopy(2,2); 346 closeRect.left = closeRect.right - kCloseSize; 347 closeRect.bottom = closeRect.top + kCloseSize; 348 349 if (!closeRect.Contains(point)) { 350 entry_ref launchRef; 351 BString launchString; 352 BMessage argMsg(B_ARGV_RECEIVED); 353 BMessage refMsg(B_REFS_RECEIVED); 354 entry_ref appRef; 355 bool useArgv = false; 356 BList messages; 357 entry_ref ref; 358 359 if (fNotification->OnClickApp() != NULL 360 && be_roster->FindApp(fNotification->OnClickApp(), &appRef) 361 == B_OK) { 362 useArgv = true; 363 } 364 365 if (fNotification->OnClickFile() != NULL 366 && be_roster->FindApp( 367 (entry_ref*)fNotification->OnClickFile(), &appRef) 368 == B_OK) { 369 useArgv = true; 370 } 371 372 for (int32 i = 0; i < fNotification->CountOnClickRefs(); i++) 373 refMsg.AddRef("refs", fNotification->OnClickRefAt(i)); 374 messages.AddItem((void*)&refMsg); 375 376 if (useArgv) { 377 int32 argc = fNotification->CountOnClickArgs() + 1; 378 BString arg; 379 380 BPath p(&appRef); 381 argMsg.AddString("argv", p.Path()); 382 383 argMsg.AddInt32("argc", argc); 384 385 for (int32 i = 0; i < argc - 1; i++) { 386 argMsg.AddString("argv", 387 fNotification->OnClickArgAt(i)); 388 } 389 390 messages.AddItem((void*)&argMsg); 391 } 392 393 if (fNotification->OnClickApp() != NULL) 394 be_roster->Launch(fNotification->OnClickApp(), &messages); 395 else 396 be_roster->Launch(fNotification->OnClickFile(), &messages); 397 } else { 398 fCloseClicked = true; 399 } 400 401 // Remove the info view after a click 402 BMessage remove_msg(kRemoveView); 403 remove_msg.AddPointer("view", this); 404 405 BMessenger msgr(Parent()); 406 msgr.SendMessage(&remove_msg); 407 break; 408 } 409 } 410 } 411 412 413 BHandler* 414 NotificationView::ResolveSpecifier(BMessage* msg, int32 index, BMessage* spec, 415 int32 form, const char* prop) 416 { 417 BPropertyInfo prop_info(message_prop_list); 418 if (prop_info.FindMatch(msg, index, spec, form, prop) >= 0) { 419 msg->PopSpecifier(); 420 return this; 421 } 422 423 return BView::ResolveSpecifier(msg, index, spec, form, prop); 424 } 425 426 427 status_t 428 NotificationView::GetSupportedSuites(BMessage* msg) 429 { 430 msg->AddString("suites", "suite/x-vnd.Haiku-notification_server"); 431 BPropertyInfo prop_info(message_prop_list); 432 msg->AddFlat("messages", &prop_info); 433 return BView::GetSupportedSuites(msg); 434 } 435 436 437 void 438 NotificationView::SetText(float newMaxWidth) 439 { 440 if (newMaxWidth < 0 && Parent()) 441 newMaxWidth = Parent()->Bounds().IntegerWidth(); 442 if (newMaxWidth <= 0) 443 newMaxWidth = kDefaultWidth; 444 445 // Delete old lines 446 LineInfoList::iterator lIt; 447 for (lIt = fLines.begin(); lIt != fLines.end(); lIt++) 448 delete (*lIt); 449 fLines.clear(); 450 451 float iconRight = kIconStripeWidth; 452 if (fBitmap != NULL) 453 iconRight += fParent->IconSize(); 454 else 455 iconRight += 32; 456 457 font_height fh; 458 be_bold_font->GetHeight(&fh); 459 float fontHeight = ceilf(fh.leading) + ceilf(fh.descent) 460 + ceilf(fh.ascent); 461 float y = 2 * fontHeight; 462 463 // Title 464 LineInfo* titleLine = new LineInfo; 465 titleLine->text = fNotification->Title(); 466 titleLine->font = *be_bold_font; 467 468 titleLine->location = BPoint(iconRight + kEdgePadding, y); 469 470 fLines.push_front(titleLine); 471 y += fontHeight; 472 473 // Rest of text is rendered with be_plain_font. 474 be_plain_font->GetHeight(&fh); 475 fontHeight = ceilf(fh.leading) + ceilf(fh.descent) 476 + ceilf(fh.ascent); 477 478 // Split text into chunks between certain characters and compose the lines. 479 const char kSeparatorCharacters[] = " \n-\\"; 480 BString textBuffer = fNotification->Content(); 481 textBuffer.ReplaceAll("\t", " "); 482 const char* chunkStart = textBuffer.String(); 483 float maxWidth = newMaxWidth - kEdgePadding - iconRight; 484 LineInfo* line = NULL; 485 ssize_t length = textBuffer.Length(); 486 while (chunkStart - textBuffer.String() < length) { 487 size_t chunkLength = strcspn(chunkStart, kSeparatorCharacters) + 1; 488 489 // Start a new line if we didn't start one before 490 BString tempText; 491 if (line != NULL) 492 tempText.SetTo(line->text); 493 tempText.Append(chunkStart, chunkLength); 494 495 if (line == NULL || chunkStart[0] == '\n' 496 || StringWidth(tempText) > maxWidth) { 497 line = new LineInfo; 498 line->font = *be_plain_font; 499 line->location = BPoint(iconRight + kEdgePadding, y); 500 501 fLines.push_front(line); 502 y += fontHeight; 503 504 // Skip the eventual new-line character at the beginning of this chunk 505 if (chunkStart[0] == '\n') { 506 chunkStart++; 507 chunkLength--; 508 } 509 510 // Skip more new-line characters and move the line further down 511 while (chunkStart[0] == '\n') { 512 chunkStart++; 513 chunkLength--; 514 line->location.y += fontHeight; 515 y += fontHeight; 516 } 517 518 // Strip space at beginning of a new line 519 while (chunkStart[0] == ' ') { 520 chunkLength--; 521 chunkStart++; 522 } 523 } 524 525 if (chunkStart[0] == '\0') 526 break; 527 528 // Append the chunk to the current line, which was either a new 529 // line or the one from the previous iteration 530 line->text.Append(chunkStart, chunkLength); 531 532 chunkStart += chunkLength; 533 } 534 535 fHeight = y + (kEdgePadding * 2); 536 537 // Make sure icon fits 538 if (fBitmap != NULL) { 539 float minHeight = fBitmap->Bounds().Height() + 2 * kEdgePadding; 540 541 if (fHeight < minHeight) 542 fHeight = minHeight; 543 } 544 545 // Make sure the progress bar is below the text, and the window is big 546 // enough. 547 static_cast<BGroupLayout*>(GetLayout())->SetInsets(kIconStripeWidth + 8, 548 fHeight, 8, 8); 549 550 _CalculateSize(); 551 } 552 553 554 const char* 555 NotificationView::MessageID() const 556 { 557 return fNotification->MessageID(); 558 } 559 560 561 void 562 NotificationView::_CalculateSize() 563 { 564 float height = fHeight; 565 566 if (fNotification->Type() == B_PROGRESS_NOTIFICATION) { 567 font_height fh; 568 be_plain_font->GetHeight(&fh); 569 float fontHeight = fh.ascent + fh.descent + fh.leading; 570 height += 9 + (kSmallPadding * 2) + (kEdgePadding * 1) 571 + fontHeight * 2; 572 } 573 574 SetExplicitMinSize(BSize(0, height)); 575 SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, height)); 576 } 577