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