1 /* 2 * Copyright 2010, 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 */ 14 15 #include <stdlib.h> 16 17 #include <Font.h> 18 #include <IconUtils.h> 19 #include <Messenger.h> 20 #include <Picture.h> 21 #include <PropertyInfo.h> 22 #include <Region.h> 23 #include <Resources.h> 24 #include <Roster.h> 25 #include <StringView.h> 26 #include <TranslationUtils.h> 27 28 #include "NotificationView.h" 29 #include "NotificationWindow.h" 30 31 const char* kSmallIconAttribute = "BEOS:M:STD_ICON"; 32 const char* kLargeIconAttribute = "BEOS:L:STD_ICON"; 33 const char* kIconAttribute = "BEOS:ICON"; 34 35 property_info message_prop_list[] = { 36 { "type", {B_GET_PROPERTY, B_SET_PROPERTY, 0}, 37 {B_DIRECT_SPECIFIER, 0}, "get the notification type"}, 38 { "app", {B_GET_PROPERTY, B_SET_PROPERTY, 0}, 39 {B_DIRECT_SPECIFIER, 0}, "get notification's app"}, 40 { "title", {B_GET_PROPERTY, B_SET_PROPERTY, 0}, 41 {B_DIRECT_SPECIFIER, 0}, "get notification's title"}, 42 { "content", {B_GET_PROPERTY, B_SET_PROPERTY, 0}, 43 {B_DIRECT_SPECIFIER, 0}, "get notification's contents"}, 44 { "icon", {B_GET_PROPERTY, 0}, 45 {B_DIRECT_SPECIFIER, 0}, "get icon as an archived bitmap"}, 46 { "progress", {B_GET_PROPERTY, B_SET_PROPERTY, 0}, 47 {B_DIRECT_SPECIFIER, 0}, "get the progress (between 0.0 and 1.0)"}, 48 { NULL } 49 }; 50 51 52 NotificationView::NotificationView(NotificationWindow* win, 53 notification_type type, const char* app, const char* title, const char* text, 54 BMessage* details) 55 : 56 BView(BRect(0, 0, win->ViewWidth(), 1), "NotificationView", 57 B_FOLLOW_LEFT_RIGHT, B_WILL_DRAW | B_FULL_UPDATE_ON_RESIZE 58 | B_FRAME_EVENTS), 59 fParent(win), 60 fType(type), 61 fRunner(NULL), 62 fProgress(0.0f), 63 fMessageID(""), 64 fDetails(details), 65 fBitmap(NULL), 66 fIsFirst(false), 67 fIsLast(false) 68 { 69 BMessage iconMsg; 70 if (fDetails->FindMessage("icon", &iconMsg) == B_OK) 71 fBitmap = new BBitmap(&iconMsg); 72 73 if (!fBitmap) 74 _LoadIcon(); 75 76 const char* messageID = NULL; 77 if (fDetails->FindString("messageID", &messageID) == B_OK) 78 fMessageID = messageID; 79 80 if (fDetails->FindFloat("progress", &fProgress) != B_OK) 81 fProgress = 0.0f; 82 83 // Progress is between 0 and 1 84 if (fProgress < 0.0f) 85 fProgress = 0.0f; 86 if (fProgress > 1.0f) 87 fProgress = 1.0f; 88 89 SetText(app, title, text); 90 ResizeToPreferred(); 91 92 switch (type) { 93 case B_IMPORTANT_NOTIFICATION: 94 SetViewColor(255, 255, 255); 95 SetLowColor(255, 255, 255); 96 break; 97 case B_ERROR_NOTIFICATION: 98 SetViewColor(ui_color(B_FAILURE_COLOR)); 99 SetLowColor(ui_color(B_FAILURE_COLOR)); 100 break; 101 default: 102 SetViewColor(ui_color(B_PANEL_BACKGROUND_COLOR)); 103 SetLowColor(ui_color(B_PANEL_BACKGROUND_COLOR)); 104 break; 105 } 106 } 107 108 109 NotificationView::~NotificationView() 110 { 111 delete fRunner; 112 delete fDetails; 113 delete fBitmap; 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 BMessage msg(kRemoveView); 125 msg.AddPointer("view", this); 126 bigtime_t timeout = -1; 127 128 if (fDetails->FindInt64("timeout", &timeout) != B_OK) 129 timeout = fParent->Timeout() * 1000000; 130 131 if (timeout > 0) 132 fRunner = new BMessageRunner(BMessenger(Parent()), &msg, timeout, 1); 133 } 134 135 136 void 137 NotificationView::MessageReceived(BMessage* msg) 138 { 139 switch (msg->what) { 140 case B_GET_PROPERTY: 141 { 142 BMessage specifier; 143 const char* property; 144 BMessage reply(B_REPLY); 145 bool msgOkay = true; 146 147 if (msg->FindMessage("specifiers", 0, &specifier) != B_OK) 148 msgOkay = false; 149 if (specifier.FindString("property", &property) != B_OK) 150 msgOkay = false; 151 152 if (msgOkay) { 153 if (strcmp(property, "type") == 0) 154 reply.AddInt32("result", fType); 155 156 if (strcmp(property, "app") == 0) 157 reply.AddString("result", fApp); 158 159 if (strcmp(property, "title") == 0) 160 reply.AddString("result", fTitle); 161 162 if (strcmp(property, "content") == 0) 163 reply.AddString("result", fText); 164 165 if (strcmp(property, "progress") == 0) 166 reply.AddFloat("result", fProgress); 167 168 if ((strcmp(property, "icon") == 0) && fBitmap) { 169 BMessage archive; 170 if (fBitmap->Archive(&archive) == B_OK) 171 reply.AddMessage("result", &archive); 172 } 173 174 reply.AddInt32("error", B_OK); 175 } else { 176 reply.what = B_MESSAGE_NOT_UNDERSTOOD; 177 reply.AddInt32("error", B_ERROR); 178 } 179 180 msg->SendReply(&reply); 181 break; 182 } 183 case B_SET_PROPERTY: 184 { 185 BMessage specifier; 186 const char* property; 187 BMessage reply(B_REPLY); 188 bool msgOkay = true; 189 190 if (msg->FindMessage("specifiers", 0, &specifier) != B_OK) 191 msgOkay = false; 192 if (specifier.FindString("property", &property) != B_OK) 193 msgOkay = false; 194 195 if (msgOkay) { 196 if (strcmp(property, "app") == 0) 197 msg->FindString("data", &fApp); 198 199 if (strcmp(property, "title") == 0) 200 msg->FindString("data", &fTitle); 201 202 if (strcmp(property, "content") == 0) 203 msg->FindString("data", &fText); 204 205 if (strcmp(property, "icon") == 0) { 206 BMessage archive; 207 if (msg->FindMessage("data", &archive) == B_OK) { 208 delete fBitmap; 209 fBitmap = new BBitmap(&archive); 210 } 211 } 212 213 SetText(Application(), Title(), Text()); 214 Invalidate(); 215 216 reply.AddInt32("error", B_OK); 217 } else { 218 reply.what = B_MESSAGE_NOT_UNDERSTOOD; 219 reply.AddInt32("error", B_ERROR); 220 } 221 222 msg->SendReply(&reply); 223 break; 224 } 225 case kRemoveView: 226 { 227 BMessage remove(kRemoveView); 228 remove.AddPointer("view", this); 229 BMessenger msgr(Window()); 230 msgr.SendMessage( &remove ); 231 break; 232 } 233 default: 234 BView::MessageReceived(msg); 235 } 236 } 237 238 239 void 240 NotificationView::GetPreferredSize(float* w, float* h) 241 { 242 // Parent width, minus the edge padding, minus the pensize 243 *w = fParent->ViewWidth() - (kEdgePadding * 2) - (kPenSize * 2); 244 *h = fHeight; 245 246 if (fType == B_PROGRESS_NOTIFICATION) { 247 font_height fh; 248 be_plain_font->GetHeight(&fh); 249 float fontHeight = fh.ascent + fh.descent + fh.leading; 250 *h += (kSmallPadding * 2) + (kEdgePadding * 1) + fontHeight; 251 } 252 } 253 254 255 void 256 NotificationView::Draw(BRect updateRect) 257 { 258 BRect progRect; 259 260 // Draw progress background 261 if (fType == B_PROGRESS_NOTIFICATION) { 262 PushState(); 263 264 font_height fh; 265 be_plain_font->GetHeight(&fh); 266 float fontHeight = fh.ascent + fh.descent + fh.leading; 267 268 progRect = Bounds(); 269 progRect.InsetBy(kEdgePadding, kEdgePadding); 270 progRect.top = progRect.bottom - (kSmallPadding * 2) - fontHeight; 271 StrokeRect(progRect); 272 273 BRect barRect = progRect; 274 barRect.InsetBy(1.0, 1.0); 275 barRect.right *= fProgress; 276 SetHighColor(ui_color(B_CONTROL_HIGHLIGHT_COLOR)); 277 FillRect(barRect); 278 279 SetHighColor(ui_color(B_PANEL_TEXT_COLOR)); 280 281 BString label = ""; 282 label << (int)(fProgress * 100) << " %"; 283 284 float labelWidth = be_plain_font->StringWidth(label.String()); 285 float labelX = progRect.left + (progRect.IntegerWidth() / 2) - (labelWidth / 2); 286 287 SetLowColor(B_TRANSPARENT_COLOR); 288 SetDrawingMode(B_OP_ALPHA); 289 DrawString(label.String(), label.Length(), 290 BPoint(labelX, progRect.top + fh.ascent + fh.leading + kSmallPadding)); 291 292 PopState(); 293 } 294 295 SetDrawingMode(B_OP_ALPHA); 296 SetBlendingMode(B_PIXEL_ALPHA, B_ALPHA_OVERLAY); 297 298 // Icon size 299 float iconSize = (float)fParent->IconSize(); 300 301 // Rectangle for icon and overlay icon 302 BRect iconRect(0, 0, 0, 0); 303 304 // Draw icon 305 if (fBitmap) { 306 LineInfo* appLine = fLines.back(); 307 font_height fh; 308 appLine->font.GetHeight(&fh); 309 310 float title_bottom = appLine->location.y + fh.descent; 311 312 float ix = kEdgePadding; 313 float iy = 0; 314 if (fParent->Layout() == TitleAboveIcon) 315 iy = title_bottom + kEdgePadding + (Bounds().Height() - title_bottom 316 - kEdgePadding * 2 - iconSize) / 2; 317 else 318 iy = (Bounds().Height() - iconSize) / 2.0; 319 320 if (fType == B_PROGRESS_NOTIFICATION) 321 // Move icon up by half progress bar height if it's present 322 iy -= (progRect.Height() + kEdgePadding) / 2.0; 323 324 iconRect.left = ix; 325 iconRect.top = iy; 326 iconRect.right = ix + iconSize; 327 iconRect.bottom = iy + iconSize; 328 329 DrawBitmapAsync(fBitmap, fBitmap->Bounds(), 330 iconRect, B_FILTER_BITMAP_BILINEAR); 331 } 332 333 // Draw content 334 LineInfoList::iterator lIt; 335 for (lIt = fLines.begin(); lIt != fLines.end(); lIt++) { 336 LineInfo *l = (*lIt); 337 338 SetFont(&l->font); 339 DrawString(l->text.String(), l->text.Length(), l->location); 340 } 341 342 rgb_color detailCol = ui_color(B_CONTROL_BORDER_COLOR); 343 detailCol = tint_color(detailCol, B_LIGHTEN_2_TINT); 344 345 // Draw the close widget 346 BRect closeRect = Bounds(); 347 closeRect.InsetBy(kEdgePadding, kEdgePadding); 348 closeRect.left = closeRect.right - kCloseSize; 349 closeRect.bottom = closeRect.top + kCloseSize; 350 351 PushState(); 352 SetHighColor(detailCol); 353 StrokeRoundRect(closeRect, kSmallPadding, kSmallPadding); 354 BRect closeCross = closeRect.InsetByCopy(kSmallPadding, kSmallPadding); 355 StrokeLine(closeCross.LeftTop(), closeCross.RightBottom()); 356 StrokeLine(closeCross.LeftBottom(), closeCross.RightTop()); 357 PopState(); 358 359 Sync(); 360 } 361 362 363 void 364 NotificationView::MouseDown(BPoint point) 365 { 366 int32 buttons; 367 Window()->CurrentMessage()->FindInt32("buttons", &buttons); 368 369 switch (buttons) { 370 case B_PRIMARY_MOUSE_BUTTON: 371 { 372 BRect closeRect = Bounds().InsetByCopy(2,2); 373 closeRect.left = closeRect.right - kCloseSize; 374 closeRect.bottom = closeRect.top + kCloseSize; 375 376 if (!closeRect.Contains(point)) { 377 entry_ref launchRef; 378 BString launchString; 379 BMessage argMsg(B_ARGV_RECEIVED); 380 BMessage refMsg(B_REFS_RECEIVED); 381 entry_ref appRef; 382 bool useArgv = false; 383 BList messages; 384 entry_ref ref; 385 386 if (fDetails->FindString("onClickApp", &launchString) == B_OK) 387 if (be_roster->FindApp(launchString.String(), &appRef) == B_OK) 388 useArgv = true; 389 if (fDetails->FindRef("onClickFile", &launchRef) == B_OK) { 390 if (be_roster->FindApp(&launchRef, &appRef) == B_OK) 391 useArgv = true; 392 } 393 394 if (fDetails->FindRef("onClickRef", &ref) == B_OK) { 395 for (int32 i = 0; fDetails->FindRef("onClickRef", i, &ref) == B_OK; i++) 396 refMsg.AddRef("refs", &ref); 397 398 messages.AddItem((void*)&refMsg); 399 } 400 401 if (useArgv) { 402 type_code type; 403 int32 argc = 0; 404 BString arg; 405 406 BPath p(&appRef); 407 argMsg.AddString("argv", p.Path()); 408 409 fDetails->GetInfo("onClickArgv", &type, &argc); 410 argMsg.AddInt32("argc", argc + 1); 411 412 for (int32 i = 0; fDetails->FindString("onClickArgv", i, &arg) == B_OK; i++) 413 argMsg.AddString("argv", arg); 414 415 messages.AddItem((void*)&argMsg); 416 } 417 418 BMessage tmp; 419 for (int32 i = 0; fDetails->FindMessage("onClickMsg", i, &tmp) == B_OK; i++) 420 messages.AddItem((void*)&tmp); 421 422 if (fDetails->FindString("onClickApp", &launchString) == B_OK) 423 be_roster->Launch(launchString.String(), &messages); 424 else 425 be_roster->Launch(&launchRef, &messages); 426 } 427 428 // Remove the info view after a click 429 BMessage remove_msg(kRemoveView); 430 remove_msg.AddPointer("view", this); 431 432 BMessenger msgr(Parent()); 433 msgr.SendMessage(&remove_msg); 434 break; 435 } 436 } 437 } 438 439 440 void 441 NotificationView::FrameResized( float w, float /*h*/) 442 { 443 SetText(Application(), Title(), Text()); 444 } 445 446 447 BHandler* 448 NotificationView::ResolveSpecifier(BMessage* msg, int32 index, BMessage* spec, int32 form, const char* prop) 449 { 450 BPropertyInfo prop_info(message_prop_list); 451 if (prop_info.FindMatch(msg, index, spec, form, prop) >= 0) { 452 msg->PopSpecifier(); 453 return this; 454 } 455 456 return BView::ResolveSpecifier(msg, index, spec, form, prop); 457 } 458 459 460 status_t 461 NotificationView::GetSupportedSuites(BMessage* msg) 462 { 463 msg->AddString("suites", "suite/x-vnd.Haiku-notification_server"); 464 BPropertyInfo prop_info(message_prop_list); 465 msg->AddFlat("messages", &prop_info); 466 return BView::GetSupportedSuites(msg); 467 } 468 469 470 const char* 471 NotificationView::Application() const 472 { 473 return fApp.Length() > 0 ? fApp.String() : NULL; 474 } 475 476 477 const char* 478 NotificationView::Title() const 479 { 480 return fTitle.Length() > 0 ? fTitle.String() : NULL; 481 } 482 483 484 const char* 485 NotificationView::Text() const 486 { 487 return fText.Length() > 0 ? fText.String() : NULL; 488 } 489 490 491 void 492 NotificationView::SetText(const char* app, const char* title, const char* text, 493 float newMaxWidth) 494 { 495 if (newMaxWidth < 0) 496 newMaxWidth = Bounds().Width() - (kEdgePadding * 2); 497 498 // Delete old lines 499 LineInfoList::iterator lIt; 500 for (lIt = fLines.begin(); lIt != fLines.end(); lIt++) 501 delete (*lIt); 502 fLines.clear(); 503 504 fApp = app; 505 fTitle = title; 506 fText = text; 507 508 float iconRight = kEdgePadding + kEdgePadding; 509 if (fBitmap != NULL) 510 iconRight += fParent->IconSize(); 511 512 font_height fh; 513 be_bold_font->GetHeight(&fh); 514 float fontHeight = ceilf(fh.leading) + ceilf(fh.descent) 515 + ceilf(fh.ascent); 516 float y = fontHeight; 517 518 // Title 519 LineInfo* titleLine = new LineInfo; 520 titleLine->text = fTitle; 521 titleLine->font = *be_bold_font; 522 523 if (fParent->Layout() == AllTextRightOfIcon) 524 titleLine->location = BPoint(iconRight, y); 525 else 526 titleLine->location = BPoint(kEdgePadding, y); 527 528 fLines.push_front(titleLine); 529 y += fontHeight; 530 531 // Rest of text is rendered with be_plain_font. 532 be_plain_font->GetHeight(&fh); 533 fontHeight = ceilf(fh.leading) + ceilf(fh.descent) 534 + ceilf(fh.ascent); 535 536 // Split text into chunks between certain characters and compose the lines. 537 const char kSeparatorCharacters[] = " \n-\\/"; 538 BString textBuffer = fText; 539 textBuffer.ReplaceAll("\t", " "); 540 const char* chunkStart = textBuffer.String(); 541 float maxWidth = newMaxWidth - kEdgePadding - iconRight; 542 LineInfo* line = NULL; 543 ssize_t length = textBuffer.Length(); 544 while (chunkStart - textBuffer.String() < length) { 545 size_t chunkLength = strcspn(chunkStart, kSeparatorCharacters) + 1; 546 547 // Start a new line if either we didn't start one before, 548 // the current offset 549 BString tempText; 550 if (line != NULL) 551 tempText.SetTo(line->text); 552 tempText.Append(chunkStart, chunkLength); 553 554 if (line == NULL || chunkStart[0] == '\n' 555 || StringWidth(tempText) > maxWidth) { 556 line = new LineInfo; 557 line->font = *be_plain_font; 558 line->location = BPoint(iconRight + kEdgePadding, y); 559 560 fLines.push_front(line); 561 y += fontHeight; 562 563 // Skip the eventual new-line character at the beginning of this 564 // chunk. 565 if (chunkStart[0] == '\n') { 566 chunkStart++; 567 chunkLength--; 568 } 569 // Skip more new-line characters and move the line further down. 570 while (chunkStart[0] == '\n') { 571 chunkStart++; 572 chunkLength--; 573 line->location.y += fontHeight; 574 y += fontHeight; 575 } 576 // Strip space at beginning of a new line. 577 while (chunkStart[0] == ' ') { 578 chunkLength--; 579 chunkStart++; 580 } 581 } 582 583 if (chunkStart[0] == '\0') 584 break; 585 586 // Append the chunk to the current line, which was either a new 587 // line or the one from the previous iteration. 588 line->text.Append(chunkStart, chunkLength); 589 590 chunkStart += chunkLength; 591 } 592 593 fHeight = y + (kEdgePadding * 2); 594 595 // Make sure icon fits 596 if (fBitmap != NULL) { 597 float minHeight = 0; 598 if (fParent->Layout() == TitleAboveIcon) { 599 LineInfo* appLine = fLines.back(); 600 font_height fh; 601 appLine->font.GetHeight(&fh); 602 minHeight = appLine->location.y + fh.descent; 603 } 604 605 minHeight += fBitmap->Bounds().Height() + 2 * kEdgePadding; 606 if (fHeight < minHeight) 607 fHeight = minHeight; 608 } 609 610 BMessenger messenger(Parent()); 611 messenger.SendMessage(kResizeToFit); 612 } 613 614 615 bool 616 NotificationView::HasMessageID(const char* id) 617 { 618 return fMessageID == id; 619 } 620 621 622 const char* 623 NotificationView::MessageID() 624 { 625 return fMessageID.String(); 626 } 627 628 629 void 630 NotificationView::SetPosition(bool first, bool last) 631 { 632 fIsFirst = first; 633 fIsLast = last; 634 } 635 636 637 BBitmap* 638 NotificationView::_ReadNodeIcon(const char* fileName, icon_size size) 639 { 640 BEntry entry(fileName, true); 641 642 entry_ref ref; 643 entry.GetRef(&ref); 644 645 BNode node(BPath(&ref).Path()); 646 647 BBitmap* ret = new BBitmap(BRect(0, 0, (float)size - 1, (float)size - 1), B_RGBA32); 648 if (BIconUtils::GetIcon(&node, kIconAttribute, kSmallIconAttribute, 649 kLargeIconAttribute, size, ret) != B_OK) { 650 delete ret; 651 ret = NULL; 652 } 653 654 return ret; 655 } 656 657 658 void 659 NotificationView::_LoadIcon() 660 { 661 // First try to get the icon from the caller application 662 app_info info; 663 BMessenger msgr = fDetails->ReturnAddress(); 664 665 if (msgr.IsValid()) 666 be_roster->GetRunningAppInfo(msgr.Team(), &info); 667 else if (fType == B_PROGRESS_NOTIFICATION) 668 be_roster->GetAppInfo("application/x-vnd.Haiku-notification_server", 669 &info); 670 671 BPath path; 672 path.SetTo(&info.ref); 673 674 fBitmap = _ReadNodeIcon(path.Path(), fParent->IconSize()); 675 if (fBitmap) 676 return; 677 678 // If that failed get icons from app_server 679 if (find_directory(B_BEOS_SERVERS_DIRECTORY, &path) != B_OK) 680 return; 681 682 path.Append("app_server"); 683 684 BFile file(path.Path(), B_READ_ONLY); 685 if (file.InitCheck() != B_OK) 686 return; 687 688 BResources res(&file); 689 if (res.InitCheck() != B_OK) 690 return; 691 692 // Which one should we choose? 693 const char* iconName = ""; 694 switch (fType) { 695 case B_INFORMATION_NOTIFICATION: 696 iconName = "info"; 697 break; 698 case B_ERROR_NOTIFICATION: 699 iconName = "stop"; 700 break; 701 case B_IMPORTANT_NOTIFICATION: 702 iconName = "warn"; 703 break; 704 default: 705 return; 706 } 707 708 // Allocate the bitmap 709 fBitmap = new BBitmap(BRect(0, 0, (float)B_LARGE_ICON - 1, 710 (float)B_LARGE_ICON - 1), B_RGBA32); 711 if (!fBitmap || fBitmap->InitCheck() != B_OK) { 712 fBitmap = NULL; 713 return; 714 } 715 716 // Load raw icon data 717 size_t size = 0; 718 const uint8* data = (const uint8*)res.LoadResource(B_VECTOR_ICON_TYPE, 719 iconName, &size); 720 if ((data == NULL 721 || BIconUtils::GetVectorIcon(data, size, fBitmap) != B_OK)) 722 fBitmap = NULL; 723 } 724