change build system to CMake and use SDL3 for everything

This commit is contained in:
Sven Balzer
2025-02-24 19:47:40 +01:00
parent e6a5a00dcb
commit 74e0d78a4c
2052 changed files with 981424 additions and 1461 deletions
+91
View File
@@ -0,0 +1,91 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
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);
}
+26
View File
@@ -0,0 +1,26 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
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);
+524
View File
@@ -0,0 +1,524 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
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 <Cocoa/Cocoa.h>
#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
+143
View File
@@ -0,0 +1,143 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
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
+791
View File
@@ -0,0 +1,791 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
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 <dlfcn.h>
#include <errno.h>
/* getpid() */
#include <unistd.h>
/* 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);
#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");
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_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];
};
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]);
}
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 (!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);
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 = (GtkMenuShell *)gtk_menu_new();
tray->menu->parent_tray = tray;
tray->menu->parent_entry = NULL;
tray->menu->nEntries = 0;
tray->menu->entries = NULL;
app_indicator_set_menu(tray->indicator, GTK_MENU(tray->menu->menu));
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->indicator) {
g_object_unref(tray->indicator);
}
SDL_free(tray);
}
+690
View File
@@ -0,0 +1,690 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
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 <windowsx.h>
#include <shellapi.h>
#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);
}