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