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.
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
18 #include "NotificationView.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>
29 #include <PropertyInfo.h>
31 #include <StatusBar.h>
33 #include <Notifications.h>
35 #include "AppGroupView.h"
36 #include "NotificationWindow.h"
39 const int kIconStripeWidth
= 32;
40 const float kCloseSize
= 6;
41 const float kEdgePadding
= 2;
42 const float kSmallPadding
= 2;
44 property_info message_prop_list
[] = {
45 { "type", {B_GET_PROPERTY
, B_SET_PROPERTY
, 0},
46 {B_DIRECT_SPECIFIER
, 0}, "get the notification type"},
47 { "app", {B_GET_PROPERTY
, B_SET_PROPERTY
, 0},
48 {B_DIRECT_SPECIFIER
, 0}, "get notification's app"},
49 { "title", {B_GET_PROPERTY
, B_SET_PROPERTY
, 0},
50 {B_DIRECT_SPECIFIER
, 0}, "get notification's title"},
51 { "content", {B_GET_PROPERTY
, B_SET_PROPERTY
, 0},
52 {B_DIRECT_SPECIFIER
, 0}, "get notification's contents"},
53 { "icon", {B_GET_PROPERTY
, 0},
54 {B_DIRECT_SPECIFIER
, 0}, "get icon as an archived bitmap"},
55 { "progress", {B_GET_PROPERTY
, B_SET_PROPERTY
, 0},
56 {B_DIRECT_SPECIFIER
, 0}, "get the progress (between 0.0 and 1.0)"},
62 NotificationView::NotificationView(BNotification
* notification
, bigtime_t timeout
,
63 float iconSize
, bool disableTimeout
)
65 BView("NotificationView", B_WILL_DRAW
),
66 fNotification(notification
),
69 fDisableTimeout(disableTimeout
),
75 if (fNotification
->Icon() != NULL
)
76 fBitmap
= new BBitmap(fNotification
->Icon());
78 BGroupLayout
* layout
= new BGroupLayout(B_VERTICAL
);
81 SetViewUIColor(B_PANEL_BACKGROUND_COLOR
);
82 SetLowColor(ui_color(B_PANEL_BACKGROUND_COLOR
));
84 switch (fNotification
->Type()) {
85 case B_IMPORTANT_NOTIFICATION
:
86 fStripeColor
= ui_color(B_CONTROL_HIGHLIGHT_COLOR
);
88 case B_ERROR_NOTIFICATION
:
89 fStripeColor
= ui_color(B_FAILURE_COLOR
);
91 case B_PROGRESS_NOTIFICATION
:
93 BStatusBar
* progress
= new BStatusBar("progress");
94 progress
->SetBarHeight(12.0f
);
95 progress
->SetMaxValue(1.0f
);
96 progress
->Update(fNotification
->Progress());
99 label
<< (int)(fNotification
->Progress() * 100) << " %";
100 progress
->SetTrailingText(label
);
102 layout
->AddView(progress
);
105 case B_INFORMATION_NOTIFICATION
:
106 fStripeColor
= tint_color(ui_color(B_PANEL_BACKGROUND_COLOR
),
113 NotificationView::~NotificationView()
117 delete fNotification
;
119 LineInfoList::iterator lIt
;
120 for (lIt
= fLines
.begin(); lIt
!= fLines
.end(); lIt
++)
126 NotificationView::AttachedToWindow()
130 if (!fDisableTimeout
) {
131 BMessage
msg(kRemoveView
);
132 msg
.AddPointer("view", this);
133 fRunner
= new BMessageRunner(BMessenger(Parent()), &msg
, fTimeout
, 1);
139 NotificationView::MessageReceived(BMessage
* msg
)
145 const char* property
;
146 BMessage
reply(B_REPLY
);
149 if (msg
->FindMessage("specifiers", 0, &specifier
) != B_OK
)
151 if (specifier
.FindString("property", &property
) != B_OK
)
155 if (strcmp(property
, "type") == 0)
156 reply
.AddInt32("result", fNotification
->Type());
158 if (strcmp(property
, "group") == 0)
159 reply
.AddString("result", fNotification
->Group());
161 if (strcmp(property
, "title") == 0)
162 reply
.AddString("result", fNotification
->Title());
164 if (strcmp(property
, "content") == 0)
165 reply
.AddString("result", fNotification
->Content());
167 if (strcmp(property
, "progress") == 0)
168 reply
.AddFloat("result", fNotification
->Progress());
170 if ((strcmp(property
, "icon") == 0) && fBitmap
) {
172 if (fBitmap
->Archive(&archive
) == B_OK
)
173 reply
.AddMessage("result", &archive
);
176 reply
.AddInt32("error", B_OK
);
178 reply
.what
= B_MESSAGE_NOT_UNDERSTOOD
;
179 reply
.AddInt32("error", B_ERROR
);
182 msg
->SendReply(&reply
);
188 const char* property
;
189 BMessage
reply(B_REPLY
);
192 if (msg
->FindMessage("specifiers", 0, &specifier
) != B_OK
)
194 if (specifier
.FindString("property", &property
) != B_OK
)
198 const char* value
= NULL
;
200 if (strcmp(property
, "group") == 0)
201 if (msg
->FindString("data", &value
) == B_OK
)
202 fNotification
->SetGroup(value
);
204 if (strcmp(property
, "title") == 0)
205 if (msg
->FindString("data", &value
) == B_OK
)
206 fNotification
->SetTitle(value
);
208 if (strcmp(property
, "content") == 0)
209 if (msg
->FindString("data", &value
) == B_OK
)
210 fNotification
->SetContent(value
);
212 if (strcmp(property
, "icon") == 0) {
214 if (msg
->FindMessage("data", &archive
) == B_OK
) {
216 fBitmap
= new BBitmap(&archive
);
223 reply
.AddInt32("error", B_OK
);
225 reply
.what
= B_MESSAGE_NOT_UNDERSTOOD
;
226 reply
.AddInt32("error", B_ERROR
);
229 msg
->SendReply(&reply
);
233 BView::MessageReceived(msg
);
239 NotificationView::Draw(BRect updateRect
)
243 SetDrawingMode(B_OP_ALPHA
);
244 SetBlendingMode(B_PIXEL_ALPHA
, B_ALPHA_OVERLAY
);
246 BRect stripeRect
= Bounds();
247 stripeRect
.right
= kIconStripeWidth
;
248 SetHighColor(tint_color(ui_color(B_PANEL_BACKGROUND_COLOR
),
250 FillRect(stripeRect
);
252 SetHighColor(fStripeColor
);
253 stripeRect
.right
= 2;
254 FillRect(stripeRect
);
256 SetHighColor(ui_color(B_PANEL_TEXT_COLOR
));
257 // Rectangle for icon and overlay icon
258 BRect
iconRect(0, 0, 0, 0);
263 float iy
= (Bounds().Height() - fIconSize
) / 4.0;
264 // Icon is vertically centered in view
266 if (fNotification
->Type() == B_PROGRESS_NOTIFICATION
)
268 // Move icon up by half progress bar height if it's present
269 iy
-= (progRect
.Height() + kEdgePadding
);
272 iconRect
.Set(ix
, iy
, ix
+ fIconSize
- 1.0, iy
+ fIconSize
- 1.0);
273 DrawBitmapAsync(fBitmap
, fBitmap
->Bounds(), iconRect
);
277 LineInfoList::iterator lIt
;
278 for (lIt
= fLines
.begin(); lIt
!= fLines
.end(); lIt
++) {
279 LineInfo
*l
= (*lIt
);
282 // Truncate the string. We have already line-wrapped the text but if
283 // there is a very long 'word' we can only truncate it.
284 BString
text(l
->text
);
285 TruncateString(&text
, B_TRUNCATE_END
,
286 Bounds().Width() - l
->location
.x
);
287 DrawString(text
.String(), text
.Length(), l
->location
);
290 AppGroupView
* groupView
= dynamic_cast<AppGroupView
*>(Parent());
291 if (groupView
!= NULL
&& groupView
->ChildrenCount() > 1)
292 _DrawCloseButton(updateRect
);
294 SetHighColor(tint_color(ViewColor(), B_DARKEN_1_TINT
));
295 BPoint
left(Bounds().left
, Bounds().top
);
296 BPoint
right(Bounds().right
, Bounds().top
);
297 StrokeLine(left
, right
);
304 NotificationView::_DrawCloseButton(const BRect
& updateRect
)
307 BRect closeRect
= Bounds();
309 closeRect
.InsetBy(3 * kEdgePadding
, 3 * kEdgePadding
);
310 closeRect
.left
= closeRect
.right
- kCloseSize
;
311 closeRect
.bottom
= closeRect
.top
+ kCloseSize
;
313 rgb_color base
= ui_color(B_PANEL_BACKGROUND_COLOR
);
314 float tint
= B_DARKEN_2_TINT
;
317 BRect
buttonRect(closeRect
.InsetByCopy(-4, -4));
318 be_control_look
->DrawButtonFrame(this, buttonRect
, updateRect
,
320 BControlLook::B_ACTIVATED
| BControlLook::B_BLEND_FRAME
);
321 be_control_look
->DrawButtonBackground(this, buttonRect
, updateRect
,
322 base
, BControlLook::B_ACTIVATED
);
324 closeRect
.OffsetBy(1, 1);
327 base
= tint_color(base
, tint
);
330 StrokeLine(closeRect
.LeftTop(), closeRect
.RightBottom());
331 StrokeLine(closeRect
.LeftBottom(), closeRect
.RightTop());
337 NotificationView::MouseDown(BPoint point
)
339 // Preview Mode ignores any mouse clicks
344 Window()->CurrentMessage()->FindInt32("buttons", &buttons
);
347 case B_PRIMARY_MOUSE_BUTTON
:
349 BRect closeRect
= Bounds().InsetByCopy(2,2);
350 closeRect
.left
= closeRect
.right
- kCloseSize
;
351 closeRect
.bottom
= closeRect
.top
+ kCloseSize
;
353 if (!closeRect
.Contains(point
)) {
355 BString launchString
;
356 BMessage
argMsg(B_ARGV_RECEIVED
);
357 BMessage
refMsg(B_REFS_RECEIVED
);
359 bool useArgv
= false;
363 if (fNotification
->OnClickApp() != NULL
364 && be_roster
->FindApp(fNotification
->OnClickApp(), &appRef
)
369 if (fNotification
->OnClickFile() != NULL
370 && be_roster
->FindApp(
371 (entry_ref
*)fNotification
->OnClickFile(), &appRef
)
376 for (int32 i
= 0; i
< fNotification
->CountOnClickRefs(); i
++)
377 refMsg
.AddRef("refs", fNotification
->OnClickRefAt(i
));
378 messages
.AddItem((void*)&refMsg
);
381 int32 argc
= fNotification
->CountOnClickArgs() + 1;
385 argMsg
.AddString("argv", p
.Path());
387 argMsg
.AddInt32("argc", argc
);
389 for (int32 i
= 0; i
< argc
- 1; i
++) {
390 argMsg
.AddString("argv",
391 fNotification
->OnClickArgAt(i
));
394 messages
.AddItem((void*)&argMsg
);
397 if (fNotification
->OnClickApp() != NULL
)
398 be_roster
->Launch(fNotification
->OnClickApp(), &messages
);
400 be_roster
->Launch(fNotification
->OnClickFile(), &messages
);
402 fCloseClicked
= true;
405 // Remove the info view after a click
406 BMessage
remove_msg(kRemoveView
);
407 remove_msg
.AddPointer("view", this);
409 BMessenger
msgr(Parent());
410 msgr
.SendMessage(&remove_msg
);
418 NotificationView::ResolveSpecifier(BMessage
* msg
, int32 index
, BMessage
* spec
,
419 int32 form
, const char* prop
)
421 BPropertyInfo
prop_info(message_prop_list
);
422 if (prop_info
.FindMatch(msg
, index
, spec
, form
, prop
) >= 0) {
427 return BView::ResolveSpecifier(msg
, index
, spec
, form
, prop
);
432 NotificationView::GetSupportedSuites(BMessage
* msg
)
434 msg
->AddString("suites", "suite/x-vnd.Haiku-notification_server");
435 BPropertyInfo
prop_info(message_prop_list
);
436 msg
->AddFlat("messages", &prop_info
);
437 return BView::GetSupportedSuites(msg
);
442 NotificationView::SetText(float newMaxWidth
)
444 if (newMaxWidth
< 0 && Parent())
445 newMaxWidth
= Parent()->Bounds().IntegerWidth();
446 if (newMaxWidth
<= 0)
447 newMaxWidth
= kDefaultWidth
;
450 LineInfoList::iterator lIt
;
451 for (lIt
= fLines
.begin(); lIt
!= fLines
.end(); lIt
++)
455 float iconRight
= kIconStripeWidth
;
457 iconRight
+= fIconSize
;
462 be_bold_font
->GetHeight(&fh
);
463 float fontHeight
= ceilf(fh
.leading
) + ceilf(fh
.descent
)
465 float y
= fontHeight
+ kEdgePadding
* 2;
468 LineInfo
* titleLine
= new LineInfo
;
469 titleLine
->text
= fNotification
->Title();
470 titleLine
->font
= *be_bold_font
;
472 titleLine
->location
= BPoint(iconRight
+ kEdgePadding
, y
);
474 fLines
.push_front(titleLine
);
477 // Rest of text is rendered with be_plain_font.
478 be_plain_font
->GetHeight(&fh
);
479 fontHeight
= ceilf(fh
.leading
) + ceilf(fh
.descent
)
482 // Split text into chunks between certain characters and compose the lines.
483 const char kSeparatorCharacters
[] = " \n-\\";
484 BString textBuffer
= fNotification
->Content();
485 textBuffer
.ReplaceAll("\t", " ");
486 const char* chunkStart
= textBuffer
.String();
487 float maxWidth
= newMaxWidth
- kEdgePadding
- iconRight
;
488 LineInfo
* line
= NULL
;
489 ssize_t length
= textBuffer
.Length();
490 while (chunkStart
- textBuffer
.String() < length
) {
491 size_t chunkLength
= strcspn(chunkStart
, kSeparatorCharacters
) + 1;
493 // Start a new line if we didn't start one before
496 tempText
.SetTo(line
->text
);
497 tempText
.Append(chunkStart
, chunkLength
);
499 if (line
== NULL
|| chunkStart
[0] == '\n'
500 || StringWidth(tempText
) > maxWidth
) {
502 line
->font
= *be_plain_font
;
503 line
->location
= BPoint(iconRight
+ kEdgePadding
, y
);
505 fLines
.push_front(line
);
508 // Skip the eventual new-line character at the beginning of this chunk
509 if (chunkStart
[0] == '\n') {
514 // Skip more new-line characters and move the line further down
515 while (chunkStart
[0] == '\n') {
518 line
->location
.y
+= fontHeight
;
522 // Strip space at beginning of a new line
523 while (chunkStart
[0] == ' ') {
529 if (chunkStart
[0] == '\0')
532 // Append the chunk to the current line, which was either a new
533 // line or the one from the previous iteration
534 line
->text
.Append(chunkStart
, chunkLength
);
536 chunkStart
+= chunkLength
;
539 fHeight
= y
+ (kEdgePadding
* 2);
541 // Make sure icon fits
542 if (fBitmap
!= NULL
) {
543 float minHeight
= fBitmap
->Bounds().Height() + 2 * kEdgePadding
;
545 if (fHeight
< minHeight
)
549 // Make sure the progress bar is below the text, and the window is big
551 static_cast<BGroupLayout
*>(GetLayout())->SetInsets(kIconStripeWidth
+ 8,
559 NotificationView::SetPreviewModeOn(bool enabled
)
561 fPreviewModeOn
= enabled
;
566 NotificationView::MessageID() const
568 return fNotification
->MessageID();
573 NotificationView::_CalculateSize()
575 float height
= fHeight
;
577 if (fNotification
->Type() == B_PROGRESS_NOTIFICATION
) {
579 be_plain_font
->GetHeight(&fh
);
580 float fontHeight
= fh
.ascent
+ fh
.descent
+ fh
.leading
;
581 height
+= 9 + (kSmallPadding
* 2) + (kEdgePadding
* 1)
585 SetExplicitMinSize(BSize(0, height
));
586 SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED
, height
));