Keyboard.mm 34 KB

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