Atlas - SDL_tray.m

Home / ext / SDL / src / tray / cocoa Lines: 1 | Size: 19039 bytes [Download] [Show on GitHub] [Search similar files] [Raw] [Raw (proxy)]
[FILE BEGIN]
1/* 2 Simple DirectMedia Layer 3 Copyright (C) 1997-2026 Sam Lantinga <[email protected]> 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/* Forward declaration */ 32struct SDL_Tray; 33 34/* Objective-C helper class to handle status item button clicks */ 35@interface SDLTrayClickHandler : NSObject 36@property (nonatomic, assign) struct SDL_Tray *tray; 37@property (nonatomic, strong) id middleClickMonitor; 38- (void)handleClick:(id)sender; 39- (void)startMonitoringMiddleClicks; 40- (void)stopMonitoringMiddleClicks; 41@end 42 43struct SDL_TrayMenu { 44 NSMenu *nsmenu; 45 46 int nEntries; 47 SDL_TrayEntry **entries; 48 49 SDL_Tray *parent_tray; 50 SDL_TrayEntry *parent_entry; 51}; 52 53struct SDL_TrayEntry { 54 NSMenuItem *nsitem; 55 56 SDL_TrayEntryFlags flags; 57 SDL_TrayCallback callback; 58 void *userdata; 59 SDL_TrayMenu *submenu; 60 61 SDL_TrayMenu *parent; 62}; 63 64struct SDL_Tray { 65 NSStatusBar *statusBar; 66 NSStatusItem *statusItem; 67 68 SDL_TrayMenu *menu; 69 SDLTrayClickHandler *clickHandler; 70 71 void *userdata; 72 SDL_TrayClickCallback left_click_callback; 73 SDL_TrayClickCallback right_click_callback; 74 SDL_TrayClickCallback middle_click_callback; 75}; 76 77@implementation SDLTrayClickHandler 78 79- (void)handleClick:(id)sender 80{ 81 if (!self.tray) { 82 return; 83 } 84 85 NSEvent *event = [NSApp currentEvent]; 86 NSUInteger buttonNumber = [event buttonNumber]; 87 88 bool show_menu = false; 89 90 if (buttonNumber == 0) { 91 /* Left click */ 92 if (self.tray->left_click_callback) { 93 show_menu = self.tray->left_click_callback(self.tray->userdata, self.tray); 94 } else { 95 show_menu = true; 96 } 97 } else if (buttonNumber == 1) { 98 /* Right click */ 99 if (self.tray->right_click_callback) { 100 show_menu = self.tray->right_click_callback(self.tray->userdata, self.tray); 101 } else { 102 show_menu = true; 103 } 104 } else if (buttonNumber == 2) { 105 /* Middle click */ 106 if (self.tray->middle_click_callback) { 107 self.tray->middle_click_callback(self.tray->userdata, self.tray); 108 } 109 } 110 111 if (show_menu && self.tray->menu) { 112 [self.tray->statusItem popUpStatusItemMenu:self.tray->menu->nsmenu]; 113 } 114} 115 116- (void)startMonitoringMiddleClicks 117{ 118 if (self.middleClickMonitor) { 119 return; 120 } 121 122 __weak SDLTrayClickHandler *weakSelf = self; 123 self.middleClickMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskOtherMouseUp handler:^NSEvent *(NSEvent *event) { 124 SDLTrayClickHandler *strongSelf = weakSelf; 125 if (!strongSelf || !strongSelf.tray || [event buttonNumber] != 2) { 126 return event; 127 } 128 129 /* Check if the click is within the status item's button bounds */ 130 NSPoint clickLocation = [event locationInWindow]; 131 NSWindow *statusItemWindow = strongSelf.tray->statusItem.button.window; 132 133 if (statusItemWindow && event.window == statusItemWindow) { 134 NSPoint localPoint = [strongSelf.tray->statusItem.button convertPoint:clickLocation fromView:nil]; 135 if (NSPointInRect(localPoint, strongSelf.tray->statusItem.button.bounds)) { 136 if (strongSelf.tray->middle_click_callback) { 137 strongSelf.tray->middle_click_callback(strongSelf.tray->userdata, strongSelf.tray); 138 } 139 } 140 } 141 142 return event; 143 }]; 144} 145 146- (void)stopMonitoringMiddleClicks 147{ 148 if (self.middleClickMonitor) { 149 [NSEvent removeMonitor:self.middleClickMonitor]; 150 self.middleClickMonitor = nil; 151 } 152} 153 154@end 155 156static void DestroySDLMenu(SDL_TrayMenu *menu) 157{ 158 for (int i = 0; i < menu->nEntries; i++) { 159 if (menu->entries[i] && menu->entries[i]->submenu) { 160 DestroySDLMenu(menu->entries[i]->submenu); 161 } 162 SDL_free(menu->entries[i]); 163 } 164 165 SDL_free(menu->entries); 166 167 if (menu->parent_entry) { 168 [menu->parent_entry->parent->nsmenu setSubmenu:nil forItem:menu->parent_entry->nsitem]; 169 } else if (menu->parent_tray) { 170 [menu->parent_tray->statusItem setMenu:nil]; 171 } 172 173 SDL_free(menu); 174} 175 176void SDL_UpdateTrays(void) 177{ 178} 179 180SDL_Tray *SDL_CreateTrayWithProperties(SDL_PropertiesID props) 181{ 182 if (!SDL_IsMainThread()) { 183 SDL_SetError("This function should be called on the main thread"); 184 return NULL; 185 } 186 187 SDL_Surface *icon = (SDL_Surface *)SDL_GetPointerProperty(props, SDL_PROP_TRAY_CREATE_ICON_POINTER, NULL); 188 const char *tooltip = SDL_GetStringProperty(props, SDL_PROP_TRAY_CREATE_TOOLTIP_STRING, NULL); 189 190 if (icon) { 191 icon = SDL_ConvertSurface(icon, SDL_PIXELFORMAT_RGBA32); 192 if (!icon) { 193 return NULL; 194 } 195 } 196 197 SDL_Tray *tray = (SDL_Tray *)SDL_calloc(1, sizeof(*tray)); 198 if (!tray) { 199 SDL_DestroySurface(icon); 200 return NULL; 201 } 202 203 tray->userdata = SDL_GetPointerProperty(props, SDL_PROP_TRAY_CREATE_USERDATA_POINTER, NULL); 204 tray->left_click_callback = (SDL_TrayClickCallback)SDL_GetPointerProperty(props, SDL_PROP_TRAY_CREATE_LEFTCLICK_CALLBACK_POINTER, NULL); 205 tray->right_click_callback = (SDL_TrayClickCallback)SDL_GetPointerProperty(props, SDL_PROP_TRAY_CREATE_RIGHTCLICK_CALLBACK_POINTER, NULL); 206 tray->middle_click_callback = (SDL_TrayClickCallback)SDL_GetPointerProperty(props, SDL_PROP_TRAY_CREATE_MIDDLECLICK_CALLBACK_POINTER, NULL); 207 208 tray->statusItem = nil; 209 tray->statusBar = [NSStatusBar systemStatusBar]; 210 tray->statusItem = [tray->statusBar statusItemWithLength:NSVariableStatusItemLength]; 211 [[NSApplication sharedApplication] activateIgnoringOtherApps:TRUE]; 212 213 if (tooltip) { 214 tray->statusItem.button.toolTip = [NSString stringWithUTF8String:tooltip]; 215 } else { 216 tray->statusItem.button.toolTip = nil; 217 } 218 219 if (icon) { 220 NSBitmapImageRep *bitmap = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:(unsigned char **)&icon->pixels 221 pixelsWide:icon->w 222 pixelsHigh:icon->h 223 bitsPerSample:8 224 samplesPerPixel:4 225 hasAlpha:YES 226 isPlanar:NO 227 colorSpaceName:NSCalibratedRGBColorSpace 228 bytesPerRow:icon->pitch 229 bitsPerPixel:32]; 230 NSImage *iconimg = [[NSImage alloc] initWithSize:NSMakeSize(icon->w, icon->h)]; 231 [iconimg addRepresentation:bitmap]; 232 233 /* A typical icon size is 22x22 on macOS. Failing to resize the icon 234 may give oversized status bar buttons. */ 235 NSImage *iconimg22 = [[NSImage alloc] initWithSize:NSMakeSize(22, 22)]; 236 [iconimg22 lockFocus]; 237 [iconimg setSize:NSMakeSize(22, 22)]; 238 [iconimg drawInRect:NSMakeRect(0, 0, 22, 22)]; 239 [iconimg22 unlockFocus]; 240 241 tray->statusItem.button.image = iconimg22; 242 243 SDL_DestroySurface(icon); 244 } 245 246 /* Create click handler and set up button to receive clicks */ 247 tray->clickHandler = [[SDLTrayClickHandler alloc] init]; 248 tray->clickHandler.tray = tray; 249 250 [tray->statusItem.button setTarget:tray->clickHandler]; 251 [tray->statusItem.button setAction:@selector(handleClick:)]; 252 [tray->statusItem.button sendActionOn:(NSEventMaskLeftMouseUp | NSEventMaskRightMouseUp)]; 253 254 /* Start monitoring for middle clicks since status items don't receive them via the normal action mechanism */ 255 [tray->clickHandler startMonitoringMiddleClicks]; 256 257 SDL_RegisterTray(tray); 258 259 return tray; 260} 261 262SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip) 263{ 264 SDL_Tray *tray; 265 SDL_PropertiesID props = SDL_CreateProperties(); 266 if (!props) { 267 return NULL; 268 } 269 if (icon) { 270 SDL_SetPointerProperty(props, SDL_PROP_TRAY_CREATE_ICON_POINTER, icon); 271 } 272 if (tooltip) { 273 SDL_SetStringProperty(props, SDL_PROP_TRAY_CREATE_TOOLTIP_STRING, tooltip); 274 } 275 tray = SDL_CreateTrayWithProperties(props); 276 SDL_DestroyProperties(props); 277 return tray; 278} 279 280void SDL_SetTrayIcon(SDL_Tray *tray, SDL_Surface *icon) 281{ 282 if (!SDL_ObjectValid(tray, SDL_OBJECT_TYPE_TRAY)) { 283 return; 284 } 285 286 if (!icon) { 287 tray->statusItem.button.image = nil; 288 return; 289 } 290 291 icon = SDL_ConvertSurface(icon, SDL_PIXELFORMAT_RGBA32); 292 if (!icon) { 293 tray->statusItem.button.image = nil; 294 return; 295 } 296 297 NSBitmapImageRep *bitmap = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:(unsigned char **)&icon->pixels 298 pixelsWide:icon->w 299 pixelsHigh:icon->h 300 bitsPerSample:8 301 samplesPerPixel:4 302 hasAlpha:YES 303 isPlanar:NO 304 colorSpaceName:NSCalibratedRGBColorSpace 305 bytesPerRow:icon->pitch 306 bitsPerPixel:32]; 307 NSImage *iconimg = [[NSImage alloc] initWithSize:NSMakeSize(icon->w, icon->h)]; 308 [iconimg addRepresentation:bitmap]; 309 310 /* A typical icon size is 22x22 on macOS. Failing to resize the icon 311 may give oversized status bar buttons. */ 312 NSImage *iconimg22 = [[NSImage alloc] initWithSize:NSMakeSize(22, 22)]; 313 [iconimg22 lockFocus]; 314 [iconimg setSize:NSMakeSize(22, 22)]; 315 [iconimg drawInRect:NSMakeRect(0, 0, 22, 22)]; 316 [iconimg22 unlockFocus]; 317 318 tray->statusItem.button.image = iconimg22; 319 320 SDL_DestroySurface(icon); 321} 322 323void SDL_SetTrayTooltip(SDL_Tray *tray, const char *tooltip) 324{ 325 if (!SDL_ObjectValid(tray, SDL_OBJECT_TYPE_TRAY)) { 326 return; 327 } 328 329 if (tooltip) { 330 tray->statusItem.button.toolTip = [NSString stringWithUTF8String:tooltip]; 331 } else { 332 tray->statusItem.button.toolTip = nil; 333 } 334} 335 336SDL_TrayMenu *SDL_CreateTrayMenu(SDL_Tray *tray) 337{ 338 CHECK_PARAM(!SDL_ObjectValid(tray, SDL_OBJECT_TYPE_TRAY)) { 339 SDL_InvalidParamError("tray"); 340 return NULL; 341 } 342 343 SDL_TrayMenu *menu = (SDL_TrayMenu *)SDL_calloc(1, sizeof(*menu)); 344 if (!menu) { 345 return NULL; 346 } 347 348 NSMenu *nsmenu = [[NSMenu alloc] init]; 349 [nsmenu setAutoenablesItems:FALSE]; 350 351 /* Don't set menu on statusItem - we handle menu display manually in the click handler */ 352 353 tray->menu = menu; 354 menu->nsmenu = nsmenu; 355 menu->nEntries = 0; 356 menu->entries = NULL; 357 menu->parent_tray = tray; 358 menu->parent_entry = NULL; 359 360 return menu; 361} 362 363SDL_TrayMenu *SDL_GetTrayMenu(SDL_Tray *tray) 364{ 365 CHECK_PARAM(!SDL_ObjectValid(tray, SDL_OBJECT_TYPE_TRAY)) { 366 SDL_InvalidParamError("tray"); 367 return NULL; 368 } 369 370 return tray->menu; 371} 372 373SDL_TrayMenu *SDL_CreateTraySubmenu(SDL_TrayEntry *entry) 374{ 375 CHECK_PARAM(!entry) { 376 SDL_InvalidParamError("entry"); 377 return NULL; 378 } 379 380 if (entry->submenu) { 381 SDL_SetError("Tray entry submenu already exists"); 382 return NULL; 383 } 384 385 if (!(entry->flags & SDL_TRAYENTRY_SUBMENU)) { 386 SDL_SetError("Cannot create submenu for entry not created with SDL_TRAYENTRY_SUBMENU"); 387 return NULL; 388 } 389 390 SDL_TrayMenu *menu = (SDL_TrayMenu *)SDL_calloc(1, sizeof(*menu)); 391 if (!menu) { 392 return NULL; 393 } 394 395 NSMenu *nsmenu = [[NSMenu alloc] init]; 396 [nsmenu setAutoenablesItems:FALSE]; 397 398 entry->submenu = menu; 399 menu->nsmenu = nsmenu; 400 menu->nEntries = 0; 401 menu->entries = NULL; 402 menu->parent_tray = NULL; 403 menu->parent_entry = entry; 404 405 [entry->parent->nsmenu setSubmenu:nsmenu forItem:entry->nsitem]; 406 407 return menu; 408} 409 410SDL_TrayMenu *SDL_GetTraySubmenu(SDL_TrayEntry *entry) 411{ 412 CHECK_PARAM(!entry) { 413 SDL_InvalidParamError("entry"); 414 return NULL; 415 } 416 417 return entry->submenu; 418} 419 420const SDL_TrayEntry **SDL_GetTrayEntries(SDL_TrayMenu *menu, int *count) 421{ 422 CHECK_PARAM(!menu) { 423 SDL_InvalidParamError("menu"); 424 return NULL; 425 } 426 427 if (count) { 428 *count = menu->nEntries; 429 } 430 return (const SDL_TrayEntry **)menu->entries; 431} 432 433void SDL_RemoveTrayEntry(SDL_TrayEntry *entry) 434{ 435 if (!entry) { 436 return; 437 } 438 439 SDL_TrayMenu *menu = entry->parent; 440 441 bool found = false; 442 for (int i = 0; i < menu->nEntries - 1; i++) { 443 if (menu->entries[i] == entry) { 444 found = true; 445 } 446 447 if (found) { 448 menu->entries[i] = menu->entries[i + 1]; 449 } 450 } 451 452 if (entry->submenu) { 453 DestroySDLMenu(entry->submenu); 454 } 455 456 menu->nEntries--; 457 SDL_TrayEntry **new_entries = (SDL_TrayEntry **)SDL_realloc(menu->entries, (menu->nEntries + 1) * sizeof(*new_entries)); 458 459 /* Not sure why shrinking would fail, but even if it does, we can live with a "too big" array */ 460 if (new_entries) { 461 menu->entries = new_entries; 462 menu->entries[menu->nEntries] = NULL; 463 } 464 465 [menu->nsmenu removeItem:entry->nsitem]; 466 467 SDL_free(entry); 468} 469 470SDL_TrayEntry *SDL_InsertTrayEntryAt(SDL_TrayMenu *menu, int pos, const char *label, SDL_TrayEntryFlags flags) 471{ 472 CHECK_PARAM(!menu) { 473 SDL_InvalidParamError("menu"); 474 return NULL; 475 } 476 477 CHECK_PARAM(pos < -1 || pos > menu->nEntries) { 478 SDL_InvalidParamError("pos"); 479 return NULL; 480 } 481 482 if (pos == -1) { 483 pos = menu->nEntries; 484 } 485 486 SDL_TrayEntry *entry = (SDL_TrayEntry *)SDL_calloc(1, sizeof(*entry)); 487 if (!entry) { 488 return NULL; 489 } 490 491 SDL_TrayEntry **new_entries = (SDL_TrayEntry **)SDL_realloc(menu->entries, (menu->nEntries + 2) * sizeof(*new_entries)); 492 if (!new_entries) { 493 SDL_free(entry); 494 return NULL; 495 } 496 497 menu->entries = new_entries; 498 menu->nEntries++; 499 500 for (int i = menu->nEntries - 1; i > pos; i--) { 501 menu->entries[i] = menu->entries[i - 1]; 502 } 503 504 new_entries[pos] = entry; 505 new_entries[menu->nEntries] = NULL; 506 507 NSMenuItem *nsitem; 508 if (label == NULL) { 509 nsitem = [NSMenuItem separatorItem]; 510 } else { 511 nsitem = [[NSMenuItem alloc] initWithTitle:[NSString stringWithUTF8String:label] action:@selector(menu:) keyEquivalent:@""]; 512 [nsitem setEnabled:((flags & SDL_TRAYENTRY_DISABLED) ? FALSE : TRUE)]; 513 [nsitem setState:((flags & SDL_TRAYENTRY_CHECKED) ? NSControlStateValueOn : NSControlStateValueOff)]; 514 [nsitem setRepresentedObject:[NSValue valueWithPointer:entry]]; 515 } 516 517 [menu->nsmenu insertItem:nsitem atIndex:pos]; 518 519 entry->nsitem = nsitem; 520 entry->flags = flags; 521 entry->callback = NULL; 522 entry->userdata = NULL; 523 entry->submenu = NULL; 524 entry->parent = menu; 525 526 return entry; 527} 528 529void SDL_SetTrayEntryLabel(SDL_TrayEntry *entry, const char *label) 530{ 531 if (!entry) { 532 return; 533 } 534 535 [entry->nsitem setTitle:[NSString stringWithUTF8String:label]]; 536} 537 538const char *SDL_GetTrayEntryLabel(SDL_TrayEntry *entry) 539{ 540 CHECK_PARAM(!entry) { 541 SDL_InvalidParamError("entry"); 542 return NULL; 543 } 544 545 return [[entry->nsitem title] UTF8String]; 546} 547 548void SDL_SetTrayEntryChecked(SDL_TrayEntry *entry, bool checked) 549{ 550 if (!entry) { 551 return; 552 } 553 554 [entry->nsitem setState:(checked ? NSControlStateValueOn : NSControlStateValueOff)]; 555} 556 557bool SDL_GetTrayEntryChecked(SDL_TrayEntry *entry) 558{ 559 if (!entry) { 560 return false; 561 } 562 563 return entry->nsitem.state == NSControlStateValueOn; 564} 565 566void SDL_SetTrayEntryEnabled(SDL_TrayEntry *entry, bool enabled) 567{ 568 if (!entry || !(entry->flags & SDL_TRAYENTRY_CHECKBOX)) { 569 return; 570 } 571 572 [entry->nsitem setEnabled:(enabled ? YES : NO)]; 573} 574 575bool SDL_GetTrayEntryEnabled(SDL_TrayEntry *entry) 576{ 577 if (!entry || !(entry->flags & SDL_TRAYENTRY_CHECKBOX)) { 578 return false; 579 } 580 581 return entry->nsitem.enabled; 582} 583 584void SDL_SetTrayEntryCallback(SDL_TrayEntry *entry, SDL_TrayCallback callback, void *userdata) 585{ 586 if (!entry) { 587 return; 588 } 589 590 entry->callback = callback; 591 entry->userdata = userdata; 592} 593 594void SDL_ClickTrayEntry(SDL_TrayEntry *entry) 595{ 596 if (!entry) { 597 return; 598 } 599 600 if (entry->flags & SDL_TRAYENTRY_CHECKBOX) { 601 SDL_SetTrayEntryChecked(entry, !SDL_GetTrayEntryChecked(entry)); 602 } 603 604 if (entry->callback) { 605 entry->callback(entry->userdata, entry); 606 } 607} 608 609SDL_TrayMenu *SDL_GetTrayEntryParent(SDL_TrayEntry *entry) 610{ 611 CHECK_PARAM(!entry) { 612 SDL_InvalidParamError("entry"); 613 return NULL; 614 } 615 616 return entry->parent; 617} 618 619SDL_TrayEntry *SDL_GetTrayMenuParentEntry(SDL_TrayMenu *menu) 620{ 621 CHECK_PARAM(!menu) { 622 SDL_InvalidParamError("menu"); 623 return NULL; 624 } 625 626 return menu->parent_entry; 627} 628 629SDL_Tray *SDL_GetTrayMenuParentTray(SDL_TrayMenu *menu) 630{ 631 CHECK_PARAM(!menu) { 632 SDL_InvalidParamError("menu"); 633 return NULL; 634 } 635 636 return menu->parent_tray; 637} 638 639void SDL_DestroyTray(SDL_Tray *tray) 640{ 641 if (!SDL_ObjectValid(tray, SDL_OBJECT_TYPE_TRAY)) { 642 return; 643 } 644 645 SDL_UnregisterTray(tray); 646 647 [[NSStatusBar systemStatusBar] removeStatusItem:tray->statusItem]; 648 649 if (tray->menu) { 650 DestroySDLMenu(tray->menu); 651 } 652 653 if (tray->clickHandler) { 654 [tray->clickHandler stopMonitoringMiddleClicks]; 655 tray->clickHandler.tray = NULL; 656 tray->clickHandler = nil; 657 } 658 659 SDL_free(tray); 660} 661 662#endif // SDL_PLATFORM_MACOS 663
[FILE END]
(C) 2025 0x4248 (C) 2025 4248 Media and 4248 Systems, All part of 0x4248 See LICENCE files for more information. Not all files are by 0x4248 always check Licencing.