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