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