From 5a079a2d114f96d4847d1ee305d5b7c16eeec50e Mon Sep 17 00:00:00 2001 From: 3gg <3gg@shellblade.net> Date: Sat, 27 Dec 2025 12:03:39 -0800 Subject: Initial commit --- contrib/SDL-3.2.8/src/tray/SDL_tray_utils.c | 91 +++ contrib/SDL-3.2.8/src/tray/SDL_tray_utils.h | 26 + contrib/SDL-3.2.8/src/tray/cocoa/SDL_tray.m | 524 +++++++++++++++++ contrib/SDL-3.2.8/src/tray/dummy/SDL_tray.c | 143 +++++ contrib/SDL-3.2.8/src/tray/unix/SDL_tray.c | 817 ++++++++++++++++++++++++++ contrib/SDL-3.2.8/src/tray/windows/SDL_tray.c | 690 ++++++++++++++++++++++ 6 files changed, 2291 insertions(+) create mode 100644 contrib/SDL-3.2.8/src/tray/SDL_tray_utils.c create mode 100644 contrib/SDL-3.2.8/src/tray/SDL_tray_utils.h create mode 100644 contrib/SDL-3.2.8/src/tray/cocoa/SDL_tray.m create mode 100644 contrib/SDL-3.2.8/src/tray/dummy/SDL_tray.c create mode 100644 contrib/SDL-3.2.8/src/tray/unix/SDL_tray.c create mode 100644 contrib/SDL-3.2.8/src/tray/windows/SDL_tray.c (limited to 'contrib/SDL-3.2.8/src/tray') diff --git a/contrib/SDL-3.2.8/src/tray/SDL_tray_utils.c b/contrib/SDL-3.2.8/src/tray/SDL_tray_utils.c new file mode 100644 index 0000000..35cf559 --- /dev/null +++ b/contrib/SDL-3.2.8/src/tray/SDL_tray_utils.c @@ -0,0 +1,91 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#include "../video/SDL_sysvideo.h" +#include "../events/SDL_events_c.h" +#include "SDL_tray_utils.h" + + +static int active_trays = 0; + +void SDL_RegisterTray(SDL_Tray *tray) +{ + SDL_SetObjectValid(tray, SDL_OBJECT_TYPE_TRAY, true); + + ++active_trays; +} + +void SDL_UnregisterTray(SDL_Tray *tray) +{ + SDL_assert(SDL_ObjectValid(tray, SDL_OBJECT_TYPE_TRAY)); + + SDL_SetObjectValid(tray, SDL_OBJECT_TYPE_TRAY, false); + + --active_trays; + if (active_trays > 0) { + return; + } + + if (!SDL_GetHintBoolean(SDL_HINT_QUIT_ON_LAST_WINDOW_CLOSE, true)) { + return; + } + + int toplevel_count = 0; + SDL_Window **windows = SDL_GetWindows(NULL); + if (windows) { + for (int i = 0; windows[i]; ++i) { + SDL_Window *window = windows[i]; + if (!window->parent && !(window->flags & SDL_WINDOW_HIDDEN)) { + ++toplevel_count; + } + } + SDL_free(windows); + } + + if (toplevel_count == 0) { + SDL_SendQuit(); + } +} + +void SDL_CleanupTrays(void) +{ + if (active_trays == 0) { + return; + } + + void **trays = (void **)SDL_malloc(active_trays * sizeof(*trays)); + if (!trays) { + return; + } + + int count = SDL_GetObjects(SDL_OBJECT_TYPE_TRAY, trays, active_trays); + SDL_assert(count == active_trays); + for (int i = 0; i < count; ++i) { + SDL_DestroyTray((SDL_Tray *)trays[i]); + } + SDL_free(trays); +} + +bool SDL_HasActiveTrays(void) +{ + return (active_trays > 0); +} diff --git a/contrib/SDL-3.2.8/src/tray/SDL_tray_utils.h b/contrib/SDL-3.2.8/src/tray/SDL_tray_utils.h new file mode 100644 index 0000000..e9e64fc --- /dev/null +++ b/contrib/SDL-3.2.8/src/tray/SDL_tray_utils.h @@ -0,0 +1,26 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +extern void SDL_RegisterTray(SDL_Tray *tray); +extern void SDL_UnregisterTray(SDL_Tray *tray); +extern void SDL_CleanupTrays(void); +extern bool SDL_HasActiveTrays(void); diff --git a/contrib/SDL-3.2.8/src/tray/cocoa/SDL_tray.m b/contrib/SDL-3.2.8/src/tray/cocoa/SDL_tray.m new file mode 100644 index 0000000..fd7f955 --- /dev/null +++ b/contrib/SDL-3.2.8/src/tray/cocoa/SDL_tray.m @@ -0,0 +1,524 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#include "SDL_internal.h" + +#ifdef SDL_PLATFORM_MACOS + +#include + +#include "../SDL_tray_utils.h" +#include "../../video/SDL_surface_c.h" + +/* applicationDockMenu */ + +struct SDL_TrayMenu { + NSMenu *nsmenu; + + int nEntries; + SDL_TrayEntry **entries; + + SDL_Tray *parent_tray; + SDL_TrayEntry *parent_entry; +}; + +struct SDL_TrayEntry { + NSMenuItem *nsitem; + + SDL_TrayEntryFlags flags; + SDL_TrayCallback callback; + void *userdata; + SDL_TrayMenu *submenu; + + SDL_TrayMenu *parent; +}; + +struct SDL_Tray { + NSStatusBar *statusBar; + NSStatusItem *statusItem; + + SDL_TrayMenu *menu; +}; + +static void DestroySDLMenu(SDL_TrayMenu *menu) +{ + for (int i = 0; i < menu->nEntries; i++) { + if (menu->entries[i] && menu->entries[i]->submenu) { + DestroySDLMenu(menu->entries[i]->submenu); + } + SDL_free(menu->entries[i]); + } + + SDL_free(menu->entries); + + if (menu->parent_entry) { + [menu->parent_entry->parent->nsmenu setSubmenu:nil forItem:menu->parent_entry->nsitem]; + } else if (menu->parent_tray) { + [menu->parent_tray->statusItem setMenu:nil]; + } + + SDL_free(menu); +} + +void SDL_UpdateTrays(void) +{ +} + +SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip) +{ + if (!SDL_IsMainThread()) { + SDL_SetError("This function should be called on the main thread"); + return NULL; + } + + if (icon) { + icon = SDL_ConvertSurface(icon, SDL_PIXELFORMAT_RGBA32); + if (!icon) { + return NULL; + } + } + + SDL_Tray *tray = (SDL_Tray *)SDL_calloc(1, sizeof(*tray)); + if (!tray) { + SDL_DestroySurface(icon); + return NULL; + } + + tray->statusItem = nil; + tray->statusBar = [NSStatusBar systemStatusBar]; + tray->statusItem = [tray->statusBar statusItemWithLength:NSVariableStatusItemLength]; + [[NSApplication sharedApplication] activateIgnoringOtherApps:TRUE]; + + if (tooltip) { + tray->statusItem.button.toolTip = [NSString stringWithUTF8String:tooltip]; + } else { + tray->statusItem.button.toolTip = nil; + } + + if (icon) { + NSBitmapImageRep *bitmap = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:(unsigned char **)&icon->pixels + pixelsWide:icon->w + pixelsHigh:icon->h + bitsPerSample:8 + samplesPerPixel:4 + hasAlpha:YES + isPlanar:NO + colorSpaceName:NSCalibratedRGBColorSpace + bytesPerRow:icon->pitch + bitsPerPixel:32]; + NSImage *iconimg = [[NSImage alloc] initWithSize:NSMakeSize(icon->w, icon->h)]; + [iconimg addRepresentation:bitmap]; + + /* A typical icon size is 22x22 on macOS. Failing to resize the icon + may give oversized status bar buttons. */ + NSImage *iconimg22 = [[NSImage alloc] initWithSize:NSMakeSize(22, 22)]; + [iconimg22 lockFocus]; + [iconimg setSize:NSMakeSize(22, 22)]; + [iconimg drawInRect:NSMakeRect(0, 0, 22, 22)]; + [iconimg22 unlockFocus]; + + tray->statusItem.button.image = iconimg22; + + SDL_DestroySurface(icon); + } + + SDL_RegisterTray(tray); + + return tray; +} + +void SDL_SetTrayIcon(SDL_Tray *tray, SDL_Surface *icon) +{ + if (!SDL_ObjectValid(tray, SDL_OBJECT_TYPE_TRAY)) { + return; + } + + if (!icon) { + tray->statusItem.button.image = nil; + return; + } + + icon = SDL_ConvertSurface(icon, SDL_PIXELFORMAT_RGBA32); + if (!icon) { + tray->statusItem.button.image = nil; + return; + } + + NSBitmapImageRep *bitmap = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:(unsigned char **)&icon->pixels + pixelsWide:icon->w + pixelsHigh:icon->h + bitsPerSample:8 + samplesPerPixel:4 + hasAlpha:YES + isPlanar:NO + colorSpaceName:NSCalibratedRGBColorSpace + bytesPerRow:icon->pitch + bitsPerPixel:32]; + NSImage *iconimg = [[NSImage alloc] initWithSize:NSMakeSize(icon->w, icon->h)]; + [iconimg addRepresentation:bitmap]; + + /* A typical icon size is 22x22 on macOS. Failing to resize the icon + may give oversized status bar buttons. */ + NSImage *iconimg22 = [[NSImage alloc] initWithSize:NSMakeSize(22, 22)]; + [iconimg22 lockFocus]; + [iconimg setSize:NSMakeSize(22, 22)]; + [iconimg drawInRect:NSMakeRect(0, 0, 22, 22)]; + [iconimg22 unlockFocus]; + + tray->statusItem.button.image = iconimg22; + + SDL_DestroySurface(icon); +} + +void SDL_SetTrayTooltip(SDL_Tray *tray, const char *tooltip) +{ + if (!SDL_ObjectValid(tray, SDL_OBJECT_TYPE_TRAY)) { + return; + } + + if (tooltip) { + tray->statusItem.button.toolTip = [NSString stringWithUTF8String:tooltip]; + } else { + tray->statusItem.button.toolTip = nil; + } +} + +SDL_TrayMenu *SDL_CreateTrayMenu(SDL_Tray *tray) +{ + if (!SDL_ObjectValid(tray, SDL_OBJECT_TYPE_TRAY)) { + SDL_InvalidParamError("tray"); + return NULL; + } + + SDL_TrayMenu *menu = (SDL_TrayMenu *)SDL_calloc(1, sizeof(*menu)); + if (!menu) { + return NULL; + } + + NSMenu *nsmenu = [[NSMenu alloc] init]; + [nsmenu setAutoenablesItems:FALSE]; + + [tray->statusItem setMenu:nsmenu]; + + tray->menu = menu; + menu->nsmenu = nsmenu; + menu->nEntries = 0; + menu->entries = NULL; + menu->parent_tray = tray; + menu->parent_entry = NULL; + + return menu; +} + +SDL_TrayMenu *SDL_GetTrayMenu(SDL_Tray *tray) +{ + if (!SDL_ObjectValid(tray, SDL_OBJECT_TYPE_TRAY)) { + SDL_InvalidParamError("tray"); + return NULL; + } + + return tray->menu; +} + +SDL_TrayMenu *SDL_CreateTraySubmenu(SDL_TrayEntry *entry) +{ + if (!entry) { + SDL_InvalidParamError("entry"); + return NULL; + } + + if (entry->submenu) { + SDL_SetError("Tray entry submenu already exists"); + return NULL; + } + + if (!(entry->flags & SDL_TRAYENTRY_SUBMENU)) { + SDL_SetError("Cannot create submenu for entry not created with SDL_TRAYENTRY_SUBMENU"); + return NULL; + } + + SDL_TrayMenu *menu = (SDL_TrayMenu *)SDL_calloc(1, sizeof(*menu)); + if (!menu) { + return NULL; + } + + NSMenu *nsmenu = [[NSMenu alloc] init]; + [nsmenu setAutoenablesItems:FALSE]; + + entry->submenu = menu; + menu->nsmenu = nsmenu; + menu->nEntries = 0; + menu->entries = NULL; + menu->parent_tray = NULL; + menu->parent_entry = entry; + + [entry->parent->nsmenu setSubmenu:nsmenu forItem:entry->nsitem]; + + return menu; +} + +SDL_TrayMenu *SDL_GetTraySubmenu(SDL_TrayEntry *entry) +{ + if (!entry) { + SDL_InvalidParamError("entry"); + return NULL; + } + + return entry->submenu; +} + +const SDL_TrayEntry **SDL_GetTrayEntries(SDL_TrayMenu *menu, int *count) +{ + if (!menu) { + SDL_InvalidParamError("menu"); + return NULL; + } + + if (count) { + *count = menu->nEntries; + } + return (const SDL_TrayEntry **)menu->entries; +} + +void SDL_RemoveTrayEntry(SDL_TrayEntry *entry) +{ + if (!entry) { + return; + } + + SDL_TrayMenu *menu = entry->parent; + + bool found = false; + for (int i = 0; i < menu->nEntries - 1; i++) { + if (menu->entries[i] == entry) { + found = true; + } + + if (found) { + menu->entries[i] = menu->entries[i + 1]; + } + } + + if (entry->submenu) { + DestroySDLMenu(entry->submenu); + } + + menu->nEntries--; + SDL_TrayEntry **new_entries = (SDL_TrayEntry **)SDL_realloc(menu->entries, (menu->nEntries + 1) * sizeof(*new_entries)); + + /* Not sure why shrinking would fail, but even if it does, we can live with a "too big" array */ + if (new_entries) { + menu->entries = new_entries; + menu->entries[menu->nEntries] = NULL; + } + + [menu->nsmenu removeItem:entry->nsitem]; + + SDL_free(entry); +} + +SDL_TrayEntry *SDL_InsertTrayEntryAt(SDL_TrayMenu *menu, int pos, const char *label, SDL_TrayEntryFlags flags) +{ + if (!menu) { + SDL_InvalidParamError("menu"); + return NULL; + } + + if (pos < -1 || pos > menu->nEntries) { + SDL_InvalidParamError("pos"); + return NULL; + } + + if (pos == -1) { + pos = menu->nEntries; + } + + SDL_TrayEntry *entry = (SDL_TrayEntry *)SDL_calloc(1, sizeof(*entry)); + if (!entry) { + return NULL; + } + + SDL_TrayEntry **new_entries = (SDL_TrayEntry **)SDL_realloc(menu->entries, (menu->nEntries + 2) * sizeof(*new_entries)); + if (!new_entries) { + SDL_free(entry); + return NULL; + } + + menu->entries = new_entries; + menu->nEntries++; + + for (int i = menu->nEntries - 1; i > pos; i--) { + menu->entries[i] = menu->entries[i - 1]; + } + + new_entries[pos] = entry; + new_entries[menu->nEntries] = NULL; + + NSMenuItem *nsitem; + if (label == NULL) { + nsitem = [NSMenuItem separatorItem]; + } else { + nsitem = [[NSMenuItem alloc] initWithTitle:[NSString stringWithUTF8String:label] action:@selector(menu:) keyEquivalent:@""]; + [nsitem setEnabled:((flags & SDL_TRAYENTRY_DISABLED) ? FALSE : TRUE)]; + [nsitem setState:((flags & SDL_TRAYENTRY_CHECKED) ? NSControlStateValueOn : NSControlStateValueOff)]; + [nsitem setRepresentedObject:[NSValue valueWithPointer:entry]]; + } + + [menu->nsmenu insertItem:nsitem atIndex:pos]; + + entry->nsitem = nsitem; + entry->flags = flags; + entry->callback = NULL; + entry->userdata = NULL; + entry->submenu = NULL; + entry->parent = menu; + + return entry; +} + +void SDL_SetTrayEntryLabel(SDL_TrayEntry *entry, const char *label) +{ + if (!entry) { + return; + } + + [entry->nsitem setTitle:[NSString stringWithUTF8String:label]]; +} + +const char *SDL_GetTrayEntryLabel(SDL_TrayEntry *entry) +{ + if (!entry) { + SDL_InvalidParamError("entry"); + return NULL; + } + + return [[entry->nsitem title] UTF8String]; +} + +void SDL_SetTrayEntryChecked(SDL_TrayEntry *entry, bool checked) +{ + if (!entry) { + return; + } + + [entry->nsitem setState:(checked ? NSControlStateValueOn : NSControlStateValueOff)]; +} + +bool SDL_GetTrayEntryChecked(SDL_TrayEntry *entry) +{ + if (!entry) { + return false; + } + + return entry->nsitem.state == NSControlStateValueOn; +} + +void SDL_SetTrayEntryEnabled(SDL_TrayEntry *entry, bool enabled) +{ + if (!entry || !(entry->flags & SDL_TRAYENTRY_CHECKBOX)) { + return; + } + + [entry->nsitem setEnabled:(enabled ? YES : NO)]; +} + +bool SDL_GetTrayEntryEnabled(SDL_TrayEntry *entry) +{ + if (!entry || !(entry->flags & SDL_TRAYENTRY_CHECKBOX)) { + return false; + } + + return entry->nsitem.enabled; +} + +void SDL_SetTrayEntryCallback(SDL_TrayEntry *entry, SDL_TrayCallback callback, void *userdata) +{ + if (!entry) { + return; + } + + entry->callback = callback; + entry->userdata = userdata; +} + +void SDL_ClickTrayEntry(SDL_TrayEntry *entry) +{ + if (!entry) { + return; + } + + if (entry->flags & SDL_TRAYENTRY_CHECKBOX) { + SDL_SetTrayEntryChecked(entry, !SDL_GetTrayEntryChecked(entry)); + } + + if (entry->callback) { + entry->callback(entry->userdata, entry); + } +} + +SDL_TrayMenu *SDL_GetTrayEntryParent(SDL_TrayEntry *entry) +{ + if (!entry) { + SDL_InvalidParamError("entry"); + return NULL; + } + + return entry->parent; +} + +SDL_TrayEntry *SDL_GetTrayMenuParentEntry(SDL_TrayMenu *menu) +{ + if (!menu) { + SDL_InvalidParamError("menu"); + return NULL; + } + + return menu->parent_entry; +} + +SDL_Tray *SDL_GetTrayMenuParentTray(SDL_TrayMenu *menu) +{ + if (!menu) { + SDL_InvalidParamError("menu"); + return NULL; + } + + return menu->parent_tray; +} + +void SDL_DestroyTray(SDL_Tray *tray) +{ + if (!SDL_ObjectValid(tray, SDL_OBJECT_TYPE_TRAY)) { + return; + } + + SDL_UnregisterTray(tray); + + [[NSStatusBar systemStatusBar] removeStatusItem:tray->statusItem]; + + if (tray->menu) { + DestroySDLMenu(tray->menu); + } + + SDL_free(tray); +} + +#endif // SDL_PLATFORM_MACOS diff --git a/contrib/SDL-3.2.8/src/tray/dummy/SDL_tray.c b/contrib/SDL-3.2.8/src/tray/dummy/SDL_tray.c new file mode 100644 index 0000000..766fb92 --- /dev/null +++ b/contrib/SDL-3.2.8/src/tray/dummy/SDL_tray.c @@ -0,0 +1,143 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#include "SDL_internal.h" + +#ifndef SDL_PLATFORM_MACOS + +#include "../SDL_tray_utils.h" + +void SDL_UpdateTrays(void) +{ +} + +SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip) +{ + SDL_Unsupported(); + return NULL; +} + +void SDL_SetTrayIcon(SDL_Tray *tray, SDL_Surface *icon) +{ +} + +void SDL_SetTrayTooltip(SDL_Tray *tray, const char *tooltip) +{ +} + +SDL_TrayMenu *SDL_CreateTrayMenu(SDL_Tray *tray) +{ + SDL_InvalidParamError("tray"); + return NULL; +} + +SDL_TrayMenu *SDL_GetTrayMenu(SDL_Tray *tray) +{ + SDL_InvalidParamError("tray"); + return NULL; +} + +SDL_TrayMenu *SDL_CreateTraySubmenu(SDL_TrayEntry *entry) +{ + SDL_InvalidParamError("entry"); + return NULL; +} + +SDL_TrayMenu *SDL_GetTraySubmenu(SDL_TrayEntry *entry) +{ + return NULL; +} + +const SDL_TrayEntry **SDL_GetTrayEntries(SDL_TrayMenu *menu, int *count) +{ + SDL_InvalidParamError("menu"); + return NULL; +} + +void SDL_RemoveTrayEntry(SDL_TrayEntry *entry) +{ +} + +SDL_TrayEntry *SDL_InsertTrayEntryAt(SDL_TrayMenu *menu, int pos, const char *label, SDL_TrayEntryFlags flags) +{ + SDL_InvalidParamError("menu"); + return NULL; +} + +void SDL_SetTrayEntryLabel(SDL_TrayEntry *entry, const char *label) +{ +} + +const char *SDL_GetTrayEntryLabel(SDL_TrayEntry *entry) +{ + SDL_InvalidParamError("entry"); + return NULL; +} + +void SDL_SetTrayEntryChecked(SDL_TrayEntry *entry, bool checked) +{ +} + +bool SDL_GetTrayEntryChecked(SDL_TrayEntry *entry) +{ + return SDL_InvalidParamError("entry"); +} + +void SDL_SetTrayEntryEnabled(SDL_TrayEntry *entry, bool enabled) +{ +} + +bool SDL_GetTrayEntryEnabled(SDL_TrayEntry *entry) +{ + return SDL_InvalidParamError("entry"); +} + +void SDL_SetTrayEntryCallback(SDL_TrayEntry *entry, SDL_TrayCallback callback, void *userdata) +{ +} + +void SDL_ClickTrayEntry(SDL_TrayEntry *entry) +{ +} + +SDL_TrayMenu *SDL_GetTrayEntryParent(SDL_TrayEntry *entry) +{ + SDL_InvalidParamError("entry"); + return NULL; +} + +SDL_TrayEntry *SDL_GetTrayMenuParentEntry(SDL_TrayMenu *menu) +{ + SDL_InvalidParamError("menu"); + return NULL; +} + +SDL_Tray *SDL_GetTrayMenuParentTray(SDL_TrayMenu *menu) +{ + SDL_InvalidParamError("menu"); + return NULL; +} + +void SDL_DestroyTray(SDL_Tray *tray) +{ +} + +#endif // !SDL_PLATFORM_MACOS diff --git a/contrib/SDL-3.2.8/src/tray/unix/SDL_tray.c b/contrib/SDL-3.2.8/src/tray/unix/SDL_tray.c new file mode 100644 index 0000000..dc2d0ca --- /dev/null +++ b/contrib/SDL-3.2.8/src/tray/unix/SDL_tray.c @@ -0,0 +1,817 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#include "SDL_internal.h" + +#include "../SDL_tray_utils.h" + +#include +#include + +/* getpid() */ +#include + +/* APPINDICATOR_HEADER is not exposed as a build setting, but the code has been + written nevertheless to make future maintenance easier. */ +#ifdef APPINDICATOR_HEADER +#include APPINDICATOR_HEADER +#else +/* ------------------------------------------------------------------------- */ +/* BEGIN THIRD-PARTY HEADER CONTENT */ +/* ------------------------------------------------------------------------- */ +/* Glib 2.0 */ + +typedef unsigned long gulong; +typedef void* gpointer; +typedef char gchar; +typedef int gint; +typedef unsigned int guint; +typedef gint gboolean; +typedef void (*GCallback)(void); +typedef struct _GClosure GClosure; +typedef void (*GClosureNotify) (gpointer data, GClosure *closure); +typedef gboolean (*GSourceFunc) (gpointer user_data); +typedef enum +{ + G_CONNECT_AFTER = 1 << 0, + G_CONNECT_SWAPPED = 1 << 1 +} GConnectFlags; + +static gulong (*g_signal_connect_data)(gpointer instance, const gchar *detailed_signal, GCallback c_handler, gpointer data, GClosureNotify destroy_data, GConnectFlags connect_flags); +static void (*g_object_unref)(gpointer object); +static gchar *(*g_mkdtemp)(gchar *template); +gpointer (*g_object_ref_sink)(gpointer object); +gpointer (*g_object_ref)(gpointer object); + +// glib_typeof requires compiler-specific code and includes that are too complex +// to be worth copy-pasting here +//#define g_object_ref(Obj) ((glib_typeof (Obj)) (g_object_ref) (Obj)) +//#define g_object_ref_sink(Obj) ((glib_typeof (Obj)) (g_object_ref_sink) (Obj)) + +#define g_signal_connect(instance, detailed_signal, c_handler, data) \ + g_signal_connect_data ((instance), (detailed_signal), (c_handler), (data), NULL, (GConnectFlags) 0) + +#define _G_TYPE_CIC(ip, gt, ct) ((ct*) ip) + +#define G_TYPE_CHECK_INSTANCE_CAST(instance, g_type, c_type) (_G_TYPE_CIC ((instance), (g_type), c_type)) + +#define G_CALLBACK(f) ((GCallback) (f)) + +#define FALSE 0 +#define TRUE 1 + +/* GTK 3.0 */ + +typedef struct _GtkMenu GtkMenu; +typedef struct _GtkMenuItem GtkMenuItem; +typedef struct _GtkMenuShell GtkMenuShell; +typedef struct _GtkWidget GtkWidget; +typedef struct _GtkCheckMenuItem GtkCheckMenuItem; + +static gboolean (*gtk_init_check)(int *argc, char ***argv); +static gboolean (*gtk_main_iteration_do)(gboolean blocking); +static GtkWidget* (*gtk_menu_new)(void); +static GtkWidget* (*gtk_separator_menu_item_new)(void); +static GtkWidget* (*gtk_menu_item_new_with_label)(const gchar *label); +static void (*gtk_menu_item_set_submenu)(GtkMenuItem *menu_item, GtkWidget *submenu); +static GtkWidget* (*gtk_check_menu_item_new_with_label)(const gchar *label); +static void (*gtk_check_menu_item_set_active)(GtkCheckMenuItem *check_menu_item, gboolean is_active); +static void (*gtk_widget_set_sensitive)(GtkWidget *widget, gboolean sensitive); +static void (*gtk_widget_show)(GtkWidget *widget); +static void (*gtk_menu_shell_append)(GtkMenuShell *menu_shell, GtkWidget *child); +static void (*gtk_menu_shell_insert)(GtkMenuShell *menu_shell, GtkWidget *child, gint position); +static void (*gtk_widget_destroy)(GtkWidget *widget); +static const gchar *(*gtk_menu_item_get_label)(GtkMenuItem *menu_item); +static void (*gtk_menu_item_set_label)(GtkMenuItem *menu_item, const gchar *label); +static gboolean (*gtk_check_menu_item_get_active)(GtkCheckMenuItem *check_menu_item); +static gboolean (*gtk_widget_get_sensitive)(GtkWidget *widget); + +#define GTK_MENU_ITEM(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GTK_TYPE_MENU_ITEM, GtkMenuItem)) +#define GTK_WIDGET(widget) (G_TYPE_CHECK_INSTANCE_CAST ((widget), GTK_TYPE_WIDGET, GtkWidget)) +#define GTK_CHECK_MENU_ITEM(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GTK_TYPE_CHECK_MENU_ITEM, GtkCheckMenuItem)) +#define GTK_MENU(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GTK_TYPE_MENU, GtkMenu)) + +/* AppIndicator */ + +typedef enum { + APP_INDICATOR_CATEGORY_APPLICATION_STATUS, + APP_INDICATOR_CATEGORY_COMMUNICATIONS, + APP_INDICATOR_CATEGORY_SYSTEM_SERVICES, + APP_INDICATOR_CATEGORY_HARDWARE, + APP_INDICATOR_CATEGORY_OTHER +} AppIndicatorCategory; + +typedef enum { + APP_INDICATOR_STATUS_PASSIVE, + APP_INDICATOR_STATUS_ACTIVE, + APP_INDICATOR_STATUS_ATTENTION +} AppIndicatorStatus; + +typedef struct _AppIndicator AppIndicator; + +static AppIndicator *(*app_indicator_new)(const gchar *id, const gchar *icon_name, AppIndicatorCategory category); +static void (*app_indicator_set_status)(AppIndicator *self, AppIndicatorStatus status); +static void (*app_indicator_set_icon)(AppIndicator *self, const gchar *icon_name); +static void (*app_indicator_set_menu)(AppIndicator *self, GtkMenu *menu); + +/* ------------------------------------------------------------------------- */ +/* END THIRD-PARTY HEADER CONTENT */ +/* ------------------------------------------------------------------------- */ +#endif + +#ifdef APPINDICATOR_HEADER + +static void quit_gtk(void) +{ +} + +static bool init_gtk(void) +{ + return true; +} + +#else + +static bool gtk_is_init = false; + +static void *libappindicator = NULL; +static void *libgtk = NULL; +static void *libgdk = NULL; + +static void quit_gtk(void) +{ + if (libappindicator) { + dlclose(libappindicator); + libappindicator = NULL; + } + + if (libgtk) { + dlclose(libgtk); + libgtk = NULL; + } + + if (libgdk) { + dlclose(libgdk); + libgdk = NULL; + } + + gtk_is_init = false; +} + +const char *appindicator_names[] = { +#ifdef SDL_PLATFORM_OPENBSD + "libayatana-appindicator3.so", + "libappindicator3.so", +#else + "libayatana-appindicator3.so.1", + "libappindicator3.so.1", +#endif + NULL +}; + +const char *gtk_names[] = { +#ifdef SDL_PLATFORM_OPENBSD + "libgtk-3.so", +#else + "libgtk-3.so.0", +#endif + NULL +}; + +const char *gdk_names[] = { +#ifdef SDL_PLATFORM_OPENBSD + "libgdk-3.so", +#else + "libgdk-3.so.0", +#endif + NULL +}; + +static void *find_lib(const char **names) +{ + const char **name_ptr = names; + void *handle = NULL; + + do { + handle = dlopen(*name_ptr, RTLD_LAZY); + } while (*++name_ptr && !handle); + + return handle; +} + +static bool init_gtk(void) +{ + if (gtk_is_init) { + return true; + } + + libappindicator = find_lib(appindicator_names); + libgtk = find_lib(gtk_names); + libgdk = find_lib(gdk_names); + + if (!libappindicator || !libgtk || !libgdk) { + quit_gtk(); + return SDL_SetError("Could not load GTK/AppIndicator libraries"); + } + + gtk_init_check = dlsym(libgtk, "gtk_init_check"); + gtk_main_iteration_do = dlsym(libgtk, "gtk_main_iteration_do"); + gtk_menu_new = dlsym(libgtk, "gtk_menu_new"); + gtk_separator_menu_item_new = dlsym(libgtk, "gtk_separator_menu_item_new"); + gtk_menu_item_new_with_label = dlsym(libgtk, "gtk_menu_item_new_with_label"); + gtk_menu_item_set_submenu = dlsym(libgtk, "gtk_menu_item_set_submenu"); + gtk_check_menu_item_new_with_label = dlsym(libgtk, "gtk_check_menu_item_new_with_label"); + gtk_check_menu_item_set_active = dlsym(libgtk, "gtk_check_menu_item_set_active"); + gtk_widget_set_sensitive = dlsym(libgtk, "gtk_widget_set_sensitive"); + gtk_widget_show = dlsym(libgtk, "gtk_widget_show"); + gtk_menu_shell_append = dlsym(libgtk, "gtk_menu_shell_append"); + gtk_menu_shell_insert = dlsym(libgtk, "gtk_menu_shell_insert"); + gtk_widget_destroy = dlsym(libgtk, "gtk_widget_destroy"); + gtk_menu_item_get_label = dlsym(libgtk, "gtk_menu_item_get_label"); + gtk_menu_item_set_label = dlsym(libgtk, "gtk_menu_item_set_label"); + gtk_check_menu_item_get_active = dlsym(libgtk, "gtk_check_menu_item_get_active"); + gtk_widget_get_sensitive = dlsym(libgtk, "gtk_widget_get_sensitive"); + + /* Technically these are GLib or GObject functions, but we can find + * them via GDK */ + g_mkdtemp = dlsym(libgdk, "g_mkdtemp"); + g_signal_connect_data = dlsym(libgdk, "g_signal_connect_data"); + g_object_unref = dlsym(libgdk, "g_object_unref"); + g_object_ref_sink = dlsym(libgdk, "g_object_ref_sink"); + g_object_ref = dlsym(libgdk, "g_object_ref"); + + app_indicator_new = dlsym(libappindicator, "app_indicator_new"); + app_indicator_set_status = dlsym(libappindicator, "app_indicator_set_status"); + app_indicator_set_icon = dlsym(libappindicator, "app_indicator_set_icon"); + app_indicator_set_menu = dlsym(libappindicator, "app_indicator_set_menu"); + + if (!gtk_init_check || + !gtk_main_iteration_do || + !gtk_menu_new || + !gtk_separator_menu_item_new || + !gtk_menu_item_new_with_label || + !gtk_menu_item_set_submenu || + !gtk_check_menu_item_new_with_label || + !gtk_check_menu_item_set_active || + !gtk_widget_set_sensitive || + !gtk_widget_show || + !gtk_menu_shell_append || + !gtk_menu_shell_insert || + !gtk_widget_destroy || + !g_mkdtemp || + !g_object_ref_sink || + !g_object_ref || + !g_signal_connect_data || + !g_object_unref || + !app_indicator_new || + !app_indicator_set_status || + !app_indicator_set_icon || + !app_indicator_set_menu || + !gtk_menu_item_get_label || + !gtk_menu_item_set_label || + !gtk_check_menu_item_get_active || + !gtk_widget_get_sensitive) { + quit_gtk(); + return SDL_SetError("Could not load GTK/AppIndicator functions"); + } + + if (gtk_init_check(0, NULL) == FALSE) { + quit_gtk(); + return SDL_SetError("Could not init GTK"); + } + + gtk_is_init = true; + + return true; +} +#endif + +struct SDL_TrayMenu { + GtkMenuShell *menu; + + int nEntries; + SDL_TrayEntry **entries; + + SDL_Tray *parent_tray; + SDL_TrayEntry *parent_entry; +}; + +struct SDL_TrayEntry { + SDL_TrayMenu *parent; + GtkWidget *item; + + /* Checkboxes are "activated" when programmatically checked/unchecked; this + is a workaround. */ + bool ignore_signal; + + SDL_TrayEntryFlags flags; + SDL_TrayCallback callback; + void *userdata; + SDL_TrayMenu *submenu; +}; + +/* Template for g_mkdtemp(). The Xs will get replaced with a random + * directory name, which is created safely and atomically. */ +#define ICON_DIR_TEMPLATE "/tmp/SDL-tray-XXXXXX" + +struct SDL_Tray { + AppIndicator *indicator; + SDL_TrayMenu *menu; + char icon_dir[sizeof(ICON_DIR_TEMPLATE)]; + char icon_path[256]; + + GtkMenuShell *menu_cached; +}; + +static void call_callback(GtkMenuItem *item, gpointer ptr) +{ + SDL_TrayEntry *entry = ptr; + + /* Not needed with AppIndicator, may be needed with other frameworks */ + /* if (entry->flags & SDL_TRAYENTRY_CHECKBOX) { + SDL_SetTrayEntryChecked(entry, !SDL_GetTrayEntryChecked(entry)); + } */ + + if (entry->ignore_signal) { + return; + } + + if (entry->callback) { + entry->callback(entry->userdata, entry); + } +} + +static bool new_tmp_filename(SDL_Tray *tray) +{ + static int count = 0; + + int would_have_written = SDL_snprintf(tray->icon_path, sizeof(tray->icon_path), "%s/%d.bmp", tray->icon_dir, count++); + + if (would_have_written > 0 && ((unsigned) would_have_written) < sizeof(tray->icon_path) - 1) { + return true; + } + + tray->icon_path[0] = '\0'; + SDL_SetError("Failed to format new temporary filename"); + return false; +} + +static const char *get_appindicator_id(void) +{ + static int count = 0; + static char buffer[256]; + + int would_have_written = SDL_snprintf(buffer, sizeof(buffer), "sdl-appindicator-%d-%d", getpid(), count++); + + if (would_have_written <= 0 || would_have_written >= sizeof(buffer) - 1) { + SDL_SetError("Couldn't fit %d bytes in buffer of size %d", would_have_written, (int) sizeof(buffer)); + return NULL; + } + + return buffer; +} + +static void DestroySDLMenu(SDL_TrayMenu *menu) +{ + for (int i = 0; i < menu->nEntries; i++) { + if (menu->entries[i] && menu->entries[i]->submenu) { + DestroySDLMenu(menu->entries[i]->submenu); + } + SDL_free(menu->entries[i]); + } + + if (menu->menu) { + g_object_unref(menu->menu); + } + + SDL_free(menu->entries); + SDL_free(menu); +} + +void SDL_UpdateTrays(void) +{ + if (SDL_HasActiveTrays()) { + gtk_main_iteration_do(FALSE); + } +} + +SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip) +{ + if (!SDL_IsMainThread()) { + SDL_SetError("This function should be called on the main thread"); + return NULL; + } + + if (init_gtk() != true) { + return NULL; + } + + SDL_Tray *tray = (SDL_Tray *)SDL_calloc(1, sizeof(*tray)); + if (!tray) { + return NULL; + } + + /* On success, g_mkdtemp edits its argument in-place to replace the Xs + * with a random directory name, which it creates safely and atomically. + * On failure, it sets errno. */ + SDL_strlcpy(tray->icon_dir, ICON_DIR_TEMPLATE, sizeof(tray->icon_dir)); + if (!g_mkdtemp(tray->icon_dir)) { + SDL_SetError("Cannot create directory for tray icon: %s", strerror(errno)); + SDL_free(tray); + return NULL; + } + + if (icon) { + if (!new_tmp_filename(tray)) { + SDL_free(tray); + return NULL; + } + + SDL_SaveBMP(icon, tray->icon_path); + } + + tray->indicator = app_indicator_new(get_appindicator_id(), tray->icon_path, + APP_INDICATOR_CATEGORY_APPLICATION_STATUS); + + app_indicator_set_status(tray->indicator, APP_INDICATOR_STATUS_ACTIVE); + + // The tray icon isn't shown before a menu is created; create one early. + tray->menu_cached = (GtkMenuShell *) g_object_ref_sink(gtk_menu_new()); + app_indicator_set_menu(tray->indicator, GTK_MENU(tray->menu_cached)); + + SDL_RegisterTray(tray); + + return tray; +} + +void SDL_SetTrayIcon(SDL_Tray *tray, SDL_Surface *icon) +{ + if (!SDL_ObjectValid(tray, SDL_OBJECT_TYPE_TRAY)) { + return; + } + + if (*tray->icon_path) { + SDL_RemovePath(tray->icon_path); + } + + /* AppIndicator caches the icon files; always change filename to avoid caching */ + + if (icon && new_tmp_filename(tray)) { + SDL_SaveBMP(icon, tray->icon_path); + app_indicator_set_icon(tray->indicator, tray->icon_path); + } else { + *tray->icon_path = '\0'; + app_indicator_set_icon(tray->indicator, NULL); + } +} + +void SDL_SetTrayTooltip(SDL_Tray *tray, const char *tooltip) +{ + /* AppIndicator provides no tooltip support. */ +} + +SDL_TrayMenu *SDL_CreateTrayMenu(SDL_Tray *tray) +{ + if (!SDL_ObjectValid(tray, SDL_OBJECT_TYPE_TRAY)) { + SDL_InvalidParamError("tray"); + return NULL; + } + + tray->menu = (SDL_TrayMenu *)SDL_calloc(1, sizeof(*tray->menu)); + if (!tray->menu) { + return NULL; + } + + tray->menu->menu = g_object_ref(tray->menu_cached); + tray->menu->parent_tray = tray; + tray->menu->parent_entry = NULL; + tray->menu->nEntries = 0; + tray->menu->entries = NULL; + + return tray->menu; +} + +SDL_TrayMenu *SDL_GetTrayMenu(SDL_Tray *tray) +{ + if (!SDL_ObjectValid(tray, SDL_OBJECT_TYPE_TRAY)) { + SDL_InvalidParamError("tray"); + return NULL; + } + + return tray->menu; +} + +SDL_TrayMenu *SDL_CreateTraySubmenu(SDL_TrayEntry *entry) +{ + if (!entry) { + SDL_InvalidParamError("entry"); + return NULL; + } + + if (entry->submenu) { + SDL_SetError("Tray entry submenu already exists"); + return NULL; + } + + if (!(entry->flags & SDL_TRAYENTRY_SUBMENU)) { + SDL_SetError("Cannot create submenu for entry not created with SDL_TRAYENTRY_SUBMENU"); + return NULL; + } + + entry->submenu = (SDL_TrayMenu *)SDL_calloc(1, sizeof(*entry->submenu)); + if (!entry->submenu) { + return NULL; + } + + entry->submenu->menu = (GtkMenuShell *)gtk_menu_new(); + entry->submenu->parent_tray = NULL; + entry->submenu->parent_entry = entry; + entry->submenu->nEntries = 0; + entry->submenu->entries = NULL; + + gtk_menu_item_set_submenu(GTK_MENU_ITEM(entry->item), GTK_WIDGET(entry->submenu->menu)); + + return entry->submenu; +} + +SDL_TrayMenu *SDL_GetTraySubmenu(SDL_TrayEntry *entry) +{ + if (!entry) { + SDL_InvalidParamError("entry"); + return NULL; + } + + return entry->submenu; +} + +const SDL_TrayEntry **SDL_GetTrayEntries(SDL_TrayMenu *menu, int *count) +{ + if (!menu) { + SDL_InvalidParamError("menu"); + return NULL; + } + + if (count) { + *count = menu->nEntries; + } + return (const SDL_TrayEntry **)menu->entries; +} + +void SDL_RemoveTrayEntry(SDL_TrayEntry *entry) +{ + if (!entry) { + return; + } + + SDL_TrayMenu *menu = entry->parent; + + bool found = false; + for (int i = 0; i < menu->nEntries - 1; i++) { + if (menu->entries[i] == entry) { + found = true; + } + + if (found) { + menu->entries[i] = menu->entries[i + 1]; + } + } + + if (entry->submenu) { + DestroySDLMenu(entry->submenu); + } + + menu->nEntries--; + SDL_TrayEntry **new_entries = (SDL_TrayEntry **)SDL_realloc(menu->entries, (menu->nEntries + 1) * sizeof(*new_entries)); + + /* Not sure why shrinking would fail, but even if it does, we can live with a "too big" array */ + if (new_entries) { + menu->entries = new_entries; + menu->entries[menu->nEntries] = NULL; + } + + gtk_widget_destroy(entry->item); + SDL_free(entry); +} + +SDL_TrayEntry *SDL_InsertTrayEntryAt(SDL_TrayMenu *menu, int pos, const char *label, SDL_TrayEntryFlags flags) +{ + if (!menu) { + SDL_InvalidParamError("menu"); + return NULL; + } + + if (pos < -1 || pos > menu->nEntries) { + SDL_InvalidParamError("pos"); + return NULL; + } + + if (pos == -1) { + pos = menu->nEntries; + } + + SDL_TrayEntry *entry = (SDL_TrayEntry *)SDL_calloc(1, sizeof(*entry)); + if (!entry) { + return NULL; + } + + entry->parent = menu; + entry->item = NULL; + entry->ignore_signal = false; + entry->flags = flags; + entry->callback = NULL; + entry->userdata = NULL; + entry->submenu = NULL; + + if (label == NULL) { + entry->item = gtk_separator_menu_item_new(); + } else if (flags & SDL_TRAYENTRY_CHECKBOX) { + entry->item = gtk_check_menu_item_new_with_label(label); + gboolean active = ((flags & SDL_TRAYENTRY_CHECKED) != 0); + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(entry->item), active); + } else { + entry->item = gtk_menu_item_new_with_label(label); + } + + gboolean sensitive = ((flags & SDL_TRAYENTRY_DISABLED) == 0); + gtk_widget_set_sensitive(entry->item, sensitive); + + SDL_TrayEntry **new_entries = (SDL_TrayEntry **)SDL_realloc(menu->entries, (menu->nEntries + 2) * sizeof(*new_entries)); + + if (!new_entries) { + SDL_free(entry); + return NULL; + } + + menu->entries = new_entries; + menu->nEntries++; + + for (int i = menu->nEntries - 1; i > pos; i--) { + menu->entries[i] = menu->entries[i - 1]; + } + + new_entries[pos] = entry; + new_entries[menu->nEntries] = NULL; + + gtk_widget_show(entry->item); + gtk_menu_shell_insert(menu->menu, entry->item, (pos == menu->nEntries) ? -1 : pos); + + g_signal_connect(entry->item, "activate", G_CALLBACK(call_callback), entry); + + return entry; +} + +void SDL_SetTrayEntryLabel(SDL_TrayEntry *entry, const char *label) +{ + if (!entry) { + return; + } + + gtk_menu_item_set_label(GTK_MENU_ITEM(entry->item), label); +} + +const char *SDL_GetTrayEntryLabel(SDL_TrayEntry *entry) +{ + if (!entry) { + SDL_InvalidParamError("entry"); + return NULL; + } + + return gtk_menu_item_get_label(GTK_MENU_ITEM(entry->item)); +} + +void SDL_SetTrayEntryChecked(SDL_TrayEntry *entry, bool checked) +{ + if (!entry || !(entry->flags & SDL_TRAYENTRY_CHECKBOX)) { + return; + } + + entry->ignore_signal = true; + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(entry->item), checked); + entry->ignore_signal = false; +} + +bool SDL_GetTrayEntryChecked(SDL_TrayEntry *entry) +{ + if (!entry || !(entry->flags & SDL_TRAYENTRY_CHECKBOX)) { + return false; + } + + return gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(entry->item)); +} + +void SDL_SetTrayEntryEnabled(SDL_TrayEntry *entry, bool enabled) +{ + if (!entry) { + return; + } + + gtk_widget_set_sensitive(entry->item, enabled); +} + +bool SDL_GetTrayEntryEnabled(SDL_TrayEntry *entry) +{ + if (!entry) { + return false; + } + + return gtk_widget_get_sensitive(entry->item); +} + +void SDL_SetTrayEntryCallback(SDL_TrayEntry *entry, SDL_TrayCallback callback, void *userdata) +{ + if (!entry) { + return; + } + + entry->callback = callback; + entry->userdata = userdata; +} + +void SDL_ClickTrayEntry(SDL_TrayEntry *entry) +{ + if (!entry) { + return; + } + + if (entry->flags & SDL_TRAYENTRY_CHECKBOX) { + SDL_SetTrayEntryChecked(entry, !SDL_GetTrayEntryChecked(entry)); + } + + if (entry->callback) { + entry->callback(entry->userdata, entry); + } +} + +SDL_TrayMenu *SDL_GetTrayEntryParent(SDL_TrayEntry *entry) +{ + if (!entry) { + SDL_InvalidParamError("entry"); + return NULL; + } + + return entry->parent; +} + +SDL_TrayEntry *SDL_GetTrayMenuParentEntry(SDL_TrayMenu *menu) +{ + return menu->parent_entry; +} + +SDL_Tray *SDL_GetTrayMenuParentTray(SDL_TrayMenu *menu) +{ + if (!menu) { + SDL_InvalidParamError("menu"); + return NULL; + } + + return menu->parent_tray; +} + +void SDL_DestroyTray(SDL_Tray *tray) +{ + if (!SDL_ObjectValid(tray, SDL_OBJECT_TYPE_TRAY)) { + return; + } + + SDL_UnregisterTray(tray); + + if (tray->menu) { + DestroySDLMenu(tray->menu); + } + + if (*tray->icon_path) { + SDL_RemovePath(tray->icon_path); + } + + if (*tray->icon_dir) { + SDL_RemovePath(tray->icon_dir); + } + + if (tray->menu_cached) { + g_object_unref(tray->menu_cached); + } + + if (tray->indicator) { + g_object_unref(tray->indicator); + } + + SDL_free(tray); +} 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 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#include "SDL_internal.h" + +#include "../SDL_tray_utils.h" +#include "../../core/windows/SDL_windows.h" +#include "../../video/windows/SDL_windowswindow.h" + +#include +#include + +#include "../../video/windows/SDL_surface_utils.h" + +#ifndef NOTIFYICON_VERSION_4 +#define NOTIFYICON_VERSION_4 4 +#endif +#ifndef NIF_SHOWTIP +#define NIF_SHOWTIP 0x00000080 +#endif + +#define WM_TRAYICON (WM_USER + 1) + +struct SDL_TrayMenu { + HMENU hMenu; + + int nEntries; + SDL_TrayEntry **entries; + + SDL_Tray *parent_tray; + SDL_TrayEntry *parent_entry; +}; + +struct SDL_TrayEntry { + SDL_TrayMenu *parent; + UINT_PTR id; + + char label_cache[4096]; + SDL_TrayEntryFlags flags; + SDL_TrayCallback callback; + void *userdata; + SDL_TrayMenu *submenu; +}; + +struct SDL_Tray { + NOTIFYICONDATAW nid; + HWND hwnd; + HICON icon; + SDL_TrayMenu *menu; +}; + +static UINT_PTR get_next_id(void) +{ + static UINT_PTR next_id = 0; + return ++next_id; +} + +static SDL_TrayEntry *find_entry_in_menu(SDL_TrayMenu *menu, UINT_PTR id) +{ + for (int i = 0; i < menu->nEntries; i++) { + SDL_TrayEntry *entry = menu->entries[i]; + + if (entry->id == id) { + return entry; + } + + if (entry->submenu) { + SDL_TrayEntry *e = find_entry_in_menu(entry->submenu, id); + + if (e) { + return e; + } + } + } + + return NULL; +} + +static SDL_TrayEntry *find_entry_with_id(SDL_Tray *tray, UINT_PTR id) +{ + if (!tray->menu) { + return NULL; + } + + return find_entry_in_menu(tray->menu, id); +} + +LRESULT CALLBACK TrayWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { + SDL_Tray *tray = (SDL_Tray *) GetWindowLongPtr(hwnd, GWLP_USERDATA); + SDL_TrayEntry *entry = NULL; + + if (!tray) { + return DefWindowProc(hwnd, uMsg, wParam, lParam); + } + + switch (uMsg) { + case WM_TRAYICON: + if (LOWORD(lParam) == WM_CONTEXTMENU || LOWORD(lParam) == WM_LBUTTONUP) { + SetForegroundWindow(hwnd); + + if (tray->menu) { + TrackPopupMenu(tray->menu->hMenu, TPM_BOTTOMALIGN | TPM_RIGHTALIGN, GET_X_LPARAM(wParam), GET_Y_LPARAM(wParam), 0, hwnd, NULL); + } + } + break; + + case WM_COMMAND: + entry = find_entry_with_id(tray, LOWORD(wParam)); + + if (entry && (entry->flags & SDL_TRAYENTRY_CHECKBOX)) { + SDL_SetTrayEntryChecked(entry, !SDL_GetTrayEntryChecked(entry)); + } + + if (entry && entry->callback) { + entry->callback(entry->userdata, entry); + } + break; + + case WM_SETTINGCHANGE: + if (wParam == 0 && lParam != 0 && SDL_wcscmp((wchar_t *)lParam, L"ImmersiveColorSet") == 0) { + WIN_UpdateDarkModeForHWND(hwnd); + } + break; + + default: + return DefWindowProc(hwnd, uMsg, wParam, lParam); + } + return 0; +} + +static void DestroySDLMenu(SDL_TrayMenu *menu) +{ + for (int i = 0; i < menu->nEntries; i++) { + if (menu->entries[i] && menu->entries[i]->submenu) { + DestroySDLMenu(menu->entries[i]->submenu); + } + SDL_free(menu->entries[i]); + } + SDL_free(menu->entries); + DestroyMenu(menu->hMenu); + SDL_free(menu); +} + +static wchar_t *escape_label(const char *in) +{ + const char *c; + char *c2; + int len = 0; + + for (c = in; *c; c++) { + len += (*c == '&') ? 2 : 1; + } + + char *escaped = (char *)SDL_malloc(SDL_strlen(in) + len + 1); + if (!escaped) { + return NULL; + } + + for (c = in, c2 = escaped; *c;) { + if (*c == '&') { + *c2++ = *c; + } + + *c2++ = *c++; + } + + *c2 = '\0'; + + wchar_t *out = WIN_UTF8ToStringW(escaped); + SDL_free(escaped); + + return out; +} + +static HICON load_default_icon() +{ + HINSTANCE hInstance = GetModuleHandle(NULL); + if (!hInstance) { + return LoadIcon(NULL, IDI_APPLICATION); + } + + const char *hint = SDL_GetHint(SDL_HINT_WINDOWS_INTRESOURCE_ICON_SMALL); + if (hint && *hint) { + HICON icon = LoadIcon(hInstance, MAKEINTRESOURCE(SDL_atoi(hint))); + return icon ? icon : LoadIcon(NULL, IDI_APPLICATION); + } + + hint = SDL_GetHint(SDL_HINT_WINDOWS_INTRESOURCE_ICON); + if (hint && *hint) { + HICON icon = LoadIcon(hInstance, MAKEINTRESOURCE(SDL_atoi(hint))); + return icon ? icon : LoadIcon(NULL, IDI_APPLICATION); + } + + return LoadIcon(NULL, IDI_APPLICATION); +} + +void SDL_UpdateTrays(void) +{ +} + +SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip) +{ + if (!SDL_IsMainThread()) { + SDL_SetError("This function should be called on the main thread"); + return NULL; + } + + SDL_Tray *tray = (SDL_Tray *)SDL_calloc(1, sizeof(*tray)); + + if (!tray) { + return NULL; + } + + tray->menu = NULL; + tray->hwnd = CreateWindowEx(0, TEXT("Message"), NULL, 0, 0, 0, 0, 0, HWND_MESSAGE, NULL, NULL, NULL); + SetWindowLongPtr(tray->hwnd, GWLP_WNDPROC, (LONG_PTR) TrayWindowProc); + + WIN_UpdateDarkModeForHWND(tray->hwnd); + + SDL_zero(tray->nid); + tray->nid.cbSize = sizeof(NOTIFYICONDATAW); + tray->nid.hWnd = tray->hwnd; + tray->nid.uID = (UINT) get_next_id(); + tray->nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP | NIF_SHOWTIP; + tray->nid.uCallbackMessage = WM_TRAYICON; + tray->nid.uVersion = NOTIFYICON_VERSION_4; + wchar_t *tooltipw = WIN_UTF8ToStringW(tooltip); + SDL_wcslcpy(tray->nid.szTip, tooltipw, sizeof(tray->nid.szTip) / sizeof(*tray->nid.szTip)); + SDL_free(tooltipw); + + if (icon) { + tray->nid.hIcon = CreateIconFromSurface(icon); + + if (!tray->nid.hIcon) { + tray->nid.hIcon = load_default_icon(); + } + + tray->icon = tray->nid.hIcon; + } else { + tray->nid.hIcon = load_default_icon(); + tray->icon = tray->nid.hIcon; + } + + Shell_NotifyIconW(NIM_ADD, &tray->nid); + Shell_NotifyIconW(NIM_SETVERSION, &tray->nid); + + SetWindowLongPtr(tray->hwnd, GWLP_USERDATA, (LONG_PTR) tray); + + SDL_RegisterTray(tray); + + return tray; +} + +void SDL_SetTrayIcon(SDL_Tray *tray, SDL_Surface *icon) +{ + if (!SDL_ObjectValid(tray, SDL_OBJECT_TYPE_TRAY)) { + return; + } + + if (tray->icon) { + DestroyIcon(tray->icon); + } + + if (icon) { + tray->nid.hIcon = CreateIconFromSurface(icon); + + if (!tray->nid.hIcon) { + tray->nid.hIcon = load_default_icon(); + } + + tray->icon = tray->nid.hIcon; + } else { + tray->nid.hIcon = load_default_icon(); + tray->icon = tray->nid.hIcon; + } + + Shell_NotifyIconW(NIM_MODIFY, &tray->nid); +} + +void SDL_SetTrayTooltip(SDL_Tray *tray, const char *tooltip) +{ + if (!SDL_ObjectValid(tray, SDL_OBJECT_TYPE_TRAY)) { + return; + } + + if (tooltip) { + wchar_t *tooltipw = WIN_UTF8ToStringW(tooltip); + SDL_wcslcpy(tray->nid.szTip, tooltipw, sizeof(tray->nid.szTip) / sizeof(*tray->nid.szTip)); + SDL_free(tooltipw); + } else { + tray->nid.szTip[0] = '\0'; + } + + Shell_NotifyIconW(NIM_MODIFY, &tray->nid); +} + +SDL_TrayMenu *SDL_CreateTrayMenu(SDL_Tray *tray) +{ + if (!SDL_ObjectValid(tray, SDL_OBJECT_TYPE_TRAY)) { + SDL_InvalidParamError("tray"); + return NULL; + } + + tray->menu = (SDL_TrayMenu *)SDL_calloc(1, sizeof(*tray->menu)); + + if (!tray->menu) { + return NULL; + } + + tray->menu->hMenu = CreatePopupMenu(); + tray->menu->parent_tray = tray; + tray->menu->parent_entry = NULL; + + return tray->menu; +} + +SDL_TrayMenu *SDL_GetTrayMenu(SDL_Tray *tray) +{ + if (!SDL_ObjectValid(tray, SDL_OBJECT_TYPE_TRAY)) { + SDL_InvalidParamError("tray"); + return NULL; + } + + return tray->menu; +} + +SDL_TrayMenu *SDL_CreateTraySubmenu(SDL_TrayEntry *entry) +{ + if (!entry) { + SDL_InvalidParamError("entry"); + return NULL; + } + + if (!entry->submenu) { + SDL_SetError("Cannot create submenu for entry not created with SDL_TRAYENTRY_SUBMENU"); + return NULL; + } + + return entry->submenu; +} + +SDL_TrayMenu *SDL_GetTraySubmenu(SDL_TrayEntry *entry) +{ + if (!entry) { + SDL_InvalidParamError("entry"); + return NULL; + } + + return entry->submenu; +} + +const SDL_TrayEntry **SDL_GetTrayEntries(SDL_TrayMenu *menu, int *count) +{ + if (!menu) { + SDL_InvalidParamError("menu"); + return NULL; + } + + if (count) { + *count = menu->nEntries; + } + return (const SDL_TrayEntry **)menu->entries; +} + +void SDL_RemoveTrayEntry(SDL_TrayEntry *entry) +{ + if (!entry) { + return; + } + + SDL_TrayMenu *menu = entry->parent; + + bool found = false; + for (int i = 0; i < menu->nEntries - 1; i++) { + if (menu->entries[i] == entry) { + found = true; + } + + if (found) { + menu->entries[i] = menu->entries[i + 1]; + } + } + + if (entry->submenu) { + DestroySDLMenu(entry->submenu); + } + + menu->nEntries--; + SDL_TrayEntry **new_entries = (SDL_TrayEntry **)SDL_realloc(menu->entries, (menu->nEntries + 1) * sizeof(*new_entries)); + + /* Not sure why shrinking would fail, but even if it does, we can live with a "too big" array */ + if (new_entries) { + menu->entries = new_entries; + menu->entries[menu->nEntries] = NULL; + } + + if (!DeleteMenu(menu->hMenu, (UINT) entry->id, MF_BYCOMMAND)) { + /* This is somewhat useless since we don't return anything, but might help with eventual bugs */ + SDL_SetError("Couldn't destroy tray entry"); + } + + SDL_free(entry); +} + +SDL_TrayEntry *SDL_InsertTrayEntryAt(SDL_TrayMenu *menu, int pos, const char *label, SDL_TrayEntryFlags flags) +{ + if (!menu) { + SDL_InvalidParamError("menu"); + return NULL; + } + + if (pos < -1 || pos > menu->nEntries) { + SDL_InvalidParamError("pos"); + return NULL; + } + + int windows_compatible_pos = pos; + + if (pos == -1) { + pos = menu->nEntries; + } else if (pos == menu->nEntries) { + windows_compatible_pos = -1; + } + + SDL_TrayEntry *entry = (SDL_TrayEntry *)SDL_calloc(1, sizeof(*entry)); + if (!entry) { + return NULL; + } + + wchar_t *label_w = NULL; + + if (label && (label_w = escape_label(label)) == NULL) { + SDL_free(entry); + return NULL; + } + + entry->parent = menu; + entry->flags = flags; + entry->callback = NULL; + entry->userdata = NULL; + entry->submenu = NULL; + SDL_snprintf(entry->label_cache, sizeof(entry->label_cache), "%s", label ? label : ""); + + if (label != NULL && flags & SDL_TRAYENTRY_SUBMENU) { + entry->submenu = (SDL_TrayMenu *)SDL_calloc(1, sizeof(*entry->submenu)); + if (!entry->submenu) { + SDL_free(entry); + SDL_free(label_w); + return NULL; + } + + entry->submenu->hMenu = CreatePopupMenu(); + entry->submenu->nEntries = 0; + entry->submenu->entries = NULL; + entry->submenu->parent_entry = entry; + entry->submenu->parent_tray = NULL; + + entry->id = (UINT_PTR) entry->submenu->hMenu; + } else { + entry->id = get_next_id(); + } + + SDL_TrayEntry **new_entries = (SDL_TrayEntry **)SDL_realloc(menu->entries, (menu->nEntries + 2) * sizeof(*new_entries)); + if (!new_entries) { + SDL_free(entry); + SDL_free(label_w); + if (entry->submenu) { + DestroyMenu(entry->submenu->hMenu); + SDL_free(entry->submenu); + } + return NULL; + } + + menu->entries = new_entries; + menu->nEntries++; + + for (int i = menu->nEntries - 1; i > pos; i--) { + menu->entries[i] = menu->entries[i - 1]; + } + + new_entries[pos] = entry; + new_entries[menu->nEntries] = NULL; + + if (label == NULL) { + InsertMenuW(menu->hMenu, windows_compatible_pos, MF_SEPARATOR | MF_BYPOSITION, entry->id, NULL); + } else { + UINT mf = MF_STRING | MF_BYPOSITION; + if (flags & SDL_TRAYENTRY_SUBMENU) { + mf = MF_POPUP; + } + + if (flags & SDL_TRAYENTRY_DISABLED) { + mf |= MF_DISABLED | MF_GRAYED; + } + + if (flags & SDL_TRAYENTRY_CHECKED) { + mf |= MF_CHECKED; + } + + InsertMenuW(menu->hMenu, windows_compatible_pos, mf, entry->id, label_w); + + SDL_free(label_w); + } + + return entry; +} + +void SDL_SetTrayEntryLabel(SDL_TrayEntry *entry, const char *label) +{ + if (!entry) { + return; + } + + SDL_snprintf(entry->label_cache, sizeof(entry->label_cache), "%s", label); + + wchar_t *label_w = escape_label(label); + + if (!label_w) { + return; + } + + MENUITEMINFOW mii; + mii.cbSize = sizeof(MENUITEMINFOW); + mii.fMask = MIIM_STRING; + + mii.dwTypeData = label_w; + mii.cch = (UINT) SDL_wcslen(label_w); + + if (!SetMenuItemInfoW(entry->parent->hMenu, (UINT) entry->id, TRUE, &mii)) { + SDL_SetError("Couldn't update tray entry label"); + } + + SDL_free(label_w); +} + +const char *SDL_GetTrayEntryLabel(SDL_TrayEntry *entry) +{ + if (!entry) { + SDL_InvalidParamError("entry"); + return NULL; + } + + return entry->label_cache; +} + +void SDL_SetTrayEntryChecked(SDL_TrayEntry *entry, bool checked) +{ + if (!entry || !(entry->flags & SDL_TRAYENTRY_CHECKBOX)) { + return; + } + + CheckMenuItem(entry->parent->hMenu, (UINT) entry->id, checked ? MF_CHECKED : MF_UNCHECKED); +} + +bool SDL_GetTrayEntryChecked(SDL_TrayEntry *entry) +{ + if (!entry || !(entry->flags & SDL_TRAYENTRY_CHECKBOX)) { + return false; + } + + MENUITEMINFOW mii; + mii.cbSize = sizeof(MENUITEMINFOW); + mii.fMask = MIIM_STATE; + + GetMenuItemInfoW(entry->parent->hMenu, (UINT) entry->id, FALSE, &mii); + + return ((mii.fState & MFS_CHECKED) != 0); +} + +void SDL_SetTrayEntryEnabled(SDL_TrayEntry *entry, bool enabled) +{ + if (!entry) { + return; + } + + EnableMenuItem(entry->parent->hMenu, (UINT) entry->id, MF_BYCOMMAND | (enabled ? MF_ENABLED : (MF_DISABLED | MF_GRAYED))); +} + +bool SDL_GetTrayEntryEnabled(SDL_TrayEntry *entry) +{ + if (!entry) { + return false; + } + + MENUITEMINFOW mii; + mii.cbSize = sizeof(MENUITEMINFOW); + mii.fMask = MIIM_STATE; + + GetMenuItemInfoW(entry->parent->hMenu, (UINT) entry->id, FALSE, &mii); + + return ((mii.fState & MFS_ENABLED) != 0); +} + +void SDL_SetTrayEntryCallback(SDL_TrayEntry *entry, SDL_TrayCallback callback, void *userdata) +{ + if (!entry) { + return; + } + + entry->callback = callback; + entry->userdata = userdata; +} + +void SDL_ClickTrayEntry(SDL_TrayEntry *entry) +{ + if (!entry) { + return; + } + + if (entry->flags & SDL_TRAYENTRY_CHECKBOX) { + SDL_SetTrayEntryChecked(entry, !SDL_GetTrayEntryChecked(entry)); + } + + if (entry->callback) { + entry->callback(entry->userdata, entry); + } +} + +SDL_TrayMenu *SDL_GetTrayEntryParent(SDL_TrayEntry *entry) +{ + if (!entry) { + SDL_InvalidParamError("entry"); + return NULL; + } + + return entry->parent; +} + +SDL_TrayEntry *SDL_GetTrayMenuParentEntry(SDL_TrayMenu *menu) +{ + if (!menu) { + SDL_InvalidParamError("menu"); + return NULL; + } + + return menu->parent_entry; +} + +SDL_Tray *SDL_GetTrayMenuParentTray(SDL_TrayMenu *menu) +{ + if (!menu) { + SDL_InvalidParamError("menu"); + return NULL; + } + + return menu->parent_tray; +} + +void SDL_DestroyTray(SDL_Tray *tray) +{ + if (!SDL_ObjectValid(tray, SDL_OBJECT_TYPE_TRAY)) { + return; + } + + SDL_UnregisterTray(tray); + + Shell_NotifyIconW(NIM_DELETE, &tray->nid); + + if (tray->menu) { + DestroySDLMenu(tray->menu); + } + + if (tray->icon) { + DestroyIcon(tray->icon); + } + + if (tray->hwnd) { + DestroyWindow(tray->hwnd); + } + + SDL_free(tray); +} -- cgit v1.2.3