diff options
author | Tor Lillqvist <tml@collabora.com> | 2020-08-24 12:03:40 +0300 |
---|---|---|
committer | Tor Lillqvist <tml@collabora.com> | 2020-08-25 13:41:20 +0200 |
commit | 2a8db1b5abe937698d2dc08c70c38d31512bb822 (patch) | |
tree | a702bc8c833470ca82abc28094ce0a00b6c0fa35 | |
parent | f6e7ec9ee137f5a3b5dc21d5e452ea3e78d11832 (diff) |
Use CollaboraOnlineWebViewKeyboardManager
For now, just copy its source files here. When/if I figure out what is
the appropriate way to package that framework for use in other
products (like the Collabora Office iOS app) I will use that instead.
Change-Id: If808f96b6a72c80e54dc84fce80a551503c96335
Reviewed-on: https://gerrit.libreoffice.org/c/online/+/101268
Tested-by: Jenkins
Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoffice@gmail.com>
Reviewed-by: Tor Lillqvist <tml@collabora.com>
4 files changed, 279 insertions, 0 deletions
diff --git a/ios/CollaboraOnlineWebViewKeyboardManager/CollaboraOnlineWebViewKeyboardManager.h b/ios/CollaboraOnlineWebViewKeyboardManager/CollaboraOnlineWebViewKeyboardManager.h new file mode 100644 index 000000000..c62f54c48 --- /dev/null +++ b/ios/CollaboraOnlineWebViewKeyboardManager/CollaboraOnlineWebViewKeyboardManager.h @@ -0,0 +1,28 @@ +// -*- Mode: ObjC; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import <WebKit/WebKit.h> + +@interface CollaboraOnlineWebViewKeyboardManager : NSObject + +/** + * @param webView The WKWebView that displays Collabora Online's loleaflet.html. Will not do + * anything useful for WKWebViews not displaying that. The loleaflet.html can be in an arbitrarily + * deeply nested iframe. + */ +- (nonnull CollaboraOnlineWebViewKeyboardManager *)initForWebView:(nonnull WKWebView *)webView; + +@end + +// vim:set shiftwidth=4 softtabstop=4 expandtab: diff --git a/ios/CollaboraOnlineWebViewKeyboardManager/CollaboraOnlineWebViewKeyboardManager.m b/ios/CollaboraOnlineWebViewKeyboardManager/CollaboraOnlineWebViewKeyboardManager.m new file mode 100644 index 000000000..0e7d3208f --- /dev/null +++ b/ios/CollaboraOnlineWebViewKeyboardManager/CollaboraOnlineWebViewKeyboardManager.m @@ -0,0 +1,240 @@ +// -*- Mode: ObjC; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import <CollaboraOnlineWebViewKeyboardManager/CollaboraOnlineWebViewKeyboardManager.h> + +@interface _COWVKMKeyInputControl : UITextView<UITextViewDelegate> { + WKWebView *webView; +} + +- (instancetype)initForWebView:(nonnull WKWebView *)webView; + +@end + +@implementation _COWVKMKeyInputControl + +- (instancetype)initForWebView:(nonnull WKWebView *)webView { + self = [super init]; + + self->webView = webView; + self.delegate = self; + + return self; +} + +- (void)postMessage:(NSString *)message { + + NSMutableString *js = [NSMutableString string]; + + [js appendString:@"" + "{" + " const message = "]; + + [js appendString:message]; + + // We check if window.COKbdMgrCallback is a function, and in that case call that directly. + // Otherwise we iterate over iframes and post a message that the event listener that we install + // will receive and handle, and recurse. + + [js appendString:@";" + " if (typeof window.COKbdMgrCallback === 'function') {" + " window.COKbdMgrCallback(message);" + " } else {" + " const iframes = document.getElementsByTagName('iframe');" + " for (let i = 0; i < iframes.length; i++) {" + " iframes[i].contentWindow.postMessage(message, '*');" + " };" + " }" + "}"]; + + [webView evaluateJavaScript:js + completionHandler:^(id _Nullable obj, NSError *_Nullable error) { + if (error) { + if (error.userInfo[@"WKJavaScriptExceptionMessage"]) + NSLog(@"Error when executing JavaScript: %@: %@", error.localizedDescription, error.userInfo[@"WKJavaScriptExceptionMessage"]); + else + NSLog(@"Error when executing JavaScript: %@", error.localizedDescription); + } + }]; +} + +- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { + NSLog(@"COKbdMgr: shouldChangeTextInRange({%lu, %lu}, '%@')", (unsigned long)range.location, (unsigned long)range.length, text); + NSLog(@"self.text is now length %lu '%@'", self.text.length, self.text); + + NSMutableString *quotedText = [NSMutableString string]; + + for (unsigned i = 0; i < text.length; i++) { + const unichar c = [text characterAtIndex:i]; + if (c == '\'' || c == '\\') { + [quotedText appendString:@"\\"]; + [quotedText appendFormat:@"%c", c]; + } else if (c < ' ' || c >= 0x7F) { + [quotedText appendFormat:@"\\u%04X", c]; + } else { + [quotedText appendFormat:@"%c", c]; + } + } + + NSMutableString *message = [NSMutableString string]; + + [message appendFormat:@"{id: 'COKbdMgr', command: 'replaceText', location: %lu, length: %lu, text: '", range.location, range.length]; + [message appendString:quotedText]; + [message appendString:@"'}"]; + + [self postMessage:message]; + + return YES; +} + +- (BOOL)canBecomeFirstResponder { + return YES; +} + +@synthesize hasText; + +@end + +@interface CollaboraOnlineWebViewKeyboardManager () <WKScriptMessageHandler> { + WKWebView *webView; + _COWVKMKeyInputControl *control; +} + +@end + +@implementation CollaboraOnlineWebViewKeyboardManager + +- (CollaboraOnlineWebViewKeyboardManager *)initForWebView:(nonnull WKWebView *)webView { + self->webView = webView; + + [webView.configuration.userContentController + addScriptMessageHandler:self + name:@"CollaboraOnlineWebViewKeyboardManager"]; + + NSString *script = @"window.addEventListener('message', function(event) {" + " if (event.data.id === 'COKbdMgr') {" + " if (typeof window.COKbdMgrCallback === 'function') {" + " window.COKbdMgrCallback(event.data);" + " } else {" + " const iframes = document.getElementsByTagName('iframe');" + " for (let i = 0; i < iframes.length; i++) {" + " iframes[i].contentWindow.postMessage(event.data, '*');" + " };" + " }" + " }" + "});"; + + WKUserScript *userScript = [[WKUserScript alloc] initWithSource:script + injectionTime:WKUserScriptInjectionTimeAtDocumentEnd + forMainFrameOnly:NO]; + + [webView.configuration.userContentController addUserScript:userScript]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(keyboardDidHide:) + name:UIKeyboardDidHideNotification + object:nil]; + + return self; +} + +- (void)displayKeyboardOfType:(NSString *)type withText:(NSString *)text at:(NSUInteger)location { + if (control == nil) { + control = [[_COWVKMKeyInputControl alloc] initForWebView:self->webView]; + if (type != nil) { + UIKeyboardType keyboardType = UIKeyboardTypeDefault; + if ([type caseInsensitiveCompare:@"default"] == NSOrderedSame) + ; + else if ([type caseInsensitiveCompare:@"asciicapable"] == NSOrderedSame) + keyboardType = UIKeyboardTypeASCIICapable; + else if ([type caseInsensitiveCompare:@"numbersandpunctuation"] == NSOrderedSame) + keyboardType = UIKeyboardTypeNumbersAndPunctuation; + else if ([type caseInsensitiveCompare:@"url"] == NSOrderedSame) + keyboardType = UIKeyboardTypeURL; + else if ([type caseInsensitiveCompare:@"numberpad"] == NSOrderedSame) + keyboardType = UIKeyboardTypeNumberPad; + else if ([type caseInsensitiveCompare:@"phonepad"] == NSOrderedSame) + keyboardType = UIKeyboardTypePhonePad; + else if ([type caseInsensitiveCompare:@"namephonepad"] == NSOrderedSame) + keyboardType = UIKeyboardTypeNamePhonePad; + else if ([type caseInsensitiveCompare:@"emailaddress"] == NSOrderedSame) + keyboardType = UIKeyboardTypeEmailAddress; + else if ([type caseInsensitiveCompare:@"decimalpad"] == NSOrderedSame) + keyboardType = UIKeyboardTypeDecimalPad; + else if ([type caseInsensitiveCompare:@"asciicapablenumberpad"] == NSOrderedSame) + keyboardType = UIKeyboardTypeASCIICapableNumberPad; + else if ([type caseInsensitiveCompare:@"alphabet"] == NSOrderedSame) + keyboardType = UIKeyboardTypeAlphabet; + else + NSLog(@"COKbdMgr: Unrecognized keyboard type %@", type); + if (keyboardType != UIKeyboardTypeDefault) + control.keyboardType = keyboardType; + } + // Don't auto-capitalize start of input as we have no idea about the context into which it + // will be added. + control.autocapitalizationType = UITextAutocapitalizationTypeNone; + + control.text = text; + control.selectedRange = NSMakeRange(location, 0); + + [self->webView addSubview:control]; + // NSLog(@"COKbdMgr: added _COWVKMKeyInputControl to webView"); + [control becomeFirstResponder]; + } +} + +- (void)hideKeyboard { + if (control != nil) { + [control removeFromSuperview]; + // NSLog(@"COKbdMgr: removed _COWVKMKeyInputControl from webView"); + control = nil; + } +} + +- (void)userContentController:(nonnull WKUserContentController *)userContentController + didReceiveScriptMessage:(nonnull WKScriptMessage *)message { + if (![message.name isEqualToString:@"CollaboraOnlineWebViewKeyboardManager"]) { + NSLog(@"Received unrecognized script message name: %@ %@", message.name, message.body); + return; + } + + if ([message.body isKindOfClass:[NSDictionary class]]) { + NSString *stringCommand = message.body[@"command"]; + if ([stringCommand isEqualToString:@"display"]) { + NSString *type = message.body[@"type"]; + NSString *text = message.body[@"text"]; + NSNumber *location = message.body[@"location"]; + [self displayKeyboardOfType:type withText:text at:(location != nil ? [location unsignedIntegerValue] : UINT_MAX)]; + } else if ([stringCommand isEqualToString:@"hide"]) { + [self hideKeyboard]; + } else if (stringCommand == nil) { + NSLog(@"No 'command' in %@", message.body); + } else { + NSLog(@"Received unrecognized command:%@", stringCommand); + } + } else { + NSLog(@"Received unrecognized message body of type %@: %@, should be a dictionary (JS object)", [message.body class], message.body); + } +} + +- (void)keyboardDidHide:(NSNotification *)notification { + if (control != nil) { + [control removeFromSuperview]; + control = nil; + } +} + +@end + +// vim:set shiftwidth=4 softtabstop=4 expandtab: diff --git a/ios/Mobile.xcodeproj/project.pbxproj b/ios/Mobile.xcodeproj/project.pbxproj index c2a5396d9..d020a0b95 100644 --- a/ios/Mobile.xcodeproj/project.pbxproj +++ b/ios/Mobile.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ BE00F8B5213ED543001CE2D4 /* libiconv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = BE00F8B4213ED543001CE2D4 /* libiconv.tbd */; }; BE00F8B7213ED573001CE2D4 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = BE00F8B6213ED573001CE2D4 /* libz.tbd */; }; BE18C7DE226DE09A001AD27E /* Branding in Resources */ = {isa = PBXBuildFile; fileRef = BE18C7DD226DE09A001AD27E /* Branding */; }; + BE2FB29E24F3B146006E18B1 /* CollaboraOnlineWebViewKeyboardManager.m in Sources */ = {isa = PBXBuildFile; fileRef = BE2FB29D24F3B146006E18B1 /* CollaboraOnlineWebViewKeyboardManager.m */; }; BE5EB5C1213FE29900E0826C /* Log.cpp in Sources */ = {isa = PBXBuildFile; fileRef = BE5EB5B9213FE29900E0826C /* Log.cpp */; }; BE5EB5C2213FE29900E0826C /* SpookyV2.cpp in Sources */ = {isa = PBXBuildFile; fileRef = BE5EB5BA213FE29900E0826C /* SpookyV2.cpp */; }; BE5EB5C3213FE29900E0826C /* Session.cpp in Sources */ = {isa = PBXBuildFile; fileRef = BE5EB5BB213FE29900E0826C /* Session.cpp */; }; @@ -106,6 +107,8 @@ BE28F896228CE04700C00C48 /* langselect.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = langselect.cxx; path = "../../ios-device/desktop/source/app/langselect.cxx"; sourceTree = "<group>"; }; BE28F897228CE04700C00C48 /* lockfile2.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = lockfile2.cxx; path = "../../ios-device/desktop/source/app/lockfile2.cxx"; sourceTree = "<group>"; }; BE28F898228CE04700C00C48 /* userinstall.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = userinstall.cxx; path = "../../ios-device/desktop/source/app/userinstall.cxx"; sourceTree = "<group>"; }; + BE2FB29C24F3B146006E18B1 /* CollaboraOnlineWebViewKeyboardManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CollaboraOnlineWebViewKeyboardManager.h; path = CollaboraOnlineWebViewKeyboardManager/CollaboraOnlineWebViewKeyboardManager.h; sourceTree = SOURCE_ROOT; }; + BE2FB29D24F3B146006E18B1 /* CollaboraOnlineWebViewKeyboardManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = CollaboraOnlineWebViewKeyboardManager.m; path = CollaboraOnlineWebViewKeyboardManager/CollaboraOnlineWebViewKeyboardManager.m; sourceTree = SOURCE_ROOT; }; BE34D10F218B66B600815297 /* docsh.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = docsh.cxx; path = "../../ios-device/sw/source/uibase/app/docsh.cxx"; sourceTree = "<group>"; }; BE34D110218B66B600815297 /* docstyle.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = docstyle.cxx; path = "../../ios-device/sw/source/uibase/app/docstyle.cxx"; sourceTree = "<group>"; }; BE34D111218B66B600815297 /* docshdrw.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = docshdrw.cxx; path = "../../ios-device/sw/source/uibase/app/docshdrw.cxx"; sourceTree = "<group>"; }; @@ -2433,6 +2436,8 @@ BE8D77292136762500AC58EA /* Mobile */ = { isa = PBXGroup; children = ( + BE2FB29C24F3B146006E18B1 /* CollaboraOnlineWebViewKeyboardManager.h */, + BE2FB29D24F3B146006E18B1 /* CollaboraOnlineWebViewKeyboardManager.m */, BE58E13021874A2E00249358 /* Mobile.entitlements */, BE5EB5D92140363100E0826C /* ios.mm */, BE00F8922139494E001CE2D4 /* Resources */, @@ -3416,6 +3421,7 @@ BE5EB5C7213FE29900E0826C /* Protocol.cpp in Sources */, BE8D772F2136762500AC58EA /* DocumentBrowserViewController.mm in Sources */, BE5EB5D0213FE2D000E0826C /* TileCache.cpp in Sources */, + BE2FB29E24F3B146006E18B1 /* CollaboraOnlineWebViewKeyboardManager.m in Sources */, BE5EB5C5213FE29900E0826C /* MessageQueue.cpp in Sources */, BE7228E22417BC9F000ADABD /* StringVector.cpp in Sources */, BE5EB5D621401E0F00E0826C /* Storage.cpp in Sources */, diff --git a/ios/Mobile/DocumentViewController.mm b/ios/Mobile/DocumentViewController.mm index 2467bbfbb..2a2d870c3 100644 --- a/ios/Mobile/DocumentViewController.mm +++ b/ios/Mobile/DocumentViewController.mm @@ -19,6 +19,7 @@ #import <sys/stat.h> #import "ios.h" +#import "CollaboraOnlineWebViewKeyboardManager.h" #import "FakeSocket.hpp" #import "LOOLWSD.hpp" #import "Log.hpp" @@ -31,6 +32,7 @@ @interface DocumentViewController() <WKNavigationDelegate, WKUIDelegate, WKScriptMessageHandler, UIScrollViewDelegate, UIDocumentPickerDelegate> { int closeNotificationPipeForForwardingThread[2]; NSURL *downloadAsTmpURL; + CollaboraOnlineWebViewKeyboardManager *keyboardManager; } @end @@ -100,6 +102,9 @@ static IMP standardImpOfInputAccessoryView = nil; // contents is handled fully in JavaScript, the WebView has no knowledge of that.) self.webView.scrollView.delegate = self; + keyboardManager = + [[CollaboraOnlineWebViewKeyboardManager alloc] initForWebView:self.webView]; + [self.view addSubview:self.webView]; self.webView.navigationDelegate = self; |