From 6de84cc35efa1ebcffefdf7739a884e086de09db Mon Sep 17 00:00:00 2001 From: Gabriel Wong Date: Fri, 6 Oct 2017 12:11:20 +0700 Subject: [PATCH] Migrate KMTextField to Swift --- .../KeymanEngine.xcodeproj/project.pbxproj | 12 +- .../KMEI/KeymanEngine/Classes/KMManager.h | 6 +- .../KMEI/KeymanEngine/Classes/KMManager.m | 5 +- .../KMEI/KeymanEngine/Classes/KMTextField.h | 34 -- .../KMEI/KeymanEngine/Classes/KMTextField.m | 335 ---------------- .../Classes/KMTextFieldDelegateProxy.m | 16 +- .../Classes/KeymanTextField.swift | 358 ++++++++++++++++++ ios/engine/KMEI/KeymanEngine/Keyman.h | 1 - .../KeymanEngineDemo/MainViewController.swift | 4 +- 9 files changed, 377 insertions(+), 394 deletions(-) delete mode 100644 ios/engine/KMEI/KeymanEngine/Classes/KMTextField.h delete mode 100644 ios/engine/KMEI/KeymanEngine/Classes/KMTextField.m create mode 100644 ios/engine/KMEI/KeymanEngine/Classes/KeymanTextField.swift diff --git a/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj b/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj index 0d01196acf..c1edd034a8 100644 --- a/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj +++ b/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj @@ -42,6 +42,7 @@ 98FFB3D41B67006F00289F0E /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 98FFB3D31B67006F00289F0E /* WebKit.framework */; }; 98FFB3D51B67008000289F0E /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 98FFB3D31B67006F00289F0E /* WebKit.framework */; }; C040E50E1F85FF8A00901EE4 /* KeyPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C040E50D1F85FF8A00901EE4 /* KeyPreviewView.swift */; }; + C040E5101F8606E300901EE4 /* KeymanTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = C040E50F1F8606E300901EE4 /* KeymanTextField.swift */; }; C040E5121F86107E00901EE4 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C040E5111F86107E00901EE4 /* AppDelegate.swift */; }; C040E5141F86108900901EE4 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C040E5131F86108900901EE4 /* MainViewController.swift */; }; C04514881F85D7F500D88416 /* KeyboardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C04514871F85D7F500D88416 /* KeyboardViewController.swift */; }; @@ -65,13 +66,11 @@ C06D37441F81F5C400F61AE0 /* KeyboardNameTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C08C62121F67C31100268D03 /* KeyboardNameTableViewCell.swift */; }; C06D37451F81F5C400F61AE0 /* KMManager.m in Sources */ = {isa = PBXBuildFile; fileRef = F273AB9815641EC500A47CEE /* KMManager.m */; }; C06D37461F81F5C400F61AE0 /* PopoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A5FF361F6682EB00BE740C /* PopoverView.swift */; }; - C06D37471F81F5C400F61AE0 /* KMTextField.m in Sources */ = {isa = PBXBuildFile; fileRef = F273AB9315641C3F00A47CEE /* KMTextField.m */; }; C06D37481F81F5C400F61AE0 /* KMTextFieldDelegateProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = F273ABA21564419700A47CEE /* KMTextFieldDelegateProxy.m */; }; C06D37491F81F5C400F61AE0 /* KMTextViewDelegateProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = 6CBD0DE1151EC30300D70779 /* KMTextViewDelegateProxy.m */; }; C06D374A1F81F5C400F61AE0 /* KMTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 6C0A1406151EA8B3007FA4AD /* KMTextView.m */; }; C06D374B1F81F5F400F61AE0 /* Keyman.h in Headers */ = {isa = PBXBuildFile; fileRef = F27FCB67157FFE0B00FBBA20 /* Keyman.h */; settings = {ATTRIBUTES = (Public, ); }; }; C06D374D1F81FFF900F61AE0 /* KMManager.h in Headers */ = {isa = PBXBuildFile; fileRef = F273AB9715641EC500A47CEE /* KMManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; - C06D374E1F81FFF900F61AE0 /* KMTextField.h in Headers */ = {isa = PBXBuildFile; fileRef = F273AB9215641C3F00A47CEE /* KMTextField.h */; settings = {ATTRIBUTES = (Public, ); }; }; C06D374F1F81FFF900F61AE0 /* KMTextFieldDelegateProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = F273ABA11564419700A47CEE /* KMTextFieldDelegateProxy.h */; settings = {ATTRIBUTES = (Public, ); }; }; C06D37521F82001100F61AE0 /* KMTextView.h in Headers */ = {isa = PBXBuildFile; fileRef = 6C0A1405151EA8B3007FA4AD /* KMTextView.h */; settings = {ATTRIBUTES = (Public, ); }; }; C06D37531F82001100F61AE0 /* KMTextViewDelegateProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = 6CBD0DE0151EC30300D70779 /* KMTextViewDelegateProxy.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -188,6 +187,7 @@ 98FFB3D31B67006F00289F0E /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; C034BBBD1F7CCE7D00021FF5 /* KeyboardMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardMenuView.swift; sourceTree = ""; }; C040E50D1F85FF8A00901EE4 /* KeyPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyPreviewView.swift; sourceTree = ""; }; + C040E50F1F8606E300901EE4 /* KeymanTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeymanTextField.swift; sourceTree = ""; }; C040E5111F86107E00901EE4 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; C040E5131F86108900901EE4 /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; C04514861F85D7F500D88416 /* SystemKeyboard-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SystemKeyboard-Bridging-Header.h"; sourceTree = ""; }; @@ -217,8 +217,6 @@ F26071FF158F8C6000F37184 /* libz.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libz.dylib; path = usr/lib/libz.dylib; sourceTree = SDKROOT; }; F2607201158F8C9600F37184 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; F2607203158F8CA200F37184 /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; }; - F273AB9215641C3F00A47CEE /* KMTextField.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = KMTextField.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; - F273AB9315641C3F00A47CEE /* KMTextField.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = KMTextField.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; F273AB9715641EC500A47CEE /* KMManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = KMManager.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; F273AB9815641EC500A47CEE /* KMManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = KMManager.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; F273AB9A15641F0E00A47CEE /* CWLSynthesizeSingleton.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CWLSynthesizeSingleton.h; sourceTree = ""; }; @@ -562,10 +560,9 @@ F273AB9E156440CD00A47CEE /* UITextField */ = { isa = PBXGroup; children = ( - F273AB9215641C3F00A47CEE /* KMTextField.h */, - F273AB9315641C3F00A47CEE /* KMTextField.m */, F273ABA11564419700A47CEE /* KMTextFieldDelegateProxy.h */, F273ABA21564419700A47CEE /* KMTextFieldDelegateProxy.m */, + C040E50F1F8606E300901EE4 /* KeymanTextField.swift */, ); name = UITextField; sourceTree = ""; @@ -633,7 +630,6 @@ C06D372F1F81F4E100F61AE0 /* KeymanEngine.h in Headers */, C06D374B1F81F5F400F61AE0 /* Keyman.h in Headers */, C06D374D1F81FFF900F61AE0 /* KMManager.h in Headers */, - C06D374E1F81FFF900F61AE0 /* KMTextField.h in Headers */, C06D374F1F81FFF900F61AE0 /* KMTextFieldDelegateProxy.h in Headers */, C06D37521F82001100F61AE0 /* KMTextView.h in Headers */, C06D37531F82001100F61AE0 /* KMTextViewDelegateProxy.h in Headers */, @@ -866,10 +862,10 @@ C06D37421F81F5C400F61AE0 /* KeyboardPickerButton.swift in Sources */, C06D37431F81F5C400F61AE0 /* KeyboardPickerBarButtonItem.swift in Sources */, C06D37441F81F5C400F61AE0 /* KeyboardNameTableViewCell.swift in Sources */, + C040E5101F8606E300901EE4 /* KeymanTextField.swift in Sources */, C06D37451F81F5C400F61AE0 /* KMManager.m in Sources */, C045148A1F85DF9100D88416 /* KeymanInputViewController.swift in Sources */, C06D37461F81F5C400F61AE0 /* PopoverView.swift in Sources */, - C06D37471F81F5C400F61AE0 /* KMTextField.m in Sources */, C06D37481F81F5C400F61AE0 /* KMTextFieldDelegateProxy.m in Sources */, C06D37491F81F5C400F61AE0 /* KMTextViewDelegateProxy.m in Sources */, C06D374A1F81F5C400F61AE0 /* KMTextView.m in Sources */, diff --git a/ios/engine/KMEI/KeymanEngine/Classes/KMManager.h b/ios/engine/KMEI/KeymanEngine/Classes/KMManager.h index 49213ad4a5..273890a6a0 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/KMManager.h +++ b/ios/engine/KMEI/KeymanEngine/Classes/KMManager.h @@ -6,7 +6,7 @@ // // // A one-stop hub for: -// - global settings and behaviour for KMTextView and KMTextField +// - global settings and behaviour for KMTextView and KeymanTextField // - displaying the keyboard picker/downloader // - all interaction with the Keyman servers (polling for info, downloading keyboards, etc) // @@ -29,7 +29,7 @@ // [[KMManager sharedInstance] setKeyboardWithID:kbID languageID:langID keyboardName:kbName languageName:langName font:kbFont, oskFont:nil]; // // -// NOTES: 1) The easiest way to use this SDK is to simply use KMTextViews and KMTextFields in place of their UIKit counterparts +// NOTES: 1) The easiest way to use this SDK is to simply use KMTextViews and KeymanTextFields in place of their UIKit counterparts // 2) All keyboard downloading and picking can be handled by globe key on the keyboard, displaying a KMKeyboardPickerButton or a // KMKeyboardPickerBarButtonItem // - when tapped, these will display a UI for the user to navigate all available Keyman keyboards @@ -191,7 +191,7 @@ typedef enum { // Displays a list of available keyboards and allows a user to add/download new keyboards or remove existing ones // - it is recommended that you pass in your current UIViewController rather than the navigation controller -// - to enable/disable the keyboard picker on the keyboard, see KMTextView/KMTextField class reference +// - to enable/disable the keyboard picker on the keyboard, see KMTextView/KeymanTextField class reference - (void)showKeyboardPickerInViewController:(UIViewController *)viewController shouldAddKeyboard:(BOOL)shouldAddKeyboard; - (void)dismissKeyboardPicker:(UIViewController *)viewController; diff --git a/ios/engine/KMEI/KeymanEngine/Classes/KMManager.m b/ios/engine/KMEI/KeymanEngine/Classes/KMManager.m index de23ce433c..a290ffe19f 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/KMManager.m +++ b/ios/engine/KMEI/KeymanEngine/Classes/KMManager.m @@ -10,7 +10,6 @@ #import "KMManager.h" #import "KMManager+Internal.h" #import "KMURLProtocol.h" -#import "KMTextField.h" #import "KMTextView.h" #import "UIView+Helpers.h" #import @@ -904,8 +903,8 @@ CWL_SYNTHESIZE_SINGLETON_FOR_CLASS_WITH_ACCESSOR(KMManager, sharedInstance); id firstResponder = [[[UIApplication sharedApplication] keyWindow] findFirstResponder]; if ([firstResponder isKindOfClass:[KMTextView class]]) [(KMTextView *)firstResponder dismissKeyboard]; - else if ([firstResponder isKindOfClass:[KMTextField class]]) - [(KMTextField *)firstResponder dismissKeyboard]; + else if ([firstResponder isKindOfClass:[KeymanTextField class]]) + [(KeymanTextField *)firstResponder dismissKeyboard]; } KeyboardPickerViewController *vc = [[KeyboardPickerViewController alloc] init]; diff --git a/ios/engine/KMEI/KeymanEngine/Classes/KMTextField.h b/ios/engine/KMEI/KeymanEngine/Classes/KMTextField.h deleted file mode 100644 index cd6ddb8266..0000000000 --- a/ios/engine/KMEI/KeymanEngine/Classes/KMTextField.h +++ /dev/null @@ -1,34 +0,0 @@ -// -// KMTextField.h -// Keyman Engine -// -// Copyright (c) 2017 SIL International. All rights reserved. -// -// KMTextField requires iOS 6.0 or greater due to use of the UITextInput protocol - -#import -#import "KMTextFieldDelegateProxy.h" - -@interface KMTextField : UITextField { - KMTextFieldDelegateProxy *delegateProxy_; - NSTimer *selectionPollingTimer_; - NSInteger lastSelectionPosition_; - NSInteger lastSelectionLength_; - UIViewController *viewController; // viewController should be set to main view controller to enable keyboard picker. - BOOL shouldSetCustomFontOnKeyboardChange_; // if YES, sets Keyman custom font (if any) to KMTextField instance on keyboard change event, defaults to YES. - BOOL isInputClickSoundEnabled_; // if YES, plays keyboard click sound whenever input received, defaults to YES. -} - -@property (nonatomic, strong) UIViewController *viewController; -@property (nonatomic, assign) BOOL shouldSetCustomFontOnKeyboardChange; -@property (nonatomic, assign) BOOL isInputClickSoundEnabled; - -// Use this KMTextFieldDelegate instead of the normal UITextFieldDelegate. -// - All of the normal UITextFieldDelegate methods are supported. -- (void)setKeymanDelegate:(id)keymanDelegate; - -// Dismisses the keyboard if this textview is the first responder. -// - Use this instead of [resignFirstResponder] as it also resigns the Keyman keyboard's responders. -- (void)dismissKeyboard; - -@end diff --git a/ios/engine/KMEI/KeymanEngine/Classes/KMTextField.m b/ios/engine/KMEI/KeymanEngine/Classes/KMTextField.m deleted file mode 100644 index 6d1dca801f..0000000000 --- a/ios/engine/KMEI/KeymanEngine/Classes/KMTextField.m +++ /dev/null @@ -1,335 +0,0 @@ -// -// KMTextField.m -// Keyman Engine -// -// Created by Patrick Weekes on 12-05-16. -// Updated by Serkan Kurt on 17/05/2013. -// Copyright (c) 2017 SIL International. All rights reserved. -// - -#import "KMTextField.h" -#import "KMManager+Internal.h" -#import - -@interface KMTextField () - @property (nonatomic, strong) KMTextFieldDelegateProxy *delegateProxy; - @property (nonatomic, strong) NSTimer *selectionPollingTimer; - @property (nonatomic, assign) NSInteger lastSelectionPosition; - @property (nonatomic, assign) NSInteger lastSelectionLength; - @property (nonatomic, assign) BOOL shouldUpdateKMText; -@end - -@implementation KMTextField - -@synthesize delegateProxy = delegateProxy_; -@synthesize selectionPollingTimer = selectionPollingTimer_; -@synthesize lastSelectionPosition = lastSelectionPosition_; -@synthesize lastSelectionLength = lastSelectionLength_; -@synthesize viewController; -@synthesize shouldSetCustomFontOnKeyboardChange; -@synthesize isInputClickSoundEnabled; -@synthesize shouldUpdateKMText; - -#pragma mark - Object Admin - -- (void)dealloc { - [[NSNotificationCenter defaultCenter] removeObserver:self]; - [self.selectionPollingTimer invalidate]; - self.delegate = nil; -} - -- (id)init { - return [self initWithFrame:CGRectZero]; -} - -- (id)initWithFrame:(CGRect)frame { - shouldSetCustomFontOnKeyboardChange = YES; - isInputClickSoundEnabled = YES; - if (self = [super initWithFrame:frame]) { - self.delegateProxy = [[KMTextFieldDelegateProxy alloc] init]; - self.delegate = self.delegateProxy; - - if ([self respondsToSelector:@selector(inputAssistantItem)]) { - UITextInputAssistantItem *inputAssistantItem = [self inputAssistantItem]; - inputAssistantItem.leadingBarButtonGroups = @[]; - inputAssistantItem.trailingBarButtonGroups = @[]; - } - - [KMManager sharedInstance]; // Preload webview keyboard - - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textfieldTextDidChange:) name:UITextFieldTextDidChangeNotification object:self]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardChanged:) name:kKeymanKeyboardChangedNotification object:nil]; - - self.lastSelectionPosition = 0; - self.lastSelectionLength = 0; - - NSString *minVersionString = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"MinimumOSVersion"]; - if ([minVersionString hasPrefix:@"4"] && [KMManager sharedInstance].debugPrintingOn) - [[KMManager sharedInstance] KMLog:@"KMTextField requires iOS 5.0 onwards. Try KMTextView instead or change your deployment target." checkDebugPrinting:NO]; - } - - return self; -} - -#pragma mark - Class Overrides - -- (UIView *)inputView { - [KMManager sharedInstance].webDelegate = self; - return [KMManager inputView]; -} - -// Prevent the delegate being set to anything but self -- (void)setDelegate:(id)delegate { - // Allow clearing of delegate - if (!delegate) { - [super setDelegate:nil]; - } else { - if (delegate != self.delegateProxy) - [[KMManager sharedInstance] KMLog:[NSString stringWithFormat:@"KMTextField: 0x%x Trying to set KMTextField's delegate directly. Use -[setKeymanDelegate:] instead.", (uint)self] checkDebugPrinting:YES]; - [super setDelegate:self.delegateProxy]; - } -} - -#pragma mark - Public Methods - -- (void)setKeymanDelegate:(id)keymanDelegate { - self.delegateProxy.keymanDelegate = keymanDelegate; - [[KMManager sharedInstance] KMLog:[NSString stringWithFormat:@"KMTextField: 0x%x keymanDelegate set to: %@", (uint)self, keymanDelegate] checkDebugPrinting:YES]; -} - -- (void)dismissKeyboard { - [[KMManager sharedInstance] KMLog:[NSString stringWithFormat:@"KMTextField: 0x%x Dismissing keyboard. Was first responder:%@", (uint)self, [self isFirstResponder]?@"YES":@"NO"] checkDebugPrinting:YES]; - - [self resignFirstResponder]; - [[KMManager inputView] endEditing:YES]; -} - -- (void)setText:(NSString *)text { - if (text == nil) - [super setText:@""]; - else - [super setText:text]; - - [KMManager setKMText:self.text]; - UITextRange *textRange = self.selectedTextRange; - NSRange newRange = NSMakeRange([self offsetFromPosition:[self beginningOfDocument] toPosition:textRange.start], [self offsetFromPosition:textRange.start toPosition:textRange.end]); - [KMManager setKMSelectionRange:newRange manually:NO]; -} - -#pragma mark - KMWebViewDelegate - -- (void)updatedFragment:(NSString *)fragment { - if (fragment && [fragment rangeOfString:@"insertText"].location != NSNotFound) { - if ([[KMManager sharedInstance] isSubKeysMenuVisible]) - return; - - if (isInputClickSoundEnabled) { - AudioServicesPlaySystemSound(0x450); - isInputClickSoundEnabled = NO; // Disable input click sound for 0.1 second to ensure it plays for single key stroke. - [self performSelector:@selector(enableInputClickSound) withObject:nil afterDelay:0.1f]; - } - - NSRange range1 = [fragment rangeOfString:@"+dn="]; - NSRange range2 = [fragment rangeOfString:@"+s="]; - range1.location += range1.length; - range1.length = range2.location - range1.location; - NSInteger dn = [[fragment substringWithRange:range1] integerValue]; - - range2.location += range2.length; - range2.length = [fragment length] - range2.location; - NSString *s = [fragment substringWithRange:range2]; - - NSMutableString *mText = nil; - NSArray *unicodes = nil; - if ([s length]) - unicodes = [s componentsSeparatedByString: @","]; - NSScanner *scanner = nil; - for(int i=0; i<[unicodes count]; i++) { - scanner = [NSScanner scannerWithString:[unicodes objectAtIndex:i]]; - unsigned int unicode; - [scanner scanHexInt:&unicode]; - if(mText == nil) - mText = [NSMutableString stringWithFormat:@"%C", (unsigned short) unicode]; - else - [mText appendString:[NSString stringWithFormat:@"%C", (unsigned short) unicode]]; - } - - if (mText == nil) - mText = [NSMutableString stringWithString:@""]; - NSString *text = [NSString stringWithString:mText]; - mText = nil; - - UITextRange *textRange = self.selectedTextRange; - NSRange selRange = NSMakeRange([self offsetFromPosition:[self beginningOfDocument] toPosition:textRange.start], [self offsetFromPosition:textRange.start toPosition:textRange.end]); - if (dn <= 0) { - if(selRange.length == 0) - [self insertText:text]; - else { - NSString *t = self.text; - NSString *r = [t stringByReplacingCharactersInRange:selRange withString:text]; - self.text = r; - } - } - else { - if (![s length]) { - for (int i=0;i"] checkDebugPrinting:YES]; -} - -- (BOOL)textFieldShouldEndEditing:(KMTextField *)textField { - if (textField == self) - [self resignFirstResponder]; - - return YES; -} - -#pragma mark - Keyman notifications - -- (void)keyboardChanged:(NSNotification *)notification { - if (!self.shouldSetCustomFontOnKeyboardChange) - return; - - NSDictionary *kbInfo = [[notification userInfo] objectForKey:kKeymanKeyboardInfoKey]; - NSString *keyboardID = [kbInfo objectForKey:kKeymanKeyboardIdKey]; - NSString *languageID = [kbInfo objectForKey:kKeymanLanguageIdKey]; - NSString *fontName = [[KMManager sharedInstance] fontNameForKeyboardWithID:keyboardID languageID:languageID]; - CGFloat fontSize = [self.font pointSize]; - if (fontName != nil) - [self setFont:[UIFont fontWithName:fontName size:fontSize]]; - else - [self setFont:[UIFont systemFontOfSize:fontSize]]; - - if ([self isFirstResponder]) { - [self resignFirstResponder]; - [self becomeFirstResponder]; - } - - [[KMManager sharedInstance] KMLog:[NSString stringWithFormat:@"TextField setFont: %@", [self font].familyName] checkDebugPrinting:YES]; -} - -#pragma mark - Private Methods - -- (void)enableInputClickSound { - isInputClickSoundEnabled = YES; -} - -@end diff --git a/ios/engine/KMEI/KeymanEngine/Classes/KMTextFieldDelegateProxy.m b/ios/engine/KMEI/KeymanEngine/Classes/KMTextFieldDelegateProxy.m index 16c3eed71f..9c66d9171d 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/KMTextFieldDelegateProxy.m +++ b/ios/engine/KMEI/KeymanEngine/Classes/KMTextFieldDelegateProxy.m @@ -7,7 +7,7 @@ // #import "KMTextFieldDelegateProxy.h" -#import "KMTextField.h" +#import "KeymanEngine/KeymanEngine-Swift.h" @implementation KMTextFieldDelegateProxy @@ -22,7 +22,7 @@ #pragma mark - UITextFieldDelegate // NOTE: the return value from the textField's hook is ignored -- (BOOL)textFieldShouldBeginEditing:(KMTextField *)textField { +- (BOOL)textFieldShouldBeginEditing:(KeymanTextField *)textField { if ([textField respondsToSelector:@selector(textFieldShouldBeginEditing:)]) { [textField textFieldShouldBeginEditing:textField]; } @@ -32,7 +32,7 @@ return YES; } -- (void)textFieldDidBeginEditing:(KMTextField *)textField { +- (void)textFieldDidBeginEditing:(KeymanTextField *)textField { if ([textField respondsToSelector:@selector(textFieldDidBeginEditing:)]) { [textField textFieldDidBeginEditing:textField]; } @@ -42,7 +42,7 @@ } // NOTE: the return value from the textField's hook is ignored -- (BOOL)textFieldShouldEndEditing:(KMTextField *)textField { +- (BOOL)textFieldShouldEndEditing:(KeymanTextField *)textField { if ([textField respondsToSelector:@selector(textFieldShouldEndEditing:)]) { [textField textFieldShouldEndEditing:textField]; } @@ -53,7 +53,7 @@ return YES; } -- (void)textFieldDidEndEditing:(KMTextField *)textField { +- (void)textFieldDidEndEditing:(KeymanTextField *)textField { if ([textField respondsToSelector:@selector(textFieldDidEndEditing:)]) { [textField textFieldDidEndEditing:textField]; } @@ -63,7 +63,7 @@ } // NOTE: the return value from the textField's hook is ignored -- (BOOL)textField:(KMTextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string { +- (BOOL)textField:(KeymanTextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string { if ([textField respondsToSelector:@selector(textField:shouldChangeCharactersInRange:replacementString:)]) { [textField textField:textField shouldChangeCharactersInRange:range replacementString:string]; } @@ -75,7 +75,7 @@ } // NOTE: the return value from the textField's hook is ignored -- (BOOL)textFieldShouldClear:(KMTextField *)textField { +- (BOOL)textFieldShouldClear:(KeymanTextField *)textField { if ([textField respondsToSelector:@selector(textFieldShouldClear:)]) { [textField textFieldShouldClear:textField]; } @@ -87,7 +87,7 @@ } // NOTE: the return value from the textField's hook is ignored -- (BOOL)textFieldShouldReturn:(KMTextField *)textField { +- (BOOL)textFieldShouldReturn:(KeymanTextField *)textField { if ([textField respondsToSelector:@selector(textFieldShouldReturn:)]) { [textField textFieldShouldReturn:textField]; } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/KeymanTextField.swift b/ios/engine/KMEI/KeymanEngine/Classes/KeymanTextField.swift new file mode 100644 index 0000000000..f3000f086a --- /dev/null +++ b/ios/engine/KMEI/KeymanEngine/Classes/KeymanTextField.swift @@ -0,0 +1,358 @@ +// +// KeymanTextField.swift +// KeymanEngine +// +// Created by Gabriel Wong on 2017-10-05. +// Copyright © 2017 SIL International. All rights reserved. +// + +import AudioToolbox +import UIKit + +public class KeymanTextField: UITextField, UITextFieldDelegate { + // viewController should be set to main view controller to enable keyboard picker. + public var viewController: UIViewController? + + // Sets Keyman custom font (if any) to KeymanTextField instance on keyboard change event + public var shouldSetCustomFontOnKeyboardChange = true + public var isInputClickSoundEnabled = true + + private var delegateProxy: KMTextFieldDelegateProxy? + private var shouldUpdateKMText = false + + // MARK: - Object Admin + deinit { + NotificationCenter.default.removeObserver(self) + delegate = nil + } + + public convenience init() { + self.init(frame: CGRect.zero) + } + + public override init(frame: CGRect) { + super.init(frame: frame) + + delegateProxy = KMTextFieldDelegateProxy() + delegate = delegateProxy + + if #available(iOS 9.0, *) { + inputAssistantItem.leadingBarButtonGroups = [] + inputAssistantItem.trailingBarButtonGroups = [] + } + + KMManager.sharedInstance() // Preload webview keyboard + + NotificationCenter.default.addObserver(self, selector: #selector(self.textFieldTextDidChange), + name: .UITextFieldTextDidChange, object: self) + NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardChanged), + name: NSNotification.Name.keymanKeyboardChanged, object: nil) + } + + public required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + + delegateProxy = KMTextFieldDelegateProxy(self) + delegate = delegateProxy + + if #available(iOS 9.0, *) { + inputAssistantItem.leadingBarButtonGroups = [] + inputAssistantItem.trailingBarButtonGroups = [] + } + + KMManager.sharedInstance() // Preload webview keyboard + + NotificationCenter.default.addObserver(self, selector: #selector(self.textFieldTextDidChange), + name: .UITextFieldTextDidChange, object: self) + NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardChanged), + name: NSNotification.Name.keymanKeyboardChanged, object: nil) + } + + // MARK: - Class Overrides + public override var inputView: UIView? { + get { + KMManager.sharedInstance().webDelegate = self + return KMManager.inputView() + } + + set(inputView) { + super.inputView = inputView + } + } + + // Prevent the delegate being set to anything but the delegateProxy + public override weak var delegate: UITextFieldDelegate? { + get { + return super.delegate + } + + set(delegate) { + // Allow clearing of delegate + guard let delegate = delegate else { + super.delegate = nil + return + } + + if delegate !== delegateProxy { + KMManager.sharedInstance().kmLog( + "Trying to set KeymanTextField's delegate directly. Use setKeymanDelegate() instead.", + checkDebugPrinting: true) + } + super.delegate = delegateProxy + } + } + + // MARK: - Public Methods + + // Use this KMTextFieldDelegate instead of the normal UITextFieldDelegate. + // All of the normal UITextFieldDelegate methods are supported. + @objc public func setKeymanDelegate(_ keymanDelegate: KMTextFieldDelegate?) { + delegateProxy?.keymanDelegate = keymanDelegate + KMManager.sharedInstance().kmLog( + "KeymanTextField: \(self.debugDescription) keymanDelegate set to: \(keymanDelegate.debugDescription)", + checkDebugPrinting: true) + } + + // Dismisses the keyboard if this textview is the first responder. + // - Use this instead of [resignFirstResponder] as it also resigns the Keyman keyboard's responders. + @objc public func dismissKeyboard() { + KMManager.sharedInstance().kmLog( + "KeymanTextField: \(self.debugDescription) Dismissing keyboard. Was first responder:\(isFirstResponder)", + checkDebugPrinting: true) + resignFirstResponder() + KMManager.inputView().endEditing(true) + } + + public override var text: String! { + get { + return super.text ?? "" + } + + set(text) { + if let text = text { + super.text = text + } else { + super.text = "" + } + + KMManager.setKMText(self.text) + let textRange = selectedTextRange! + let newRange = NSRange(location: offset(from: beginningOfDocument, to: textRange.start), + length: offset(from: textRange.start, to: textRange.end)) + KMManager.setKMSelectionRange(newRange, manually: false) + } + } + + // MARK: - KMWebViewDelegate + @objc public func updatedFragment(_ fragment: String) { + if fragment.contains("insertText") { + processInsertText(fragment) + } else if fragment.contains("hideKeyboard") { + dismissKeyboard() + } else if fragment.contains("menuKeyUp") { + if let viewController = viewController { + KMManager.sharedInstance().showKeyboardPicker(in: viewController, shouldAddKeyboard: false) + } else { + KMManager.sharedInstance().switchToNextKeyboard() + } + } + } + + private func processInsertText(_ fragment: String) { + if KMManager.sharedInstance().isSubKeysMenuVisible() { + return + } + + if isInputClickSoundEnabled { + AudioServicesPlaySystemSound(0x450) + + // Disable input click sound for 0.1 second to ensure it plays for single key stroke. + isInputClickSoundEnabled = false + perform(#selector(self.enableInputClickSound), with: nil, afterDelay: 0.1) + } + + // TODO: Refactor duplicate logic in KeymanInputViewController and KMTextView + let dnRange = fragment.range(of: "+dn=")! + let sRange = fragment.range(of: "+s=")! + + let dn = Int(fragment[dnRange.upperBound.. UInt16 in + let scanner = Scanner(string: hexString) + var codeUnit: UInt32 = 0 + scanner.scanHexInt32(&codeUnit) + return UInt16(codeUnit) + } + + let text = String(utf16CodeUnits: codeUnits, count: codeUnits.count) + + let textRange = selectedTextRange! + let selRange = NSRange(location: offset(from: beginningOfDocument, to: textRange.start), + length: offset(from: textRange.start, to: textRange.end)) + + if dn <= 0 { + if selRange.length == 0 { + insertText(text) + } else { + self.text = (self.text! as NSString).replacingCharacters(in: selRange, with: text) + } + } else { + if s.isEmpty { + for _ in 0.. Bool { + shouldUpdateKMText = true // Enable text update to catch copy/paste operations + return true + } + + @objc public func textFieldTextDidChange(_ notification: Notification) { + if shouldUpdateKMText { + // Catches copy/paste operations + KMManager.setKMText(text) + let textRange = selectedTextRange! + let newRange = NSRange(location: offset(from: beginningOfDocument, to: textRange.start), + length: offset(from: textRange.start, to: textRange.end)) + KMManager.setKMSelectionRange(newRange, manually: false) + shouldUpdateKMText = false + } + } + + public func textFieldShouldClear(_ textField: UITextField) -> Bool { + if textField == self { + KMManager.clearText() + } + return true + } + + public func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { + let leftToRightMark = "\u{200e}" + let rightToLeftOverride = "\u{202e}" + let keyboardID = KMManager.sharedInstance().keyboardID + let languageID = KMManager.sharedInstance().languageID + let textWD = baseWritingDirection(for: beginningOfDocument, in: .forward) + + if KMManager.sharedInstance().isRTLKeyboard(withID: keyboardID, languageID: languageID) { + if textWD != .rightToLeft { + if text.hasPrefix(leftToRightMark) { + text = String(text.utf16.dropFirst()) + } + + if text.isEmpty || !text.hasPrefix(rightToLeftOverride) { + text = "\(rightToLeftOverride)\(text!)" + } + makeTextWritingDirectionRightToLeft(nil) + } + } else { + if textWD != .leftToRight { + if text.hasPrefix(rightToLeftOverride) { + text = String(text.utf16.dropFirst()) + } + + if text.isEmpty || !text.hasPrefix(leftToRightMark) { + text = "\(leftToRightMark)\(text!)" + } + makeTextWritingDirectionLeftToRight(nil) + } + } + return true + } + + public func textFieldDidBeginEditing(_ textField: UITextField) { + KMManager.sharedInstance().webDelegate = self + + let keyboardID = KMManager.sharedInstance().keyboardID + let languageID = KMManager.sharedInstance().languageID + let fontName = KMManager.sharedInstance().fontNameForKeyboard(withID: keyboardID, languageID: languageID) + let fontSize = font?.pointSize ?? UIFont.systemFontSize + if let fontName = fontName { + font = UIFont(name: fontName, size: fontSize) + } else { + font = UIFont.systemFont(ofSize: fontSize) + } + + KMManager.sharedInstance().kmLog("TextField setFont: \(String(describing: font?.familyName))", + checkDebugPrinting: true) + + // copy this textField's text to the webview + KMManager.setKMText(text) + let textRange = selectedTextRange! + let newRange = NSRange(location: offset(from: beginningOfDocument, to: textRange.start), + length: offset(from: textRange.start, to: textRange.end)) + KMManager.setKMSelectionRange(newRange, manually: false) + KMManager.sharedInstance().kmLog( + "KeymanTextField: \(self.debugDescription) Became first responder. Value: \(String(describing: text))", + checkDebugPrinting: true) + } + + public func textFieldShouldEndEditing(_ textField: UITextField) -> Bool { + if textField == self { + resignFirstResponder() + } + + return true + } + + // MARK: - Keyman notifications + @objc public func keyboardChanged(_ notification: Notification) { + if !shouldSetCustomFontOnKeyboardChange { + return + } + + let kbInfo = notification.userInfo?[kKeymanKeyboardInfoKey] as? [AnyHashable: Any] + let keyboardID = kbInfo?[kKeymanKeyboardIdKey] as? String + let languageID = kbInfo?[kKeymanLanguageIdKey] as? String + let fontName = KMManager.sharedInstance().fontNameForKeyboard(withID: keyboardID, languageID: languageID) + let fontSize = font?.pointSize ?? UIFont.systemFontSize + if let fontName = fontName { + font = UIFont(name: fontName, size: fontSize) + } else { + font = UIFont.systemFont(ofSize: fontSize) + } + + if isFirstResponder { + resignFirstResponder() + becomeFirstResponder() + } + KMManager.sharedInstance().kmLog( + "KeymanTextField \(self.debugDescription) setFont: \(font!.familyName)", checkDebugPrinting: true) + } + + @objc public func enableInputClickSound() { + isInputClickSoundEnabled = true + } +} diff --git a/ios/engine/KMEI/KeymanEngine/Keyman.h b/ios/engine/KMEI/KeymanEngine/Keyman.h index e78ca7626d..7d3e0726a8 100644 --- a/ios/engine/KMEI/KeymanEngine/Keyman.h +++ b/ios/engine/KMEI/KeymanEngine/Keyman.h @@ -12,5 +12,4 @@ #import "KMTextView.h" #import "KMTextViewDelegateProxy.h" -#import "KMTextField.h" #import "KMTextFieldDelegateProxy.h" diff --git a/ios/engine/KMEI/KeymanEngineDemo/MainViewController.swift b/ios/engine/KMEI/KeymanEngineDemo/MainViewController.swift index ab5d7c3c34..fae5c040aa 100644 --- a/ios/engine/KMEI/KeymanEngineDemo/MainViewController.swift +++ b/ios/engine/KMEI/KeymanEngineDemo/MainViewController.swift @@ -12,7 +12,7 @@ import QuartzCore class MainViewController: UIViewController, UIAlertViewDelegate, KMTextViewDelegate, KMTextFieldDelegate { var textView1: KMTextView! - var textView2: KMTextField! + var textView2: KeymanTextField! var textView3: UITextView! deinit { @@ -90,7 +90,7 @@ class MainViewController: UIViewController, UIAlertViewDelegate, KMTextViewDeleg // Text Views/Fields textView1 = KMTextView() - textView2 = KMTextField() + textView2 = KeymanTextField() textView3 = UITextView() setTextViewStyle(textView1) setTextFieldStyle(textView2)