summaryrefslogtreecommitdiff
path: root/contrib/SDL-3.2.8/src/tray/windows
diff options
context:
space:
mode:
Diffstat (limited to 'contrib/SDL-3.2.8/src/tray/windows')
-rw-r--r--contrib/SDL-3.2.8/src/tray/windows/SDL_tray.c690
1 files changed, 690 insertions, 0 deletions
diff --git a/contrib/SDL-3.2.8/src/tray/windows/SDL_tray.c b/contrib/SDL-3.2.8/src/tray/windows/SDL_tray.c
new file mode 100644
index 0000000..18008ee
--- /dev/null
+++ b/contrib/SDL-3.2.8/src/tray/windows/SDL_tray.c
@@ -0,0 +1,690 @@
1/*
2 Simple DirectMedia Layer
3 Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
4
5 This software is provided 'as-is', without any express or implied
6 warranty. In no event will the authors be held liable for any damages
7 arising from the use of this software.
8
9 Permission is granted to anyone to use this software for any purpose,
10 including commercial applications, and to alter it and redistribute it
11 freely, subject to the following restrictions:
12
13 1. The origin of this software must not be misrepresented; you must not
14 claim that you wrote the original software. If you use this software
15 in a product, an acknowledgment in the product documentation would be
16 appreciated but is not required.
17 2. Altered source versions must be plainly marked as such, and must not be
18 misrepresented as being the original software.
19 3. This notice may not be removed or altered from any source distribution.
20*/
21
22#include "SDL_internal.h"
23
24#include "../SDL_tray_utils.h"
25#include "../../core/windows/SDL_windows.h"
26#include "../../video/windows/SDL_windowswindow.h"
27
28#include <windowsx.h>
29#include <shellapi.h>
30
31#include "../../video/windows/SDL_surface_utils.h"
32
33#ifndef NOTIFYICON_VERSION_4
34#define NOTIFYICON_VERSION_4 4
35#endif
36#ifndef NIF_SHOWTIP
37#define NIF_SHOWTIP 0x00000080
38#endif
39
40#define WM_TRAYICON (WM_USER + 1)
41
42struct SDL_TrayMenu {
43 HMENU hMenu;
44
45 int nEntries;
46 SDL_TrayEntry **entries;
47
48 SDL_Tray *parent_tray;
49 SDL_TrayEntry *parent_entry;
50};
51
52struct SDL_TrayEntry {
53 SDL_TrayMenu *parent;
54 UINT_PTR id;
55
56 char label_cache[4096];
57 SDL_TrayEntryFlags flags;
58 SDL_TrayCallback callback;
59 void *userdata;
60 SDL_TrayMenu *submenu;
61};
62
63struct SDL_Tray {
64 NOTIFYICONDATAW nid;
65 HWND hwnd;
66 HICON icon;
67 SDL_TrayMenu *menu;
68};
69
70static UINT_PTR get_next_id(void)
71{
72 static UINT_PTR next_id = 0;
73 return ++next_id;
74}
75
76static SDL_TrayEntry *find_entry_in_menu(SDL_TrayMenu *menu, UINT_PTR id)
77{
78 for (int i = 0; i < menu->nEntries; i++) {
79 SDL_TrayEntry *entry = menu->entries[i];
80
81 if (entry->id == id) {
82 return entry;
83 }
84
85 if (entry->submenu) {
86 SDL_TrayEntry *e = find_entry_in_menu(entry->submenu, id);
87
88 if (e) {
89 return e;
90 }
91 }
92 }
93
94 return NULL;
95}
96
97static SDL_TrayEntry *find_entry_with_id(SDL_Tray *tray, UINT_PTR id)
98{
99 if (!tray->menu) {
100 return NULL;
101 }
102
103 return find_entry_in_menu(tray->menu, id);
104}
105
106LRESULT CALLBACK TrayWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
107 SDL_Tray *tray = (SDL_Tray *) GetWindowLongPtr(hwnd, GWLP_USERDATA);
108 SDL_TrayEntry *entry = NULL;
109
110 if (!tray) {
111 return DefWindowProc(hwnd, uMsg, wParam, lParam);
112 }
113
114 switch (uMsg) {
115 case WM_TRAYICON:
116 if (LOWORD(lParam) == WM_CONTEXTMENU || LOWORD(lParam) == WM_LBUTTONUP) {
117 SetForegroundWindow(hwnd);
118
119 if (tray->menu) {
120 TrackPopupMenu(tray->menu->hMenu, TPM_BOTTOMALIGN | TPM_RIGHTALIGN, GET_X_LPARAM(wParam), GET_Y_LPARAM(wParam), 0, hwnd, NULL);
121 }
122 }
123 break;
124
125 case WM_COMMAND:
126 entry = find_entry_with_id(tray, LOWORD(wParam));
127
128 if (entry && (entry->flags & SDL_TRAYENTRY_CHECKBOX)) {
129 SDL_SetTrayEntryChecked(entry, !SDL_GetTrayEntryChecked(entry));
130 }
131
132 if (entry && entry->callback) {
133 entry->callback(entry->userdata, entry);
134 }
135 break;
136
137 case WM_SETTINGCHANGE:
138 if (wParam == 0 && lParam != 0 && SDL_wcscmp((wchar_t *)lParam, L"ImmersiveColorSet") == 0) {
139 WIN_UpdateDarkModeForHWND(hwnd);
140 }
141 break;
142
143 default:
144 return DefWindowProc(hwnd, uMsg, wParam, lParam);
145 }
146 return 0;
147}
148
149static void DestroySDLMenu(SDL_TrayMenu *menu)
150{
151 for (int i = 0; i < menu->nEntries; i++) {
152 if (menu->entries[i] && menu->entries[i]->submenu) {
153 DestroySDLMenu(menu->entries[i]->submenu);
154 }
155 SDL_free(menu->entries[i]);
156 }
157 SDL_free(menu->entries);
158 DestroyMenu(menu->hMenu);
159 SDL_free(menu);
160}
161
162static wchar_t *escape_label(const char *in)
163{
164 const char *c;
165 char *c2;
166 int len = 0;
167
168 for (c = in; *c; c++) {
169 len += (*c == '&') ? 2 : 1;
170 }
171
172 char *escaped = (char *)SDL_malloc(SDL_strlen(in) + len + 1);
173 if (!escaped) {
174 return NULL;
175 }
176
177 for (c = in, c2 = escaped; *c;) {
178 if (*c == '&') {
179 *c2++ = *c;
180 }
181
182 *c2++ = *c++;
183 }
184
185 *c2 = '\0';
186
187 wchar_t *out = WIN_UTF8ToStringW(escaped);
188 SDL_free(escaped);
189
190 return out;
191}
192
193static HICON load_default_icon()
194{
195 HINSTANCE hInstance = GetModuleHandle(NULL);
196 if (!hInstance) {
197 return LoadIcon(NULL, IDI_APPLICATION);
198 }
199
200 const char *hint = SDL_GetHint(SDL_HINT_WINDOWS_INTRESOURCE_ICON_SMALL);
201 if (hint && *hint) {
202 HICON icon = LoadIcon(hInstance, MAKEINTRESOURCE(SDL_atoi(hint)));
203 return icon ? icon : LoadIcon(NULL, IDI_APPLICATION);
204 }
205
206 hint = SDL_GetHint(SDL_HINT_WINDOWS_INTRESOURCE_ICON);
207 if (hint && *hint) {
208 HICON icon = LoadIcon(hInstance, MAKEINTRESOURCE(SDL_atoi(hint)));
209 return icon ? icon : LoadIcon(NULL, IDI_APPLICATION);
210 }
211
212 return LoadIcon(NULL, IDI_APPLICATION);
213}
214
215void SDL_UpdateTrays(void)
216{
217}
218
219SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip)
220{
221 if (!SDL_IsMainThread()) {
222 SDL_SetError("This function should be called on the main thread");
223 return NULL;
224 }
225
226 SDL_Tray *tray = (SDL_Tray *)SDL_calloc(1, sizeof(*tray));
227
228 if (!tray) {
229 return NULL;
230 }
231
232 tray->menu = NULL;
233 tray->hwnd = CreateWindowEx(0, TEXT("Message"), NULL, 0, 0, 0, 0, 0, HWND_MESSAGE, NULL, NULL, NULL);
234 SetWindowLongPtr(tray->hwnd, GWLP_WNDPROC, (LONG_PTR) TrayWindowProc);
235
236 WIN_UpdateDarkModeForHWND(tray->hwnd);
237
238 SDL_zero(tray->nid);
239 tray->nid.cbSize = sizeof(NOTIFYICONDATAW);
240 tray->nid.hWnd = tray->hwnd;
241 tray->nid.uID = (UINT) get_next_id();
242 tray->nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP | NIF_SHOWTIP;
243 tray->nid.uCallbackMessage = WM_TRAYICON;
244 tray->nid.uVersion = NOTIFYICON_VERSION_4;
245 wchar_t *tooltipw = WIN_UTF8ToStringW(tooltip);
246 SDL_wcslcpy(tray->nid.szTip, tooltipw, sizeof(tray->nid.szTip) / sizeof(*tray->nid.szTip));
247 SDL_free(tooltipw);
248
249 if (icon) {
250 tray->nid.hIcon = CreateIconFromSurface(icon);
251
252 if (!tray->nid.hIcon) {
253 tray->nid.hIcon = load_default_icon();
254 }
255
256 tray->icon = tray->nid.hIcon;
257 } else {
258 tray->nid.hIcon = load_default_icon();
259 tray->icon = tray->nid.hIcon;
260 }
261
262 Shell_NotifyIconW(NIM_ADD, &tray->nid);
263 Shell_NotifyIconW(NIM_SETVERSION, &tray->nid);
264
265 SetWindowLongPtr(tray->hwnd, GWLP_USERDATA, (LONG_PTR) tray);
266
267 SDL_RegisterTray(tray);
268
269 return tray;
270}
271
272void SDL_SetTrayIcon(SDL_Tray *tray, SDL_Surface *icon)
273{
274 if (!SDL_ObjectValid(tray, SDL_OBJECT_TYPE_TRAY)) {
275 return;
276 }
277
278 if (tray->icon) {
279 DestroyIcon(tray->icon);
280 }
281
282 if (icon) {
283 tray->nid.hIcon = CreateIconFromSurface(icon);
284
285 if (!tray->nid.hIcon) {
286 tray->nid.hIcon = load_default_icon();
287 }
288
289 tray->icon = tray->nid.hIcon;
290 } else {
291 tray->nid.hIcon = load_default_icon();
292 tray->icon = tray->nid.hIcon;
293 }
294
295 Shell_NotifyIconW(NIM_MODIFY, &tray->nid);
296}
297
298void SDL_SetTrayTooltip(SDL_Tray *tray, const char *tooltip)
299{
300 if (!SDL_ObjectValid(tray, SDL_OBJECT_TYPE_TRAY)) {
301 return;
302 }
303
304 if (tooltip) {
305 wchar_t *tooltipw = WIN_UTF8ToStringW(tooltip);
306 SDL_wcslcpy(tray->nid.szTip, tooltipw, sizeof(tray->nid.szTip) / sizeof(*tray->nid.szTip));
307 SDL_free(tooltipw);
308 } else {
309 tray->nid.szTip[0] = '\0';
310 }
311
312 Shell_NotifyIconW(NIM_MODIFY, &tray->nid);
313}
314
315SDL_TrayMenu *SDL_CreateTrayMenu(SDL_Tray *tray)
316{
317 if (!SDL_ObjectValid(tray, SDL_OBJECT_TYPE_TRAY)) {
318 SDL_InvalidParamError("tray");
319 return NULL;
320 }
321
322 tray->menu = (SDL_TrayMenu *)SDL_calloc(1, sizeof(*tray->menu));
323
324 if (!tray->menu) {
325 return NULL;
326 }
327
328 tray->menu->hMenu = CreatePopupMenu();
329 tray->menu->parent_tray = tray;
330 tray->menu->parent_entry = NULL;
331
332 return tray->menu;
333}
334
335SDL_TrayMenu *SDL_GetTrayMenu(SDL_Tray *tray)
336{
337 if (!SDL_ObjectValid(tray, SDL_OBJECT_TYPE_TRAY)) {
338 SDL_InvalidParamError("tray");
339 return NULL;
340 }
341
342 return tray->menu;
343}
344
345SDL_TrayMenu *SDL_CreateTraySubmenu(SDL_TrayEntry *entry)
346{
347 if (!entry) {
348 SDL_InvalidParamError("entry");
349 return NULL;
350 }
351
352 if (!entry->submenu) {
353 SDL_SetError("Cannot create submenu for entry not created with SDL_TRAYENTRY_SUBMENU");
354 return NULL;
355 }
356
357 return entry->submenu;
358}
359
360SDL_TrayMenu *SDL_GetTraySubmenu(SDL_TrayEntry *entry)
361{
362 if (!entry) {
363 SDL_InvalidParamError("entry");
364 return NULL;
365 }
366
367 return entry->submenu;
368}
369
370const SDL_TrayEntry **SDL_GetTrayEntries(SDL_TrayMenu *menu, int *count)
371{
372 if (!menu) {
373 SDL_InvalidParamError("menu");
374 return NULL;
375 }
376
377 if (count) {
378 *count = menu->nEntries;
379 }
380 return (const SDL_TrayEntry **)menu->entries;
381}
382
383void SDL_RemoveTrayEntry(SDL_TrayEntry *entry)
384{
385 if (!entry) {
386 return;
387 }
388
389 SDL_TrayMenu *menu = entry->parent;
390
391 bool found = false;
392 for (int i = 0; i < menu->nEntries - 1; i++) {
393 if (menu->entries[i] == entry) {
394 found = true;
395 }
396
397 if (found) {
398 menu->entries[i] = menu->entries[i + 1];
399 }
400 }
401
402 if (entry->submenu) {
403 DestroySDLMenu(entry->submenu);
404 }
405
406 menu->nEntries--;
407 SDL_TrayEntry **new_entries = (SDL_TrayEntry **)SDL_realloc(menu->entries, (menu->nEntries + 1) * sizeof(*new_entries));
408
409 /* Not sure why shrinking would fail, but even if it does, we can live with a "too big" array */
410 if (new_entries) {
411 menu->entries = new_entries;
412 menu->entries[menu->nEntries] = NULL;
413 }
414
415 if (!DeleteMenu(menu->hMenu, (UINT) entry->id, MF_BYCOMMAND)) {
416 /* This is somewhat useless since we don't return anything, but might help with eventual bugs */
417 SDL_SetError("Couldn't destroy tray entry");
418 }
419
420 SDL_free(entry);
421}
422
423SDL_TrayEntry *SDL_InsertTrayEntryAt(SDL_TrayMenu *menu, int pos, const char *label, SDL_TrayEntryFlags flags)
424{
425 if (!menu) {
426 SDL_InvalidParamError("menu");
427 return NULL;
428 }
429
430 if (pos < -1 || pos > menu->nEntries) {
431 SDL_InvalidParamError("pos");
432 return NULL;
433 }
434
435 int windows_compatible_pos = pos;
436
437 if (pos == -1) {
438 pos = menu->nEntries;
439 } else if (pos == menu->nEntries) {
440 windows_compatible_pos = -1;
441 }
442
443 SDL_TrayEntry *entry = (SDL_TrayEntry *)SDL_calloc(1, sizeof(*entry));
444 if (!entry) {
445 return NULL;
446 }
447
448 wchar_t *label_w = NULL;
449
450 if (label && (label_w = escape_label(label)) == NULL) {
451 SDL_free(entry);
452 return NULL;
453 }
454
455 entry->parent = menu;
456 entry->flags = flags;
457 entry->callback = NULL;
458 entry->userdata = NULL;
459 entry->submenu = NULL;
460 SDL_snprintf(entry->label_cache, sizeof(entry->label_cache), "%s", label ? label : "");
461
462 if (label != NULL && flags & SDL_TRAYENTRY_SUBMENU) {
463 entry->submenu = (SDL_TrayMenu *)SDL_calloc(1, sizeof(*entry->submenu));
464 if (!entry->submenu) {
465 SDL_free(entry);
466 SDL_free(label_w);
467 return NULL;
468 }
469
470 entry->submenu->hMenu = CreatePopupMenu();
471 entry->submenu->nEntries = 0;
472 entry->submenu->entries = NULL;
473 entry->submenu->parent_entry = entry;
474 entry->submenu->parent_tray = NULL;
475
476 entry->id = (UINT_PTR) entry->submenu->hMenu;
477 } else {
478 entry->id = get_next_id();
479 }
480
481 SDL_TrayEntry **new_entries = (SDL_TrayEntry **)SDL_realloc(menu->entries, (menu->nEntries + 2) * sizeof(*new_entries));
482 if (!new_entries) {
483 SDL_free(entry);
484 SDL_free(label_w);
485 if (entry->submenu) {
486 DestroyMenu(entry->submenu->hMenu);
487 SDL_free(entry->submenu);
488 }
489 return NULL;
490 }
491
492 menu->entries = new_entries;
493 menu->nEntries++;
494
495 for (int i = menu->nEntries - 1; i > pos; i--) {
496 menu->entries[i] = menu->entries[i - 1];
497 }
498
499 new_entries[pos] = entry;
500 new_entries[menu->nEntries] = NULL;
501
502 if (label == NULL) {
503 InsertMenuW(menu->hMenu, windows_compatible_pos, MF_SEPARATOR | MF_BYPOSITION, entry->id, NULL);
504 } else {
505 UINT mf = MF_STRING | MF_BYPOSITION;
506 if (flags & SDL_TRAYENTRY_SUBMENU) {
507 mf = MF_POPUP;
508 }
509
510 if (flags & SDL_TRAYENTRY_DISABLED) {
511 mf |= MF_DISABLED | MF_GRAYED;
512 }
513
514 if (flags & SDL_TRAYENTRY_CHECKED) {
515 mf |= MF_CHECKED;
516 }
517
518 InsertMenuW(menu->hMenu, windows_compatible_pos, mf, entry->id, label_w);
519
520 SDL_free(label_w);
521 }
522
523 return entry;
524}
525
526void SDL_SetTrayEntryLabel(SDL_TrayEntry *entry, const char *label)
527{
528 if (!entry) {
529 return;
530 }
531
532 SDL_snprintf(entry->label_cache, sizeof(entry->label_cache), "%s", label);
533
534 wchar_t *label_w = escape_label(label);
535
536 if (!label_w) {
537 return;
538 }
539
540 MENUITEMINFOW mii;
541 mii.cbSize = sizeof(MENUITEMINFOW);
542 mii.fMask = MIIM_STRING;
543
544 mii.dwTypeData = label_w;
545 mii.cch = (UINT) SDL_wcslen(label_w);
546
547 if (!SetMenuItemInfoW(entry->parent->hMenu, (UINT) entry->id, TRUE, &mii)) {
548 SDL_SetError("Couldn't update tray entry label");
549 }
550
551 SDL_free(label_w);
552}
553
554const char *SDL_GetTrayEntryLabel(SDL_TrayEntry *entry)
555{
556 if (!entry) {
557 SDL_InvalidParamError("entry");
558 return NULL;
559 }
560
561 return entry->label_cache;
562}
563
564void SDL_SetTrayEntryChecked(SDL_TrayEntry *entry, bool checked)
565{
566 if (!entry || !(entry->flags & SDL_TRAYENTRY_CHECKBOX)) {
567 return;
568 }
569
570 CheckMenuItem(entry->parent->hMenu, (UINT) entry->id, checked ? MF_CHECKED : MF_UNCHECKED);
571}
572
573bool SDL_GetTrayEntryChecked(SDL_TrayEntry *entry)
574{
575 if (!entry || !(entry->flags & SDL_TRAYENTRY_CHECKBOX)) {
576 return false;
577 }
578
579 MENUITEMINFOW mii;
580 mii.cbSize = sizeof(MENUITEMINFOW);
581 mii.fMask = MIIM_STATE;
582
583 GetMenuItemInfoW(entry->parent->hMenu, (UINT) entry->id, FALSE, &mii);
584
585 return ((mii.fState & MFS_CHECKED) != 0);
586}
587
588void SDL_SetTrayEntryEnabled(SDL_TrayEntry *entry, bool enabled)
589{
590 if (!entry) {
591 return;
592 }
593
594 EnableMenuItem(entry->parent->hMenu, (UINT) entry->id, MF_BYCOMMAND | (enabled ? MF_ENABLED : (MF_DISABLED | MF_GRAYED)));
595}
596
597bool SDL_GetTrayEntryEnabled(SDL_TrayEntry *entry)
598{
599 if (!entry) {
600 return false;
601 }
602
603 MENUITEMINFOW mii;
604 mii.cbSize = sizeof(MENUITEMINFOW);
605 mii.fMask = MIIM_STATE;
606
607 GetMenuItemInfoW(entry->parent->hMenu, (UINT) entry->id, FALSE, &mii);
608
609 return ((mii.fState & MFS_ENABLED) != 0);
610}
611
612void SDL_SetTrayEntryCallback(SDL_TrayEntry *entry, SDL_TrayCallback callback, void *userdata)
613{
614 if (!entry) {
615 return;
616 }
617
618 entry->callback = callback;
619 entry->userdata = userdata;
620}
621
622void SDL_ClickTrayEntry(SDL_TrayEntry *entry)
623{
624 if (!entry) {
625 return;
626 }
627
628 if (entry->flags & SDL_TRAYENTRY_CHECKBOX) {
629 SDL_SetTrayEntryChecked(entry, !SDL_GetTrayEntryChecked(entry));
630 }
631
632 if (entry->callback) {
633 entry->callback(entry->userdata, entry);
634 }
635}
636
637SDL_TrayMenu *SDL_GetTrayEntryParent(SDL_TrayEntry *entry)
638{
639 if (!entry) {
640 SDL_InvalidParamError("entry");
641 return NULL;
642 }
643
644 return entry->parent;
645}
646
647SDL_TrayEntry *SDL_GetTrayMenuParentEntry(SDL_TrayMenu *menu)
648{
649 if (!menu) {
650 SDL_InvalidParamError("menu");
651 return NULL;
652 }
653
654 return menu->parent_entry;
655}
656
657SDL_Tray *SDL_GetTrayMenuParentTray(SDL_TrayMenu *menu)
658{
659 if (!menu) {
660 SDL_InvalidParamError("menu");
661 return NULL;
662 }
663
664 return menu->parent_tray;
665}
666
667void SDL_DestroyTray(SDL_Tray *tray)
668{
669 if (!SDL_ObjectValid(tray, SDL_OBJECT_TYPE_TRAY)) {
670 return;
671 }
672
673 SDL_UnregisterTray(tray);
674
675 Shell_NotifyIconW(NIM_DELETE, &tray->nid);
676
677 if (tray->menu) {
678 DestroySDLMenu(tray->menu);
679 }
680
681 if (tray->icon) {
682 DestroyIcon(tray->icon);
683 }
684
685 if (tray->hwnd) {
686 DestroyWindow(tray->hwnd);
687 }
688
689 SDL_free(tray);
690}