xref: /haiku/src/servers/notification/NotificationWindow.cpp (revision 579f1dbca962a2a03df54f69fdc6e9423f91f20e)
1 /*
2  * Copyright 2010, 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  */
13 #include "NotificationWindow.h"
14 
15 #include <algorithm>
16 
17 #include <Alert.h>
18 #include <Application.h>
19 #include <Catalog.h>
20 #include <Deskbar.h>
21 #include <Directory.h>
22 #include <File.h>
23 #include <FindDirectory.h>
24 #include <GroupLayout.h>
25 #include <NodeMonitor.h>
26 #include <Notifications.h>
27 #include <Path.h>
28 #include <PropertyInfo.h>
29 
30 #include "AppGroupView.h"
31 #include "AppUsage.h"
32 
33 
34 #undef B_TRANSLATION_CONTEXT
35 #define B_TRANSLATION_CONTEXT "NotificationWindow"
36 
37 
38 property_info main_prop_list[] = {
39 	{"message", {B_GET_PROPERTY, 0}, {B_INDEX_SPECIFIER, 0},
40 		"get a message"},
41 	{"message", {B_COUNT_PROPERTIES, 0}, {B_DIRECT_SPECIFIER, 0},
42 		"count messages"},
43 	{"message", {B_CREATE_PROPERTY, 0}, {B_DIRECT_SPECIFIER, 0},
44 		"create a message"},
45 	{"message", {B_SET_PROPERTY, 0}, {B_INDEX_SPECIFIER, 0},
46 		"modify a message"},
47 	{0}
48 };
49 
50 
51 const float kCloseSize				= 6;
52 const float kExpandSize				= 8;
53 const float kPenSize				= 1;
54 const float kEdgePadding			= 2;
55 const float kSmallPadding			= 2;
56 
57 NotificationWindow::NotificationWindow()
58 	:
59 	BWindow(BRect(0, 0, -1, -1), B_TRANSLATE_MARK("Notification"),
60 		B_BORDERED_WINDOW_LOOK, B_FLOATING_ALL_WINDOW_FEEL, B_AVOID_FRONT
61 		| B_AVOID_FOCUS | B_NOT_CLOSABLE | B_NOT_ZOOMABLE | B_NOT_MINIMIZABLE
62 		| B_NOT_RESIZABLE | B_NOT_MOVABLE | B_AUTO_UPDATE_SIZE_LIMITS,
63 		B_ALL_WORKSPACES)
64 {
65 	SetLayout(new BGroupLayout(B_VERTICAL, 0));
66 
67 	_LoadSettings(true);
68 	_LoadAppFilters(true);
69 
70 	// Start the message loop
71 	Hide();
72 	Show();
73 }
74 
75 
76 NotificationWindow::~NotificationWindow()
77 {
78 	appfilter_t::iterator aIt;
79 	for (aIt = fAppFilters.begin(); aIt != fAppFilters.end(); aIt++)
80 		delete aIt->second;
81 }
82 
83 
84 bool
85 NotificationWindow::QuitRequested()
86 {
87 	appview_t::iterator aIt;
88 	for (aIt = fAppViews.begin(); aIt != fAppViews.end(); aIt++) {
89 		aIt->second->RemoveSelf();
90 		delete aIt->second;
91 	}
92 
93 	BMessenger(be_app).SendMessage(B_QUIT_REQUESTED);
94 	return BWindow::QuitRequested();
95 }
96 
97 
98 void
99 NotificationWindow::WorkspaceActivated(int32 /*workspace*/, bool active)
100 {
101 	// Ensure window is in the correct position
102 	if (active)
103 		SetPosition();
104 }
105 
106 
107 void
108 NotificationWindow::FrameResized(float width, float height)
109 {
110 	SetPosition();
111 }
112 
113 
114 void
115 NotificationWindow::MessageReceived(BMessage* message)
116 {
117 	switch (message->what) {
118 		case B_NODE_MONITOR:
119 		{
120 			_LoadSettings();
121 			_LoadAppFilters();
122 			break;
123 		}
124 		case B_COUNT_PROPERTIES:
125 		{
126 			BMessage reply(B_REPLY);
127 			BMessage specifier;
128 			const char* property = NULL;
129 			bool messageOkay = true;
130 
131 			if (message->FindMessage("specifiers", 0, &specifier) != B_OK)
132 				messageOkay = false;
133 			if (specifier.FindString("property", &property) != B_OK)
134 				messageOkay = false;
135 			if (strcmp(property, "message") != 0)
136 				messageOkay = false;
137 
138 			if (messageOkay)
139 				reply.AddInt32("result", fViews.size());
140 			else {
141 				reply.what = B_MESSAGE_NOT_UNDERSTOOD;
142 				reply.AddInt32("error", B_ERROR);
143 			}
144 
145 			message->SendReply(&reply);
146 			break;
147 		}
148 		case B_CREATE_PROPERTY:
149 		case kNotificationMessage:
150 		{
151 			BMessage reply(B_REPLY);
152 			BNotification* notification = new BNotification(message);
153 
154 			if (notification->InitCheck() == B_OK) {
155 				bigtime_t timeout;
156 				if (message->FindInt64("timeout", &timeout) != B_OK)
157 					timeout = -1;
158 				BMessenger messenger = message->ReturnAddress();
159 				app_info info;
160 
161 				if (messenger.IsValid())
162 					be_roster->GetRunningAppInfo(messenger.Team(), &info);
163 				else
164 					be_roster->GetAppInfo("application/x-vnd.Be-SHEL", &info);
165 
166 				NotificationView* view = new NotificationView(this,
167 					notification, timeout);
168 
169 				bool allow = false;
170 				appfilter_t::iterator it = fAppFilters.find(info.signature);
171 
172 				if (it == fAppFilters.end()) {
173 					AppUsage* appUsage = new AppUsage(notification->Group(),
174 						true);
175 
176 					appUsage->Allowed(notification->Title(),
177 							notification->Type());
178 					fAppFilters[info.signature] = appUsage;
179 					allow = true;
180 				} else {
181 					allow = it->second->Allowed(notification->Title(),
182 						notification->Type());
183 				}
184 
185 				if (allow) {
186 					BString groupName(notification->Group());
187 					appview_t::iterator aIt = fAppViews.find(groupName);
188 					AppGroupView* group = NULL;
189 					if (aIt == fAppViews.end()) {
190 						group = new AppGroupView(this,
191 							groupName == "" ? NULL : groupName.String());
192 						fAppViews[groupName] = group;
193 						GetLayout()->AddView(group);
194 					} else
195 						group = aIt->second;
196 
197 					group->AddInfo(view);
198 
199 					_ShowHide();
200 
201 					reply.AddInt32("error", B_OK);
202 				} else
203 					reply.AddInt32("error", B_NOT_ALLOWED);
204 			} else {
205 				reply.what = B_MESSAGE_NOT_UNDERSTOOD;
206 				reply.AddInt32("error", B_ERROR);
207 			}
208 
209 			message->SendReply(&reply);
210 			break;
211 		}
212 		case kRemoveView:
213 		{
214 			NotificationView* view = NULL;
215 			if (message->FindPointer("view", (void**)&view) != B_OK)
216 				return;
217 
218 			views_t::iterator it = find(fViews.begin(), fViews.end(), view);
219 
220 			if (it != fViews.end())
221 				fViews.erase(it);
222 			break;
223 		}
224 		case kRemoveGroupView:
225 		{
226 			AppGroupView* view = NULL;
227 			if (message->FindPointer("view", (void**)&view) != B_OK)
228 				return;
229 
230 			// It's possible that between sending this message, and us receiving
231 			// it, the view has become used again, in which case we shouldn't
232 			// delete it.
233 			if (view->HasChildren())
234 				return;
235 
236 			// this shouldn't happen
237 			if (fAppViews.erase(view->Group()) < 1)
238 				break;
239 
240 			if (GetLayout()->RemoveView(view))
241 				delete view;
242 
243 			_ShowHide();
244 			break;
245 		}
246 		default:
247 			BWindow::MessageReceived(message);
248 	}
249 }
250 
251 
252 BHandler*
253 NotificationWindow::ResolveSpecifier(BMessage* msg, int32 index,
254 	BMessage* spec, int32 form, const char* prop)
255 {
256 	BPropertyInfo prop_info(main_prop_list);
257 	BHandler* handler = NULL;
258 
259 	if (strcmp(prop,"message") == 0) {
260 		switch (msg->what) {
261 			case B_CREATE_PROPERTY:
262 			{
263 				msg->PopSpecifier();
264 				handler = this;
265 				break;
266 			}
267 			case B_SET_PROPERTY:
268 			case B_GET_PROPERTY:
269 			{
270 				int32 i;
271 
272 				if (spec->FindInt32("index", &i) != B_OK)
273 					i = -1;
274 
275 				if (i >= 0 && i < (int32)fViews.size()) {
276 					msg->PopSpecifier();
277 					handler = fViews[i];
278 				} else
279 					handler = NULL;
280 				break;
281 			}
282 			case B_COUNT_PROPERTIES:
283 				msg->PopSpecifier();
284 				handler = this;
285 				break;
286 			default:
287 				break;
288 		}
289 	}
290 
291 	if (!handler)
292 		handler = BWindow::ResolveSpecifier(msg, index, spec, form, prop);
293 
294 	return handler;
295 }
296 
297 
298 icon_size
299 NotificationWindow::IconSize()
300 {
301 	return fIconSize;
302 }
303 
304 
305 int32
306 NotificationWindow::Timeout()
307 {
308 	return fTimeout;
309 }
310 
311 
312 float
313 NotificationWindow::Width()
314 {
315 	return fWidth;
316 }
317 
318 
319 void
320 NotificationWindow::_ShowHide()
321 {
322 	if (fAppViews.empty() && !IsHidden()) {
323 		Hide();
324 		return;
325 	}
326 
327 	if (IsHidden()) {
328 		SetPosition();
329 		Show();
330 	}
331 }
332 
333 
334 void
335 NotificationWindow::NotificationViewSwapped(NotificationView* stale,
336 	NotificationView* fresh)
337 {
338 	views_t::iterator it = find(fViews.begin(), fViews.end(), stale);
339 
340 	if (it != fViews.end())
341 		*it = fresh;
342 }
343 
344 
345 void
346 NotificationWindow::SetPosition()
347 {
348 	Layout(true);
349 
350 	BRect bounds = DecoratorFrame();
351 	float width = Bounds().Width() + 1;
352 	float height = Bounds().Height() + 1;
353 
354 	float leftOffset = Frame().left - bounds.left;
355 	float topOffset = Frame().top - bounds.top + 1;
356 	float rightOffset = bounds.right - Frame().right;
357 	float bottomOffset = bounds.bottom - Frame().bottom;
358 		// Size of the borders around the window
359 
360 	float x = Frame().left, y = Frame().top;
361 		// If we can't guess, don't move...
362 
363 	BDeskbar deskbar;
364 	BRect frame = deskbar.Frame();
365 
366 	switch (deskbar.Location()) {
367 		case B_DESKBAR_TOP:
368 			// Put it just under, top right corner
369 			y = frame.bottom + topOffset;
370 			x = frame.right - width + rightOffset;
371 			break;
372 		case B_DESKBAR_BOTTOM:
373 			// Put it just above, lower left corner
374 			y = frame.top - height - bottomOffset;
375 			x = frame.right - width + rightOffset;
376 			break;
377 		case B_DESKBAR_RIGHT_TOP:
378 			x = frame.left - width - rightOffset;
379 			y = frame.top - topOffset + 1;
380 			break;
381 		case B_DESKBAR_LEFT_TOP:
382 			x = frame.right + leftOffset;
383 			y = frame.top - topOffset + 1;
384 			break;
385 		case B_DESKBAR_RIGHT_BOTTOM:
386 			y = frame.bottom - height + bottomOffset;
387 			x = frame.left - width - rightOffset;
388 			break;
389 		case B_DESKBAR_LEFT_BOTTOM:
390 			y = frame.bottom - height + bottomOffset;
391 			x = frame.right + leftOffset;
392 			break;
393 		default:
394 			break;
395 	}
396 
397 	MoveTo(x, y);
398 }
399 
400 
401 void
402 NotificationWindow::_LoadSettings(bool startMonitor)
403 {
404 	_LoadGeneralSettings(startMonitor);
405 	_LoadDisplaySettings(startMonitor);
406 }
407 
408 
409 void
410 NotificationWindow::_LoadAppFilters(bool startMonitor)
411 {
412 	BPath path;
413 
414 	if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK)
415 		return;
416 
417 	path.Append(kSettingsDirectory);
418 
419 	if (create_directory(path.Path(), 0755) != B_OK)
420 		return;
421 
422 	path.Append(kFiltersSettings);
423 
424 	BFile file(path.Path(), B_READ_ONLY);
425 	BMessage settings;
426 	if (settings.Unflatten(&file) != B_OK)
427 		return;
428 
429 	type_code type;
430 	int32 count = 0;
431 
432 	if (settings.GetInfo("app_usage", &type, &count) != B_OK)
433 		return;
434 
435 	for (int32 i = 0; i < count; i++) {
436 		AppUsage* app = new AppUsage();
437 		settings.FindFlat("app_usage", i, app);
438 		fAppFilters[app->Name()] = app;
439 	}
440 
441 	if (startMonitor) {
442 		node_ref nref;
443 		BEntry entry(path.Path());
444 		entry.GetNodeRef(&nref);
445 
446 		if (watch_node(&nref, B_WATCH_ALL, BMessenger(this)) != B_OK) {
447 			BAlert* alert = new BAlert(B_TRANSLATE("Warning"),
448 					B_TRANSLATE("Couldn't start filter monitor."
449 						" Live filter changes disabled."), B_TRANSLATE("Darn."));
450 			alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);
451 			alert->Go();
452 		}
453 	}
454 }
455 
456 
457 void
458 NotificationWindow::_SaveAppFilters()
459 {
460 	BPath path;
461 
462 	if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK)
463 		return;
464 
465 	path.Append(kSettingsDirectory);
466 	path.Append(kFiltersSettings);
467 
468 	BMessage settings;
469 	BFile file(path.Path(), B_WRITE_ONLY);
470 
471 	appfilter_t::iterator fIt;
472 	for (fIt = fAppFilters.begin(); fIt != fAppFilters.end(); fIt++)
473 		settings.AddFlat("app_usage", fIt->second);
474 
475 	settings.Flatten(&file);
476 }
477 
478 
479 void
480 NotificationWindow::_LoadGeneralSettings(bool startMonitor)
481 {
482 	BPath path;
483 	BMessage settings;
484 
485 	if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK)
486 		return;
487 
488 	path.Append(kSettingsDirectory);
489 	if (create_directory(path.Path(), 0755) == B_OK) {
490 		path.Append(kGeneralSettings);
491 
492 		BFile file(path.Path(), B_READ_ONLY);
493 		settings.Unflatten(&file);
494 	}
495 
496 	if (settings.FindInt32(kTimeoutName, &fTimeout) != B_OK)
497 		fTimeout = kDefaultTimeout;
498 
499 	// Notify the view about the change
500 	views_t::iterator it;
501 	for (it = fViews.begin(); it != fViews.end(); ++it) {
502 		NotificationView* view = (*it);
503 		view->Invalidate();
504 	}
505 
506 	if (startMonitor) {
507 		node_ref nref;
508 		BEntry entry(path.Path());
509 		entry.GetNodeRef(&nref);
510 
511 		if (watch_node(&nref, B_WATCH_ALL, BMessenger(this)) != B_OK) {
512 			BAlert* alert = new BAlert(B_TRANSLATE("Warning"),
513 						B_TRANSLATE("Couldn't start general settings monitor.\n"
514 						"Live filter changes disabled."), B_TRANSLATE("OK"));
515 			alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);
516 			alert->Go();
517 		}
518 	}
519 }
520 
521 
522 void
523 NotificationWindow::_LoadDisplaySettings(bool startMonitor)
524 {
525 	BPath path;
526 	BMessage settings;
527 
528 	if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK)
529 		return;
530 
531 	path.Append(kSettingsDirectory);
532 	if (create_directory(path.Path(), 0755) == B_OK) {
533 		path.Append(kDisplaySettings);
534 
535 		BFile file(path.Path(), B_READ_ONLY);
536 		settings.Unflatten(&file);
537 	}
538 
539 	int32 setting;
540 
541 	if (settings.FindFloat(kWidthName, &fWidth) != B_OK)
542 		fWidth = kDefaultWidth;
543 	GetLayout()->SetExplicitMaxSize(BSize(fWidth, B_SIZE_UNSET));
544 	GetLayout()->SetExplicitMinSize(BSize(fWidth, B_SIZE_UNSET));
545 
546 	if (settings.FindInt32(kIconSizeName, &setting) != B_OK)
547 		fIconSize = kDefaultIconSize;
548 	else
549 		fIconSize = (icon_size)setting;
550 
551 	// Notify the view about the change
552 	views_t::iterator it;
553 	for (it = fViews.begin(); it != fViews.end(); ++it) {
554 		NotificationView* view = (*it);
555 		view->Invalidate();
556 	}
557 
558 	if (startMonitor) {
559 		node_ref nref;
560 		BEntry entry(path.Path());
561 		entry.GetNodeRef(&nref);
562 
563 		if (watch_node(&nref, B_WATCH_ALL, BMessenger(this)) != B_OK) {
564 			BAlert* alert = new BAlert(B_TRANSLATE("Warning"),
565 				B_TRANSLATE("Couldn't start display settings monitor.\n"
566 					"Live filter changes disabled."), B_TRANSLATE("OK"));
567 			alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);
568 			alert->Go();
569 		}
570 	}
571 }
572