xref: /haiku/src/servers/notification/NotificationView.cpp (revision 894526b51a3d931c423878fc0eb8da610fa1fb2a)
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 <Notifications.h>
33 
34 #include "AppGroupView.h"
35 #include "NotificationWindow.h"
36 
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 	BNotification* notification, bigtime_t timeout)
59 	:
60 	BView("NotificationView", B_WILL_DRAW),
61 	fParent(win),
62 	fNotification(notification),
63 	fTimeout(timeout),
64 	fRunner(NULL),
65 	fBitmap(NULL),
66 	fCloseClicked(false)
67 {
68 	if (fNotification->Icon() != NULL)
69 		fBitmap = new BBitmap(fNotification->Icon());
70 
71 	if (fTimeout <= 0)
72 		fTimeout = fParent->Timeout() * 1000000;
73 
74 	BGroupLayout* layout = new BGroupLayout(B_VERTICAL);
75 	SetLayout(layout);
76 
77 	SetViewColor(ui_color(B_PANEL_BACKGROUND_COLOR));
78 	SetLowColor(ui_color(B_PANEL_BACKGROUND_COLOR));
79 
80 	switch (fNotification->Type()) {
81 		case B_IMPORTANT_NOTIFICATION:
82 			fStripeColor = ui_color(B_CONTROL_HIGHLIGHT_COLOR);
83 			break;
84 		case B_ERROR_NOTIFICATION:
85 			fStripeColor = ui_color(B_FAILURE_COLOR);
86 			break;
87 		case B_PROGRESS_NOTIFICATION:
88 		{
89 			BStatusBar* progress = new BStatusBar("progress");
90 			progress->SetBarHeight(12.0f);
91 			progress->SetMaxValue(1.0f);
92 			progress->Update(fNotification->Progress());
93 
94 			BString label = "";
95 			label << (int)(fNotification->Progress() * 100) << " %";
96 			progress->SetTrailingText(label);
97 
98 			layout->AddView(progress);
99 		}
100 		// fall through.
101 		case B_INFORMATION_NOTIFICATION:
102 			fStripeColor = tint_color(ui_color(B_PANEL_BACKGROUND_COLOR),
103 				B_DARKEN_1_TINT);
104 			break;
105 	}
106 }
107 
108 
109 NotificationView::~NotificationView()
110 {
111 	delete fRunner;
112 	delete fBitmap;
113 	delete fNotification;
114 
115 	LineInfoList::iterator lIt;
116 	for (lIt = fLines.begin(); lIt != fLines.end(); lIt++)
117 		delete (*lIt);
118 }
119 
120 
121 void
122 NotificationView::AttachedToWindow()
123 {
124 	SetText();
125 
126 	BMessage msg(kRemoveView);
127 	msg.AddPointer("view", this);
128 
129 	fRunner = new BMessageRunner(BMessenger(Parent()), &msg, fTimeout, 1);
130 }
131 
132 
133 void
134 NotificationView::MessageReceived(BMessage* msg)
135 {
136 	switch (msg->what) {
137 		case B_GET_PROPERTY:
138 		{
139 			BMessage specifier;
140 			const char* property;
141 			BMessage reply(B_REPLY);
142 			bool msgOkay = true;
143 
144 			if (msg->FindMessage("specifiers", 0, &specifier) != B_OK)
145 				msgOkay = false;
146 			if (specifier.FindString("property", &property) != B_OK)
147 				msgOkay = false;
148 
149 			if (msgOkay) {
150 				if (strcmp(property, "type") == 0)
151 					reply.AddInt32("result", fNotification->Type());
152 
153 				if (strcmp(property, "group") == 0)
154 					reply.AddString("result", fNotification->Group());
155 
156 				if (strcmp(property, "title") == 0)
157 					reply.AddString("result", fNotification->Title());
158 
159 				if (strcmp(property, "content") == 0)
160 					reply.AddString("result", fNotification->Content());
161 
162 				if (strcmp(property, "progress") == 0)
163 					reply.AddFloat("result", fNotification->Progress());
164 
165 				if ((strcmp(property, "icon") == 0) && fBitmap) {
166 					BMessage archive;
167 					if (fBitmap->Archive(&archive) == B_OK)
168 						reply.AddMessage("result", &archive);
169 				}
170 
171 				reply.AddInt32("error", B_OK);
172 			} else {
173 				reply.what = B_MESSAGE_NOT_UNDERSTOOD;
174 				reply.AddInt32("error", B_ERROR);
175 			}
176 
177 			msg->SendReply(&reply);
178 			break;
179 		}
180 		case B_SET_PROPERTY:
181 		{
182 			BMessage specifier;
183 			const char* property;
184 			BMessage reply(B_REPLY);
185 			bool msgOkay = true;
186 
187 			if (msg->FindMessage("specifiers", 0, &specifier) != B_OK)
188 				msgOkay = false;
189 			if (specifier.FindString("property", &property) != B_OK)
190 				msgOkay = false;
191 
192 			if (msgOkay) {
193 				const char* value = NULL;
194 
195 				if (strcmp(property, "group") == 0)
196 					if (msg->FindString("data", &value) == B_OK)
197 						fNotification->SetGroup(value);
198 
199 				if (strcmp(property, "title") == 0)
200 					if (msg->FindString("data", &value) == B_OK)
201 						fNotification->SetTitle(value);
202 
203 				if (strcmp(property, "content") == 0)
204 					if (msg->FindString("data", &value) == B_OK)
205 						fNotification->SetContent(value);
206 
207 				if (strcmp(property, "icon") == 0) {
208 					BMessage archive;
209 					if (msg->FindMessage("data", &archive) == B_OK) {
210 						delete fBitmap;
211 						fBitmap = new BBitmap(&archive);
212 					}
213 				}
214 
215 				SetText();
216 				Invalidate();
217 
218 				reply.AddInt32("error", B_OK);
219 			} else {
220 				reply.what = B_MESSAGE_NOT_UNDERSTOOD;
221 				reply.AddInt32("error", B_ERROR);
222 			}
223 
224 			msg->SendReply(&reply);
225 			break;
226 		}
227 		default:
228 			BView::MessageReceived(msg);
229 	}
230 }
231 
232 
233 void
234 NotificationView::Draw(BRect updateRect)
235 {
236 	BRect progRect;
237 
238 	SetDrawingMode(B_OP_ALPHA);
239 	SetBlendingMode(B_PIXEL_ALPHA, B_ALPHA_OVERLAY);
240 
241 	// Icon size
242 	float iconSize = (float)fParent->IconSize();
243 
244 	BRect stripeRect = Bounds();
245 	stripeRect.right = kIconStripeWidth;
246 	SetHighColor(tint_color(ui_color(B_PANEL_BACKGROUND_COLOR),
247 		B_DARKEN_1_TINT));
248 	FillRect(stripeRect);
249 
250 	SetHighColor(fStripeColor);
251 	stripeRect.right = 2;
252 	FillRect(stripeRect);
253 
254 	SetHighColor(ui_color(B_PANEL_TEXT_COLOR));
255 	// Rectangle for icon and overlay icon
256 	BRect iconRect(0, 0, 0, 0);
257 
258 	// Draw icon
259 	if (fBitmap) {
260 		float ix = 18;
261 		float iy = (Bounds().Height() - iconSize) / 4.0;
262 			// Icon is vertically centered in view
263 
264 		if (fNotification->Type() == B_PROGRESS_NOTIFICATION)
265 		{
266 			// Move icon up by half progress bar height if it's present
267 			iy -= (progRect.Height() + kEdgePadding);
268 		}
269 
270 		iconRect.Set(ix, iy, ix + iconSize - 1.0, iy + iconSize - 1.0);
271 		DrawBitmapAsync(fBitmap, fBitmap->Bounds(), iconRect);
272 	}
273 
274 	// Draw content
275 	LineInfoList::iterator lIt;
276 	for (lIt = fLines.begin(); lIt != fLines.end(); lIt++) {
277 		LineInfo *l = (*lIt);
278 
279 		SetFont(&l->font);
280 		// Truncate the string. We have already line-wrapped the text but if
281 		// there is a very long 'word' we can only truncate it.
282 		TruncateString(&(l->text), B_TRUNCATE_END,
283 			Bounds().Width() - l->location.x);
284 		DrawString(l->text.String(), l->text.Length(), l->location);
285 	}
286 
287 	rgb_color detailCol = ui_color(B_CONTROL_BORDER_COLOR);
288 	detailCol = tint_color(detailCol, B_LIGHTEN_2_TINT);
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 	int32 buttons;
340 	Window()->CurrentMessage()->FindInt32("buttons", &buttons);
341 
342 	switch (buttons) {
343 		case B_PRIMARY_MOUSE_BUTTON:
344 		{
345 			BRect closeRect = Bounds().InsetByCopy(2,2);
346 			closeRect.left = closeRect.right - kCloseSize;
347 			closeRect.bottom = closeRect.top + kCloseSize;
348 
349 			if (!closeRect.Contains(point)) {
350 				entry_ref launchRef;
351 				BString launchString;
352 				BMessage argMsg(B_ARGV_RECEIVED);
353 				BMessage refMsg(B_REFS_RECEIVED);
354 				entry_ref appRef;
355 				bool useArgv = false;
356 				BList messages;
357 				entry_ref ref;
358 
359 				if (fNotification->OnClickApp() != NULL
360 					&& be_roster->FindApp(fNotification->OnClickApp(), &appRef)
361 				   		== B_OK) {
362 					useArgv = true;
363 				}
364 
365 				if (fNotification->OnClickFile() != NULL
366 					&& be_roster->FindApp(
367 							(entry_ref*)fNotification->OnClickFile(), &appRef)
368 				   		== B_OK) {
369 					useArgv = true;
370 				}
371 
372 				for (int32 i = 0; i < fNotification->CountOnClickRefs(); i++)
373 					refMsg.AddRef("refs", fNotification->OnClickRefAt(i));
374 				messages.AddItem((void*)&refMsg);
375 
376 				if (useArgv) {
377 					int32 argc = fNotification->CountOnClickArgs() + 1;
378 					BString arg;
379 
380 					BPath p(&appRef);
381 					argMsg.AddString("argv", p.Path());
382 
383 					argMsg.AddInt32("argc", argc);
384 
385 					for (int32 i = 0; i < argc - 1; i++) {
386 						argMsg.AddString("argv",
387 							fNotification->OnClickArgAt(i));
388 					}
389 
390 					messages.AddItem((void*)&argMsg);
391 				}
392 
393 				if (fNotification->OnClickApp() != NULL)
394 					be_roster->Launch(fNotification->OnClickApp(), &messages);
395 				else
396 					be_roster->Launch(fNotification->OnClickFile(), &messages);
397 			} else {
398 				fCloseClicked = true;
399 			}
400 
401 			// Remove the info view after a click
402 			BMessage remove_msg(kRemoveView);
403 			remove_msg.AddPointer("view", this);
404 
405 			BMessenger msgr(Parent());
406 			msgr.SendMessage(&remove_msg);
407 			break;
408 		}
409 	}
410 }
411 
412 
413 BHandler*
414 NotificationView::ResolveSpecifier(BMessage* msg, int32 index, BMessage* spec,
415 	int32 form, const char* prop)
416 {
417 	BPropertyInfo prop_info(message_prop_list);
418 	if (prop_info.FindMatch(msg, index, spec, form, prop) >= 0) {
419 		msg->PopSpecifier();
420 		return this;
421 	}
422 
423 	return BView::ResolveSpecifier(msg, index, spec, form, prop);
424 }
425 
426 
427 status_t
428 NotificationView::GetSupportedSuites(BMessage* msg)
429 {
430 	msg->AddString("suites", "suite/x-vnd.Haiku-notification_server");
431 	BPropertyInfo prop_info(message_prop_list);
432 	msg->AddFlat("messages", &prop_info);
433 	return BView::GetSupportedSuites(msg);
434 }
435 
436 
437 void
438 NotificationView::SetText(float newMaxWidth)
439 {
440 	if (newMaxWidth < 0 && Parent())
441 		newMaxWidth = Parent()->Bounds().IntegerWidth();
442 	if (newMaxWidth <= 0)
443 		newMaxWidth = kDefaultWidth;
444 
445 	// Delete old lines
446 	LineInfoList::iterator lIt;
447 	for (lIt = fLines.begin(); lIt != fLines.end(); lIt++)
448 		delete (*lIt);
449 	fLines.clear();
450 
451 	float iconRight = kIconStripeWidth;
452 	if (fBitmap != NULL)
453 		iconRight += fParent->IconSize();
454 	else
455 		iconRight += 32;
456 
457 	font_height fh;
458 	be_bold_font->GetHeight(&fh);
459 	float fontHeight = ceilf(fh.leading) + ceilf(fh.descent)
460 		+ ceilf(fh.ascent);
461 	float y = 2 * fontHeight;
462 
463 	// Title
464 	LineInfo* titleLine = new LineInfo;
465 	titleLine->text = fNotification->Title();
466 	titleLine->font = *be_bold_font;
467 
468 	titleLine->location = BPoint(iconRight + kEdgePadding, y);
469 
470 	fLines.push_front(titleLine);
471 	y += fontHeight;
472 
473 	// Rest of text is rendered with be_plain_font.
474 	be_plain_font->GetHeight(&fh);
475 	fontHeight = ceilf(fh.leading) + ceilf(fh.descent)
476 		+ ceilf(fh.ascent);
477 
478 	// Split text into chunks between certain characters and compose the lines.
479 	const char kSeparatorCharacters[] = " \n-\\";
480 	BString textBuffer = fNotification->Content();
481 	textBuffer.ReplaceAll("\t", "    ");
482 	const char* chunkStart = textBuffer.String();
483 	float maxWidth = newMaxWidth - kEdgePadding - iconRight;
484 	LineInfo* line = NULL;
485 	ssize_t length = textBuffer.Length();
486 	while (chunkStart - textBuffer.String() < length) {
487 		size_t chunkLength = strcspn(chunkStart, kSeparatorCharacters) + 1;
488 
489 		// Start a new line if we didn't start one before
490 		BString tempText;
491 		if (line != NULL)
492 			tempText.SetTo(line->text);
493 		tempText.Append(chunkStart, chunkLength);
494 
495 		if (line == NULL || chunkStart[0] == '\n'
496 			|| StringWidth(tempText) > maxWidth) {
497 			line = new LineInfo;
498 			line->font = *be_plain_font;
499 			line->location = BPoint(iconRight + kEdgePadding, y);
500 
501 			fLines.push_front(line);
502 			y += fontHeight;
503 
504 			// Skip the eventual new-line character at the beginning of this chunk
505 			if (chunkStart[0] == '\n') {
506 				chunkStart++;
507 				chunkLength--;
508 			}
509 
510 			// Skip more new-line characters and move the line further down
511 			while (chunkStart[0] == '\n') {
512 				chunkStart++;
513 				chunkLength--;
514 				line->location.y += fontHeight;
515 				y += fontHeight;
516 			}
517 
518 			// Strip space at beginning of a new line
519 			while (chunkStart[0] == ' ') {
520 				chunkLength--;
521 				chunkStart++;
522 			}
523 		}
524 
525 		if (chunkStart[0] == '\0')
526 			break;
527 
528 		// Append the chunk to the current line, which was either a new
529 		// line or the one from the previous iteration
530 		line->text.Append(chunkStart, chunkLength);
531 
532 		chunkStart += chunkLength;
533 	}
534 
535 	fHeight = y + (kEdgePadding * 2);
536 
537 	// Make sure icon fits
538 	if (fBitmap != NULL) {
539 		float minHeight = fBitmap->Bounds().Height() + 2 * kEdgePadding;
540 
541 		if (fHeight < minHeight)
542 			fHeight = minHeight;
543 	}
544 
545 	// Make sure the progress bar is below the text, and the window is big
546 	// enough.
547 	static_cast<BGroupLayout*>(GetLayout())->SetInsets(kIconStripeWidth + 8,
548 		fHeight, 8, 8);
549 
550 	_CalculateSize();
551 }
552 
553 
554 const char*
555 NotificationView::MessageID() const
556 {
557 	return fNotification->MessageID();
558 }
559 
560 
561 void
562 NotificationView::_CalculateSize()
563 {
564 	float height = fHeight;
565 
566 	if (fNotification->Type() == B_PROGRESS_NOTIFICATION) {
567 		font_height fh;
568 		be_plain_font->GetHeight(&fh);
569 		float fontHeight = fh.ascent + fh.descent + fh.leading;
570 		height += 9 + (kSmallPadding * 2) + (kEdgePadding * 1)
571 			+ fontHeight * 2;
572 	}
573 
574 	SetExplicitMinSize(BSize(0, height));
575 	SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, height));
576 }
577