diff --git a/android/KMEA/app/src/main/assets/android-host.js b/android/KMEA/app/src/main/assets/android-host.js index 2e614fc8f7..d86c6d158c 100644 --- a/android/KMEA/app/src/main/assets/android-host.js +++ b/android/KMEA/app/src/main/assets/android-host.js @@ -1,3 +1,10 @@ +var _debug = 0; + +// Android harness attachment +if(window.parent && window.parent.jsInterface && !window.jsInterface) { + window.jsInterface = window.parent.jsInterface; +} + var device = window.jsInterface.getDeviceType(); var oskHeight = Math.ceil(window.jsInterface.getKeyboardHeight() / window.devicePixelRatio); var oskWidth = 0; @@ -19,7 +26,7 @@ function init() { //window.console.log('Device type = '+device); //window.console.log('Keyboard height = '+oskHeight); var kmw=com.keyman.singleton; - kmw.init({'app':device,'fonts':'packages/'}); + kmw.init({'app':device,'fonts':'packages/',root:'./'}); kmw['util']['setOption']('attachType','manual'); kmw['oninserttext'] = insertText; kmw['showKeyboardList'] = showMenu; @@ -48,6 +55,7 @@ function init() { } function notifyHost(event, params) { + console_debug('notifyHost(event='+event+',params='+params+')'); // TODO: Update all other host notifications to use notifyHost instead of directly setting window.location.hash window.setTimeout(function() { // We use a timeout so that the navigation doesn't cause the calling function to abort after the call @@ -142,6 +150,7 @@ function setSpacebarText(mode) { * @param dr Number of post-caret code points to delete. (optional) */ function insertText(dn, s, dr) { + console_debug('insertText(dn='+dn+',s='+s+',dr='+dr+')'); dr = dr || 0; // Sets a default value of zero when dr is undefined //window.console.log('insertText('+ dn +', ' + s +', ' + dr + ');'); window.jsInterface.insertText(dn, s, dr); @@ -189,29 +198,43 @@ function setNumericLayer() { } function updateKMText(text) { + var ta = document.getElementById('ta'); + console_debug('updateKMText(text='+text+') ta.value='+ta.value); + if(text == undefined) { text = ''; } - var ta = document.getElementById('ta'); - var kmw = window['keyman']; - var resetContext = ta.value != text; - ta.value = text; - kmw['setActiveElement'](ta); - if(resetContext) { - kmw.resetContext(); + if(ta.value != text) { + ta.value = text; + window.resetContext(); + } +} + +function console_debug(s) { + if(_debug) { + console.debug(s); } } function updateKMSelectionRange(start, end) { var ta = document.getElementById('ta'); - var kmw = window['keyman']; - var resetContext = (ta.selectionStart != start || ta.selectionEnd != end); - ta.selectionStart = ta._KeymanWebSelectionStart = start; - ta.selectionEnd = ta._KeymanWebSelectionEnd = end; - kmw['setActiveElement'](ta); - if(resetContext) { - kmw.resetContext(); + console_debug('updateKMSelectionRange('+start+','+end+'): ta.selectionStart='+ta.selectionStart+' '+ + '['+ta._KeymanWebSelectionStart+'] ta.selectionEnd='+ta.selectionEnd+' '+ta._KeymanWebSelectionEnd); + + var selDirection = 'forward'; + if(start > end) { + var e0 = end; + end = start; + start = e0; + selDirection = 'backward'; + } + + if(ta.selectionStart != start || ta.selectionEnd != end || ta.selectionDirection != selDirection) { + ta.selectionStart = ta._KeymanWebSelectionStart = start; + ta.selectionEnd = ta._KeymanWebSelectionEnd = end; + ta.selectionDirection = selDirection; + keyman.resetContext(); } } @@ -301,6 +324,7 @@ function executePopupKey(keyID, keyText) { } function executeHardwareKeystroke(code, shift, lstates, eventModifiers) { + console_debug('executeHardwareKeystroke(code='+code+',shift='+shift+',lstates='+lstates+',eventModifiers='+eventModifiers+')'); var kmw=window['keyman']; //window.console.log('executeHardwareKeystroke:('+code+', ' + shift + ', ' + lstates + ');'); try { @@ -342,7 +366,7 @@ function toHex(theString) { * Reference: Issue #5376 */ function checkTextArea() { - var uaRe = /Chrome\/([0-9]*)./g; + var uaRe = /Chrome\/([0-9]*)\./g; var chromeMajorVersion = uaRe.exec(navigator.userAgent); if (chromeMajorVersion && parseInt(chromeMajorVersion[1]) <= 37) { var ta = document.getElementById('ta'); diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMManager.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMManager.java index 54d470f47d..963c74d9a7 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMManager.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMManager.java @@ -2683,17 +2683,24 @@ public final class KMManager { start = temp; } - if (dn <= 0) { + int deleteLeft = dn; + + if(start != end && dn == 1 && s.length() == 0) { + /* Handle backspace with a selection: just delete selection */ + deleteLeft = 0; + } + + if (deleteLeft <= 0) { if (start == end) { if (s.length() > 0 && s.charAt(0) == '\n') { textView.keyDownUp(KeyEvent.KEYCODE_ENTER); - } else { - // *** TO DO: Try to find a solution to the bug on API < 17, insert overwrites on next line - if (s.length() > 0) { + } else if (s.length() > 0) { + // *** TO DO: Try to find a solution to the bug on API < 17, insert overwrites on next line InAppKeyboardShouldIgnoreTextChange = true; InAppKeyboardShouldIgnoreSelectionChange = true; textView.getText().insert(start, s); - } + } else { + textView.getText().delete(start, end); } } else { if (s.length() > 0 && s.charAt(0) == '\n') { @@ -2712,7 +2719,16 @@ public final class KMManager { } } } else { - for (int i = 0; i < dn; i++) { + if(start != end) { + // Delete the selection + InAppKeyboardShouldIgnoreTextChange = true; + InAppKeyboardShouldIgnoreSelectionChange = true; + textView.getText().delete(start, end); + textView.setSelection(start); + end = start; + deleteLeft = 0; + } + for (int i = 0; i < deleteLeft; i++) { CharSequence chars = textView.getText().subSequence(0, start); if (chars != null && chars.length() > 0) { char c = chars.charAt(start - 1); @@ -2736,6 +2752,8 @@ public final class KMManager { } } + // Collapse the selection + textView.setSelection(start + s.length()); textView.endBatchEdit(); } }); @@ -2789,6 +2807,7 @@ public final class KMManager { // This annotation is required in Jelly Bean and later: @JavascriptInterface public void insertText(final int dn, final String s, final int dr) { + // TODO: Unify in-app and system insertText Handler mainLoop = new Handler(Looper.getMainLooper()); mainLoop.post(new Runnable() { public void run() { @@ -2813,11 +2832,19 @@ public final class KMManager { ic.beginBatchEdit(); + int deleteLeft = dn; + // Delete any existing selected text. ExtractedText icText = ic.getExtractedText(new ExtractedTextRequest(), 0); if (icText != null) { // This can be null if the input connection becomes invalid. int start = icText.startOffset + icText.selectionStart; int end = icText.startOffset + icText.selectionEnd; + if (end < start) { + // Swap start/end for backward selection + int temp = start; + start = end; + end = temp; + } if (end > start) { if (s.length() == 0) { ic.setSelection(start, start); @@ -2829,6 +2856,10 @@ public final class KMManager { ic.setSelection(start, start); ic.deleteSurroundingText(0, end - start); } + + // KeymanWeb tells us how to delete the selection, but we don't + // want to do that twice + deleteLeft = 0; } } @@ -2839,8 +2870,8 @@ public final class KMManager { } // Perform left-deletions - if (dn > 0) { - performLeftDeletions(ic, dn); + if (deleteLeft > 0) { + performLeftDeletions(ic, deleteLeft); } // Perform right-deletions @@ -2875,8 +2906,8 @@ public final class KMManager { } /* - // TODO: Chromium has a bug where deleteSurroundingText deletes an entire grapheme cluster - // instead of one code-point. See Chromium issue #1024738 + // Chromium up until version M81 had a bug where deleteSurroundingText deletes an entire + // grapheme cluster instead of one code-point. See Chromium issue #1024738 // https://bugs.chromium.org/p/chromium/issues/detail?id=1024738 // // We'll retrieve up to (dn*2+16) characters before the cursor to collect enough characters @@ -2894,7 +2925,17 @@ public final class KMManager { return; } - int numPairs = CharSequenceUtil.countSurrogatePairs(charsBackup, dn); + // Count the number of characters which are surrogate pairs + int index = lastIndex, dnx = dn, numPairs = 0; + while(index > 0 && dnx > 0) { + if(Character.isLowSurrogate(charsBackup.charAt(index)) && + Character.isHighSurrogate(charsBackup.charAt(index-1))) { + numPairs++; + index--; + } + index--; + dnx--; + } // Chop dn+numPairs code points from the end of charsBackup // subSequence indices are start(inclusive) to end(exclusive)