No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Keyboard.mm 39KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105
  1. #include "Keyboard.h"
  2. #include "DisplayManager.h"
  3. #include "UnityAppController.h"
  4. #include "UnityForwardDecls.h"
  5. #include <string>
  6. #ifndef FILTER_EMOJIS_IOS_KEYBOARD
  7. #define FILTER_EMOJIS_IOS_KEYBOARD 0
  8. #endif
  9. static KeyboardDelegate* _keyboard = nil;
  10. static bool _shouldHideInput = false;
  11. static bool _shouldHideInputChanged = false;
  12. static const unsigned kToolBarHeight = 40;
  13. static const unsigned kSingleLineFontSize = 20;
  14. extern "C" void UnityKeyboard_StatusChanged(int status);
  15. extern "C" void UnityKeyboard_TextChanged(NSString* text);
  16. extern "C" void UnityKeyboard_LayoutChanged(NSString* layout);
  17. @implementation KeyboardDelegate
  18. {
  19. // UI handling
  20. // in case of single line we use UITextField inside UIToolbar
  21. // in case of multi-line input we use UITextView with UIToolbar as accessory view
  22. // tvOS does not support multiline input thus only UITextField option is implemented
  23. // tvOS does not support UIToolbar so we rely on tvOS default processing
  24. #if PLATFORM_IOS || PLATFORM_VISIONOS
  25. UITextView* textView;
  26. UIToolbar* viewToolbar;
  27. UIToolbar* fieldToolbar;
  28. // toolbar items are kept around to prevent releasing them
  29. UIBarButtonItem *multiLineDone, *multiLineCancel;
  30. UIBarButtonItem *singleLineDone, *singleLineCancel, *singleLineInputField;
  31. NSLayoutConstraint* widthConstraint;
  32. int singleLineSystemButtonsSpace;
  33. #endif
  34. UITextField* textField;
  35. // inputView is view used for actual input (it will be responder): UITextField [single-line] or UITextView [multi-line]
  36. // editView is the "root" view for keyboard: UIToolbar [single-line] or UITextView [multi-line]
  37. UIView* inputView;
  38. UIView* editView;
  39. KeyboardShowParam cachedKeyboardParam;
  40. CGRect _area;
  41. NSString* initialText;
  42. UIKeyboardType keyboardType;
  43. BOOL _multiline;
  44. BOOL _inputHidden;
  45. BOOL _active;
  46. KeyboardStatus _status;
  47. int _characterLimit;
  48. // not pretty but seems like easiest way to keep "we are rotating" status
  49. BOOL _rotating;
  50. NSRange _hiddenSelection;
  51. NSRange _selectionRequest;
  52. // used for < iOS 14 external keyboard
  53. CGFloat _heightOfKeyboard;
  54. }
  55. @synthesize area;
  56. @synthesize active = _active;
  57. @synthesize status = _status;
  58. @synthesize text;
  59. @synthesize selection;
  60. @synthesize hasUsedDictation;
  61. - (void)setPendingSelectionRequest
  62. {
  63. if (_selectionRequest.location != NSNotFound)
  64. {
  65. _keyboard.selection = _selectionRequest;
  66. _selectionRequest.location = NSNotFound;
  67. }
  68. }
  69. - (BOOL)textFieldShouldReturn:(UITextField*)textFieldObj
  70. {
  71. [self textInputDone: nil];
  72. return YES;
  73. }
  74. - (void)textInputDone:(id)sender
  75. {
  76. if (_status == Visible)
  77. {
  78. _status = Done;
  79. UnityKeyboard_StatusChanged(_status);
  80. }
  81. [self hide];
  82. }
  83. - (void)becomeFirstResponder
  84. {
  85. if (_status == Visible)
  86. {
  87. [_keyboard->inputView becomeFirstResponder];
  88. }
  89. }
  90. - (void)textInputCancel:(id)sender
  91. {
  92. _status = Canceled;
  93. UnityKeyboard_StatusChanged(_status);
  94. [self hide];
  95. }
  96. - (void)textInputLostFocus
  97. {
  98. if (_status == Visible)
  99. {
  100. _status = LostFocus;
  101. UnityKeyboard_StatusChanged(_status);
  102. }
  103. [self hide];
  104. }
  105. - (void)textViewDidChange:(UITextView *)textView
  106. {
  107. UnityKeyboard_TextChanged(textView.text);
  108. }
  109. - (void)textFieldDidChange:(UITextField*)textField
  110. {
  111. UnityKeyboard_TextChanged(textField.text);
  112. }
  113. - (BOOL)textViewShouldBeginEditing:(UITextView*)view
  114. {
  115. #if !PLATFORM_TVOS && !PLATFORM_VISIONOS
  116. view.inputAccessoryView = viewToolbar;
  117. #endif
  118. return YES;
  119. }
  120. #if PLATFORM_IOS || PLATFORM_VISIONOS
  121. - (void)textInputModeDidChange:(NSNotification*)notification
  122. {
  123. [self setPendingSelectionRequest];
  124. // Apple reports back the primary language of the current keyboard text input mode using BCP 47 language code i.e "en-GB"
  125. // but this also (undocumented) will return "dictation" when using voice dictation and "emoji" when using the emoji keyboard.
  126. if ([_keyboard->inputView.textInputMode.primaryLanguage isEqualToString: @"dictation"])
  127. {
  128. hasUsedDictation = YES;
  129. }
  130. }
  131. - (void)keyboardWillShow:(NSNotification *)notification
  132. {
  133. if (notification.userInfo == nil || inputView == nil)
  134. return;
  135. [self setPendingSelectionRequest];
  136. CGRect srcRect = [[notification.userInfo objectForKey: UIKeyboardFrameEndUserInfoKey] CGRectValue];
  137. CGRect rect = [UnityGetGLView() convertRect: srcRect fromView: nil];
  138. [self positionInput: rect x: rect.origin.x y: rect.origin.y];
  139. }
  140. - (void)keyboardDidShow:(NSNotification*)notification
  141. {
  142. _active = YES;
  143. UnityKeyboard_LayoutChanged(textField.textInputMode.primaryLanguage);
  144. // We only need to do this in < iOS 14
  145. // Used in keyboardDidShow as keyboardWillShow might not have the height ready yet as it's not on screen and
  146. // we're only interested in the height when it's fully on screen.
  147. if (@available(iOS 14, tvOS 14, *)) {}
  148. else
  149. {
  150. CGRect srcRect = [[notification.userInfo objectForKey: UIKeyboardFrameEndUserInfoKey] CGRectValue];
  151. CGRect rect = [UnityGetGLView() convertRect: srcRect fromView: nil];
  152. _heightOfKeyboard = rect.size.height;
  153. }
  154. }
  155. - (void)keyboardWillHide:(NSNotification*)notification
  156. {
  157. if (_keyboard)
  158. {
  159. // Reset selection to avoid selection graphics staying on the screen
  160. if (_keyboard.selection.length > 0)
  161. {
  162. NSRange range = NSMakeRange(_keyboard.text.length, 0);
  163. _keyboard.selection = range;
  164. }
  165. }
  166. UnityKeyboard_LayoutChanged(nil);
  167. [self systemHideKeyboard];
  168. }
  169. - (void)keyboardDidHide:(NSNotification*)notification
  170. {
  171. // The audio engine starts and restarts by listening to AVAudioSessionInterruptionNotifications, However
  172. // Apple does *not* guarantee that the AVAudioSessionInterruptionTypeEnded will be sent, especially if
  173. // the app is in the foreground - This can happen when using the dictate function on the keyboard
  174. // so we send the notification ourselves to ensure the audio restarts.
  175. if (hasUsedDictation)
  176. {
  177. [[NSNotificationCenter defaultCenter] postNotificationName: AVAudioSessionInterruptionNotification
  178. object: [AVAudioSession sharedInstance]
  179. userInfo: @{AVAudioSessionInterruptionTypeKey: [NSNumber numberWithUnsignedInteger: AVAudioSessionInterruptionTypeEnded]}];
  180. }
  181. }
  182. - (void)keyboardDidChangeFrame:(NSNotification*)notification
  183. {
  184. _active = true;
  185. CGRect srcRect = [[notification.userInfo objectForKey: UIKeyboardFrameEndUserInfoKey] CGRectValue];
  186. CGRect rect = [UnityGetGLView() convertRect: srcRect fromView: nil];
  187. // there are several ways to hide keyboard:
  188. // one, using the hide button on the keyboard, will move it outside view
  189. // another, for ipad floating keyboard, will "minimize" it (making its height/width zero)
  190. // if input field is multiline we need to account for the toolbar height
  191. float expectedHeight = _multiline ? kToolBarHeight : 1e-6;
  192. if (rect.origin.y + expectedHeight >= [UnityGetGLView() bounds].size.height || rect.size.width < 1e-6)
  193. {
  194. [self systemHideKeyboard];
  195. }
  196. else
  197. {
  198. [self positionInput: rect x: rect.origin.x y: rect.origin.y];
  199. }
  200. }
  201. - (void)positionInput:(CGRect)kbRect x:(float)x y:(float)y
  202. {
  203. const float safeAreaInsetLeft = [UnityGetGLView() safeAreaInsets].left;
  204. const float safeAreaInsetRight = [UnityGetGLView() safeAreaInsets].right;
  205. if (_multiline)
  206. {
  207. // use smaller area for iphones and bigger one for ipads
  208. int height = UnityDeviceDPI() > 300 ? 75 : 100;
  209. editView.frame = CGRectMake(safeAreaInsetLeft, y - height, kbRect.size.width - safeAreaInsetLeft - safeAreaInsetRight, height);
  210. }
  211. else
  212. {
  213. editView.frame = CGRectMake(0, y - kToolBarHeight, kbRect.size.width, kToolBarHeight);
  214. // old constraint must be removed, changing value while constraint is active causes conflict when changing inputView.frame
  215. [inputView removeConstraint: widthConstraint];
  216. inputView.frame = CGRectMake(inputView.frame.origin.x,
  217. inputView.frame.origin.y,
  218. kbRect.size.width - safeAreaInsetLeft - safeAreaInsetRight - self->singleLineSystemButtonsSpace,
  219. inputView.frame.size.height);
  220. // required to avoid auto-resizing on iOS 11 in case if input text is too long
  221. widthConstraint.constant = inputView.frame.size.width;
  222. [inputView addConstraint: widthConstraint];
  223. }
  224. _area = CGRectMake(x, y, kbRect.size.width, kbRect.size.height);
  225. [self updateInputHidden];
  226. }
  227. #endif
  228. + (void)Initialize
  229. {
  230. NSAssert(_keyboard == nil, @"[KeyboardDelegate Initialize] called after creating keyboard");
  231. if (!_keyboard)
  232. _keyboard = [[KeyboardDelegate alloc] init];
  233. }
  234. + (KeyboardDelegate*)Instance
  235. {
  236. if (!_keyboard)
  237. _keyboard = [[KeyboardDelegate alloc] init];
  238. return _keyboard;
  239. }
  240. + (void)Destroy
  241. {
  242. _keyboard = nil;
  243. }
  244. #if PLATFORM_IOS || PLATFORM_VISIONOS
  245. - (UIToolbar*)createToolbarWithItems:(NSArray*)items
  246. {
  247. UIToolbar* toolbar = [[UIToolbar alloc] initWithFrame: CGRectMake(0, 840, 320, kToolBarHeight)];
  248. UnitySetViewTouchProcessing(toolbar, touchesIgnored);
  249. toolbar.hidden = NO;
  250. toolbar.items = items;
  251. return toolbar;
  252. }
  253. - (void)createToolbars
  254. {
  255. multiLineDone = [[UIBarButtonItem alloc] initWithBarButtonSystemItem: UIBarButtonSystemItemDone target: self action: @selector(textInputDone:)];
  256. multiLineCancel = [[UIBarButtonItem alloc] initWithBarButtonSystemItem: UIBarButtonSystemItemCancel target: self action: @selector(textInputCancel:)];
  257. viewToolbar = [self createToolbarWithItems: @[multiLineDone, multiLineCancel]];
  258. singleLineInputField = [[UIBarButtonItem alloc] initWithCustomView: textField];
  259. singleLineDone = [[UIBarButtonItem alloc] initWithBarButtonSystemItem: UIBarButtonSystemItemDone target: self action: @selector(textInputDone:)];
  260. singleLineCancel = [[UIBarButtonItem alloc] initWithBarButtonSystemItem: UIBarButtonSystemItemCancel target: self action: @selector(textInputCancel:)];
  261. fieldToolbar = [self createToolbarWithItems: @[singleLineInputField, singleLineDone, singleLineCancel]];
  262. // Gather round boys, let's hear the story of apple ingenious api.
  263. // Did you see UIBarButtonItem above? oh the marvel of design
  264. // Maybe you thought it will have some connection to UIView or something?
  265. // Yes, internally, in private members, hidden like dirty laundry in a room of a youngster
  266. // But, you may ask, why do we care? Oh, easy - sometimes you want to use non-english language
  267. // And in these languages, not good enough to be english, done/cancel items can have different sizes
  268. // And we insist on having input field size set because, yes, we cannot quite do a layout inside UIToolbar
  269. // [because there are no views we can actually touch, thanks for asking]
  270. // Obviously, localizing system strings is also well hidden, and what works now might stop working tomorrow
  271. // That's why we keep UIBarButtonSystemItemDone/UIBarButtonSystemItemCancel above
  272. // and try to translate "Done"/"Cancel" in a way that "should" work
  273. // if localization fails we will still have "some" values (coming from english)
  274. // and while this wont work with, say, asian languages - it should not regress the current behavior
  275. UIFont* font = [UIFont systemFontOfSize: kSingleLineFontSize];
  276. NSBundle* uikitBundle = [NSBundle bundleForClass: UIApplication.class];
  277. NSString* doneStr = [uikitBundle localizedStringForKey: @"Done" value: nil table: nil];
  278. NSString* cancelStr = [uikitBundle localizedStringForKey: @"Cancel" value: nil table: nil];
  279. // mind you, all of that is highly empirical.
  280. // we assume space between items to be 18 [both between buttons and on the sides]
  281. // we also assume that button width would be more or less the title width exactly (it should be quite close though)
  282. const int doneW = (int)[doneStr sizeWithAttributes: @{NSFontAttributeName: font}].width;
  283. const int cancelW = (int)[cancelStr sizeWithAttributes: @{NSFontAttributeName: font}].width;
  284. singleLineSystemButtonsSpace = doneW + cancelW + 3 * 18;
  285. }
  286. #endif
  287. - (id)init
  288. {
  289. NSAssert(_keyboard == nil, @"You can have only one instance of KeyboardDelegate");
  290. self = [super init];
  291. if (self)
  292. {
  293. #if PLATFORM_IOS || PLATFORM_VISIONOS
  294. textView = [[UITextView alloc] initWithFrame: CGRectMake(0, 840, 480, 30)];
  295. textView.delegate = self;
  296. textView.font = [UIFont systemFontOfSize: 18.0];
  297. textView.hidden = YES;
  298. // For some unknown reason, the `textView` has visual issues when
  299. // using Dark Mode (some parts of the view become transparent). See case 1367091.
  300. // However, setting alpha to a value different than 1 fixes the issue.
  301. textView.alpha = 0.99;
  302. #endif
  303. textField = [[UITextField alloc] initWithFrame: CGRectMake(0, 0, 120, 30)];
  304. textField.delegate = self;
  305. textField.borderStyle = UITextBorderStyleRoundedRect;
  306. textField.font = [UIFont systemFontOfSize: kSingleLineFontSize];
  307. textField.clearButtonMode = UITextFieldViewModeWhileEditing;
  308. #if PLATFORM_IOS || PLATFORM_VISIONOS
  309. widthConstraint = [NSLayoutConstraint constraintWithItem: textField attribute: NSLayoutAttributeWidth relatedBy: NSLayoutRelationEqual toItem: nil attribute: NSLayoutAttributeNotAnAttribute multiplier: 1.0 constant: textField.frame.size.width];
  310. [textField addConstraint: widthConstraint];
  311. #endif
  312. [textField addTarget: self action: @selector(textFieldDidChange:) forControlEvents: UIControlEventEditingChanged];
  313. #if PLATFORM_IOS || PLATFORM_VISIONOS
  314. [self createToolbars];
  315. #endif
  316. #if PLATFORM_IOS || PLATFORM_VISIONOS
  317. [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(keyboardWillShow:) name: UIKeyboardWillShowNotification object: nil];
  318. [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(keyboardDidShow:) name: UIKeyboardDidShowNotification object: nil];
  319. [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(keyboardWillHide:) name: UIKeyboardWillHideNotification object: nil];
  320. [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(keyboardDidHide:) name: UIKeyboardDidHideNotification object: nil];
  321. [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(keyboardDidChangeFrame:) name: UIKeyboardDidChangeFrameNotification object: nil];
  322. [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(textInputModeDidChange:) name: UITextInputCurrentInputModeDidChangeNotification object: nil];
  323. #endif
  324. [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(textInputDone:) name: UITextFieldTextDidEndEditingNotification object: nil];
  325. [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(textInputDone:) name: UITextViewTextDidEndEditingNotification object: nil];
  326. }
  327. return self;
  328. }
  329. - (void)setTextInputTraits:(id<UITextInputTraits>)traits
  330. withParam:(KeyboardShowParam)param
  331. {
  332. UITextAutocapitalizationType capitalization = [KeyboardDelegate capitalizationForKeyboardParam: param];
  333. if (!_inputHidden)
  334. traits.secureTextEntry = param.secure;
  335. if (param.secure)
  336. {
  337. traits.autocorrectionType = UITextAutocorrectionTypeNo;
  338. traits.spellCheckingType = UITextSpellCheckingTypeNo;
  339. traits.autocapitalizationType = UITextAutocapitalizationTypeNone;
  340. }
  341. else
  342. {
  343. traits.autocorrectionType = param.autocorrectionType;
  344. traits.spellCheckingType = param.spellcheckingType;
  345. traits.autocapitalizationType = capitalization;
  346. }
  347. traits.keyboardType = param.keyboardType;
  348. traits.keyboardAppearance = param.appearance;
  349. }
  350. + (UITextAutocapitalizationType)capitalizationForKeyboardParam:(KeyboardShowParam)param
  351. {
  352. if (param.secure)
  353. return UITextAutocapitalizationTypeNone;
  354. UITextAutocapitalizationType capitalization;
  355. switch (param.keyboardType)
  356. {
  357. case UIKeyboardTypeURL:
  358. case UIKeyboardTypeEmailAddress:
  359. case UIKeyboardTypeWebSearch:
  360. capitalization = UITextAutocapitalizationTypeNone;
  361. break;
  362. default:
  363. capitalization = UITextAutocapitalizationTypeSentences;
  364. }
  365. return capitalization;
  366. }
  367. - (void)setKeyboardParams:(KeyboardShowParam)param
  368. {
  369. [NSObject cancelPreviousPerformRequestsWithTarget: self];
  370. if (cachedKeyboardParam.multiline != param.multiline ||
  371. cachedKeyboardParam.secure != param.secure ||
  372. cachedKeyboardParam.keyboardType != param.keyboardType ||
  373. cachedKeyboardParam.autocorrectionType != param.autocorrectionType ||
  374. cachedKeyboardParam.appearance != param.appearance)
  375. {
  376. [self hideUIDelayed];
  377. }
  378. cachedKeyboardParam = param;
  379. if (_active)
  380. [self hide];
  381. initialText = param.text ? [[NSString alloc] initWithUTF8String: param.text] : @"";
  382. _characterLimit = param.characterLimit;
  383. #if PLATFORM_IOS || PLATFORM_VISIONOS
  384. _multiline = param.multiline;
  385. if (_multiline)
  386. {
  387. [self setTextInputTraits: textView withParam: param];
  388. }
  389. else
  390. {
  391. if (param.oneTimeCode)
  392. textField.textContentType = UITextContentTypeOneTimeCode;
  393. [self setTextInputTraits: textField withParam: param];
  394. textField.placeholder = [NSString stringWithUTF8String: param.placeholder];
  395. }
  396. inputView = _multiline ? textView : textField;
  397. editView = _multiline ? textView : fieldToolbar;
  398. // Initially hide input fields in case external keyboard is connected.
  399. // This is needed for certain cases where external keyboard is connected
  400. // and soft keyboard is reopened without closing it first.
  401. // If external keyboard does not exist, these values will be updated by keyboardWillShow
  402. editView.hidden = YES;
  403. viewToolbar.hidden = YES;
  404. inputView.hidden = YES;
  405. #else // PLATFORM_TVOS
  406. [self setTextInputTraits: textField withParam: param];
  407. textField.placeholder = [NSString stringWithUTF8String: param.placeholder];
  408. inputView = textField;
  409. editView = textField;
  410. #endif
  411. [self shouldHideInput: _shouldHideInput];
  412. [KeyboardDelegate Instance].text = initialText;
  413. _status = Visible;
  414. UnityKeyboard_StatusChanged(_status);
  415. _active = YES;
  416. _selectionRequest.location = NSNotFound;
  417. }
  418. // we need to show/hide keyboard to react to orientation too, so extract we extract UI fiddling
  419. - (void)showUI
  420. {
  421. // if we unhide everything now the input will be shown smaller then needed quickly (and resized later)
  422. // so unhide only when keyboard is actually shown (we will update it when reacting to ios notifications)
  423. [NSObject cancelPreviousPerformRequestsWithTarget: self];
  424. if (!inputView.isFirstResponder)
  425. {
  426. editView.hidden = YES;
  427. [UnityGetGLView() addSubview: editView];
  428. [inputView becomeFirstResponder];
  429. #if PLATFORM_TVOS
  430. // make keyboard usable via controller by allowing exit to home temporarily
  431. // val 3, as second lowest bit indicates a temporary disable
  432. if (UnityGetAppleTVRemoteAllowExitToMenu() == 0)
  433. UnitySetAppleTVRemoteAllowExitToMenu(3);
  434. #endif
  435. }
  436. // we need to reload input views when switching the keyboard type for already active keyboard
  437. // otherwise the changed traits may not be immediately applied
  438. [inputView reloadInputViews];
  439. }
  440. - (void)hideUI
  441. {
  442. [NSObject cancelPreviousPerformRequestsWithTarget: self];
  443. [self performSelector: @selector(hideUIDelayed) withObject: nil afterDelay: 0.05]; // to avoid unnecessary hiding
  444. }
  445. - (void)hideUIDelayed
  446. {
  447. [inputView resignFirstResponder];
  448. [editView removeFromSuperview];
  449. editView.hidden = YES;
  450. // Keyboard notifications are not supported on tvOS so keyboardWillHide: will never be called which would set _active to false.
  451. // To work around that limitation we will update _active from here.
  452. #if PLATFORM_TVOS
  453. BOOL wasActive = _active;
  454. _active = editView.isFirstResponder;
  455. // if closing, restore exit value to what it was (getter ignores temp value and returns what it is meant to be)
  456. if (!_active && wasActive)
  457. UnitySetAppleTVRemoteAllowExitToMenu(UnityGetAppleTVRemoteAllowExitToMenu());
  458. #endif
  459. }
  460. - (void)systemHideKeyboard
  461. {
  462. // when we are rotating os will bombard us with keyboardWillHide: and keyboardDidChangeFrame:
  463. // ignore all of them (we do it here only to simplify code: we call systemHideKeyboard only from these notification handlers)
  464. if (_rotating)
  465. return;
  466. _active = editView.isFirstResponder;
  467. editView.hidden = YES;
  468. #if PLATFORM_IOS || PLATFORM_VISIONOS
  469. viewToolbar.hidden = YES;
  470. #endif
  471. _area = CGRectMake(0, 0, 0, 0);
  472. }
  473. - (void)show
  474. {
  475. [self showUI];
  476. }
  477. - (void)hide
  478. {
  479. [self hideUI];
  480. }
  481. - (void)updateInputHidden
  482. {
  483. if (_shouldHideInputChanged)
  484. {
  485. [self shouldHideInput: _shouldHideInput];
  486. _shouldHideInputChanged = false;
  487. }
  488. textField.returnKeyType = _inputHidden ? UIReturnKeyDone : UIReturnKeyDefault;
  489. #if PLATFORM_IOS || PLATFORM_VISIONOS
  490. UIView* unityView = UnityGetGLView();
  491. NSMutableArray<UIAccessibilityElement*>* elements = unityView.accessibilityElements ? [unityView.accessibilityElements mutableCopy] : [NSMutableArray array];
  492. viewToolbar.hidden = !_multiline || _inputHidden ? YES : NO;
  493. [elements removeObject: (UIAccessibilityElement*)viewToolbar];
  494. if (!viewToolbar.hidden)
  495. {
  496. [elements addObject: (UIAccessibilityElement*)viewToolbar];
  497. }
  498. [elements removeObject: (UIAccessibilityElement*)fieldToolbar];
  499. editView.hidden = _inputHidden ? YES : NO;
  500. if (!_multiline && !editView.hidden)
  501. {
  502. [elements addObject: (UIAccessibilityElement*)fieldToolbar];
  503. }
  504. unityView.accessibilityElements = elements;
  505. #else
  506. editView.hidden = _inputHidden ? YES : NO;
  507. #endif
  508. inputView.hidden = _inputHidden ? YES : NO;
  509. [self setTextInputTraits: textField withParam: cachedKeyboardParam];
  510. }
  511. - (CGRect)queryArea
  512. {
  513. return editView.hidden ? _area : CGRectUnion(_area, editView.frame);
  514. }
  515. - (NSRange)querySelection
  516. {
  517. UIView<UITextInput>* textInput;
  518. #if PLATFORM_TVOS
  519. textInput = textField;
  520. #else
  521. textInput = _multiline ? textView : textField;
  522. #endif
  523. UITextPosition* beginning = textInput.beginningOfDocument;
  524. UITextRange* selectedRange = textInput.selectedTextRange;
  525. UITextPosition* selectionStart = selectedRange.start;
  526. UITextPosition* selectionEnd = selectedRange.end;
  527. const NSInteger location = [textInput offsetFromPosition: beginning toPosition: selectionStart];
  528. const NSInteger length = [textInput offsetFromPosition: selectionStart toPosition: selectionEnd];
  529. return NSMakeRange(location, length);
  530. }
  531. - (void)assignSelection:(NSRange)range
  532. {
  533. UIView<UITextInput>* textInput;
  534. #if PLATFORM_TVOS
  535. textInput = textField;
  536. #else
  537. textInput = _multiline ? textView : textField;
  538. #endif
  539. UITextPosition* begin = [textInput beginningOfDocument];
  540. UITextPosition* caret = [textInput positionFromPosition: begin offset: range.location];
  541. UITextPosition* select = [textInput positionFromPosition: caret offset: range.length];
  542. UITextRange* textRange = [textInput textRangeFromPosition: caret toPosition: select];
  543. [textInput setSelectedTextRange: textRange];
  544. if (_inputHidden)
  545. _hiddenSelection = range;
  546. _selectionRequest = range;
  547. }
  548. + (void)StartReorientation
  549. {
  550. if (_keyboard && _keyboard.active)
  551. _keyboard->_rotating = YES;
  552. }
  553. + (void)FinishReorientation
  554. {
  555. if (_keyboard)
  556. _keyboard->_rotating = NO;
  557. }
  558. - (NSString*)getText
  559. {
  560. if (_status == Canceled)
  561. return initialText;
  562. else
  563. {
  564. #if PLATFORM_TVOS
  565. return [textField text];
  566. #else
  567. return _multiline ? [textView text] : [textField text];
  568. #endif
  569. }
  570. }
  571. - (void)setText:(NSString*)newText
  572. {
  573. #if PLATFORM_IOS || PLATFORM_VISIONOS
  574. if (_multiline)
  575. textView.text = newText;
  576. else
  577. textField.text = newText;
  578. #else
  579. textField.text = newText;
  580. #endif
  581. // for hidden selection place cursor at the end when text changes
  582. _hiddenSelection.location = newText.length;
  583. _hiddenSelection.length = 0;
  584. }
  585. - (void)shouldHideInput:(BOOL)hide
  586. {
  587. if (hide)
  588. {
  589. switch (keyboardType)
  590. {
  591. case UIKeyboardTypeDefault: hide = YES; break;
  592. case UIKeyboardTypeASCIICapable: hide = YES; break;
  593. case UIKeyboardTypeNumbersAndPunctuation: hide = YES; break;
  594. case UIKeyboardTypeURL: hide = YES; break;
  595. case UIKeyboardTypeNumberPad: hide = NO; break;
  596. case UIKeyboardTypePhonePad: hide = NO; break;
  597. case UIKeyboardTypeNamePhonePad: hide = NO; break;
  598. case UIKeyboardTypeEmailAddress: hide = YES; break;
  599. case UIKeyboardTypeTwitter: hide = YES; break;
  600. case UIKeyboardTypeWebSearch: hide = YES; break;
  601. case UIKeyboardTypeDecimalPad: hide = NO; break;
  602. default: hide = NO; break;
  603. }
  604. }
  605. _inputHidden = hide;
  606. }
  607. - (BOOL)hasExternalKeyboard
  608. {
  609. // iOS 14 and above has a public API in the GameController framework. If this is missing then this will return false
  610. if (@available(iOS 14, tvOS 14, *))
  611. return [NSClassFromString(@"GCKeyboard") valueForKey: @"coalescedKeyboard"] != nil;
  612. else // The minimum height a software keyboard will be on iOS is 160, A bluetooth keyboard just uses a toolbar which will be smaller than this.
  613. return _heightOfKeyboard < 160.0f;
  614. }
  615. static bool StringContainsEmoji(NSString *string);
  616. - (BOOL)textField:(UITextField*)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString*)string_
  617. {
  618. BOOL stringContainsEmoji = NO;
  619. #if FILTER_EMOJIS_IOS_KEYBOARD
  620. stringContainsEmoji = StringContainsEmoji(string_);
  621. #endif
  622. if (range.length + range.location > textField.text.length)
  623. return NO;
  624. return [self currentText: textField.text shouldChangeInRange: range replacementText: string_] && !stringContainsEmoji;
  625. }
  626. - (BOOL)textView:(UITextView*)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString*)text_
  627. {
  628. BOOL stringContainsEmoji = NO;
  629. #if FILTER_EMOJIS_IOS_KEYBOARD
  630. stringContainsEmoji = StringContainsEmoji(text_);
  631. #endif
  632. if (range.length + range.location > textView.text.length)
  633. return NO;
  634. return [self currentText: textView.text shouldChangeInRange: range replacementText: text_] && !stringContainsEmoji;
  635. }
  636. - (BOOL)currentText:(NSString*)currentText shouldChangeInRange:(NSRange)range replacementText:(NSString*)text_
  637. {
  638. NSUInteger newLength = currentText.length + (text_.length - range.length);
  639. if (newLength > _characterLimit && _characterLimit != 0 && newLength >= currentText.length)
  640. {
  641. // If the user inserts any emoji that exceeds the character limit it should quickly reject it, else it'll crash. We need to check regardless of FILTER_EMOJIS_IOS_KEYBOARD status as sometimes this method gets called before we've filtered out an emoji.
  642. if (StringContainsEmoji(text_))
  643. return NO;
  644. NSString* newReplacementText = @"";
  645. if ((currentText.length - range.length) < _characterLimit)
  646. newReplacementText = [text_ substringWithRange: NSMakeRange(0, _characterLimit - (currentText.length - range.length))];
  647. NSString* newText = [currentText stringByReplacingCharactersInRange: range withString: newReplacementText];
  648. #if PLATFORM_IOS || PLATFORM_VISIONOS
  649. if (_multiline)
  650. [textView setText: newText];
  651. else
  652. [textField setText: newText];
  653. #else
  654. [textField setText: newText];
  655. #endif
  656. // If we're trying to exceed the max length of the field BUT the text can merge into
  657. // precomposed characters then we should allow the input.
  658. NSString* precomposedNewText = [currentText precomposedStringWithCompatibilityMapping];
  659. __block int count = 0;
  660. [precomposedNewText enumerateSubstringsInRange: NSMakeRange(0, [precomposedNewText length]) options: NSStringEnumerationByComposedCharacterSequences
  661. usingBlock: ^(NSString *inSubstring, NSRange inSubstringRange, NSRange inEnclosingRange, BOOL *outStop) {
  662. count++;
  663. }];
  664. // count of characters of precomposed string will equal the character limit
  665. // if there has been characters merged bringing us under the limit.
  666. return count <= _characterLimit;
  667. }
  668. else
  669. {
  670. if (_inputHidden && _hiddenSelection.length > 0)
  671. {
  672. NSString* newText = [currentText stringByReplacingCharactersInRange: _hiddenSelection withString: text_];
  673. #if PLATFORM_IOS || PLATFORM_VISIONOS
  674. if (_multiline)
  675. [textView setText: newText];
  676. else
  677. [textField setText: newText];
  678. #else
  679. [textField setText: newText];
  680. #endif
  681. _hiddenSelection.location = _hiddenSelection.location + text_.length;
  682. _hiddenSelection.length = 0;
  683. self.selection = _hiddenSelection;
  684. return NO;
  685. }
  686. _hiddenSelection.location = range.location + text_.length;
  687. _hiddenSelection.length = 0;
  688. return YES;
  689. }
  690. }
  691. @end
  692. //==============================================================================
  693. //
  694. // Unity Interface:
  695. extern "C" void UnityKeyboard_Create(unsigned keyboardType, int autocorrection, int multiline, int secure, int alert, const char* text, const char* placeholder, int characterLimit)
  696. {
  697. #if PLATFORM_TVOS
  698. // Not supported. The API for showing keyboard for editing multi-line text is not available on tvOS
  699. multiline = false;
  700. #endif
  701. static const UIKeyboardType keyboardTypes[] =
  702. {
  703. UIKeyboardTypeDefault,
  704. UIKeyboardTypeASCIICapable,
  705. UIKeyboardTypeNumbersAndPunctuation,
  706. UIKeyboardTypeURL,
  707. UIKeyboardTypeNumberPad,
  708. UIKeyboardTypePhonePad,
  709. UIKeyboardTypeNamePhonePad,
  710. UIKeyboardTypeEmailAddress,
  711. UIKeyboardTypeDefault, // Default is used in case Wii U specific NintendoNetworkAccount type is selected (indexed at 8 in UnityEngine.TouchScreenKeyboardType)
  712. UIKeyboardTypeTwitter,
  713. UIKeyboardTypeWebSearch,
  714. UIKeyboardTypeDecimalPad
  715. };
  716. // on iOS 15, QuickType bar was decoupled from autocorrection (so it still shows candidates)
  717. // for a principle of "the least surprise" we keep it coupled internally, so autocorrection == spellchecking
  718. // TODO: should we expose the control of it?
  719. static const UITextAutocorrectionType autocorrectionTypes[] =
  720. {
  721. UITextAutocorrectionTypeNo,
  722. UITextAutocorrectionTypeDefault,
  723. };
  724. static const UITextSpellCheckingType spellcheckingTypes[] =
  725. {
  726. UITextSpellCheckingTypeNo,
  727. UITextSpellCheckingTypeDefault,
  728. };
  729. static const UIKeyboardAppearance keyboardAppearances[] =
  730. {
  731. UIKeyboardAppearanceDefault,
  732. UIKeyboardAppearanceAlert,
  733. };
  734. // Note: TouchScreenKeyboard with value 12 is OneTimeCode and does not directly translate to a UIKeyboardType.
  735. // We show a number pad but change the content type so that codes can be autofilled when received in Messages.
  736. KeyboardShowParam param =
  737. {
  738. text, placeholder,
  739. keyboardTypes[keyboardType == 12 ? UIKeyboardTypeNumberPad : keyboardType],
  740. autocorrectionTypes[autocorrection],
  741. spellcheckingTypes[autocorrection],
  742. keyboardAppearances[alert],
  743. (BOOL)multiline, (BOOL)secure,
  744. characterLimit,
  745. keyboardType == 12
  746. };
  747. [[KeyboardDelegate Instance] setKeyboardParams: param];
  748. }
  749. extern "C" void UnityKeyboard_Show()
  750. {
  751. // do not send hide if didnt create keyboard
  752. // TODO: probably assert?
  753. if (!_keyboard)
  754. return;
  755. [[KeyboardDelegate Instance] show];
  756. }
  757. extern "C" void UnityKeyboard_Hide()
  758. {
  759. // do not send hide if didnt create keyboard
  760. // TODO: probably assert?
  761. if (!_keyboard)
  762. return;
  763. [[KeyboardDelegate Instance] textInputLostFocus];
  764. }
  765. extern "C" void UnityKeyboard_SetText(const char* text)
  766. {
  767. [KeyboardDelegate Instance].text = [NSString stringWithUTF8String: text];
  768. }
  769. extern "C" NSString* UnityKeyboard_GetText()
  770. {
  771. return [KeyboardDelegate Instance].text;
  772. }
  773. extern "C" int UnityKeyboard_IsActive()
  774. {
  775. return (_keyboard && _keyboard.active) ? 1 : 0;
  776. }
  777. extern "C" int UnityKeyboard_Status()
  778. {
  779. return _keyboard ? _keyboard.status : Canceled;
  780. }
  781. extern "C" void UnityKeyboard_SetInputHidden(int hidden)
  782. {
  783. _shouldHideInput = hidden;
  784. _shouldHideInputChanged = true;
  785. // update hidden status only if keyboard is on screen to avoid showing input view out of nowhere
  786. if (_keyboard && _keyboard.active)
  787. [_keyboard updateInputHidden];
  788. }
  789. extern "C" int UnityKeyboard_IsInputHidden()
  790. {
  791. return _shouldHideInput ? 1 : 0;
  792. }
  793. extern "C" void UnityKeyboard_GetRect(float* x, float* y, float* w, float* h)
  794. {
  795. CGRect area = _keyboard ? _keyboard.area : CGRectMake(0, 0, 0, 0);
  796. // convert to unity coord system
  797. float multX = (float)GetMainDisplaySurface()->targetW / UnityGetGLView().bounds.size.width;
  798. float multY = (float)GetMainDisplaySurface()->targetH / UnityGetGLView().bounds.size.height;
  799. *x = 0;
  800. *y = area.origin.y * multY;
  801. *w = area.size.width * multX;
  802. *h = area.size.height * multY;
  803. }
  804. extern "C" void UnityKeyboard_SetCharacterLimit(unsigned characterLimit)
  805. {
  806. [KeyboardDelegate Instance].characterLimit = characterLimit;
  807. }
  808. extern "C" int UnityKeyboard_CanGetSelection()
  809. {
  810. return (_keyboard) ? 1 : 0;
  811. }
  812. extern "C" void UnityKeyboard_GetSelection(int* location, int* length)
  813. {
  814. if (_keyboard)
  815. {
  816. NSRange selection = _keyboard.selection;
  817. *location = (int)selection.location;
  818. *length = (int)selection.length;
  819. }
  820. else
  821. {
  822. *location = 0;
  823. *length = 0;
  824. }
  825. }
  826. extern "C" int UnityKeyboard_CanSetSelection()
  827. {
  828. return (_keyboard) ? 1 : 0;
  829. }
  830. extern "C" void UnityKeyboard_SetSelection(int location, int length)
  831. {
  832. if (_keyboard)
  833. {
  834. _keyboard.selection = NSMakeRange(location, length);
  835. }
  836. }
  837. //==============================================================================
  838. //
  839. // Emoji Filtering: unicode magic
  840. static bool StringContainsEmoji(NSString *string)
  841. {
  842. __block BOOL returnValue = NO;
  843. [string enumerateSubstringsInRange: NSMakeRange(0, string.length)
  844. options: NSStringEnumerationByComposedCharacterSequences
  845. usingBlock:^(NSString* substring, NSRange substringRange, NSRange enclosingRange, BOOL* stop)
  846. {
  847. const unichar hs = [substring characterAtIndex: 0];
  848. const unichar ls = substring.length > 1 ? [substring characterAtIndex: 1] : 0;
  849. #define IS_IN(val, min, max) (((val) >= (min)) && ((val) <= (max)))
  850. if (IS_IN(hs, 0xD800, 0xDBFF))
  851. {
  852. if (substring.length > 1)
  853. {
  854. const int uc = ((hs - 0xD800) * 0x400) + (ls - 0xDC00) + 0x10000;
  855. // Musical: [U+1D000, U+1D24F]
  856. // Enclosed Alphanumeric Supplement: [U+1F100, U+1F1FF]
  857. // Enclosed Ideographic Supplement: [U+1F200, U+1F2FF]
  858. // Miscellaneous Symbols and Pictographs: [U+1F300, U+1F5FF]
  859. // Supplemental Symbols and Pictographs: [U+1F900, U+1F9FF]
  860. // Emoticons: [U+1F600, U+1F64F]
  861. // Transport and Map Symbols: [U+1F680, U+1F6FF]
  862. if (IS_IN(uc, 0x1D000, 0x1F9FF))
  863. returnValue = YES;
  864. }
  865. }
  866. else if (substring.length > 1 && ls == 0x20E3)
  867. {
  868. // emojis for numbers: number + modifier ls = U+20E3
  869. returnValue = YES;
  870. }
  871. else
  872. {
  873. if ( // Latin-1 Supplement
  874. hs == 0x00A9 || hs == 0x00AE
  875. // General Punctuation
  876. || hs == 0x203C || hs == 0x2049
  877. // Letterlike Symbols
  878. || hs == 0x2122 || hs == 0x2139
  879. // Arrows
  880. || IS_IN(hs, 0x2194, 0x2199) || IS_IN(hs, 0x21A9, 0x21AA)
  881. // Miscellaneous Technical
  882. || IS_IN(hs, 0x231A, 0x231B) || IS_IN(hs, 0x23E9, 0x23F3) || IS_IN(hs, 0x23F8, 0x23FA) || hs == 0x2328 || hs == 0x23CF
  883. // Geometric Shapes
  884. || IS_IN(hs, 0x25AA, 0x25AB) || IS_IN(hs, 0x25FB, 0x25FE) || hs == 0x25B6 || hs == 0x25C0
  885. // Miscellaneous Symbols
  886. || IS_IN(hs, 0x2600, 0x2604) || IS_IN(hs, 0x2614, 0x2615) || IS_IN(hs, 0x2622, 0x2623) || IS_IN(hs, 0x262E, 0x262F)
  887. || IS_IN(hs, 0x2638, 0x263A) || IS_IN(hs, 0x2648, 0x2653) || IS_IN(hs, 0x2665, 0x2666) || IS_IN(hs, 0x2692, 0x2694)
  888. || IS_IN(hs, 0x2696, 0x2697) || IS_IN(hs, 0x269B, 0x269C) || IS_IN(hs, 0x26A0, 0x26A1) || IS_IN(hs, 0x26AA, 0x26AB)
  889. || IS_IN(hs, 0x26B0, 0x26B1) || IS_IN(hs, 0x26BD, 0x26BE) || IS_IN(hs, 0x26C4, 0x26C5) || IS_IN(hs, 0x26CE, 0x26CF)
  890. || IS_IN(hs, 0x26D3, 0x26D4) || IS_IN(hs, 0x26D3, 0x26D4) || IS_IN(hs, 0x26E9, 0x26EA) || IS_IN(hs, 0x26F0, 0x26F5)
  891. || IS_IN(hs, 0x26F7, 0x26FA)
  892. || hs == 0x260E || hs == 0x2611 || hs == 0x2618 || hs == 0x261D || hs == 0x2620 || hs == 0x2626 || hs == 0x262A
  893. || hs == 0x2660 || hs == 0x2663 || hs == 0x2668 || hs == 0x267B || hs == 0x267F || hs == 0x2699 || hs == 0x26C8
  894. || hs == 0x26D1 || hs == 0x26FD
  895. // Dingbats
  896. || IS_IN(hs, 0x2708, 0x270D) || IS_IN(hs, 0x2733, 0x2734) || IS_IN(hs, 0x2753, 0x2755)
  897. || IS_IN(hs, 0x2763, 0x2764) || IS_IN(hs, 0x2795, 0x2797)
  898. || hs == 0x2702 || hs == 0x2705 || hs == 0x270F || hs == 0x2712 || hs == 0x2714 || hs == 0x2716 || hs == 0x271D
  899. || hs == 0x2721 || hs == 0x2728 || hs == 0x2744 || hs == 0x2747 || hs == 0x274C || hs == 0x274E || hs == 0x2757
  900. || hs == 0x27A1 || hs == 0x27B0 || hs == 0x27BF
  901. // CJK Symbols and Punctuation
  902. || hs == 0x3030 || hs == 0x303D
  903. // Enclosed CJK Letters and Months
  904. || hs == 0x3297 || hs == 0x3299
  905. // Supplemental Arrows-B
  906. || IS_IN(hs, 0x2934, 0x2935)
  907. // Miscellaneous Symbols and Arrows
  908. || IS_IN(hs, 0x2B05, 0x2B07) || IS_IN(hs, 0x2B1B, 0x2B1C) || hs == 0x2B50 || hs == 0x2B55
  909. )
  910. {
  911. returnValue = YES;
  912. }
  913. }
  914. #undef IS_IN
  915. }];
  916. return returnValue;
  917. }