xref: /haiku/src/servers/notification/NotificationView.cpp (revision 0044a8c39ab5721051b6279506d1a8c511e20453)
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 #include <stdlib.h>
17 
18 #include <ControlLook.h>
19 #include <Font.h>
20 #include <IconUtils.h>
21 #include <Messenger.h>
22 #include <Picture.h>
23 #include <PropertyInfo.h>
24 #include <Region.h>
25 #include <Resources.h>
26 #include <Roster.h>
27 #include <StatusBar.h>
28 #include <StringView.h>
29 #include <TranslationUtils.h>
30 
31 #include "NotificationView.h"
32 #include "NotificationWindow.h"
33 
34 const char* kSmallIconAttribute	= "BEOS:M:STD_ICON";
35 const char* kLargeIconAttribute	= "BEOS:L:STD_ICON";
36 const char* kIconAttribute		= "BEOS:ICON";
37 
38 static const int kIconStripeWidth = 32;
39 
40 property_info message_prop_list[] = {
41 	{ "type", {B_GET_PROPERTY, B_SET_PROPERTY, 0},
42 		{B_DIRECT_SPECIFIER, 0}, "get the notification type"},
43 	{ "app", {B_GET_PROPERTY, B_SET_PROPERTY, 0},
44 		{B_DIRECT_SPECIFIER, 0}, "get notification's app"},
45 	{ "title", {B_GET_PROPERTY, B_SET_PROPERTY, 0},
46 		{B_DIRECT_SPECIFIER, 0}, "get notification's title"},
47 	{ "content", {B_GET_PROPERTY, B_SET_PROPERTY, 0},
48 		{B_DIRECT_SPECIFIER, 0}, "get notification's contents"},
49 	{ "icon", {B_GET_PROPERTY, 0},
50 		{B_DIRECT_SPECIFIER, 0}, "get icon as an archived bitmap"},
51 	{ "progress", {B_GET_PROPERTY, B_SET_PROPERTY, 0},
52 		{B_DIRECT_SPECIFIER, 0}, "get the progress (between 0.0 and 1.0)"},
53 	{ NULL }
54 };
55 
56 
57 NotificationView::NotificationView(NotificationWindow* win,
58 	notification_type type, const char* app, const char* title, const char* text,
59 	BMessage* details)
60 	:
61 	BView(BRect(0, 0, win->ViewWidth(), 1), "NotificationView",
62 		B_FOLLOW_LEFT_RIGHT, B_WILL_DRAW | B_FULL_UPDATE_ON_RESIZE
63 		| B_FRAME_EVENTS),
64 	fParent(win),
65 	fType(type),
66 	fRunner(NULL),
67 	fProgress(0.0f),
68 	fMessageID(""),
69 	fDetails(details),
70 	fBitmap(NULL),
71 	fIsFirst(false),
72 	fIsLast(false)
73 {
74 	BMessage iconMsg;
75 	if (fDetails->FindMessage("icon", &iconMsg) == B_OK)
76 		fBitmap = new BBitmap(&iconMsg);
77 
78 	if (!fBitmap)
79 		_LoadIcon();
80 
81 	const char* messageID = NULL;
82 	if (fDetails->FindString("messageID", &messageID) == B_OK)
83 		fMessageID = messageID;
84 
85 	if (fDetails->FindFloat("progress", &fProgress) != B_OK)
86 		fProgress = 0.0f;
87 
88 	// Progress is between 0 and 1
89 	if (fProgress < 0.0f)
90 		fProgress = 0.0f;
91 	if (fProgress > 1.0f)
92 		fProgress = 1.0f;
93 
94 	SetText(app, title, text);
95 	ResizeToPreferred();
96 
97 	switch (type) {
98 		case B_IMPORTANT_NOTIFICATION:
99 			SetViewColor(255, 255, 255);
100 			SetLowColor(255, 255, 255);
101 			break;
102 		case B_ERROR_NOTIFICATION:
103 			SetViewColor(ui_color(B_FAILURE_COLOR));
104 			SetLowColor(ui_color(B_FAILURE_COLOR));
105 			break;
106 		case B_PROGRESS_NOTIFICATION:
107 		{
108 			BRect frame(kIconStripeWidth + 9, Bounds().bottom - 36,
109 				Bounds().right - 8, Bounds().bottom - 10);
110 			BStatusBar* progress = new BStatusBar(frame, "progress");
111 			progress->SetBarHeight(12.0f);
112 			progress->SetMaxValue(1.0f);
113 			progress->Update(fProgress);
114 
115 			BString label = "";
116 			label << (int)(fProgress * 100) << " %";
117 			progress->SetTrailingText(label);
118 
119 			AddChild(progress);
120 		}
121 		default:
122 			SetViewColor(ui_color(B_PANEL_BACKGROUND_COLOR));
123 			SetLowColor(ui_color(B_PANEL_BACKGROUND_COLOR));
124 			break;
125 	}
126 }
127 
128 
129 NotificationView::~NotificationView()
130 {
131 	delete fRunner;
132 	delete fDetails;
133 	delete fBitmap;
134 
135 	LineInfoList::iterator lIt;
136 	for (lIt = fLines.begin(); lIt != fLines.end(); lIt++)
137 		delete (*lIt);
138 }
139 
140 
141 void
142 NotificationView::AttachedToWindow()
143 {
144 	BMessage msg(kRemoveView);
145 	msg.AddPointer("view", this);
146 	bigtime_t timeout = -1;
147 
148 	if (fDetails->FindInt64("timeout", &timeout) != B_OK)
149 		timeout = fParent->Timeout() * 1000000;
150 
151 	if (timeout > 0)
152 		fRunner = new BMessageRunner(BMessenger(Parent()), &msg, timeout, 1);
153 }
154 
155 
156 void
157 NotificationView::MessageReceived(BMessage* msg)
158 {
159 	switch (msg->what) {
160 		case B_GET_PROPERTY:
161 		{
162 			BMessage specifier;
163 			const char* property;
164 			BMessage reply(B_REPLY);
165 			bool msgOkay = true;
166 
167 			if (msg->FindMessage("specifiers", 0, &specifier) != B_OK)
168 				msgOkay = false;
169 			if (specifier.FindString("property", &property) != B_OK)
170 				msgOkay = false;
171 
172 			if (msgOkay) {
173 				if (strcmp(property, "type") == 0)
174 					reply.AddInt32("result", fType);
175 
176 				if (strcmp(property, "app") == 0)
177 					reply.AddString("result", fApp);
178 
179 				if (strcmp(property, "title") == 0)
180 					reply.AddString("result", fTitle);
181 
182 				if (strcmp(property, "content") == 0)
183 					reply.AddString("result", fText);
184 
185 				if (strcmp(property, "progress") == 0)
186 					reply.AddFloat("result", fProgress);
187 
188 				if ((strcmp(property, "icon") == 0) && fBitmap) {
189 					BMessage archive;
190 					if (fBitmap->Archive(&archive) == B_OK)
191 						reply.AddMessage("result", &archive);
192 				}
193 
194 				reply.AddInt32("error", B_OK);
195 			} else {
196 				reply.what = B_MESSAGE_NOT_UNDERSTOOD;
197 				reply.AddInt32("error", B_ERROR);
198 			}
199 
200 			msg->SendReply(&reply);
201 			break;
202 		}
203 		case B_SET_PROPERTY:
204 		{
205 			BMessage specifier;
206 			const char* property;
207 			BMessage reply(B_REPLY);
208 			bool msgOkay = true;
209 
210 			if (msg->FindMessage("specifiers", 0, &specifier) != B_OK)
211 				msgOkay = false;
212 			if (specifier.FindString("property", &property) != B_OK)
213 				msgOkay = false;
214 
215 			if (msgOkay) {
216 				if (strcmp(property, "app") == 0)
217 					msg->FindString("data", &fApp);
218 
219 				if (strcmp(property, "title") == 0)
220 					msg->FindString("data", &fTitle);
221 
222 				if (strcmp(property, "content") == 0)
223 					msg->FindString("data", &fText);
224 
225 				if (strcmp(property, "icon") == 0) {
226 					BMessage archive;
227 					if (msg->FindMessage("data", &archive) == B_OK) {
228 						delete fBitmap;
229 						fBitmap = new BBitmap(&archive);
230 					}
231 				}
232 
233 				SetText(Application(), Title(), Text());
234 				Invalidate();
235 
236 				reply.AddInt32("error", B_OK);
237 			} else {
238 				reply.what = B_MESSAGE_NOT_UNDERSTOOD;
239 				reply.AddInt32("error", B_ERROR);
240 			}
241 
242 			msg->SendReply(&reply);
243 			break;
244 		}
245 		case kRemoveView:
246 		{
247 			BMessage remove(kRemoveView);
248 			remove.AddPointer("view", this);
249 			BMessenger msgr(Window());
250 			msgr.SendMessage( &remove );
251 			break;
252 		}
253 		default:
254 			BView::MessageReceived(msg);
255 	}
256 }
257 
258 
259 void
260 NotificationView::GetPreferredSize(float* w, float* h)
261 {
262 	*w = fParent->ViewWidth();
263 	*h = fHeight;
264 
265 	if (fType == B_PROGRESS_NOTIFICATION) {
266 		*h += 16 + kEdgePadding;
267 			// 16 is progress bar default size as stated in the BeBook
268 	}
269 }
270 
271 
272 void
273 NotificationView::Draw(BRect updateRect)
274 {
275 	BRect progRect;
276 
277 	SetDrawingMode(B_OP_ALPHA);
278 	SetBlendingMode(B_PIXEL_ALPHA, B_ALPHA_OVERLAY);
279 
280 	// Icon size
281 	float iconSize = (float)fParent->IconSize();
282 
283 	BRect stripeRect = Bounds();
284 	stripeRect.right = kIconStripeWidth;
285 	SetHighColor(tint_color(ViewColor(), B_DARKEN_1_TINT));
286 	FillRect(stripeRect);
287 
288 	SetHighColor(ui_color(B_PANEL_TEXT_COLOR));
289 	// Rectangle for icon and overlay icon
290 	BRect iconRect(0, 0, 0, 0);
291 
292 	// Draw icon
293 	if (fBitmap) {
294 		float ix = 18;
295 		float iy = (Bounds().Height() - iconSize) / 4.0;
296 			// Icon is vertically centered in view
297 
298 		if (fType == B_PROGRESS_NOTIFICATION)
299 		{
300 			// Move icon up by half progress bar height if it's present
301 			iy -= (progRect.Height() + kEdgePadding);
302 		}
303 
304 		iconRect.Set(ix, iy, ix + iconSize - 1.0, iy + iconSize - 1.0);
305 		DrawBitmapAsync(fBitmap, fBitmap->Bounds(), iconRect);
306 	}
307 
308 	// Draw content
309 	LineInfoList::iterator lIt;
310 	for (lIt = fLines.begin(); lIt != fLines.end(); lIt++) {
311 		LineInfo *l = (*lIt);
312 
313 		SetFont(&l->font);
314 		DrawString(l->text.String(), l->text.Length(), l->location);
315 	}
316 
317 	rgb_color detailCol = ui_color(B_CONTROL_BORDER_COLOR);
318 	detailCol = tint_color(detailCol, B_LIGHTEN_2_TINT);
319 
320 	// Draw the close widget
321 	BRect closeRect = Bounds();
322 	closeRect.InsetBy(2 * kEdgePadding, 2 * kEdgePadding);
323 	closeRect.left = closeRect.right - kCloseSize;
324 	closeRect.bottom = closeRect.top + kCloseSize;
325 
326 	PushState();
327 		SetHighColor(detailCol);
328 		StrokeRoundRect(closeRect, kSmallPadding, kSmallPadding);
329 		BRect closeCross = closeRect.InsetByCopy(kSmallPadding, kSmallPadding);
330 		StrokeLine(closeCross.LeftTop(), closeCross.RightBottom());
331 		StrokeLine(closeCross.LeftBottom(), closeCross.RightTop());
332 	PopState();
333 
334 	SetHighColor(tint_color(ViewColor(), B_DARKEN_1_TINT));
335 	BPoint left(Bounds().left, Bounds().bottom - 1);
336 	BPoint right(Bounds().right, Bounds().bottom - 1);
337 	StrokeLine(left, right);
338 
339 	Sync();
340 }
341 
342 
343 void
344 NotificationView::MouseDown(BPoint point)
345 {
346 	int32 buttons;
347 	Window()->CurrentMessage()->FindInt32("buttons", &buttons);
348 
349 	switch (buttons) {
350 		case B_PRIMARY_MOUSE_BUTTON:
351 		{
352 			BRect closeRect = Bounds().InsetByCopy(2,2);
353 			closeRect.left = closeRect.right - kCloseSize;
354 			closeRect.bottom = closeRect.top + kCloseSize;
355 
356 			if (!closeRect.Contains(point)) {
357 				entry_ref launchRef;
358 				BString launchString;
359 				BMessage argMsg(B_ARGV_RECEIVED);
360 				BMessage refMsg(B_REFS_RECEIVED);
361 				entry_ref appRef;
362 				bool useArgv = false;
363 				BList messages;
364 				entry_ref ref;
365 
366 				if (fDetails->FindString("onClickApp", &launchString) == B_OK)
367 					if (be_roster->FindApp(launchString.String(), &appRef) == B_OK)
368 						useArgv = true;
369 				if (fDetails->FindRef("onClickFile", &launchRef) == B_OK) {
370 					if (be_roster->FindApp(&launchRef, &appRef) == B_OK)
371 						useArgv = true;
372 				}
373 
374 				if (fDetails->FindRef("onClickRef", &ref) == B_OK) {
375 					for (int32 i = 0; fDetails->FindRef("onClickRef", i, &ref) == B_OK; i++)
376 						refMsg.AddRef("refs", &ref);
377 
378 					messages.AddItem((void*)&refMsg);
379 				}
380 
381 				if (useArgv) {
382 					type_code type;
383 					int32 argc = 0;
384 					BString arg;
385 
386 					BPath p(&appRef);
387 					argMsg.AddString("argv", p.Path());
388 
389 					fDetails->GetInfo("onClickArgv", &type, &argc);
390 					argMsg.AddInt32("argc", argc + 1);
391 
392 					for (int32 i = 0; fDetails->FindString("onClickArgv", i, &arg) == B_OK; i++)
393 						argMsg.AddString("argv", arg);
394 
395 					messages.AddItem((void*)&argMsg);
396 				}
397 
398 				BMessage tmp;
399 				for (int32 i = 0; fDetails->FindMessage("onClickMsg", i, &tmp) == B_OK; i++)
400 					messages.AddItem((void*)&tmp);
401 
402 				if (fDetails->FindString("onClickApp", &launchString) == B_OK)
403 					be_roster->Launch(launchString.String(), &messages);
404 				else
405 					be_roster->Launch(&launchRef, &messages);
406 			}
407 
408 			// Remove the info view after a click
409 			BMessage remove_msg(kRemoveView);
410 			remove_msg.AddPointer("view", this);
411 
412 			BMessenger msgr(Parent());
413 			msgr.SendMessage(&remove_msg);
414 			break;
415 		}
416 	}
417 }
418 
419 
420 void
421 NotificationView::FrameResized( float w, float /*h*/)
422 {
423 	SetText(Application(), Title(), Text());
424 }
425 
426 
427 BHandler*
428 NotificationView::ResolveSpecifier(BMessage* msg, int32 index, BMessage* spec, int32 form, const char* prop)
429 {
430 	BPropertyInfo prop_info(message_prop_list);
431 	if (prop_info.FindMatch(msg, index, spec, form, prop) >= 0) {
432 		msg->PopSpecifier();
433 		return this;
434 	}
435 
436 	return BView::ResolveSpecifier(msg, index, spec, form, prop);
437 }
438 
439 
440 status_t
441 NotificationView::GetSupportedSuites(BMessage* msg)
442 {
443 	msg->AddString("suites", "suite/x-vnd.Haiku-notification_server");
444 	BPropertyInfo prop_info(message_prop_list);
445 	msg->AddFlat("messages", &prop_info);
446 	return BView::GetSupportedSuites(msg);
447 }
448 
449 
450 const char*
451 NotificationView::Application() const
452 {
453 	return fApp.Length() > 0 ? fApp.String() : NULL;
454 }
455 
456 
457 const char*
458 NotificationView::Title() const
459 {
460 	return fTitle.Length() > 0 ? fTitle.String() : NULL;
461 }
462 
463 
464 const char*
465 NotificationView::Text() const
466 {
467 	return fText.Length() > 0 ? fText.String() : NULL;
468 }
469 
470 
471 void
472 NotificationView::SetText(const char* app, const char* title, const char* text,
473 	float newMaxWidth)
474 {
475 	if (newMaxWidth < 0)
476 		newMaxWidth = Bounds().Width() - (kEdgePadding * 2);
477 
478 	// Delete old lines
479 	LineInfoList::iterator lIt;
480 	for (lIt = fLines.begin(); lIt != fLines.end(); lIt++)
481 		delete (*lIt);
482 	fLines.clear();
483 
484 	fApp = app;
485 	fTitle = title;
486 	fText = text;
487 
488 	float iconRight = kIconStripeWidth;
489 	if (fBitmap != NULL)
490 		iconRight += fParent->IconSize();
491 	else
492 		iconRight += 32;
493 
494 	font_height fh;
495 	be_bold_font->GetHeight(&fh);
496 	float fontHeight = ceilf(fh.leading) + ceilf(fh.descent)
497 		+ ceilf(fh.ascent);
498 	float y = 2 * fontHeight;
499 
500 	// Title
501 	LineInfo* titleLine = new LineInfo;
502 	titleLine->text = fTitle;
503 	titleLine->font = *be_bold_font;
504 
505 	titleLine->location = BPoint(iconRight, y);
506 
507 	fLines.push_front(titleLine);
508 	y += fontHeight;
509 
510 	// Rest of text is rendered with be_plain_font.
511 	be_plain_font->GetHeight(&fh);
512 	fontHeight = ceilf(fh.leading) + ceilf(fh.descent)
513 		+ ceilf(fh.ascent);
514 
515 	// Split text into chunks between certain characters and compose the lines.
516 	const char kSeparatorCharacters[] = " \n-\\/";
517 	BString textBuffer = fText;
518 	textBuffer.ReplaceAll("\t", "    ");
519 	const char* chunkStart = textBuffer.String();
520 	float maxWidth = newMaxWidth - kEdgePadding - iconRight;
521 	LineInfo* line = NULL;
522 	ssize_t length = textBuffer.Length();
523 	while (chunkStart - textBuffer.String() < length) {
524 		size_t chunkLength = strcspn(chunkStart, kSeparatorCharacters) + 1;
525 
526 		// Start a new line if either we didn't start one before,
527 		// the current offset
528 		BString tempText;
529 		if (line != NULL)
530 			tempText.SetTo(line->text);
531 		tempText.Append(chunkStart, chunkLength);
532 
533 		if (line == NULL || chunkStart[0] == '\n'
534 			|| StringWidth(tempText) > maxWidth) {
535 			line = new LineInfo;
536 			line->font = *be_plain_font;
537 			line->location = BPoint(iconRight + kEdgePadding, y);
538 
539 			fLines.push_front(line);
540 			y += fontHeight;
541 
542 			// Skip the eventual new-line character at the beginning of this
543 			// chunk.
544 			if (chunkStart[0] == '\n') {
545 				chunkStart++;
546 				chunkLength--;
547 			}
548 			// Skip more new-line characters and move the line further down.
549 			while (chunkStart[0] == '\n') {
550 				chunkStart++;
551 				chunkLength--;
552 				line->location.y += fontHeight;
553 				y += fontHeight;
554 			}
555 			// Strip space at beginning of a new line.
556 			while (chunkStart[0] == ' ') {
557 				chunkLength--;
558 				chunkStart++;
559 			}
560 		}
561 
562 		if (chunkStart[0] == '\0')
563 			break;
564 
565 		// Append the chunk to the current line, which was either a new
566 		// line or the one from the previous iteration.
567 		line->text.Append(chunkStart, chunkLength);
568 
569 		chunkStart += chunkLength;
570 	}
571 
572 	fHeight = y + (kEdgePadding * 2);
573 
574 	// Make sure icon fits
575 	if (fBitmap != NULL) {
576 		float minHeight = 0;
577 		if (fParent->Layout() == TitleAboveIcon) {
578 			LineInfo* appLine = fLines.back();
579 			font_height fh;
580 			appLine->font.GetHeight(&fh);
581 			minHeight = appLine->location.y + fh.descent;
582 		}
583 
584 		minHeight += fBitmap->Bounds().Height() + 2 * kEdgePadding;
585 		if (fHeight < minHeight)
586 			fHeight = minHeight;
587 	}
588 
589 	BMessenger messenger(Parent());
590 	messenger.SendMessage(kResizeToFit);
591 }
592 
593 
594 bool
595 NotificationView::HasMessageID(const char* id)
596 {
597 	return fMessageID == id;
598 }
599 
600 
601 const char*
602 NotificationView::MessageID()
603 {
604 	return fMessageID.String();
605 }
606 
607 
608 void
609 NotificationView::SetPosition(bool first, bool last)
610 {
611 	fIsFirst = first;
612 	fIsLast = last;
613 }
614 
615 
616 BBitmap*
617 NotificationView::_ReadNodeIcon(const char* fileName, icon_size size)
618 {
619 	BEntry entry(fileName, true);
620 
621 	entry_ref ref;
622 	entry.GetRef(&ref);
623 
624 	BNode node(BPath(&ref).Path());
625 
626 	BBitmap* ret = new BBitmap(BRect(0, 0, (float)size - 1, (float)size - 1), B_RGBA32);
627 	if (BIconUtils::GetIcon(&node, kIconAttribute, kSmallIconAttribute,
628 		kLargeIconAttribute, size, ret) != B_OK) {
629 		delete ret;
630 		ret = NULL;
631 	}
632 
633 	return ret;
634 }
635 
636 
637 void
638 NotificationView::_LoadIcon()
639 {
640 	// First try to get the icon from the caller application
641 	app_info info;
642 	BMessenger msgr = fDetails->ReturnAddress();
643 
644 	if (msgr.IsValid())
645 		be_roster->GetRunningAppInfo(msgr.Team(), &info);
646 	else if (fType == B_PROGRESS_NOTIFICATION)
647 		be_roster->GetAppInfo("application/x-vnd.Haiku-notification_server",
648 			&info);
649 
650 	BPath path;
651 	path.SetTo(&info.ref);
652 
653 	fBitmap = _ReadNodeIcon(path.Path(), fParent->IconSize());
654 	if (fBitmap)
655 		return;
656 
657 	// If that failed get icons from app_server
658 	if (find_directory(B_BEOS_SERVERS_DIRECTORY, &path) != B_OK)
659 		return;
660 
661 	path.Append("app_server");
662 
663 	BFile file(path.Path(), B_READ_ONLY);
664 	if (file.InitCheck() != B_OK)
665 		return;
666 
667 	BResources res(&file);
668 	if (res.InitCheck() != B_OK)
669 		return;
670 
671 	// Which one should we choose?
672 	const char* iconName = "";
673 	switch (fType) {
674 		case B_INFORMATION_NOTIFICATION:
675 			iconName = "info";
676 			break;
677 		case B_ERROR_NOTIFICATION:
678 			iconName = "stop";
679 			break;
680 		case B_IMPORTANT_NOTIFICATION:
681 			iconName = "warn";
682 			break;
683 		default:
684 			return;
685 	}
686 
687 	// Allocate the bitmap
688 	fBitmap = new BBitmap(BRect(0, 0, (float)B_LARGE_ICON - 1,
689 		(float)B_LARGE_ICON - 1), B_RGBA32);
690 	if (!fBitmap || fBitmap->InitCheck() != B_OK) {
691 		fBitmap = NULL;
692 		return;
693 	}
694 
695 	// Load raw icon data
696 	size_t size = 0;
697 	const uint8* data = (const uint8*)res.LoadResource(B_VECTOR_ICON_TYPE,
698 		iconName, &size);
699 	if ((data == NULL
700 		|| BIconUtils::GetVectorIcon(data, size, fBitmap) != B_OK))
701 		fBitmap = NULL;
702 }
703