Add system tray support (#10873)

This commit is contained in:
Semphriss
2024-12-24 13:36:39 -05:00
committed by GitHub
parent 17a029502a
commit 01b9b0edb7
20 changed files with 3125 additions and 1 deletions

View File

@@ -1208,6 +1208,27 @@ SDL3_0.0.0 {
SDL_RenderTextureAffine;
SDL_WaitAndAcquireGPUSwapchainTexture;
SDL_RenderDebugTextFormat;
SDL_CreateTray;
SDL_SetTrayIcon;
SDL_SetTrayTooltip;
SDL_CreateTrayMenu;
SDL_CreateTraySubmenu;
SDL_GetTrayMenu;
SDL_GetTraySubmenu;
SDL_GetTrayEntries;
SDL_RemoveTrayEntry;
SDL_InsertTrayEntryAt;
SDL_SetTrayEntryLabel;
SDL_GetTrayEntryLabel;
SDL_SetTrayEntryChecked;
SDL_GetTrayEntryChecked;
SDL_SetTrayEntryEnabled;
SDL_GetTrayEntryEnabled;
SDL_SetTrayEntryCallback;
SDL_DestroyTray;
SDL_GetTrayEntryParent;
SDL_GetTrayMenuParentEntry;
SDL_GetTrayMenuParentTray;
# extra symbols go here (don't modify this line)
local: *;
};

View File

@@ -1233,3 +1233,24 @@
#define SDL_RenderTextureAffine SDL_RenderTextureAffine_REAL
#define SDL_WaitAndAcquireGPUSwapchainTexture SDL_WaitAndAcquireGPUSwapchainTexture_REAL
#define SDL_RenderDebugTextFormat SDL_RenderDebugTextFormat_REAL
#define SDL_CreateTray SDL_CreateTray_REAL
#define SDL_SetTrayIcon SDL_SetTrayIcon_REAL
#define SDL_SetTrayTooltip SDL_SetTrayTooltip_REAL
#define SDL_CreateTrayMenu SDL_CreateTrayMenu_REAL
#define SDL_CreateTraySubmenu SDL_CreateTraySubmenu_REAL
#define SDL_GetTrayMenu SDL_GetTrayMenu_REAL
#define SDL_GetTraySubmenu SDL_GetTraySubmenu_REAL
#define SDL_GetTrayEntries SDL_GetTrayEntries_REAL
#define SDL_RemoveTrayEntry SDL_RemoveTrayEntry_REAL
#define SDL_InsertTrayEntryAt SDL_InsertTrayEntryAt_REAL
#define SDL_SetTrayEntryLabel SDL_SetTrayEntryLabel_REAL
#define SDL_GetTrayEntryLabel SDL_GetTrayEntryLabel_REAL
#define SDL_SetTrayEntryChecked SDL_SetTrayEntryChecked_REAL
#define SDL_GetTrayEntryChecked SDL_GetTrayEntryChecked_REAL
#define SDL_SetTrayEntryEnabled SDL_SetTrayEntryEnabled_REAL
#define SDL_GetTrayEntryEnabled SDL_GetTrayEntryEnabled_REAL
#define SDL_SetTrayEntryCallback SDL_SetTrayEntryCallback_REAL
#define SDL_DestroyTray SDL_DestroyTray_REAL
#define SDL_GetTrayEntryParent SDL_GetTrayEntryParent_REAL
#define SDL_GetTrayMenuParentEntry SDL_GetTrayMenuParentEntry_REAL
#define SDL_GetTrayMenuParentTray SDL_GetTrayMenuParentTray_REAL

View File

@@ -1241,3 +1241,24 @@ SDL_DYNAPI_PROC(bool,SDL_WaitAndAcquireGPUSwapchainTexture,(SDL_GPUCommandBuffer
#ifndef SDL_DYNAPI_PROC_NO_VARARGS
SDL_DYNAPI_PROC(bool,SDL_RenderDebugTextFormat,(SDL_Renderer *a,float b,float c,SDL_PRINTF_FORMAT_STRING const char *d,...),(a,b,c,d),return)
#endif
SDL_DYNAPI_PROC(SDL_Tray*,SDL_CreateTray,(SDL_Surface *a,const char *b),(a,b),return)
SDL_DYNAPI_PROC(void,SDL_SetTrayIcon,(SDL_Tray *a,SDL_Surface *b),(a,b),)
SDL_DYNAPI_PROC(void,SDL_SetTrayTooltip,(SDL_Tray *a,const char *b),(a,b),)
SDL_DYNAPI_PROC(SDL_TrayMenu*,SDL_CreateTrayMenu,(SDL_Tray *a),(a),return)
SDL_DYNAPI_PROC(SDL_TrayMenu*,SDL_CreateTraySubmenu,(SDL_TrayEntry *a),(a),return)
SDL_DYNAPI_PROC(SDL_TrayMenu*,SDL_GetTrayMenu,(SDL_Tray *a),(a),return)
SDL_DYNAPI_PROC(SDL_TrayMenu*,SDL_GetTraySubmenu,(SDL_TrayEntry *a),(a),return)
SDL_DYNAPI_PROC(const SDL_TrayEntry**,SDL_GetTrayEntries,(SDL_TrayMenu *a,int *b),(a,b),return)
SDL_DYNAPI_PROC(void,SDL_RemoveTrayEntry,(SDL_TrayEntry *a),(a),)
SDL_DYNAPI_PROC(SDL_TrayEntry*,SDL_InsertTrayEntryAt,(SDL_TrayMenu *a,int b,const char *c,SDL_TrayEntryFlags d),(a,b,c,d),return)
SDL_DYNAPI_PROC(void,SDL_SetTrayEntryLabel,(SDL_TrayEntry *a,const char *b),(a,b),)
SDL_DYNAPI_PROC(const char*,SDL_GetTrayEntryLabel,(SDL_TrayEntry *a),(a),return)
SDL_DYNAPI_PROC(void,SDL_SetTrayEntryChecked,(SDL_TrayEntry *a,bool b),(a,b),)
SDL_DYNAPI_PROC(bool,SDL_GetTrayEntryChecked,(SDL_TrayEntry *a),(a),return)
SDL_DYNAPI_PROC(void,SDL_SetTrayEntryEnabled,(SDL_TrayEntry *a,bool b),(a,b),)
SDL_DYNAPI_PROC(bool,SDL_GetTrayEntryEnabled,(SDL_TrayEntry *a),(a),return)
SDL_DYNAPI_PROC(void,SDL_SetTrayEntryCallback,(SDL_TrayEntry *a,SDL_TrayCallback b,void *c),(a,b,c),)
SDL_DYNAPI_PROC(void,SDL_DestroyTray,(SDL_Tray *a),(a),)
SDL_DYNAPI_PROC(SDL_TrayMenu*,SDL_GetTrayEntryParent,(SDL_TrayEntry *a),(a),return)
SDL_DYNAPI_PROC(SDL_TrayEntry*,SDL_GetTrayMenuParentEntry,(SDL_TrayMenu *a),(a),return)
SDL_DYNAPI_PROC(SDL_Tray*,SDL_GetTrayMenuParentTray,(SDL_TrayMenu *a),(a),return)

458
src/tray/cocoa/SDL_tray.m Normal file
View File

@@ -0,0 +1,458 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2024 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 <Cocoa/Cocoa.h>
#include "../../video/SDL_surface_c.h"
/* applicationDockMenu */
struct SDL_TrayMenu {
NSMenu *nsmenu;
size_t 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 NSApplication *app = NULL;
@interface AppDelegate: NSObject <NSApplicationDelegate>
- (IBAction)menu:(id)sender;
@end
@implementation AppDelegate{}
- (IBAction)menu:(id)sender
{
SDL_TrayEntry *entry = [[sender representedObject] pointerValue];
if (!entry) {
return;
}
if (entry->flags & SDL_TRAYENTRY_CHECKBOX) {
SDL_SetTrayEntryChecked(entry, !SDL_GetTrayEntryChecked(entry));
}
if (entry->callback) {
entry->callback(entry->userdata, entry);
}
}
@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);
}
SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip)
{
SDL_Tray *tray = (SDL_Tray *) SDL_malloc(sizeof(SDL_Tray));
AppDelegate *delegate = [[AppDelegate alloc] init];
app = [NSApplication sharedApplication];
[app setDelegate:delegate];
if (!tray) {
return NULL;
}
SDL_memset((void *) tray, 0, sizeof(*tray));
tray->statusItem = nil;
tray->statusBar = [NSStatusBar systemStatusBar];
tray->statusItem = [tray->statusBar statusItemWithLength:NSVariableStatusItemLength];
[app activateIgnoringOtherApps:TRUE];
if (tooltip) {
tray->statusItem.button.toolTip = [NSString stringWithUTF8String:tooltip];
} else {
tray->statusItem.button.toolTip = nil;
}
if (icon) {
SDL_Surface *iconfmt = SDL_ConvertSurface(icon, SDL_PIXELFORMAT_RGBA32);
if (!iconfmt) {
goto skip_putting_an_icon;
}
NSBitmapImageRep *bitmap = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:(unsigned char **)&iconfmt->pixels
pixelsWide:iconfmt->w
pixelsHigh:iconfmt->h
bitsPerSample:8
samplesPerPixel:4
hasAlpha:YES
isPlanar:NO
colorSpaceName:NSCalibratedRGBColorSpace
bytesPerRow:iconfmt->pitch
bitsPerPixel:32];
NSImage *iconimg = [[NSImage alloc] initWithSize:NSMakeSize(iconfmt->w, iconfmt->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(iconfmt);
}
skip_putting_an_icon:
return tray;
}
void SDL_SetTrayIcon(SDL_Tray *tray, SDL_Surface *icon)
{
if (!icon) {
tray->statusItem.button.image = nil;
return;
}
SDL_Surface *iconfmt = SDL_ConvertSurface(icon, SDL_PIXELFORMAT_RGBA32);
if (!iconfmt) {
tray->statusItem.button.image = nil;
return;
}
NSBitmapImageRep *bitmap = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:(unsigned char **)&iconfmt->pixels
pixelsWide:iconfmt->w
pixelsHigh:iconfmt->h
bitsPerSample:8
samplesPerPixel:4
hasAlpha:YES
isPlanar:NO
colorSpaceName:NSCalibratedRGBColorSpace
bytesPerRow:iconfmt->pitch
bitsPerPixel:32];
NSImage *iconimg = [[NSImage alloc] initWithSize:NSMakeSize(iconfmt->w, iconfmt->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(iconfmt);
}
void SDL_SetTrayTooltip(SDL_Tray *tray, const char *tooltip)
{
if (tooltip) {
tray->statusItem.button.toolTip = [NSString stringWithUTF8String:tooltip];
} else {
tray->statusItem.button.toolTip = nil;
}
}
SDL_TrayMenu *SDL_CreateTrayMenu(SDL_Tray *tray)
{
SDL_TrayMenu *menu = SDL_malloc(sizeof(SDL_TrayMenu));
if (!menu) {
return NULL;
}
SDL_memset((void *) menu, 0, sizeof(*menu));
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)
{
return tray->menu;
}
SDL_TrayMenu *SDL_CreateTraySubmenu(SDL_TrayEntry *entry)
{
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_malloc(sizeof(SDL_TrayMenu));
if (!menu) {
return NULL;
}
SDL_memset((void *) menu, 0, sizeof(*menu));
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)
{
return entry->submenu;
}
const SDL_TrayEntry **SDL_GetTrayEntries(SDL_TrayMenu *menu, int *size)
{
if (size) {
*size = 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_realloc(menu->entries, menu->nEntries * sizeof(SDL_TrayEntry *));
/* 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->nsmenu removeItem:entry->nsitem];
SDL_free(entry);
}
SDL_TrayEntry *SDL_InsertTrayEntryAt(SDL_TrayMenu *menu, int pos, const char *label, SDL_TrayEntryFlags flags)
{
if (pos < -1 || pos > (int) menu->nEntries) {
SDL_InvalidParamError("pos");
return NULL;
}
if (pos == -1) {
pos = menu->nEntries;
}
SDL_TrayEntry *entry = SDL_malloc(sizeof(SDL_TrayEntry));
if (!entry) {
return NULL;
}
SDL_memset((void *) entry, 0, sizeof(*entry));
SDL_TrayEntry **new_entries = (SDL_TrayEntry **) SDL_realloc(menu->entries, (menu->nEntries + 1) * sizeof(SDL_TrayEntry *));
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;
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)
{
[entry->nsitem setTitle:[NSString stringWithUTF8String:label]];
}
const char *SDL_GetTrayEntryLabel(SDL_TrayEntry *entry)
{
return [[entry->nsitem title] UTF8String];
}
void SDL_SetTrayEntryChecked(SDL_TrayEntry *entry, bool checked)
{
[entry->nsitem setState:(checked ? NSControlStateValueOn : NSControlStateValueOff)];
}
bool SDL_GetTrayEntryChecked(SDL_TrayEntry *entry)
{
return entry->nsitem.state == NSControlStateValueOn;
}
void SDL_SetTrayEntryEnabled(SDL_TrayEntry *entry, bool enabled)
{
if (!(entry->flags & SDL_TRAYENTRY_CHECKBOX)) {
SDL_SetError("Cannot update check for entry not created with SDL_TRAYENTRY_CHECKBOX");
return;
}
[entry->nsitem setEnabled:(enabled ? YES : NO)];
}
bool SDL_GetTrayEntryEnabled(SDL_TrayEntry *entry)
{
if (!(entry->flags & SDL_TRAYENTRY_CHECKBOX)) {
SDL_SetError("Cannot fetch check for entry not created with SDL_TRAYENTRY_CHECKBOX");
return false;
}
return entry->nsitem.enabled;
}
void SDL_SetTrayEntryCallback(SDL_TrayEntry *entry, SDL_TrayCallback callback, void *userdata)
{
entry->callback = callback;
entry->userdata = userdata;
}
SDL_TrayMenu *SDL_GetTrayEntryParent(SDL_TrayEntry *entry)
{
return entry->parent;
}
SDL_TrayEntry *SDL_GetTrayMenuParentEntry(SDL_TrayMenu *menu)
{
return menu->parent_entry;
}
SDL_Tray *SDL_GetTrayMenuParentTray(SDL_TrayMenu *menu)
{
return menu->parent_tray;
}
void SDL_DestroyTray(SDL_Tray *tray)
{
if (!tray) {
return;
}
[[NSStatusBar systemStatusBar] removeStatusItem:tray->statusItem];
if (tray->menu) {
DestroySDLMenu(tray->menu);
}
SDL_free(tray);
}

139
src/tray/dummy/SDL_tray.c Normal file
View File

@@ -0,0 +1,139 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2024 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"
SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip)
{
SDL_Unsupported();
return NULL;
}
void SDL_SetTrayIcon(SDL_Tray *tray, SDL_Surface *icon)
{
SDL_Unsupported();
}
void SDL_SetTrayTooltip(SDL_Tray *tray, const char *tooltip)
{
SDL_Unsupported();
}
SDL_TrayMenu *SDL_CreateTrayMenu(SDL_Tray *tray)
{
SDL_Unsupported();
return NULL;
}
SDL_TrayMenu *SDL_GetTrayMenu(SDL_Tray *tray)
{
SDL_Unsupported();
return NULL;
}
SDL_TrayMenu *SDL_CreateTraySubmenu(SDL_TrayEntry *entry)
{
SDL_Unsupported();
return NULL;
}
SDL_TrayMenu *SDL_GetTraySubmenu(SDL_TrayEntry *entry)
{
return NULL;
}
const SDL_TrayEntry **SDL_GetTrayEntries(SDL_TrayMenu *menu, int *size)
{
SDL_Unsupported();
return NULL;
}
void SDL_RemoveTrayEntry(SDL_TrayEntry *entry)
{
SDL_Unsupported();
}
SDL_TrayEntry *SDL_InsertTrayEntryAt(SDL_TrayMenu *menu, int pos, const char *label, SDL_TrayEntryFlags flags)
{
SDL_Unsupported();
return NULL;
}
void SDL_SetTrayEntryLabel(SDL_TrayEntry *entry, const char *label)
{
SDL_Unsupported();
}
const char *SDL_GetTrayEntryLabel(SDL_TrayEntry *entry)
{
SDL_Unsupported();
return NULL;
}
void SDL_SetTrayEntryChecked(SDL_TrayEntry *entry, bool checked)
{
SDL_Unsupported();
}
bool SDL_GetTrayEntryChecked(SDL_TrayEntry *entry)
{
SDL_Unsupported();
return false;
}
void SDL_SetTrayEntryEnabled(SDL_TrayEntry *entry, bool enabled)
{
SDL_Unsupported();
}
bool SDL_GetTrayEntryEnabled(SDL_TrayEntry *entry)
{
SDL_Unsupported();
return false;
}
void SDL_SetTrayEntryCallback(SDL_TrayEntry *entry, SDL_TrayCallback callback, void *userdata)
{
SDL_Unsupported();
}
SDL_TrayMenu *SDL_GetTrayEntryParent(SDL_TrayEntry *entry)
{
SDL_Unsupported();
return NULL;
}
SDL_TrayEntry *SDL_GetTrayMenuParentEntry(SDL_TrayMenu *menu)
{
SDL_Unsupported();
return NULL;
}
SDL_Tray *SDL_GetTrayMenuParentTray(SDL_TrayMenu *menu)
{
SDL_Unsupported();
return NULL;
}
void SDL_DestroyTray(SDL_Tray *tray)
{
SDL_Unsupported();
}

664
src/tray/unix/SDL_tray.c Normal file
View File

@@ -0,0 +1,664 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2024 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 <dlfcn.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;
gulong (*g_signal_connect_data)(gpointer instance, const gchar *detailed_signal, GCallback c_handler, gpointer data, GClosureNotify destroy_data, GConnectFlags connect_flags);
#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;
gboolean (*gtk_init_check)(int *argc, char ***argv);
void (*gtk_main)(void);
GtkWidget* (*gtk_menu_new)(void);
GtkWidget* (*gtk_separator_menu_item_new)(void);
GtkWidget* (*gtk_menu_item_new_with_label)(const gchar *label);
void (*gtk_menu_item_set_submenu)(GtkMenuItem *menu_item, GtkWidget *submenu);
GtkWidget* (*gtk_check_menu_item_new_with_label)(const gchar *label);
void (*gtk_check_menu_item_set_active)(GtkCheckMenuItem *check_menu_item, gboolean is_active);
void (*gtk_widget_set_sensitive)(GtkWidget *widget, gboolean sensitive);
void (*gtk_widget_show)(GtkWidget *widget);
void (*gtk_menu_shell_append)(GtkMenuShell *menu_shell, GtkWidget *child);
void (*gtk_menu_shell_insert)(GtkMenuShell *menu_shell, GtkWidget *child, gint position);
void (*gtk_widget_destroy)(GtkWidget *widget);
const gchar *(*gtk_menu_item_get_label)(GtkMenuItem *menu_item);
void (*gtk_menu_item_set_label)(GtkMenuItem *menu_item, const gchar *label);
gboolean (*gtk_check_menu_item_get_active)(GtkCheckMenuItem *check_menu_item);
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;
AppIndicator *(*app_indicator_new)(const gchar *id, const gchar *icon_name, AppIndicatorCategory category);
void (*app_indicator_set_status)(AppIndicator *self, AppIndicatorStatus status);
void (*app_indicator_set_icon)(AppIndicator *self, const gchar *icon_name);
void (*app_indicator_set_menu)(AppIndicator *self, GtkMenu *menu);
/* ------------------------------------------------------------------------- */
/* END THIRD-PARTY HEADER CONTENT */
/* ------------------------------------------------------------------------- */
#endif
static int main_gtk_thread(void *data)
{
gtk_main();
return 0;
}
#ifdef APPINDICATOR_HEADER
static void quit_gtk(void)
{
}
static bool init_gtk(void)
{
SDL_DetachThread(SDL_CreateThread(main_gtk_thread, "tray gtk", NULL));
}
#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[] = {
"libayatana-appindicator3.so",
"libayatana-appindicator3.so.1",
"libayatana-appindicator.so",
"libappindicator3.so",
"libappindicator3.so.1",
"libappindicator.so",
"libappindicator.so.1",
NULL
};
const char *gtk_names[] = {
"libgtk-3.so",
"libgtk-3.so.0",
NULL
};
const char *gdk_names[] = {
"libgdk-3.so",
"libgdk-3.so.0",
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 = dlsym(libgtk, "gtk_main");
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");
g_signal_connect_data = dlsym(libgdk, "g_signal_connect_data");
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 ||
!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_signal_connect_data ||
!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;
SDL_DetachThread(SDL_CreateThread(main_gtk_thread, "tray gtk", NULL));
return true;
}
#endif
struct SDL_TrayMenu {
GtkMenuShell *menu;
size_t 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_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);
}
}
/* Since AppIndicator deals only in filenames, which are inherently subject to
timing attacks, don't bother generating a secure filename. */
static bool get_tmp_filename(char *buffer, size_t size)
{
static int count = 0;
if (size < 64) {
return SDL_SetError("Can't create temporary file for icon: size %ld < 64", size);
}
int would_have_written = SDL_snprintf(buffer, size, "/tmp/sdl_appindicator_icon_%d_%d.bmp", getpid(), count++);
return would_have_written > 0 && would_have_written < size - 1;
}
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 %ld", would_have_written, 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);
}
SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip)
{
if (init_gtk() != true) {
return NULL;
}
SDL_Tray *tray = (SDL_Tray *) SDL_malloc(sizeof(SDL_Tray));
if (!tray) {
return NULL;
}
SDL_memset((void *) tray, 0, sizeof(*tray));
get_tmp_filename(tray->icon_path, sizeof(tray->icon_path));
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);
return tray;
}
void SDL_SetTrayIcon(SDL_Tray *tray, SDL_Surface *icon)
{
if (*tray->icon_path) {
SDL_RemovePath(tray->icon_path);
}
/* AppIndicator caches the icon files; always change filename to avoid caching */
if (icon) {
get_tmp_filename(tray->icon_path, sizeof(tray->icon_path));
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)
{
tray->menu = SDL_malloc(sizeof(SDL_TrayMenu));
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)
{
return tray->menu;
}
SDL_TrayMenu *SDL_CreateTraySubmenu(SDL_TrayEntry *entry)
{
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_malloc(sizeof(SDL_TrayMenu));
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)
{
return entry->submenu;
}
const SDL_TrayEntry **SDL_GetTrayEntries(SDL_TrayMenu *menu, int *size)
{
if (size) {
*size = 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_realloc(menu->entries, menu->nEntries * sizeof(SDL_TrayEntry *));
/* 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;
}
gtk_widget_destroy(entry->item);
SDL_free(entry);
}
SDL_TrayEntry *SDL_InsertTrayEntryAt(SDL_TrayMenu *menu, int pos, const char *label, SDL_TrayEntryFlags flags)
{
if (pos < -1 || pos > (int) menu->nEntries) {
SDL_InvalidParamError("pos");
return NULL;
}
if (pos == -1) {
pos = menu->nEntries;
}
SDL_TrayEntry *entry = SDL_malloc(sizeof(SDL_TrayEntry));
if (!entry) {
return NULL;
}
SDL_memset((void *) entry, 0, sizeof(*entry));
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);
gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(entry->item), !!(flags & SDL_TRAYENTRY_CHECKED));
} else {
entry->item = gtk_menu_item_new_with_label(label);
}
gtk_widget_set_sensitive(entry->item, !(flags & SDL_TRAYENTRY_DISABLED));
SDL_TrayEntry **new_entries = (SDL_TrayEntry **) SDL_realloc(menu->entries, (menu->nEntries + 1) * sizeof(SDL_TrayEntry *));
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;
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)
{
gtk_menu_item_set_label(GTK_MENU_ITEM(entry->item), label);
}
const char *SDL_GetTrayEntryLabel(SDL_TrayEntry *entry)
{
return gtk_menu_item_get_label(GTK_MENU_ITEM(entry->item));
}
void SDL_SetTrayEntryChecked(SDL_TrayEntry *entry, bool checked)
{
if (!(entry->flags & SDL_TRAYENTRY_CHECKBOX)) {
SDL_SetError("Cannot update check for entry not created with 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->flags & SDL_TRAYENTRY_CHECKBOX)) {
SDL_SetError("Cannot fetch check for entry not created with 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)
{
gtk_widget_set_sensitive(entry->item, enabled);
}
bool SDL_GetTrayEntryEnabled(SDL_TrayEntry *entry)
{
return gtk_widget_get_sensitive(entry->item);
}
void SDL_SetTrayEntryCallback(SDL_TrayEntry *entry, SDL_TrayCallback callback, void *userdata)
{
entry->callback = callback;
entry->userdata = userdata;
}
SDL_TrayMenu *SDL_GetTrayEntryParent(SDL_TrayEntry *entry)
{
return entry->parent;
}
SDL_TrayEntry *SDL_GetTrayMenuParentEntry(SDL_TrayMenu *menu)
{
return menu->parent_entry;
}
SDL_Tray *SDL_GetTrayMenuParentTray(SDL_TrayMenu *menu)
{
return menu->parent_tray;
}
void SDL_DestroyTray(SDL_Tray *tray)
{
if (!tray) {
return;
}
if (tray->menu) {
DestroySDLMenu(tray->menu);
}
if (*tray->icon_path) {
SDL_RemovePath(tray->icon_path);
}
SDL_free(tray);
}

589
src/tray/windows/SDL_tray.c Normal file
View File

@@ -0,0 +1,589 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2024 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/windows/SDL_surface_utils.h"
#include <windows.h>
#include <windowsx.h>
#include <shellapi.h>
#include <stdlib.h>
#define WM_TRAYICON (WM_USER + 1)
struct SDL_TrayMenu {
HMENU hMenu;
size_t 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 (size_t 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);
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;
default:
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
return 0;
}
static void DestroySDLMenu(SDL_TrayMenu *menu)
{
for (size_t 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 *convert_label(const char *in)
{
const char *c;
char *c2;
int len = 0;
for (c = in; *c; c++) {
len += (*c == '&') ? 2 : 1;
}
char *escaped = 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';
int len_w = MultiByteToWideChar(CP_UTF8, 0, escaped, len + 1, NULL, 0);
wchar_t *out = (wchar_t *)SDL_malloc(len_w * sizeof(wchar_t));
if (!out) {
SDL_free(escaped);
return NULL;
}
MultiByteToWideChar(CP_UTF8, 0, escaped, -1, out, len_w);
SDL_free(escaped);
return out;
}
static void register_tray_window_class(void)
{
static bool init = false;
if (init) {
return;
}
HINSTANCE hInstance = GetModuleHandle(NULL);
WNDCLASSW wc;
ZeroMemory(&wc, sizeof(WNDCLASS));
wc.lpfnWndProc = TrayWindowProc;
wc.hInstance = hInstance;
wc.lpszClassName = L"SDLTrayRunner";
RegisterClassW(&wc);
init = true;
}
SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip)
{
SDL_Tray *tray = SDL_malloc(sizeof(SDL_Tray));
if (!tray) {
return NULL;
}
tray->hwnd = NULL;
tray->menu = NULL;
register_tray_window_class();
HINSTANCE hInstance = GetModuleHandle(NULL);
tray->hwnd = CreateWindowExW(0, L"SDLTrayRunner", NULL, 0, 0, 0, 0, 0, HWND_MESSAGE, NULL, hInstance, NULL);
ZeroMemory(&tray->nid, sizeof(NOTIFYICONDATAW));
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;
mbstowcs_s(NULL, tray->nid.szTip, sizeof(tray->nid.szTip) / sizeof(*tray->nid.szTip), tooltip, _TRUNCATE);
if (icon) {
tray->nid.hIcon = CreateIconFromSurface(icon);
if (!tray->nid.hIcon) {
tray->nid.hIcon = LoadIcon(NULL, IDI_APPLICATION);
}
tray->icon = tray->nid.hIcon;
} else {
tray->nid.hIcon = LoadIcon(NULL, IDI_APPLICATION);
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);
return tray;
}
void SDL_SetTrayIcon(SDL_Tray *tray, SDL_Surface *icon)
{
if (tray->icon) {
DestroyIcon(tray->icon);
}
if (icon) {
tray->nid.hIcon = CreateIconFromSurface(icon);
if (!tray->nid.hIcon) {
tray->nid.hIcon = LoadIcon(NULL, IDI_APPLICATION);
}
tray->icon = tray->nid.hIcon;
} else {
tray->nid.hIcon = LoadIcon(NULL, IDI_APPLICATION);
tray->icon = tray->nid.hIcon;
}
Shell_NotifyIconW(NIM_MODIFY, &tray->nid);
}
void SDL_SetTrayTooltip(SDL_Tray *tray, const char *tooltip)
{
if (tooltip) {
mbstowcs_s(NULL, tray->nid.szTip, sizeof(tray->nid.szTip) / sizeof(*tray->nid.szTip), tooltip, _TRUNCATE);
} else {
tray->nid.szTip[0] = '\0';
}
Shell_NotifyIconW(NIM_MODIFY, &tray->nid);
}
SDL_TrayMenu *SDL_CreateTrayMenu(SDL_Tray *tray)
{
tray->menu = SDL_malloc(sizeof(SDL_TrayMenu));
if (!tray->menu) {
return NULL;
}
SDL_memset((void *) tray->menu, 0, sizeof(*tray->menu));
tray->menu->hMenu = CreatePopupMenu();
tray->menu->parent_tray = tray;
return tray->menu;
}
SDL_TrayMenu *SDL_GetTrayMenu(SDL_Tray *tray)
{
return tray->menu;
}
SDL_TrayMenu *SDL_CreateTraySubmenu(SDL_TrayEntry *entry)
{
if (!entry->submenu) {
SDL_SetError("Cannot create submenu for entry not created with SDL_TRAYENTRY_SUBMENU");
}
return entry->submenu;
}
SDL_TrayMenu *SDL_GetTraySubmenu(SDL_TrayEntry *entry)
{
return entry->submenu;
}
const SDL_TrayEntry **SDL_GetTrayEntries(SDL_TrayMenu *menu, int *size)
{
if (size) {
*size = (int) 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 (size_t 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_realloc(menu->entries, menu->nEntries * sizeof(SDL_TrayEntry *));
/* 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;
}
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 (pos < -1 || pos > (int) menu->nEntries) {
SDL_InvalidParamError("pos");
return NULL;
}
int windows_compatible_pos = pos;
if (pos == -1) {
pos = (int) menu->nEntries;
} else if (pos == menu->nEntries) {
windows_compatible_pos = -1;
}
SDL_TrayEntry *entry = SDL_malloc(sizeof(SDL_TrayEntry));
if (!entry) {
return NULL;
}
wchar_t *label_w = NULL;
if (label && !(label_w = convert_label(label))) {
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_malloc(sizeof(SDL_TrayMenu));
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->id = (UINT_PTR) entry->submenu->hMenu;
} else {
entry->id = get_next_id();
}
SDL_TrayEntry **new_entries = (SDL_TrayEntry **) SDL_realloc(menu->entries, (menu->nEntries + 1) * sizeof(SDL_TrayEntry **));
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 = (int) menu->nEntries - 1; i > pos; i--) {
menu->entries[i] = menu->entries[i - 1];
}
new_entries[pos] = entry;
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)
{
SDL_snprintf(entry->label_cache, sizeof(entry->label_cache), "%s", label);
wchar_t *label_w = convert_label(label);
if (!label_w) {
return;
}
MENUITEMINFOW mii;
mii.cbSize = sizeof(MENUITEMINFOW);
mii.fMask = MIIM_STRING;
mii.dwTypeData = label_w;
mii.cch = (UINT) 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)
{
return entry->label_cache;
}
void SDL_SetTrayEntryChecked(SDL_TrayEntry *entry, bool checked)
{
if (!(entry->flags & SDL_TRAYENTRY_CHECKBOX)) {
SDL_SetError("Can't check/uncheck tray entry not created with SDL_TRAYENTRY_CHECKBOX");
return;
}
CheckMenuItem(entry->parent->hMenu, (UINT) entry->id, checked ? MF_CHECKED : MF_UNCHECKED);
}
bool SDL_GetTrayEntryChecked(SDL_TrayEntry *entry)
{
if (!(entry->flags & SDL_TRAYENTRY_CHECKBOX)) {
SDL_SetError("Can't get check status of tray entry not created with 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);
}
void SDL_SetTrayEntryEnabled(SDL_TrayEntry *entry, bool enabled)
{
if (!(entry->flags & SDL_TRAYENTRY_CHECKBOX)) {
SDL_SetError("Cannot update check for entry not created with SDL_TRAYENTRY_CHECKBOX");
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->flags & SDL_TRAYENTRY_CHECKBOX)) {
SDL_SetError("Cannot fetch check for entry not created with 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_ENABLED);
}
void SDL_SetTrayEntryCallback(SDL_TrayEntry *entry, SDL_TrayCallback callback, void *userdata)
{
entry->callback = callback;
entry->userdata = userdata;
}
SDL_TrayMenu *SDL_GetTrayEntryParent(SDL_TrayEntry *entry)
{
return entry->parent;
}
SDL_TrayEntry *SDL_GetTrayMenuParentEntry(SDL_TrayMenu *menu)
{
return menu->parent_entry;
}
SDL_Tray *SDL_GetTrayMenuParentTray(SDL_TrayMenu *menu)
{
return menu->parent_tray;
}
void SDL_DestroyTray(SDL_Tray *tray)
{
if (!tray) {
return;
}
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);
}

View File

@@ -0,0 +1,95 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2024 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_surface_utils.h"
#include "../SDL_surface_c.h"
HICON CreateIconFromSurface(SDL_Surface *surface)
{
SDL_Surface *s = SDL_ConvertSurface(surface, SDL_PIXELFORMAT_RGBA32);
if (!s) {
return NULL;
}
/* The dimensions will be needed after s is freed */
const int width = s->w;
const int height = s->h;
BITMAPINFO bmpInfo;
ZeroMemory(&bmpInfo, sizeof(BITMAPINFO));
bmpInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmpInfo.bmiHeader.biWidth = width;
bmpInfo.bmiHeader.biHeight = -height; /* Top-down bitmap */
bmpInfo.bmiHeader.biPlanes = 1;
bmpInfo.bmiHeader.biBitCount = 32;
bmpInfo.bmiHeader.biCompression = BI_RGB;
HDC hdc = GetDC(NULL);
void* pBits = NULL;
HBITMAP hBitmap = CreateDIBSection(hdc, &bmpInfo, DIB_RGB_COLORS, &pBits, NULL, 0);
if (!hBitmap) {
ReleaseDC(NULL, hdc);
SDL_DestroySurface(s);
return NULL;
}
SDL_memcpy(pBits, s->pixels, width * height * 4);
SDL_DestroySurface(s);
HBITMAP hMask = CreateBitmap(width, height, 1, 1, NULL);
if (!hMask) {
DeleteObject(hBitmap);
ReleaseDC(NULL, hdc);
return NULL;
}
HDC hdcMem = CreateCompatibleDC(hdc);
HGDIOBJ oldBitmap = SelectObject(hdcMem, hMask);
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
BYTE* pixel = (BYTE*)pBits + (y * width + x) * 4;
BYTE alpha = pixel[3];
COLORREF maskColor = (alpha == 0) ? RGB(0, 0, 0) : RGB(255, 255, 255);
SetPixel(hdcMem, x, y, maskColor);
}
}
ICONINFO iconInfo;
iconInfo.fIcon = TRUE;
iconInfo.xHotspot = 0;
iconInfo.yHotspot = 0;
iconInfo.hbmMask = hMask;
iconInfo.hbmColor = hBitmap;
HICON hIcon = CreateIconIndirect(&iconInfo);
SelectObject(hdcMem, oldBitmap);
DeleteDC(hdcMem);
DeleteObject(hBitmap);
DeleteObject(hMask);
ReleaseDC(NULL, hdc);
return hIcon;
}

View File

@@ -0,0 +1,38 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2024 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_surface_utils_h_
#define SDL_surface_utils_h_
#include <windows.h>
#ifdef __cplusplus
extern "C" {
#endif
extern HICON CreateIconFromSurface(SDL_Surface *surface);
#ifdef __cplusplus
}
#endif
#endif