The Battle for Wesnoth 1.21.0-dev
 
Loading...
Searching...
No Matches
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{
38{
39constexpr int ICON_ID = 1007; // just a random number
40constexpr unsigned int WM_TRAYNOTIFY = 32868; // WM_APP+100
41constexpr std::size_t MAX_TITLE_LENGTH = 63; // 64 including the terminating null character
42constexpr std::size_t MAX_MESSAGE_LENGTH = 255; // 256 including the terminating null character
43
44NOTIFYICONDATA* nid = nullptr;
45bool message_reset = false;
46
47namespace 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
55std::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
157bool 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
170void 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
198bool 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
225bool 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:712
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