tdf#133284: Improve hardware and on-screen keyboard in the iOS app

This is a quite complicated change that should both fix tdf#133284
(cursor keys on a hardware keyboard do not work in a spreadsheet
document) and also improve the interaction with
CollaboraOnlineWebViewKeyboardManager that manages the on-screen
keyboard. We need to jump through complicated hoops in order to get
the hardware cursor keys handled right after loading a spreadsheet
document.

In the CollaboraOnlineWebViewKeyboardManager case we try harder to
keep loleaflet's _textArea buffer in sync with what the UITextView in
CollaboraOnlineWebViewKeyboardManager uses to provide suggestions
above the on-screen keyboard.

Also merges in related changes from today to
CollaboraOnlineWebViewKeyboardManager.

Change-Id: Ic4acb54bd4e815aa8bfb2bf40b08493446ae5ab0
Reviewed-on: https://gerrit.libreoffice.org/c/online/+/101878
Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoffice@gmail.com>
Reviewed-by: Tor Lillqvist <tml@collabora.com>
diff --git a/ios/CollaboraOnlineWebViewKeyboardManager/CollaboraOnlineWebViewKeyboardManager.m b/ios/CollaboraOnlineWebViewKeyboardManager/CollaboraOnlineWebViewKeyboardManager.m
index 561b726..fd1bcaf 100644
--- a/ios/CollaboraOnlineWebViewKeyboardManager/CollaboraOnlineWebViewKeyboardManager.m
+++ b/ios/CollaboraOnlineWebViewKeyboardManager/CollaboraOnlineWebViewKeyboardManager.m
@@ -79,6 +79,18 @@

    NSMutableString *quotedText = [NSMutableString string];

    int location = range.location;

    if (location < self.text.length && location + range.length == self.text.length) {
        // To guard against possible mismatch between our self.text and the _textArea.value in
        // TextInput.js, we indicate deletion or replacement from the end with negative location.
        location = location - self.text.length;
    }
    else if (range.location == 0 && range.length == 0 && text.length == 0) {
        // Backspace without anything known about preceding text
        location = -1;
    }

    for (unsigned i = 0; i < text.length; i++) {
        const unichar c = [text characterAtIndex:i];
        if (c == '\'' || c == '\\') {
@@ -93,7 +105,7 @@

    NSMutableString *message = [NSMutableString string];

    [message appendFormat:@"{id: 'COKbdMgr', command: 'replaceText', location: %lu, length: %lu, text: '", range.location, range.length];
    [message appendFormat:@"{id: 'COKbdMgr', command: 'replaceText', location: %d, length: %lu, text: '", location, range.length];
    [message appendString:quotedText];
    [message appendString:@"'}"];

@@ -209,15 +221,15 @@
        // will be added.
        control.autocapitalizationType = UITextAutocapitalizationTypeNone;

        control.text = text;
        control.selectedRange = NSMakeRange(location, 0);

        lastCommandIsHide = NO;

        [self->webView addSubview:control];
        NSLog(@"COKbdMgr: Added _COWVKMKeyInputControl to webView");
        [control becomeFirstResponder];
    }
    control.text = text;
    control.selectedRange = NSMakeRange(location, 0);

}

- (void)hideKeyboard {
@@ -253,6 +265,8 @@
            NSString *text = message.body[@"text"];
            NSNumber *location = message.body[@"location"];
            NSLog(@"COKbdMgr: command=display type=%@ text=%@ location=%@", type, text, location);
            if (text == nil)
                text = @"";
            [self displayKeyboardOfType:type withText:text at:(location != nil ? [location unsignedIntegerValue] : UINT_MAX)];
        } else if ([stringCommand isEqualToString:@"hide"]) {
            lastCommandIsHide = YES;
diff --git a/ios/Mobile/DocumentViewController.mm b/ios/Mobile/DocumentViewController.mm
index 2a2d870..5be21cd 100644
--- a/ios/Mobile/DocumentViewController.mm
+++ b/ios/Mobile/DocumentViewController.mm
@@ -102,8 +102,10 @@ 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];
    if (!isExternalKeyboardAttached()) {
        keyboardManager =
            [[CollaboraOnlineWebViewKeyboardManager alloc] initForWebView:self.webView];
    }

    [self.view addSubview:self.webView];

@@ -442,6 +444,27 @@ static IMP standardImpOfInputAccessoryView = nil;
                }];

            return;
        } else if ([message.body isEqualToString:@"FOCUSIFHWKBD"]) {
            if (isExternalKeyboardAttached()) {
                NSString *hwKeyboardMagic = @"{"
                    "    if (window.MagicToGetHWKeyboardWorking) {"
                    "        window.MagicToGetHWKeyboardWorking();"
                    "    }"
                    "}";
                [self.webView evaluateJavaScript:hwKeyboardMagic
                               completionHandler:^(id _Nullable obj, NSError * _Nullable error)
                     {
                         if (error) {
                             LOG_ERR("Error after " << [hwKeyboardMagic UTF8String] << ": " << [[error localizedDescription] UTF8String]);
                             NSString *jsException = error.userInfo[@"WKJavaScriptExceptionMessage"];
                             if (jsException != nil)
                                 LOG_ERR("JavaScript exception: " << [jsException UTF8String]);
                         }
                     }
                 ];
            }

            return;
        } else if ([message.body hasPrefix:@"HYPERLINK"]) {
            NSArray *messageBodyItems = [message.body componentsSeparatedByString:@" "];
            if ([messageBodyItems count] >= 2) {
diff --git a/loleaflet/html/loleaflet.html.m4 b/loleaflet/html/loleaflet.html.m4
index 8ec3439..31820d1 100644
--- a/loleaflet/html/loleaflet.html.m4
+++ b/loleaflet/html/loleaflet.html.m4
@@ -70,7 +70,20 @@ m4_ifelse(ANDROIDAPP,[true],
)

if (window.ThisIsTheiOSApp) {
  window.addEventListener("keydown", function(e) { e.preventDefault(); });
  window.addEventListener('keydown', function(e) {
    if (e.metaKey) {
      e.preventDefault();
    }
    if (window.MagicKeyDownHandler)
      window.MagicKeyDownHandler(e);
  });
  window.addEventListener('keyup', function(e) {
    if (e.metaKey) {
      e.preventDefault();
    }
    if (window.MagicKeyUpHandler)
      window.MagicKeyUpHandler(e);
  });
}

var Base64ToArrayBuffer = function(base64Str) {
diff --git a/loleaflet/src/layer/marker/TextInput.js b/loleaflet/src/layer/marker/TextInput.js
index bae93c2..7ce02fa 100644
--- a/loleaflet/src/layer/marker/TextInput.js
+++ b/loleaflet/src/layer/marker/TextInput.js
@@ -99,10 +99,28 @@ L.TextInput = L.Layer.extend({
			this._onFocusBlur({ type: 'focus' });
		}

		if (window.ThisIsTheiOSApp) {
			var that = this;
			window.MagicToGetHWKeyboardWorking = function() {
				var that2 = that;
				window.MagicKeyDownHandler = function(e) {
					that2._onKeyDown(e);
				};
				window.MagicKeyUpHandler = function(e) {
					that2._onKeyUp(e);
				};
			};
			window.postMobileMessage('FOCUSIFHWKBD');
		}

		L.DomEvent.on(this._map.getContainer(), 'mousedown touchstart', this._abortComposition, this);
	},

	onRemove: function() {
		window.MagicToGetHWKeyboardWorking = null;
		window.MagicKeyDownHandler = null;
		window.MagicKeyUpHandler = null;

		if (this._container) {
			this.getPane().removeChild(this._container);
		}
@@ -223,10 +241,16 @@ L.TextInput = L.Layer.extend({
						throw errorMessage;
					}

					if (that._textArea.value.length == 2 && message.length == 0 && message.text.length == 0) {
						that._removeTextContent(1, 0);
					} else {
						that._textArea.value = that._textArea.value.slice(0, message.location + 1) + message.text + that._textArea.value.slice(message.location + 1 + message.length);
					if (message.location < 0) {
						if (that._textArea.value.length > 2) {
							that._textArea.value = that._textArea.value.slice(0, message.location - 1) + that._textArea.value.slice(-1);
							that._onInput({});
						} else {
							that._removeTextContent(-message.location, 0);
						}
					}
					if (message.text.length > 0) {
						that._textArea.value = that._textArea.value.slice(0, -1) + message.text + that._textArea.value.slice(-1);
						that._onInput({});
					}
				} else {
@@ -236,8 +260,7 @@ L.TextInput = L.Layer.extend({
				}
			};

			// We don't know the seed text to feed CollaboraOnlineWebViewKeyboardManager
			window.webkit.messageHandlers.CollaboraOnlineWebViewKeyboardManager.postMessage({command: 'display'});
			window.webkit.messageHandlers.CollaboraOnlineWebViewKeyboardManager.postMessage({command: 'display', text: this._textArea.value.slice(1, -1)});
			this._onFocusBlur({type: 'focus'});

			return;