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