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
38property_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
51const float kCloseSize				= 6;
52const float kExpandSize				= 8;
53const float kPenSize				= 1;
54const float kEdgePadding			= 2;
55const float kSmallPadding			= 2;
56
57NotificationWindow::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
76NotificationWindow::~NotificationWindow()
77{
78	appfilter_t::iterator aIt;
79	for (aIt = fAppFilters.begin(); aIt != fAppFilters.end(); aIt++)
80		delete aIt->second;
81}
82
83
84bool
85NotificationWindow::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
98void
99NotificationWindow::WorkspaceActivated(int32 /*workspace*/, bool active)
100{
101	// Ensure window is in the correct position
102	if (active)
103		SetPosition();
104}
105
106
107void
108NotificationWindow::FrameResized(float width, float height)
109{
110	SetPosition();
111}
112
113
114void
115NotificationWindow::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
252BHandler*
253NotificationWindow::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
298icon_size
299NotificationWindow::IconSize()
300{
301	return fIconSize;
302}
303
304
305int32
306NotificationWindow::Timeout()
307{
308	return fTimeout;
309}
310
311
312float
313NotificationWindow::Width()
314{
315	return fWidth;
316}
317
318
319void
320NotificationWindow::_ShowHide()
321{
322	if (fAppViews.empty() && !IsHidden()) {
323		Hide();
324		return;
325	}
326
327	if (IsHidden()) {
328		SetPosition();
329		Show();
330	}
331}
332
333
334void
335NotificationWindow::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
345void
346NotificationWindow::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;
380			break;
381		case B_DESKBAR_LEFT_TOP:
382			x = frame.right + leftOffset;
383			y = frame.top - topOffset;
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
401void
402NotificationWindow::_LoadSettings(bool startMonitor)
403{
404	_LoadGeneralSettings(startMonitor);
405	_LoadDisplaySettings(startMonitor);
406}
407
408
409void
410NotificationWindow::_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
457void
458NotificationWindow::_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
479void
480NotificationWindow::_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
522void
523NotificationWindow::_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