The Battle for Wesnoth  1.19.25+dev
windows_tray_notification.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2013 - 2025
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 "gettext.hpp"
21 #include "sdl/window.hpp"
22 #include "video.hpp" // for get_window
23 
24 //forces to call Unicode winapi functions instead of ASCII (default)
25 #ifndef UNICODE
26 #define UNICODE
27 #endif
28 
29 #define WIN32_LEAN_AND_MEAN
30 
31 // ShellAPI.h should be included after Windows.h only!
32 #include <windows.h>
33 #include <shellapi.h>
34 
36 {
37 namespace implementation
38 {
39 constexpr int ICON_ID = 1007; // just a random number
40 constexpr unsigned int WM_TRAYNOTIFY = 32868; // WM_APP+100
41 constexpr std::size_t MAX_TITLE_LENGTH = 63; // 64 including the terminating null character
42 constexpr std::size_t MAX_MESSAGE_LENGTH = 255; // 256 including the terminating null character
43 
44 NOTIFYICONDATA* nid = nullptr;
45 bool message_reset = false;
46 
47 namespace helper
48 {
50 {
51  auto props = SDL_GetWindowProperties(video::get_window());
52  return static_cast<HWND>(SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr));
53 }
54 
55 std::wstring string_to_wstring(const std::string& string, std::size_t maxlength)
56 {
57  std::u16string u16_string = unicode_cast<std::u16string>(string);
58  if (u16_string.size() > maxlength) {
59  if ((u16_string[maxlength - 1] & 0xDC00) == 0xD800)
60  u16_string.resize(maxlength - 1);
61  else
62  u16_string.resize(maxlength);
63  }
64  return std::wstring(u16_string.begin(), u16_string.end());
65 }
66 
67 } // helper
68 
70 {
71  if (nid == nullptr) {
72  return;
73  }
74 
75  if (!message_reset){
76  Shell_NotifyIcon(NIM_DELETE, nid);
77  delete nid;
78  nid = nullptr;
79  } else {
80  message_reset = false;
81  }
82 }
83 
85 {
86  // getting handle to a 32x32 icon, contained in "WESNOTH_ICON" icon group of wesnoth.exe resources
87  const HMODULE wesnoth_exe = GetModuleHandle(nullptr);
88  if (wesnoth_exe == nullptr) {
89  return false;
90  }
91 
92  const HRSRC group_icon_info = FindResource(wesnoth_exe, TEXT("WESNOTH_ICON"), RT_GROUP_ICON);
93  if (group_icon_info == nullptr) {
94  return false;
95  }
96 
97  HGLOBAL hGlobal = LoadResource(wesnoth_exe, group_icon_info);
98  if (hGlobal == nullptr) {
99  return false;
100  }
101 
102  const PBYTE group_icon_res = static_cast<PBYTE>(LockResource(hGlobal));
103  if (group_icon_res == nullptr) {
104  return false;
105  }
106 
107  const int nID = LookupIconIdFromDirectoryEx(group_icon_res, TRUE, 32, 32, LR_DEFAULTCOLOR);
108  if (nID == 0) {
109  return false;
110  }
111 
112  const HRSRC icon_info = FindResource(wesnoth_exe, MAKEINTRESOURCE(nID), MAKEINTRESOURCE(3));
113  if (icon_info == nullptr) {
114  return false;
115  }
116 
117  hGlobal = LoadResource(wesnoth_exe, icon_info);
118  if (hGlobal == nullptr) {
119  return false;
120  }
121 
122  const PBYTE icon_res = static_cast<PBYTE>(LockResource(hGlobal));
123  if (icon_res == nullptr) {
124  return false;
125  }
126 
127  const HICON icon = CreateIconFromResource(icon_res, SizeofResource(wesnoth_exe, icon_info), TRUE, 0x00030000);
128  if (icon == nullptr) {
129  return false;
130  }
131 
132  const HWND window = helper::get_window_handle();
133  if (window == nullptr) {
134  return false;
135  }
136 
137  const std::wstring& wtip = helper::string_to_wstring(_("The Battle for Wesnoth"), MAX_TITLE_LENGTH);
138 
139  // filling notification structure
140  nid = new NOTIFYICONDATA;
141  memset(nid, 0, sizeof(*nid));
142  nid->cbSize = NOTIFYICONDATA_V2_SIZE;
143  nid->hWnd = window;
144  nid->uFlags = NIF_ICON | NIF_TIP | NIF_MESSAGE;
145  nid->dwInfoFlags = NIIF_USER;
146  nid->uVersion = NOTIFYICON_VERSION;
147  nid->uCallbackMessage = WM_TRAYNOTIFY;
148  nid->uID = ICON_ID;
149  nid->hIcon = icon;
150  nid->hBalloonIcon = icon;
151  lstrcpyW(nid->szTip, wtip.c_str());
152 
153  // creating icon notification
154  return Shell_NotifyIcon(NIM_ADD, nid) != FALSE;
155 }
156 
157 bool set_tray_message(const std::string& title, const std::string& message)
158 {
159  // prevents deletion of icon when resetting already existing notification
160  message_reset = (nid->uFlags & NIF_INFO) != 0;
161 
162  nid->uFlags |= NIF_INFO;
163  lstrcpyW(nid->szInfoTitle, helper::string_to_wstring(title, MAX_TITLE_LENGTH).c_str());
164  lstrcpyW(nid->szInfo, helper::string_to_wstring(message, MAX_MESSAGE_LENGTH).c_str());
165 
166  // setting notification
167  return Shell_NotifyIcon(NIM_MODIFY, nid) != FALSE;
168 }
169 
170 void adjust_length(std::string& title, std::string& message)
171 {
172  static const int ELIPSIS_LENGTH = 3;
173 
174  // limitations set by winapi
175  if (title.length() > MAX_TITLE_LENGTH) {
176  utils::ellipsis_truncate(title, MAX_TITLE_LENGTH - ELIPSIS_LENGTH);
177  }
178  if (message.length() > MAX_MESSAGE_LENGTH) {
179  utils::ellipsis_truncate(message, MAX_MESSAGE_LENGTH - ELIPSIS_LENGTH);
180  }
181 }
182 
184 {
185  const HWND window = helper::get_window_handle();
186  if (window == nullptr) {
187  return;
188  }
189 
190  if (IsIconic(window)) {
191  ShowWindow(window, SW_RESTORE);
192  }
193  SetForegroundWindow(window);
194 }
195 
196 } // implementation
197 
198 bool show(std::string title, std::string message)
199 {
200  implementation::adjust_length(title, message);
201 
202  const bool tray_icon_exist = implementation::nid != nullptr;
203  if (!tray_icon_exist) {
204  const bool tray_icon_created = implementation::create_tray_icon();
205  if (!tray_icon_created) {
206  const bool memory_allocated = implementation::nid != nullptr;
207  if (memory_allocated) {
209  }
210  return false;
211  }
212  }
213 
214  // at this point tray icon was just created or already existed before, so it's safe to call `set_tray_message`
215 
216  const bool result = implementation::set_tray_message(title, message);
217  // the `destroy_tray_icon` will be called by event only if `set_tray_message` succeeded
218  // if it doesn't succeed, we have to call `destroy_tray_icon` manually
219  if (!result) {
221  }
222  return result;
223 }
224 
225 bool message_hook(const MSG& msg)
226 {
227  switch(msg.lParam) {
228  case NIN_BALLOONUSERCLICK:
231  return true;
232 
233  case NIN_BALLOONTIMEOUT:
235  return true;
236 
237  // Scenario: More than one notification arrives before the time-out triggers the tray icon destruction.
238  // Problem: Events seem to be triggered differently in SDL 2.0. For the example of two notifications arriving at once:
239  // 1. Balloon created for first notification
240  // 2. Balloon created for second notification (message_reset set to true because of first notification already present)
241  // 3. Balloon time-out for first notification (destroy_tray_icon skips tray icon destruction because of message_reset flag)
242  // 4. SDL 1.2: Balloon time-out for second notification (destroy_tray_icon destroys tray icon)
243  // SDL 2.0: Balloon time-out for second notification event is never received (tray icon remains indefinitely)
244  // 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).
245  // 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'.
246  // 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.
247  // Drawback: The tray icon can still get 'stuck' if the user does not move the mouse cursor over the tray icon.
248  // 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.
249  case 0x0200:
251  return false;
252  }
253 
255  return true;
256 
257  default:
258  return false;
259  }
260 }
261 
262 } // windows_tray_notification
static std::string _(const char *str)
Definition: gettext.hpp:100
Contains the implementation details for lexical_cast and shouldn't be used directly.
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:690
static void msg(const char *act, debug_info &i, const char *to="", const char *result="")
Definition: debugger.cpp:109
std::wstring string_to_wstring(const std::string &string, std::size_t maxlength)
void adjust_length(std::string &title, std::string &message)
bool set_tray_message(const std::string &title, const std::string &message)
bool show(std::string title, std::string message)
Displays a tray notification.
bool message_hook(const MSG &msg)
Frees resources when a notification disappears, switches user to the wesnoth window if the notificati...
Contains a wrapper class for the SDL_Window class.
struct tagMSG MSG