summaryrefslogtreecommitdiff
path: root/contrib/SDL-3.2.8/src/tray/cocoa/SDL_tray.m
diff options
context:
space:
mode:
author3gg <3gg@shellblade.net>2025-12-27 12:03:39 -0800
committer3gg <3gg@shellblade.net>2025-12-27 12:03:39 -0800
commit5a079a2d114f96d4847d1ee305d5b7c16eeec50e (patch)
tree8926ab44f168acf787d8e19608857b3af0f82758 /contrib/SDL-3.2.8/src/tray/cocoa/SDL_tray.m
Initial commit
Diffstat (limited to 'contrib/SDL-3.2.8/src/tray/cocoa/SDL_tray.m')
-rw-r--r--contrib/SDL-3.2.8/src/tray/cocoa/SDL_tray.m524
1 files changed, 524 insertions, 0 deletions
diff --git a/contrib/SDL-3.2.8/src/tray/cocoa/SDL_tray.m b/contrib/SDL-3.2.8/src/tray/cocoa/SDL_tray.m
new file mode 100644
index 0000000..fd7f955
--- /dev/null
+++ b/contrib/SDL-3.2.8/src/tray/cocoa/SDL_tray.m
@@ -0,0 +1,524 @@
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#ifdef SDL_PLATFORM_MACOS
25
26#include <Cocoa/Cocoa.h>
27
28#include "../SDL_tray_utils.h"
29#include "../../video/SDL_surface_c.h"
30
31/* applicationDockMenu */
32
33struct SDL_TrayMenu {
34 NSMenu *nsmenu;
35
36 int nEntries;
37 SDL_TrayEntry **entries;
38
39 SDL_Tray *parent_tray;
40 SDL_TrayEntry *parent_entry;
41};
42
43struct SDL_TrayEntry {
44 NSMenuItem *nsitem;
45
46 SDL_TrayEntryFlags flags;
47 SDL_TrayCallback callback;
48 void *userdata;
49 SDL_TrayMenu *submenu;
50
51 SDL_TrayMenu *parent;
52};
53
54struct SDL_Tray {
55 NSStatusBar *statusBar;
56 NSStatusItem *statusItem;
57
58 SDL_TrayMenu *menu;
59};
60
61static void DestroySDLMenu(SDL_TrayMenu *menu)
62{
63 for (int i = 0; i < menu->nEntries; i++) {
64 if (menu->entries[i] && menu->entries[i]->submenu) {
65 DestroySDLMenu(menu->entries[i]->submenu);
66 }
67 SDL_free(menu->entries[i]);
68 }
69
70 SDL_free(menu->entries);
71
72 if (menu->parent_entry) {
73 [menu->parent_entry->parent->nsmenu setSubmenu:nil forItem:menu->parent_entry->nsitem];
74 } else if (menu->parent_tray) {
75 [menu->parent_tray->statusItem setMenu:nil];
76 }
77
78 SDL_free(menu);
79}
80
81void SDL_UpdateTrays(void)
82{
83}
84
85SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip)
86{
87 if (!SDL_IsMainThread()) {
88 SDL_SetError("This function should be called on the main thread");
89 return NULL;
90 }
91
92 if (icon) {
93 icon = SDL_ConvertSurface(icon, SDL_PIXELFORMAT_RGBA32);
94 if (!icon) {
95 return NULL;
96 }
97 }
98
99 SDL_Tray *tray = (SDL_Tray *)SDL_calloc(1, sizeof(*tray));
100 if (!tray) {
101 SDL_DestroySurface(icon);
102 return NULL;
103 }
104
105 tray->statusItem = nil;
106 tray->statusBar = [NSStatusBar systemStatusBar];
107 tray->statusItem = [tray->statusBar statusItemWithLength:NSVariableStatusItemLength];
108 [[NSApplication sharedApplication] activateIgnoringOtherApps:TRUE];
109
110 if (tooltip) {
111 tray->statusItem.button.toolTip = [NSString stringWithUTF8String:tooltip];
112 } else {
113 tray->statusItem.button.toolTip = nil;
114 }
115
116 if (icon) {
117 NSBitmapImageRep *bitmap = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:(unsigned char **)&icon->pixels
118 pixelsWide:icon->w
119 pixelsHigh:icon->h
120 bitsPerSample:8
121 samplesPerPixel:4
122 hasAlpha:YES
123 isPlanar:NO
124 colorSpaceName:NSCalibratedRGBColorSpace
125 bytesPerRow:icon->pitch
126 bitsPerPixel:32];
127 NSImage *iconimg = [[NSImage alloc] initWithSize:NSMakeSize(icon->w, icon->h)];
128 [iconimg addRepresentation:bitmap];
129
130 /* A typical icon size is 22x22 on macOS. Failing to resize the icon
131 may give oversized status bar buttons. */
132 NSImage *iconimg22 = [[NSImage alloc] initWithSize:NSMakeSize(22, 22)];
133 [iconimg22 lockFocus];
134 [iconimg setSize:NSMakeSize(22, 22)];
135 [iconimg drawInRect:NSMakeRect(0, 0, 22, 22)];
136 [iconimg22 unlockFocus];
137
138 tray->statusItem.button.image = iconimg22;
139
140 SDL_DestroySurface(icon);
141 }
142
143 SDL_RegisterTray(tray);
144
145 return tray;
146}
147
148void SDL_SetTrayIcon(SDL_Tray *tray, SDL_Surface *icon)
149{
150 if (!SDL_ObjectValid(tray, SDL_OBJECT_TYPE_TRAY)) {
151 return;
152 }
153
154 if (!icon) {
155 tray->statusItem.button.image = nil;
156 return;
157 }
158
159 icon = SDL_ConvertSurface(icon, SDL_PIXELFORMAT_RGBA32);
160 if (!icon) {
161 tray->statusItem.button.image = nil;
162 return;
163 }
164
165 NSBitmapImageRep *bitmap = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:(unsigned char **)&icon->pixels
166 pixelsWide:icon->w
167 pixelsHigh:icon->h
168 bitsPerSample:8
169 samplesPerPixel:4
170 hasAlpha:YES
171 isPlanar:NO
172 colorSpaceName:NSCalibratedRGBColorSpace
173 bytesPerRow:icon->pitch
174 bitsPerPixel:32];
175 NSImage *iconimg = [[NSImage alloc] initWithSize:NSMakeSize(icon->w, icon->h)];
176 [iconimg addRepresentation:bitmap];
177
178 /* A typical icon size is 22x22 on macOS. Failing to resize the icon
179 may give oversized status bar buttons. */
180 NSImage *iconimg22 = [[NSImage alloc] initWithSize:NSMakeSize(22, 22)];
181 [iconimg22 lockFocus];
182 [iconimg setSize:NSMakeSize(22, 22)];
183 [iconimg drawInRect:NSMakeRect(0, 0, 22, 22)];
184 [iconimg22 unlockFocus];
185
186 tray->statusItem.button.image = iconimg22;
187
188 SDL_DestroySurface(icon);
189}
190
191void SDL_SetTrayTooltip(SDL_Tray *tray, const char *tooltip)
192{
193 if (!SDL_ObjectValid(tray, SDL_OBJECT_TYPE_TRAY)) {
194 return;
195 }
196
197 if (tooltip) {
198 tray->statusItem.button.toolTip = [NSString stringWithUTF8String:tooltip];
199 } else {
200 tray->statusItem.button.toolTip = nil;
201 }
202}
203
204SDL_TrayMenu *SDL_CreateTrayMenu(SDL_Tray *tray)
205{
206 if (!SDL_ObjectValid(tray, SDL_OBJECT_TYPE_TRAY)) {
207 SDL_InvalidParamError("tray");
208 return NULL;
209 }
210
211 SDL_TrayMenu *menu = (SDL_TrayMenu *)SDL_calloc(1, sizeof(*menu));
212 if (!menu) {
213 return NULL;
214 }
215
216 NSMenu *nsmenu = [[NSMenu alloc] init];
217 [nsmenu setAutoenablesItems:FALSE];
218
219 [tray->statusItem setMenu:nsmenu];
220
221 tray->menu = menu;
222 menu->nsmenu = nsmenu;
223 menu->nEntries = 0;
224 menu->entries = NULL;
225 menu->parent_tray = tray;
226 menu->parent_entry = NULL;
227
228 return menu;
229}
230
231SDL_TrayMenu *SDL_GetTrayMenu(SDL_Tray *tray)
232{
233 if (!SDL_ObjectValid(tray, SDL_OBJECT_TYPE_TRAY)) {
234 SDL_InvalidParamError("tray");
235 return NULL;
236 }
237
238 return tray->menu;
239}
240
241SDL_TrayMenu *SDL_CreateTraySubmenu(SDL_TrayEntry *entry)
242{
243 if (!entry) {
244 SDL_InvalidParamError("entry");
245 return NULL;
246 }
247
248 if (entry->submenu) {
249 SDL_SetError("Tray entry submenu already exists");
250 return NULL;
251 }
252
253 if (!(entry->flags & SDL_TRAYENTRY_SUBMENU)) {
254 SDL_SetError("Cannot create submenu for entry not created with SDL_TRAYENTRY_SUBMENU");
255 return NULL;
256 }
257
258 SDL_TrayMenu *menu = (SDL_TrayMenu *)SDL_calloc(1, sizeof(*menu));
259 if (!menu) {
260 return NULL;
261 }
262
263 NSMenu *nsmenu = [[NSMenu alloc] init];
264 [nsmenu setAutoenablesItems:FALSE];
265
266 entry->submenu = menu;
267 menu->nsmenu = nsmenu;
268 menu->nEntries = 0;
269 menu->entries = NULL;
270 menu->parent_tray = NULL;
271 menu->parent_entry = entry;
272
273 [entry->parent->nsmenu setSubmenu:nsmenu forItem:entry->nsitem];
274
275 return menu;
276}
277
278SDL_TrayMenu *SDL_GetTraySubmenu(SDL_TrayEntry *entry)
279{
280 if (!entry) {
281 SDL_InvalidParamError("entry");
282 return NULL;
283 }
284
285 return entry->submenu;
286}
287
288const SDL_TrayEntry **SDL_GetTrayEntries(SDL_TrayMenu *menu, int *count)
289{
290 if (!menu) {
291 SDL_InvalidParamError("menu");
292 return NULL;
293 }
294
295 if (count) {
296 *count = menu->nEntries;
297 }
298 return (const SDL_TrayEntry **)menu->entries;
299}
300
301void SDL_RemoveTrayEntry(SDL_TrayEntry *entry)
302{
303 if (!entry) {
304 return;
305 }
306
307 SDL_TrayMenu *menu = entry->parent;
308
309 bool found = false;
310 for (int i = 0; i < menu->nEntries - 1; i++) {
311 if (menu->entries[i] == entry) {
312 found = true;
313 }
314
315 if (found) {
316 menu->entries[i] = menu->entries[i + 1];
317 }
318 }
319
320 if (entry->submenu) {
321 DestroySDLMenu(entry->submenu);
322 }
323
324 menu->nEntries--;
325 SDL_TrayEntry **new_entries = (SDL_TrayEntry **)SDL_realloc(menu->entries, (menu->nEntries + 1) * sizeof(*new_entries));
326
327 /* Not sure why shrinking would fail, but even if it does, we can live with a "too big" array */
328 if (new_entries) {
329 menu->entries = new_entries;
330 menu->entries[menu->nEntries] = NULL;
331 }
332
333 [menu->nsmenu removeItem:entry->nsitem];
334
335 SDL_free(entry);
336}
337
338SDL_TrayEntry *SDL_InsertTrayEntryAt(SDL_TrayMenu *menu, int pos, const char *label, SDL_TrayEntryFlags flags)
339{
340 if (!menu) {
341 SDL_InvalidParamError("menu");
342 return NULL;
343 }
344
345 if (pos < -1 || pos > menu->nEntries) {
346 SDL_InvalidParamError("pos");
347 return NULL;
348 }
349
350 if (pos == -1) {
351 pos = menu->nEntries;
352 }
353
354 SDL_TrayEntry *entry = (SDL_TrayEntry *)SDL_calloc(1, sizeof(*entry));
355 if (!entry) {
356 return NULL;
357 }
358
359 SDL_TrayEntry **new_entries = (SDL_TrayEntry **)SDL_realloc(menu->entries, (menu->nEntries + 2) * sizeof(*new_entries));
360 if (!new_entries) {
361 SDL_free(entry);
362 return NULL;
363 }
364
365 menu->entries = new_entries;
366 menu->nEntries++;
367
368 for (int i = menu->nEntries - 1; i > pos; i--) {
369 menu->entries[i] = menu->entries[i - 1];
370 }
371
372 new_entries[pos] = entry;
373 new_entries[menu->nEntries] = NULL;
374
375 NSMenuItem *nsitem;
376 if (label == NULL) {
377 nsitem = [NSMenuItem separatorItem];
378 } else {
379 nsitem = [[NSMenuItem alloc] initWithTitle:[NSString stringWithUTF8String:label] action:@selector(menu:) keyEquivalent:@""];
380 [nsitem setEnabled:((flags & SDL_TRAYENTRY_DISABLED) ? FALSE : TRUE)];
381 [nsitem setState:((flags & SDL_TRAYENTRY_CHECKED) ? NSControlStateValueOn : NSControlStateValueOff)];
382 [nsitem setRepresentedObject:[NSValue valueWithPointer:entry]];
383 }
384
385 [menu->nsmenu insertItem:nsitem atIndex:pos];
386
387 entry->nsitem = nsitem;
388 entry->flags = flags;
389 entry->callback = NULL;
390 entry->userdata = NULL;
391 entry->submenu = NULL;
392 entry->parent = menu;
393
394 return entry;
395}
396
397void SDL_SetTrayEntryLabel(SDL_TrayEntry *entry, const char *label)
398{
399 if (!entry) {
400 return;
401 }
402
403 [entry->nsitem setTitle:[NSString stringWithUTF8String:label]];
404}
405
406const char *SDL_GetTrayEntryLabel(SDL_TrayEntry *entry)
407{
408 if (!entry) {
409 SDL_InvalidParamError("entry");
410 return NULL;
411 }
412
413 return [[entry->nsitem title] UTF8String];
414}
415
416void SDL_SetTrayEntryChecked(SDL_TrayEntry *entry, bool checked)
417{
418 if (!entry) {
419 return;
420 }
421
422 [entry->nsitem setState:(checked ? NSControlStateValueOn : NSControlStateValueOff)];
423}
424
425bool SDL_GetTrayEntryChecked(SDL_TrayEntry *entry)
426{
427 if (!entry) {
428 return false;
429 }
430
431 return entry->nsitem.state == NSControlStateValueOn;
432}
433
434void SDL_SetTrayEntryEnabled(SDL_TrayEntry *entry, bool enabled)
435{
436 if (!entry || !(entry->flags & SDL_TRAYENTRY_CHECKBOX)) {
437 return;
438 }
439
440 [entry->nsitem setEnabled:(enabled ? YES : NO)];
441}
442
443bool SDL_GetTrayEntryEnabled(SDL_TrayEntry *entry)
444{
445 if (!entry || !(entry->flags & SDL_TRAYENTRY_CHECKBOX)) {
446 return false;
447 }
448
449 return entry->nsitem.enabled;
450}
451
452void SDL_SetTrayEntryCallback(SDL_TrayEntry *entry, SDL_TrayCallback callback, void *userdata)
453{
454 if (!entry) {
455 return;
456 }
457
458 entry->callback = callback;
459 entry->userdata = userdata;
460}
461
462void SDL_ClickTrayEntry(SDL_TrayEntry *entry)
463{
464 if (!entry) {
465 return;
466 }
467
468 if (entry->flags & SDL_TRAYENTRY_CHECKBOX) {
469 SDL_SetTrayEntryChecked(entry, !SDL_GetTrayEntryChecked(entry));
470 }
471
472 if (entry->callback) {
473 entry->callback(entry->userdata, entry);
474 }
475}
476
477SDL_TrayMenu *SDL_GetTrayEntryParent(SDL_TrayEntry *entry)
478{
479 if (!entry) {
480 SDL_InvalidParamError("entry");
481 return NULL;
482 }
483
484 return entry->parent;
485}
486
487SDL_TrayEntry *SDL_GetTrayMenuParentEntry(SDL_TrayMenu *menu)
488{
489 if (!menu) {
490 SDL_InvalidParamError("menu");
491 return NULL;
492 }
493
494 return menu->parent_entry;
495}
496
497SDL_Tray *SDL_GetTrayMenuParentTray(SDL_TrayMenu *menu)
498{
499 if (!menu) {
500 SDL_InvalidParamError("menu");
501 return NULL;
502 }
503
504 return menu->parent_tray;
505}
506
507void SDL_DestroyTray(SDL_Tray *tray)
508{
509 if (!SDL_ObjectValid(tray, SDL_OBJECT_TYPE_TRAY)) {
510 return;
511 }
512
513 SDL_UnregisterTray(tray);
514
515 [[NSStatusBar systemStatusBar] removeStatusItem:tray->statusItem];
516
517 if (tray->menu) {
518 DestroySDLMenu(tray->menu);
519 }
520
521 SDL_free(tray);
522}
523
524#endif // SDL_PLATFORM_MACOS