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