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
40const int kIconStripeWidth			= 32;
41const float kCloseSize				= 6;
42const float kEdgePadding			= 2;
43const float kSmallPadding			= 2;
44
45property_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
63NotificationView::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
118NotificationView::~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
130void
131NotificationView::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
143void
144NotificationView::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
243void
244NotificationView::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
307void
308NotificationView::_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
340void
341NotificationView::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
421BHandler*
422NotificationView::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
435status_t
436NotificationView::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
445void
446NotificationView::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
562void
563NotificationView::SetPreviewModeOn(bool enabled)
564{
565	fPreviewModeOn = enabled;
566}
567
568
569const char*
570NotificationView::MessageID() const
571{
572	return fNotification->MessageID();
573}
574
575
576void
577NotificationView::_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