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 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 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 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 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 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 144 NotificationWindow::FrameResized(float width, float height) 145 { 146 SetPosition(); 147 } 148 149 150 void 151 NotificationWindow::ScreenChanged(BRect frame, color_space mode) 152 { 153 SetPosition(); 154 } 155 156 157 void 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 259 NotificationWindow::IconSize() 260 { 261 return fIconSize; 262 } 263 264 265 int32 266 NotificationWindow::Timeout() 267 { 268 return fTimeout; 269 } 270 271 272 float 273 NotificationWindow::Width() 274 { 275 return fWidth; 276 } 277 278 279 void 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 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 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 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 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 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