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