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