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