Keyboard.mm 38 KB

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