1/*
2 * Copyright 2010-2017, 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 *		Brian Hill, supernova@tycho.email
13 */
14#include "NotificationWindow.h"
15
16#include <algorithm>
17
18#include <Alert.h>
19#include <Application.h>
20#include <Catalog.h>
21#include <Deskbar.h>
22#include <Directory.h>
23#include <File.h>
24#include <FindDirectory.h>
25#include <GroupLayout.h>
26#include <NodeMonitor.h>
27#include <Notifications.h>
28#include <Path.h>
29#include <Point.h>
30#include <PropertyInfo.h>
31#include <Screen.h>
32
33#include "AppGroupView.h"
34#include "AppUsage.h"
35
36
37#undef B_TRANSLATION_CONTEXT
38#define B_TRANSLATION_CONTEXT "NotificationWindow"
39
40
41property_info main_prop_list[] = {
42	{"message", {B_GET_PROPERTY, 0}, {B_INDEX_SPECIFIER, 0},
43		"get a message"},
44	{"message", {B_COUNT_PROPERTIES, 0}, {B_DIRECT_SPECIFIER, 0},
45		"count messages"},
46	{"message", {B_CREATE_PROPERTY, 0}, {B_DIRECT_SPECIFIER, 0},
47		"create a message"},
48	{"message", {B_SET_PROPERTY, 0}, {B_INDEX_SPECIFIER, 0},
49		"modify a message"},
50
51	{ 0 }
52};
53
54
55/**
56 * Checks if notification position overlaps with
57 * deskbar position
58 */
59static bool
60is_overlapping(deskbar_location deskbar,
61		uint32 notification) {
62	if (deskbar == B_DESKBAR_RIGHT_TOP
63			&& notification == (B_FOLLOW_RIGHT | B_FOLLOW_TOP))
64		return true;
65	if (deskbar == B_DESKBAR_RIGHT_BOTTOM
66			&& notification == (B_FOLLOW_RIGHT | B_FOLLOW_BOTTOM))
67		return true;
68	if (deskbar == B_DESKBAR_LEFT_TOP
69			&& notification == (B_FOLLOW_LEFT | B_FOLLOW_TOP))
70		return true;
71	if (deskbar == B_DESKBAR_LEFT_BOTTOM
72			&& notification == (B_FOLLOW_LEFT | B_FOLLOW_BOTTOM))
73		return true;
74	if (deskbar == B_DESKBAR_TOP
75			&& (notification == (B_FOLLOW_LEFT | B_FOLLOW_TOP)
76			|| notification == (B_FOLLOW_RIGHT | B_FOLLOW_TOP)))
77		return true;
78	if (deskbar == B_DESKBAR_BOTTOM
79			&& (notification == (B_FOLLOW_LEFT | B_FOLLOW_BOTTOM)
80			|| notification == (B_FOLLOW_RIGHT | B_FOLLOW_BOTTOM)))
81		return true;
82	return false;
83}
84
85
86NotificationWindow::NotificationWindow()
87	:
88	BWindow(BRect(0, 0, -1, -1), B_TRANSLATE_MARK("Notification"),
89		B_BORDERED_WINDOW_LOOK, B_FLOATING_ALL_WINDOW_FEEL, B_AVOID_FRONT
90		| B_AVOID_FOCUS | B_NOT_CLOSABLE | B_NOT_ZOOMABLE | B_NOT_MINIMIZABLE
91		| B_NOT_RESIZABLE | B_NOT_MOVABLE | B_AUTO_UPDATE_SIZE_LIMITS,
92		B_ALL_WORKSPACES),
93	fShouldRun(true)
94{
95	status_t result = find_directory(B_USER_CACHE_DIRECTORY, &fCachePath);
96	fCachePath.Append("Notifications");
97	BDirectory cacheDir;
98	result = cacheDir.SetTo(fCachePath.Path());
99	if (result == B_ENTRY_NOT_FOUND)
100		cacheDir.CreateDirectory(fCachePath.Path(), NULL);
101
102	SetLayout(new BGroupLayout(B_VERTICAL, 0));
103
104	_LoadSettings(true);
105
106	// Start the message loop
107	Hide();
108	Show();
109}
110
111
112NotificationWindow::~NotificationWindow()
113{
114	appfilter_t::iterator aIt;
115	for (aIt = fAppFilters.begin(); aIt != fAppFilters.end(); aIt++)
116		delete aIt->second;
117}
118
119
120bool
121NotificationWindow::QuitRequested()
122{
123	appview_t::iterator aIt;
124	for (aIt = fAppViews.begin(); aIt != fAppViews.end(); aIt++) {
125		aIt->second->RemoveSelf();
126		delete aIt->second;
127	}
128
129	BMessenger(be_app).SendMessage(B_QUIT_REQUESTED);
130	return BWindow::QuitRequested();
131}
132
133
134void
135NotificationWindow::WorkspaceActivated(int32 /*workspace*/, bool active)
136{
137	// Ensure window is in the correct position
138	if (active)
139		SetPosition();
140}
141
142
143void
144NotificationWindow::FrameResized(float width, float height)
145{
146	SetPosition();
147}
148
149
150void
151NotificationWindow::ScreenChanged(BRect frame, color_space mode)
152{
153	SetPosition();
154}
155
156
157void
158NotificationWindow::MessageReceived(BMessage* message)
159{
160	switch (message->what) {
161		case B_NODE_MONITOR:
162		{
163			_LoadSettings();
164			break;
165		}
166		case kNotificationMessage:
167		{
168			if (!fShouldRun)
169				break;
170
171			BMessage reply(B_REPLY);
172			BNotification* notification = new BNotification(message);
173
174			if (notification->InitCheck() == B_OK) {
175				bigtime_t timeout;
176				if (message->FindInt64("timeout", &timeout) != B_OK)
177					timeout = fTimeout;
178				BString sourceSignature(notification->SourceSignature());
179				BString sourceName(notification->SourceName());
180
181				bool allow = false;
182				appfilter_t::iterator it = fAppFilters
183					.find(sourceSignature.String());
184
185				AppUsage* appUsage = NULL;
186				if (it == fAppFilters.end()) {
187					if (sourceSignature.Length() > 0
188						&& sourceName.Length() > 0) {
189						appUsage = new AppUsage(sourceName.String(),
190							sourceSignature.String(), true);
191						fAppFilters[sourceSignature.String()] = appUsage;
192						// TODO save back to settings file
193					}
194					allow = true;
195				} else {
196					appUsage = it->second;
197					allow = appUsage->Allowed();
198				}
199
200				if (allow) {
201					BString groupName(notification->Group());
202					appview_t::iterator aIt = fAppViews.find(groupName);
203					AppGroupView* group = NULL;
204					if (aIt == fAppViews.end()) {
205						group = new AppGroupView(this,
206							groupName == "" ? NULL : groupName.String());
207						fAppViews[groupName] = group;
208						GetLayout()->AddView(group);
209					} else
210						group = aIt->second;
211
212					NotificationView* view = new NotificationView(notification,
213						timeout, fIconSize);
214
215					group->AddInfo(view);
216
217					_ShowHide();
218
219					reply.AddInt32("error", B_OK);
220				} else
221					reply.AddInt32("error", B_NOT_ALLOWED);
222			} else {
223				reply.what = B_MESSAGE_NOT_UNDERSTOOD;
224				reply.AddInt32("error", B_ERROR);
225			}
226
227			message->SendReply(&reply);
228			break;
229		}
230		case kRemoveGroupView:
231		{
232			AppGroupView* view = NULL;
233			if (message->FindPointer("view", (void**)&view) != B_OK)
234				return;
235
236			// It's possible that between sending this message, and us receiving
237			// it, the view has become used again, in which case we shouldn't
238			// delete it.
239			if (view->HasChildren())
240				return;
241
242			// this shouldn't happen
243			if (fAppViews.erase(view->Group()) < 1)
244				break;
245
246			view->RemoveSelf();
247			delete view;
248
249			_ShowHide();
250			break;
251		}
252		default:
253			BWindow::MessageReceived(message);
254	}
255}
256
257
258icon_size
259NotificationWindow::IconSize()
260{
261	return fIconSize;
262}
263
264
265int32
266NotificationWindow::Timeout()
267{
268	return fTimeout;
269}
270
271
272float
273NotificationWindow::Width()
274{
275	return fWidth;
276}
277
278
279void
280NotificationWindow::_ShowHide()
281{
282	if (fAppViews.empty() && !IsHidden()) {
283		Hide();
284		return;
285	}
286
287	if (IsHidden()) {
288		SetPosition();
289		Show();
290	}
291}
292
293
294void
295NotificationWindow::SetPosition()
296{
297	Layout(true);
298
299	BRect bounds = DecoratorFrame();
300	float width = Bounds().Width() + 1;
301	float height = Bounds().Height() + 1;
302
303	float leftOffset = Frame().left - bounds.left;
304	float topOffset = Frame().top - bounds.top + 1;
305	float rightOffset = bounds.right - Frame().right;
306	float bottomOffset = bounds.bottom - Frame().bottom;
307		// Size of the borders around the window
308
309	float x = Frame().left;
310	float y = Frame().top;
311		// If we cant guess, don't move...
312	BPoint location(x, y);
313
314	BDeskbar deskbar;
315
316	// If notification and deskbar position are same
317	// then follow deskbar position
318	uint32 position = (is_overlapping(deskbar.Location(), fPosition))
319			? B_FOLLOW_DESKBAR
320			: fPosition;
321
322
323	if (position == B_FOLLOW_DESKBAR) {
324		BRect frame = deskbar.Frame();
325		switch (deskbar.Location()) {
326			case B_DESKBAR_TOP:
327				// In case of overlapping here or for bottom
328				// use user's notification position
329				y = frame.bottom + topOffset;
330				x = (fPosition == (B_FOLLOW_LEFT | B_FOLLOW_TOP))
331					? frame.left + rightOffset
332					: frame.right - width + rightOffset;
333				break;
334			case B_DESKBAR_BOTTOM:
335				y = frame.top - height - bottomOffset;
336				x = (fPosition == (B_FOLLOW_LEFT | B_FOLLOW_BOTTOM))
337					? frame.left + rightOffset
338					: frame.right - width + rightOffset;
339				break;
340			case B_DESKBAR_RIGHT_TOP:
341				y = frame.top - topOffset + 1;
342				x = frame.left - width - rightOffset;
343				break;
344			case B_DESKBAR_LEFT_TOP:
345				y = frame.top - topOffset + 1;
346				x = frame.right + leftOffset;
347				break;
348			case B_DESKBAR_RIGHT_BOTTOM:
349				y = frame.bottom - height + bottomOffset;
350				x = frame.left - width - rightOffset;
351				break;
352			case B_DESKBAR_LEFT_BOTTOM:
353				y = frame.bottom - height + bottomOffset;
354				x = frame.right + leftOffset;
355				break;
356			default:
357				break;
358		}
359		location = BPoint(x, y);
360	} else if (position == (B_FOLLOW_RIGHT | B_FOLLOW_BOTTOM)) {
361		location = BScreen().Frame().RightBottom();
362		location -= BPoint(width, height);
363	} else if (position == (B_FOLLOW_LEFT | B_FOLLOW_BOTTOM)) {
364		location = BScreen().Frame().LeftBottom();
365		location -= BPoint(0, height);
366	} else if (position == (B_FOLLOW_RIGHT | B_FOLLOW_TOP)) {
367		location = BScreen().Frame().RightTop();
368		location -= BPoint(width, 0);
369	} else if (position == (B_FOLLOW_LEFT | B_FOLLOW_TOP)) {
370		location = BScreen().Frame().LeftTop();
371	}
372
373	MoveTo(location);
374}
375
376
377void
378NotificationWindow::_LoadSettings(bool startMonitor)
379{
380	BPath path;
381	BMessage settings;
382
383	if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK)
384		return;
385
386	path.Append(kSettingsFile);
387
388	BFile file(path.Path(), B_READ_ONLY | B_CREATE_FILE);
389	settings.Unflatten(&file);
390
391	_LoadGeneralSettings(settings);
392	_LoadDisplaySettings(settings);
393	_LoadAppFilters(settings);
394
395	if (startMonitor) {
396		node_ref nref;
397		BEntry entry(path.Path());
398		entry.GetNodeRef(&nref);
399
400		if (watch_node(&nref, B_WATCH_ALL, BMessenger(this)) != B_OK) {
401			BAlert* alert = new BAlert(B_TRANSLATE("Warning"),
402				B_TRANSLATE("Couldn't start general settings monitor.\n"
403					"Live filter changes disabled."), B_TRANSLATE("OK"));
404			alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);
405			alert->Go(NULL);
406		}
407	}
408}
409
410
411void
412NotificationWindow::_LoadAppFilters(BMessage& settings)
413{
414	type_code type;
415	int32 count = 0;
416
417	if (settings.GetInfo("app_usage", &type, &count) != B_OK)
418		return;
419
420	for (int32 i = 0; i < count; i++) {
421		AppUsage* app = new AppUsage();
422		if (settings.FindFlat("app_usage", i, app) == B_OK)
423			fAppFilters[app->Signature()] = app;
424		else
425			delete app;
426	}
427}
428
429
430void
431NotificationWindow::_LoadGeneralSettings(BMessage& settings)
432{
433	if (settings.FindBool(kAutoStartName, &fShouldRun) == B_OK) {
434		if (fShouldRun == false) {
435			// We should not start. Quit the app!
436			be_app_messenger.SendMessage(B_QUIT_REQUESTED);
437		}
438	} else
439		fShouldRun = true;
440
441	if (settings.FindInt32(kTimeoutName, &fTimeout) != B_OK)
442		fTimeout = kDefaultTimeout;
443	fTimeout *= 1000000;
444		// Convert from seconds to microseconds
445}
446
447
448void
449NotificationWindow::_LoadDisplaySettings(BMessage& settings)
450{
451	int32 setting;
452	float originalWidth = fWidth;
453
454	if (settings.FindFloat(kWidthName, &fWidth) != B_OK)
455		fWidth = kDefaultWidth;
456	if (originalWidth != fWidth)
457		GetLayout()->SetExplicitSize(BSize(fWidth, B_SIZE_UNSET));
458
459	if (settings.FindInt32(kIconSizeName, &setting) != B_OK)
460		fIconSize = kDefaultIconSize;
461	else
462		fIconSize = (icon_size)setting;
463
464	int32 position;
465	if (settings.FindInt32(kNotificationPositionName, &position) != B_OK)
466		fPosition = kDefaultNotificationPosition;
467	else
468		fPosition = position;
469
470	// Notify the views about the change
471	appview_t::iterator aIt;
472	for (aIt = fAppViews.begin(); aIt != fAppViews.end(); ++aIt) {
473		AppGroupView* view = aIt->second;
474		view->Invalidate();
475	}
476}
477