SDL_tray.c 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. /*
  2. Simple DirectMedia Layer
  3. Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
  4. This software is provided 'as-is', without any express or implied
  5. warranty. In no event will the authors be held liable for any damages
  6. arising from the use of this software.
  7. Permission is granted to anyone to use this software for any purpose,
  8. including commercial applications, and to alter it and redistribute it
  9. freely, subject to the following restrictions:
  10. 1. The origin of this software must not be misrepresented; you must not
  11. claim that you wrote the original software. If you use this software
  12. in a product, an acknowledgment in the product documentation would be
  13. appreciated but is not required.
  14. 2. Altered source versions must be plainly marked as such, and must not be
  15. misrepresented as being the original software.
  16. 3. This notice may not be removed or altered from any source distribution.
  17. */
  18. #include "SDL_internal.h"
  19. #include "../SDL_tray_utils.h"
  20. #include "../../core/windows/SDL_windows.h"
  21. #include <windowsx.h>
  22. #include <shellapi.h>
  23. #include "../../video/windows/SDL_surface_utils.h"
  24. #ifndef NOTIFYICON_VERSION_4
  25. #define NOTIFYICON_VERSION_4 4
  26. #endif
  27. #ifndef NIF_SHOWTIP
  28. #define NIF_SHOWTIP 0x00000080
  29. #endif
  30. #define WM_TRAYICON (WM_USER + 1)
  31. struct SDL_TrayMenu {
  32. HMENU hMenu;
  33. size_t nEntries;
  34. SDL_TrayEntry **entries;
  35. SDL_Tray *parent_tray;
  36. SDL_TrayEntry *parent_entry;
  37. };
  38. struct SDL_TrayEntry {
  39. SDL_TrayMenu *parent;
  40. UINT_PTR id;
  41. char label_cache[4096];
  42. SDL_TrayEntryFlags flags;
  43. SDL_TrayCallback callback;
  44. void *userdata;
  45. SDL_TrayMenu *submenu;
  46. };
  47. struct SDL_Tray {
  48. NOTIFYICONDATAW nid;
  49. HWND hwnd;
  50. HICON icon;
  51. SDL_TrayMenu *menu;
  52. };
  53. static UINT_PTR get_next_id(void)
  54. {
  55. static UINT_PTR next_id = 0;
  56. return ++next_id;
  57. }
  58. static SDL_TrayEntry *find_entry_in_menu(SDL_TrayMenu *menu, UINT_PTR id)
  59. {
  60. for (size_t i = 0; i < menu->nEntries; i++) {
  61. SDL_TrayEntry *entry = menu->entries[i];
  62. if (entry->id == id) {
  63. return entry;
  64. }
  65. if (entry->submenu) {
  66. SDL_TrayEntry *e = find_entry_in_menu(entry->submenu, id);
  67. if (e) {
  68. return e;
  69. }
  70. }
  71. }
  72. return NULL;
  73. }
  74. static SDL_TrayEntry *find_entry_with_id(SDL_Tray *tray, UINT_PTR id)
  75. {
  76. if (!tray->menu) {
  77. return NULL;
  78. }
  79. return find_entry_in_menu(tray->menu, id);
  80. }
  81. LRESULT CALLBACK TrayWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
  82. SDL_Tray *tray = (SDL_Tray *) GetWindowLongPtr(hwnd, GWLP_USERDATA);
  83. SDL_TrayEntry *entry = NULL;
  84. if (!tray) {
  85. return DefWindowProc(hwnd, uMsg, wParam, lParam);
  86. }
  87. switch (uMsg) {
  88. case WM_TRAYICON:
  89. if (LOWORD(lParam) == WM_CONTEXTMENU || LOWORD(lParam) == WM_LBUTTONUP) {
  90. SetForegroundWindow(hwnd);
  91. TrackPopupMenu(tray->menu->hMenu, TPM_BOTTOMALIGN | TPM_RIGHTALIGN, GET_X_LPARAM(wParam), GET_Y_LPARAM(wParam), 0, hwnd, NULL);
  92. }
  93. break;
  94. case WM_COMMAND:
  95. entry = find_entry_with_id(tray, LOWORD(wParam));
  96. if (entry && (entry->flags & SDL_TRAYENTRY_CHECKBOX)) {
  97. SDL_SetTrayEntryChecked(entry, !SDL_GetTrayEntryChecked(entry));
  98. }
  99. if (entry && entry->callback) {
  100. entry->callback(entry->userdata, entry);
  101. }
  102. break;
  103. default:
  104. return DefWindowProc(hwnd, uMsg, wParam, lParam);
  105. }
  106. return 0;
  107. }
  108. static void DestroySDLMenu(SDL_TrayMenu *menu)
  109. {
  110. for (size_t i = 0; i < menu->nEntries; i++) {
  111. if (menu->entries[i] && menu->entries[i]->submenu) {
  112. DestroySDLMenu(menu->entries[i]->submenu);
  113. }
  114. SDL_free(menu->entries[i]);
  115. }
  116. SDL_free(menu->entries);
  117. DestroyMenu(menu->hMenu);
  118. SDL_free(menu);
  119. }
  120. static wchar_t *escape_label(const char *in)
  121. {
  122. const char *c;
  123. char *c2;
  124. int len = 0;
  125. for (c = in; *c; c++) {
  126. len += (*c == '&') ? 2 : 1;
  127. }
  128. char *escaped = SDL_malloc(SDL_strlen(in) + len + 1);
  129. if (!escaped) {
  130. return NULL;
  131. }
  132. for (c = in, c2 = escaped; *c;) {
  133. if (*c == '&') {
  134. *c2++ = *c;
  135. }
  136. *c2++ = *c++;
  137. }
  138. *c2 = '\0';
  139. wchar_t *out = WIN_UTF8ToStringW(escaped);
  140. SDL_free(escaped);
  141. return out;
  142. }
  143. SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip)
  144. {
  145. SDL_Tray *tray = SDL_malloc(sizeof(SDL_Tray));
  146. if (!tray) {
  147. return NULL;
  148. }
  149. tray->menu = NULL;
  150. tray->hwnd = CreateWindowEx(0, TEXT("Message"), NULL, 0, 0, 0, 0, 0, HWND_MESSAGE, NULL, NULL, NULL);
  151. SetWindowLongPtr(tray->hwnd, GWLP_WNDPROC, (LONG_PTR) TrayWindowProc);
  152. SDL_zero(tray->nid);
  153. tray->nid.cbSize = sizeof(NOTIFYICONDATAW);
  154. tray->nid.hWnd = tray->hwnd;
  155. tray->nid.uID = (UINT) get_next_id();
  156. tray->nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP | NIF_SHOWTIP;
  157. tray->nid.uCallbackMessage = WM_TRAYICON;
  158. tray->nid.uVersion = NOTIFYICON_VERSION_4;
  159. wchar_t *tooltipw = WIN_UTF8ToStringW(tooltip);
  160. SDL_wcslcpy(tray->nid.szTip, tooltipw, sizeof(tray->nid.szTip) / sizeof(*tray->nid.szTip));
  161. SDL_free(tooltipw);
  162. if (icon) {
  163. tray->nid.hIcon = CreateIconFromSurface(icon);
  164. if (!tray->nid.hIcon) {
  165. tray->nid.hIcon = LoadIcon(NULL, IDI_APPLICATION);
  166. }
  167. tray->icon = tray->nid.hIcon;
  168. } else {
  169. tray->nid.hIcon = LoadIcon(NULL, IDI_APPLICATION);
  170. tray->icon = tray->nid.hIcon;
  171. }
  172. Shell_NotifyIconW(NIM_ADD, &tray->nid);
  173. Shell_NotifyIconW(NIM_SETVERSION, &tray->nid);
  174. SetWindowLongPtr(tray->hwnd, GWLP_USERDATA, (LONG_PTR) tray);
  175. SDL_IncrementTrayCount();
  176. return tray;
  177. }
  178. void SDL_SetTrayIcon(SDL_Tray *tray, SDL_Surface *icon)
  179. {
  180. if (tray->icon) {
  181. DestroyIcon(tray->icon);
  182. }
  183. if (icon) {
  184. tray->nid.hIcon = CreateIconFromSurface(icon);
  185. if (!tray->nid.hIcon) {
  186. tray->nid.hIcon = LoadIcon(NULL, IDI_APPLICATION);
  187. }
  188. tray->icon = tray->nid.hIcon;
  189. } else {
  190. tray->nid.hIcon = LoadIcon(NULL, IDI_APPLICATION);
  191. tray->icon = tray->nid.hIcon;
  192. }
  193. Shell_NotifyIconW(NIM_MODIFY, &tray->nid);
  194. }
  195. void SDL_SetTrayTooltip(SDL_Tray *tray, const char *tooltip)
  196. {
  197. if (tooltip) {
  198. wchar_t *tooltipw = WIN_UTF8ToStringW(tooltip);
  199. SDL_wcslcpy(tray->nid.szTip, tooltipw, sizeof(tray->nid.szTip) / sizeof(*tray->nid.szTip));
  200. SDL_free(tooltipw);
  201. } else {
  202. tray->nid.szTip[0] = '\0';
  203. }
  204. Shell_NotifyIconW(NIM_MODIFY, &tray->nid);
  205. }
  206. SDL_TrayMenu *SDL_CreateTrayMenu(SDL_Tray *tray)
  207. {
  208. tray->menu = SDL_malloc(sizeof(SDL_TrayMenu));
  209. if (!tray->menu) {
  210. return NULL;
  211. }
  212. SDL_memset((void *) tray->menu, 0, sizeof(*tray->menu));
  213. tray->menu->hMenu = CreatePopupMenu();
  214. tray->menu->parent_tray = tray;
  215. return tray->menu;
  216. }
  217. SDL_TrayMenu *SDL_GetTrayMenu(SDL_Tray *tray)
  218. {
  219. return tray->menu;
  220. }
  221. SDL_TrayMenu *SDL_CreateTraySubmenu(SDL_TrayEntry *entry)
  222. {
  223. if (!entry->submenu) {
  224. SDL_SetError("Cannot create submenu for entry not created with SDL_TRAYENTRY_SUBMENU");
  225. }
  226. return entry->submenu;
  227. }
  228. SDL_TrayMenu *SDL_GetTraySubmenu(SDL_TrayEntry *entry)
  229. {
  230. return entry->submenu;
  231. }
  232. const SDL_TrayEntry **SDL_GetTrayEntries(SDL_TrayMenu *menu, int *size)
  233. {
  234. if (size) {
  235. *size = (int) menu->nEntries;
  236. }
  237. return (const SDL_TrayEntry **) menu->entries;
  238. }
  239. void SDL_RemoveTrayEntry(SDL_TrayEntry *entry)
  240. {
  241. if (!entry) {
  242. return;
  243. }
  244. SDL_TrayMenu *menu = entry->parent;
  245. bool found = false;
  246. for (size_t i = 0; i < menu->nEntries - 1; i++) {
  247. if (menu->entries[i] == entry) {
  248. found = true;
  249. }
  250. if (found) {
  251. menu->entries[i] = menu->entries[i + 1];
  252. }
  253. }
  254. if (entry->submenu) {
  255. DestroySDLMenu(entry->submenu);
  256. }
  257. menu->nEntries--;
  258. SDL_TrayEntry ** new_entries = SDL_realloc(menu->entries, (menu->nEntries + 1) * sizeof(SDL_TrayEntry *));
  259. /* Not sure why shrinking would fail, but even if it does, we can live with a "too big" array */
  260. if (new_entries) {
  261. menu->entries = new_entries;
  262. menu->entries[menu->nEntries] = NULL;
  263. }
  264. if (!DeleteMenu(menu->hMenu, (UINT) entry->id, MF_BYCOMMAND)) {
  265. /* This is somewhat useless since we don't return anything, but might help with eventual bugs */
  266. SDL_SetError("Couldn't destroy tray entry");
  267. }
  268. SDL_free(entry);
  269. }
  270. SDL_TrayEntry *SDL_InsertTrayEntryAt(SDL_TrayMenu *menu, int pos, const char *label, SDL_TrayEntryFlags flags)
  271. {
  272. if (pos < -1 || pos > (int) menu->nEntries) {
  273. SDL_InvalidParamError("pos");
  274. return NULL;
  275. }
  276. int windows_compatible_pos = pos;
  277. if (pos == -1) {
  278. pos = (int) menu->nEntries;
  279. } else if (pos == menu->nEntries) {
  280. windows_compatible_pos = -1;
  281. }
  282. SDL_TrayEntry *entry = SDL_malloc(sizeof(SDL_TrayEntry));
  283. if (!entry) {
  284. return NULL;
  285. }
  286. wchar_t *label_w = NULL;
  287. if (label && !(label_w = escape_label(label))) {
  288. SDL_free(entry);
  289. return NULL;
  290. }
  291. entry->parent = menu;
  292. entry->flags = flags;
  293. entry->callback = NULL;
  294. entry->userdata = NULL;
  295. entry->submenu = NULL;
  296. SDL_snprintf(entry->label_cache, sizeof(entry->label_cache), "%s", label ? label : "");
  297. if (label != NULL && flags & SDL_TRAYENTRY_SUBMENU) {
  298. entry->submenu = SDL_malloc(sizeof(SDL_TrayMenu));
  299. if (!entry->submenu) {
  300. SDL_free(entry);
  301. SDL_free(label_w);
  302. return NULL;
  303. }
  304. entry->submenu->hMenu = CreatePopupMenu();
  305. entry->submenu->nEntries = 0;
  306. entry->submenu->entries = NULL;
  307. entry->id = (UINT_PTR) entry->submenu->hMenu;
  308. } else {
  309. entry->id = get_next_id();
  310. }
  311. SDL_TrayEntry **new_entries = (SDL_TrayEntry **) SDL_realloc(menu->entries, (menu->nEntries + 2) * sizeof(SDL_TrayEntry **));
  312. if (!new_entries) {
  313. SDL_free(entry);
  314. SDL_free(label_w);
  315. if (entry->submenu) {
  316. DestroyMenu(entry->submenu->hMenu);
  317. SDL_free(entry->submenu);
  318. }
  319. return NULL;
  320. }
  321. menu->entries = new_entries;
  322. menu->nEntries++;
  323. for (int i = (int) menu->nEntries - 1; i > pos; i--) {
  324. menu->entries[i] = menu->entries[i - 1];
  325. }
  326. new_entries[pos] = entry;
  327. new_entries[menu->nEntries] = NULL;
  328. if (label == NULL) {
  329. InsertMenuW(menu->hMenu, windows_compatible_pos, MF_SEPARATOR | MF_BYPOSITION, entry->id, NULL);
  330. } else {
  331. UINT mf = MF_STRING | MF_BYPOSITION;
  332. if (flags & SDL_TRAYENTRY_SUBMENU) {
  333. mf = MF_POPUP;
  334. }
  335. if (flags & SDL_TRAYENTRY_DISABLED) {
  336. mf |= MF_DISABLED | MF_GRAYED;
  337. }
  338. if (flags & SDL_TRAYENTRY_CHECKED) {
  339. mf |= MF_CHECKED;
  340. }
  341. InsertMenuW(menu->hMenu, windows_compatible_pos, mf, entry->id, label_w);
  342. SDL_free(label_w);
  343. }
  344. return entry;
  345. }
  346. void SDL_SetTrayEntryLabel(SDL_TrayEntry *entry, const char *label)
  347. {
  348. SDL_snprintf(entry->label_cache, sizeof(entry->label_cache), "%s", label);
  349. wchar_t *label_w = escape_label(label);
  350. if (!label_w) {
  351. return;
  352. }
  353. MENUITEMINFOW mii;
  354. mii.cbSize = sizeof(MENUITEMINFOW);
  355. mii.fMask = MIIM_STRING;
  356. mii.dwTypeData = label_w;
  357. mii.cch = (UINT) SDL_wcslen(label_w);
  358. if (!SetMenuItemInfoW(entry->parent->hMenu, (UINT) entry->id, TRUE, &mii)) {
  359. SDL_SetError("Couldn't update tray entry label");
  360. }
  361. SDL_free(label_w);
  362. }
  363. const char *SDL_GetTrayEntryLabel(SDL_TrayEntry *entry)
  364. {
  365. return entry->label_cache;
  366. }
  367. void SDL_SetTrayEntryChecked(SDL_TrayEntry *entry, bool checked)
  368. {
  369. if (!(entry->flags & SDL_TRAYENTRY_CHECKBOX)) {
  370. SDL_SetError("Can't check/uncheck tray entry not created with SDL_TRAYENTRY_CHECKBOX");
  371. return;
  372. }
  373. CheckMenuItem(entry->parent->hMenu, (UINT) entry->id, checked ? MF_CHECKED : MF_UNCHECKED);
  374. }
  375. bool SDL_GetTrayEntryChecked(SDL_TrayEntry *entry)
  376. {
  377. if (!(entry->flags & SDL_TRAYENTRY_CHECKBOX)) {
  378. SDL_SetError("Can't get check status of tray entry not created with SDL_TRAYENTRY_CHECKBOX");
  379. return false;
  380. }
  381. MENUITEMINFOW mii;
  382. mii.cbSize = sizeof(MENUITEMINFOW);
  383. mii.fMask = MIIM_STATE;
  384. GetMenuItemInfoW(entry->parent->hMenu, (UINT) entry->id, FALSE, &mii);
  385. return !!(mii.fState & MFS_CHECKED);
  386. }
  387. void SDL_SetTrayEntryEnabled(SDL_TrayEntry *entry, bool enabled)
  388. {
  389. if (!(entry->flags & SDL_TRAYENTRY_CHECKBOX)) {
  390. SDL_SetError("Cannot update check for entry not created with SDL_TRAYENTRY_CHECKBOX");
  391. return;
  392. }
  393. EnableMenuItem(entry->parent->hMenu, (UINT) entry->id, MF_BYCOMMAND | (enabled ? MF_ENABLED : (MF_DISABLED | MF_GRAYED)));
  394. }
  395. bool SDL_GetTrayEntryEnabled(SDL_TrayEntry *entry)
  396. {
  397. if (!(entry->flags & SDL_TRAYENTRY_CHECKBOX)) {
  398. SDL_SetError("Cannot fetch check for entry not created with SDL_TRAYENTRY_CHECKBOX");
  399. return false;
  400. }
  401. MENUITEMINFOW mii;
  402. mii.cbSize = sizeof(MENUITEMINFOW);
  403. mii.fMask = MIIM_STATE;
  404. GetMenuItemInfoW(entry->parent->hMenu, (UINT) entry->id, FALSE, &mii);
  405. return !!(mii.fState & MFS_ENABLED);
  406. }
  407. void SDL_SetTrayEntryCallback(SDL_TrayEntry *entry, SDL_TrayCallback callback, void *userdata)
  408. {
  409. entry->callback = callback;
  410. entry->userdata = userdata;
  411. }
  412. SDL_TrayMenu *SDL_GetTrayEntryParent(SDL_TrayEntry *entry)
  413. {
  414. return entry->parent;
  415. }
  416. SDL_TrayEntry *SDL_GetTrayMenuParentEntry(SDL_TrayMenu *menu)
  417. {
  418. return menu->parent_entry;
  419. }
  420. SDL_Tray *SDL_GetTrayMenuParentTray(SDL_TrayMenu *menu)
  421. {
  422. return menu->parent_tray;
  423. }
  424. void SDL_DestroyTray(SDL_Tray *tray)
  425. {
  426. if (!tray) {
  427. return;
  428. }
  429. Shell_NotifyIconW(NIM_DELETE, &tray->nid);
  430. if (tray->menu) {
  431. DestroySDLMenu(tray->menu);
  432. }
  433. if (tray->icon) {
  434. DestroyIcon(tray->icon);
  435. }
  436. if (tray->hwnd) {
  437. DestroyWindow(tray->hwnd);
  438. }
  439. SDL_free(tray);
  440. SDL_DecrementTrayCount();
  441. }