Keyboard.mm 31 KB

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