The Battle for Wesnoth  1.15.0-dev
windows_tray_notification.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2013 - 2018 by Maxim Biro <nurupo.contributions@gmail.com>
3  Part of the Battle for Wesnoth Project http://www.wesnoth.org/
4 
5  This program is free software; you can redistribute it and/or modify
6  it under the terms of the GNU General Public License as published by
7  the Free Software Foundation; either version 2 of the License, or
8  (at your option) any later version.
9  This program is distributed in the hope that it will be useful,
10  but WITHOUT ANY WARRANTY.
11 
12  See the COPYING file for more details.
13 */
14 
16 
17 #include <SDL_syswm.h>
18 
19 #include "gettext.hpp"
22 #include "sdl/window.hpp"
23 #include "video.hpp" // CVideo class holds the window -> SDL_Window object which contains the handle of the main window
24 
25 NOTIFYICONDATA* windows_tray_notification::nid = nullptr;
27 
29 {
30  if (nid == nullptr) {
31  return;
32  }
33 
34  if (!message_reset){
35  Shell_NotifyIcon(NIM_DELETE, nid);
36  delete nid;
37  nid = nullptr;
38  } else {
39  message_reset = false;
40  }
41 }
42 
44 {
45  if (event.syswm.msg->msg.win.msg != WM_TRAYNOTIFY) {
46  return;
47  }
48 
49  if (event.syswm.msg->msg.win.lParam == NIN_BALLOONUSERCLICK) {
52  } else if (event.syswm.msg->msg.win.lParam == NIN_BALLOONTIMEOUT) {
54  }
55  // Scenario: More than one notification arrives before the time-out triggers the tray icon destruction.
56  // Problem: Events seem to be triggered differently in SDL 2.0. For the example of two notifications arriving at once:
57  // 1. Balloon created for first notification
58  // 2. Balloon created for second notification (message_reset set to true because of first notification already present)
59  // 3. Balloon time-out for first notification (destroy_tray_icon skips tray icon destruction because of message_reset flag)
60  // 4. SDL 1.2: Balloon time-out for second notification (destroy_tray_icon destroys tray icon)
61  // SDL 2.0: Balloon time-out for second notification event is never received (tray icon remains indefinitely)
62  // This results in the tray icon being 'stuck' until the user quits Wesnoth *and* hovers over the tray icon (and is only then killed off by the OS).
63  // As a less-than-ideal-but-better-than-nothing-solution, call destroy_tray_icon when the user hovers mouse cursor over the tray icon. At least then the tray is 'reset'.
64  // I could not find the matching definition for 0x0200 in the headers, but this message value is received when the mouse cursor is over the tray icon.
65  // Drawback: The tray icon can still get 'stuck' if the user does not move the mouse cursor over the tray icon.
66  // Also, accidental destruction of the tray icon can occur if the user moves the mouse cursor over the tray icon before the balloon for a single notification has expired.
67  else if (event.syswm.msg->msg.win.lParam == 0x0200 && !message_reset) {
69  }
70 }
71 
73 {
74  // getting handle to a 32x32 icon, contained in "WESNOTH_ICON" icon group of wesnoth.exe resources
75  const HMODULE wesnoth_exe = GetModuleHandle(nullptr);
76  if (wesnoth_exe == nullptr) {
77  return false;
78  }
79 
80  const HRSRC group_icon_info = FindResource(wesnoth_exe, TEXT("WESNOTH_ICON"), RT_GROUP_ICON);
81  if (group_icon_info == nullptr) {
82  return false;
83  }
84 
85  HGLOBAL hGlobal = LoadResource(wesnoth_exe, group_icon_info);
86  if (hGlobal == nullptr) {
87  return false;
88  }
89 
90  const PBYTE group_icon_res = static_cast<PBYTE>(LockResource(hGlobal));
91  if (group_icon_res == nullptr) {
92  return false;
93  }
94 
95  const int nID = LookupIconIdFromDirectoryEx(group_icon_res, TRUE, 32, 32, LR_DEFAULTCOLOR);
96  if (nID == 0) {
97  return false;
98  }
99 
100  const HRSRC icon_info = FindResource(wesnoth_exe, MAKEINTRESOURCE(nID), MAKEINTRESOURCE(3));
101  if (icon_info == nullptr) {
102  return false;
103  }
104 
105  hGlobal = LoadResource(wesnoth_exe, icon_info);
106  if (hGlobal == nullptr) {
107  return false;
108  }
109 
110  const PBYTE icon_res = static_cast<PBYTE>(LockResource(hGlobal));
111  if (icon_res == nullptr) {
112  return false;
113  }
114 
115  const HICON icon = CreateIconFromResource(icon_res, SizeofResource(wesnoth_exe, icon_info), TRUE, 0x00030000);
116  if (icon == nullptr) {
117  return false;
118  }
119 
120  const HWND window = get_window_handle();
121  if (window == nullptr) {
122  return false;
123  }
124 
125  const std::wstring& wtip = string_to_wstring(_("The Battle for Wesnoth"), MAX_TITLE_LENGTH);
126 
127  // filling notification structure
128  nid = new NOTIFYICONDATA;
129  memset(nid, 0, sizeof(*nid));
130  nid->cbSize = NOTIFYICONDATA_V2_SIZE;
131  nid->hWnd = window;
132  nid->uFlags = NIF_ICON | NIF_TIP | NIF_MESSAGE;
133  nid->dwInfoFlags = NIIF_USER;
134  nid->uVersion = NOTIFYICON_VERSION;
135  nid->uCallbackMessage = WM_TRAYNOTIFY;
136  nid->uID = ICON_ID;
137  nid->hIcon = icon;
138  nid->hBalloonIcon = icon;
139  lstrcpyW(nid->szTip, wtip.c_str());
140 
141  // creating icon notification
142  return Shell_NotifyIcon(NIM_ADD, nid) != FALSE;
143 }
144 
145 bool windows_tray_notification::set_tray_message(const std::string& title, const std::string& message)
146 {
147  // prevents deletion of icon when resetting already existing notification
148  message_reset = (nid->uFlags & NIF_INFO) != 0;
149 
150  nid->uFlags |= NIF_INFO;
151  lstrcpyW(nid->szInfoTitle, string_to_wstring(title, MAX_TITLE_LENGTH).c_str());
152  lstrcpyW(nid->szInfo, string_to_wstring(message, MAX_MESSAGE_LENGTH).c_str());
153 
154  // setting notification
155  return Shell_NotifyIcon(NIM_MODIFY, nid) != FALSE;
156 }
157 
158 void windows_tray_notification::adjust_length(std::string& title, std::string& message)
159 {
160  static const int ELIPSIS_LENGTH = 3;
161 
162  // limitations set by winapi
163  if (title.length() > MAX_TITLE_LENGTH) {
164  utils::ellipsis_truncate(title, MAX_TITLE_LENGTH - ELIPSIS_LENGTH);
165  }
166  if (message.length() > MAX_MESSAGE_LENGTH) {
167  utils::ellipsis_truncate(message, MAX_MESSAGE_LENGTH - ELIPSIS_LENGTH);
168  }
169 }
170 
172 {
173  SDL_SysWMinfo wmInfo;
174  SDL_VERSION(&wmInfo.version);
176  // SDL 1.2 keeps track of window handles internally whereas SDL 2.0 allows the caller control over which window to use
177  if (!window || SDL_GetWindowWMInfo (static_cast<SDL_Window *> (*window), &wmInfo) != SDL_TRUE) {
178  return nullptr;
179  }
180 
181  return wmInfo.info.win.window;
182 }
183 
185 {
186  const HWND window = get_window_handle();
187  if (window == nullptr) {
188  return;
189  }
190 
191  if (IsIconic(window)) {
192  ShowWindow(window, SW_RESTORE);
193  }
194  SetForegroundWindow(window);
195 }
196 
197 std::wstring windows_tray_notification::string_to_wstring(const std::string& string, std::size_t maxlength)
198 {
199  std::u16string u16_string = unicode_cast<std::u16string>(string);
200  if(u16_string.size() > maxlength) {
201  if((u16_string[maxlength-1] & 0xDC00) == 0xD800)
202  u16_string.resize(maxlength - 1);
203  else
204  u16_string.resize(maxlength);
205  }
206  return std::wstring(u16_string.begin(), u16_string.end());
207 }
208 
209 bool windows_tray_notification::show(std::string title, std::string message)
210 {
211  adjust_length(title, message);
212 
213  const bool tray_icon_exist = nid != nullptr;
214  if (!tray_icon_exist) {
215  const bool tray_icon_created = create_tray_icon();
216  if (!tray_icon_created) {
217  const bool memory_allocated = nid != nullptr;
218  if (memory_allocated) {
220  }
221  return false;
222  }
223  }
224 
225  // at this point tray icon was just created or already existed before, so it's safe to call `set_tray_message`
226 
227  const bool result = set_tray_message(title, message);
228  // the `destroy_tray_icon` will be called by event only if `set_tray_message` succeeded
229  // if it doesn't succeed, we have to call `destroy_tray_icon` manually
230  if (!result) {
232  }
233  return result;
234 }
static const unsigned int WM_TRAYNOTIFY
static const std::size_t MAX_TITLE_LENGTH
#define NIN_BALLOONTIMEOUT
ucs4_convert_impl::enableif< TD, typename TS::value_type >::type unicode_cast(const TS &source)
void ellipsis_truncate(std::string &str, const std::size_t size)
Truncates a string to a given utf-8 character count and then appends an ellipsis. ...
static CVideo & get_singleton()
Definition: video.hpp:48
static bool set_tray_message(const std::string &title, const std::string &message)
static const std::size_t MAX_MESSAGE_LENGTH
-file util.hpp
#define NIIF_USER
static UNUSEDNOWARN std::string _(const char *str)
Definition: gettext.hpp:89
The wrapper class for the SDL_Window class.
Definition: window.hpp:45
static std::wstring string_to_wstring(const std::string &string, std::size_t maxlength)
#define NIN_BALLOONUSERCLICK
sdl::window * get_window()
Returns a pointer to the underlying SDL window.
Definition: video.cpp:318
window(const window &)=delete
static bool show(std::string title, std::string message)
Displays a tray notification.
Contains a wrapper class for the SDL_Window class.
static void adjust_length(std::string &title, std::string &message)
static void handle_system_event(const SDL_Event &event)
Frees resources when a notification disappears, switches user to the wesnoth window if the notificati...