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