1/* RetroArch - A frontend for libretro. 2 * Copyright (C) 2013-2014 - Jason Fetters 3 * Copyright (C) 2011-2017 - Daniel De Matteis 4 * 5 * RetroArch is free software: you can redistribute it and/or modify it under the terms 6 * of the GNU General Public License as published by the Free Software Found- 7 * ation, either version 3 of the License, or (at your option) any later version. 8 * 9 * RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 10 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR 11 * PURPOSE. See the GNU General Public License for more details. 12 * 13 * You should have received a copy of the GNU General Public License along with RetroArch. 14 * If not, see <http://www.gnu.org/licenses/>. 15 */ 16 17#import <AvailabilityMacros.h> 18#include <sys/stat.h> 19 20#include <retro_assert.h> 21 22#include "cocoa_common.h" 23#include "apple_platform.h" 24#include "../ui_cocoa.h" 25 26#ifdef HAVE_COCOATOUCH 27#import "../../../pkg/apple/WebServer/GCDWebUploader/GCDWebUploader.h" 28#import "WebServer.h" 29#endif 30 31#include "../../../configuration.h" 32#include "../../../retroarch.h" 33#include "../../../verbosity.h" 34 35static CocoaView* g_instance; 36 37#ifdef HAVE_COCOATOUCH 38void *glkitview_init(void); 39 40@interface CocoaView()<GCDWebUploaderDelegate> { 41 42} 43@end 44#endif 45 46@implementation CocoaView 47 48#if defined(OSX) 49#ifdef HAVE_COCOA_METAL 50- (BOOL)layer:(CALayer *)layer shouldInheritContentsScale:(CGFloat)newScale fromWindow:(NSWindow *)window { return YES; } 51#endif 52- (void)scrollWheel:(NSEvent *)theEvent { } 53#endif 54 55+ (CocoaView*)get 56{ 57 CocoaView *view = (BRIDGE CocoaView*)nsview_get_ptr(); 58 if (!view) 59 { 60 view = [CocoaView new]; 61 nsview_set_ptr(view); 62 } 63 return view; 64} 65 66- (id)init 67{ 68 self = [super init]; 69 70#if defined(OSX) 71 [self setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; 72 NSArray *array = [NSArray arrayWithObjects:NSColorPboardType, NSFilenamesPboardType, nil]; 73 [self registerForDraggedTypes:array]; 74#endif 75 76#if defined(HAVE_COCOA) 77 ui_window_cocoa_t cocoa_view; 78 cocoa_view.data = (CocoaView*)self; 79#elif defined(HAVE_COCOATOUCH) 80#if defined(HAVE_COCOA_METAL) 81 self.view = [UIView new]; 82#else 83 self.view = (BRIDGE GLKView*)glkitview_init(); 84#endif 85#endif 86 87#if defined(OSX) 88 video_driver_display_type_set(RARCH_DISPLAY_OSX); 89 video_driver_display_set(0); 90 video_driver_display_userdata_set((uintptr_t)self); 91#elif TARGET_OS_IOS 92 UISwipeGestureRecognizer *swipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(showNativeMenu)]; 93 swipe.numberOfTouchesRequired = 4; 94 swipe.direction = UISwipeGestureRecognizerDirectionDown; 95 [self.view addGestureRecognizer:swipe]; 96#endif 97 98 return self; 99} 100 101#if defined(OSX) 102- (void)setFrame:(NSRect)frameRect 103{ 104 [super setFrame:frameRect]; 105/* forward declarations */ 106#if defined(HAVE_OPENGL) 107 void cocoa_gl_gfx_ctx_update(void); 108 cocoa_gl_gfx_ctx_update(); 109#endif 110} 111 112/* Stop the annoying sound when pressing a key. */ 113- (BOOL)acceptsFirstResponder { return YES; } 114- (BOOL)isFlipped { return YES; } 115- (void)keyDown:(NSEvent*)theEvent { } 116 117- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender 118{ 119 NSDragOperation sourceDragMask = [sender draggingSourceOperationMask]; 120 NSPasteboard *pboard = [sender draggingPasteboard]; 121 122 if ( [[pboard types] containsObject:NSFilenamesPboardType] ) 123 { 124 if (sourceDragMask & NSDragOperationCopy) 125 return NSDragOperationCopy; 126 } 127 128 return NSDragOperationNone; 129} 130 131- (BOOL)performDragOperation:(id<NSDraggingInfo>)sender 132{ 133 NSPasteboard *pboard = [sender draggingPasteboard]; 134 135 if ( [[pboard types] containsObject:NSURLPboardType]) 136 { 137 NSURL *fileURL = [NSURL URLFromPasteboard:pboard]; 138 NSString *s = [fileURL path]; 139 if (s != nil) 140 { 141 RARCH_LOG("Drop name is: %s\n", [s UTF8String]); 142 } 143 } 144 return YES; 145} 146 147- (void)draggingExited:(id <NSDraggingInfo>)sender { [self setNeedsDisplay: YES]; } 148 149#elif TARGET_OS_IOS 150-(void) showNativeMenu 151{ 152 dispatch_async(dispatch_get_main_queue(), ^{ 153 command_event(CMD_EVENT_MENU_TOGGLE, NULL); 154 }); 155} 156 157-(BOOL)prefersHomeIndicatorAutoHidden { return YES; } 158-(void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator 159{ 160 [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; 161 if (@available(iOS 11, *)) 162 { 163 [coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) { 164 [self adjustViewFrameForSafeArea]; 165 } completion:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) { 166 }]; 167 } 168} 169 170-(void)adjustViewFrameForSafeArea 171{ 172 /* This is for adjusting the view frame to account for 173 * the notch in iPhone X phones */ 174 if (@available(iOS 11, *)) 175 { 176 RAScreen *screen = (BRIDGE RAScreen*)cocoa_screen_get_chosen(); 177 CGRect screenSize = [screen bounds]; 178 UIEdgeInsets inset = [[UIApplication sharedApplication] delegate].window.safeAreaInsets; 179 UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation]; 180 switch (orientation) 181 { 182 case UIInterfaceOrientationPortrait: 183 self.view.frame = CGRectMake(screenSize.origin.x, 184 screenSize.origin.y + inset.top, 185 screenSize.size.width, 186 screenSize.size.height - inset.top); 187 break; 188 case UIInterfaceOrientationLandscapeLeft: 189 self.view.frame = CGRectMake(screenSize.origin.x + inset.right, 190 screenSize.origin.y, 191 screenSize.size.width - inset.right * 2, 192 screenSize.size.height); 193 break; 194 case UIInterfaceOrientationLandscapeRight: 195 self.view.frame = CGRectMake(screenSize.origin.x + inset.left, 196 screenSize.origin.y, 197 screenSize.size.width - inset.left * 2, 198 screenSize.size.height); 199 break; 200 default: 201 self.view.frame = screenSize; 202 break; 203 } 204 } 205} 206 207- (void)viewWillLayoutSubviews 208{ 209 float width = 0.0f, height = 0.0f; 210 RAScreen *screen = (BRIDGE RAScreen*)cocoa_screen_get_chosen(); 211 UIInterfaceOrientation orientation = self.interfaceOrientation; 212 CGRect screenSize = [screen bounds]; 213 SEL selector = NSSelectorFromString(BOXSTRING("coordinateSpace")); 214 215 if ([screen respondsToSelector:selector]) 216 { 217 screenSize = [[screen coordinateSpace] bounds]; 218 width = CGRectGetWidth(screenSize); 219 height = CGRectGetHeight(screenSize); 220 } 221 else 222 { 223 width = ((int)orientation < 3) 224 ? CGRectGetWidth(screenSize) 225 : CGRectGetHeight(screenSize); 226 height = ((int)orientation < 3) 227 ? CGRectGetHeight(screenSize) 228 : CGRectGetWidth(screenSize); 229 } 230 231 [self adjustViewFrameForSafeArea]; 232} 233 234/* NOTE: This version runs on iOS6+. */ 235- (NSUInteger)supportedInterfaceOrientations 236{ 237 return (NSUInteger)apple_frontend_settings.orientation_flags; 238} 239 240/* NOTE: This version runs on iOS2-iOS5, but not iOS6+. */ 241- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation 242{ 243 unsigned orientation_flags = apple_frontend_settings.orientation_flags; 244 245 switch (interfaceOrientation) 246 { 247 case UIInterfaceOrientationPortrait: 248 return (orientation_flags 249 & UIInterfaceOrientationMaskPortrait); 250 case UIInterfaceOrientationPortraitUpsideDown: 251 return (orientation_flags 252 & UIInterfaceOrientationMaskPortraitUpsideDown); 253 case UIInterfaceOrientationLandscapeLeft: 254 return (orientation_flags 255 & UIInterfaceOrientationMaskLandscapeLeft); 256 case UIInterfaceOrientationLandscapeRight: 257 return (orientation_flags 258 & UIInterfaceOrientationMaskLandscapeRight); 259 260 default: 261 break; 262 } 263 264 return (orientation_flags 265 & UIInterfaceOrientationMaskAll); 266} 267#endif 268 269#ifdef HAVE_COCOATOUCH 270- (void)viewDidAppear:(BOOL)animated 271{ 272#if TARGET_OS_IOS 273 if (@available(iOS 11.0, *)) 274 [self setNeedsUpdateOfHomeIndicatorAutoHidden]; 275#endif 276} 277 278-(void)viewWillAppear:(BOOL)animated 279{ 280 [super viewWillAppear:animated]; 281#if TARGET_OS_TV 282 [[WebServer sharedInstance] startUploader]; 283 [WebServer sharedInstance].webUploader.delegate = self; 284#endif 285} 286 287#pragma mark GCDWebServerDelegate 288- (void)webServerDidCompleteBonjourRegistration:(GCDWebServer*)server 289{ 290 NSMutableString *servers = [[NSMutableString alloc] init]; 291 if (server.serverURL != nil) 292 [servers appendString:[NSString stringWithFormat:@"%@",server.serverURL]]; 293 if (servers.length > 0) 294 [servers appendString:@"\n\n"]; 295 if (server.bonjourServerURL != nil) 296 [servers appendString:[NSString stringWithFormat:@"%@",server.bonjourServerURL]]; 297 298#if TARGET_OS_TV || TARGET_OS_IOS 299 UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Welcome to RetroArch" message:[NSString stringWithFormat:@"To transfer files from your computer, go to one of these addresses on your web browser:\n\n%@",servers] preferredStyle:UIAlertControllerStyleAlert]; 300#if TARGET_OS_TV 301 [alert addAction:[UIAlertAction actionWithTitle:@"OK" 302 style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { 303 }]]; 304#elif TARGET_OS_IOS 305 [alert addAction:[UIAlertAction actionWithTitle:@"Stop Server" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { 306 [[WebServer sharedInstance] webUploader].delegate = nil; 307 [[WebServer sharedInstance] stopUploader]; 308 }]]; 309#endif 310 [self presentViewController:alert animated:YES completion:^{ 311 }]; 312#endif 313} 314#endif 315 316@end 317 318void *cocoa_screen_get_chosen(void) 319{ 320 unsigned monitor_index; 321 settings_t *settings = config_get_ptr(); 322 NSArray *screens = [RAScreen screens]; 323 if (!screens || !settings) 324 return NULL; 325 326 monitor_index = settings->uints.video_monitor_index; 327 328 if (monitor_index >= screens.count) 329 { 330 RARCH_WARN("video_monitor_index is greater than the number of connected monitors; using main screen instead."); 331 return (BRIDGE void*)screens; 332 } 333 334 return ((BRIDGE void*)[screens objectAtIndex:monitor_index]); 335} 336 337bool cocoa_has_focus(void *data) 338{ 339#if defined(HAVE_COCOATOUCH) 340 return ([[UIApplication sharedApplication] applicationState] 341 == UIApplicationStateActive); 342#else 343 return [NSApp isActive]; 344#endif 345} 346 347void cocoa_show_mouse(void *data, bool state) 348{ 349#ifdef OSX 350 if (state) 351 [NSCursor unhide]; 352 else 353 [NSCursor hide]; 354#endif 355} 356 357#ifdef OSX 358#if MAC_OS_X_VERSION_10_7 359/* NOTE: backingScaleFactor only available on MacOS X 10.7 and up. */ 360float cocoa_screen_get_backing_scale_factor(void) 361{ 362 static float 363 backing_scale_def = 0.0f; 364 if (backing_scale_def == 0.0f) 365 { 366 RAScreen *screen = (BRIDGE RAScreen*)cocoa_screen_get_chosen(); 367 if (!screen) 368 return 1.0f; 369 backing_scale_def = [screen backingScaleFactor]; 370 } 371 return backing_scale_def; 372} 373#else 374float cocoa_screen_get_backing_scale_factor(void) { return 1.0f; } 375#endif 376#else 377static float get_from_selector( 378 Class obj_class, id obj_id, SEL selector, CGFloat *ret) 379{ 380 NSInvocation *invocation = [NSInvocation invocationWithMethodSignature: 381 [obj_class instanceMethodSignatureForSelector:selector]]; 382 [invocation setSelector:selector]; 383 [invocation setTarget:obj_id]; 384 [invocation invoke]; 385 [invocation getReturnValue:ret]; 386 RELEASE(invocation); 387 return *ret; 388} 389 390/* NOTE: nativeScale only available on iOS 8.0 and up. */ 391float cocoa_screen_get_native_scale(void) 392{ 393 SEL selector; 394 static CGFloat ret = 0.0f; 395 RAScreen *screen = NULL; 396 397 if (ret != 0.0f) 398 return ret; 399 screen = (BRIDGE RAScreen*)cocoa_screen_get_chosen(); 400 if (!screen) 401 return 0.0f; 402 403 selector = NSSelectorFromString(BOXSTRING("nativeScale")); 404 405 if ([screen respondsToSelector:selector]) 406 ret = (float)get_from_selector( 407 [screen class], screen, selector, &ret); 408 else 409 { 410 ret = 1.0f; 411 selector = NSSelectorFromString(BOXSTRING("scale")); 412 if ([screen respondsToSelector:selector]) 413 ret = screen.scale; 414 } 415 416 return ret; 417} 418#endif 419 420void *nsview_get_ptr(void) 421{ 422#if defined(OSX) 423 video_driver_display_type_set(RARCH_DISPLAY_OSX); 424 video_driver_display_set(0); 425 video_driver_display_userdata_set((uintptr_t)g_instance); 426#endif 427 return (BRIDGE void *)g_instance; 428} 429 430void nsview_set_ptr(CocoaView *p) { g_instance = p; } 431 432CocoaView *cocoaview_get(void) 433{ 434#if defined(HAVE_COCOA_METAL) 435 return (CocoaView*)apple_platform.renderView; 436#elif defined(HAVE_COCOA) 437 return g_instance; 438#else 439 /* TODO/FIXME - implement */ 440 return NULL; 441#endif 442} 443 444#ifdef OSX 445void cocoa_update_title(void *data) 446{ 447 const ui_window_t *window = ui_companion_driver_get_window_ptr(); 448 449 if (window) 450 { 451 char title[128]; 452 453 title[0] = '\0'; 454 455 video_driver_get_window_title(title, sizeof(title)); 456 457 if (title[0]) 458 window->set_title((void*)video_driver_display_userdata_get(), title); 459 } 460} 461 462bool cocoa_get_metrics( 463 void *data, enum display_metric_types type, 464 float *value) 465{ 466 RAScreen *screen = (BRIDGE RAScreen*)cocoa_screen_get_chosen(); 467 NSDictionary *desc = [screen deviceDescription]; 468 CGSize display_physical_size = CGDisplayScreenSize( 469 [[desc objectForKey:@"NSScreenNumber"] unsignedIntValue]); 470 471 float physical_width = display_physical_size.width; 472 float physical_height = display_physical_size.height; 473 474 switch (type) 475 { 476 case DISPLAY_METRIC_MM_WIDTH: 477 *value = physical_width; 478 break; 479 case DISPLAY_METRIC_MM_HEIGHT: 480 *value = physical_height; 481 break; 482 case DISPLAY_METRIC_DPI: 483 { 484 NSSize disp_pixel_size = [[desc objectForKey:NSDeviceSize] sizeValue]; 485 float dispwidth = disp_pixel_size.width; 486 float scale = cocoa_screen_get_backing_scale_factor(); 487 float dpi = (dispwidth / physical_width) * 25.4f * scale; 488 *value = dpi; 489 } 490 break; 491 case DISPLAY_METRIC_NONE: 492 default: 493 *value = 0; 494 return false; 495 } 496 497 return true; 498} 499#else 500bool cocoa_get_metrics( 501 void *data, enum display_metric_types type, 502 float *value) 503{ 504 RAScreen *screen = (BRIDGE RAScreen*)cocoa_screen_get_chosen(); 505 float scale = cocoa_screen_get_native_scale(); 506 CGRect screen_rect = [screen bounds]; 507 float physical_width = screen_rect.size.width * scale; 508 float physical_height = screen_rect.size.height * scale; 509 float dpi = 160 * scale; 510 NSInteger idiom_type = UI_USER_INTERFACE_IDIOM(); 511 512 switch (idiom_type) 513 { 514 case -1: /* UIUserInterfaceIdiomUnspecified */ 515 /* TODO */ 516 break; 517 case UIUserInterfaceIdiomPad: 518 dpi = 132 * scale; 519 break; 520 case UIUserInterfaceIdiomPhone: 521 { 522 CGFloat maxSize = fmaxf(physical_width, physical_height); 523 /* Larger iPhones: iPhone Plus, X, XR, XS, XS Max, 11, 11 Pro Max */ 524 if (maxSize >= 2208.0) 525 dpi = 81 * scale; 526 else 527 dpi = 163 * scale; 528 } 529 break; 530 case UIUserInterfaceIdiomTV: 531 case UIUserInterfaceIdiomCarPlay: 532 /* TODO */ 533 break; 534 } 535 536 switch (type) 537 { 538 case DISPLAY_METRIC_MM_WIDTH: 539 *value = physical_width; 540 break; 541 case DISPLAY_METRIC_MM_HEIGHT: 542 *value = physical_height; 543 break; 544 case DISPLAY_METRIC_DPI: 545 *value = dpi; 546 break; 547 case DISPLAY_METRIC_NONE: 548 default: 549 *value = 0; 550 return false; 551 } 552 553 return true; 554} 555#endif 556 557#if defined(HAVE_COCOA_METAL) && !defined(HAVE_COCOATOUCH) 558@implementation WindowListener 559 560/* Similarly to SDL, we'll respond to key events 561 * by doing nothing so we don't beep. 562 */ 563- (void)flagsChanged:(NSEvent *)event { } 564- (void)keyDown:(NSEvent *)event { } 565- (void)keyUp:(NSEvent *)event { } 566 567@end 568#endif 569