From e347c423bf48d8ce0aea04ded2948cc83b0b4232 Mon Sep 17 00:00:00 2001 From: jahorton Date: Wed, 16 Jun 2021 12:57:05 +0700 Subject: [PATCH 01/44] refactor(web): wraps in-browser subkey selection with Promise --- web/source/osk/browser/subkeyPopup.ts | 18 ++++- web/source/osk/visualKeyboard.ts | 103 +++++++++++++++++--------- 2 files changed, 84 insertions(+), 37 deletions(-) diff --git a/web/source/osk/browser/subkeyPopup.ts b/web/source/osk/browser/subkeyPopup.ts index 4879140c13..c917c1829e 100644 --- a/web/source/osk/browser/subkeyPopup.ts +++ b/web/source/osk/browser/subkeyPopup.ts @@ -4,12 +4,16 @@ namespace com.keyman.osk.browser { export class SubkeyPopup { public readonly element: HTMLDivElement; public readonly shim: HTMLDivElement; + public readonly baseKey: KeyElement; - private baseKey: KeyElement; private callout: HTMLDivElement; - constructor(vkbd: VisualKeyboard, e: KeyElement) { + // Resolves the promise that generated this SubkeyPopup. + private resolver: (keyEvent: text.KeyEvent) => void; + + constructor(vkbd: VisualKeyboard, e: KeyElement, resolve: (keyEvent: text.KeyEvent) => void) { let keyman = com.keyman.singleton; + this.resolver = resolve; // A tag we directly set on a key element during its construction. let subKeySpec: OSKKeySpec[] = e['subKeys']; @@ -72,6 +76,13 @@ namespace com.keyman.osk.browser { } } + resolve(keyEvent: text.KeyEvent) { + if(this.resolver) { + this.resolver(keyEvent); + } + this.resolver = null; + } + reposition(vkbd: VisualKeyboard) { let keyman = com.keyman.singleton; @@ -182,6 +193,9 @@ namespace com.keyman.osk.browser { } clear() { + // If not yet resolved, resolve the corresponding Promise. + this.resolve(null); + // Remove the displayed subkey array, if any if(this.element.parentNode) { this.element.parentNode.removeChild(this.element); diff --git a/web/source/osk/visualKeyboard.ts b/web/source/osk/visualKeyboard.ts index 99ca19ecfc..e1824e4757 100644 --- a/web/source/osk/visualKeyboard.ts +++ b/web/source/osk/visualKeyboard.ts @@ -6,6 +6,11 @@ /// namespace com.keyman.osk { + interface SubkeyDeferment { + timerId: number; + resolve: (KeyEvent) => void; + } + export class VisualKeyboard { // Legacy alias, maintaining a reference for code built against older // versions of KMW. @@ -60,10 +65,10 @@ namespace com.keyman.osk { currentTarget: KeyElement; // Popup key management - popupBaseKey: KeyElement; popupPending: boolean = false; - subkeyDelayTimer: number; + //subkeyDelayTimer: number; popupDelay: number = 500; + subkeyDeferment: SubkeyDeferment menuEvent: KeyElement; // Used by embedded-mode. keytip: KeyTip; subkeyPopup: browser.SubkeyPopup; @@ -425,7 +430,7 @@ namespace com.keyman.osk { let kbdAspectRatio = layerGroup.offsetWidth / this.kbdDiv.offsetHeight; let baseKeyProbabilities = this.layout.getLayer(this.layerId).getTouchProbabilities(touchKbdPos, kbdAspectRatio); - if(!this.popupBaseKey || !this.popupBaseKey.key) { + if(!this.subkeyPopup || !this.subkeyPopup.baseKey.key) { return baseKeyProbabilities; } else { // A temp-hack, as this was noted just before 14.0's release. @@ -437,7 +442,7 @@ namespace com.keyman.osk { let baseMass = 1.0; let baseKeyMass = 1.0; - let baseKeyID = this.popupBaseKey.key.spec.coreID; + let baseKeyID = this.subkeyPopup.baseKey.key.spec.coreID; let popupKeyMass = 0.0; let popupKeyID: string = null; @@ -560,7 +565,13 @@ namespace com.keyman.osk { } else { if(this.keyPending) { this.highlightKey(this.keyPending, false); - this.modelKeyClick(this.keyPending, this.touchPending); + + if(this.subkeyPopup) { + let keyEvent = this.initKeyEvent(this.keyPending, this.touchPending); + this.subkeyPopup.resolve(keyEvent); + } else { + this.modelKeyClick(this.keyPending, this.touchPending); + } this.clearPopup(); // Decrement the number of unreleased touch points to prevent // sending the keystroke again when the key is actually released @@ -594,7 +605,7 @@ namespace com.keyman.osk { } // Cancel (but do not execute) pending key if neither a popup key or the base key - if((t == null) || ((t.id.indexOf('popup') < 0) && (t.id != this.popupBaseKey.id))) { + if((t == null) || (!(t.key instanceof browser.OSKSubKey) && (t.id != this.subkeyPopup.baseKey.id))) { this.highlightKey(this.keyPending,false); this.clearPopup(); this.keyPending = null; @@ -630,10 +641,15 @@ namespace com.keyman.osk { // Process and clear highlighting of pending target if(this.keyPending) { this.highlightKey(this.keyPending,false); - + // Output character unless moved off key if(this.keyPending.className.indexOf('hidden') < 0 && tc > 0 && !beyondEdge) { - this.modelKeyClick(this.keyPending, e.changedTouches[0]); + if(this.subkeyPopup) { + let keyEvent = this.initKeyEvent(this.keyPending, e.changedTouches[0]); + this.subkeyPopup.resolve(keyEvent); + } else { + this.modelKeyClick(this.keyPending, e.changedTouches[0]); + } } this.clearPopup(); this.keyPending = null; @@ -710,7 +726,7 @@ namespace com.keyman.osk { this.keyPending=null; this.touchPending=null; } else { - if(key1 == this.popupBaseKey) { + if(this.subkeyPopup && key1 == this.subkeyPopup.baseKey) { if(!key1.classList.contains('kmw-key-touched')) { this.highlightKey(key1,true); } @@ -727,11 +743,11 @@ namespace com.keyman.osk { return; } - var sk=document.getElementById('kmw-popup-keys'); + var sk=this.subkeyPopup // Use the popup duplicate of the base key if a phone with a visible popup array - if(sk && sk.style.visibility == 'visible' && this.device.formFactor == 'phone' && key1 == this.popupBaseKey) { - key1 = sk.childNodes[0].firstChild; + if(sk && sk.element.style.visibility == 'visible' && this.device.formFactor == 'phone' && key1 == sk.baseKey) { + key1 = sk.element.childNodes[0].firstChild; } // Identify current touch position (to manage off-key release) @@ -745,15 +761,15 @@ namespace com.keyman.osk { // If popup is visible, need to move over popup, not over main keyboard this.highlightSubKeys(key1,x,y); - if(sk && sk.style.visibility == 'visible') { + if(sk && sk.element.style.visibility == 'visible') { // Once a subkey array is displayed, do not allow changing the base key. // Keep that array visible and accept no other options until the touch ends. - if(key1 && key1.id.indexOf('popup') < 0 && key1 != this.popupBaseKey) { + if(key1 && key1.id.indexOf('popup') < 0 && key1 != this.subkeyPopup.baseKey) { return; } // Highlight the base key on devices that do not append it to the subkey array. - if(key1 && key1 == this.popupBaseKey && key1.className.indexOf('kmw-key-touched') < 0) { + if(key1 && key1 == this.subkeyPopup.baseKey) { this.highlightKey(key1,true); } // Cancel touch if moved up and off keyboard, unless popup keys visible @@ -1072,15 +1088,16 @@ namespace com.keyman.osk { clearPopup() { // Remove the displayed subkey array, if any, and cancel popup request if(this.subkeyPopup) { + this.subkeyPopup.resolve(null); this.subkeyPopup.clear(); this.subkeyPopup = null; } - if(this.subkeyDelayTimer) { - window.clearTimeout(this.subkeyDelayTimer); - this.subkeyDelayTimer = null; + if(this.subkeyDeferment) { + window.clearTimeout(this.subkeyDeferment.timerId); + this.subkeyDeferment.resolve(null); + this.subkeyDeferment = null; } - this.popupBaseKey = null; } //#region 'native'-mode subkey handling @@ -1088,7 +1105,7 @@ namespace com.keyman.osk { * Display touch-hold array of 'sub-keys' above the currently touched key * @param {Object} e primary key element */ - showSubKeys(e: KeyElement) { + showSubKeys(e: KeyElement, resolve: (KeyElement) => void) { // Do not show subkeys if key already released if(this.keyPending == null) { return; @@ -1096,9 +1113,8 @@ namespace com.keyman.osk { // Clear key preview if any this.showKeyTip(null,false); - this.popupBaseKey = e; - let subKeys = this.subkeyPopup = new browser.SubkeyPopup(this, e); + let subKeys = this.subkeyPopup = new browser.SubkeyPopup(this, e, resolve); // Otherwise append the touch-hold (subkey) array to the OSK let keyman = com.keyman.singleton; @@ -1678,19 +1694,39 @@ namespace com.keyman.osk { * * @param {Object} key base key object */ - touchHold(key: KeyElement) { + touchHold(key: KeyElement, force?: boolean) { // Clear and restart the popup timer - if(this.subkeyDelayTimer) { - window.clearTimeout(this.subkeyDelayTimer); - this.subkeyDelayTimer = null; + if(this.subkeyDeferment) { + window.clearTimeout(this.subkeyDeferment.timerId); + // Cancel the potential subkey event. + this.subkeyDeferment.resolve(null); + this.subkeyDeferment = null; } if(typeof key['subKeys'] != 'undefined' && key['subKeys'] != null) { - this.subkeyDelayTimer = window.setTimeout( - function(this: VisualKeyboard) { - this.clearPopup(); - this.showSubKeys(key); - }.bind(this), this.popupDelay); + let _this = this; + + let promise = new Promise(function(resolve, reject) { + let timerId = window.setTimeout( + function() { + // It's no longer deferred; it's being fulfilled. + // Even if the actual subkey itself is still async. + _this.subkeyDeferment = null; + _this.clearPopup(); + _this.showSubKeys(key, resolve); + }, force ? 0 : _this.popupDelay); + + _this.subkeyDeferment = { + timerId: timerId, + resolve: resolve + }; + }).then(function(keyEvent: text.KeyEvent) { + if(keyEvent) { + // Do something. + console.log("Promise fulfilled for " + keyEvent.kName); + PreProcessor.raiseKeyEvent(keyEvent); + } + }); } }; @@ -1730,10 +1766,7 @@ namespace com.keyman.osk { // Show popup keys immediately if touch moved up towards key array (KMEW-100, Build 353) if((this.touchY-y > 5) && skBox == null) { - if(this.subkeyDelayTimer) { - window.clearTimeout(this.subkeyDelayTimer); - } - this.showSubKeys(k); + this.touchHold(k, true); } //#endregion }; From a8e051b6f48299dda4e23595ee9c9b791a8ef39a Mon Sep 17 00:00:00 2001 From: jahorton Date: Wed, 16 Jun 2021 15:16:13 +0700 Subject: [PATCH 02/44] refactor(web): wraps embedded subkey selection with Promise --- web/source/kmwembedded.ts | 38 +++++++++++++--------- web/source/osk/embedded/subkeyDelegator.ts | 19 +++++++++++ web/source/osk/visualKeyboard.ts | 3 +- 3 files changed, 43 insertions(+), 17 deletions(-) create mode 100644 web/source/osk/embedded/subkeyDelegator.ts diff --git a/web/source/kmwembedded.ts b/web/source/kmwembedded.ts index a95f16dcc9..0d3039371a 100644 --- a/web/source/kmwembedded.ts +++ b/web/source/kmwembedded.ts @@ -2,6 +2,7 @@ // References the base Keyman object (and consequently, the rest of the core objects). /// /// +/// // KeymanWeb 11.0 // Copyright 2019 SIL International @@ -60,8 +61,19 @@ namespace com.keyman.osk { // #3718: No longer prepend base key to subkey array - this.popupBaseKey = key; - this.popupPending=true; + let _this = this; + let promise = new Promise(function(resolve, reject) { + _this.subkeyDeferment = { + timerId: 0, + resolve: resolve + }; + + let delegator = new embedded.SubkeyDelegator(key, resolve); + _this.subkeyDelegator = delegator; + }).then(function(keyEvent) { + PreProcessor.raiseKeyEvent(keyEvent); + }); + window['oskCreatePopup'](key['subKeys'], xBase, yBase, key.offsetWidth, key.offsetHeight); } }; @@ -349,11 +361,13 @@ namespace com.keyman.text { keymanweb.domManager.initActiveElement(Lelem); var nextLayer: string; - + var delegator: com.keyman.osk.embedded.SubkeyDelegator = null; // This should be set if we're within this method... but it's best to guard against nulls here, just in case. - if(osk.vkbd.popupBaseKey && osk.vkbd.popupBaseKey['key']) { + if(osk.vkbd.subkeyDelegator) { + delegator = osk.vkbd.subkeyDelegator as com.keyman.osk.embedded.SubkeyDelegator; + // This is set with the base key of our current subkey elsewhere within the engine. - var baseKey: com.keyman.osk.OSKKeySpec = osk.vkbd.popupBaseKey['key'].spec; + var baseKey: com.keyman.osk.OSKKeySpec = delegator.baseKey.key.spec; var found = false; if(baseKey.coreID == keyName) { @@ -375,6 +389,8 @@ namespace com.keyman.text { if(!found) { console.warn("Could not find subkey '" + origArg + "' under the current base key '" + baseKey.coreID + "'!"); } + + osk.vkbd.subkeyDelegator = null; } else { console.warn("No base key exists for the subkey being executed: '" + origArg + "'"); } @@ -415,17 +431,9 @@ namespace com.keyman.text { Lkc.vkCode=Lkc.Lcode; - // Now that we have a valid key event, hand it off to the Processor for execution. - // This allows the Processor to also handle any predictive-text tasks necessary. - let retVal = com.keyman.osk.PreProcessor.handleClick(Lkc, com.keyman.dom.Utils.getOutputTarget(Lelem), null); - - // Special case for embedded to pass K_TAB back to device to process - if(Lkc.Lcode == Codes.keyCodes["K_TAB"] || Lkc.Lcode == Codes.keyCodes["K_TABBACK"] - || Lkc.Lcode == Codes.keyCodes["K_TABFWD"]) { - return false; + if(delegator) { + delegator.resolve(Lkc); } - - return retVal; }; /** diff --git a/web/source/osk/embedded/subkeyDelegator.ts b/web/source/osk/embedded/subkeyDelegator.ts new file mode 100644 index 0000000000..de1df534f5 --- /dev/null +++ b/web/source/osk/embedded/subkeyDelegator.ts @@ -0,0 +1,19 @@ +namespace com.keyman.osk.embedded { + export class SubkeyDelegator { + private resolver: (keyEvent: text.KeyEvent) => void; + + public readonly baseKey: KeyElement; + + constructor(e: KeyElement, resolve: (keyEvent: text.KeyEvent) => void) { + this.resolver = resolve; + this.baseKey = e; + } + + public resolve(keyEvent: text.KeyEvent) { + if(this.resolver) { + this.resolver(keyEvent); + } + this.resolver = null; + } + } +} \ No newline at end of file diff --git a/web/source/osk/visualKeyboard.ts b/web/source/osk/visualKeyboard.ts index e1824e4757..d15eaf2c77 100644 --- a/web/source/osk/visualKeyboard.ts +++ b/web/source/osk/visualKeyboard.ts @@ -65,13 +65,12 @@ namespace com.keyman.osk { currentTarget: KeyElement; // Popup key management - popupPending: boolean = false; - //subkeyDelayTimer: number; popupDelay: number = 500; subkeyDeferment: SubkeyDeferment menuEvent: KeyElement; // Used by embedded-mode. keytip: KeyTip; subkeyPopup: browser.SubkeyPopup; + subkeyDelegator: any; // a temp, intermediate property during subkey abstraction work get layerId(): string { return this._layerId; From 7c08d9b6607b9c6d4e2057fb6fe810a8032abf55 Mon Sep 17 00:00:00 2001 From: jahorton Date: Wed, 16 Jun 2021 16:13:28 +0700 Subject: [PATCH 03/44] fix(web): forgotten null guard --- web/source/kmwembedded.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/source/kmwembedded.ts b/web/source/kmwembedded.ts index 0d3039371a..e081d2927d 100644 --- a/web/source/kmwembedded.ts +++ b/web/source/kmwembedded.ts @@ -71,7 +71,9 @@ namespace com.keyman.osk { let delegator = new embedded.SubkeyDelegator(key, resolve); _this.subkeyDelegator = delegator; }).then(function(keyEvent) { - PreProcessor.raiseKeyEvent(keyEvent); + if(keyEvent) { + PreProcessor.raiseKeyEvent(keyEvent); + } }); window['oskCreatePopup'](key['subKeys'], xBase, yBase, key.offsetWidth, key.offsetHeight); From 8794f7d018394d7356a41e029353497e80d781a1 Mon Sep 17 00:00:00 2001 From: jahorton Date: Thu, 17 Jun 2021 09:37:34 +0700 Subject: [PATCH 04/44] change(web): allows passive cancel for subkey promises --- web/source/kmwembedded.ts | 7 ++---- web/source/osk/browser/subkeyPopup.ts | 6 ++++-- web/source/osk/visualKeyboard.ts | 31 ++++++++++++--------------- 3 files changed, 20 insertions(+), 24 deletions(-) diff --git a/web/source/kmwembedded.ts b/web/source/kmwembedded.ts index e081d2927d..c875f9cbe1 100644 --- a/web/source/kmwembedded.ts +++ b/web/source/kmwembedded.ts @@ -63,14 +63,11 @@ namespace com.keyman.osk { let _this = this; let promise = new Promise(function(resolve, reject) { - _this.subkeyDeferment = { - timerId: 0, - resolve: resolve - }; - let delegator = new embedded.SubkeyDelegator(key, resolve); _this.subkeyDelegator = delegator; }).then(function(keyEvent) { + // Allow active cancellation, even if the source should allow passive. + // It's an easy and cheap null guard. if(keyEvent) { PreProcessor.raiseKeyEvent(keyEvent); } diff --git a/web/source/osk/browser/subkeyPopup.ts b/web/source/osk/browser/subkeyPopup.ts index c917c1829e..c991ef38fc 100644 --- a/web/source/osk/browser/subkeyPopup.ts +++ b/web/source/osk/browser/subkeyPopup.ts @@ -193,8 +193,10 @@ namespace com.keyman.osk.browser { } clear() { - // If not yet resolved, resolve the corresponding Promise. - this.resolve(null); + // Discard the reference to the Promise's resolve method, allowing + // GC to clean it up. The corresponding Promise's contract allows + // passive cancellation. + this.resolver = null; // Remove the displayed subkey array, if any if(this.element.parentNode) { diff --git a/web/source/osk/visualKeyboard.ts b/web/source/osk/visualKeyboard.ts index d15eaf2c77..4b6b67418b 100644 --- a/web/source/osk/visualKeyboard.ts +++ b/web/source/osk/visualKeyboard.ts @@ -66,7 +66,7 @@ namespace com.keyman.osk { // Popup key management popupDelay: number = 500; - subkeyDeferment: SubkeyDeferment + subkeyDelayTimer: number; menuEvent: KeyElement; // Used by embedded-mode. keytip: KeyTip; subkeyPopup: browser.SubkeyPopup; @@ -1087,15 +1087,13 @@ namespace com.keyman.osk { clearPopup() { // Remove the displayed subkey array, if any, and cancel popup request if(this.subkeyPopup) { - this.subkeyPopup.resolve(null); this.subkeyPopup.clear(); this.subkeyPopup = null; } - if(this.subkeyDeferment) { - window.clearTimeout(this.subkeyDeferment.timerId); - this.subkeyDeferment.resolve(null); - this.subkeyDeferment = null; + if(this.subkeyDelayTimer) { + window.clearTimeout(this.subkeyDelayTimer); + this.subkeyDelayTimer = null; } } @@ -1695,34 +1693,33 @@ namespace com.keyman.osk { */ touchHold(key: KeyElement, force?: boolean) { // Clear and restart the popup timer - if(this.subkeyDeferment) { - window.clearTimeout(this.subkeyDeferment.timerId); + if(this.subkeyDelayTimer) { + window.clearTimeout(this.subkeyDelayTimer); // Cancel the potential subkey event. - this.subkeyDeferment.resolve(null); - this.subkeyDeferment = null; + this.subkeyDelayTimer = null; } if(typeof key['subKeys'] != 'undefined' && key['subKeys'] != null) { let _this = this; + // This Promise receives the user's selected subkey, should it exist. + // Note: the OSK's use of this Promise will allow for passive cancellation. + // If cancelled (so, `null`), the Promise ought remain unresolved. let promise = new Promise(function(resolve, reject) { let timerId = window.setTimeout( function() { // It's no longer deferred; it's being fulfilled. // Even if the actual subkey itself is still async. - _this.subkeyDeferment = null; + _this.subkeyDelayTimer = null; _this.clearPopup(); _this.showSubKeys(key, resolve); }, force ? 0 : _this.popupDelay); - _this.subkeyDeferment = { - timerId: timerId, - resolve: resolve - }; + _this.subkeyDelayTimer = timerId; }).then(function(keyEvent: text.KeyEvent) { + // Allow active cancellation, even if the source should allow passive. + // It's an easy and cheap null guard. if(keyEvent) { - // Do something. - console.log("Promise fulfilled for " + keyEvent.kName); PreProcessor.raiseKeyEvent(keyEvent); } }); From dc4532ecb3c723fda279357b5c61d38203609b75 Mon Sep 17 00:00:00 2001 From: jahorton Date: Thu, 17 Jun 2021 12:13:02 +0700 Subject: [PATCH 05/44] fix(web): base key rehighlighting during subkey event when embedded --- web/source/kmwembedded.ts | 1 + web/source/osk/visualKeyboard.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/web/source/kmwembedded.ts b/web/source/kmwembedded.ts index c875f9cbe1..a6ad1cde2f 100644 --- a/web/source/kmwembedded.ts +++ b/web/source/kmwembedded.ts @@ -66,6 +66,7 @@ namespace com.keyman.osk { let delegator = new embedded.SubkeyDelegator(key, resolve); _this.subkeyDelegator = delegator; }).then(function(keyEvent) { + _this.subkeyDelegator = null; // Allow active cancellation, even if the source should allow passive. // It's an easy and cheap null guard. if(keyEvent) { diff --git a/web/source/osk/visualKeyboard.ts b/web/source/osk/visualKeyboard.ts index 4b6b67418b..592e32c575 100644 --- a/web/source/osk/visualKeyboard.ts +++ b/web/source/osk/visualKeyboard.ts @@ -725,7 +725,8 @@ namespace com.keyman.osk { this.keyPending=null; this.touchPending=null; } else { - if(this.subkeyPopup && key1 == this.subkeyPopup.baseKey) { + // Re-apply highlighting to the key if it's the current popup's base key. + if(this.subkeyDelegator && key1 == this.subkeyDelegator.baseKey) { if(!key1.classList.contains('kmw-key-touched')) { this.highlightKey(key1,true); } From 2d06c65412e7ab376b8aca2e5f839d98dd6101ac Mon Sep 17 00:00:00 2001 From: jahorton Date: Wed, 16 Jun 2021 16:10:49 +0700 Subject: [PATCH 06/44] refactor(web): simplifies embedded executePopupKey --- web/source/kmwembedded.ts | 45 +++++--------------------------- web/source/osk/visualKeyboard.ts | 11 +++++--- 2 files changed, 14 insertions(+), 42 deletions(-) diff --git a/web/source/kmwembedded.ts b/web/source/kmwembedded.ts index a6ad1cde2f..1996a17e48 100644 --- a/web/source/kmwembedded.ts +++ b/web/source/kmwembedded.ts @@ -360,8 +360,8 @@ namespace com.keyman.text { keymanweb.domManager.initActiveElement(Lelem); - var nextLayer: string; var delegator: com.keyman.osk.embedded.SubkeyDelegator = null; + let selectedKey: com.keyman.osk.OSKKeySpec = null; // This should be set if we're within this method... but it's best to guard against nulls here, just in case. if(osk.vkbd.subkeyDelegator) { delegator = osk.vkbd.subkeyDelegator as com.keyman.osk.embedded.SubkeyDelegator; @@ -371,15 +371,14 @@ namespace com.keyman.text { var found = false; if(baseKey.coreID == keyName) { - nextLayer = baseKey.nextlayer; + selectedKey = baseKey; found = true; } else { // Search for the specified subkey so we can retrieve its useful properties. // It should be within the popupBaseKey's subkey list. for(let subKey of baseKey.sk) { if(subKey.coreID == keyName) { - // ... to consider: why are we not just taking the keyspec wholesale right here? - nextLayer = subKey.nextlayer; + selectedKey = subKey; found = true; break; } @@ -395,42 +394,12 @@ namespace com.keyman.text { console.warn("No base key exists for the subkey being executed: '" + origArg + "'"); } - let Codes = com.keyman.text.Codes; - - // Check the virtual key - let Lkc: com.keyman.text.KeyEvent = { - Lmodifiers: keyShiftState, - Lstates: 0, - Lcode: Codes.keyCodes[keyName], - LisVirtualKey: true, - kName: keyName, - kNextLayer: nextLayer, - vkCode: null, // was originally undefined - isSynthetic: true, - device: keymanweb.util.device.coreSpec - }; - - // Process modifier key action - if(core.keyboardProcessor.selectLayer(Lkc, true)) { // ignores key's 'nextLayer' property for this check - return true; + let Lkc: com.keyman.text.KeyEvent = null; + if(selectedKey) { + Lkc = osk.vkbd.keyEventFromSpec(selectedKey as com.keyman.keyboards.ActiveKey, null); + Lkc.vkCode=Lkc.Lcode; } - // While we can't source the base KeyEvent properties for embedded subkeys the same way as native, - // we can handle many other pre-processing steps the same way with this common method. - core.keyboardProcessor.setSyntheticEventDefaults(Lkc); - - //if(!Lkc.Lcode) return false; // Value is now zero if not known (Build 347) - //Build 353: revert to prior test to try to fix lack of KMEI output, May 1, 2014 - if(isNaN(Lkc.Lcode) || !Lkc.Lcode) { - // Addresses modifier SHIFT keys. - if(nextLayer) { - core.keyboardProcessor.selectLayer(Lkc); - } - return false; - } - - Lkc.vkCode=Lkc.Lcode; - if(delegator) { delegator.resolve(Lkc); } diff --git a/web/source/osk/visualKeyboard.ts b/web/source/osk/visualKeyboard.ts index fbd09d3fce..5e6c407e81 100644 --- a/web/source/osk/visualKeyboard.ts +++ b/web/source/osk/visualKeyboard.ts @@ -960,8 +960,6 @@ namespace com.keyman.osk { // Turn off key highlighting (or preview) this.highlightKey(e,false); - let core = com.keyman.singleton.core; // only singleton-based ref currently needed here. - // Future note: we need to refactor osk.OSKKeySpec to instead be a 'tag field' for // keyboards.ActiveKey. (Prob with generics, allowing the Web-only parts to // be fully specified within the tag.) @@ -973,7 +971,13 @@ namespace com.keyman.osk { console.error("OSK key with ID '" + e.id + "', keyID '" + e.keyId + "' missing needed specification"); return null; } + + // Return the event object. + return this.keyEventFromSpec(keySpec, touch); + } + keyEventFromSpec(keySpec: keyboards.ActiveKey, touch?: Touch) { + let core = com.keyman.singleton.core; // only singleton-based ref currently needed here. // Start: mirrors _GetKeyEventProperties @@ -994,8 +998,7 @@ namespace com.keyman.osk { Lkc.source = touch; Lkc.keyDistribution = this.getTouchProbabilities(touch);; } - - // Return the event object. + return Lkc; } From 50ee62a7e02cec5d70efc915f1848863a825d0e3 Mon Sep 17 00:00:00 2001 From: jahorton Date: Thu, 24 Jun 2021 09:45:27 +0700 Subject: [PATCH 07/44] change(web): subkey lookup through base key --- .../src/keyboards/activeLayout.ts | 4 ++-- web/source/kmwembedded.ts | 13 ++++--------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/common/core/web/keyboard-processor/src/keyboards/activeLayout.ts b/common/core/web/keyboard-processor/src/keyboards/activeLayout.ts index 6cdddf7d83..442d23de64 100644 --- a/common/core/web/keyboard-processor/src/keyboards/activeLayout.ts +++ b/common/core/web/keyboard-processor/src/keyboards/activeLayout.ts @@ -221,10 +221,10 @@ namespace com.keyman.keyboards { return Lkc; } - public getSubkey(id: string): ActiveKey { + public getSubkey(coreID: string): ActiveKey { if(this.sk) { for(let key of this.sk) { - if(key.id == id) { + if(key.coreID == coreID) { return key; } } diff --git a/web/source/kmwembedded.ts b/web/source/kmwembedded.ts index 1996a17e48..6e05faf83b 100644 --- a/web/source/kmwembedded.ts +++ b/web/source/kmwembedded.ts @@ -374,15 +374,10 @@ namespace com.keyman.text { selectedKey = baseKey; found = true; } else { - // Search for the specified subkey so we can retrieve its useful properties. - // It should be within the popupBaseKey's subkey list. - for(let subKey of baseKey.sk) { - if(subKey.coreID == keyName) { - selectedKey = subKey; - found = true; - break; - } - } + // ... yeah, there are some funky type shenanigans between the two. + // OSKKeySpec is the OSK's... reinterpretation of the ActiveKey type. + selectedKey = (baseKey as com.keyman.keyboards.ActiveKey).getSubkey(keyName) as com.keyman.osk.OSKKeySpec; + found = !!selectedKey; } if(!found) { From d4d9808727fa5224cbb88ae4d99f25615ae74ed2 Mon Sep 17 00:00:00 2001 From: jahorton Date: Thu, 17 Jun 2021 14:33:13 +0700 Subject: [PATCH 08/44] refactor(web): in-browser subkey tracking managed by SubkeyPopup --- web/source/osk/browser/subkeyPopup.ts | 40 +++++++++- web/source/osk/visualKeyboard.ts | 109 +++++++++----------------- 2 files changed, 74 insertions(+), 75 deletions(-) diff --git a/web/source/osk/browser/subkeyPopup.ts b/web/source/osk/browser/subkeyPopup.ts index c991ef38fc..7c7ecdf260 100644 --- a/web/source/osk/browser/subkeyPopup.ts +++ b/web/source/osk/browser/subkeyPopup.ts @@ -4,7 +4,10 @@ namespace com.keyman.osk.browser { export class SubkeyPopup { public readonly element: HTMLDivElement; public readonly shim: HTMLDivElement; + + private vkbd: VisualKeyboard; public readonly baseKey: KeyElement; + private currentSelection: KeyElement; private callout: HTMLDivElement; @@ -15,6 +18,10 @@ namespace com.keyman.osk.browser { let keyman = com.keyman.singleton; this.resolver = resolve; + this.vkbd = vkbd; + this.baseKey = e; + this.currentSelection = null; + // A tag we directly set on a key element during its construction. let subKeySpec: OSKKeySpec[] = e['subKeys']; @@ -22,7 +29,7 @@ namespace com.keyman.osk.browser { // is possible while the array is visible. So it is simplest to let the keys have // position:static and display:inline-block var subKeys = this.element = document.createElement('div'); - this.baseKey = e; + var i; subKeys.id='kmw-popup-keys'; @@ -76,8 +83,12 @@ namespace com.keyman.osk.browser { } } - resolve(keyEvent: text.KeyEvent) { + finalize(touch: Touch) { if(this.resolver) { + let keyEvent: text.KeyEvent = null; + if(this.currentSelection) { + keyEvent = this.vkbd.initKeyEvent(this.currentSelection, touch); + } this.resolver(keyEvent); } this.resolver = null; @@ -211,5 +222,30 @@ namespace com.keyman.osk.browser { this.callout.parentNode.removeChild(this.callout); } } + + updateTouch(touch: Touch) { + let x = touch.clientX; + let y = touch.clientY; + + this.currentSelection = null; + + for(let i=0; i < this.baseKey['subKeys'].length; i++) { + try { + let sk= this.element.childNodes[i].firstChild as KeyElement; + let x0 = dom.Utils.getAbsoluteX(sk); + let y0 = dom.Utils.getAbsoluteY(sk);//-document.body.scrollTop; + + let x1=x0+sk.offsetWidth; + let y1=y0+sk.offsetHeight; + + let onKey=(x > x0 && x < x1 && y > y0 && y < y1); + if(onKey) { + this.baseKey.key.highlight(false); + this.currentSelection = sk; + } + sk.key.highlight(onKey); + } catch(ex){} + } + } } } \ No newline at end of file diff --git a/web/source/osk/visualKeyboard.ts b/web/source/osk/visualKeyboard.ts index 5e6c407e81..f4072a1585 100644 --- a/web/source/osk/visualKeyboard.ts +++ b/web/source/osk/visualKeyboard.ts @@ -566,12 +566,11 @@ namespace com.keyman.osk { this.highlightKey(this.keyPending, false); if(this.subkeyPopup) { - let keyEvent = this.initKeyEvent(this.keyPending, this.touchPending); - this.subkeyPopup.resolve(keyEvent); + this.subkeyPopup.updateTouch(e.changedTouches[0]); + this.subkeyPopup.finalize(e.changedTouches[0]); } else { this.modelKeyClick(this.keyPending, this.touchPending); } - this.clearPopup(); // Decrement the number of unreleased touch points to prevent // sending the keystroke again when the key is actually released this.touchCount--; @@ -597,19 +596,18 @@ namespace com.keyman.osk { // Clear repeated backspace if active, preventing 'sticky' behavior. this.cancelDelete(); - if((sk && sk.style.visibility == 'visible')) { + if((this.subkeyPopup && this.subkeyPopup.element.style.visibility == 'visible')) { // Ignore release if a multiple touch if(e.touches.length > 0) { return; } - // Cancel (but do not execute) pending key if neither a popup key or the base key - if((t == null) || (!(t.key instanceof browser.OSKSubKey) && (t.id != this.subkeyPopup.baseKey.id))) { - this.highlightKey(this.keyPending,false); - this.clearPopup(); - this.keyPending = null; - this.touchPending = null; - } + this.subkeyPopup.finalize(e.changedTouches[0]); + this.highlightKey(this.keyPending,false); + this.keyPending = null; + this.touchPending = null; + + return; } // Only set when embedded in our Android/iOS app. Signals that the device is handling @@ -643,12 +641,7 @@ namespace com.keyman.osk { // Output character unless moved off key if(this.keyPending.className.indexOf('hidden') < 0 && tc > 0 && !beyondEdge) { - if(this.subkeyPopup) { - let keyEvent = this.initKeyEvent(this.keyPending, e.changedTouches[0]); - this.subkeyPopup.resolve(keyEvent); - } else { - this.modelKeyClick(this.keyPending, e.changedTouches[0]); - } + this.modelKeyClick(this.keyPending, e.changedTouches[0]); } this.clearPopup(); this.keyPending = null; @@ -716,6 +709,11 @@ namespace com.keyman.osk { return; } + // Clear previous key highlighting + if(!this.subkeyPopup && key0 && key1 && key1 !== key0) { + this.highlightKey(key0,false); + } + // Do not move over keys if device popup visible if(this.popupVisible) { if(key1 == null) { @@ -743,51 +741,33 @@ namespace com.keyman.osk { return; } - var sk=this.subkeyPopup + this.currentTarget = null; - // Use the popup duplicate of the base key if a phone with a visible popup array - if(sk && sk.element.style.visibility == 'visible' && this.device.formFactor == 'phone' && key1 == sk.baseKey) { - key1 = sk.element.childNodes[0].firstChild; + // If popup is visible, need to move over popup, not over main keyboard + this.highlightSubKeys(key1,x,y); + + if(this.subkeyPopup) { + this.subkeyPopup.updateTouch(e.touches[0]); + return; } // Identify current touch position (to manage off-key release) this.currentTarget = key1; - // Clear previous key highlighting - if(key0 && key1 && key1 !== key0) { + // _Box has (most of) the useful client values. + let _Box = this.kbdDiv.parentElement ? this.kbdDiv.parentElement : keyman.osk._Box; + let height = this.kbdDiv.offsetHeight; + // We need to adjust the offset properties by any offsets related to the active banner. + + // Determine the y-threshold at which touch-cancellation should automatically occur. + let rowCount = this.layers[this.layerIndex].row.length; + let yBufferThreshold = (0.333 * height / rowCount); // Allows vertical movement by 1/3 the height of a row. + var yMin = (this.kbdDiv && _Box) ? Math.max(5, this.kbdDiv.offsetTop - yBufferThreshold) : 5; + if(key0 && e.touches[0].pageY < yMin) { this.highlightKey(key0,false); - } - - // If popup is visible, need to move over popup, not over main keyboard - this.highlightSubKeys(key1,x,y); - - if(sk && sk.element.style.visibility == 'visible') { - // Once a subkey array is displayed, do not allow changing the base key. - // Keep that array visible and accept no other options until the touch ends. - if(key1 && key1.id.indexOf('popup') < 0 && key1 != this.subkeyPopup.baseKey) { - return; - } - - // Highlight the base key on devices that do not append it to the subkey array. - if(key1 && key1 == this.subkeyPopup.baseKey) { - this.highlightKey(key1,true); - } - // Cancel touch if moved up and off keyboard, unless popup keys visible - } else { - // _Box has (most of) the useful client values. - let _Box = this.kbdDiv.parentElement ? this.kbdDiv.parentElement : keyman.osk._Box; - let height = this.kbdDiv.offsetHeight; - - // Determine the y-threshold at which touch-cancellation should automatically occur. - let rowCount = this.layers[this.layerIndex].row.length; - let yBufferThreshold = (0.333 * height / rowCount); // Allows vertical movement by 1/3 the height of a row. - var yMin = (this.kbdDiv && _Box) ? Math.max(5, this.kbdDiv.offsetTop - yBufferThreshold) : 5; - if(key0 && e.touches[0].pageY < yMin) { - this.highlightKey(key0,false); - this.showKeyTip(null,false); - this.keyPending = null; - this.touchPending = null; - } + this.showKeyTip(null,false); + this.keyPending = null; + this.touchPending = null; } // Replace the target key, if any, by the new target key @@ -806,24 +786,6 @@ namespace com.keyman.osk { if(key0 && key1 && (key1 != key0) && (key1.id != '')) { // Display the touch-hold keys (after a pause) this.touchHold(key1); - /* - // Clear and restart the popup timer - if(this.subkeyDelayTimer) - { - window.clearTimeout(this.subkeyDelayTimer); - this.subkeyDelayTimer = null; - } - if(key1.subKeys != null) - { - this.subkeyDelayTimer = window.setTimeout( - function() - { - this.clearPopup(); - this.showSubKeys(key1); - }.bind(this), - this.popupDelay); - } - */ } }.bind(this); @@ -1728,6 +1690,7 @@ namespace com.keyman.osk { if(keyEvent) { PreProcessor.raiseKeyEvent(keyEvent); } + _this.clearPopup(); }); } }; From 125a2ec0d0377b76eae9acf2b3353f72342a1475 Mon Sep 17 00:00:00 2001 From: jahorton Date: Thu, 17 Jun 2021 15:48:11 +0700 Subject: [PATCH 09/44] refactor(web): two-stages embedded longpress/subkey --- web/source/kmwembedded.ts | 40 ++++++++++++++------- web/source/osk/banner.ts | 2 +- web/source/osk/embedded/pendingLongpress.ts | 25 +++++++++++++ web/source/osk/embedded/subkeyDelegator.ts | 9 +++-- web/source/osk/visualKeyboard.ts | 8 ++--- 5 files changed, 65 insertions(+), 19 deletions(-) create mode 100644 web/source/osk/embedded/pendingLongpress.ts diff --git a/web/source/kmwembedded.ts b/web/source/kmwembedded.ts index 6e05faf83b..24524ef150 100644 --- a/web/source/kmwembedded.ts +++ b/web/source/kmwembedded.ts @@ -2,7 +2,7 @@ // References the base Keyman object (and consequently, the rest of the core objects). /// /// -/// +/// // KeymanWeb 11.0 // Copyright 2019 SIL International @@ -62,17 +62,21 @@ namespace com.keyman.osk { // #3718: No longer prepend base key to subkey array let _this = this; - let promise = new Promise(function(resolve, reject) { - let delegator = new embedded.SubkeyDelegator(key, resolve); + let pendingLongpress = new embedded.PendingLongpress(key); + pendingLongpress.promise.then(function(delegator) { _this.subkeyDelegator = delegator; - }).then(function(keyEvent) { - _this.subkeyDelegator = null; - // Allow active cancellation, even if the source should allow passive. - // It's an easy and cheap null guard. - if(keyEvent) { - PreProcessor.raiseKeyEvent(keyEvent); + if(delegator) { + delegator.promise.then(function(keyEvent) { + _this.subkeyDelegator = null; + // Allow active cancellation, even if the source should allow passive. + // It's an easy and cheap null guard. + if(keyEvent) { + PreProcessor.raiseKeyEvent(keyEvent); + } + }); } }); + this.embeddedPendingLongpress = pendingLongpress; window['oskCreatePopup'](key['subKeys'], xBase, yBase, key.offsetWidth, key.offsetHeight); } @@ -287,9 +291,21 @@ namespace com.keyman.text { * @param {boolean} isVisible * **/ - keymanweb['popupVisible'] = function(isVisible) - { - osk.vkbd.popupVisible = isVisible; + keymanweb['popupVisible'] = function(isVisible) { + let delegator = osk.vkbd.subkeyDelegator as com.keyman.osk.embedded.SubkeyDelegator; + let pendingLongpress = osk.vkbd.embeddedPendingLongpress as com.keyman.osk.embedded.PendingLongpress; + + if(!isVisible) { + if(delegator) { + delegator.resolve(null); + osk.vkbd.subkeyDelegator = null; + } + } + + if(isVisible && osk.vkbd.embeddedPendingLongpress) { + // Fulfills the first-stage promise. + pendingLongpress.markActiveSubkeys(); + } }; /** diff --git a/web/source/osk/banner.ts b/web/source/osk/banner.ts index 7d69399876..3eaba765a2 100644 --- a/web/source/osk/banner.ts +++ b/web/source/osk/banner.ts @@ -493,7 +493,7 @@ namespace com.keyman.osk { // Utilized by the mobile apps; allows them to 'take over' touch handling, // blocking it within KMW when the apps are already managing an ongoing touch-hold. let keyman = com.keyman.singleton; - return keyman['osk'].vkbd.popupVisible; + return keyman['osk'].vkbd.subkeyDelegator; } protected dealiasSubTarget(target: HTMLDivElement): HTMLDivElement { diff --git a/web/source/osk/embedded/pendingLongpress.ts b/web/source/osk/embedded/pendingLongpress.ts new file mode 100644 index 0000000000..bff829a472 --- /dev/null +++ b/web/source/osk/embedded/pendingLongpress.ts @@ -0,0 +1,25 @@ +/// + +namespace com.keyman.osk.embedded { + export class PendingLongpress { + private resolver: (delegator: SubkeyDelegator) => void; + + public readonly baseKey: KeyElement; + public readonly promise: Promise; + + constructor(e: KeyElement) { + let _this = this; + this.promise = new Promise(function(resolve) { + _this.resolver = resolve; + }); + this.baseKey = e; + } + + public markActiveSubkeys() { + if(this.resolver) { + this.resolver(new SubkeyDelegator(this.baseKey)); + } + this.resolver = null; + } + } +} \ No newline at end of file diff --git a/web/source/osk/embedded/subkeyDelegator.ts b/web/source/osk/embedded/subkeyDelegator.ts index de1df534f5..0005f47449 100644 --- a/web/source/osk/embedded/subkeyDelegator.ts +++ b/web/source/osk/embedded/subkeyDelegator.ts @@ -3,9 +3,14 @@ namespace com.keyman.osk.embedded { private resolver: (keyEvent: text.KeyEvent) => void; public readonly baseKey: KeyElement; + public readonly promise: Promise; + + constructor(e: KeyElement) { + let _this = this; + this.promise = new Promise(function(resolve) { + _this.resolver = resolve; + }); - constructor(e: KeyElement, resolve: (keyEvent: text.KeyEvent) => void) { - this.resolver = resolve; this.baseKey = e; } diff --git a/web/source/osk/visualKeyboard.ts b/web/source/osk/visualKeyboard.ts index f4072a1585..d0b2681797 100644 --- a/web/source/osk/visualKeyboard.ts +++ b/web/source/osk/visualKeyboard.ts @@ -45,7 +45,6 @@ namespace com.keyman.osk { // State-related properties ddOSK: boolean = false; - popupVisible: boolean; keyPending: KeyElement; touchPending: Touch; deleteKey: KeyElement; @@ -70,6 +69,7 @@ namespace com.keyman.osk { menuEvent: KeyElement; // Used by embedded-mode. keytip: KeyTip; subkeyPopup: browser.SubkeyPopup; + embeddedPendingLongpress: any; // a temp, intermediate property during subkey abstraction work subkeyDelegator: any; // a temp, intermediate property during subkey abstraction work get layerId(): string { @@ -518,7 +518,7 @@ namespace com.keyman.osk { // Prevent multi-touch if popup displayed var sk = document.getElementById('kmw-popup-keys'); - if((sk && sk.style.visibility == 'visible') || this.popupVisible) { + if((sk && sk.style.visibility == 'visible') || this.subkeyDelegator) { return; } @@ -615,7 +615,7 @@ namespace com.keyman.osk { // // Note that on iOS (at least), this.release() will trigger before kmwembedded.ts's // executePopupKey() function. - if(this.popupVisible) { + if(this.subkeyDelegator) { return; } @@ -715,7 +715,7 @@ namespace com.keyman.osk { } // Do not move over keys if device popup visible - if(this.popupVisible) { + if(this.subkeyDelegator) { if(key1 == null) { if(key0) { this.highlightKey(key0,false); From d99203545b6f6e70b82e0c7f1877270b6d407eaf Mon Sep 17 00:00:00 2001 From: jahorton Date: Thu, 24 Jun 2021 10:05:35 +0700 Subject: [PATCH 10/44] refactor(web): encapsulates embedded popup-key lookup ops --- web/source/kmwembedded.ts | 34 ++++------------------ web/source/osk/embedded/subkeyDelegator.ts | 28 +++++++++++++++++- 2 files changed, 33 insertions(+), 29 deletions(-) diff --git a/web/source/kmwembedded.ts b/web/source/kmwembedded.ts index 24524ef150..9644141d10 100644 --- a/web/source/kmwembedded.ts +++ b/web/source/kmwembedded.ts @@ -297,7 +297,7 @@ namespace com.keyman.text { if(!isVisible) { if(delegator) { - delegator.resolve(null); + delegator.resolve(null, null); osk.vkbd.subkeyDelegator = null; } } @@ -377,43 +377,21 @@ namespace com.keyman.text { keymanweb.domManager.initActiveElement(Lelem); var delegator: com.keyman.osk.embedded.SubkeyDelegator = null; - let selectedKey: com.keyman.osk.OSKKeySpec = null; // This should be set if we're within this method... but it's best to guard against nulls here, just in case. if(osk.vkbd.subkeyDelegator) { delegator = osk.vkbd.subkeyDelegator as com.keyman.osk.embedded.SubkeyDelegator; - // This is set with the base key of our current subkey elsewhere within the engine. - var baseKey: com.keyman.osk.OSKKeySpec = delegator.baseKey.key.spec; - var found = false; - - if(baseKey.coreID == keyName) { - selectedKey = baseKey; - found = true; - } else { - // ... yeah, there are some funky type shenanigans between the two. - // OSKKeySpec is the OSK's... reinterpretation of the ActiveKey type. - selectedKey = (baseKey as com.keyman.keyboards.ActiveKey).getSubkey(keyName) as com.keyman.osk.OSKKeySpec; - found = !!selectedKey; - } - - if(!found) { - console.warn("Could not find subkey '" + origArg + "' under the current base key '" + baseKey.coreID + "'!"); + try { + delegator.resolve(keyName, osk.vkbd); + } catch (e) { + let err = e as Error; + console.warn(err.message); } osk.vkbd.subkeyDelegator = null; } else { console.warn("No base key exists for the subkey being executed: '" + origArg + "'"); } - - let Lkc: com.keyman.text.KeyEvent = null; - if(selectedKey) { - Lkc = osk.vkbd.keyEventFromSpec(selectedKey as com.keyman.keyboards.ActiveKey, null); - Lkc.vkCode=Lkc.Lcode; - } - - if(delegator) { - delegator.resolve(Lkc); - } }; /** diff --git a/web/source/osk/embedded/subkeyDelegator.ts b/web/source/osk/embedded/subkeyDelegator.ts index 0005f47449..3b6b844b3f 100644 --- a/web/source/osk/embedded/subkeyDelegator.ts +++ b/web/source/osk/embedded/subkeyDelegator.ts @@ -14,8 +14,34 @@ namespace com.keyman.osk.embedded { this.baseKey = e; } - public resolve(keyEvent: text.KeyEvent) { + public resolve(keyCoreID: string, vkbd: VisualKeyboard) { if(this.resolver) { + // This is set with the base key of our current subkey elsewhere within the engine. + var baseKey: OSKKeySpec = this.baseKey.key.spec; + var found = false; + let selectedKey: OSKKeySpec; + + if(baseKey.coreID == keyCoreID) { + selectedKey = baseKey; + found = true; + } else { + // ... yeah, there are some funky type shenanigans between the two. + // OSKKeySpec is the OSK's... reinterpretation of the ActiveKey type. + selectedKey = (baseKey as keyboards.ActiveKey).getSubkey(keyCoreID) as OSKKeySpec; + found = !!selectedKey; + } + + if(!found) { + this.resolver(null); // Maintains existing behavior. + throw new Error("Could not find subkey '" + keyCoreID + "' under base key '" + baseKey.coreID + "'!"); + } + + let keyEvent: text.KeyEvent = null; + if(selectedKey) { + keyEvent = vkbd.keyEventFromSpec(selectedKey as keyboards.ActiveKey, null); + keyEvent.vkCode=keyEvent.Lcode; + } + this.resolver(keyEvent); } this.resolver = null; From 39ec2cbb2ead4d4179e80affcd7b3e40e7bc10b8 Mon Sep 17 00:00:00 2001 From: jahorton Date: Thu, 24 Jun 2021 10:15:49 +0700 Subject: [PATCH 11/44] change(web): embedded longpress tracks source vkbd --- web/source/kmwembedded.ts | 6 +++--- web/source/osk/embedded/pendingLongpress.ts | 6 ++++-- web/source/osk/embedded/subkeyDelegator.ts | 9 ++++++--- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/web/source/kmwembedded.ts b/web/source/kmwembedded.ts index 9644141d10..4064dd9149 100644 --- a/web/source/kmwembedded.ts +++ b/web/source/kmwembedded.ts @@ -62,7 +62,7 @@ namespace com.keyman.osk { // #3718: No longer prepend base key to subkey array let _this = this; - let pendingLongpress = new embedded.PendingLongpress(key); + let pendingLongpress = new embedded.PendingLongpress(this, key); pendingLongpress.promise.then(function(delegator) { _this.subkeyDelegator = delegator; if(delegator) { @@ -297,7 +297,7 @@ namespace com.keyman.text { if(!isVisible) { if(delegator) { - delegator.resolve(null, null); + delegator.resolve(null); osk.vkbd.subkeyDelegator = null; } } @@ -382,7 +382,7 @@ namespace com.keyman.text { delegator = osk.vkbd.subkeyDelegator as com.keyman.osk.embedded.SubkeyDelegator; try { - delegator.resolve(keyName, osk.vkbd); + delegator.resolve(keyName); } catch (e) { let err = e as Error; console.warn(err.message); diff --git a/web/source/osk/embedded/pendingLongpress.ts b/web/source/osk/embedded/pendingLongpress.ts index bff829a472..8aa1983bc7 100644 --- a/web/source/osk/embedded/pendingLongpress.ts +++ b/web/source/osk/embedded/pendingLongpress.ts @@ -3,11 +3,13 @@ namespace com.keyman.osk.embedded { export class PendingLongpress { private resolver: (delegator: SubkeyDelegator) => void; + private readonly vkbd: VisualKeyboard; public readonly baseKey: KeyElement; public readonly promise: Promise; - constructor(e: KeyElement) { + constructor(vkbd: VisualKeyboard, e: KeyElement) { + this.vkbd = vkbd; let _this = this; this.promise = new Promise(function(resolve) { _this.resolver = resolve; @@ -17,7 +19,7 @@ namespace com.keyman.osk.embedded { public markActiveSubkeys() { if(this.resolver) { - this.resolver(new SubkeyDelegator(this.baseKey)); + this.resolver(new SubkeyDelegator(this.vkbd, this.baseKey)); } this.resolver = null; } diff --git a/web/source/osk/embedded/subkeyDelegator.ts b/web/source/osk/embedded/subkeyDelegator.ts index 3b6b844b3f..719d655ffe 100644 --- a/web/source/osk/embedded/subkeyDelegator.ts +++ b/web/source/osk/embedded/subkeyDelegator.ts @@ -1,11 +1,14 @@ namespace com.keyman.osk.embedded { export class SubkeyDelegator { private resolver: (keyEvent: text.KeyEvent) => void; + private readonly vkbd: VisualKeyboard; public readonly baseKey: KeyElement; public readonly promise: Promise; - constructor(e: KeyElement) { + constructor(vkbd: VisualKeyboard, e: KeyElement) { + this.vkbd = vkbd; + let _this = this; this.promise = new Promise(function(resolve) { _this.resolver = resolve; @@ -14,7 +17,7 @@ namespace com.keyman.osk.embedded { this.baseKey = e; } - public resolve(keyCoreID: string, vkbd: VisualKeyboard) { + public resolve(keyCoreID: string) { if(this.resolver) { // This is set with the base key of our current subkey elsewhere within the engine. var baseKey: OSKKeySpec = this.baseKey.key.spec; @@ -38,7 +41,7 @@ namespace com.keyman.osk.embedded { let keyEvent: text.KeyEvent = null; if(selectedKey) { - keyEvent = vkbd.keyEventFromSpec(selectedKey as keyboards.ActiveKey, null); + keyEvent = this.vkbd.keyEventFromSpec(selectedKey as keyboards.ActiveKey, null); keyEvent.vkCode=keyEvent.Lcode; } From 0359a68c154e37de15d4dfd6a5580aee50cca96a Mon Sep 17 00:00:00 2001 From: jahorton Date: Wed, 23 Jun 2021 14:24:39 +0700 Subject: [PATCH 12/44] fix(web): prevent error when attempting to reselect base key (embedded) --- web/source/osk/embedded/subkeyDelegator.ts | 49 ++++++++++++---------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/web/source/osk/embedded/subkeyDelegator.ts b/web/source/osk/embedded/subkeyDelegator.ts index 719d655ffe..ee70bbccc7 100644 --- a/web/source/osk/embedded/subkeyDelegator.ts +++ b/web/source/osk/embedded/subkeyDelegator.ts @@ -19,30 +19,33 @@ namespace com.keyman.osk.embedded { public resolve(keyCoreID: string) { if(this.resolver) { - // This is set with the base key of our current subkey elsewhere within the engine. - var baseKey: OSKKeySpec = this.baseKey.key.spec; - var found = false; - let selectedKey: OSKKeySpec; - - if(baseKey.coreID == keyCoreID) { - selectedKey = baseKey; - found = true; - } else { - // ... yeah, there are some funky type shenanigans between the two. - // OSKKeySpec is the OSK's... reinterpretation of the ActiveKey type. - selectedKey = (baseKey as keyboards.ActiveKey).getSubkey(keyCoreID) as OSKKeySpec; - found = !!selectedKey; - } - - if(!found) { - this.resolver(null); // Maintains existing behavior. - throw new Error("Could not find subkey '" + keyCoreID + "' under base key '" + baseKey.coreID + "'!"); - } - let keyEvent: text.KeyEvent = null; - if(selectedKey) { - keyEvent = this.vkbd.keyEventFromSpec(selectedKey as keyboards.ActiveKey, null); - keyEvent.vkCode=keyEvent.Lcode; + + if(keyCoreID != null) { + // This is set with the base key of our current subkey elsewhere within the engine. + var baseKey: OSKKeySpec = this.baseKey.key.spec; + var found = false; + let selectedKey: OSKKeySpec; + + if(baseKey.coreID == keyCoreID) { + selectedKey = baseKey; + found = true; + } else { + // ... yeah, there are some funky type shenanigans between the two. + // OSKKeySpec is the OSK's... reinterpretation of the ActiveKey type. + selectedKey = (baseKey as keyboards.ActiveKey).getSubkey(keyCoreID) as OSKKeySpec; + found = !!selectedKey; + } + + if(!found) { + this.resolver(null); // Maintains existing behavior. + throw new Error("Could not find subkey '" + keyCoreID + "' under base key '" + baseKey.coreID + "'!"); + } + + if(selectedKey) { + keyEvent = this.vkbd.keyEventFromSpec(selectedKey as keyboards.ActiveKey, null); + keyEvent.vkCode=keyEvent.Lcode; + } } this.resolver(keyEvent); From 36536f89bc398b05d55dc3e14509101a15142d58 Mon Sep 17 00:00:00 2001 From: jahorton Date: Thu, 17 Jun 2021 16:26:50 +0700 Subject: [PATCH 13/44] refactor(web): browser subkey control flow abstraction prep --- web/source/kmwembedded.ts | 2 - web/source/osk/browser/pendingLongpress.ts | 60 ++++++++++++++ web/source/osk/browser/subkeyPopup.ts | 10 ++- web/source/osk/visualKeyboard.ts | 95 ++++++++-------------- 4 files changed, 104 insertions(+), 63 deletions(-) create mode 100644 web/source/osk/browser/pendingLongpress.ts diff --git a/web/source/kmwembedded.ts b/web/source/kmwembedded.ts index 4064dd9149..d1d54dbc65 100644 --- a/web/source/kmwembedded.ts +++ b/web/source/kmwembedded.ts @@ -150,8 +150,6 @@ namespace com.keyman.text { // Skip full page initialization - skips native-mode only code keymanweb.isEmbedded = true; - com.keyman.osk.VisualKeyboard.prototype.popupDelay = 400; // Delay must be less than native touch-hold delay - // Set default device options keymanweb.setDefaultDeviceOptions = function(opt) { opt['attachType'] = 'manual'; diff --git a/web/source/osk/browser/pendingLongpress.ts b/web/source/osk/browser/pendingLongpress.ts new file mode 100644 index 0000000000..038478924b --- /dev/null +++ b/web/source/osk/browser/pendingLongpress.ts @@ -0,0 +1,60 @@ +/// + +namespace com.keyman.osk.browser { + export class PendingLongpress { + public readonly baseKey: KeyElement; + //public readonly initialTouch: Touch; + + public readonly subkeyUI: SubkeyPopup; + + public readonly promise: Promise; + + private readonly vkbd: VisualKeyboard; + private resolver: (subkeyPopup: SubkeyPopup) => void; + + private timerId: number; + private popupDelay: number = 500; + + constructor(vkbd: VisualKeyboard, baseKey: KeyElement/*, initialTouch: Touch*/) { + this.vkbd = vkbd; + this.baseKey = baseKey; + //this.initialTouch = initialTouch; + + let _this = this; + this.promise = new Promise(function(resolve, reject) { + _this.timerId = window.setTimeout( + function() { + // It's no longer deferred; it's being fulfilled. + // Even if the actual subkey itself is still async. + _this.showSubkeys(); + }, _this.popupDelay); + }); + + this.promise = new Promise(function(resolve) { + _this.resolver = resolve; + }); + } + + updateTouch(touch: Touch) { + this.subkeyUI.updateTouch(touch); + } + + public cancel() { + if(this.timerId) { + window.clearTimeout(this.timerId); + this.timerId = null; + } + + if(this.resolver) { + this.resolver(null); + this.resolver = null; + } + } + + public showSubkeys() { + if(this.resolver) { + this.resolver(new SubkeyPopup(this.vkbd, this.baseKey)); + } + } + } +} \ No newline at end of file diff --git a/web/source/osk/browser/subkeyPopup.ts b/web/source/osk/browser/subkeyPopup.ts index 7c7ecdf260..3c52532399 100644 --- a/web/source/osk/browser/subkeyPopup.ts +++ b/web/source/osk/browser/subkeyPopup.ts @@ -11,12 +11,18 @@ namespace com.keyman.osk.browser { private callout: HTMLDivElement; + public promise: Promise; + // Resolves the promise that generated this SubkeyPopup. private resolver: (keyEvent: text.KeyEvent) => void; - constructor(vkbd: VisualKeyboard, e: KeyElement, resolve: (keyEvent: text.KeyEvent) => void) { + constructor(vkbd: VisualKeyboard, e: KeyElement) { let keyman = com.keyman.singleton; - this.resolver = resolve; + let _this = this; + + this.promise = new Promise(function(resolve) { + _this.resolver = resolve; + }) this.vkbd = vkbd; this.baseKey = e; diff --git a/web/source/osk/visualKeyboard.ts b/web/source/osk/visualKeyboard.ts index d0b2681797..a7737be260 100644 --- a/web/source/osk/visualKeyboard.ts +++ b/web/source/osk/visualKeyboard.ts @@ -3,14 +3,9 @@ /// /// /// -/// +/// namespace com.keyman.osk { - interface SubkeyDeferment { - timerId: number; - resolve: (KeyEvent) => void; - } - export class VisualKeyboard { // Legacy alias, maintaining a reference for code built against older // versions of KMW. @@ -64,11 +59,10 @@ namespace com.keyman.osk { currentTarget: KeyElement; // Popup key management - popupDelay: number = 500; - subkeyDelayTimer: number; menuEvent: KeyElement; // Used by embedded-mode. keytip: KeyTip; subkeyPopup: browser.SubkeyPopup; + browserPendingLongpress: browser.PendingLongpress; embeddedPendingLongpress: any; // a temp, intermediate property during subkey abstraction work subkeyDelegator: any; // a temp, intermediate property during subkey abstraction work @@ -1059,37 +1053,11 @@ namespace com.keyman.osk { this.subkeyPopup = null; } - if(this.subkeyDelayTimer) { - window.clearTimeout(this.subkeyDelayTimer); - this.subkeyDelayTimer = null; + if(this.browserPendingLongpress) { + this.browserPendingLongpress.cancel(); } } - //#region 'native'-mode subkey handling - /** - * Display touch-hold array of 'sub-keys' above the currently touched key - * @param {Object} e primary key element - */ - showSubKeys(e: KeyElement, resolve: (KeyElement) => void) { - // Do not show subkeys if key already released - if(this.keyPending == null) { - return; - } - - // Clear key preview if any - this.showKeyTip(null,false); - - let subKeys = this.subkeyPopup = new browser.SubkeyPopup(this, e, resolve); - - // Otherwise append the touch-hold (subkey) array to the OSK - let keyman = com.keyman.singleton; - keyman.osk._Box.appendChild(subKeys.element); - keyman.osk._Box.appendChild(subKeys.shim); - - // Must be placed after its `.element` has been inserted into the DOM. - subKeys.reposition(this); - } - //#endregion /** @@ -1661,37 +1629,46 @@ namespace com.keyman.osk { */ touchHold(key: KeyElement, force?: boolean) { // Clear and restart the popup timer - if(this.subkeyDelayTimer) { - window.clearTimeout(this.subkeyDelayTimer); - // Cancel the potential subkey event. - this.subkeyDelayTimer = null; + if(this.browserPendingLongpress) { + this.browserPendingLongpress.cancel(); } if(typeof key['subKeys'] != 'undefined' && key['subKeys'] != null) { let _this = this; - // This Promise receives the user's selected subkey, should it exist. - // Note: the OSK's use of this Promise will allow for passive cancellation. - // If cancelled (so, `null`), the Promise ought remain unresolved. - let promise = new Promise(function(resolve, reject) { - let timerId = window.setTimeout( - function() { - // It's no longer deferred; it's being fulfilled. - // Even if the actual subkey itself is still async. - _this.subkeyDelayTimer = null; - _this.clearPopup(); - _this.showSubKeys(key, resolve); - }, force ? 0 : _this.popupDelay); + // First-level object/Promise: will produce a subkey popup when the longpress gesture completes. + // 'Returns' a second-level object/Promise: resolves when a subkey is selected or is cancelled. + this.browserPendingLongpress = new browser.PendingLongpress(this, key); + this.browserPendingLongpress.promise.then(function(subkeyPopup) { + _this.browserPendingLongpress = null; + if(subkeyPopup) { + // Clear key preview if any + _this.showKeyTip(null,false); - _this.subkeyDelayTimer = timerId; - }).then(function(keyEvent: text.KeyEvent) { - // Allow active cancellation, even if the source should allow passive. - // It's an easy and cheap null guard. - if(keyEvent) { - PreProcessor.raiseKeyEvent(keyEvent); + _this.subkeyPopup = subkeyPopup; + subkeyPopup.promise.then(function(keyEvent: text.KeyEvent) { + // Allow active cancellation, even if the source should allow passive. + // It's an easy and cheap null guard. + if(keyEvent) { + PreProcessor.raiseKeyEvent(keyEvent); + } + _this.clearPopup(); + }); + + // Otherwise append the touch-hold (subkey) array to the OSK + let keyman = com.keyman.singleton; + keyman.osk._Box.appendChild(subkeyPopup.element); + keyman.osk._Box.appendChild(subkeyPopup.shim); + + // Must be placed after its `.element` has been inserted into the DOM. + subkeyPopup.reposition(_this); } - _this.clearPopup(); }); + + if(force) { + // Instantly resolves the first-level promise. + this.browserPendingLongpress.showSubkeys(); + } } }; From 2015ef792768bed8a6a61123b4350e6ec8fa3425 Mon Sep 17 00:00:00 2001 From: jahorton Date: Thu, 17 Jun 2021 16:27:53 +0700 Subject: [PATCH 14/44] chore(web): mild formatting tweak --- web/source/osk/visualKeyboard.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/web/source/osk/visualKeyboard.ts b/web/source/osk/visualKeyboard.ts index a7737be260..25e21c89d4 100644 --- a/web/source/osk/visualKeyboard.ts +++ b/web/source/osk/visualKeyboard.ts @@ -1655,13 +1655,13 @@ namespace com.keyman.osk { _this.clearPopup(); }); - // Otherwise append the touch-hold (subkey) array to the OSK - let keyman = com.keyman.singleton; - keyman.osk._Box.appendChild(subkeyPopup.element); - keyman.osk._Box.appendChild(subkeyPopup.shim); + // Otherwise append the touch-hold (subkey) array to the OSK + let keyman = com.keyman.singleton; + keyman.osk._Box.appendChild(subkeyPopup.element); + keyman.osk._Box.appendChild(subkeyPopup.shim); - // Must be placed after its `.element` has been inserted into the DOM. - subkeyPopup.reposition(_this); + // Must be placed after its `.element` has been inserted into the DOM. + subkeyPopup.reposition(_this); } }); From 3a982049197871036549f522c05287db9fbb399c Mon Sep 17 00:00:00 2001 From: jahorton Date: Wed, 23 Jun 2021 10:50:36 +0700 Subject: [PATCH 15/44] fix(web): pending-longpress field management --- web/source/osk/browser/pendingLongpress.ts | 5 +---- web/source/osk/visualKeyboard.ts | 10 ++++++++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/web/source/osk/browser/pendingLongpress.ts b/web/source/osk/browser/pendingLongpress.ts index 038478924b..f55b0a2ef8 100644 --- a/web/source/osk/browser/pendingLongpress.ts +++ b/web/source/osk/browser/pendingLongpress.ts @@ -22,6 +22,7 @@ namespace com.keyman.osk.browser { let _this = this; this.promise = new Promise(function(resolve, reject) { + _this.resolver = resolve; _this.timerId = window.setTimeout( function() { // It's no longer deferred; it's being fulfilled. @@ -29,10 +30,6 @@ namespace com.keyman.osk.browser { _this.showSubkeys(); }, _this.popupDelay); }); - - this.promise = new Promise(function(resolve) { - _this.resolver = resolve; - }); } updateTouch(touch: Touch) { diff --git a/web/source/osk/visualKeyboard.ts b/web/source/osk/visualKeyboard.ts index 25e21c89d4..630b8945f2 100644 --- a/web/source/osk/visualKeyboard.ts +++ b/web/source/osk/visualKeyboard.ts @@ -1055,6 +1055,7 @@ namespace com.keyman.osk { if(this.browserPendingLongpress) { this.browserPendingLongpress.cancel(); + this.browserPendingLongpress = null; } } @@ -1631,6 +1632,7 @@ namespace com.keyman.osk { // Clear and restart the popup timer if(this.browserPendingLongpress) { this.browserPendingLongpress.cancel(); + this.browserPendingLongpress = null; } if(typeof key['subKeys'] != 'undefined' && key['subKeys'] != null) { @@ -1638,9 +1640,13 @@ namespace com.keyman.osk { // First-level object/Promise: will produce a subkey popup when the longpress gesture completes. // 'Returns' a second-level object/Promise: resolves when a subkey is selected or is cancelled. - this.browserPendingLongpress = new browser.PendingLongpress(this, key); + let pl = this.browserPendingLongpress = new browser.PendingLongpress(this, key); this.browserPendingLongpress.promise.then(function(subkeyPopup) { - _this.browserPendingLongpress = null; + // Clear the longpress field upon any sort of fulfillment if it is still the current one. + if(_this.browserPendingLongpress == pl) { + _this.browserPendingLongpress = null; + } + if(subkeyPopup) { // Clear key preview if any _this.showKeyTip(null,false); From c3748202631512c301e9f6d096e5b266a82e81c3 Mon Sep 17 00:00:00 2001 From: jahorton Date: Thu, 17 Jun 2021 14:33:13 +0700 Subject: [PATCH 16/44] refactor(web): in-browser subkey tracking managed by SubkeyPopup --- web/source/osk/visualKeyboard.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/web/source/osk/visualKeyboard.ts b/web/source/osk/visualKeyboard.ts index 630b8945f2..9b65e59347 100644 --- a/web/source/osk/visualKeyboard.ts +++ b/web/source/osk/visualKeyboard.ts @@ -1669,6 +1669,7 @@ namespace com.keyman.osk { // Must be placed after its `.element` has been inserted into the DOM. subkeyPopup.reposition(_this); } + _this.clearPopup(); }); if(force) { From 0d109b12b0e860ffc207c700d415ee0e7d96e6fa Mon Sep 17 00:00:00 2001 From: jahorton Date: Tue, 22 Jun 2021 10:45:17 +0700 Subject: [PATCH 17/44] refactor(web): 'easy' part of abstracted RealizedGesture for subkeys --- web/source/kmwembedded.ts | 23 +++++----- web/source/osk/banner.ts | 2 +- web/source/osk/browser/pendingLongpress.ts | 7 +-- web/source/osk/browser/subkeyPopup.ts | 13 ++++-- web/source/osk/embedded/subkeyDelegator.ts | 12 +++++- web/source/osk/realizedGesture.interface.ts | 9 ++++ web/source/osk/visualKeyboard.ts | 48 +++++++++------------ 7 files changed, 63 insertions(+), 51 deletions(-) create mode 100644 web/source/osk/realizedGesture.interface.ts diff --git a/web/source/kmwembedded.ts b/web/source/kmwembedded.ts index d1d54dbc65..0f209dd726 100644 --- a/web/source/kmwembedded.ts +++ b/web/source/kmwembedded.ts @@ -63,11 +63,11 @@ namespace com.keyman.osk { let _this = this; let pendingLongpress = new embedded.PendingLongpress(this, key); - pendingLongpress.promise.then(function(delegator) { - _this.subkeyDelegator = delegator; - if(delegator) { - delegator.promise.then(function(keyEvent) { - _this.subkeyDelegator = null; + pendingLongpress.promise.then(function(gesture) { + _this.subkeyGesture = gesture; + if(gesture) { + gesture.promise.then(function(keyEvent) { + _this.subkeyGesture = null; // Allow active cancellation, even if the source should allow passive. // It's an easy and cheap null guard. if(keyEvent) { @@ -290,13 +290,13 @@ namespace com.keyman.text { * **/ keymanweb['popupVisible'] = function(isVisible) { - let delegator = osk.vkbd.subkeyDelegator as com.keyman.osk.embedded.SubkeyDelegator; + let delegator = osk.vkbd.subkeyGesture as com.keyman.osk.embedded.SubkeyDelegator; let pendingLongpress = osk.vkbd.embeddedPendingLongpress as com.keyman.osk.embedded.PendingLongpress; if(!isVisible) { if(delegator) { delegator.resolve(null); - osk.vkbd.subkeyDelegator = null; + osk.vkbd.subkeyGesture = null; } } @@ -374,19 +374,18 @@ namespace com.keyman.text { keymanweb.domManager.initActiveElement(Lelem); - var delegator: com.keyman.osk.embedded.SubkeyDelegator = null; // This should be set if we're within this method... but it's best to guard against nulls here, just in case. - if(osk.vkbd.subkeyDelegator) { - delegator = osk.vkbd.subkeyDelegator as com.keyman.osk.embedded.SubkeyDelegator; + if(osk.vkbd.subkeyGesture) { + let gesture = osk.vkbd.subkeyGesture as com.keyman.osk.embedded.SubkeyDelegator; try { - delegator.resolve(keyName); + gesture.resolve(keyName); } catch (e) { let err = e as Error; console.warn(err.message); } - osk.vkbd.subkeyDelegator = null; + osk.vkbd.subkeyGesture = null; } else { console.warn("No base key exists for the subkey being executed: '" + origArg + "'"); } diff --git a/web/source/osk/banner.ts b/web/source/osk/banner.ts index 3eaba765a2..0ba00a3920 100644 --- a/web/source/osk/banner.ts +++ b/web/source/osk/banner.ts @@ -493,7 +493,7 @@ namespace com.keyman.osk { // Utilized by the mobile apps; allows them to 'take over' touch handling, // blocking it within KMW when the apps are already managing an ongoing touch-hold. let keyman = com.keyman.singleton; - return keyman['osk'].vkbd.subkeyDelegator; + return keyman['osk'].vkbd.subkeyGesture && keyman.isEmbedded; } protected dealiasSubTarget(target: HTMLDivElement): HTMLDivElement { diff --git a/web/source/osk/browser/pendingLongpress.ts b/web/source/osk/browser/pendingLongpress.ts index f55b0a2ef8..08f89caca0 100644 --- a/web/source/osk/browser/pendingLongpress.ts +++ b/web/source/osk/browser/pendingLongpress.ts @@ -3,12 +3,11 @@ namespace com.keyman.osk.browser { export class PendingLongpress { public readonly baseKey: KeyElement; + public readonly promise: Promise; //public readonly initialTouch: Touch; public readonly subkeyUI: SubkeyPopup; - public readonly promise: Promise; - private readonly vkbd: VisualKeyboard; private resolver: (subkeyPopup: SubkeyPopup) => void; @@ -32,10 +31,6 @@ namespace com.keyman.osk.browser { }); } - updateTouch(touch: Touch) { - this.subkeyUI.updateTouch(touch); - } - public cancel() { if(this.timerId) { window.clearTimeout(this.timerId); diff --git a/web/source/osk/browser/subkeyPopup.ts b/web/source/osk/browser/subkeyPopup.ts index 3c52532399..4b35fa09ab 100644 --- a/web/source/osk/browser/subkeyPopup.ts +++ b/web/source/osk/browser/subkeyPopup.ts @@ -1,17 +1,18 @@ /// +/// namespace com.keyman.osk.browser { - export class SubkeyPopup { + export class SubkeyPopup implements RealizedGesture { public readonly element: HTMLDivElement; public readonly shim: HTMLDivElement; private vkbd: VisualKeyboard; - public readonly baseKey: KeyElement; private currentSelection: KeyElement; - + private callout: HTMLDivElement; - public promise: Promise; + public readonly baseKey: KeyElement; + public readonly promise: Promise; // Resolves the promise that generated this SubkeyPopup. private resolver: (keyEvent: text.KeyEvent) => void; @@ -209,6 +210,10 @@ namespace com.keyman.osk.browser { } } + isVisible(): boolean { + return this.element.style.visibility == 'visible'; + } + clear() { // Discard the reference to the Promise's resolve method, allowing // GC to clean it up. The corresponding Promise's contract allows diff --git a/web/source/osk/embedded/subkeyDelegator.ts b/web/source/osk/embedded/subkeyDelegator.ts index ee70bbccc7..73b29efc22 100644 --- a/web/source/osk/embedded/subkeyDelegator.ts +++ b/web/source/osk/embedded/subkeyDelegator.ts @@ -1,5 +1,7 @@ +/// + namespace com.keyman.osk.embedded { - export class SubkeyDelegator { + export class SubkeyDelegator implements RealizedGesture { private resolver: (keyEvent: text.KeyEvent) => void; private readonly vkbd: VisualKeyboard; @@ -52,5 +54,13 @@ namespace com.keyman.osk.embedded { } this.resolver = null; } + + public isVisible(): boolean { + return true; + } + + public clear() { + // no-op; it's fully controlled on the app side. + } } } \ No newline at end of file diff --git a/web/source/osk/realizedGesture.interface.ts b/web/source/osk/realizedGesture.interface.ts new file mode 100644 index 0000000000..6b18cff188 --- /dev/null +++ b/web/source/osk/realizedGesture.interface.ts @@ -0,0 +1,9 @@ +namespace com.keyman.osk { + export interface RealizedGesture { + readonly baseKey: KeyElement; + readonly promise: Promise; + + clear(): void; + isVisible(): boolean; + } +} \ No newline at end of file diff --git a/web/source/osk/visualKeyboard.ts b/web/source/osk/visualKeyboard.ts index 9b65e59347..6a05671312 100644 --- a/web/source/osk/visualKeyboard.ts +++ b/web/source/osk/visualKeyboard.ts @@ -61,10 +61,9 @@ namespace com.keyman.osk { // Popup key management menuEvent: KeyElement; // Used by embedded-mode. keytip: KeyTip; - subkeyPopup: browser.SubkeyPopup; browserPendingLongpress: browser.PendingLongpress; - embeddedPendingLongpress: any; // a temp, intermediate property during subkey abstraction work - subkeyDelegator: any; // a temp, intermediate property during subkey abstraction work + embeddedPendingLongpress: any; // a temp, intermediate property during subkey abstraction work + subkeyGesture: RealizedGesture; get layerId(): string { return this._layerId; @@ -423,7 +422,7 @@ namespace com.keyman.osk { let kbdAspectRatio = layerGroup.offsetWidth / this.kbdDiv.offsetHeight; let baseKeyProbabilities = this.layout.getLayer(this.layerId).getTouchProbabilities(touchKbdPos, kbdAspectRatio); - if(!this.subkeyPopup || !this.subkeyPopup.baseKey.key) { + if(!this.subkeyGesture || !this.subkeyGesture.baseKey.key) { return baseKeyProbabilities; } else { // A temp-hack, as this was noted just before 14.0's release. @@ -435,7 +434,7 @@ namespace com.keyman.osk { let baseMass = 1.0; let baseKeyMass = 1.0; - let baseKeyID = this.subkeyPopup.baseKey.key.spec.coreID; + let baseKeyID = this.subkeyGesture.baseKey.key.spec.coreID; let popupKeyMass = 0.0; let popupKeyID: string = null; @@ -512,7 +511,7 @@ namespace com.keyman.osk { // Prevent multi-touch if popup displayed var sk = document.getElementById('kmw-popup-keys'); - if((sk && sk.style.visibility == 'visible') || this.subkeyDelegator) { + if((sk && sk.style.visibility == 'visible') || this.subkeyGesture) { return; } @@ -559,9 +558,10 @@ namespace com.keyman.osk { if(this.keyPending) { this.highlightKey(this.keyPending, false); - if(this.subkeyPopup) { - this.subkeyPopup.updateTouch(e.changedTouches[0]); - this.subkeyPopup.finalize(e.changedTouches[0]); + if(this.subkeyGesture && this.subkeyGesture instanceof browser.SubkeyPopup) { + let subkeyPopup = this.subkeyGesture as browser.SubkeyPopup; + subkeyPopup.updateTouch(e.changedTouches[0]); + subkeyPopup.finalize(e.changedTouches[0]); } else { this.modelKeyClick(this.keyPending, this.touchPending); } @@ -585,18 +585,21 @@ namespace com.keyman.osk { **/ release: (e: TouchEvent) => void = function(this: VisualKeyboard, e: TouchEvent) { // Prevent incorrect multi-touch behaviour if native or device popup visible - var sk = document.getElementById('kmw-popup-keys'), t = this.currentTarget; + var t = this.currentTarget; // Clear repeated backspace if active, preventing 'sticky' behavior. this.cancelDelete(); - if((this.subkeyPopup && this.subkeyPopup.element.style.visibility == 'visible')) { + if((this.subkeyGesture && this.subkeyGesture.isVisible())) { // Ignore release if a multiple touch if(e.touches.length > 0) { return; } - this.subkeyPopup.finalize(e.changedTouches[0]); + if(this.subkeyGesture instanceof browser.SubkeyPopup) { + let subkeyPopup = this.subkeyGesture as browser.SubkeyPopup; + subkeyPopup.finalize(e.changedTouches[0]); + } this.highlightKey(this.keyPending,false); this.keyPending = null; this.touchPending = null; @@ -604,15 +607,6 @@ namespace com.keyman.osk { return; } - // Only set when embedded in our Android/iOS app. Signals that the device is handling - // subkeys, so we shouldn't allow output for the base key. - // - // Note that on iOS (at least), this.release() will trigger before kmwembedded.ts's - // executePopupKey() function. - if(this.subkeyDelegator) { - return; - } - // Handle menu key release event if(t && t.id) { this.optionKey(t, t.id, false); @@ -1048,9 +1042,9 @@ namespace com.keyman.osk { clearPopup() { // Remove the displayed subkey array, if any, and cancel popup request - if(this.subkeyPopup) { - this.subkeyPopup.clear(); - this.subkeyPopup = null; + if(this.subkeyGesture) { + this.subkeyGesture.clear(); + this.subkeyGesture = null; } if(this.browserPendingLongpress) { @@ -1651,7 +1645,7 @@ namespace com.keyman.osk { // Clear key preview if any _this.showKeyTip(null,false); - _this.subkeyPopup = subkeyPopup; + _this.subkeyGesture = subkeyPopup; subkeyPopup.promise.then(function(keyEvent: text.KeyEvent) { // Allow active cancellation, even if the source should allow passive. // It's an easy and cheap null guard. @@ -1734,8 +1728,8 @@ namespace com.keyman.osk { return; } - var sk=this.subkeyPopup, - popup = (sk && sk.element.style.visibility == 'visible') + let sk = this.subkeyGesture; + let popup = (sk && sk.isVisible()); // If popup keys are active, do not show the key tip. on = popup ? false : on; From d5e563524292a623dac041f943cea67bc6192519 Mon Sep 17 00:00:00 2001 From: jahorton Date: Tue, 22 Jun 2021 12:00:25 +0700 Subject: [PATCH 18/44] refactor(web): merged subkey highlighting control --- web/source/osk/browser/subkeyPopup.ts | 10 +---- web/source/osk/embedded/subkeyDelegator.ts | 4 ++ web/source/osk/oskKey.ts | 14 +++++++ web/source/osk/realizedGesture.interface.ts | 1 + web/source/osk/visualKeyboard.ts | 46 +++++++-------------- 5 files changed, 34 insertions(+), 41 deletions(-) diff --git a/web/source/osk/browser/subkeyPopup.ts b/web/source/osk/browser/subkeyPopup.ts index 4b35fa09ab..f927a8bd61 100644 --- a/web/source/osk/browser/subkeyPopup.ts +++ b/web/source/osk/browser/subkeyPopup.ts @@ -235,21 +235,13 @@ namespace com.keyman.osk.browser { } updateTouch(touch: Touch) { - let x = touch.clientX; - let y = touch.clientY; - this.currentSelection = null; for(let i=0; i < this.baseKey['subKeys'].length; i++) { try { let sk= this.element.childNodes[i].firstChild as KeyElement; - let x0 = dom.Utils.getAbsoluteX(sk); - let y0 = dom.Utils.getAbsoluteY(sk);//-document.body.scrollTop; - - let x1=x0+sk.offsetWidth; - let y1=y0+sk.offsetHeight; - let onKey=(x > x0 && x < x1 && y > y0 && y < y1); + let onKey = sk.key.isUnderTouch(touch) if(onKey) { this.baseKey.key.highlight(false); this.currentSelection = sk; diff --git a/web/source/osk/embedded/subkeyDelegator.ts b/web/source/osk/embedded/subkeyDelegator.ts index 73b29efc22..3fc2d33bb4 100644 --- a/web/source/osk/embedded/subkeyDelegator.ts +++ b/web/source/osk/embedded/subkeyDelegator.ts @@ -62,5 +62,9 @@ namespace com.keyman.osk.embedded { public clear() { // no-op; it's fully controlled on the app side. } + + updateTouch(touch: Touch) { + this.baseKey.key.highlight(this.baseKey.key.isUnderTouch(touch)); + } } } \ No newline at end of file diff --git a/web/source/osk/oskKey.ts b/web/source/osk/oskKey.ts index f9e4509992..da52968507 100644 --- a/web/source/osk/oskKey.ts +++ b/web/source/osk/oskKey.ts @@ -430,5 +430,19 @@ namespace com.keyman.osk { return t; } + + public isUnderTouch(touch: Touch): boolean { + let x = touch.clientX; + let y = touch.clientY; + + let btn = this.btn; + let x0 = dom.Utils.getAbsoluteX(btn); + let y0 = dom.Utils.getAbsoluteY(btn);//-document.body.scrollTop; + + let x1=x0 + btn.offsetWidth; + let y1=y0 + btn.offsetHeight; + + return (x > x0 && x < x1 && y > y0 && y < y1); + } } } \ No newline at end of file diff --git a/web/source/osk/realizedGesture.interface.ts b/web/source/osk/realizedGesture.interface.ts index 6b18cff188..877164898a 100644 --- a/web/source/osk/realizedGesture.interface.ts +++ b/web/source/osk/realizedGesture.interface.ts @@ -5,5 +5,6 @@ namespace com.keyman.osk { clear(): void; isVisible(): boolean; + updateTouch(touch: Touch): void; } } \ No newline at end of file diff --git a/web/source/osk/visualKeyboard.ts b/web/source/osk/visualKeyboard.ts index 6a05671312..ef4158c9de 100644 --- a/web/source/osk/visualKeyboard.ts +++ b/web/source/osk/visualKeyboard.ts @@ -510,8 +510,7 @@ namespace com.keyman.osk { this.cancelDelete(); // Prevent multi-touch if popup displayed - var sk = document.getElementById('kmw-popup-keys'); - if((sk && sk.style.visibility == 'visible') || this.subkeyGesture) { + if(this.subkeyGesture && this.subkeyGesture.isVisible()) { return; } @@ -697,45 +696,28 @@ namespace com.keyman.osk { return; } - // Clear previous key highlighting - if(!this.subkeyPopup && key0 && key1 && key1 !== key0) { - this.highlightKey(key0,false); - } - - // Do not move over keys if device popup visible - if(this.subkeyDelegator) { - if(key1 == null) { - if(key0) { - this.highlightKey(key0,false); - } - this.keyPending=null; - this.touchPending=null; - } else { - // Re-apply highlighting to the key if it's the current popup's base key. - if(this.subkeyDelegator && key1 == this.subkeyDelegator.baseKey) { - if(!key1.classList.contains('kmw-key-touched')) { - this.highlightKey(key1,true); - } - this.keyPending = key1; - this.touchPending = e.touches[0]; - } else { - if(key0) { - this.highlightKey(key0,false); - } - this.keyPending = null; - this.touchPending = null; - } + // Clear previous key highlighting, allow subkey controller to + // highlight as appropriate. + if(this.subkeyGesture) { + if(key0) { + key0.key.highlight(false); } + this.subkeyGesture.updateTouch(e.touches[0]); + + this.keyPending = null; + this.touchPending = null; + return; } this.currentTarget = null; // If popup is visible, need to move over popup, not over main keyboard + // TODO: responsible for the shortcutting gesture for early subkey display. this.highlightSubKeys(key1,x,y); - if(this.subkeyPopup) { - this.subkeyPopup.updateTouch(e.touches[0]); + // As the previous line can trigger the start of the subkey gesture... + if(this.subkeyGesture) { return; } From 60081b7c4a4be0d491556449c16410d8b97cd79c Mon Sep 17 00:00:00 2001 From: jahorton Date: Tue, 22 Jun 2021 13:14:11 +0700 Subject: [PATCH 19/44] refactor(web): initial mutual abstraction for pending longpresses --- web/source/kmwembedded.ts | 25 ++++---- web/source/osk/browser/pendingLongpress.ts | 7 ++- web/source/osk/embedded/pendingLongpress.ts | 13 +++- web/source/osk/pendingGesture.interface.ts | 9 +++ web/source/osk/visualKeyboard.ts | 67 ++++++++------------- 5 files changed, 60 insertions(+), 61 deletions(-) create mode 100644 web/source/osk/pendingGesture.interface.ts diff --git a/web/source/kmwembedded.ts b/web/source/kmwembedded.ts index 0f209dd726..2b7bae20f0 100644 --- a/web/source/kmwembedded.ts +++ b/web/source/kmwembedded.ts @@ -54,7 +54,6 @@ namespace com.keyman.osk { * @param {Object} key base key element */ VisualKeyboard.prototype.touchHold = function(this: VisualKeyboard, key: KeyElement) { - let util = com.keyman.singleton.util; if(key['subKeys'] && (typeof(window['oskCreatePopup']) == 'function')) { var xBase = dom.Utils.getAbsoluteX(key) - dom.Utils.getAbsoluteX(this.kbdDiv) + key.offsetWidth/2, yBase = dom.Utils.getAbsoluteY(key); @@ -62,6 +61,8 @@ namespace com.keyman.osk { // #3718: No longer prepend base key to subkey array let _this = this; + window['oskCreatePopup'](key['subKeys'], xBase, yBase, key.offsetWidth, key.offsetHeight); + let pendingLongpress = new embedded.PendingLongpress(this, key); pendingLongpress.promise.then(function(gesture) { _this.subkeyGesture = gesture; @@ -76,9 +77,7 @@ namespace com.keyman.osk { }); } }); - this.embeddedPendingLongpress = pendingLongpress; - - window['oskCreatePopup'](key['subKeys'], xBase, yBase, key.offsetWidth, key.offsetHeight); + this.pendingSubkey = pendingLongpress; } }; @@ -89,10 +88,6 @@ namespace com.keyman.osk { } }; - VisualKeyboard.prototype.highlightSubKeys = function(this: VisualKeyboard, k, x, y) { - // a dummy function; it's only really used for 'native' KMW. - } - VisualKeyboard.prototype.waitForFonts = function(this: VisualKeyboard, kfd, ofd) { // a dummy function; it's only really used for 'native' KMW. return true; @@ -290,19 +285,21 @@ namespace com.keyman.text { * **/ keymanweb['popupVisible'] = function(isVisible) { - let delegator = osk.vkbd.subkeyGesture as com.keyman.osk.embedded.SubkeyDelegator; - let pendingLongpress = osk.vkbd.embeddedPendingLongpress as com.keyman.osk.embedded.PendingLongpress; + let gesture = osk.vkbd.subkeyGesture as com.keyman.osk.embedded.SubkeyDelegator; + let pendingLongpress = osk.vkbd.pendingSubkey; if(!isVisible) { - if(delegator) { - delegator.resolve(null); + if(gesture) { + gesture.resolve(null); osk.vkbd.subkeyGesture = null; + } else if(pendingLongpress) { + pendingLongpress.cancel(); } } - if(isVisible && osk.vkbd.embeddedPendingLongpress) { + if(isVisible && pendingLongpress) { // Fulfills the first-stage promise. - pendingLongpress.markActiveSubkeys(); + pendingLongpress.resolve(); } }; diff --git a/web/source/osk/browser/pendingLongpress.ts b/web/source/osk/browser/pendingLongpress.ts index 08f89caca0..9e708c579f 100644 --- a/web/source/osk/browser/pendingLongpress.ts +++ b/web/source/osk/browser/pendingLongpress.ts @@ -1,7 +1,8 @@ /// +/// namespace com.keyman.osk.browser { - export class PendingLongpress { + export class PendingLongpress implements PendingGesture { public readonly baseKey: KeyElement; public readonly promise: Promise; //public readonly initialTouch: Touch; @@ -26,7 +27,7 @@ namespace com.keyman.osk.browser { function() { // It's no longer deferred; it's being fulfilled. // Even if the actual subkey itself is still async. - _this.showSubkeys(); + _this.resolve(); }, _this.popupDelay); }); } @@ -43,7 +44,7 @@ namespace com.keyman.osk.browser { } } - public showSubkeys() { + public resolve() { if(this.resolver) { this.resolver(new SubkeyPopup(this.vkbd, this.baseKey)); } diff --git a/web/source/osk/embedded/pendingLongpress.ts b/web/source/osk/embedded/pendingLongpress.ts index 8aa1983bc7..6594edbac0 100644 --- a/web/source/osk/embedded/pendingLongpress.ts +++ b/web/source/osk/embedded/pendingLongpress.ts @@ -1,7 +1,8 @@ /// +/// namespace com.keyman.osk.embedded { - export class PendingLongpress { + export class PendingLongpress implements PendingGesture { private resolver: (delegator: SubkeyDelegator) => void; private readonly vkbd: VisualKeyboard; @@ -11,17 +12,25 @@ namespace com.keyman.osk.embedded { constructor(vkbd: VisualKeyboard, e: KeyElement) { this.vkbd = vkbd; let _this = this; + this.promise = new Promise(function(resolve) { _this.resolver = resolve; }); this.baseKey = e; } - public markActiveSubkeys() { + public resolve() { if(this.resolver) { this.resolver(new SubkeyDelegator(this.vkbd, this.baseKey)); } this.resolver = null; } + + public cancel() { + if(this.resolver) { + this.resolver(null); + this.resolver = null; + } + } } } \ No newline at end of file diff --git a/web/source/osk/pendingGesture.interface.ts b/web/source/osk/pendingGesture.interface.ts new file mode 100644 index 0000000000..dd42149360 --- /dev/null +++ b/web/source/osk/pendingGesture.interface.ts @@ -0,0 +1,9 @@ +namespace com.keyman.osk { + export interface PendingGesture { + readonly baseKey: KeyElement; + readonly promise: Promise; + + cancel(): void; + resolve?(): void; + } +} \ No newline at end of file diff --git a/web/source/osk/visualKeyboard.ts b/web/source/osk/visualKeyboard.ts index ef4158c9de..08d50ef506 100644 --- a/web/source/osk/visualKeyboard.ts +++ b/web/source/osk/visualKeyboard.ts @@ -58,11 +58,12 @@ namespace com.keyman.osk { touchCount: number; currentTarget: KeyElement; - // Popup key management + // Used by embedded-mode's globe key menuEvent: KeyElement; // Used by embedded-mode. + + // Popup key management keytip: KeyTip; - browserPendingLongpress: browser.PendingLongpress; - embeddedPendingLongpress: any; // a temp, intermediate property during subkey abstraction work + pendingSubkey: PendingGesture; subkeyGesture: RealizedGesture; get layerId(): string { @@ -714,9 +715,15 @@ namespace com.keyman.osk { // If popup is visible, need to move over popup, not over main keyboard // TODO: responsible for the shortcutting gesture for early subkey display. - this.highlightSubKeys(key1,x,y); - // As the previous line can trigger the start of the subkey gesture... + if(key1 && key1['subKeys'] != null) { + // Show popup keys immediately if touch moved up towards key array (KMEW-100, Build 353) + if((this.touchY - e.touches[0].pageY > 5) && this.pendingSubkey && this.pendingSubkey instanceof browser.PendingLongpress) { + this.pendingSubkey.resolve(); + } + } + + // As the previous block can trigger the start of the subkey gesture... if(this.subkeyGesture) { return; } @@ -1029,9 +1036,9 @@ namespace com.keyman.osk { this.subkeyGesture = null; } - if(this.browserPendingLongpress) { - this.browserPendingLongpress.cancel(); - this.browserPendingLongpress = null; + if(this.pendingSubkey) { + this.pendingSubkey.cancel(); + this.pendingSubkey = null; } } @@ -1606,9 +1613,9 @@ namespace com.keyman.osk { */ touchHold(key: KeyElement, force?: boolean) { // Clear and restart the popup timer - if(this.browserPendingLongpress) { - this.browserPendingLongpress.cancel(); - this.browserPendingLongpress = null; + if(this.pendingSubkey) { + this.pendingSubkey.cancel(); + this.pendingSubkey = null; } if(typeof key['subKeys'] != 'undefined' && key['subKeys'] != null) { @@ -1616,13 +1623,12 @@ namespace com.keyman.osk { // First-level object/Promise: will produce a subkey popup when the longpress gesture completes. // 'Returns' a second-level object/Promise: resolves when a subkey is selected or is cancelled. - let pl = this.browserPendingLongpress = new browser.PendingLongpress(this, key); - this.browserPendingLongpress.promise.then(function(subkeyPopup) { - // Clear the longpress field upon any sort of fulfillment if it is still the current one. - if(_this.browserPendingLongpress == pl) { - _this.browserPendingLongpress = null; + let pendingLongpress = this.pendingSubkey = new browser.PendingLongpress(this, key); + pendingLongpress.promise.then(function(subkeyPopup) { + if(_this.pendingSubkey == pendingLongpress) { + _this.pendingSubkey = null; } - + if(subkeyPopup) { // Clear key preview if any _this.showKeyTip(null,false); @@ -1648,9 +1654,9 @@ namespace com.keyman.osk { _this.clearPopup(); }); - if(force) { + if(force && this.pendingSubkey instanceof browser.PendingLongpress) { // Instantly resolves the first-level promise. - this.browserPendingLongpress.showSubkeys(); + this.pendingSubkey.resolve(); } } }; @@ -1673,29 +1679,6 @@ namespace com.keyman.osk { } }; - // Manage popup key highlighting - highlightSubKeys(k: KeyElement, x: number, y: number) { - // Test for subkey array, return if none - - // Issue: if `k` is itself a subkey, this won't do subkey highlighting correctly. - // That "common case" is actually handled through _standard_ key highlighting. - if(k == null || k['subKeys'] == null) { - return; - } - - // Highlight key at touch position (and clear other highlighting) - var skBox=document.getElementById('kmw-popup-keys'); - - //#region This section fills a different role than the method name would suggest. - // Might correspond better to a 'checkInstantSubkeys' or something. - - // Show popup keys immediately if touch moved up towards key array (KMEW-100, Build 353) - if((this.touchY-y > 5) && skBox == null) { - this.touchHold(k, true); - } - //#endregion - }; - /** * Add (or remove) the keytip preview (if KeymanWeb on a phone device) * From 0607178d25cb6ebc260dac15ff40f555d7c0af24 Mon Sep 17 00:00:00 2001 From: jahorton Date: Wed, 23 Jun 2021 11:52:58 +0700 Subject: [PATCH 20/44] fix(web): subkeys were being auto-cleared by mistake --- web/source/osk/visualKeyboard.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/web/source/osk/visualKeyboard.ts b/web/source/osk/visualKeyboard.ts index 08d50ef506..9076e23b4a 100644 --- a/web/source/osk/visualKeyboard.ts +++ b/web/source/osk/visualKeyboard.ts @@ -1651,7 +1651,6 @@ namespace com.keyman.osk { // Must be placed after its `.element` has been inserted into the DOM. subkeyPopup.reposition(_this); } - _this.clearPopup(); }); if(force && this.pendingSubkey instanceof browser.PendingLongpress) { From 8f9493d7ab97ae2712f78633f624c0942378d2be Mon Sep 17 00:00:00 2001 From: jahorton Date: Thu, 24 Jun 2021 10:25:42 +0700 Subject: [PATCH 21/44] refactor(web): removes unused vars in executePopupKey --- web/source/kmwembedded.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/web/source/kmwembedded.ts b/web/source/kmwembedded.ts index 2b7bae20f0..c2d1ac6534 100644 --- a/web/source/kmwembedded.ts +++ b/web/source/kmwembedded.ts @@ -341,8 +341,6 @@ namespace com.keyman.text { * @param {string} keyName key identifier **/ keymanweb['executePopupKey'] = function(keyName: string) { - let core = ( keymanweb).core; - var origArg = keyName; if(!keymanweb.core.activeKeyboard || !osk.vkbd) { return false; @@ -356,19 +354,14 @@ namespace com.keyman.text { // Can't just split on '-' because some layers like ctrl-shift contain it. let separatorIndex = keyName.lastIndexOf('-'); - var layer = core.keyboardProcessor.layerId; + //var layer = core.keyboardProcessor.layerId; if (separatorIndex > 0) { - layer = keyName.substring(0, separatorIndex); keyName = keyName.substring(separatorIndex+1); } - if(layer == 'undefined') { - layer=core.keyboardProcessor.layerId; - } // Note: this assumes Lelem is properly attached and has an element interface. // Currently true in the Android and iOS apps. - var Lelem=keymanweb.domManager.getLastActiveElement(),keyShiftState=com.keyman.text.KeyboardProcessor.getModifierState(layer); - + var Lelem=keymanweb.domManager.getLastActiveElement(); keymanweb.domManager.initActiveElement(Lelem); // This should be set if we're within this method... but it's best to guard against nulls here, just in case. From 5067337429ba4c07d3f40d4b20047943628ad242 Mon Sep 17 00:00:00 2001 From: jahorton Date: Wed, 23 Jun 2021 14:24:39 +0700 Subject: [PATCH 22/44] fix(web): selection of base key after subkey display (embedded) --- web/source/osk/embedded/subkeyDelegator.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/web/source/osk/embedded/subkeyDelegator.ts b/web/source/osk/embedded/subkeyDelegator.ts index 3fc2d33bb4..488634467c 100644 --- a/web/source/osk/embedded/subkeyDelegator.ts +++ b/web/source/osk/embedded/subkeyDelegator.ts @@ -8,6 +8,9 @@ namespace com.keyman.osk.embedded { public readonly baseKey: KeyElement; public readonly promise: Promise; + private movedFromBaseKey: boolean = false; + private baseKeySelected: boolean = false; + constructor(vkbd: VisualKeyboard, e: KeyElement) { this.vkbd = vkbd; @@ -23,7 +26,10 @@ namespace com.keyman.osk.embedded { if(this.resolver) { let keyEvent: text.KeyEvent = null; - if(keyCoreID != null) { + if(keyCoreID == null && this.baseKeySelected) { + keyEvent = this.vkbd.keyEventFromSpec(this.baseKey.key.spec as keyboards.ActiveKey, null); + this.baseKey.key.highlight(false); + } else { // This is set with the base key of our current subkey elsewhere within the engine. var baseKey: OSKKeySpec = this.baseKey.key.spec; var found = false; @@ -64,7 +70,15 @@ namespace com.keyman.osk.embedded { } updateTouch(touch: Touch) { - this.baseKey.key.highlight(this.baseKey.key.isUnderTouch(touch)); + let baseKeyTouched = this.baseKey.key.isUnderTouch(touch); + this.baseKeySelected = this.baseKey.key.isUnderTouch(touch) + + // Prevent highlighting & selection before the touch has moved from the base key. + if(this.movedFromBaseKey) { + this.baseKey.key.highlight(this.baseKeySelected); + } else { + this.movedFromBaseKey = !baseKeyTouched; + } } } } \ No newline at end of file From 3d615239a798017cb171d316b22df02cf1fb9ef3 Mon Sep 17 00:00:00 2001 From: jahorton Date: Wed, 23 Jun 2021 14:24:39 +0700 Subject: [PATCH 23/44] fix(web): prevent error when attempting to reselect base key (embedded) --- web/source/osk/embedded/subkeyDelegator.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/web/source/osk/embedded/subkeyDelegator.ts b/web/source/osk/embedded/subkeyDelegator.ts index 488634467c..213bd091e7 100644 --- a/web/source/osk/embedded/subkeyDelegator.ts +++ b/web/source/osk/embedded/subkeyDelegator.ts @@ -27,6 +27,7 @@ namespace com.keyman.osk.embedded { let keyEvent: text.KeyEvent = null; if(keyCoreID == null && this.baseKeySelected) { + // Handle selection of base key underneath the subkey array. keyEvent = this.vkbd.keyEventFromSpec(this.baseKey.key.spec as keyboards.ActiveKey, null); this.baseKey.key.highlight(false); } else { From aab0eb94e2370a3cffd72d869b9d7e7e870c3bbf Mon Sep 17 00:00:00 2001 From: jahorton Date: Wed, 23 Jun 2021 13:04:51 +0700 Subject: [PATCH 24/44] refactor(web): initGestures, updateGestures --- web/source/kmwembedded.ts | 24 +---- web/source/osk/visualKeyboard.ts | 175 ++++++++++++++++++------------- 2 files changed, 110 insertions(+), 89 deletions(-) diff --git a/web/source/kmwembedded.ts b/web/source/kmwembedded.ts index c2d1ac6534..7e0cacd9a7 100644 --- a/web/source/kmwembedded.ts +++ b/web/source/kmwembedded.ts @@ -53,31 +53,17 @@ namespace com.keyman.osk { * * @param {Object} key base key element */ - VisualKeyboard.prototype.touchHold = function(this: VisualKeyboard, key: KeyElement) { - if(key['subKeys'] && (typeof(window['oskCreatePopup']) == 'function')) { + VisualKeyboard.prototype.startLongpress = function(this: VisualKeyboard, key: KeyElement): PendingGesture { + if(typeof(window['oskCreatePopup']) == 'function') { var xBase = dom.Utils.getAbsoluteX(key) - dom.Utils.getAbsoluteX(this.kbdDiv) + key.offsetWidth/2, yBase = dom.Utils.getAbsoluteY(key); // #3718: No longer prepend base key to subkey array - - let _this = this; window['oskCreatePopup'](key['subKeys'], xBase, yBase, key.offsetWidth, key.offsetHeight); - let pendingLongpress = new embedded.PendingLongpress(this, key); - pendingLongpress.promise.then(function(gesture) { - _this.subkeyGesture = gesture; - if(gesture) { - gesture.promise.then(function(keyEvent) { - _this.subkeyGesture = null; - // Allow active cancellation, even if the source should allow passive. - // It's an easy and cheap null guard. - if(keyEvent) { - PreProcessor.raiseKeyEvent(keyEvent); - } - }); - } - }); - this.pendingSubkey = pendingLongpress; + return new embedded.PendingLongpress(this, key); + } else { + return null; } }; diff --git a/web/source/osk/visualKeyboard.ts b/web/source/osk/visualKeyboard.ts index 9076e23b4a..b4f0e1feca 100644 --- a/web/source/osk/visualKeyboard.ts +++ b/web/source/osk/visualKeyboard.ts @@ -570,7 +570,7 @@ namespace com.keyman.osk { this.touchCount--; } else { // If this key has subkey, start timer to display subkeys after delay, set up release - this.touchHold(key); + this.initGestures(key, e.changedTouches[0]); } this.keyPending = key; this.touchPending = e.changedTouches[0]; @@ -697,34 +697,9 @@ namespace com.keyman.osk { return; } - // Clear previous key highlighting, allow subkey controller to - // highlight as appropriate. - if(this.subkeyGesture) { - if(key0) { - key0.key.highlight(false); - } - this.subkeyGesture.updateTouch(e.touches[0]); - - this.keyPending = null; - this.touchPending = null; - - return; - } - - this.currentTarget = null; - - // If popup is visible, need to move over popup, not over main keyboard - // TODO: responsible for the shortcutting gesture for early subkey display. - - if(key1 && key1['subKeys'] != null) { - // Show popup keys immediately if touch moved up towards key array (KMEW-100, Build 353) - if((this.touchY - e.touches[0].pageY > 5) && this.pendingSubkey && this.pendingSubkey instanceof browser.PendingLongpress) { - this.pendingSubkey.resolve(); - } - } - - // As the previous block can trigger the start of the subkey gesture... - if(this.subkeyGesture) { + // Update all gesture tracking. The function returns true if further input processing + // should be blocked. + if(this.updateGestures(key1, key0, e.changedTouches[0])) { return; } @@ -759,11 +734,6 @@ namespace com.keyman.osk { this.highlightKey(key1,true); } } - - if(key0 && key1 && (key1 != key0) && (key1.id != '')) { - // Display the touch-hold keys (after a pause) - this.touchHold(key1); - } }.bind(this); /** @@ -1606,44 +1576,22 @@ namespace com.keyman.osk { } } - /** - * Touch hold key display management - * - * @param {Object} key base key object - */ - touchHold(key: KeyElement, force?: boolean) { - // Clear and restart the popup timer - if(this.pendingSubkey) { - this.pendingSubkey.cancel(); - this.pendingSubkey = null; - } - - if(typeof key['subKeys'] != 'undefined' && key['subKeys'] != null) { + /** + * Starts an implementation-specific longpress gesture. Separately implemented for + * in-browser and embedded modes. + * @param key The base key of the longpress. + * @returns + */ + startLongpress(key: KeyElement): PendingGesture { let _this = this; // First-level object/Promise: will produce a subkey popup when the longpress gesture completes. // 'Returns' a second-level object/Promise: resolves when a subkey is selected or is cancelled. - let pendingLongpress = this.pendingSubkey = new browser.PendingLongpress(this, key); + let pendingLongpress = new browser.PendingLongpress(this, key); pendingLongpress.promise.then(function(subkeyPopup) { - if(_this.pendingSubkey == pendingLongpress) { - _this.pendingSubkey = null; - } - + // In-browser-specific handling. if(subkeyPopup) { - // Clear key preview if any - _this.showKeyTip(null,false); - - _this.subkeyGesture = subkeyPopup; - subkeyPopup.promise.then(function(keyEvent: text.KeyEvent) { - // Allow active cancellation, even if the source should allow passive. - // It's an easy and cheap null guard. - if(keyEvent) { - PreProcessor.raiseKeyEvent(keyEvent); - } - _this.clearPopup(); - }); - - // Otherwise append the touch-hold (subkey) array to the OSK + // Append the touch-hold (subkey) array to the OSK let keyman = com.keyman.singleton; keyman.osk._Box.appendChild(subkeyPopup.element); keyman.osk._Box.appendChild(subkeyPopup.shim); @@ -1653,12 +1601,99 @@ namespace com.keyman.osk { } }); - if(force && this.pendingSubkey instanceof browser.PendingLongpress) { - // Instantly resolves the first-level promise. - this.pendingSubkey.resolve(); + return pendingLongpress; + } + + /** + * Initializes all supported gestures given a base key and the triggering touch coordinates. + * @param key The gesture's base key + * @param touch The starting touch coordinates for the gesture + * @returns + */ + initGestures(key: KeyElement, touch: Touch) { + if(key['subKeys']) { + let _this = this; + + let pendingLongpress = this.startLongpress(key); + if(pendingLongpress == null) { + return; + } + this.pendingSubkey = pendingLongpress; + + pendingLongpress.promise.then(function(subkeyPopup) { + if(_this.pendingSubkey == pendingLongpress) { + _this.pendingSubkey = null; + } + + if(subkeyPopup) { + // Clear key preview if any + _this.showKeyTip(null,false); + + _this.subkeyGesture = subkeyPopup; + subkeyPopup.promise.then(function(keyEvent: text.KeyEvent) { + // Allow active cancellation, even if the source should allow passive. + // It's an easy and cheap null guard. + if(keyEvent) { + PreProcessor.raiseKeyEvent(keyEvent); + } + _this.clearPopup(); + }); + } + }); } } - }; + + /** + * Updates all currently-pending and activated gestures. + * + * @param currentKey The key currently underneath the most recent touch coordinate + * @param previousKey The previously-selected key + * @param touch The current touch-coordinate for the gesture + * @returns true if should fully capture input, false if input should 'fall through'. + */ + updateGestures(currentKey: KeyElement, previousKey: KeyElement, touch: Touch): boolean { + let key0 = previousKey; + let key1 = currentKey; + + // Clear previous key highlighting, allow subkey controller to + // highlight as appropriate. + if(this.subkeyGesture) { + if(key0) { + key0.key.highlight(false); + } + this.subkeyGesture.updateTouch(touch); + + this.keyPending = null; + this.touchPending = null; + + return true; + } + + this.currentTarget = null; + + // If popup is visible, need to move over popup, not over main keyboard + // TODO: responsible for the shortcutting gesture for early subkey display. + + // TODO: Could be made part of the browser-specific implementation's update func? + if(key1 && key1['subKeys'] != null) { + // Show popup keys immediately if touch moved up towards key array (KMEW-100, Build 353) + if((this.touchY - touch.pageY > 5) && this.pendingSubkey && this.pendingSubkey instanceof browser.PendingLongpress) { + this.pendingSubkey.resolve(); + } + } + + // As the previous block can trigger the start of the subkey gesture... + if(this.subkeyGesture) { + return true; + } + + if(key0 && key1 && (key1 != key0) && (key1.id != '')) { + // Display the touch-hold keys (after a pause) + this.clearPopup(); + this.initGestures(key1, touch); + } + return false; + } optionKey(e: KeyElement, keyName: string, keyDown: boolean) { let keyman = com.keyman.singleton; From 625b5fd8c8e40407b4ea3956b3a01e98b57d1c1c Mon Sep 17 00:00:00 2001 From: jahorton Date: Wed, 23 Jun 2021 13:14:54 +0700 Subject: [PATCH 25/44] fix(web): output base key if subkey touch returns to it (in-browser) --- web/source/osk/browser/subkeyPopup.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/web/source/osk/browser/subkeyPopup.ts b/web/source/osk/browser/subkeyPopup.ts index f927a8bd61..d9c0c05077 100644 --- a/web/source/osk/browser/subkeyPopup.ts +++ b/web/source/osk/browser/subkeyPopup.ts @@ -236,6 +236,7 @@ namespace com.keyman.osk.browser { updateTouch(touch: Touch) { this.currentSelection = null; + let matchFound = false; for(let i=0; i < this.baseKey['subKeys'].length; i++) { try { @@ -245,10 +246,17 @@ namespace com.keyman.osk.browser { if(onKey) { this.baseKey.key.highlight(false); this.currentSelection = sk; + matchFound = true; } sk.key.highlight(onKey); } catch(ex){} } + + // Use the popup duplicate of the base key if a phone with a visible popup array + if(!matchFound && this.baseKey.key.isUnderTouch(touch)) { + this.baseKey.key.highlight(true); + this.currentSelection = this.baseKey; + } } } } \ No newline at end of file From 6034f252e70c2218a1983059324f32d2bdf15daa Mon Sep 17 00:00:00 2001 From: jahorton Date: Thu, 24 Jun 2021 14:29:36 +0700 Subject: [PATCH 26/44] docs(web): tweak for note re future enhancement --- web/source/osk/visualKeyboard.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/web/source/osk/visualKeyboard.ts b/web/source/osk/visualKeyboard.ts index 9076e23b4a..dfb519e7d6 100644 --- a/web/source/osk/visualKeyboard.ts +++ b/web/source/osk/visualKeyboard.ts @@ -714,8 +714,9 @@ namespace com.keyman.osk { this.currentTarget = null; // If popup is visible, need to move over popup, not over main keyboard - // TODO: responsible for the shortcutting gesture for early subkey display. - + // Could be turned into a browser-longpress specific implementation within browser.PendingLongpress? + // Not completely sure what the correct, generalized abstraction would be for that... + // and this PR's already big enough, anyway. if(key1 && key1['subKeys'] != null) { // Show popup keys immediately if touch moved up towards key array (KMEW-100, Build 353) if((this.touchY - e.touches[0].pageY > 5) && this.pendingSubkey && this.pendingSubkey instanceof browser.PendingLongpress) { @@ -1628,7 +1629,7 @@ namespace com.keyman.osk { if(_this.pendingSubkey == pendingLongpress) { _this.pendingSubkey = null; } - + if(subkeyPopup) { // Clear key preview if any _this.showKeyTip(null,false); From 975e2343f15cfaeedc9386dbf4b4261a56fbddf8 Mon Sep 17 00:00:00 2001 From: jahorton Date: Thu, 24 Jun 2021 14:40:50 +0700 Subject: [PATCH 27/44] docs(web): fixes up refactored docs --- web/source/osk/visualKeyboard.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/web/source/osk/visualKeyboard.ts b/web/source/osk/visualKeyboard.ts index b4f0e1feca..8c5e933e62 100644 --- a/web/source/osk/visualKeyboard.ts +++ b/web/source/osk/visualKeyboard.ts @@ -1672,9 +1672,9 @@ namespace com.keyman.osk { this.currentTarget = null; // If popup is visible, need to move over popup, not over main keyboard - // TODO: responsible for the shortcutting gesture for early subkey display. - - // TODO: Could be made part of the browser-specific implementation's update func? + // Could be turned into a browser-longpress specific implementation within browser.PendingLongpress? + // Not completely sure what the correct, generalized abstraction would be for that... + // and this PR's already big enough, anyway. if(key1 && key1['subKeys'] != null) { // Show popup keys immediately if touch moved up towards key array (KMEW-100, Build 353) if((this.touchY - touch.pageY > 5) && this.pendingSubkey && this.pendingSubkey instanceof browser.PendingLongpress) { @@ -1682,7 +1682,8 @@ namespace com.keyman.osk { } } - // As the previous block can trigger the start of the subkey gesture... + // If there is an active popup menu (which can occur from the previous block), + // a subkey popup exists; do not allow base key output. if(this.subkeyGesture) { return true; } From 3f793a0b850d7825ab0a420d0bb76188d18caa90 Mon Sep 17 00:00:00 2001 From: jahorton Date: Thu, 24 Jun 2021 14:42:48 +0700 Subject: [PATCH 28/44] docs(web): another doc tweak --- web/source/osk/visualKeyboard.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/source/osk/visualKeyboard.ts b/web/source/osk/visualKeyboard.ts index 8c5e933e62..22f9c76500 100644 --- a/web/source/osk/visualKeyboard.ts +++ b/web/source/osk/visualKeyboard.ts @@ -1655,8 +1655,7 @@ namespace com.keyman.osk { let key0 = previousKey; let key1 = currentKey; - // Clear previous key highlighting, allow subkey controller to - // highlight as appropriate. + // Clear previous key highlighting, allow subkey controller to highlight as appropriate. if(this.subkeyGesture) { if(key0) { key0.key.highlight(false); @@ -1689,7 +1688,8 @@ namespace com.keyman.osk { } if(key0 && key1 && (key1 != key0) && (key1.id != '')) { - // Display the touch-hold keys (after a pause) + // While there may not be an active subkey menu, we should probably update which base key + // is being highlighted by the current touch & start a pending longpress for it. this.clearPopup(); this.initGestures(key1, touch); } From 74dc8ad115e7aebabe55fc250f6399373e5f4d51 Mon Sep 17 00:00:00 2001 From: jahorton Date: Tue, 29 Jun 2021 07:48:34 +0700 Subject: [PATCH 29/44] docs(web): reason for SubkeyDelegator nomenclature --- web/source/osk/embedded/subkeyDelegator.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/source/osk/embedded/subkeyDelegator.ts b/web/source/osk/embedded/subkeyDelegator.ts index 213bd091e7..b2e75ae684 100644 --- a/web/source/osk/embedded/subkeyDelegator.ts +++ b/web/source/osk/embedded/subkeyDelegator.ts @@ -1,6 +1,8 @@ /// namespace com.keyman.osk.embedded { + // "Delegator", rather than "Popup", because KMW delegates display + selection + // of subkeys to the host app when in the embedded context. export class SubkeyDelegator implements RealizedGesture { private resolver: (keyEvent: text.KeyEvent) => void; private readonly vkbd: VisualKeyboard; From e0cf57d508dbf02465bd00cb0d10b56eea071d26 Mon Sep 17 00:00:00 2001 From: Joshua Horton Date: Wed, 30 Jun 2021 10:11:46 +0700 Subject: [PATCH 30/44] chore(web): Apply suggestions from code review Co-authored-by: Marc Durdin --- web/source/osk/browser/pendingLongpress.ts | 11 ++++------- web/source/osk/browser/subkeyPopup.ts | 6 +++--- web/source/osk/embedded/subkeyDelegator.ts | 15 ++++++--------- web/source/osk/oskKey.ts | 6 +++--- web/source/osk/visualKeyboard.ts | 1 - 5 files changed, 16 insertions(+), 23 deletions(-) diff --git a/web/source/osk/browser/pendingLongpress.ts b/web/source/osk/browser/pendingLongpress.ts index 9e708c579f..a0b90f0bae 100644 --- a/web/source/osk/browser/pendingLongpress.ts +++ b/web/source/osk/browser/pendingLongpress.ts @@ -23,12 +23,9 @@ namespace com.keyman.osk.browser { let _this = this; this.promise = new Promise(function(resolve, reject) { _this.resolver = resolve; - _this.timerId = window.setTimeout( - function() { - // It's no longer deferred; it's being fulfilled. - // Even if the actual subkey itself is still async. - _this.resolve(); - }, _this.popupDelay); + // After the timeout, it's no longer deferred; it's being fulfilled. + // Even if the actual subkey itself is still async. + _this.timerId = window.setTimeout(_this.resolve, _this.popupDelay); }); } @@ -50,4 +47,4 @@ namespace com.keyman.osk.browser { } } } -} \ No newline at end of file +} diff --git a/web/source/osk/browser/subkeyPopup.ts b/web/source/osk/browser/subkeyPopup.ts index d9c0c05077..1207e1f32e 100644 --- a/web/source/osk/browser/subkeyPopup.ts +++ b/web/source/osk/browser/subkeyPopup.ts @@ -240,9 +240,9 @@ namespace com.keyman.osk.browser { for(let i=0; i < this.baseKey['subKeys'].length; i++) { try { - let sk= this.element.childNodes[i].firstChild as KeyElement; + let sk = this.element.childNodes[i].firstChild as KeyElement; - let onKey = sk.key.isUnderTouch(touch) + let onKey = sk.key.isUnderTouch(touch); if(onKey) { this.baseKey.key.highlight(false); this.currentSelection = sk; @@ -259,4 +259,4 @@ namespace com.keyman.osk.browser { } } } -} \ No newline at end of file +} diff --git a/web/source/osk/embedded/subkeyDelegator.ts b/web/source/osk/embedded/subkeyDelegator.ts index b2e75ae684..e4ec911540 100644 --- a/web/source/osk/embedded/subkeyDelegator.ts +++ b/web/source/osk/embedded/subkeyDelegator.ts @@ -34,7 +34,7 @@ namespace com.keyman.osk.embedded { this.baseKey.key.highlight(false); } else { // This is set with the base key of our current subkey elsewhere within the engine. - var baseKey: OSKKeySpec = this.baseKey.key.spec; + let baseKey: OSKKeySpec = this.baseKey.key.spec; var found = false; let selectedKey: OSKKeySpec; @@ -53,10 +53,8 @@ namespace com.keyman.osk.embedded { throw new Error("Could not find subkey '" + keyCoreID + "' under base key '" + baseKey.coreID + "'!"); } - if(selectedKey) { - keyEvent = this.vkbd.keyEventFromSpec(selectedKey as keyboards.ActiveKey, null); - keyEvent.vkCode=keyEvent.Lcode; - } + keyEvent = this.vkbd.keyEventFromSpec(selectedKey as keyboards.ActiveKey, null); + keyEvent.vkCode=keyEvent.Lcode; } this.resolver(keyEvent); @@ -73,15 +71,14 @@ namespace com.keyman.osk.embedded { } updateTouch(touch: Touch) { - let baseKeyTouched = this.baseKey.key.isUnderTouch(touch); - this.baseKeySelected = this.baseKey.key.isUnderTouch(touch) + this.baseKeySelected = this.baseKey.key.isUnderTouch(touch); // Prevent highlighting & selection before the touch has moved from the base key. if(this.movedFromBaseKey) { this.baseKey.key.highlight(this.baseKeySelected); } else { - this.movedFromBaseKey = !baseKeyTouched; + this.movedFromBaseKey = !this.baseKeySelected; } } } -} \ No newline at end of file +} diff --git a/web/source/osk/oskKey.ts b/web/source/osk/oskKey.ts index da52968507..7a0bd2dd32 100644 --- a/web/source/osk/oskKey.ts +++ b/web/source/osk/oskKey.ts @@ -439,10 +439,10 @@ namespace com.keyman.osk { let x0 = dom.Utils.getAbsoluteX(btn); let y0 = dom.Utils.getAbsoluteY(btn);//-document.body.scrollTop; - let x1=x0 + btn.offsetWidth; - let y1=y0 + btn.offsetHeight; + let x1 = x0 + btn.offsetWidth; + let y1 = y0 + btn.offsetHeight; return (x > x0 && x < x1 && y > y0 && y < y1); } } -} \ No newline at end of file +} diff --git a/web/source/osk/visualKeyboard.ts b/web/source/osk/visualKeyboard.ts index 22f9c76500..44024eb918 100644 --- a/web/source/osk/visualKeyboard.ts +++ b/web/source/osk/visualKeyboard.ts @@ -626,7 +626,6 @@ namespace com.keyman.osk { // Process and clear highlighting of pending target if(this.keyPending) { this.highlightKey(this.keyPending,false); - // Output character unless moved off key if(this.keyPending.className.indexOf('hidden') < 0 && tc > 0 && !beyondEdge) { this.modelKeyClick(this.keyPending, e.changedTouches[0]); From a8e82e5bea0a85d484e6e76d26d3f56b5ea3a742 Mon Sep 17 00:00:00 2001 From: jahorton Date: Wed, 30 Jun 2021 11:09:32 +0700 Subject: [PATCH 31/44] chore(web): more changes from PR review --- web/source/osk/browser/pendingLongpress.ts | 4 +--- web/source/osk/browser/subkeyPopup.ts | 4 +--- web/source/osk/embedded/subkeyDelegator.ts | 9 ++++----- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/web/source/osk/browser/pendingLongpress.ts b/web/source/osk/browser/pendingLongpress.ts index a0b90f0bae..51086eb50c 100644 --- a/web/source/osk/browser/pendingLongpress.ts +++ b/web/source/osk/browser/pendingLongpress.ts @@ -5,7 +5,6 @@ namespace com.keyman.osk.browser { export class PendingLongpress implements PendingGesture { public readonly baseKey: KeyElement; public readonly promise: Promise; - //public readonly initialTouch: Touch; public readonly subkeyUI: SubkeyPopup; @@ -15,10 +14,9 @@ namespace com.keyman.osk.browser { private timerId: number; private popupDelay: number = 500; - constructor(vkbd: VisualKeyboard, baseKey: KeyElement/*, initialTouch: Touch*/) { + constructor(vkbd: VisualKeyboard, baseKey: KeyElement) { this.vkbd = vkbd; this.baseKey = baseKey; - //this.initialTouch = initialTouch; let _this = this; this.promise = new Promise(function(resolve, reject) { diff --git a/web/source/osk/browser/subkeyPopup.ts b/web/source/osk/browser/subkeyPopup.ts index 1207e1f32e..d85d0bc088 100644 --- a/web/source/osk/browser/subkeyPopup.ts +++ b/web/source/osk/browser/subkeyPopup.ts @@ -236,7 +236,6 @@ namespace com.keyman.osk.browser { updateTouch(touch: Touch) { this.currentSelection = null; - let matchFound = false; for(let i=0; i < this.baseKey['subKeys'].length; i++) { try { @@ -246,14 +245,13 @@ namespace com.keyman.osk.browser { if(onKey) { this.baseKey.key.highlight(false); this.currentSelection = sk; - matchFound = true; } sk.key.highlight(onKey); } catch(ex){} } // Use the popup duplicate of the base key if a phone with a visible popup array - if(!matchFound && this.baseKey.key.isUnderTouch(touch)) { + if(this.currentSelection && this.baseKey.key.isUnderTouch(touch)) { this.baseKey.key.highlight(true); this.currentSelection = this.baseKey; } diff --git a/web/source/osk/embedded/subkeyDelegator.ts b/web/source/osk/embedded/subkeyDelegator.ts index e4ec911540..7986c9cd61 100644 --- a/web/source/osk/embedded/subkeyDelegator.ts +++ b/web/source/osk/embedded/subkeyDelegator.ts @@ -35,21 +35,20 @@ namespace com.keyman.osk.embedded { } else { // This is set with the base key of our current subkey elsewhere within the engine. let baseKey: OSKKeySpec = this.baseKey.key.spec; - var found = false; let selectedKey: OSKKeySpec; if(baseKey.coreID == keyCoreID) { selectedKey = baseKey; - found = true; } else { // ... yeah, there are some funky type shenanigans between the two. // OSKKeySpec is the OSK's... reinterpretation of the ActiveKey type. selectedKey = (baseKey as keyboards.ActiveKey).getSubkey(keyCoreID) as OSKKeySpec; - found = !!selectedKey; } - if(!found) { - this.resolver(null); // Maintains existing behavior. + if(!selectedKey) { + // While we can't complete successfully, the subkey operation is done; we + // should still signal that and update related gesture state management. + this.resolver(null); throw new Error("Could not find subkey '" + keyCoreID + "' under base key '" + baseKey.coreID + "'!"); } From 052711da622a7477cfec331c4da820377f1f55de Mon Sep 17 00:00:00 2001 From: jahorton Date: Wed, 30 Jun 2021 11:10:09 +0700 Subject: [PATCH 32/44] docs(web): for PendingGesture, RealizedGesture --- web/source/osk/pendingGesture.interface.ts | 36 +++++++++++++++++++++ web/source/osk/realizedGesture.interface.ts | 24 ++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/web/source/osk/pendingGesture.interface.ts b/web/source/osk/pendingGesture.interface.ts index dd42149360..8cea889b89 100644 --- a/web/source/osk/pendingGesture.interface.ts +++ b/web/source/osk/pendingGesture.interface.ts @@ -1,4 +1,40 @@ namespace com.keyman.osk { + /** + * Used for evaluating potential gestures. Classes adhering to this interface + * should instantiated whenever the (implied) state-machine allows a new touch + * event to mark the start of a potential new gesture. + * + * For example, whenever a user touches a base key and there are no "realized" + * (fully-completed, but as-of-yet unresolved) gestures, that state allows the + * start of a potential new longpress event. + * + * The role of the `PendingGesture` is complete whenever all touch-events and + * conditions necessary for a modeled gesture have been met. As this point, + * it should be `resolve`d, fulfilling its `promise`. This results in a + * `RealizedGesture` appropriate for the gesture type that is used to obtain + * the final `KeyEvent` for the overall gesture sequence. + * + * For example, a "longpress" is considered resolved once the user has maintained + * an active, stationary touch point on the same key for a sufficiently long + * period without releasing it. + * * Were it released earlier, that would result in selection of a base key. + * + * Alternatively, a "flick" might be considered resolved if: + * * a user has rapidly moved a touch point in a consistent direction + * * for a long enough distance + * * and _then_ releases that touch point within a short timeframe. + * + * The pending gesture should only `resolve` to a realized gesture once + * _all_ such conditions are met, confirming that this specific gesture, + * and _only_ this specific gesture, could have resulted from the active + * touch sequence. + * + * The `RealizedGesture` that results and is 'returned' via the Promise will + * be handled by the `VisualKeyboard` class, which will retrieve and forward + * any `KeyEvent` that results from the overall gesture input sequence. + * + * @see `RealizedGesture` + */ export interface PendingGesture { readonly baseKey: KeyElement; readonly promise: Promise; diff --git a/web/source/osk/realizedGesture.interface.ts b/web/source/osk/realizedGesture.interface.ts index 877164898a..6683b4357e 100644 --- a/web/source/osk/realizedGesture.interface.ts +++ b/web/source/osk/realizedGesture.interface.ts @@ -1,4 +1,28 @@ namespace com.keyman.osk { + /* + * Implementations of this interface allow individual types of gestures to + * specify any additional user interaction and functionality (which may + * include UI elements) appropriate for obtaining a key event that may be + * produced by the modeled gesture type. These should only be instantiated + * once the associated `PendingLongpress` is no longer 'pending' - once it + * has become clear that the input touch-event sequence could only correspond + * to the modeled gesture. + * + * For example, when a longpress gesture completes - and hence, the user has + * kept their finger stationary on the same key for a long enough period - + * we display a popup view presenting subkeys corresponding to the gesture's + * underlying element. This popup view accepts touch input and completes only + * upon release of the ongoing touch sequence. + * + * Gestures are events that occur over intervals of time, and since some of them + * will require time and user interaction after becoming 'realized', these cases + * will be inherently async. The simplest way to model this is with `Promise`s. + * + * If appropriate for the modeled gesture type, an implementation may supply an + * instantly-resolving `Promise``. This may be appropriate for modeling "flick" + * or "swipe" gestures in the future, which may require no additional input once + * such a gesture is fully realized. + */ export interface RealizedGesture { readonly baseKey: KeyElement; readonly promise: Promise; From cafe421e9890a38b24ef838a2909608f25af7d2a Mon Sep 17 00:00:00 2001 From: jahorton Date: Wed, 30 Jun 2021 12:09:19 +0700 Subject: [PATCH 33/44] docs(web): gesture subclasses --- web/source/kmwembedded.ts | 20 ++++++++++++-- web/source/osk/browser/pendingLongpress.ts | 17 ++++++++++++ web/source/osk/browser/subkeyPopup.ts | 16 +++++++++++ web/source/osk/embedded/pendingLongpress.ts | 10 +++++++ web/source/osk/embedded/subkeyDelegator.ts | 30 +++++++++++++++++++-- 5 files changed, 89 insertions(+), 4 deletions(-) diff --git a/web/source/kmwembedded.ts b/web/source/kmwembedded.ts index 7e0cacd9a7..0cc1308a76 100644 --- a/web/source/kmwembedded.ts +++ b/web/source/kmwembedded.ts @@ -265,7 +265,9 @@ namespace com.keyman.text { }; /** - * Function called by Android and iOS when a device-implemented keyboard popup is displayed or hidden + * Function called by Android and iOS when a device-implemented keyboard popup + * is displayed or hidden. As this is controlled by the app, it's the perfect + * trigger for 'embedded'-mode gesture state management. * * @param {boolean} isVisible * @@ -274,15 +276,27 @@ namespace com.keyman.text { let gesture = osk.vkbd.subkeyGesture as com.keyman.osk.embedded.SubkeyDelegator; let pendingLongpress = osk.vkbd.pendingSubkey; + /* + * If a longpress popup was visible, but is no longer, this means that the + * associated longpress gesture was cancelled. It is possible for the base key + * to emit if selected at this time, but only if appropriate - and this is + * managed by the `SubkeyDelegator`. + */ if(!isVisible) { if(gesture) { gesture.resolve(null); osk.vkbd.subkeyGesture = null; } else if(pendingLongpress) { pendingLongpress.cancel(); + osk.vkbd.pendingSubkey = null; } } + /* + * If the popup was not visible, but now is, that means our previously-pending + * longpress is now 'realized' (complete). Certain aspects of the OSK rely on + * this state information, which will be properly updated by `resolve`. + */ if(isVisible && pendingLongpress) { // Fulfills the first-stage promise. pendingLongpress.resolve(); @@ -340,7 +354,7 @@ namespace com.keyman.text { // Can't just split on '-' because some layers like ctrl-shift contain it. let separatorIndex = keyName.lastIndexOf('-'); - //var layer = core.keyboardProcessor.layerId; + if (separatorIndex > 0) { keyName = keyName.substring(separatorIndex+1); } @@ -355,9 +369,11 @@ namespace com.keyman.text { let gesture = osk.vkbd.subkeyGesture as com.keyman.osk.embedded.SubkeyDelegator; try { + // Can trigger an error if the corresponding subkey cannot be found. gesture.resolve(keyName); } catch (e) { let err = e as Error; + // Prevent the Android app from triggering a "fatal error" keyboard reset. console.warn(err.message); } diff --git a/web/source/osk/browser/pendingLongpress.ts b/web/source/osk/browser/pendingLongpress.ts index 51086eb50c..00fca0e500 100644 --- a/web/source/osk/browser/pendingLongpress.ts +++ b/web/source/osk/browser/pendingLongpress.ts @@ -2,6 +2,23 @@ /// namespace com.keyman.osk.browser { + /** + * (Conceptually) represents a finite-state-machine that determines + * whether or not a series of touch events corresponds to a longpress + * touch input. The `resolve` method may be used to trigger the + * subkey menu early, as with the upward quick-display shortcut. + * + * This is the default implementation of longpress behavior for KMW. + * Alterate implementations are modeled through the `embedded` + * namespace's equivalent, which is designed to facilitate custom + * modeling for such gestures. + * + * Once the conditions to recognize a longpress gesture have been + * fulfilled, this class's `promise` will resolve with a `SubkeyPopup` + * matching the gesture's 'base' key, which itself provides a + * `promise` field that will resolve to a `KeyEvent` once the touch + * sequence is completed. + */ export class PendingLongpress implements PendingGesture { public readonly baseKey: KeyElement; public readonly promise: Promise; diff --git a/web/source/osk/browser/subkeyPopup.ts b/web/source/osk/browser/subkeyPopup.ts index d85d0bc088..85b0371746 100644 --- a/web/source/osk/browser/subkeyPopup.ts +++ b/web/source/osk/browser/subkeyPopup.ts @@ -2,6 +2,22 @@ /// namespace com.keyman.osk.browser { + /** + * Represents a 'realized' longpress gesture's default implementation + * within KeymanWeb. Once a touch sequence has been confirmed to + * correspond to a longpress gesture, implementations of this class + * provide the following: + * * The UI needed to present a subkey menu + * * The state management needed to present feedback about the + * currently-selected subkey to the user + * * A `Promise` that will resolve to the user's selected subkey + * once the longpress operation is complete. + * + * As selection of the subkey occurs after the subkey popup is + * displayed, selection of the subkey is inherently asynchronous. + * The `Promise` may also resolve to `null` if the user indicates + * the desire to cancel subkey selection. + */ export class SubkeyPopup implements RealizedGesture { public readonly element: HTMLDivElement; public readonly shim: HTMLDivElement; diff --git a/web/source/osk/embedded/pendingLongpress.ts b/web/source/osk/embedded/pendingLongpress.ts index 6594edbac0..3b407cd610 100644 --- a/web/source/osk/embedded/pendingLongpress.ts +++ b/web/source/osk/embedded/pendingLongpress.ts @@ -2,6 +2,16 @@ /// namespace com.keyman.osk.embedded { + /** + * As control over the subkey display timer and the subkey popup are + * both handled by the host app within the Android app, this class + * serves mostly to communicate longpress state management from the + * app to the VisualKeyboard. + * + * The `resolve()` function should be triggered, in some fashion, by + * the host app whenever it has recognized a completed longpress and + * will begin displaying its subkey popup. + */ export class PendingLongpress implements PendingGesture { private resolver: (delegator: SubkeyDelegator) => void; private readonly vkbd: VisualKeyboard; diff --git a/web/source/osk/embedded/subkeyDelegator.ts b/web/source/osk/embedded/subkeyDelegator.ts index 7986c9cd61..207edf7310 100644 --- a/web/source/osk/embedded/subkeyDelegator.ts +++ b/web/source/osk/embedded/subkeyDelegator.ts @@ -1,8 +1,18 @@ /// namespace com.keyman.osk.embedded { - // "Delegator", rather than "Popup", because KMW delegates display + selection - // of subkeys to the host app when in the embedded context. + /** + * As the subkey popup view is handled by the host app when in embedded mode, + * this class represents the fact that KMW has "delegated" subkey UI and + * selection to the host app. Hence, "Delegator", rather than "Popup". + * + * The `resolve` method should be triggered, in some fashion, by the host app + * whenever the user has completed their longpress, potentially selecting + * a subkey. + * + * This class will also track the ongoing touch event in case the base key is + * reselected, which _is_ managed by this class, not the host app. + */ export class SubkeyDelegator implements RealizedGesture { private resolver: (keyEvent: text.KeyEvent) => void; private readonly vkbd: VisualKeyboard; @@ -24,6 +34,17 @@ namespace com.keyman.osk.embedded { this.baseKey = e; } + /** + * Resolves the ongoing longpress -> subkey gesture, fulfilling this + * `SubkeyDelegator`'s `promise` of a `KeyEvent`. + * + * If no subkey is selected but the original base key is, `resolve(null)` + * will return a key event corresponding to the base key. + + * + * @param keyCoreID {string} The 'core ID' (id + modifier layer) of + * a selected subkey. May be `null`. + */ public resolve(keyCoreID: string) { if(this.resolver) { let keyEvent: text.KeyEvent = null; @@ -69,6 +90,11 @@ namespace com.keyman.osk.embedded { // no-op; it's fully controlled on the app side. } + /** + * Allows this class to detect if the user may have changed their mind and + * re-selected the base key. + * @param touch + */ updateTouch(touch: Touch) { this.baseKeySelected = this.baseKey.key.isUnderTouch(touch); From c6a5961421ae9850802ae870b1f59a900eb2b315 Mon Sep 17 00:00:00 2001 From: jahorton Date: Wed, 30 Jun 2021 12:16:15 +0700 Subject: [PATCH 34/44] feat(web): adds error log to SubkeyPopup.updateTouch --- web/source/osk/browser/subkeyPopup.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/web/source/osk/browser/subkeyPopup.ts b/web/source/osk/browser/subkeyPopup.ts index 85b0371746..3f4b1b3524 100644 --- a/web/source/osk/browser/subkeyPopup.ts +++ b/web/source/osk/browser/subkeyPopup.ts @@ -263,7 +263,13 @@ namespace com.keyman.osk.browser { this.currentSelection = sk; } sk.key.highlight(onKey); - } catch(ex){} + } catch(ex) { + if(ex.message) { + console.error("Unexpected error when attempting to update selected subkey:" + ex.message); + } else { + console.error("Unexpected error (and error type) when attempting to update selected subkey."); + } + } } // Use the popup duplicate of the base key if a phone with a visible popup array From 4f3e37bbb2f7942070ef46dcf990b7869b35e72b Mon Sep 17 00:00:00 2001 From: jahorton Date: Wed, 30 Jun 2021 12:17:52 +0700 Subject: [PATCH 35/44] chore(web): removes dev-artifact comment --- web/source/osk/visualKeyboard.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/web/source/osk/visualKeyboard.ts b/web/source/osk/visualKeyboard.ts index 44024eb918..2095daab74 100644 --- a/web/source/osk/visualKeyboard.ts +++ b/web/source/osk/visualKeyboard.ts @@ -1671,8 +1671,6 @@ namespace com.keyman.osk { // If popup is visible, need to move over popup, not over main keyboard // Could be turned into a browser-longpress specific implementation within browser.PendingLongpress? - // Not completely sure what the correct, generalized abstraction would be for that... - // and this PR's already big enough, anyway. if(key1 && key1['subKeys'] != null) { // Show popup keys immediately if touch moved up towards key array (KMEW-100, Build 353) if((this.touchY - touch.pageY > 5) && this.pendingSubkey && this.pendingSubkey instanceof browser.PendingLongpress) { From 363811ebda0c85ee2a96592b716ee71ad7b433a2 Mon Sep 17 00:00:00 2001 From: jahorton Date: Wed, 30 Jun 2021 12:35:54 +0700 Subject: [PATCH 36/44] fix(web): embedded subkey cancellation --- web/source/osk/embedded/subkeyDelegator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/source/osk/embedded/subkeyDelegator.ts b/web/source/osk/embedded/subkeyDelegator.ts index 207edf7310..3fe6aba144 100644 --- a/web/source/osk/embedded/subkeyDelegator.ts +++ b/web/source/osk/embedded/subkeyDelegator.ts @@ -53,7 +53,7 @@ namespace com.keyman.osk.embedded { // Handle selection of base key underneath the subkey array. keyEvent = this.vkbd.keyEventFromSpec(this.baseKey.key.spec as keyboards.ActiveKey, null); this.baseKey.key.highlight(false); - } else { + } else if(keyCoreID != null) { // This is set with the base key of our current subkey elsewhere within the engine. let baseKey: OSKKeySpec = this.baseKey.key.spec; let selectedKey: OSKKeySpec; From 8aa9f49fd96013d24cc80de71c901233a9348a61 Mon Sep 17 00:00:00 2001 From: jahorton Date: Wed, 30 Jun 2021 13:02:43 +0700 Subject: [PATCH 37/44] change(web): failed subkey lookup - bye bye, catch & try --- web/source/kmwembedded.ts | 11 +---------- web/source/osk/embedded/subkeyDelegator.ts | 3 ++- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/web/source/kmwembedded.ts b/web/source/kmwembedded.ts index 0cc1308a76..393f32727d 100644 --- a/web/source/kmwembedded.ts +++ b/web/source/kmwembedded.ts @@ -367,16 +367,7 @@ namespace com.keyman.text { // This should be set if we're within this method... but it's best to guard against nulls here, just in case. if(osk.vkbd.subkeyGesture) { let gesture = osk.vkbd.subkeyGesture as com.keyman.osk.embedded.SubkeyDelegator; - - try { - // Can trigger an error if the corresponding subkey cannot be found. - gesture.resolve(keyName); - } catch (e) { - let err = e as Error; - // Prevent the Android app from triggering a "fatal error" keyboard reset. - console.warn(err.message); - } - + gesture.resolve(keyName); osk.vkbd.subkeyGesture = null; } else { console.warn("No base key exists for the subkey being executed: '" + origArg + "'"); diff --git a/web/source/osk/embedded/subkeyDelegator.ts b/web/source/osk/embedded/subkeyDelegator.ts index 3fe6aba144..d8c0c638c0 100644 --- a/web/source/osk/embedded/subkeyDelegator.ts +++ b/web/source/osk/embedded/subkeyDelegator.ts @@ -70,7 +70,8 @@ namespace com.keyman.osk.embedded { // While we can't complete successfully, the subkey operation is done; we // should still signal that and update related gesture state management. this.resolver(null); - throw new Error("Could not find subkey '" + keyCoreID + "' under base key '" + baseKey.coreID + "'!"); + console.error("Could not find subkey '" + keyCoreID + "' under base key '" + baseKey.coreID + "'!"); + return; } keyEvent = this.vkbd.keyEventFromSpec(selectedKey as keyboards.ActiveKey, null); From 8d0364016f6dd7812d1c0cbfe05c2df9dcd60f24 Mon Sep 17 00:00:00 2001 From: jahorton Date: Thu, 1 Jul 2021 08:11:43 +0700 Subject: [PATCH 38/44] fix(web): missed a '!' for in-browser base key reselection --- web/source/osk/browser/subkeyPopup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/source/osk/browser/subkeyPopup.ts b/web/source/osk/browser/subkeyPopup.ts index 3f4b1b3524..f9ea22aefb 100644 --- a/web/source/osk/browser/subkeyPopup.ts +++ b/web/source/osk/browser/subkeyPopup.ts @@ -273,7 +273,7 @@ namespace com.keyman.osk.browser { } // Use the popup duplicate of the base key if a phone with a visible popup array - if(this.currentSelection && this.baseKey.key.isUnderTouch(touch)) { + if(!this.currentSelection && this.baseKey.key.isUnderTouch(touch)) { this.baseKey.key.highlight(true); this.currentSelection = this.baseKey; } From b77561893701891ad61effa69da0263f996b308b Mon Sep 17 00:00:00 2001 From: jahorton Date: Fri, 2 Jul 2021 09:09:58 +0700 Subject: [PATCH 39/44] fix(web): embedded ios stuck base key highlights --- web/source/osk/browser/subkeyPopup.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/web/source/osk/browser/subkeyPopup.ts b/web/source/osk/browser/subkeyPopup.ts index f9ea22aefb..9b272f3bab 100644 --- a/web/source/osk/browser/subkeyPopup.ts +++ b/web/source/osk/browser/subkeyPopup.ts @@ -111,6 +111,7 @@ namespace com.keyman.osk.browser { let keyEvent: text.KeyEvent = null; if(this.currentSelection) { keyEvent = this.vkbd.initKeyEvent(this.currentSelection, touch); + this.currentSelection.key.highlight(false); } this.resolver(keyEvent); } From 543812c0d7828bef7bb58f0a7e59a7355849ff96 Mon Sep 17 00:00:00 2001 From: jahorton Date: Fri, 2 Jul 2021 09:25:29 +0700 Subject: [PATCH 40/44] fix(web): missing 'bind' for in-browser resolve --- web/source/osk/browser/pendingLongpress.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/source/osk/browser/pendingLongpress.ts b/web/source/osk/browser/pendingLongpress.ts index 00fca0e500..84b2364dc0 100644 --- a/web/source/osk/browser/pendingLongpress.ts +++ b/web/source/osk/browser/pendingLongpress.ts @@ -40,7 +40,7 @@ namespace com.keyman.osk.browser { _this.resolver = resolve; // After the timeout, it's no longer deferred; it's being fulfilled. // Even if the actual subkey itself is still async. - _this.timerId = window.setTimeout(_this.resolve, _this.popupDelay); + _this.timerId = window.setTimeout(_this.resolve.bind(_this), _this.popupDelay); }); } From c731f28fe08cf74f8b05f2791cb83b46734149e9 Mon Sep 17 00:00:00 2001 From: jahorton Date: Fri, 2 Jul 2021 09:37:30 +0700 Subject: [PATCH 41/44] docs(web): suggested tweaks from review --- web/source/kmwembedded.ts | 19 +++++++++++++------ web/source/osk/embedded/pendingLongpress.ts | 8 +++++--- web/source/osk/embedded/subkeyDelegator.ts | 10 +++++----- web/source/osk/pendingGesture.interface.ts | 4 ++-- web/source/osk/realizedGesture.interface.ts | 4 ++-- 5 files changed, 27 insertions(+), 18 deletions(-) diff --git a/web/source/kmwembedded.ts b/web/source/kmwembedded.ts index 1c8fbbf692..5dc967d4b3 100644 --- a/web/source/kmwembedded.ts +++ b/web/source/kmwembedded.ts @@ -63,6 +63,10 @@ namespace com.keyman.osk { return new embedded.PendingLongpress(this, key); } else { + // When embedded within our Android app, we expect the `oskCreatePopup` function to + // exist; all subkey control is delegated to the app. + // + // No function = big problem. return null; } }; @@ -266,7 +270,7 @@ namespace com.keyman.text { /** * Function called by Android and iOS when a device-implemented keyboard popup - * is displayed or hidden. As this is controlled by the app, it's the perfect + * is displayed or hidden. As this is controlled by the app, we use it as a * trigger for 'embedded'-mode gesture state management. * * @param {boolean} isVisible @@ -278,9 +282,9 @@ namespace com.keyman.text { /* * If a longpress popup was visible, but is no longer, this means that the - * associated longpress gesture was cancelled. It is possible for the base key - * to emit if selected at this time, but only if appropriate - and this is - * managed by the `SubkeyDelegator`. + * associated longpress gesture was cancelled. It is possible for the base + * key to emit if selected at this time; detecton of this is managed by + * the `SubkeyDelegator` class. */ if(!isVisible) { if(gesture) { @@ -294,8 +298,11 @@ namespace com.keyman.text { /* * If the popup was not visible, but now is, that means our previously-pending - * longpress is now 'realized' (complete). Certain aspects of the OSK rely on - * this state information, which will be properly updated by `resolve`. + * longpress is now 'realized' (complete). The OSK relies upon this state + * information, which will be properly updated by `resolve`. + * + * Prominent uses of such state info helps prevent change of base key, key + * previews, and key output from occurring while a subkey popup remains active. */ if(isVisible && pendingLongpress) { // Fulfills the first-stage promise. diff --git a/web/source/osk/embedded/pendingLongpress.ts b/web/source/osk/embedded/pendingLongpress.ts index 3b407cd610..504c60f1e3 100644 --- a/web/source/osk/embedded/pendingLongpress.ts +++ b/web/source/osk/embedded/pendingLongpress.ts @@ -8,9 +8,11 @@ namespace com.keyman.osk.embedded { * serves mostly to communicate longpress state management from the * app to the VisualKeyboard. * - * The `resolve()` function should be triggered, in some fashion, by - * the host app whenever it has recognized a completed longpress and - * will begin displaying its subkey popup. + * The `resolve()` function should be triggered by the host app + * whenever it has recognized a completed longpress and has thus + * begun displaying its subkey popup. (Should these two events + * ever become separated in time, the latter is the more critical + * aspect.) */ export class PendingLongpress implements PendingGesture { private resolver: (delegator: SubkeyDelegator) => void; diff --git a/web/source/osk/embedded/subkeyDelegator.ts b/web/source/osk/embedded/subkeyDelegator.ts index d8c0c638c0..645bb8c6a7 100644 --- a/web/source/osk/embedded/subkeyDelegator.ts +++ b/web/source/osk/embedded/subkeyDelegator.ts @@ -2,9 +2,10 @@ namespace com.keyman.osk.embedded { /** - * As the subkey popup view is handled by the host app when in embedded mode, - * this class represents the fact that KMW has "delegated" subkey UI and - * selection to the host app. Hence, "Delegator", rather than "Popup". + * As the subkey popup view is handled by the host app when in embedded mode + * within our Android app, this class represents the fact that KMW has + * "delegated" subkey UI and selection to the host app. Hence, "Delegator", + * rather than "Popup". * * The `resolve` method should be triggered, in some fashion, by the host app * whenever the user has completed their longpress, potentially selecting @@ -40,7 +41,6 @@ namespace com.keyman.osk.embedded { * * If no subkey is selected but the original base key is, `resolve(null)` * will return a key event corresponding to the base key. - * * @param keyCoreID {string} The 'core ID' (id + modifier layer) of * a selected subkey. May be `null`. @@ -76,7 +76,7 @@ namespace com.keyman.osk.embedded { keyEvent = this.vkbd.keyEventFromSpec(selectedKey as keyboards.ActiveKey, null); keyEvent.vkCode=keyEvent.Lcode; - } + } // else /* if(keyCoreID == null) */ keyEvent = null; // As initialized at the top. this.resolver(keyEvent); } diff --git a/web/source/osk/pendingGesture.interface.ts b/web/source/osk/pendingGesture.interface.ts index 8cea889b89..637b108f58 100644 --- a/web/source/osk/pendingGesture.interface.ts +++ b/web/source/osk/pendingGesture.interface.ts @@ -1,8 +1,8 @@ namespace com.keyman.osk { /** * Used for evaluating potential gestures. Classes adhering to this interface - * should instantiated whenever the (implied) state-machine allows a new touch - * event to mark the start of a potential new gesture. + * should be instantiated whenever the (implied) state-machine allows a new + * touch event to mark the start of a potential new gesture. * * For example, whenever a user touches a base key and there are no "realized" * (fully-completed, but as-of-yet unresolved) gestures, that state allows the diff --git a/web/source/osk/realizedGesture.interface.ts b/web/source/osk/realizedGesture.interface.ts index 6683b4357e..faabf0a367 100644 --- a/web/source/osk/realizedGesture.interface.ts +++ b/web/source/osk/realizedGesture.interface.ts @@ -1,5 +1,5 @@ namespace com.keyman.osk { - /* + /** * Implementations of this interface allow individual types of gestures to * specify any additional user interaction and functionality (which may * include UI elements) appropriate for obtaining a key event that may be @@ -19,7 +19,7 @@ namespace com.keyman.osk { * will be inherently async. The simplest way to model this is with `Promise`s. * * If appropriate for the modeled gesture type, an implementation may supply an - * instantly-resolving `Promise``. This may be appropriate for modeling "flick" + * instantly-resolving `Promise`. This may be appropriate for modeling "flick" * or "swipe" gestures in the future, which may require no additional input once * such a gesture is fully realized. */ From da5c540661c63ee53252fc760e67abd2df8580dd Mon Sep 17 00:00:00 2001 From: jahorton Date: Fri, 2 Jul 2021 09:38:17 +0700 Subject: [PATCH 42/44] change(web): adds error log for missing embed-link func --- web/source/kmwembedded.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/web/source/kmwembedded.ts b/web/source/kmwembedded.ts index 5dc967d4b3..d548909314 100644 --- a/web/source/kmwembedded.ts +++ b/web/source/kmwembedded.ts @@ -67,6 +67,7 @@ namespace com.keyman.osk { // exist; all subkey control is delegated to the app. // // No function = big problem. + console.error("Missing `oskCreatePopup` function for engine integration."); return null; } }; From fefffc80fd2d5b766ae0c6bc0a0821ea11266668 Mon Sep 17 00:00:00 2001 From: jahorton Date: Tue, 6 Jul 2021 14:14:14 +0700 Subject: [PATCH 43/44] fix(web): more consistent clear on base key highlights --- web/source/osk/browser/subkeyPopup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/source/osk/browser/subkeyPopup.ts b/web/source/osk/browser/subkeyPopup.ts index 9b272f3bab..1f7378d936 100644 --- a/web/source/osk/browser/subkeyPopup.ts +++ b/web/source/osk/browser/subkeyPopup.ts @@ -253,6 +253,7 @@ namespace com.keyman.osk.browser { updateTouch(touch: Touch) { this.currentSelection = null; + this.baseKey.key.highlight(false); for(let i=0; i < this.baseKey['subKeys'].length; i++) { try { @@ -260,7 +261,6 @@ namespace com.keyman.osk.browser { let onKey = sk.key.isUnderTouch(touch); if(onKey) { - this.baseKey.key.highlight(false); this.currentSelection = sk; } sk.key.highlight(onKey); From 43d5181092db17c4d7a24799e985a56c8e292a95 Mon Sep 17 00:00:00 2001 From: jahorton Date: Tue, 6 Jul 2021 14:27:37 +0700 Subject: [PATCH 44/44] change(web): more consistent base-key longpress release --- web/source/osk/browser/subkeyPopup.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/web/source/osk/browser/subkeyPopup.ts b/web/source/osk/browser/subkeyPopup.ts index 1f7378d936..23c7e24bee 100644 --- a/web/source/osk/browser/subkeyPopup.ts +++ b/web/source/osk/browser/subkeyPopup.ts @@ -43,7 +43,11 @@ namespace com.keyman.osk.browser { this.vkbd = vkbd; this.baseKey = e; - this.currentSelection = null; + + // If the user doesn't move their finger and releases, we'll output the base key + // by default. + this.currentSelection = e; + e.key.highlight(true); // A tag we directly set on a key element during its construction. let subKeySpec: OSKKeySpec[] = e['subKeys'];