Merge commit '206dcdedf195fb320913584180edb12c7731e396' as 'external/SDL'

This commit is contained in:
2026-04-02 14:13:02 +02:00
2200 changed files with 961856 additions and 0 deletions
+93
View File
@@ -0,0 +1,93 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2026 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);
SDL_ClearProperty(SDL_GetGlobalProperties(), SDL_PROP_TRAY_CLEANUP);
}
bool SDL_HasActiveTrays(void)
{
return (active_trays > 0);
}
+28
View File
@@ -0,0 +1,28 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2026 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"
#define SDL_PROP_TRAY_CLEANUP "SDL.internal.tray.cleanup"
extern void SDL_RegisterTray(SDL_Tray *tray);
extern void SDL_UnregisterTray(SDL_Tray *tray);
extern void SDL_CleanupTrays(void);
extern bool SDL_HasActiveTrays(void);
+662
View File
@@ -0,0 +1,662 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2026 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"
/* Forward declaration */
struct SDL_Tray;
/* Objective-C helper class to handle status item button clicks */
@interface SDLTrayClickHandler : NSObject
@property (nonatomic, assign) struct SDL_Tray *tray;
@property (nonatomic, strong) id middleClickMonitor;
- (void)handleClick:(id)sender;
- (void)startMonitoringMiddleClicks;
- (void)stopMonitoringMiddleClicks;
@end
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;
SDLTrayClickHandler *clickHandler;
void *userdata;
SDL_TrayClickCallback left_click_callback;
SDL_TrayClickCallback right_click_callback;
SDL_TrayClickCallback middle_click_callback;
};
@implementation SDLTrayClickHandler
- (void)handleClick:(id)sender
{
if (!self.tray) {
return;
}
NSEvent *event = [NSApp currentEvent];
NSUInteger buttonNumber = [event buttonNumber];
bool show_menu = false;
if (buttonNumber == 0) {
/* Left click */
if (self.tray->left_click_callback) {
show_menu = self.tray->left_click_callback(self.tray->userdata, self.tray);
} else {
show_menu = true;
}
} else if (buttonNumber == 1) {
/* Right click */
if (self.tray->right_click_callback) {
show_menu = self.tray->right_click_callback(self.tray->userdata, self.tray);
} else {
show_menu = true;
}
} else if (buttonNumber == 2) {
/* Middle click */
if (self.tray->middle_click_callback) {
self.tray->middle_click_callback(self.tray->userdata, self.tray);
}
}
if (show_menu && self.tray->menu) {
[self.tray->statusItem popUpStatusItemMenu:self.tray->menu->nsmenu];
}
}
- (void)startMonitoringMiddleClicks
{
if (self.middleClickMonitor) {
return;
}
__weak SDLTrayClickHandler *weakSelf = self;
self.middleClickMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskOtherMouseUp handler:^NSEvent *(NSEvent *event) {
SDLTrayClickHandler *strongSelf = weakSelf;
if (!strongSelf || !strongSelf.tray || [event buttonNumber] != 2) {
return event;
}
/* Check if the click is within the status item's button bounds */
NSPoint clickLocation = [event locationInWindow];
NSWindow *statusItemWindow = strongSelf.tray->statusItem.button.window;
if (statusItemWindow && event.window == statusItemWindow) {
NSPoint localPoint = [strongSelf.tray->statusItem.button convertPoint:clickLocation fromView:nil];
if (NSPointInRect(localPoint, strongSelf.tray->statusItem.button.bounds)) {
if (strongSelf.tray->middle_click_callback) {
strongSelf.tray->middle_click_callback(strongSelf.tray->userdata, strongSelf.tray);
}
}
}
return event;
}];
}
- (void)stopMonitoringMiddleClicks
{
if (self.middleClickMonitor) {
[NSEvent removeMonitor:self.middleClickMonitor];
self.middleClickMonitor = nil;
}
}
@end
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_CreateTrayWithProperties(SDL_PropertiesID props)
{
if (!SDL_IsMainThread()) {
SDL_SetError("This function should be called on the main thread");
return NULL;
}
SDL_Surface *icon = (SDL_Surface *)SDL_GetPointerProperty(props, SDL_PROP_TRAY_CREATE_ICON_POINTER, NULL);
const char *tooltip = SDL_GetStringProperty(props, SDL_PROP_TRAY_CREATE_TOOLTIP_STRING, 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->userdata = SDL_GetPointerProperty(props, SDL_PROP_TRAY_CREATE_USERDATA_POINTER, NULL);
tray->left_click_callback = (SDL_TrayClickCallback)SDL_GetPointerProperty(props, SDL_PROP_TRAY_CREATE_LEFTCLICK_CALLBACK_POINTER, NULL);
tray->right_click_callback = (SDL_TrayClickCallback)SDL_GetPointerProperty(props, SDL_PROP_TRAY_CREATE_RIGHTCLICK_CALLBACK_POINTER, NULL);
tray->middle_click_callback = (SDL_TrayClickCallback)SDL_GetPointerProperty(props, SDL_PROP_TRAY_CREATE_MIDDLECLICK_CALLBACK_POINTER, 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);
}
/* Create click handler and set up button to receive clicks */
tray->clickHandler = [[SDLTrayClickHandler alloc] init];
tray->clickHandler.tray = tray;
[tray->statusItem.button setTarget:tray->clickHandler];
[tray->statusItem.button setAction:@selector(handleClick:)];
[tray->statusItem.button sendActionOn:(NSEventMaskLeftMouseUp | NSEventMaskRightMouseUp)];
/* Start monitoring for middle clicks since status items don't receive them via the normal action mechanism */
[tray->clickHandler startMonitoringMiddleClicks];
SDL_RegisterTray(tray);
return tray;
}
SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip)
{
SDL_Tray *tray;
SDL_PropertiesID props = SDL_CreateProperties();
if (!props) {
return NULL;
}
if (icon) {
SDL_SetPointerProperty(props, SDL_PROP_TRAY_CREATE_ICON_POINTER, icon);
}
if (tooltip) {
SDL_SetStringProperty(props, SDL_PROP_TRAY_CREATE_TOOLTIP_STRING, tooltip);
}
tray = SDL_CreateTrayWithProperties(props);
SDL_DestroyProperties(props);
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)
{
CHECK_PARAM(!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];
/* Don't set menu on statusItem - we handle menu display manually in the click handler */
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)
{
CHECK_PARAM(!SDL_ObjectValid(tray, SDL_OBJECT_TYPE_TRAY)) {
SDL_InvalidParamError("tray");
return NULL;
}
return tray->menu;
}
SDL_TrayMenu *SDL_CreateTraySubmenu(SDL_TrayEntry *entry)
{
CHECK_PARAM(!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)
{
CHECK_PARAM(!entry) {
SDL_InvalidParamError("entry");
return NULL;
}
return entry->submenu;
}
const SDL_TrayEntry **SDL_GetTrayEntries(SDL_TrayMenu *menu, int *count)
{
CHECK_PARAM(!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)
{
CHECK_PARAM(!menu) {
SDL_InvalidParamError("menu");
return NULL;
}
CHECK_PARAM(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)
{
CHECK_PARAM(!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)
{
CHECK_PARAM(!entry) {
SDL_InvalidParamError("entry");
return NULL;
}
return entry->parent;
}
SDL_TrayEntry *SDL_GetTrayMenuParentEntry(SDL_TrayMenu *menu)
{
CHECK_PARAM(!menu) {
SDL_InvalidParamError("menu");
return NULL;
}
return menu->parent_entry;
}
SDL_Tray *SDL_GetTrayMenuParentTray(SDL_TrayMenu *menu)
{
CHECK_PARAM(!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);
}
if (tray->clickHandler) {
[tray->clickHandler stopMonitoringMiddleClicks];
tray->clickHandler.tray = NULL;
tray->clickHandler = nil;
}
SDL_free(tray);
}
#endif // SDL_PLATFORM_MACOS
+149
View File
@@ -0,0 +1,149 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2026 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_TRAY_DUMMY
#include "../SDL_tray_utils.h"
void SDL_UpdateTrays(void)
{
}
SDL_Tray *SDL_CreateTrayWithProperties(SDL_PropertiesID props)
{
SDL_Unsupported();
return NULL;
}
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_TRAY_DUMMY
+786
View File
@@ -0,0 +1,786 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2026 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 "../../video/SDL_stb_c.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
#include "../../core/unix/SDL_gtk.h"
/* ------------------------------------------------------------------------- */
/* BEGIN THIRD-PARTY HEADER CONTENT */
/* ------------------------------------------------------------------------- */
/* 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
static void *libappindicator = NULL;
static void quit_appindicator(void)
{
if (libappindicator) {
dlclose(libappindicator);
libappindicator = NULL;
}
}
const char *appindicator_names[] = {
#ifdef SDL_PLATFORM_OPENBSD
"libayatana-appindicator3.so",
"libappindicator3.so",
#else
"libayatana-appindicator3.so.1",
"libappindicator3.so.1",
#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_appindicator(void)
{
if (libappindicator) {
return true;
}
libappindicator = find_lib(appindicator_names);
if (!libappindicator) {
quit_appindicator();
return SDL_SetError("Could not load AppIndicator libraries");
}
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 (!app_indicator_new ||
!app_indicator_set_status ||
!app_indicator_set_icon ||
!app_indicator_set_menu) {
quit_appindicator();
return SDL_SetError("Could not load AppIndicator functions");
}
return true;
}
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;
};
struct SDL_Tray {
AppIndicator *indicator;
SDL_TrayMenu *menu;
char *icon_dir;
char *icon_path;
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_asprintf(&tray->icon_path, "%s/%d.png", tray->icon_dir, count++);
if (would_have_written >= 0) {
return true;
}
tray->icon_path = NULL;
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) {
SDL_GtkContext *gtk = SDL_Gtk_EnterContext();
if (gtk) {
gtk->g.object_unref(menu->menu);
SDL_Gtk_ExitContext(gtk);
}
}
SDL_free(menu->entries);
SDL_free(menu);
}
void SDL_UpdateTrays(void)
{
if (SDL_HasActiveTrays()) {
SDL_UpdateGtk();
}
}
SDL_Tray *SDL_CreateTrayWithProperties(SDL_PropertiesID props)
{
if (!SDL_IsMainThread()) {
SDL_SetError("This function should be called on the main thread");
return NULL;
}
if (!init_appindicator()) {
return NULL;
}
SDL_Surface *icon = (SDL_Surface *)SDL_GetPointerProperty(props, SDL_PROP_TRAY_CREATE_ICON_POINTER, NULL);
SDL_Tray *tray = NULL;
SDL_GtkContext *gtk = SDL_Gtk_EnterContext();
if (!gtk) {
goto tray_error;
}
tray = (SDL_Tray *)SDL_calloc(1, sizeof(*tray));
if (!tray) {
goto tray_error;
}
const gchar *cache_dir = gtk->g.get_user_cache_dir();
if (!cache_dir) {
SDL_SetError("Cannot get user cache directory: %s", strerror(errno));
goto tray_error;
}
char *sdl_dir;
SDL_asprintf(&sdl_dir, "%s/SDL", cache_dir);
if (!SDL_GetPathInfo(sdl_dir, NULL)) {
if (!SDL_CreateDirectory(sdl_dir)) {
SDL_SetError("Cannot create directory for tray icon: %s", strerror(errno));
goto sdl_dir_error;
}
}
/* 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_asprintf(&tray->icon_dir, "%s/tray-XXXXXX", sdl_dir);
if (!gtk->g.mkdtemp(tray->icon_dir)) {
SDL_SetError("Cannot create directory for tray icon: %s", strerror(errno));
goto icon_dir_error;
}
if (icon) {
if (!new_tmp_filename(tray)) {
goto icon_dir_error;
}
SDL_SavePNG(icon, tray->icon_path);
} else {
// allocate a dummy icon path
SDL_asprintf(&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 *)gtk->g.object_ref_sink(gtk->gtk.menu_new());
app_indicator_set_menu(tray->indicator, GTK_MENU(tray->menu_cached));
SDL_RegisterTray(tray);
SDL_Gtk_ExitContext(gtk);
SDL_free(sdl_dir);
return tray;
icon_dir_error:
SDL_free(tray->icon_dir);
sdl_dir_error:
SDL_free(sdl_dir);
tray_error:
SDL_free(tray);
if (gtk) {
SDL_Gtk_ExitContext(gtk);
}
return NULL;
}
SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip)
{
SDL_Tray *tray;
SDL_PropertiesID props = SDL_CreateProperties();
if (!props) {
return NULL;
}
if (icon) {
SDL_SetPointerProperty(props, SDL_PROP_TRAY_CREATE_ICON_POINTER, icon);
}
if (tooltip) {
SDL_SetStringProperty(props, SDL_PROP_TRAY_CREATE_TOOLTIP_STRING, tooltip);
}
tray = SDL_CreateTrayWithProperties(props);
SDL_DestroyProperties(props);
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);
SDL_free(tray->icon_path);
tray->icon_path = NULL;
}
/* AppIndicator caches the icon files; always change filename to avoid caching */
if (icon && new_tmp_filename(tray)) {
SDL_SavePNG(icon, tray->icon_path);
app_indicator_set_icon(tray->indicator, tray->icon_path);
} else {
SDL_free(tray->icon_path);
tray->icon_path = NULL;
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)
{
CHECK_PARAM(!SDL_ObjectValid(tray, SDL_OBJECT_TYPE_TRAY)) {
SDL_InvalidParamError("tray");
return NULL;
}
SDL_GtkContext *gtk = SDL_Gtk_EnterContext();
if (!gtk) {
return NULL;
}
tray->menu = (SDL_TrayMenu *)SDL_calloc(1, sizeof(*tray->menu));
if (!tray->menu) {
SDL_Gtk_ExitContext(gtk);
return NULL;
}
tray->menu->menu = gtk->g.object_ref(tray->menu_cached);
tray->menu->parent_tray = tray;
tray->menu->parent_entry = NULL;
tray->menu->nEntries = 0;
tray->menu->entries = NULL;
SDL_Gtk_ExitContext(gtk);
return tray->menu;
}
SDL_TrayMenu *SDL_GetTrayMenu(SDL_Tray *tray)
{
CHECK_PARAM(!SDL_ObjectValid(tray, SDL_OBJECT_TYPE_TRAY)) {
SDL_InvalidParamError("tray");
return NULL;
}
return tray->menu;
}
SDL_TrayMenu *SDL_CreateTraySubmenu(SDL_TrayEntry *entry)
{
CHECK_PARAM(!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_GtkContext *gtk = SDL_Gtk_EnterContext();
if (!gtk) {
return NULL;
}
entry->submenu = (SDL_TrayMenu *)SDL_calloc(1, sizeof(*entry->submenu));
if (!entry->submenu) {
SDL_Gtk_ExitContext(gtk);
return NULL;
}
entry->submenu->menu = gtk->g.object_ref_sink(gtk->gtk.menu_new());
entry->submenu->parent_tray = NULL;
entry->submenu->parent_entry = entry;
entry->submenu->nEntries = 0;
entry->submenu->entries = NULL;
gtk->gtk.menu_item_set_submenu(GTK_MENU_ITEM(entry->item), GTK_WIDGET(entry->submenu->menu));
SDL_Gtk_ExitContext(gtk);
return entry->submenu;
}
SDL_TrayMenu *SDL_GetTraySubmenu(SDL_TrayEntry *entry)
{
CHECK_PARAM(!entry) {
SDL_InvalidParamError("entry");
return NULL;
}
return entry->submenu;
}
const SDL_TrayEntry **SDL_GetTrayEntries(SDL_TrayMenu *menu, int *count)
{
CHECK_PARAM(!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;
}
SDL_GtkContext *gtk = SDL_Gtk_EnterContext();
if (gtk) {
gtk->gtk.widget_destroy(entry->item);
SDL_Gtk_ExitContext(gtk);
}
SDL_free(entry);
}
SDL_TrayEntry *SDL_InsertTrayEntryAt(SDL_TrayMenu *menu, int pos, const char *label, SDL_TrayEntryFlags flags)
{
CHECK_PARAM(!menu) {
SDL_InvalidParamError("menu");
return NULL;
}
CHECK_PARAM(pos < -1 || pos > menu->nEntries) {
SDL_InvalidParamError("pos");
return NULL;
}
if (pos == -1) {
pos = menu->nEntries;
}
SDL_TrayEntry *entry = NULL;
SDL_GtkContext *gtk = SDL_Gtk_EnterContext();
if (!gtk) {
goto error;
}
entry = (SDL_TrayEntry *)SDL_calloc(1, sizeof(*entry));
if (!entry) {
goto error;
}
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->gtk.separator_menu_item_new();
} else if (flags & SDL_TRAYENTRY_CHECKBOX) {
entry->item = gtk->gtk.check_menu_item_new_with_label(label);
gboolean active = ((flags & SDL_TRAYENTRY_CHECKED) != 0);
gtk->gtk.check_menu_item_set_active(GTK_CHECK_MENU_ITEM(entry->item), active);
} else {
entry->item = gtk->gtk.menu_item_new_with_label(label);
}
gboolean sensitive = ((flags & SDL_TRAYENTRY_DISABLED) == 0);
gtk->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) {
goto error;
}
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->gtk.widget_show(entry->item);
gtk->gtk.menu_shell_insert(menu->menu, entry->item, (pos == menu->nEntries) ? -1 : pos);
gtk->g.signal_connect(entry->item, "activate", call_callback, entry);
SDL_Gtk_ExitContext(gtk);
return entry;
error:
if (entry) {
SDL_free(entry);
}
if (gtk) {
SDL_Gtk_ExitContext(gtk);
}
return NULL;
}
void SDL_SetTrayEntryLabel(SDL_TrayEntry *entry, const char *label)
{
if (!entry) {
return;
}
SDL_GtkContext *gtk = SDL_Gtk_EnterContext();
if (gtk) {
gtk->gtk.menu_item_set_label(GTK_MENU_ITEM(entry->item), label);
SDL_Gtk_ExitContext(gtk);
}
}
const char *SDL_GetTrayEntryLabel(SDL_TrayEntry *entry)
{
CHECK_PARAM(!entry) {
SDL_InvalidParamError("entry");
return NULL;
}
const char *label = NULL;
SDL_GtkContext *gtk = SDL_Gtk_EnterContext();
if (gtk) {
label = gtk->gtk.menu_item_get_label(GTK_MENU_ITEM(entry->item));
SDL_Gtk_ExitContext(gtk);
}
return label;
}
void SDL_SetTrayEntryChecked(SDL_TrayEntry *entry, bool checked)
{
if (!entry || !(entry->flags & SDL_TRAYENTRY_CHECKBOX)) {
return;
}
SDL_GtkContext *gtk = SDL_Gtk_EnterContext();
if (gtk) {
entry->ignore_signal = true;
gtk->gtk.check_menu_item_set_active(GTK_CHECK_MENU_ITEM(entry->item), checked);
entry->ignore_signal = false;
SDL_Gtk_ExitContext(gtk);
}
}
bool SDL_GetTrayEntryChecked(SDL_TrayEntry *entry)
{
if (!entry || !(entry->flags & SDL_TRAYENTRY_CHECKBOX)) {
return false;
}
bool checked = false;
SDL_GtkContext *gtk = SDL_Gtk_EnterContext();
if (gtk) {
checked = gtk->gtk.check_menu_item_get_active(GTK_CHECK_MENU_ITEM(entry->item));
SDL_Gtk_ExitContext(gtk);
}
return checked;
}
void SDL_SetTrayEntryEnabled(SDL_TrayEntry *entry, bool enabled)
{
if (!entry) {
return;
}
SDL_GtkContext *gtk = SDL_Gtk_EnterContext();
if (gtk) {
gtk->gtk.widget_set_sensitive(entry->item, enabled);
SDL_Gtk_ExitContext(gtk);
}
}
bool SDL_GetTrayEntryEnabled(SDL_TrayEntry *entry)
{
if (!entry) {
return false;
}
bool enabled = false;
SDL_GtkContext *gtk = SDL_Gtk_EnterContext();
if (gtk) {
enabled = gtk->gtk.widget_get_sensitive(entry->item);
SDL_Gtk_ExitContext(gtk);
}
return 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)
{
CHECK_PARAM(!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)
{
CHECK_PARAM(!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);
SDL_free(tray->icon_path);
}
if (tray->icon_dir) {
SDL_RemovePath(tray->icon_dir);
SDL_free(tray->icon_dir);
}
SDL_GtkContext *gtk = SDL_Gtk_EnterContext();
if (gtk) {
if (tray->menu_cached) {
gtk->g.object_unref(tray->menu_cached);
}
if (tray->indicator) {
gtk->g.object_unref(tray->indicator);
}
SDL_Gtk_ExitContext(gtk);
}
SDL_free(tray);
}
+806
View File
@@ -0,0 +1,806 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2026 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 <windowsx.h>
#include <shellapi.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;
void *userdata;
SDL_TrayClickCallback left_click_callback;
SDL_TrayClickCallback right_click_callback;
SDL_TrayClickCallback middle_click_callback;
};
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);
}
static UINT s_taskbarRestart = 0;
if (s_taskbarRestart == 0) {
s_taskbarRestart = RegisterWindowMessage(TEXT("TaskbarCreated"));
}
if (uMsg == s_taskbarRestart) {
Shell_NotifyIconW(NIM_ADD, &tray->nid);
Shell_NotifyIconW(NIM_SETVERSION, &tray->nid);
}
switch (uMsg) {
case WM_TRAYICON:
{
bool show_menu = false;
switch (LOWORD(lParam)) {
case WM_LBUTTONUP:
if (tray->left_click_callback) {
show_menu = tray->left_click_callback(tray->userdata, tray);
} else {
show_menu = true;
}
break;
case WM_CONTEXTMENU:
if (tray->right_click_callback) {
show_menu = tray->right_click_callback(tray->userdata, tray);
} else {
show_menu = true;
}
break;
case WM_MBUTTONUP:
if (tray->middle_click_callback) {
tray->middle_click_callback(tray->userdata, tray);
}
break;
}
if (show_menu && tray->menu) {
SetForegroundWindow(hwnd);
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(void)
{
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)
{
}
static void SDL_PropTrayCleanupCb(void *userdata, void *cls)
{
WNDCLASSEX wcex;
wcex.hIcon = NULL;
wcex.hIconSm = NULL;
HINSTANCE h = GetModuleHandle(NULL);
if (GetClassInfoEx(h, cls, &wcex)) {
UnregisterClass(cls, h);
}
}
static bool SDL_RegisterTrayClass(LPCWSTR className)
{
SDL_PropertiesID props = SDL_GetGlobalProperties();
if (!props) {
return false;
}
if (SDL_GetPointerProperty(props, SDL_PROP_TRAY_CLEANUP, NULL) != NULL) {
return true;
}
WNDCLASSEX wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.hCursor = NULL;
wcex.hIcon = NULL;
wcex.hIconSm = NULL;
wcex.lpszMenuName = NULL;
wcex.lpszClassName = className;
wcex.style = 0;
wcex.hbrBackground = NULL;
wcex.lpfnWndProc = TrayWindowProc;
wcex.hInstance = NULL;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
if (!RegisterClassEx(&wcex)) {
return SDL_SetError("Couldn't register tray class");
}
SDL_SetPointerPropertyWithCleanup(props, SDL_PROP_TRAY_CLEANUP, (void *)wcex.lpszClassName, SDL_PropTrayCleanupCb, NULL);
return true;
}
SDL_Tray *SDL_CreateTrayWithProperties(SDL_PropertiesID props)
{
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;
}
SDL_Surface *icon = (SDL_Surface *)SDL_GetPointerProperty(props, SDL_PROP_TRAY_CREATE_ICON_POINTER, NULL);
const char *tooltip = SDL_GetStringProperty(props, SDL_PROP_TRAY_CREATE_TOOLTIP_STRING, NULL);
tray->userdata = SDL_GetPointerProperty(props, SDL_PROP_TRAY_CREATE_USERDATA_POINTER, NULL);
tray->left_click_callback = (SDL_TrayClickCallback)SDL_GetPointerProperty(props, SDL_PROP_TRAY_CREATE_LEFTCLICK_CALLBACK_POINTER, NULL);
tray->right_click_callback = (SDL_TrayClickCallback)SDL_GetPointerProperty(props, SDL_PROP_TRAY_CREATE_RIGHTCLICK_CALLBACK_POINTER, NULL);
tray->middle_click_callback = (SDL_TrayClickCallback)SDL_GetPointerProperty(props, SDL_PROP_TRAY_CREATE_MIDDLECLICK_CALLBACK_POINTER, NULL);
tray->menu = NULL;
if (!SDL_RegisterTrayClass(TEXT("SDL_TRAY"))) {
SDL_SetError("Failed to register SDL_TRAY window class");
SDL_free(tray);
return NULL;
}
tray->hwnd = CreateWindowEx(0, TEXT("SDL_TRAY"), NULL, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, NULL, NULL);
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;
if (tooltip) {
wchar_t *tooltipw = WIN_UTF8ToStringW(tooltip);
if(tooltipw) {
SDL_wcslcpy(tray->nid.szTip, tooltipw, sizeof(tray->nid.szTip) / sizeof(*tray->nid.szTip));
SDL_free(tooltipw);
}
}
if (icon) {
tray->nid.hIcon = WIN_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;
}
SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip)
{
SDL_Tray *tray;
SDL_PropertiesID props = SDL_CreateProperties();
if (!props) {
return NULL;
}
if (icon) {
SDL_SetPointerProperty(props, SDL_PROP_TRAY_CREATE_ICON_POINTER, icon);
}
if (tooltip) {
SDL_SetStringProperty(props, SDL_PROP_TRAY_CREATE_TOOLTIP_STRING, tooltip);
}
tray = SDL_CreateTrayWithProperties(props);
SDL_DestroyProperties(props);
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 = WIN_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)
{
CHECK_PARAM(!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)
{
CHECK_PARAM(!SDL_ObjectValid(tray, SDL_OBJECT_TYPE_TRAY)) {
SDL_InvalidParamError("tray");
return NULL;
}
return tray->menu;
}
SDL_TrayMenu *SDL_CreateTraySubmenu(SDL_TrayEntry *entry)
{
CHECK_PARAM(!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)
{
CHECK_PARAM(!entry) {
SDL_InvalidParamError("entry");
return NULL;
}
return entry->submenu;
}
const SDL_TrayEntry **SDL_GetTrayEntries(SDL_TrayMenu *menu, int *count)
{
CHECK_PARAM(!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)
{
CHECK_PARAM(!menu) {
SDL_InvalidParamError("menu");
return NULL;
}
CHECK_PARAM(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, FALSE, &mii)) {
SDL_SetError("Couldn't update tray entry label");
}
SDL_free(label_w);
}
const char *SDL_GetTrayEntryLabel(SDL_TrayEntry *entry)
{
CHECK_PARAM(!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)
{
CHECK_PARAM(!entry) {
SDL_InvalidParamError("entry");
return NULL;
}
return entry->parent;
}
SDL_TrayEntry *SDL_GetTrayMenuParentEntry(SDL_TrayMenu *menu)
{
CHECK_PARAM(!menu) {
SDL_InvalidParamError("menu");
return NULL;
}
return menu->parent_entry;
}
SDL_Tray *SDL_GetTrayMenuParentTray(SDL_TrayMenu *menu)
{
CHECK_PARAM(!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);
}