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