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