SDL_cocoaevents.m (17978B)
1 /* 2 Simple DirectMedia Layer 3 Copyright (C) 1997-2020 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 #include "../../SDL_internal.h" 22 23 #if SDL_VIDEO_DRIVER_COCOA 24 25 #include "SDL_timer.h" 26 27 #include "SDL_cocoavideo.h" 28 #include "../../events/SDL_events_c.h" 29 #include "SDL_hints.h" 30 31 /* This define was added in the 10.9 SDK. */ 32 #ifndef kIOPMAssertPreventUserIdleDisplaySleep 33 #define kIOPMAssertPreventUserIdleDisplaySleep kIOPMAssertionTypePreventUserIdleDisplaySleep 34 #endif 35 #ifndef NSAppKitVersionNumber10_8 36 #define NSAppKitVersionNumber10_8 1187 37 #endif 38 39 @interface SDLApplication : NSApplication 40 41 - (void)terminate:(id)sender; 42 - (void)sendEvent:(NSEvent *)theEvent; 43 44 + (void)registerUserDefaults; 45 46 @end 47 48 @implementation SDLApplication 49 50 // Override terminate to handle Quit and System Shutdown smoothly. 51 - (void)terminate:(id)sender 52 { 53 SDL_SendQuit(); 54 } 55 56 static SDL_bool s_bShouldHandleEventsInSDLApplication = SDL_FALSE; 57 58 static void Cocoa_DispatchEvent(NSEvent *theEvent) 59 { 60 SDL_VideoDevice *_this = SDL_GetVideoDevice(); 61 62 switch ([theEvent type]) { 63 case NSEventTypeLeftMouseDown: 64 case NSEventTypeOtherMouseDown: 65 case NSEventTypeRightMouseDown: 66 case NSEventTypeLeftMouseUp: 67 case NSEventTypeOtherMouseUp: 68 case NSEventTypeRightMouseUp: 69 case NSEventTypeLeftMouseDragged: 70 case NSEventTypeRightMouseDragged: 71 case NSEventTypeOtherMouseDragged: /* usually middle mouse dragged */ 72 case NSEventTypeMouseMoved: 73 case NSEventTypeScrollWheel: 74 Cocoa_HandleMouseEvent(_this, theEvent); 75 break; 76 case NSEventTypeKeyDown: 77 case NSEventTypeKeyUp: 78 case NSEventTypeFlagsChanged: 79 Cocoa_HandleKeyEvent(_this, theEvent); 80 break; 81 default: 82 break; 83 } 84 } 85 86 // Dispatch events here so that we can handle events caught by 87 // nextEventMatchingMask in SDL, as well as events caught by other 88 // processes (such as CEF) that are passed down to NSApp. 89 - (void)sendEvent:(NSEvent *)theEvent 90 { 91 if (s_bShouldHandleEventsInSDLApplication) { 92 Cocoa_DispatchEvent(theEvent); 93 } 94 95 [super sendEvent:theEvent]; 96 } 97 98 + (void)registerUserDefaults 99 { 100 NSDictionary *appDefaults = [[NSDictionary alloc] initWithObjectsAndKeys: 101 [NSNumber numberWithBool:NO], @"AppleMomentumScrollSupported", 102 [NSNumber numberWithBool:NO], @"ApplePressAndHoldEnabled", 103 [NSNumber numberWithBool:YES], @"ApplePersistenceIgnoreState", 104 nil]; 105 [[NSUserDefaults standardUserDefaults] registerDefaults:appDefaults]; 106 [appDefaults release]; 107 } 108 109 @end // SDLApplication 110 111 /* setAppleMenu disappeared from the headers in 10.4 */ 112 @interface NSApplication(NSAppleMenu) 113 - (void)setAppleMenu:(NSMenu *)menu; 114 @end 115 116 @interface SDLAppDelegate : NSObject <NSApplicationDelegate> { 117 @public 118 BOOL seenFirstActivate; 119 } 120 121 - (id)init; 122 - (void)localeDidChange:(NSNotification *)notification; 123 @end 124 125 @implementation SDLAppDelegate : NSObject 126 - (id)init 127 { 128 self = [super init]; 129 if (self) { 130 NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; 131 132 seenFirstActivate = NO; 133 134 [center addObserver:self 135 selector:@selector(windowWillClose:) 136 name:NSWindowWillCloseNotification 137 object:nil]; 138 139 [center addObserver:self 140 selector:@selector(focusSomeWindow:) 141 name:NSApplicationDidBecomeActiveNotification 142 object:nil]; 143 144 [center addObserver:self 145 selector:@selector(localeDidChange:) 146 name:NSCurrentLocaleDidChangeNotification 147 object:nil]; 148 } 149 150 return self; 151 } 152 153 - (void)dealloc 154 { 155 NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; 156 157 [center removeObserver:self name:NSWindowWillCloseNotification object:nil]; 158 [center removeObserver:self name:NSApplicationDidBecomeActiveNotification object:nil]; 159 [center removeObserver:self name:NSCurrentLocaleDidChangeNotification object:nil]; 160 161 [super dealloc]; 162 } 163 164 - (void)windowWillClose:(NSNotification *)notification; 165 { 166 NSWindow *win = (NSWindow*)[notification object]; 167 168 if (![win isKeyWindow]) { 169 return; 170 } 171 172 /* HACK: Make the next window in the z-order key when the key window is 173 * closed. The custom event loop and/or windowing code we have seems to 174 * prevent the normal behavior: https://bugzilla.libsdl.org/show_bug.cgi?id=1825 175 */ 176 177 /* +[NSApp orderedWindows] never includes the 'About' window, but we still 178 * want to try its list first since the behavior in other apps is to only 179 * make the 'About' window key if no other windows are on-screen. 180 */ 181 for (NSWindow *window in [NSApp orderedWindows]) { 182 if (window != win && [window canBecomeKeyWindow]) { 183 if (![window isOnActiveSpace]) { 184 continue; 185 } 186 [window makeKeyAndOrderFront:self]; 187 return; 188 } 189 } 190 191 /* If a window wasn't found above, iterate through all visible windows in 192 * the active Space in z-order (including the 'About' window, if it's shown) 193 * and make the first one key. 194 */ 195 for (NSNumber *num in [NSWindow windowNumbersWithOptions:0]) { 196 NSWindow *window = [NSApp windowWithWindowNumber:[num integerValue]]; 197 if (window && window != win && [window canBecomeKeyWindow]) { 198 [window makeKeyAndOrderFront:self]; 199 return; 200 } 201 } 202 } 203 204 - (void)focusSomeWindow:(NSNotification *)aNotification 205 { 206 /* HACK: Ignore the first call. The application gets a 207 * applicationDidBecomeActive: a little bit after the first window is 208 * created, and if we don't ignore it, a window that has been created with 209 * SDL_WINDOW_MINIMIZED will ~immediately be restored. 210 */ 211 if (!seenFirstActivate) { 212 seenFirstActivate = YES; 213 return; 214 } 215 216 SDL_VideoDevice *device = SDL_GetVideoDevice(); 217 if (device && device->windows) { 218 SDL_Window *window = device->windows; 219 int i; 220 for (i = 0; i < device->num_displays; ++i) { 221 SDL_Window *fullscreen_window = device->displays[i].fullscreen_window; 222 if (fullscreen_window) { 223 if (fullscreen_window->flags & SDL_WINDOW_MINIMIZED) { 224 SDL_RestoreWindow(fullscreen_window); 225 } 226 return; 227 } 228 } 229 230 if (window->flags & SDL_WINDOW_MINIMIZED) { 231 SDL_RestoreWindow(window); 232 } else { 233 SDL_RaiseWindow(window); 234 } 235 } 236 } 237 238 - (void)localeDidChange:(NSNotification *)notification; 239 { 240 SDL_SendLocaleChangedEvent(); 241 } 242 243 - (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename 244 { 245 return (BOOL)SDL_SendDropFile(NULL, [filename UTF8String]) && SDL_SendDropComplete(NULL); 246 } 247 248 - (void)applicationDidFinishLaunching:(NSNotification *)notification 249 { 250 /* The menu bar of SDL apps which don't have the typical .app bundle 251 * structure fails to work the first time a window is created (until it's 252 * de-focused and re-focused), if this call is in Cocoa_RegisterApp instead 253 * of here. https://bugzilla.libsdl.org/show_bug.cgi?id=3051 254 */ 255 if (!SDL_GetHintBoolean(SDL_HINT_MAC_BACKGROUND_APP, SDL_FALSE)) { 256 /* Get more aggressive for Catalina: activate the Dock first so we definitely reset all activation state. */ 257 for (NSRunningApplication *i in [NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.apple.dock"]) { 258 [i activateWithOptions:NSApplicationActivateIgnoringOtherApps]; 259 break; 260 } 261 SDL_Delay(300); /* !!! FIXME: this isn't right. */ 262 [NSApp activateIgnoringOtherApps:YES]; 263 } 264 265 [[NSAppleEventManager sharedAppleEventManager] 266 setEventHandler:self 267 andSelector:@selector(handleURLEvent:withReplyEvent:) 268 forEventClass:kInternetEventClass 269 andEventID:kAEGetURL]; 270 271 /* If we call this before NSApp activation, macOS might print a complaint 272 * about ApplePersistenceIgnoreState. */ 273 [SDLApplication registerUserDefaults]; 274 } 275 276 - (void)handleURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent 277 { 278 NSString* path = [[event paramDescriptorForKeyword:keyDirectObject] stringValue]; 279 SDL_SendDropFile(NULL, [path UTF8String]); 280 SDL_SendDropComplete(NULL); 281 } 282 283 @end 284 285 static SDLAppDelegate *appDelegate = nil; 286 287 static NSString * 288 GetApplicationName(void) 289 { 290 NSString *appName; 291 292 /* Determine the application name */ 293 appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"]; 294 if (!appName) { 295 appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"]; 296 } 297 298 if (![appName length]) { 299 appName = [[NSProcessInfo processInfo] processName]; 300 } 301 302 return appName; 303 } 304 305 static bool 306 LoadMainMenuNibIfAvailable(void) 307 { 308 NSDictionary *infoDict; 309 NSString *mainNibFileName; 310 bool success = false; 311 312 if (floor(NSAppKitVersionNumber) < NSAppKitVersionNumber10_8) { 313 return false; 314 } 315 infoDict = [[NSBundle mainBundle] infoDictionary]; 316 if (infoDict) { 317 mainNibFileName = [infoDict valueForKey:@"NSMainNibFile"]; 318 319 if (mainNibFileName) { 320 success = [[NSBundle mainBundle] loadNibNamed:mainNibFileName owner:[NSApplication sharedApplication] topLevelObjects:nil]; 321 } 322 } 323 324 return success; 325 } 326 327 static void 328 CreateApplicationMenus(void) 329 { 330 NSString *appName; 331 NSString *title; 332 NSMenu *appleMenu; 333 NSMenu *serviceMenu; 334 NSMenu *windowMenu; 335 NSMenuItem *menuItem; 336 NSMenu *mainMenu; 337 338 if (NSApp == nil) { 339 return; 340 } 341 342 mainMenu = [[NSMenu alloc] init]; 343 344 /* Create the main menu bar */ 345 [NSApp setMainMenu:mainMenu]; 346 347 [mainMenu release]; /* we're done with it, let NSApp own it. */ 348 mainMenu = nil; 349 350 /* Create the application menu */ 351 appName = GetApplicationName(); 352 appleMenu = [[NSMenu alloc] initWithTitle:@""]; 353 354 /* Add menu items */ 355 title = [@"About " stringByAppendingString:appName]; 356 [appleMenu addItemWithTitle:title action:@selector(orderFrontStandardAboutPanel:) keyEquivalent:@""]; 357 358 [appleMenu addItem:[NSMenuItem separatorItem]]; 359 360 [appleMenu addItemWithTitle:@"Preferences…" action:nil keyEquivalent:@","]; 361 362 [appleMenu addItem:[NSMenuItem separatorItem]]; 363 364 serviceMenu = [[NSMenu alloc] initWithTitle:@""]; 365 menuItem = (NSMenuItem *)[appleMenu addItemWithTitle:@"Services" action:nil keyEquivalent:@""]; 366 [menuItem setSubmenu:serviceMenu]; 367 368 [NSApp setServicesMenu:serviceMenu]; 369 [serviceMenu release]; 370 371 [appleMenu addItem:[NSMenuItem separatorItem]]; 372 373 title = [@"Hide " stringByAppendingString:appName]; 374 [appleMenu addItemWithTitle:title action:@selector(hide:) keyEquivalent:@"h"]; 375 376 menuItem = (NSMenuItem *)[appleMenu addItemWithTitle:@"Hide Others" action:@selector(hideOtherApplications:) keyEquivalent:@"h"]; 377 [menuItem setKeyEquivalentModifierMask:(NSEventModifierFlagOption|NSEventModifierFlagCommand)]; 378 379 [appleMenu addItemWithTitle:@"Show All" action:@selector(unhideAllApplications:) keyEquivalent:@""]; 380 381 [appleMenu addItem:[NSMenuItem separatorItem]]; 382 383 title = [@"Quit " stringByAppendingString:appName]; 384 [appleMenu addItemWithTitle:title action:@selector(terminate:) keyEquivalent:@"q"]; 385 386 /* Put menu into the menubar */ 387 menuItem = [[NSMenuItem alloc] initWithTitle:@"" action:nil keyEquivalent:@""]; 388 [menuItem setSubmenu:appleMenu]; 389 [[NSApp mainMenu] addItem:menuItem]; 390 [menuItem release]; 391 392 /* Tell the application object that this is now the application menu */ 393 [NSApp setAppleMenu:appleMenu]; 394 [appleMenu release]; 395 396 397 /* Create the window menu */ 398 windowMenu = [[NSMenu alloc] initWithTitle:@"Window"]; 399 400 /* Add menu items */ 401 [windowMenu addItemWithTitle:@"Close" action:@selector(performClose:) keyEquivalent:@"w"]; 402 403 [windowMenu addItemWithTitle:@"Minimize" action:@selector(performMiniaturize:) keyEquivalent:@"m"]; 404 405 [windowMenu addItemWithTitle:@"Zoom" action:@selector(performZoom:) keyEquivalent:@""]; 406 407 /* Add the fullscreen toggle menu option, if supported */ 408 if (floor(NSAppKitVersionNumber) > NSAppKitVersionNumber10_6) { 409 /* Cocoa should update the title to Enter or Exit Full Screen automatically. 410 * But if not, then just fallback to Toggle Full Screen. 411 */ 412 menuItem = [[NSMenuItem alloc] initWithTitle:@"Toggle Full Screen" action:@selector(toggleFullScreen:) keyEquivalent:@"f"]; 413 [menuItem setKeyEquivalentModifierMask:NSEventModifierFlagControl | NSEventModifierFlagCommand]; 414 [windowMenu addItem:menuItem]; 415 [menuItem release]; 416 } 417 418 /* Put menu into the menubar */ 419 menuItem = [[NSMenuItem alloc] initWithTitle:@"Window" action:nil keyEquivalent:@""]; 420 [menuItem setSubmenu:windowMenu]; 421 [[NSApp mainMenu] addItem:menuItem]; 422 [menuItem release]; 423 424 /* Tell the application object that this is now the window menu */ 425 [NSApp setWindowsMenu:windowMenu]; 426 [windowMenu release]; 427 } 428 429 void 430 Cocoa_RegisterApp(void) 431 { @autoreleasepool 432 { 433 /* This can get called more than once! Be careful what you initialize! */ 434 435 if (NSApp == nil) { 436 [SDLApplication sharedApplication]; 437 SDL_assert(NSApp != nil); 438 439 s_bShouldHandleEventsInSDLApplication = SDL_TRUE; 440 441 if (!SDL_GetHintBoolean(SDL_HINT_MAC_BACKGROUND_APP, SDL_FALSE)) { 442 [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; 443 } 444 445 /* If there aren't already menus in place, look to see if there's 446 * a nib we should use. If not, then manually create the basic 447 * menus we meed. 448 */ 449 if ([NSApp mainMenu] == nil) { 450 bool nibLoaded; 451 452 nibLoaded = LoadMainMenuNibIfAvailable(); 453 if (!nibLoaded) { 454 CreateApplicationMenus(); 455 } 456 } 457 [NSApp finishLaunching]; 458 if ([NSApp delegate]) { 459 /* The SDL app delegate calls this in didFinishLaunching if it's 460 * attached to the NSApp, otherwise we need to call it manually. 461 */ 462 [SDLApplication registerUserDefaults]; 463 } 464 } 465 if (NSApp && !appDelegate) { 466 appDelegate = [[SDLAppDelegate alloc] init]; 467 468 /* If someone else has an app delegate, it means we can't turn a 469 * termination into SDL_Quit, and we can't handle application:openFile: 470 */ 471 if (![NSApp delegate]) { 472 [(NSApplication *)NSApp setDelegate:appDelegate]; 473 } else { 474 appDelegate->seenFirstActivate = YES; 475 } 476 } 477 }} 478 479 void 480 Cocoa_PumpEvents(_THIS) 481 { @autoreleasepool 482 { 483 #if MAC_OS_X_VERSION_MIN_REQUIRED < 1070 484 /* Update activity every 30 seconds to prevent screensaver */ 485 SDL_VideoData *data = (SDL_VideoData *)_this->driverdata; 486 if (_this->suspend_screensaver && !data->screensaver_use_iopm) { 487 Uint32 now = SDL_GetTicks(); 488 if (!data->screensaver_activity || 489 SDL_TICKS_PASSED(now, data->screensaver_activity + 30000)) { 490 UpdateSystemActivity(UsrActivity); 491 data->screensaver_activity = now; 492 } 493 } 494 #endif 495 496 for ( ; ; ) { 497 NSEvent *event = [NSApp nextEventMatchingMask:NSEventMaskAny untilDate:[NSDate distantPast] inMode:NSDefaultRunLoopMode dequeue:YES ]; 498 if ( event == nil ) { 499 break; 500 } 501 502 if (!s_bShouldHandleEventsInSDLApplication) { 503 Cocoa_DispatchEvent(event); 504 } 505 506 // Pass events down to SDLApplication to be handled in sendEvent: 507 [NSApp sendEvent:event]; 508 } 509 }} 510 511 void 512 Cocoa_SuspendScreenSaver(_THIS) 513 { @autoreleasepool 514 { 515 SDL_VideoData *data = (SDL_VideoData *)_this->driverdata; 516 517 if (!data->screensaver_use_iopm) { 518 return; 519 } 520 521 if (data->screensaver_assertion) { 522 IOPMAssertionRelease(data->screensaver_assertion); 523 data->screensaver_assertion = 0; 524 } 525 526 if (_this->suspend_screensaver) { 527 /* FIXME: this should ideally describe the real reason why the game 528 * called SDL_DisableScreenSaver. Note that the name is only meant to be 529 * seen by OS X power users. there's an additional optional human-readable 530 * (localized) reason parameter which we don't set. 531 */ 532 NSString *name = [GetApplicationName() stringByAppendingString:@" using SDL_DisableScreenSaver"]; 533 IOPMAssertionCreateWithDescription(kIOPMAssertPreventUserIdleDisplaySleep, 534 (CFStringRef) name, 535 NULL, NULL, NULL, 0, NULL, 536 &data->screensaver_assertion); 537 } 538 }} 539 540 #endif /* SDL_VIDEO_DRIVER_COCOA */ 541 542 /* vi: set ts=4 sw=4 expandtab: */