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