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