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