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