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