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 | |
42 | typedef unsigned long gulong; |
43 | typedef void* gpointer; |
44 | typedef char gchar; |
45 | typedef int gint; |
46 | typedef unsigned int guint; |
47 | typedef gint gboolean; |
48 | typedef void (*GCallback)(void); |
49 | typedef struct _GClosure GClosure; |
50 | typedef void (*GClosureNotify) (gpointer data, GClosure *closure); |
51 | typedef gboolean (*GSourceFunc) (gpointer user_data); |
52 | typedef enum |
53 | { |
54 | G_CONNECT_AFTER = 1 << 0, |
55 | G_CONNECT_SWAPPED = 1 << 1 |
56 | } GConnectFlags; |
57 | |
58 | static gulong (*g_signal_connect_data)(gpointer instance, const gchar *detailed_signal, GCallback c_handler, gpointer data, GClosureNotify destroy_data, GConnectFlags connect_flags); |
59 | static void (*g_object_unref)(gpointer object); |
60 | static gchar *(*g_mkdtemp)(gchar *template); |
61 | gpointer (*g_object_ref_sink)(gpointer object); |
62 | gpointer (*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 | |
83 | typedef struct ; |
84 | typedef struct ; |
85 | typedef struct ; |
86 | typedef struct _GtkWidget GtkWidget; |
87 | typedef struct ; |
88 | |
89 | static gboolean (*gtk_init_check)(int *argc, char ***argv); |
90 | static gboolean (*gtk_main_iteration_do)(gboolean blocking); |
91 | static GtkWidget* (*)(void); |
92 | static GtkWidget* (*)(void); |
93 | static GtkWidget* (*)(const gchar *label); |
94 | static void (*)(GtkMenuItem *, GtkWidget *); |
95 | static GtkWidget* (*)(const gchar *label); |
96 | static void (*)(GtkCheckMenuItem *, gboolean is_active); |
97 | static void (*gtk_widget_set_sensitive)(GtkWidget *widget, gboolean sensitive); |
98 | static void (*gtk_widget_show)(GtkWidget *widget); |
99 | static void (*)(GtkMenuShell *, GtkWidget *child); |
100 | static void (*)(GtkMenuShell *, GtkWidget *child, gint position); |
101 | static void (*gtk_widget_destroy)(GtkWidget *widget); |
102 | static const gchar *(*)(GtkMenuItem *); |
103 | static void (*)(GtkMenuItem *, const gchar *label); |
104 | static gboolean (*)(GtkCheckMenuItem *); |
105 | static gboolean (*gtk_widget_get_sensitive)(GtkWidget *widget); |
106 | |
107 | #define (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 (obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GTK_TYPE_CHECK_MENU_ITEM, GtkCheckMenuItem)) |
110 | #define (obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GTK_TYPE_MENU, GtkMenu)) |
111 | |
112 | /* AppIndicator */ |
113 | |
114 | typedef 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 | |
122 | typedef enum { |
123 | APP_INDICATOR_STATUS_PASSIVE, |
124 | APP_INDICATOR_STATUS_ACTIVE, |
125 | APP_INDICATOR_STATUS_ATTENTION |
126 | } AppIndicatorStatus; |
127 | |
128 | typedef struct _AppIndicator AppIndicator; |
129 | |
130 | static AppIndicator *(*app_indicator_new)(const gchar *id, const gchar *icon_name, AppIndicatorCategory category); |
131 | static void (*app_indicator_set_status)(AppIndicator *self, AppIndicatorStatus status); |
132 | static void (*app_indicator_set_icon)(AppIndicator *self, const gchar *icon_name); |
133 | static void (*)(AppIndicator *self, GtkMenu *); |
134 | |
135 | /* ------------------------------------------------------------------------- */ |
136 | /* END THIRD-PARTY HEADER CONTENT */ |
137 | /* ------------------------------------------------------------------------- */ |
138 | #endif |
139 | |
140 | #ifdef APPINDICATOR_HEADER |
141 | |
142 | static void quit_gtk(void) |
143 | { |
144 | } |
145 | |
146 | static bool init_gtk(void) |
147 | { |
148 | return true; |
149 | } |
150 | |
151 | #else |
152 | |
153 | static bool gtk_is_init = false; |
154 | |
155 | static void *libappindicator = NULL; |
156 | static void *libgtk = NULL; |
157 | static void *libgdk = NULL; |
158 | |
159 | static 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 | |
179 | const 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 | |
190 | const 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 | |
199 | const 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 | |
208 | static 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 | |
220 | static 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 | |
307 | struct { |
308 | GtkMenuShell *; |
309 | |
310 | int ; |
311 | SDL_TrayEntry **; |
312 | |
313 | SDL_Tray *; |
314 | SDL_TrayEntry *; |
315 | }; |
316 | |
317 | struct 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 *; |
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 | |
335 | struct SDL_Tray { |
336 | AppIndicator *indicator; |
337 | SDL_TrayMenu *; |
338 | char icon_dir[sizeof(ICON_DIR_TEMPLATE)]; |
339 | char icon_path[256]; |
340 | |
341 | GtkMenuShell *; |
342 | }; |
343 | |
344 | static 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 | |
362 | static 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 | |
377 | static 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 | |
392 | static void (SDL_TrayMenu *) |
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 | |
409 | void SDL_UpdateTrays(void) |
410 | { |
411 | if (SDL_HasActiveTrays()) { |
412 | gtk_main_iteration_do(FALSE); |
413 | } |
414 | } |
415 | |
416 | SDL_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 | |
465 | void 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 | |
486 | void SDL_SetTrayTooltip(SDL_Tray *tray, const char *tooltip) |
487 | { |
488 | /* AppIndicator provides no tooltip support. */ |
489 | } |
490 | |
491 | SDL_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 | |
512 | SDL_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 | |
522 | SDL_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 | |
555 | SDL_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 | |
565 | const SDL_TrayEntry **SDL_GetTrayEntries(SDL_TrayMenu *, 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 | |
578 | void SDL_RemoveTrayEntry(SDL_TrayEntry *entry) |
579 | { |
580 | if (!entry) { |
581 | return; |
582 | } |
583 | |
584 | SDL_TrayMenu * = 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 | |
614 | SDL_TrayEntry *SDL_InsertTrayEntryAt(SDL_TrayMenu *, 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 | |
681 | void 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 | |
690 | const 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 | |
700 | void 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 | |
711 | bool 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 | |
720 | void 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 | |
729 | bool SDL_GetTrayEntryEnabled(SDL_TrayEntry *entry) |
730 | { |
731 | if (!entry) { |
732 | return false; |
733 | } |
734 | |
735 | return gtk_widget_get_sensitive(entry->item); |
736 | } |
737 | |
738 | void 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 | |
748 | void 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 | |
763 | SDL_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 | |
773 | SDL_TrayEntry *SDL_GetTrayMenuParentEntry(SDL_TrayMenu *) |
774 | { |
775 | return menu->parent_entry; |
776 | } |
777 | |
778 | SDL_Tray *SDL_GetTrayMenuParentTray(SDL_TrayMenu *) |
779 | { |
780 | if (!menu) { |
781 | SDL_InvalidParamError("menu" ); |
782 | return NULL; |
783 | } |
784 | |
785 | return menu->parent_tray; |
786 | } |
787 | |
788 | void 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 | |