1/* RetroArch - A frontend for libretro. 2 * Copyright (C) 2011-2016 - Daniel De Matteis 3 * 4 * RetroArch is free software: you can redistribute it and/or modify it under the terms 5 * of the GNU General Public License as published by the Free Software Found- 6 * ation, either version 3 of the License, or (at your option) any later version. 7 * 8 * RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 9 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR 10 * PURPOSE. See the GNU General Public License for more details. 11 * 12 * You should have received a copy of the GNU General Public License along with RetroArch. 13 * If not, see <http://www.gnu.org/licenses/>. 14 */ 15 16#include <stdint.h> 17#include <stddef.h> 18#include <stdlib.h> 19#include <string.h> 20 21#include <boolean.h> 22 23#include <file/file_path.h> 24#include <queues/task_queue.h> 25#include <string/stdstring.h> 26#include <retro_timers.h> 27 28#include "cocoa/cocoa_common.h" 29#include "cocoa/apple_platform.h" 30#include "../ui_companion_driver.h" 31#include "../../configuration.h" 32#include "../../frontend/frontend.h" 33#include "../../input/drivers/cocoa_input.h" 34#include "../../input/drivers_keyboard/keyboard_event_apple.h" 35#include "../../retroarch.h" 36 37#ifdef HAVE_MENU 38#include "../../menu/menu_setting.h" 39#endif 40 41#import <AVFoundation/AVFoundation.h> 42 43#if defined(HAVE_COCOA_METAL) || defined(HAVE_COCOATOUCH) 44id<ApplePlatform> apple_platform; 45#else 46static id apple_platform; 47#endif 48static CFRunLoopObserverRef iterate_observer; 49 50/* Forward declaration */ 51static void apple_rarch_exited(void); 52 53static void rarch_enable_ui(void) 54{ 55 bool boolean = true; 56 57 ui_companion_set_foreground(true); 58 59 rarch_ctl(RARCH_CTL_SET_PAUSED, &boolean); 60 rarch_ctl(RARCH_CTL_SET_IDLE, &boolean); 61 retroarch_menu_running(); 62} 63 64static void rarch_disable_ui(void) 65{ 66 bool boolean = false; 67 68 ui_companion_set_foreground(false); 69 70 rarch_ctl(RARCH_CTL_SET_PAUSED, &boolean); 71 rarch_ctl(RARCH_CTL_SET_IDLE, &boolean); 72 retroarch_menu_running_finished(false); 73} 74 75static void ui_companion_cocoatouch_event_command( 76 void *data, enum event_command cmd) { } 77 78static void rarch_draw_observer(CFRunLoopObserverRef observer, 79 CFRunLoopActivity activity, void *info) 80{ 81 int ret = runloop_iterate(); 82 83 task_queue_check(); 84 85 if (ret == -1) 86 { 87 ui_companion_cocoatouch_event_command( 88 NULL, CMD_EVENT_MENU_SAVE_CURRENT_CONFIG); 89 main_exit(NULL); 90 return; 91 } 92 93 if (rarch_ctl(RARCH_CTL_IS_IDLE, NULL)) 94 return; 95 CFRunLoopWakeUp(CFRunLoopGetMain()); 96} 97 98apple_frontend_settings_t apple_frontend_settings; 99 100void get_ios_version(int *major, int *minor) 101{ 102 NSArray *decomposed_os_version = [[UIDevice currentDevice].systemVersion componentsSeparatedByString:@"."]; 103 104 if (major && decomposed_os_version.count > 0) 105 *major = (int)[decomposed_os_version[0] integerValue]; 106 if (minor && decomposed_os_version.count > 1) 107 *minor = (int)[decomposed_os_version[1] integerValue]; 108} 109 110/* Input helpers: This is kept here because it needs ObjC */ 111static void handle_touch_event(NSArray* touches) 112{ 113 unsigned i; 114 cocoa_input_data_t *apple = (cocoa_input_data_t*)input_driver_get_data(); 115 float scale = cocoa_screen_get_native_scale(); 116 117 if (!apple) 118 return; 119 120 apple->touch_count = 0; 121 122 for (i = 0; i < touches.count && (apple->touch_count < MAX_TOUCHES); i++) 123 { 124 UITouch *touch = [touches objectAtIndex:i]; 125 CGPoint coord = [touch locationInView:[touch view]]; 126 if (touch.phase != UITouchPhaseEnded && touch.phase != UITouchPhaseCancelled) 127 { 128 apple->touches[apple->touch_count ].screen_x = coord.x * scale; 129 apple->touches[apple->touch_count ++].screen_y = coord.y * scale; 130 } 131 } 132} 133 134#ifndef HAVE_APPLE_STORE 135/* iOS7 Keyboard support */ 136@interface UIEvent(iOS7Keyboard) 137@property(readonly, nonatomic) long long _keyCode; 138@property(readonly, nonatomic) _Bool _isKeyDown; 139@property(retain, nonatomic) NSString *_privateInput; 140@property(nonatomic) long long _modifierFlags; 141- (struct __IOHIDEvent { }*)_hidEvent; 142@end 143 144@interface UIApplication(iOS7Keyboard) 145- (void)handleKeyUIEvent:(UIEvent*)event; 146- (id)_keyCommandForEvent:(UIEvent*)event; 147@end 148#endif 149 150@interface RApplication : UIApplication 151@end 152 153@implementation RApplication 154 155#ifndef HAVE_APPLE_STORE 156/* Keyboard handler for iOS 7. */ 157 158/* This is copied here as it isn't 159 * defined in any standard iOS header */ 160enum 161{ 162 NSAlphaShiftKeyMask = 1 << 16, 163 NSShiftKeyMask = 1 << 17, 164 NSControlKeyMask = 1 << 18, 165 NSAlternateKeyMask = 1 << 19, 166 NSCommandKeyMask = 1 << 20, 167 NSNumericPadKeyMask = 1 << 21, 168 NSHelpKeyMask = 1 << 22, 169 NSFunctionKeyMask = 1 << 23, 170 NSDeviceIndependentModifierFlagsMask = 0xffff0000U 171}; 172 173/* This is specifically for iOS 9, according to the private headers */ 174-(void)handleKeyUIEvent:(UIEvent *)event 175{ 176 /* This gets called twice with the same timestamp 177 * for each keypress, that's fine for polling 178 * but is bad for business with events. */ 179 static double last_time_stamp; 180 181 if (last_time_stamp == event.timestamp) 182 return [super handleKeyUIEvent:event]; 183 184 last_time_stamp = event.timestamp; 185 186 /* If the _hidEvent is null, [event _keyCode] will crash. 187 * (This happens with the on screen keyboard). */ 188 if (event._hidEvent) 189 { 190 NSString *ch = (NSString*)event._privateInput; 191 uint32_t character = 0; 192 uint32_t mod = 0; 193 NSUInteger mods = event._modifierFlags; 194 195 if (mods & NSAlphaShiftKeyMask) 196 mod |= RETROKMOD_CAPSLOCK; 197 if (mods & NSShiftKeyMask) 198 mod |= RETROKMOD_SHIFT; 199 if (mods & NSControlKeyMask) 200 mod |= RETROKMOD_CTRL; 201 if (mods & NSAlternateKeyMask) 202 mod |= RETROKMOD_ALT; 203 if (mods & NSCommandKeyMask) 204 mod |= RETROKMOD_META; 205 if (mods & NSNumericPadKeyMask) 206 mod |= RETROKMOD_NUMLOCK; 207 208 if (ch && ch.length != 0) 209 { 210 unsigned i; 211 character = [ch characterAtIndex:0]; 212 213 apple_input_keyboard_event(event._isKeyDown, 214 (uint32_t)event._keyCode, 0, mod, 215 RETRO_DEVICE_KEYBOARD); 216 217 for (i = 1; i < ch.length; i++) 218 apple_input_keyboard_event(event._isKeyDown, 219 0, [ch characterAtIndex:i], mod, 220 RETRO_DEVICE_KEYBOARD); 221 } 222 223 apple_input_keyboard_event(event._isKeyDown, 224 (uint32_t)event._keyCode, character, mod, 225 RETRO_DEVICE_KEYBOARD); 226 } 227 228 [super handleKeyUIEvent:event]; 229} 230 231/* This is for iOS versions < 9.0 */ 232- (id)_keyCommandForEvent:(UIEvent*)event 233{ 234 /* This gets called twice with the same timestamp 235 * for each keypress, that's fine for polling 236 * but is bad for business with events. */ 237 static double last_time_stamp; 238 239 if (last_time_stamp == event.timestamp) 240 return [super _keyCommandForEvent:event]; 241 last_time_stamp = event.timestamp; 242 243 /* If the _hidEvent is null, [event _keyCode] will crash. 244 * (This happens with the on screen keyboard). */ 245 if (event._hidEvent) 246 { 247 NSString *ch = (NSString*)event._privateInput; 248 uint32_t character = 0; 249 uint32_t mod = 0; 250 NSUInteger mods = event._modifierFlags; 251 252 if (mods & NSAlphaShiftKeyMask) 253 mod |= RETROKMOD_CAPSLOCK; 254 if (mods & NSShiftKeyMask) 255 mod |= RETROKMOD_SHIFT; 256 if (mods & NSControlKeyMask) 257 mod |= RETROKMOD_CTRL; 258 if (mods & NSAlternateKeyMask) 259 mod |= RETROKMOD_ALT; 260 if (mods & NSCommandKeyMask) 261 mod |= RETROKMOD_META; 262 if (mods & NSNumericPadKeyMask) 263 mod |= RETROKMOD_NUMLOCK; 264 265 if (ch && ch.length != 0) 266 { 267 unsigned i; 268 character = [ch characterAtIndex:0]; 269 270 apple_input_keyboard_event(event._isKeyDown, 271 (uint32_t)event._keyCode, 0, mod, 272 RETRO_DEVICE_KEYBOARD); 273 274 for (i = 1; i < ch.length; i++) 275 apple_input_keyboard_event(event._isKeyDown, 276 0, [ch characterAtIndex:i], mod, 277 RETRO_DEVICE_KEYBOARD); 278 } 279 280 apple_input_keyboard_event(event._isKeyDown, 281 (uint32_t)event._keyCode, character, mod, 282 RETRO_DEVICE_KEYBOARD); 283 } 284 285 return [super _keyCommandForEvent:event]; 286} 287#endif 288 289#define GSEVENT_TYPE_KEYDOWN 10 290#define GSEVENT_TYPE_KEYUP 11 291 292- (void)sendEvent:(UIEvent *)event 293{ 294 [super sendEvent:event]; 295 296 if (event.allTouches.count) 297 handle_touch_event(event.allTouches.allObjects); 298 299#if __IPHONE_OS_VERSION_MAX_ALLOWED < 70000 300 { 301 int major, minor; 302 get_ios_version(&major, &minor); 303 304 if ((major < 7) && [event respondsToSelector:@selector(_gsEvent)]) 305 { 306 /* Keyboard event hack for iOS versions prior to iOS 7. 307 * 308 * Derived from: 309 * http://nacho4d-nacho4d.blogspot.com/2012/01/ 310 * catching-keyboard-events-in-ios.html 311 */ 312 const uint8_t *eventMem = objc_unretainedPointer([event performSelector:@selector(_gsEvent)]); 313 int eventType = eventMem ? *(int*)&eventMem[8] : 0; 314 315 switch (eventType) 316 { 317 case GSEVENT_TYPE_KEYDOWN: 318 case GSEVENT_TYPE_KEYUP: 319 apple_input_keyboard_event(eventType == GSEVENT_TYPE_KEYDOWN, 320 *(uint16_t*)&eventMem[0x3C], 0, 0, RETRO_DEVICE_KEYBOARD); 321 break; 322 } 323 } 324 } 325#endif 326} 327 328@end 329 330@implementation RetroArch_iOS 331 332#pragma mark - ApplePlatform 333-(id)renderView { return _renderView; } 334-(bool)hasFocus { return YES; } 335 336- (void)setViewType:(apple_view_type_t)vt 337{ 338 if (vt == _vt) 339 return; 340 341 _vt = vt; 342 if (_renderView != nil) 343 { 344 [_renderView removeFromSuperview]; 345 _renderView = nil; 346 } 347 348 switch (vt) 349 { 350#ifdef HAVE_COCOA_METAL 351 case APPLE_VIEW_TYPE_VULKAN: 352 case APPLE_VIEW_TYPE_METAL: 353 { 354 MetalView *v = [MetalView new]; 355 v.paused = YES; 356 v.enableSetNeedsDisplay = NO; 357#if TARGET_OS_IOS 358 v.multipleTouchEnabled = YES; 359#endif 360 _renderView = v; 361 } 362 break; 363#endif 364 case APPLE_VIEW_TYPE_OPENGL_ES: 365 _renderView = (BRIDGE GLKView*)glkitview_init(); 366 break; 367 368 case APPLE_VIEW_TYPE_NONE: 369 default: 370 return; 371 } 372 373 _renderView.translatesAutoresizingMaskIntoConstraints = NO; 374 UIView *rootView = [CocoaView get].view; 375 [rootView addSubview:_renderView]; 376 [[_renderView.topAnchor constraintEqualToAnchor:rootView.topAnchor] setActive:YES]; 377 [[_renderView.bottomAnchor constraintEqualToAnchor:rootView.bottomAnchor] setActive:YES]; 378 [[_renderView.leadingAnchor constraintEqualToAnchor:rootView.leadingAnchor] setActive:YES]; 379 [[_renderView.trailingAnchor constraintEqualToAnchor:rootView.trailingAnchor] setActive:YES]; 380} 381 382- (apple_view_type_t)viewType { return _vt; } 383 384- (void)setVideoMode:(gfx_ctx_mode_t)mode 385{ 386#ifdef HAVE_COCOA_METAL 387 MetalView *metalView = (MetalView*) _renderView; 388 CGFloat scale = [[UIScreen mainScreen] scale]; 389 [metalView setDrawableSize:CGSizeMake( 390 _renderView.bounds.size.width * scale, 391 _renderView.bounds.size.height * scale 392 )]; 393#endif 394} 395 396- (void)setCursorVisible:(bool)v { /* no-op for iOS */ } 397- (bool)setDisableDisplaySleep:(bool)disable { /* no-op for iOS */ return NO; } 398+ (RetroArch_iOS*)get { return (RetroArch_iOS*)[[UIApplication sharedApplication] delegate]; } 399 400-(NSString*)documentsDirectory 401{ 402 if (_documentsDirectory == nil) 403 { 404#if TARGET_OS_IOS 405 NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); 406#elif TARGET_OS_TV 407 NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); 408#endif 409 410 _documentsDirectory = paths.firstObject; 411 } 412 return _documentsDirectory; 413} 414 415- (void)applicationDidFinishLaunching:(UIApplication *)application 416{ 417 NSError *error; 418 char arguments[] = "retroarch"; 419 char *argv[] = {arguments, NULL}; 420 int argc = 1; 421 apple_platform = self; 422 423 [self setDelegate:self]; 424 425 /* Setup window */ 426 self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; 427 [self.window makeKeyAndVisible]; 428 429 [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryAmbient error:&error]; 430 431 [self refreshSystemConfig]; 432 [self showGameView]; 433 434 if (rarch_main(argc, argv, NULL)) 435 apple_rarch_exited(); 436 437 iterate_observer = CFRunLoopObserverCreate(0, kCFRunLoopBeforeWaiting, 438 true, 0, rarch_draw_observer, 0); 439 CFRunLoopAddObserver(CFRunLoopGetMain(), iterate_observer, kCFRunLoopCommonModes); 440 441#ifdef HAVE_MFI 442 extern void *apple_gamecontroller_joypad_init(void *data); 443 apple_gamecontroller_joypad_init(NULL); 444#endif 445} 446 447- (void)applicationDidEnterBackground:(UIApplication *)application { } 448 449- (void)applicationWillTerminate:(UIApplication *)application 450{ 451 CFRunLoopObserverInvalidate(iterate_observer); 452 CFRelease(iterate_observer); 453 iterate_observer = NULL; 454} 455 456- (void)applicationDidBecomeActive:(UIApplication *)application 457{ 458 settings_t *settings = config_get_ptr(); 459 bool ui_companion_start_on_boot = settings->bools.ui_companion_start_on_boot; 460 461 if (!ui_companion_start_on_boot) 462 [self showGameView]; 463} 464 465-(BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options { 466 NSFileManager *manager = [NSFileManager defaultManager]; 467 NSString *filename = (NSString*)url.path.lastPathComponent; 468 NSError *error = nil; 469 NSString *destination = [self.documentsDirectory stringByAppendingPathComponent:filename]; 470 471 // copy file to documents directory if its not already inside of documents directory 472 if ([url startAccessingSecurityScopedResource]) { 473 if (![[url path] containsString: self.documentsDirectory]) 474 if (![manager fileExistsAtPath:destination]) 475 if (![manager copyItemAtPath:[url path] toPath:destination error:&error]) 476 printf("%s\n", [[error description] UTF8String]); 477 [url stopAccessingSecurityScopedResource]; 478 } 479 return true; 480} 481 482- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated 483{ 484#if TARGET_OS_IOS 485 [self setToolbarHidden:![[viewController toolbarItems] count] animated:YES]; 486#endif 487 [self refreshSystemConfig]; 488} 489 490- (void)showGameView 491{ 492 [self popToRootViewControllerAnimated:NO]; 493 494#if TARGET_OS_IOS 495 [self setToolbarHidden:true animated:NO]; 496 [[UIApplication sharedApplication] setStatusBarHidden:true withAnimation:UIStatusBarAnimationNone]; 497#endif 498 499 [[UIApplication sharedApplication] setIdleTimerDisabled:true]; 500 [self.window setRootViewController:[CocoaView get]]; 501 502 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ 503 command_event(CMD_EVENT_AUDIO_START, NULL); 504 }); 505 rarch_disable_ui(); 506} 507 508- (IBAction)showPauseMenu:(id)sender 509{ 510 rarch_enable_ui(); 511 512#if TARGET_OS_IOS 513 [[UIApplication sharedApplication] setStatusBarHidden:false withAnimation:UIStatusBarAnimationNone]; 514#endif 515 516 [[UIApplication sharedApplication] setIdleTimerDisabled:false]; 517 [self.window setRootViewController:self]; 518} 519 520- (void)refreshSystemConfig 521{ 522#if TARGET_OS_IOS 523 /* Get enabled orientations */ 524 apple_frontend_settings.orientation_flags = UIInterfaceOrientationMaskAll; 525 526 if (string_is_equal(apple_frontend_settings.orientations, "landscape")) 527 apple_frontend_settings.orientation_flags = 528 UIInterfaceOrientationMaskLandscape; 529 else if (string_is_equal(apple_frontend_settings.orientations, "portrait")) 530 apple_frontend_settings.orientation_flags = 531 UIInterfaceOrientationMaskPortrait 532 | UIInterfaceOrientationMaskPortraitUpsideDown; 533#endif 534} 535 536- (void)supportOtherAudioSessions { } 537@end 538 539int main(int argc, char *argv[]) 540{ 541 @autoreleasepool { 542 return UIApplicationMain(argc, argv, NSStringFromClass([RApplication class]), NSStringFromClass([RetroArch_iOS class])); 543 } 544} 545 546static void apple_rarch_exited(void) 547{ 548 RetroArch_iOS *ap = (RetroArch_iOS *)apple_platform; 549 550 if (!ap) 551 return; 552 [ap showPauseMenu:ap]; 553} 554