1/*
2 Simple DirectMedia Layer
3 Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
4
5 This software is provided 'as-is', without any express or implied
6 warranty. In no event will the authors be held liable for any damages
7 arising from the use of this software.
8
9 Permission is granted to anyone to use this software for any purpose,
10 including commercial applications, and to alter it and redistribute it
11 freely, subject to the following restrictions:
12
13 1. The origin of this software must not be misrepresented; you must not
14 claim that you wrote the original software. If you use this software
15 in a product, an acknowledgment in the product documentation would be
16 appreciated but is not required.
17 2. Altered source versions must be plainly marked as such, and must not be
18 misrepresented as being the original software.
19 3. This notice may not be removed or altered from any source distribution.
20*/
21
22#include "SDL_internal.h"
23
24#include "../SDL_tray_utils.h"
25
26#include <dlfcn.h>
27#include <errno.h>
28
29/* getpid() */
30#include <unistd.h>
31
32/* APPINDICATOR_HEADER is not exposed as a build setting, but the code has been
33 written nevertheless to make future maintenance easier. */
34#ifdef APPINDICATOR_HEADER
35#include APPINDICATOR_HEADER
36#else
37/* ------------------------------------------------------------------------- */
38/* BEGIN THIRD-PARTY HEADER CONTENT */
39/* ------------------------------------------------------------------------- */
40/* Glib 2.0 */
41
42typedef unsigned long gulong;
43typedef void* gpointer;
44typedef char gchar;
45typedef int gint;
46typedef unsigned int guint;
47typedef gint gboolean;
48typedef void (*GCallback)(void);
49typedef struct _GClosure GClosure;
50typedef void (*GClosureNotify) (gpointer data, GClosure *closure);
51typedef gboolean (*GSourceFunc) (gpointer user_data);
52typedef enum
53{
54 G_CONNECT_AFTER = 1 << 0,
55 G_CONNECT_SWAPPED = 1 << 1
56} GConnectFlags;
57
58static gulong (*g_signal_connect_data)(gpointer instance, const gchar *detailed_signal, GCallback c_handler, gpointer data, GClosureNotify destroy_data, GConnectFlags connect_flags);
59static void (*g_object_unref)(gpointer object);
60static gchar *(*g_mkdtemp)(gchar *template);
61gpointer (*g_object_ref_sink)(gpointer object);
62gpointer (*g_object_ref)(gpointer object);
63
64// glib_typeof requires compiler-specific code and includes that are too complex
65// to be worth copy-pasting here
66//#define g_object_ref(Obj) ((glib_typeof (Obj)) (g_object_ref) (Obj))
67//#define g_object_ref_sink(Obj) ((glib_typeof (Obj)) (g_object_ref_sink) (Obj))
68
69#define g_signal_connect(instance, detailed_signal, c_handler, data) \
70 g_signal_connect_data ((instance), (detailed_signal), (c_handler), (data), NULL, (GConnectFlags) 0)
71
72#define _G_TYPE_CIC(ip, gt, ct) ((ct*) ip)
73
74#define G_TYPE_CHECK_INSTANCE_CAST(instance, g_type, c_type) (_G_TYPE_CIC ((instance), (g_type), c_type))
75
76#define G_CALLBACK(f) ((GCallback) (f))
77
78#define FALSE 0
79#define TRUE 1
80
81/* GTK 3.0 */
82
83typedef struct _GtkMenu GtkMenu;
84typedef struct _GtkMenuItem GtkMenuItem;
85typedef struct _GtkMenuShell GtkMenuShell;
86typedef struct _GtkWidget GtkWidget;
87typedef struct _GtkCheckMenuItem GtkCheckMenuItem;
88
89static gboolean (*gtk_init_check)(int *argc, char ***argv);
90static gboolean (*gtk_main_iteration_do)(gboolean blocking);
91static GtkWidget* (*gtk_menu_new)(void);
92static GtkWidget* (*gtk_separator_menu_item_new)(void);
93static GtkWidget* (*gtk_menu_item_new_with_label)(const gchar *label);
94static void (*gtk_menu_item_set_submenu)(GtkMenuItem *menu_item, GtkWidget *submenu);
95static GtkWidget* (*gtk_check_menu_item_new_with_label)(const gchar *label);
96static void (*gtk_check_menu_item_set_active)(GtkCheckMenuItem *check_menu_item, gboolean is_active);
97static void (*gtk_widget_set_sensitive)(GtkWidget *widget, gboolean sensitive);
98static void (*gtk_widget_show)(GtkWidget *widget);
99static void (*gtk_menu_shell_append)(GtkMenuShell *menu_shell, GtkWidget *child);
100static void (*gtk_menu_shell_insert)(GtkMenuShell *menu_shell, GtkWidget *child, gint position);
101static void (*gtk_widget_destroy)(GtkWidget *widget);
102static const gchar *(*gtk_menu_item_get_label)(GtkMenuItem *menu_item);
103static void (*gtk_menu_item_set_label)(GtkMenuItem *menu_item, const gchar *label);
104static gboolean (*gtk_check_menu_item_get_active)(GtkCheckMenuItem *check_menu_item);
105static gboolean (*gtk_widget_get_sensitive)(GtkWidget *widget);
106
107#define GTK_MENU_ITEM(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GTK_TYPE_MENU_ITEM, GtkMenuItem))
108#define GTK_WIDGET(widget) (G_TYPE_CHECK_INSTANCE_CAST ((widget), GTK_TYPE_WIDGET, GtkWidget))
109#define GTK_CHECK_MENU_ITEM(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GTK_TYPE_CHECK_MENU_ITEM, GtkCheckMenuItem))
110#define GTK_MENU(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GTK_TYPE_MENU, GtkMenu))
111
112/* AppIndicator */
113
114typedef enum {
115 APP_INDICATOR_CATEGORY_APPLICATION_STATUS,
116 APP_INDICATOR_CATEGORY_COMMUNICATIONS,
117 APP_INDICATOR_CATEGORY_SYSTEM_SERVICES,
118 APP_INDICATOR_CATEGORY_HARDWARE,
119 APP_INDICATOR_CATEGORY_OTHER
120} AppIndicatorCategory;
121
122typedef enum {
123 APP_INDICATOR_STATUS_PASSIVE,
124 APP_INDICATOR_STATUS_ACTIVE,
125 APP_INDICATOR_STATUS_ATTENTION
126} AppIndicatorStatus;
127
128typedef struct _AppIndicator AppIndicator;
129
130static AppIndicator *(*app_indicator_new)(const gchar *id, const gchar *icon_name, AppIndicatorCategory category);
131static void (*app_indicator_set_status)(AppIndicator *self, AppIndicatorStatus status);
132static void (*app_indicator_set_icon)(AppIndicator *self, const gchar *icon_name);
133static void (*app_indicator_set_menu)(AppIndicator *self, GtkMenu *menu);
134
135/* ------------------------------------------------------------------------- */
136/* END THIRD-PARTY HEADER CONTENT */
137/* ------------------------------------------------------------------------- */
138#endif
139
140#ifdef APPINDICATOR_HEADER
141
142static void quit_gtk(void)
143{
144}
145
146static bool init_gtk(void)
147{
148 return true;
149}
150
151#else
152
153static bool gtk_is_init = false;
154
155static void *libappindicator = NULL;
156static void *libgtk = NULL;
157static void *libgdk = NULL;
158
159static void quit_gtk(void)
160{
161 if (libappindicator) {
162 dlclose(libappindicator);
163 libappindicator = NULL;
164 }
165
166 if (libgtk) {
167 dlclose(libgtk);
168 libgtk = NULL;
169 }
170
171 if (libgdk) {
172 dlclose(libgdk);
173 libgdk = NULL;
174 }
175
176 gtk_is_init = false;
177}
178
179const char *appindicator_names[] = {
180#ifdef SDL_PLATFORM_OPENBSD
181 "libayatana-appindicator3.so",
182 "libappindicator3.so",
183#else
184 "libayatana-appindicator3.so.1",
185 "libappindicator3.so.1",
186#endif
187 NULL
188};
189
190const char *gtk_names[] = {
191#ifdef SDL_PLATFORM_OPENBSD
192 "libgtk-3.so",
193#else
194 "libgtk-3.so.0",
195#endif
196 NULL
197};
198
199const char *gdk_names[] = {
200#ifdef SDL_PLATFORM_OPENBSD
201 "libgdk-3.so",
202#else
203 "libgdk-3.so.0",
204#endif
205 NULL
206};
207
208static void *find_lib(const char **names)
209{
210 const char **name_ptr = names;
211 void *handle = NULL;
212
213 do {
214 handle = dlopen(*name_ptr, RTLD_LAZY);
215 } while (*++name_ptr && !handle);
216
217 return handle;
218}
219
220static bool init_gtk(void)
221{
222 if (gtk_is_init) {
223 return true;
224 }
225
226 libappindicator = find_lib(appindicator_names);
227 libgtk = find_lib(gtk_names);
228 libgdk = find_lib(gdk_names);
229
230 if (!libappindicator || !libgtk || !libgdk) {
231 quit_gtk();
232 return SDL_SetError("Could not load GTK/AppIndicator libraries");
233 }
234
235 gtk_init_check = dlsym(libgtk, "gtk_init_check");
236 gtk_main_iteration_do = dlsym(libgtk, "gtk_main_iteration_do");
237 gtk_menu_new = dlsym(libgtk, "gtk_menu_new");
238 gtk_separator_menu_item_new = dlsym(libgtk, "gtk_separator_menu_item_new");
239 gtk_menu_item_new_with_label = dlsym(libgtk, "gtk_menu_item_new_with_label");
240 gtk_menu_item_set_submenu = dlsym(libgtk, "gtk_menu_item_set_submenu");
241 gtk_check_menu_item_new_with_label = dlsym(libgtk, "gtk_check_menu_item_new_with_label");
242 gtk_check_menu_item_set_active = dlsym(libgtk, "gtk_check_menu_item_set_active");
243 gtk_widget_set_sensitive = dlsym(libgtk, "gtk_widget_set_sensitive");
244 gtk_widget_show = dlsym(libgtk, "gtk_widget_show");
245 gtk_menu_shell_append = dlsym(libgtk, "gtk_menu_shell_append");
246 gtk_menu_shell_insert = dlsym(libgtk, "gtk_menu_shell_insert");
247 gtk_widget_destroy = dlsym(libgtk, "gtk_widget_destroy");
248 gtk_menu_item_get_label = dlsym(libgtk, "gtk_menu_item_get_label");
249 gtk_menu_item_set_label = dlsym(libgtk, "gtk_menu_item_set_label");
250 gtk_check_menu_item_get_active = dlsym(libgtk, "gtk_check_menu_item_get_active");
251 gtk_widget_get_sensitive = dlsym(libgtk, "gtk_widget_get_sensitive");
252
253 /* Technically these are GLib or GObject functions, but we can find
254 * them via GDK */
255 g_mkdtemp = dlsym(libgdk, "g_mkdtemp");
256 g_signal_connect_data = dlsym(libgdk, "g_signal_connect_data");
257 g_object_unref = dlsym(libgdk, "g_object_unref");
258 g_object_ref_sink = dlsym(libgdk, "g_object_ref_sink");
259 g_object_ref = dlsym(libgdk, "g_object_ref");
260
261 app_indicator_new = dlsym(libappindicator, "app_indicator_new");
262 app_indicator_set_status = dlsym(libappindicator, "app_indicator_set_status");
263 app_indicator_set_icon = dlsym(libappindicator, "app_indicator_set_icon");
264 app_indicator_set_menu = dlsym(libappindicator, "app_indicator_set_menu");
265
266 if (!gtk_init_check ||
267 !gtk_main_iteration_do ||
268 !gtk_menu_new ||
269 !gtk_separator_menu_item_new ||
270 !gtk_menu_item_new_with_label ||
271 !gtk_menu_item_set_submenu ||
272 !gtk_check_menu_item_new_with_label ||
273 !gtk_check_menu_item_set_active ||
274 !gtk_widget_set_sensitive ||
275 !gtk_widget_show ||
276 !gtk_menu_shell_append ||
277 !gtk_menu_shell_insert ||
278 !gtk_widget_destroy ||
279 !g_mkdtemp ||
280 !g_object_ref_sink ||
281 !g_object_ref ||
282 !g_signal_connect_data ||
283 !g_object_unref ||
284 !app_indicator_new ||
285 !app_indicator_set_status ||
286 !app_indicator_set_icon ||
287 !app_indicator_set_menu ||
288 !gtk_menu_item_get_label ||
289 !gtk_menu_item_set_label ||
290 !gtk_check_menu_item_get_active ||
291 !gtk_widget_get_sensitive) {
292 quit_gtk();
293 return SDL_SetError("Could not load GTK/AppIndicator functions");
294 }
295
296 if (gtk_init_check(0, NULL) == FALSE) {
297 quit_gtk();
298 return SDL_SetError("Could not init GTK");
299 }
300
301 gtk_is_init = true;
302
303 return true;
304}
305#endif
306
307struct SDL_TrayMenu {
308 GtkMenuShell *menu;
309
310 int nEntries;
311 SDL_TrayEntry **entries;
312
313 SDL_Tray *parent_tray;
314 SDL_TrayEntry *parent_entry;
315};
316
317struct SDL_TrayEntry {
318 SDL_TrayMenu *parent;
319 GtkWidget *item;
320
321 /* Checkboxes are "activated" when programmatically checked/unchecked; this
322 is a workaround. */
323 bool ignore_signal;
324
325 SDL_TrayEntryFlags flags;
326 SDL_TrayCallback callback;
327 void *userdata;
328 SDL_TrayMenu *submenu;
329};
330
331/* Template for g_mkdtemp(). The Xs will get replaced with a random
332 * directory name, which is created safely and atomically. */
333#define ICON_DIR_TEMPLATE "/tmp/SDL-tray-XXXXXX"
334
335struct SDL_Tray {
336 AppIndicator *indicator;
337 SDL_TrayMenu *menu;
338 char icon_dir[sizeof(ICON_DIR_TEMPLATE)];
339 char icon_path[256];
340
341 GtkMenuShell *menu_cached;
342};
343
344static void call_callback(GtkMenuItem *item, gpointer ptr)
345{
346 SDL_TrayEntry *entry = ptr;
347
348 /* Not needed with AppIndicator, may be needed with other frameworks */
349 /* if (entry->flags & SDL_TRAYENTRY_CHECKBOX) {
350 SDL_SetTrayEntryChecked(entry, !SDL_GetTrayEntryChecked(entry));
351 } */
352
353 if (entry->ignore_signal) {
354 return;
355 }
356
357 if (entry->callback) {
358 entry->callback(entry->userdata, entry);
359 }
360}
361
362static bool new_tmp_filename(SDL_Tray *tray)
363{
364 static int count = 0;
365
366 int would_have_written = SDL_snprintf(tray->icon_path, sizeof(tray->icon_path), "%s/%d.bmp", tray->icon_dir, count++);
367
368 if (would_have_written > 0 && ((unsigned) would_have_written) < sizeof(tray->icon_path) - 1) {
369 return true;
370 }
371
372 tray->icon_path[0] = '\0';
373 SDL_SetError("Failed to format new temporary filename");
374 return false;
375}
376
377static const char *get_appindicator_id(void)
378{
379 static int count = 0;
380 static char buffer[256];
381
382 int would_have_written = SDL_snprintf(buffer, sizeof(buffer), "sdl-appindicator-%d-%d", getpid(), count++);
383
384 if (would_have_written <= 0 || would_have_written >= sizeof(buffer) - 1) {
385 SDL_SetError("Couldn't fit %d bytes in buffer of size %d", would_have_written, (int) sizeof(buffer));
386 return NULL;
387 }
388
389 return buffer;
390}
391
392static void DestroySDLMenu(SDL_TrayMenu *menu)
393{
394 for (int i = 0; i < menu->nEntries; i++) {
395 if (menu->entries[i] && menu->entries[i]->submenu) {
396 DestroySDLMenu(menu->entries[i]->submenu);
397 }
398 SDL_free(menu->entries[i]);
399 }
400
401 if (menu->menu) {
402 g_object_unref(menu->menu);
403 }
404
405 SDL_free(menu->entries);
406 SDL_free(menu);
407}
408
409void SDL_UpdateTrays(void)
410{
411 if (SDL_HasActiveTrays()) {
412 gtk_main_iteration_do(FALSE);
413 }
414}
415
416SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip)
417{
418 if (!SDL_IsMainThread()) {
419 SDL_SetError("This function should be called on the main thread");
420 return NULL;
421 }
422
423 if (init_gtk() != true) {
424 return NULL;
425 }
426
427 SDL_Tray *tray = (SDL_Tray *)SDL_calloc(1, sizeof(*tray));
428 if (!tray) {
429 return NULL;
430 }
431
432 /* On success, g_mkdtemp edits its argument in-place to replace the Xs
433 * with a random directory name, which it creates safely and atomically.
434 * On failure, it sets errno. */
435 SDL_strlcpy(tray->icon_dir, ICON_DIR_TEMPLATE, sizeof(tray->icon_dir));
436 if (!g_mkdtemp(tray->icon_dir)) {
437 SDL_SetError("Cannot create directory for tray icon: %s", strerror(errno));
438 SDL_free(tray);
439 return NULL;
440 }
441
442 if (icon) {
443 if (!new_tmp_filename(tray)) {
444 SDL_free(tray);
445 return NULL;
446 }
447
448 SDL_SaveBMP(icon, tray->icon_path);
449 }
450
451 tray->indicator = app_indicator_new(get_appindicator_id(), tray->icon_path,
452 APP_INDICATOR_CATEGORY_APPLICATION_STATUS);
453
454 app_indicator_set_status(tray->indicator, APP_INDICATOR_STATUS_ACTIVE);
455
456 // The tray icon isn't shown before a menu is created; create one early.
457 tray->menu_cached = (GtkMenuShell *) g_object_ref_sink(gtk_menu_new());
458 app_indicator_set_menu(tray->indicator, GTK_MENU(tray->menu_cached));
459
460 SDL_RegisterTray(tray);
461
462 return tray;
463}
464
465void SDL_SetTrayIcon(SDL_Tray *tray, SDL_Surface *icon)
466{
467 if (!SDL_ObjectValid(tray, SDL_OBJECT_TYPE_TRAY)) {
468 return;
469 }
470
471 if (*tray->icon_path) {
472 SDL_RemovePath(tray->icon_path);
473 }
474
475 /* AppIndicator caches the icon files; always change filename to avoid caching */
476
477 if (icon && new_tmp_filename(tray)) {
478 SDL_SaveBMP(icon, tray->icon_path);
479 app_indicator_set_icon(tray->indicator, tray->icon_path);
480 } else {
481 *tray->icon_path = '\0';
482 app_indicator_set_icon(tray->indicator, NULL);
483 }
484}
485
486void SDL_SetTrayTooltip(SDL_Tray *tray, const char *tooltip)
487{
488 /* AppIndicator provides no tooltip support. */
489}
490
491SDL_TrayMenu *SDL_CreateTrayMenu(SDL_Tray *tray)
492{
493 if (!SDL_ObjectValid(tray, SDL_OBJECT_TYPE_TRAY)) {
494 SDL_InvalidParamError("tray");
495 return NULL;
496 }
497
498 tray->menu = (SDL_TrayMenu *)SDL_calloc(1, sizeof(*tray->menu));
499 if (!tray->menu) {
500 return NULL;
501 }
502
503 tray->menu->menu = g_object_ref(tray->menu_cached);
504 tray->menu->parent_tray = tray;
505 tray->menu->parent_entry = NULL;
506 tray->menu->nEntries = 0;
507 tray->menu->entries = NULL;
508
509 return tray->menu;
510}
511
512SDL_TrayMenu *SDL_GetTrayMenu(SDL_Tray *tray)
513{
514 if (!SDL_ObjectValid(tray, SDL_OBJECT_TYPE_TRAY)) {
515 SDL_InvalidParamError("tray");
516 return NULL;
517 }
518
519 return tray->menu;
520}
521
522SDL_TrayMenu *SDL_CreateTraySubmenu(SDL_TrayEntry *entry)
523{
524 if (!entry) {
525 SDL_InvalidParamError("entry");
526 return NULL;
527 }
528
529 if (entry->submenu) {
530 SDL_SetError("Tray entry submenu already exists");
531 return NULL;
532 }
533
534 if (!(entry->flags & SDL_TRAYENTRY_SUBMENU)) {
535 SDL_SetError("Cannot create submenu for entry not created with SDL_TRAYENTRY_SUBMENU");
536 return NULL;
537 }
538
539 entry->submenu = (SDL_TrayMenu *)SDL_calloc(1, sizeof(*entry->submenu));
540 if (!entry->submenu) {
541 return NULL;
542 }
543
544 entry->submenu->menu = (GtkMenuShell *)gtk_menu_new();
545 entry->submenu->parent_tray = NULL;
546 entry->submenu->parent_entry = entry;
547 entry->submenu->nEntries = 0;
548 entry->submenu->entries = NULL;
549
550 gtk_menu_item_set_submenu(GTK_MENU_ITEM(entry->item), GTK_WIDGET(entry->submenu->menu));
551
552 return entry->submenu;
553}
554
555SDL_TrayMenu *SDL_GetTraySubmenu(SDL_TrayEntry *entry)
556{
557 if (!entry) {
558 SDL_InvalidParamError("entry");
559 return NULL;
560 }
561
562 return entry->submenu;
563}
564
565const SDL_TrayEntry **SDL_GetTrayEntries(SDL_TrayMenu *menu, int *count)
566{
567 if (!menu) {
568 SDL_InvalidParamError("menu");
569 return NULL;
570 }
571
572 if (count) {
573 *count = menu->nEntries;
574 }
575 return (const SDL_TrayEntry **)menu->entries;
576}
577
578void SDL_RemoveTrayEntry(SDL_TrayEntry *entry)
579{
580 if (!entry) {
581 return;
582 }
583
584 SDL_TrayMenu *menu = entry->parent;
585
586 bool found = false;
587 for (int i = 0; i < menu->nEntries - 1; i++) {
588 if (menu->entries[i] == entry) {
589 found = true;
590 }
591
592 if (found) {
593 menu->entries[i] = menu->entries[i + 1];
594 }
595 }
596
597 if (entry->submenu) {
598 DestroySDLMenu(entry->submenu);
599 }
600
601 menu->nEntries--;
602 SDL_TrayEntry **new_entries = (SDL_TrayEntry **)SDL_realloc(menu->entries, (menu->nEntries + 1) * sizeof(*new_entries));
603
604 /* Not sure why shrinking would fail, but even if it does, we can live with a "too big" array */
605 if (new_entries) {
606 menu->entries = new_entries;
607 menu->entries[menu->nEntries] = NULL;
608 }
609
610 gtk_widget_destroy(entry->item);
611 SDL_free(entry);
612}
613
614SDL_TrayEntry *SDL_InsertTrayEntryAt(SDL_TrayMenu *menu, int pos, const char *label, SDL_TrayEntryFlags flags)
615{
616 if (!menu) {
617 SDL_InvalidParamError("menu");
618 return NULL;
619 }
620
621 if (pos < -1 || pos > menu->nEntries) {
622 SDL_InvalidParamError("pos");
623 return NULL;
624 }
625
626 if (pos == -1) {
627 pos = menu->nEntries;
628 }
629
630 SDL_TrayEntry *entry = (SDL_TrayEntry *)SDL_calloc(1, sizeof(*entry));
631 if (!entry) {
632 return NULL;
633 }
634
635 entry->parent = menu;
636 entry->item = NULL;
637 entry->ignore_signal = false;
638 entry->flags = flags;
639 entry->callback = NULL;
640 entry->userdata = NULL;
641 entry->submenu = NULL;
642
643 if (label == NULL) {
644 entry->item = gtk_separator_menu_item_new();
645 } else if (flags & SDL_TRAYENTRY_CHECKBOX) {
646 entry->item = gtk_check_menu_item_new_with_label(label);
647 gboolean active = ((flags & SDL_TRAYENTRY_CHECKED) != 0);
648 gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(entry->item), active);
649 } else {
650 entry->item = gtk_menu_item_new_with_label(label);
651 }
652
653 gboolean sensitive = ((flags & SDL_TRAYENTRY_DISABLED) == 0);
654 gtk_widget_set_sensitive(entry->item, sensitive);
655
656 SDL_TrayEntry **new_entries = (SDL_TrayEntry **)SDL_realloc(menu->entries, (menu->nEntries + 2) * sizeof(*new_entries));
657
658 if (!new_entries) {
659 SDL_free(entry);
660 return NULL;
661 }
662
663 menu->entries = new_entries;
664 menu->nEntries++;
665
666 for (int i = menu->nEntries - 1; i > pos; i--) {
667 menu->entries[i] = menu->entries[i - 1];
668 }
669
670 new_entries[pos] = entry;
671 new_entries[menu->nEntries] = NULL;
672
673 gtk_widget_show(entry->item);
674 gtk_menu_shell_insert(menu->menu, entry->item, (pos == menu->nEntries) ? -1 : pos);
675
676 g_signal_connect(entry->item, "activate", G_CALLBACK(call_callback), entry);
677
678 return entry;
679}
680
681void SDL_SetTrayEntryLabel(SDL_TrayEntry *entry, const char *label)
682{
683 if (!entry) {
684 return;
685 }
686
687 gtk_menu_item_set_label(GTK_MENU_ITEM(entry->item), label);
688}
689
690const char *SDL_GetTrayEntryLabel(SDL_TrayEntry *entry)
691{
692 if (!entry) {
693 SDL_InvalidParamError("entry");
694 return NULL;
695 }
696
697 return gtk_menu_item_get_label(GTK_MENU_ITEM(entry->item));
698}
699
700void SDL_SetTrayEntryChecked(SDL_TrayEntry *entry, bool checked)
701{
702 if (!entry || !(entry->flags & SDL_TRAYENTRY_CHECKBOX)) {
703 return;
704 }
705
706 entry->ignore_signal = true;
707 gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(entry->item), checked);
708 entry->ignore_signal = false;
709}
710
711bool SDL_GetTrayEntryChecked(SDL_TrayEntry *entry)
712{
713 if (!entry || !(entry->flags & SDL_TRAYENTRY_CHECKBOX)) {
714 return false;
715 }
716
717 return gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(entry->item));
718}
719
720void SDL_SetTrayEntryEnabled(SDL_TrayEntry *entry, bool enabled)
721{
722 if (!entry) {
723 return;
724 }
725
726 gtk_widget_set_sensitive(entry->item, enabled);
727}
728
729bool SDL_GetTrayEntryEnabled(SDL_TrayEntry *entry)
730{
731 if (!entry) {
732 return false;
733 }
734
735 return gtk_widget_get_sensitive(entry->item);
736}
737
738void SDL_SetTrayEntryCallback(SDL_TrayEntry *entry, SDL_TrayCallback callback, void *userdata)
739{
740 if (!entry) {
741 return;
742 }
743
744 entry->callback = callback;
745 entry->userdata = userdata;
746}
747
748void SDL_ClickTrayEntry(SDL_TrayEntry *entry)
749{
750 if (!entry) {
751 return;
752 }
753
754 if (entry->flags & SDL_TRAYENTRY_CHECKBOX) {
755 SDL_SetTrayEntryChecked(entry, !SDL_GetTrayEntryChecked(entry));
756 }
757
758 if (entry->callback) {
759 entry->callback(entry->userdata, entry);
760 }
761}
762
763SDL_TrayMenu *SDL_GetTrayEntryParent(SDL_TrayEntry *entry)
764{
765 if (!entry) {
766 SDL_InvalidParamError("entry");
767 return NULL;
768 }
769
770 return entry->parent;
771}
772
773SDL_TrayEntry *SDL_GetTrayMenuParentEntry(SDL_TrayMenu *menu)
774{
775 return menu->parent_entry;
776}
777
778SDL_Tray *SDL_GetTrayMenuParentTray(SDL_TrayMenu *menu)
779{
780 if (!menu) {
781 SDL_InvalidParamError("menu");
782 return NULL;
783 }
784
785 return menu->parent_tray;
786}
787
788void SDL_DestroyTray(SDL_Tray *tray)
789{
790 if (!SDL_ObjectValid(tray, SDL_OBJECT_TYPE_TRAY)) {
791 return;
792 }
793
794 SDL_UnregisterTray(tray);
795
796 if (tray->menu) {
797 DestroySDLMenu(tray->menu);
798 }
799
800 if (*tray->icon_path) {
801 SDL_RemovePath(tray->icon_path);
802 }
803
804 if (*tray->icon_dir) {
805 SDL_RemovePath(tray->icon_dir);
806 }
807
808 if (tray->menu_cached) {
809 g_object_unref(tray->menu_cached);
810 }
811
812 if (tray->indicator) {
813 g_object_unref(tray->indicator);
814 }
815
816 SDL_free(tray);
817}
818