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