desktop_notification_controller.cc 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. #define NOMINMAX
  2. #define WIN32_LEAN_AND_MEAN
  3. #include "brightray/browser/win/win32_desktop_notifications/desktop_notification_controller.h"
  4. #include <windowsx.h>
  5. #include <algorithm>
  6. #include <vector>
  7. #include "brightray/browser/win/win32_desktop_notifications/common.h"
  8. #include "brightray/browser/win/win32_desktop_notifications/toast.h"
  9. using std::make_shared;
  10. using std::shared_ptr;
  11. namespace brightray {
  12. HBITMAP CopyBitmap(HBITMAP bitmap) {
  13. HBITMAP ret = NULL;
  14. BITMAP bm;
  15. if (bitmap && GetObject(bitmap, sizeof(bm), &bm)) {
  16. HDC hdc_screen = GetDC(NULL);
  17. ret = CreateCompatibleBitmap(hdc_screen, bm.bmWidth, bm.bmHeight);
  18. ReleaseDC(NULL, hdc_screen);
  19. if (ret) {
  20. HDC hdc_src = CreateCompatibleDC(NULL);
  21. HDC hdc_dst = CreateCompatibleDC(NULL);
  22. SelectBitmap(hdc_src, bitmap);
  23. SelectBitmap(hdc_dst, ret);
  24. BitBlt(hdc_dst, 0, 0, bm.bmWidth, bm.bmHeight, hdc_src, 0, 0, SRCCOPY);
  25. DeleteDC(hdc_dst);
  26. DeleteDC(hdc_src);
  27. }
  28. }
  29. return ret;
  30. }
  31. HINSTANCE DesktopNotificationController::RegisterWndClasses() {
  32. // We keep a static `module` variable which serves a dual purpose:
  33. // 1. Stores the HINSTANCE where the window classes are registered,
  34. // which can be passed to `CreateWindow`
  35. // 2. Indicates whether we already attempted the registration so that
  36. // we don't do it twice (we don't retry even if registration fails,
  37. // as there is no point).
  38. static HMODULE module = NULL;
  39. if (!module) {
  40. if (GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
  41. GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
  42. reinterpret_cast<LPCWSTR>(&RegisterWndClasses),
  43. &module)) {
  44. Toast::Register(module);
  45. WNDCLASSEX wc = {sizeof(wc)};
  46. wc.lpfnWndProc = &WndProc;
  47. wc.lpszClassName = class_name_;
  48. wc.cbWndExtra = sizeof(DesktopNotificationController*);
  49. wc.hInstance = module;
  50. RegisterClassEx(&wc);
  51. }
  52. }
  53. return module;
  54. }
  55. DesktopNotificationController::DesktopNotificationController(
  56. unsigned maximum_toasts) {
  57. instances_.reserve(maximum_toasts);
  58. }
  59. DesktopNotificationController::~DesktopNotificationController() {
  60. for (auto&& inst : instances_)
  61. DestroyToast(inst);
  62. if (hwnd_controller_)
  63. DestroyWindow(hwnd_controller_);
  64. ClearAssets();
  65. }
  66. LRESULT CALLBACK DesktopNotificationController::WndProc(HWND hwnd,
  67. UINT message,
  68. WPARAM wparam,
  69. LPARAM lparam) {
  70. switch (message) {
  71. case WM_CREATE: {
  72. auto& cs = reinterpret_cast<const CREATESTRUCT*&>(lparam);
  73. SetWindowLongPtr(hwnd, 0, (LONG_PTR)cs->lpCreateParams);
  74. } break;
  75. case WM_TIMER:
  76. if (wparam == TimerID_Animate) {
  77. Get(hwnd)->AnimateAll();
  78. }
  79. return 0;
  80. case WM_DISPLAYCHANGE: {
  81. auto inst = Get(hwnd);
  82. inst->ClearAssets();
  83. inst->AnimateAll();
  84. } break;
  85. case WM_SETTINGCHANGE:
  86. if (wparam == SPI_SETWORKAREA) {
  87. Get(hwnd)->AnimateAll();
  88. }
  89. break;
  90. }
  91. return DefWindowProc(hwnd, message, wparam, lparam);
  92. }
  93. void DesktopNotificationController::StartAnimation() {
  94. _ASSERT(hwnd_controller_);
  95. if (!is_animating_ && hwnd_controller_) {
  96. // NOTE: 15ms is shorter than what we'd need for 60 fps, but since
  97. // the timer is not accurate we must request a higher frame rate
  98. // to get at least 60
  99. SetTimer(hwnd_controller_, TimerID_Animate, 15, nullptr);
  100. is_animating_ = true;
  101. }
  102. }
  103. HFONT DesktopNotificationController::GetCaptionFont() {
  104. InitializeFonts();
  105. return caption_font_;
  106. }
  107. HFONT DesktopNotificationController::GetBodyFont() {
  108. InitializeFonts();
  109. return body_font_;
  110. }
  111. void DesktopNotificationController::InitializeFonts() {
  112. if (!body_font_) {
  113. NONCLIENTMETRICS metrics = {sizeof(metrics)};
  114. if (SystemParametersInfo(SPI_GETNONCLIENTMETRICS, 0, &metrics, 0)) {
  115. auto base_height = metrics.lfMessageFont.lfHeight;
  116. HDC hdc = GetDC(NULL);
  117. auto base_dpi_y = GetDeviceCaps(hdc, LOGPIXELSY);
  118. ReleaseDC(NULL, hdc);
  119. ScreenMetrics scr;
  120. metrics.lfMessageFont.lfHeight =
  121. (LONG)ScaleForDpi(base_height * 1.1f, scr.dpi_y, base_dpi_y);
  122. body_font_ = CreateFontIndirect(&metrics.lfMessageFont);
  123. if (caption_font_)
  124. DeleteFont(caption_font_);
  125. metrics.lfMessageFont.lfHeight =
  126. (LONG)ScaleForDpi(base_height * 1.4f, scr.dpi_y, base_dpi_y);
  127. caption_font_ = CreateFontIndirect(&metrics.lfMessageFont);
  128. }
  129. }
  130. }
  131. void DesktopNotificationController::ClearAssets() {
  132. if (caption_font_) {
  133. DeleteFont(caption_font_);
  134. caption_font_ = NULL;
  135. }
  136. if (body_font_) {
  137. DeleteFont(body_font_);
  138. body_font_ = NULL;
  139. }
  140. }
  141. void DesktopNotificationController::AnimateAll() {
  142. // NOTE: This function refreshes position and size of all toasts according
  143. // to all current conditions. Animation time is only one of the variables
  144. // influencing them. Screen resolution is another.
  145. bool keep_animating = false;
  146. if (!instances_.empty()) {
  147. RECT work_area;
  148. if (SystemParametersInfo(SPI_GETWORKAREA, 0, &work_area, 0)) {
  149. ScreenMetrics metrics;
  150. POINT origin = {work_area.right,
  151. work_area.bottom - metrics.Y(toast_margin_)};
  152. auto hdwp = BeginDeferWindowPos(static_cast<int>(instances_.size()));
  153. for (auto&& inst : instances_) {
  154. if (!inst.hwnd)
  155. continue;
  156. auto notification = Toast::Get(inst.hwnd);
  157. hdwp = notification->Animate(hdwp, origin);
  158. if (!hdwp)
  159. break;
  160. keep_animating |= notification->IsAnimationActive();
  161. }
  162. if (hdwp)
  163. EndDeferWindowPos(hdwp);
  164. }
  165. }
  166. if (!keep_animating) {
  167. _ASSERT(hwnd_controller_);
  168. if (hwnd_controller_)
  169. KillTimer(hwnd_controller_, TimerID_Animate);
  170. is_animating_ = false;
  171. }
  172. // Purge dismissed notifications and collapse the stack between
  173. // items which are highlighted
  174. if (!instances_.empty()) {
  175. auto is_alive = [](ToastInstance& inst) {
  176. return inst.hwnd && IsWindowVisible(inst.hwnd);
  177. };
  178. auto is_highlighted = [](ToastInstance& inst) {
  179. return inst.hwnd && Toast::Get(inst.hwnd)->IsHighlighted();
  180. };
  181. for (auto it = instances_.begin();; ++it) {
  182. // find next highlighted item
  183. auto it2 = find_if(it, instances_.end(), is_highlighted);
  184. // collapse the stack in front of the highlighted item
  185. it = stable_partition(it, it2, is_alive);
  186. // purge the dead items
  187. for_each(it, it2, [this](auto&& inst) { DestroyToast(inst); });
  188. if (it2 == instances_.end()) {
  189. instances_.erase(it, it2);
  190. break;
  191. }
  192. it = move(it2);
  193. }
  194. }
  195. // Set new toast positions
  196. if (!instances_.empty()) {
  197. ScreenMetrics metrics;
  198. auto margin = metrics.Y(toast_margin_);
  199. int target_pos = 0;
  200. for (auto&& inst : instances_) {
  201. if (inst.hwnd) {
  202. auto toast = Toast::Get(inst.hwnd);
  203. if (toast->IsHighlighted())
  204. target_pos = toast->GetVerticalPosition();
  205. else
  206. toast->SetVerticalPosition(target_pos);
  207. target_pos += toast->GetHeight() + margin;
  208. }
  209. }
  210. }
  211. // Create new toasts from the queue
  212. CheckQueue();
  213. }
  214. DesktopNotificationController::Notification
  215. DesktopNotificationController::AddNotification(std::wstring caption,
  216. std::wstring body_text,
  217. HBITMAP image) {
  218. NotificationLink data(this);
  219. data->caption = move(caption);
  220. data->body_text = move(body_text);
  221. data->image = CopyBitmap(image);
  222. // Enqueue new notification
  223. Notification ret{*queue_.insert(queue_.end(), move(data))};
  224. CheckQueue();
  225. return ret;
  226. }
  227. void DesktopNotificationController::CloseNotification(
  228. Notification& notification) {
  229. // Remove it from the queue
  230. auto it = find(queue_.begin(), queue_.end(), notification.data_);
  231. if (it != queue_.end()) {
  232. queue_.erase(it);
  233. this->OnNotificationClosed(notification);
  234. return;
  235. }
  236. // Dismiss active toast
  237. auto hwnd = GetToast(notification.data_.get());
  238. if (hwnd) {
  239. auto toast = Toast::Get(hwnd);
  240. toast->Dismiss();
  241. }
  242. }
  243. void DesktopNotificationController::CheckQueue() {
  244. while (instances_.size() < instances_.capacity() && !queue_.empty()) {
  245. CreateToast(move(queue_.front()));
  246. queue_.pop_front();
  247. }
  248. }
  249. void DesktopNotificationController::CreateToast(NotificationLink&& data) {
  250. auto hinstance = RegisterWndClasses();
  251. auto hwnd = Toast::Create(hinstance, data);
  252. if (hwnd) {
  253. int toast_pos = 0;
  254. if (!instances_.empty()) {
  255. auto& item = instances_.back();
  256. _ASSERT(item.hwnd);
  257. ScreenMetrics scr;
  258. auto toast = Toast::Get(item.hwnd);
  259. toast_pos = toast->GetVerticalPosition() + toast->GetHeight() +
  260. scr.Y(toast_margin_);
  261. }
  262. instances_.push_back({hwnd, move(data)});
  263. if (!hwnd_controller_) {
  264. // NOTE: We cannot use a message-only window because we need to
  265. // receive system notifications
  266. hwnd_controller_ = CreateWindow(class_name_, nullptr, 0, 0, 0, 0, 0, NULL,
  267. NULL, hinstance, this);
  268. }
  269. auto toast = Toast::Get(hwnd);
  270. toast->PopUp(toast_pos);
  271. }
  272. }
  273. HWND DesktopNotificationController::GetToast(
  274. const NotificationData* data) const {
  275. auto it =
  276. find_if(instances_.cbegin(), instances_.cend(), [data](auto&& inst) {
  277. if (!inst.hwnd)
  278. return false;
  279. auto toast = Toast::Get(inst.hwnd);
  280. return data == toast->GetNotification().get();
  281. });
  282. return (it != instances_.cend()) ? it->hwnd : NULL;
  283. }
  284. void DesktopNotificationController::DestroyToast(ToastInstance& inst) {
  285. if (inst.hwnd) {
  286. auto data = Toast::Get(inst.hwnd)->GetNotification();
  287. DestroyWindow(inst.hwnd);
  288. inst.hwnd = NULL;
  289. Notification notification(data);
  290. OnNotificationClosed(notification);
  291. }
  292. }
  293. DesktopNotificationController::Notification::Notification(
  294. const shared_ptr<NotificationData>& data)
  295. : data_(data) {
  296. _ASSERT(data != nullptr);
  297. }
  298. bool DesktopNotificationController::Notification::operator==(
  299. const Notification& other) const {
  300. return data_ == other.data_;
  301. }
  302. void DesktopNotificationController::Notification::Close() {
  303. // No business calling this when not pointing to a valid instance
  304. _ASSERT(data_);
  305. if (data_->controller)
  306. data_->controller->CloseNotification(*this);
  307. }
  308. void DesktopNotificationController::Notification::Set(std::wstring caption,
  309. std::wstring body_text,
  310. HBITMAP image) {
  311. // No business calling this when not pointing to a valid instance
  312. _ASSERT(data_);
  313. // Do nothing when the notification has been closed
  314. if (!data_->controller)
  315. return;
  316. if (data_->image)
  317. DeleteBitmap(data_->image);
  318. data_->caption = move(caption);
  319. data_->body_text = move(body_text);
  320. data_->image = CopyBitmap(image);
  321. auto hwnd = data_->controller->GetToast(data_.get());
  322. if (hwnd) {
  323. auto toast = Toast::Get(hwnd);
  324. toast->ResetContents();
  325. }
  326. // Change of contents can affect size and position of all toasts
  327. data_->controller->StartAnimation();
  328. }
  329. DesktopNotificationController::NotificationLink::NotificationLink(
  330. DesktopNotificationController* controller)
  331. : shared_ptr(make_shared<NotificationData>()) {
  332. get()->controller = controller;
  333. }
  334. DesktopNotificationController::NotificationLink::~NotificationLink() {
  335. auto p = get();
  336. if (p)
  337. p->controller = nullptr;
  338. }
  339. } // namespace brightray