xref: /haiku/src/servers/notification/NotificationView.cpp (revision e81a954787e50e56a7f06f72705b7859b6ab06d1)
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 
39 const int kIconStripeWidth			= 32;
40 const float kCloseSize				= 6;
41 const float kEdgePadding			= 2;
42 const float kSmallPadding			= 2;
43 
44 property_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 
62 NotificationView::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 
113 NotificationView::~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 
125 void
126 NotificationView::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 
138 void
139 NotificationView::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 
238 void
239 NotificationView::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 		{
268 			// Move icon up by half progress bar height if it's present
269 			iy -= (progRect.Height() + kEdgePadding);
270 		}
271 
272 		iconRect.Set(ix, iy, ix + fIconSize - 1.0, iy + fIconSize - 1.0);
273 		DrawBitmapAsync(fBitmap, fBitmap->Bounds(), iconRect);
274 	}
275 
276 	// Draw content
277 	LineInfoList::iterator lIt;
278 	for (lIt = fLines.begin(); lIt != fLines.end(); lIt++) {
279 		LineInfo *l = (*lIt);
280 
281 		SetFont(&l->font);
282 		// Truncate the string. We have already line-wrapped the text but if
283 		// there is a very long 'word' we can only truncate it.
284 		BString text(l->text);
285 		TruncateString(&text, B_TRUNCATE_END,
286 			Bounds().Width() - l->location.x);
287 		DrawString(text.String(), text.Length(), l->location);
288 	}
289 
290 	AppGroupView* groupView = dynamic_cast<AppGroupView*>(Parent());
291 	if (groupView != NULL && groupView->ChildrenCount() > 1)
292 		_DrawCloseButton(updateRect);
293 
294 	SetHighColor(tint_color(ViewColor(), B_DARKEN_1_TINT));
295 	BPoint left(Bounds().left, Bounds().top);
296 	BPoint right(Bounds().right, Bounds().top);
297 	StrokeLine(left, right);
298 
299 	Sync();
300 }
301 
302 
303 void
304 NotificationView::_DrawCloseButton(const BRect& updateRect)
305 {
306 	PushState();
307 	BRect closeRect = Bounds();
308 
309 	closeRect.InsetBy(3 * kEdgePadding, 3 * kEdgePadding);
310 	closeRect.left = closeRect.right - kCloseSize;
311 	closeRect.bottom = closeRect.top + kCloseSize;
312 
313 	rgb_color base = ui_color(B_PANEL_BACKGROUND_COLOR);
314 	float tint = B_DARKEN_2_TINT;
315 
316 	if (fCloseClicked) {
317 		BRect buttonRect(closeRect.InsetByCopy(-4, -4));
318 		be_control_look->DrawButtonFrame(this, buttonRect, updateRect,
319 			base, base,
320 			BControlLook::B_ACTIVATED | BControlLook::B_BLEND_FRAME);
321 		be_control_look->DrawButtonBackground(this, buttonRect, updateRect,
322 			base, BControlLook::B_ACTIVATED);
323 		tint *= 1.2;
324 		closeRect.OffsetBy(1, 1);
325 	}
326 
327 	base = tint_color(base, tint);
328 	SetHighColor(base);
329 	SetPenSize(2);
330 	StrokeLine(closeRect.LeftTop(), closeRect.RightBottom());
331 	StrokeLine(closeRect.LeftBottom(), closeRect.RightTop());
332 	PopState();
333 }
334 
335 
336 void
337 NotificationView::MouseDown(BPoint point)
338 {
339 	// Preview Mode ignores any mouse clicks
340 	if (fPreviewModeOn)
341 		return;
342 
343 	int32 buttons;
344 	Window()->CurrentMessage()->FindInt32("buttons", &buttons);
345 
346 	switch (buttons) {
347 		case B_PRIMARY_MOUSE_BUTTON:
348 		{
349 			BRect closeRect = Bounds().InsetByCopy(2,2);
350 			closeRect.left = closeRect.right - kCloseSize;
351 			closeRect.bottom = closeRect.top + kCloseSize;
352 
353 			if (!closeRect.Contains(point)) {
354 				entry_ref launchRef;
355 				BString launchString;
356 				BMessage argMsg(B_ARGV_RECEIVED);
357 				BMessage refMsg(B_REFS_RECEIVED);
358 				entry_ref appRef;
359 				bool useArgv = false;
360 				BList messages;
361 				entry_ref ref;
362 
363 				if (fNotification->OnClickApp() != NULL
364 					&& be_roster->FindApp(fNotification->OnClickApp(), &appRef)
365 				   		== B_OK) {
366 					useArgv = true;
367 				}
368 
369 				if (fNotification->OnClickFile() != NULL
370 					&& be_roster->FindApp(
371 							(entry_ref*)fNotification->OnClickFile(), &appRef)
372 				   		== B_OK) {
373 					useArgv = true;
374 				}
375 
376 				for (int32 i = 0; i < fNotification->CountOnClickRefs(); i++)
377 					refMsg.AddRef("refs", fNotification->OnClickRefAt(i));
378 				messages.AddItem((void*)&refMsg);
379 
380 				if (useArgv) {
381 					int32 argc = fNotification->CountOnClickArgs() + 1;
382 					BString arg;
383 
384 					BPath p(&appRef);
385 					argMsg.AddString("argv", p.Path());
386 
387 					argMsg.AddInt32("argc", argc);
388 
389 					for (int32 i = 0; i < argc - 1; i++) {
390 						argMsg.AddString("argv",
391 							fNotification->OnClickArgAt(i));
392 					}
393 
394 					messages.AddItem((void*)&argMsg);
395 				}
396 
397 				if (fNotification->OnClickApp() != NULL)
398 					be_roster->Launch(fNotification->OnClickApp(), &messages);
399 				else
400 					be_roster->Launch(fNotification->OnClickFile(), &messages);
401 			} else {
402 				fCloseClicked = true;
403 			}
404 
405 			// Remove the info view after a click
406 			BMessage remove_msg(kRemoveView);
407 			remove_msg.AddPointer("view", this);
408 
409 			BMessenger msgr(Parent());
410 			msgr.SendMessage(&remove_msg);
411 			break;
412 		}
413 	}
414 }
415 
416 
417 BHandler*
418 NotificationView::ResolveSpecifier(BMessage* msg, int32 index, BMessage* spec,
419 	int32 form, const char* prop)
420 {
421 	BPropertyInfo prop_info(message_prop_list);
422 	if (prop_info.FindMatch(msg, index, spec, form, prop) >= 0) {
423 		msg->PopSpecifier();
424 		return this;
425 	}
426 
427 	return BView::ResolveSpecifier(msg, index, spec, form, prop);
428 }
429 
430 
431 status_t
432 NotificationView::GetSupportedSuites(BMessage* msg)
433 {
434 	msg->AddString("suites", "suite/x-vnd.Haiku-notification_server");
435 	BPropertyInfo prop_info(message_prop_list);
436 	msg->AddFlat("messages", &prop_info);
437 	return BView::GetSupportedSuites(msg);
438 }
439 
440 
441 void
442 NotificationView::SetText(float newMaxWidth)
443 {
444 	if (newMaxWidth < 0 && Parent())
445 		newMaxWidth = Parent()->Bounds().IntegerWidth();
446 	if (newMaxWidth <= 0)
447 		newMaxWidth = kDefaultWidth;
448 
449 	// Delete old lines
450 	LineInfoList::iterator lIt;
451 	for (lIt = fLines.begin(); lIt != fLines.end(); lIt++)
452 		delete (*lIt);
453 	fLines.clear();
454 
455 	float iconRight = kIconStripeWidth;
456 	if (fBitmap != NULL)
457 		iconRight += fIconSize;
458 	else
459 		iconRight += 32;
460 
461 	font_height fh;
462 	be_bold_font->GetHeight(&fh);
463 	float fontHeight = ceilf(fh.leading) + ceilf(fh.descent)
464 		+ ceilf(fh.ascent);
465 	float y = fontHeight + kEdgePadding * 2;
466 
467 	// Title
468 	LineInfo* titleLine = new LineInfo;
469 	titleLine->text = fNotification->Title();
470 	titleLine->font = *be_bold_font;
471 
472 	titleLine->location = BPoint(iconRight + kEdgePadding, y);
473 
474 	fLines.push_front(titleLine);
475 	y += fontHeight;
476 
477 	// Rest of text is rendered with be_plain_font.
478 	be_plain_font->GetHeight(&fh);
479 	fontHeight = ceilf(fh.leading) + ceilf(fh.descent)
480 		+ ceilf(fh.ascent);
481 
482 	// Split text into chunks between certain characters and compose the lines.
483 	const char kSeparatorCharacters[] = " \n-\\";
484 	BString textBuffer = fNotification->Content();
485 	textBuffer.ReplaceAll("\t", "    ");
486 	const char* chunkStart = textBuffer.String();
487 	float maxWidth = newMaxWidth - kEdgePadding - iconRight;
488 	LineInfo* line = NULL;
489 	ssize_t length = textBuffer.Length();
490 	while (chunkStart - textBuffer.String() < length) {
491 		size_t chunkLength = strcspn(chunkStart, kSeparatorCharacters) + 1;
492 
493 		// Start a new line if we didn't start one before
494 		BString tempText;
495 		if (line != NULL)
496 			tempText.SetTo(line->text);
497 		tempText.Append(chunkStart, chunkLength);
498 
499 		if (line == NULL || chunkStart[0] == '\n'
500 			|| StringWidth(tempText) > maxWidth) {
501 			line = new LineInfo;
502 			line->font = *be_plain_font;
503 			line->location = BPoint(iconRight + kEdgePadding, y);
504 
505 			fLines.push_front(line);
506 			y += fontHeight;
507 
508 			// Skip the eventual new-line character at the beginning of this chunk
509 			if (chunkStart[0] == '\n') {
510 				chunkStart++;
511 				chunkLength--;
512 			}
513 
514 			// Skip more new-line characters and move the line further down
515 			while (chunkStart[0] == '\n') {
516 				chunkStart++;
517 				chunkLength--;
518 				line->location.y += fontHeight;
519 				y += fontHeight;
520 			}
521 
522 			// Strip space at beginning of a new line
523 			while (chunkStart[0] == ' ') {
524 				chunkLength--;
525 				chunkStart++;
526 			}
527 		}
528 
529 		if (chunkStart[0] == '\0')
530 			break;
531 
532 		// Append the chunk to the current line, which was either a new
533 		// line or the one from the previous iteration
534 		line->text.Append(chunkStart, chunkLength);
535 
536 		chunkStart += chunkLength;
537 	}
538 
539 	fHeight = y + (kEdgePadding * 2);
540 
541 	// Make sure icon fits
542 	if (fBitmap != NULL) {
543 		float minHeight = fBitmap->Bounds().Height() + 2 * kEdgePadding;
544 
545 		if (fHeight < minHeight)
546 			fHeight = minHeight;
547 	}
548 
549 	// Make sure the progress bar is below the text, and the window is big
550 	// enough.
551 	static_cast<BGroupLayout*>(GetLayout())->SetInsets(kIconStripeWidth + 8,
552 		fHeight, 8, 8);
553 
554 	_CalculateSize();
555 }
556 
557 
558 void
559 NotificationView::SetPreviewModeOn(bool enabled)
560 {
561 	fPreviewModeOn = enabled;
562 }
563 
564 
565 const char*
566 NotificationView::MessageID() const
567 {
568 	return fNotification->MessageID();
569 }
570 
571 
572 void
573 NotificationView::_CalculateSize()
574 {
575 	float height = fHeight;
576 
577 	if (fNotification->Type() == B_PROGRESS_NOTIFICATION) {
578 		font_height fh;
579 		be_plain_font->GetHeight(&fh);
580 		float fontHeight = fh.ascent + fh.descent + fh.leading;
581 		height += 9 + (kSmallPadding * 2) + (kEdgePadding * 1)
582 			+ fontHeight * 2;
583 	}
584 
585 	SetExplicitMinSize(BSize(0, height));
586 	SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, height));
587 }
588