xref: /haiku/src/servers/notification/NotificationWindow.cpp (revision 726fb6c4416276caa98d6655eaf6759d3a4b5925)
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 
41 property_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  */
59 static bool
is_overlapping(deskbar_location deskbar,uint32 notification)60 is_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 
NotificationWindow()86 NotificationWindow::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 
~NotificationWindow()112 NotificationWindow::~NotificationWindow()
113 {
114 	appfilter_t::iterator aIt;
115 	for (aIt = fAppFilters.begin(); aIt != fAppFilters.end(); aIt++)
116 		delete aIt->second;
117 }
118 
119 
120 bool
QuitRequested()121 NotificationWindow::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 
134 void
WorkspaceActivated(int32,bool active)135 NotificationWindow::WorkspaceActivated(int32 /*workspace*/, bool active)
136 {
137 	// Ensure window is in the correct position
138 	if (active)
139 		SetPosition();
140 }
141 
142 
143 void
FrameResized(float width,float height)144 NotificationWindow::FrameResized(float width, float height)
145 {
146 	SetPosition();
147 }
148 
149 
150 void
ScreenChanged(BRect frame,color_space mode)151 NotificationWindow::ScreenChanged(BRect frame, color_space mode)
152 {
153 	SetPosition();
154 }
155 
156 
157 void
MessageReceived(BMessage * message)158 NotificationWindow::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 
258 icon_size
IconSize()259 NotificationWindow::IconSize()
260 {
261 	return fIconSize;
262 }
263 
264 
265 int32
Timeout()266 NotificationWindow::Timeout()
267 {
268 	return fTimeout;
269 }
270 
271 
272 float
Width()273 NotificationWindow::Width()
274 {
275 	return fWidth;
276 }
277 
278 
279 void
_ShowHide()280 NotificationWindow::_ShowHide()
281 {
282 	if (fAppViews.empty() && !IsHidden()) {
283 		Hide();
284 		return;
285 	}
286 
287 	if (IsHidden()) {
288 		SetPosition();
289 		Show();
290 	}
291 }
292 
293 
294 void
SetPosition()295 NotificationWindow::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 
377 void
_LoadSettings(bool startMonitor)378 NotificationWindow::_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 
411 void
_LoadAppFilters(BMessage & settings)412 NotificationWindow::_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 
430 void
_LoadGeneralSettings(BMessage & settings)431 NotificationWindow::_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 
448 void
_LoadDisplaySettings(BMessage & settings)449 NotificationWindow::_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