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