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