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_UIKIT
24
25#include "SDL_video.h"
26#include "SDL_assert.h"
27#include "SDL_hints.h"
28#include "../SDL_sysvideo.h"
29#include "../../events/SDL_events_c.h"
30
31#import "SDL_uikitviewcontroller.h"
32#import "SDL_uikitmessagebox.h"
33#include "SDL_uikitvideo.h"
34#include "SDL_uikitmodes.h"
35#include "SDL_uikitwindow.h"
36#include "SDL_uikitopengles.h"
37
38#if SDL_IPHONE_KEYBOARD
39#include "keyinfotable.h"
40#endif
41
42#if TARGET_OS_TV
43static void SDLCALL
44SDL_AppleTVControllerUIHintChanged(void *userdata, const char *name, const char *oldValue, const char *hint)
45{
46    @autoreleasepool {
47        SDL_uikitviewcontroller *viewcontroller = (__bridge SDL_uikitviewcontroller *) userdata;
48        viewcontroller.controllerUserInteractionEnabled = hint && (*hint != '0');
49    }
50}
51#endif
52
53#if !TARGET_OS_TV
54static void SDLCALL
55SDL_HideHomeIndicatorHintChanged(void *userdata, const char *name, const char *oldValue, const char *hint)
56{
57    @autoreleasepool {
58        SDL_uikitviewcontroller *viewcontroller = (__bridge SDL_uikitviewcontroller *) userdata;
59        viewcontroller.homeIndicatorHidden = (hint && *hint) ? SDL_atoi(hint) : -1;
60#pragma clang diagnostic push
61#pragma clang diagnostic ignored "-Wunguarded-availability-new"
62        if ([viewcontroller respondsToSelector:@selector(setNeedsUpdateOfHomeIndicatorAutoHidden)]) {
63            [viewcontroller setNeedsUpdateOfHomeIndicatorAutoHidden];
64            [viewcontroller setNeedsUpdateOfScreenEdgesDeferringSystemGestures];
65        }
66#pragma clang diagnostic pop
67    }
68}
69#endif
70
71@implementation SDL_uikitviewcontroller {
72    CADisplayLink *displayLink;
73    int animationInterval;
74    void (*animationCallback)(void*);
75    void *animationCallbackParam;
76
77#if SDL_IPHONE_KEYBOARD
78    UITextField *textField;
79    BOOL hardwareKeyboard;
80    BOOL showingKeyboard;
81    BOOL rotatingOrientation;
82    NSString *changeText;
83    NSString *obligateForBackspace;
84#endif
85}
86
87@synthesize window;
88
89- (instancetype)initWithSDLWindow:(SDL_Window *)_window
90{
91    if (self = [super initWithNibName:nil bundle:nil]) {
92        self.window = _window;
93
94#if SDL_IPHONE_KEYBOARD
95        [self initKeyboard];
96        hardwareKeyboard = NO;
97        showingKeyboard = NO;
98        rotatingOrientation = NO;
99#endif
100
101#if TARGET_OS_TV
102        SDL_AddHintCallback(SDL_HINT_APPLE_TV_CONTROLLER_UI_EVENTS,
103                            SDL_AppleTVControllerUIHintChanged,
104                            (__bridge void *) self);
105#endif
106
107#if !TARGET_OS_TV
108        SDL_AddHintCallback(SDL_HINT_IOS_HIDE_HOME_INDICATOR,
109                            SDL_HideHomeIndicatorHintChanged,
110                            (__bridge void *) self);
111#endif
112    }
113    return self;
114}
115
116- (void)dealloc
117{
118#if SDL_IPHONE_KEYBOARD
119    [self deinitKeyboard];
120#endif
121
122#if TARGET_OS_TV
123    SDL_DelHintCallback(SDL_HINT_APPLE_TV_CONTROLLER_UI_EVENTS,
124                        SDL_AppleTVControllerUIHintChanged,
125                        (__bridge void *) self);
126#endif
127
128#if !TARGET_OS_TV
129    SDL_DelHintCallback(SDL_HINT_IOS_HIDE_HOME_INDICATOR,
130                        SDL_HideHomeIndicatorHintChanged,
131                        (__bridge void *) self);
132#endif
133}
134
135- (void)setAnimationCallback:(int)interval
136                    callback:(void (*)(void*))callback
137               callbackParam:(void*)callbackParam
138{
139    [self stopAnimation];
140
141    animationInterval = interval;
142    animationCallback = callback;
143    animationCallbackParam = callbackParam;
144
145    if (animationCallback) {
146        [self startAnimation];
147    }
148}
149
150- (void)startAnimation
151{
152    displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(doLoop:)];
153
154#ifdef __IPHONE_10_3
155    SDL_WindowData *data = (__bridge SDL_WindowData *) window->driverdata;
156
157    if ([displayLink respondsToSelector:@selector(preferredFramesPerSecond)]
158        && data != nil && data.uiwindow != nil
159        && [data.uiwindow.screen respondsToSelector:@selector(maximumFramesPerSecond)]) {
160        displayLink.preferredFramesPerSecond = data.uiwindow.screen.maximumFramesPerSecond / animationInterval;
161    } else
162#endif
163    {
164#if __IPHONE_OS_VERSION_MIN_REQUIRED < 100300
165        [displayLink setFrameInterval:animationInterval];
166#endif
167    }
168
169    [displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
170}
171
172- (void)stopAnimation
173{
174    [displayLink invalidate];
175    displayLink = nil;
176}
177
178- (void)doLoop:(CADisplayLink*)sender
179{
180    /* Don't run the game loop while a messagebox is up */
181    if (!UIKit_ShowingMessageBox()) {
182        /* See the comment in the function definition. */
183#if SDL_VIDEO_OPENGL_ES || SDL_VIDEO_OPENGL_ES2
184        UIKit_GL_RestoreCurrentContext();
185#endif
186
187        animationCallback(animationCallbackParam);
188    }
189}
190
191- (void)loadView
192{
193    /* Do nothing. */
194}
195
196- (void)viewDidLayoutSubviews
197{
198    const CGSize size = self.view.bounds.size;
199    int w = (int) size.width;
200    int h = (int) size.height;
201
202    SDL_SendWindowEvent(window, SDL_WINDOWEVENT_RESIZED, w, h);
203}
204
205#if !TARGET_OS_TV
206- (NSUInteger)supportedInterfaceOrientations
207{
208    return UIKit_GetSupportedOrientations(window);
209}
210
211#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_7_0
212- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)orient
213{
214    return ([self supportedInterfaceOrientations] & (1 << orient)) != 0;
215}
216#endif
217
218- (BOOL)prefersStatusBarHidden
219{
220    BOOL hidden = (window->flags & (SDL_WINDOW_FULLSCREEN|SDL_WINDOW_BORDERLESS)) != 0;
221    return hidden;
222}
223
224- (BOOL)prefersHomeIndicatorAutoHidden
225{
226    BOOL hidden = NO;
227    if (self.homeIndicatorHidden == 1) {
228        hidden = YES;
229    }
230    return hidden;
231}
232
233- (UIRectEdge)preferredScreenEdgesDeferringSystemGestures
234{
235    if (self.homeIndicatorHidden >= 0) {
236        if (self.homeIndicatorHidden == 2) {
237            return UIRectEdgeAll;
238        } else {
239            return UIRectEdgeNone;
240        }
241    }
242
243    /* By default, fullscreen and borderless windows get all screen gestures */
244    if ((window->flags & (SDL_WINDOW_FULLSCREEN|SDL_WINDOW_BORDERLESS)) != 0) {
245        return UIRectEdgeAll;
246    } else {
247        return UIRectEdgeNone;
248    }
249}
250#endif
251
252/*
253 ---- Keyboard related functionality below this line ----
254 */
255#if SDL_IPHONE_KEYBOARD
256
257@synthesize textInputRect;
258@synthesize keyboardHeight;
259@synthesize keyboardVisible;
260
261/* Set ourselves up as a UITextFieldDelegate */
262- (void)initKeyboard
263{
264    changeText = nil;
265    obligateForBackspace = @"                                                                "; /* 64 space */
266    textField = [[UITextField alloc] initWithFrame:CGRectZero];
267    textField.delegate = self;
268    /* placeholder so there is something to delete! */
269    textField.text = obligateForBackspace;
270
271    /* set UITextInputTrait properties, mostly to defaults */
272    textField.autocapitalizationType = UITextAutocapitalizationTypeNone;
273    textField.autocorrectionType = UITextAutocorrectionTypeNo;
274    textField.enablesReturnKeyAutomatically = NO;
275    textField.keyboardAppearance = UIKeyboardAppearanceDefault;
276    textField.keyboardType = UIKeyboardTypeDefault;
277    textField.returnKeyType = UIReturnKeyDefault;
278    textField.secureTextEntry = NO;
279
280    textField.hidden = YES;
281    keyboardVisible = NO;
282
283    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
284#if !TARGET_OS_TV
285    [center addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
286    [center addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
287#endif
288    [center addObserver:self selector:@selector(textFieldTextDidChange:) name:UITextFieldTextDidChangeNotification object:nil];
289}
290
291- (NSArray *)keyCommands
292{
293    NSMutableArray *commands = [[NSMutableArray alloc] init];
294    [commands addObject:[UIKeyCommand keyCommandWithInput:UIKeyInputUpArrow modifierFlags:kNilOptions action:@selector(handleCommand:)]];
295    [commands addObject:[UIKeyCommand keyCommandWithInput:UIKeyInputDownArrow modifierFlags:kNilOptions action:@selector(handleCommand:)]];
296    [commands addObject:[UIKeyCommand keyCommandWithInput:UIKeyInputLeftArrow modifierFlags:kNilOptions action:@selector(handleCommand:)]];
297    [commands addObject:[UIKeyCommand keyCommandWithInput:UIKeyInputRightArrow modifierFlags:kNilOptions action:@selector(handleCommand:)]];
298    [commands addObject:[UIKeyCommand keyCommandWithInput:UIKeyInputEscape modifierFlags:kNilOptions action:@selector(handleCommand:)]];
299    return [NSArray arrayWithArray:commands];
300}
301
302- (void)handleCommand:(UIKeyCommand *)keyCommand
303{
304    SDL_Scancode scancode = SDL_SCANCODE_UNKNOWN;
305    NSString *input = keyCommand.input;
306
307    if (input == UIKeyInputUpArrow) {
308        scancode = SDL_SCANCODE_UP;
309    } else if (input == UIKeyInputDownArrow) {
310        scancode = SDL_SCANCODE_DOWN;
311    } else if (input == UIKeyInputLeftArrow) {
312        scancode = SDL_SCANCODE_LEFT;
313    } else if (input == UIKeyInputRightArrow) {
314        scancode = SDL_SCANCODE_RIGHT;
315    } else if (input == UIKeyInputEscape) {
316        scancode = SDL_SCANCODE_ESCAPE;
317    }
318
319    if (scancode != SDL_SCANCODE_UNKNOWN) {
320        SDL_SendKeyboardKeyAutoRelease(scancode);
321    }
322}
323
324- (void)setView:(UIView *)view
325{
326    [super setView:view];
327
328    [view addSubview:textField];
329
330    if (keyboardVisible) {
331        [self showKeyboard];
332    }
333}
334
335/* willRotateToInterfaceOrientation and didRotateFromInterfaceOrientation are deprecated in iOS 8+ in favor of viewWillTransitionToSize */
336#if TARGET_OS_TV || __IPHONE_OS_VERSION_MIN_REQUIRED >= 80000
337- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
338{
339    [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
340    rotatingOrientation = YES;
341    [coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {}
342                                 completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
343        self->rotatingOrientation = NO;
344    }];
345}
346#else
347- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
348    [super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration];
349    rotatingOrientation = YES;
350}
351
352- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {
353    [super didRotateFromInterfaceOrientation:fromInterfaceOrientation];
354    rotatingOrientation = NO;
355}
356#endif /* TARGET_OS_TV || __IPHONE_OS_VERSION_MIN_REQUIRED >= 80000 */
357
358- (void)deinitKeyboard
359{
360    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
361#if !TARGET_OS_TV
362    [center removeObserver:self name:UIKeyboardWillShowNotification object:nil];
363    [center removeObserver:self name:UIKeyboardWillHideNotification object:nil];
364#endif
365    [center removeObserver:self name:UITextFieldTextDidChangeNotification object:nil];
366}
367
368/* reveal onscreen virtual keyboard */
369- (void)showKeyboard
370{
371    keyboardVisible = YES;
372    if (textField.window) {
373        showingKeyboard = YES;
374        [textField becomeFirstResponder];
375        showingKeyboard = NO;
376    }
377}
378
379/* hide onscreen virtual keyboard */
380- (void)hideKeyboard
381{
382    keyboardVisible = NO;
383    [textField resignFirstResponder];
384}
385
386- (void)keyboardWillShow:(NSNotification *)notification
387{
388#if !TARGET_OS_TV
389    CGRect kbrect = [[notification userInfo][UIKeyboardFrameEndUserInfoKey] CGRectValue];
390
391    /* The keyboard rect is in the coordinate space of the screen/window, but we
392     * want its height in the coordinate space of the view. */
393    kbrect = [self.view convertRect:kbrect fromView:nil];
394
395    [self setKeyboardHeight:(int)kbrect.size.height];
396#endif
397}
398
399- (void)keyboardWillHide:(NSNotification *)notification
400{
401    if (!showingKeyboard && !rotatingOrientation) {
402        SDL_StopTextInput();
403    }
404    [self setKeyboardHeight:0];
405}
406
407- (void)textFieldTextDidChange:(NSNotification *)notification
408{
409    if (changeText!=nil && textField.markedTextRange == nil)
410    {
411        NSUInteger len = changeText.length;
412        if (len > 0) {
413            if (!SDL_HardwareKeyboardKeyPressed()) {
414                /* Go through all the characters in the string we've been sent and
415                 * convert them to key presses */
416                int i;
417                for (i = 0; i < len; i++) {
418                    unichar c = [changeText characterAtIndex:i];
419                    SDL_Scancode code;
420                    Uint16 mod;
421
422                    if (c < 127) {
423                        /* Figure out the SDL_Scancode and SDL_keymod for this unichar */
424                        code = unicharToUIKeyInfoTable[c].code;
425                        mod  = unicharToUIKeyInfoTable[c].mod;
426                    } else {
427                        /* We only deal with ASCII right now */
428                        code = SDL_SCANCODE_UNKNOWN;
429                        mod = 0;
430                    }
431
432                    if (mod & KMOD_SHIFT) {
433                        /* If character uses shift, press shift down */
434                        SDL_SendKeyboardKey(SDL_PRESSED, SDL_SCANCODE_LSHIFT);
435                    }
436
437                    /* send a keydown and keyup even for the character */
438                    SDL_SendKeyboardKey(SDL_PRESSED, code);
439                    SDL_SendKeyboardKey(SDL_RELEASED, code);
440
441                    if (mod & KMOD_SHIFT) {
442                        /* If character uses shift, press shift back up */
443                        SDL_SendKeyboardKey(SDL_RELEASED, SDL_SCANCODE_LSHIFT);
444                    }
445                }
446            }
447            SDL_SendKeyboardText([changeText UTF8String]);
448        }
449        changeText = nil;
450    }
451}
452
453- (void)updateKeyboard
454{
455    CGAffineTransform t = self.view.transform;
456    CGPoint offset = CGPointMake(0.0, 0.0);
457    CGRect frame = UIKit_ComputeViewFrame(window, self.view.window.screen);
458
459    if (self.keyboardHeight) {
460        int rectbottom = self.textInputRect.y + self.textInputRect.h;
461        int keybottom = self.view.bounds.size.height - self.keyboardHeight;
462        if (keybottom < rectbottom) {
463            offset.y = keybottom - rectbottom;
464        }
465    }
466
467    /* Apply this view's transform (except any translation) to the offset, in
468     * order to orient it correctly relative to the frame's coordinate space. */
469    t.tx = 0.0;
470    t.ty = 0.0;
471    offset = CGPointApplyAffineTransform(offset, t);
472
473    /* Apply the updated offset to the view's frame. */
474    frame.origin.x += offset.x;
475    frame.origin.y += offset.y;
476
477    self.view.frame = frame;
478}
479
480- (void)setKeyboardHeight:(int)height
481{
482    keyboardVisible = height > 0;
483    keyboardHeight = height;
484    [self updateKeyboard];
485}
486
487/* UITextFieldDelegate method.  Invoked when user types something. */
488- (BOOL)textField:(UITextField *)_textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
489{
490    NSUInteger len = string.length;
491    if (len == 0) {
492        changeText = nil;
493        if (textField.markedTextRange == nil) {
494            /* it wants to replace text with nothing, ie a delete */
495            SDL_SendKeyboardKeyAutoRelease(SDL_SCANCODE_BACKSPACE);
496        }
497        if (textField.text.length < 16) {
498            textField.text = obligateForBackspace;
499        }
500    } else {
501        changeText = string;
502    }
503    return YES;
504}
505
506/* Terminates the editing session */
507- (BOOL)textFieldShouldReturn:(UITextField*)_textField
508{
509    SDL_SendKeyboardKeyAutoRelease(SDL_SCANCODE_RETURN);
510    if (keyboardVisible &&
511        SDL_GetHintBoolean(SDL_HINT_RETURN_KEY_HIDES_IME, SDL_FALSE)) {
512         SDL_StopTextInput();
513    }
514    return YES;
515}
516
517#endif
518
519@end
520
521/* iPhone keyboard addition functions */
522#if SDL_IPHONE_KEYBOARD
523
524static SDL_uikitviewcontroller *
525GetWindowViewController(SDL_Window * window)
526{
527    if (!window || !window->driverdata) {
528        SDL_SetError("Invalid window");
529        return nil;
530    }
531
532    SDL_WindowData *data = (__bridge SDL_WindowData *)window->driverdata;
533
534    return data.viewcontroller;
535}
536
537SDL_bool
538UIKit_HasScreenKeyboardSupport(_THIS)
539{
540    return SDL_TRUE;
541}
542
543void
544UIKit_ShowScreenKeyboard(_THIS, SDL_Window *window)
545{
546    @autoreleasepool {
547        SDL_uikitviewcontroller *vc = GetWindowViewController(window);
548        [vc showKeyboard];
549    }
550}
551
552void
553UIKit_HideScreenKeyboard(_THIS, SDL_Window *window)
554{
555    @autoreleasepool {
556        SDL_uikitviewcontroller *vc = GetWindowViewController(window);
557        [vc hideKeyboard];
558    }
559}
560
561SDL_bool
562UIKit_IsScreenKeyboardShown(_THIS, SDL_Window *window)
563{
564    @autoreleasepool {
565        SDL_uikitviewcontroller *vc = GetWindowViewController(window);
566        if (vc != nil) {
567            return vc.keyboardVisible;
568        }
569        return SDL_FALSE;
570    }
571}
572
573void
574UIKit_SetTextInputRect(_THIS, SDL_Rect *rect)
575{
576    if (!rect) {
577        SDL_InvalidParamError("rect");
578        return;
579    }
580
581    @autoreleasepool {
582        SDL_uikitviewcontroller *vc = GetWindowViewController(SDL_GetFocusWindow());
583        if (vc != nil) {
584            vc.textInputRect = *rect;
585
586            if (vc.keyboardVisible) {
587                [vc updateKeyboard];
588            }
589        }
590    }
591}
592
593
594#endif /* SDL_IPHONE_KEYBOARD */
595
596#endif /* SDL_VIDEO_DRIVER_UIKIT */
597
598/* vi: set ts=4 sw=4 expandtab: */
599