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