From 28ab0e80753d0307a3f411a3651d03fe6fcc8561 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Fri, 20 Oct 2023 11:25:59 +0700 Subject: [PATCH 01/25] feat(web): start of the gesture preview host --- .../osk/src/input/gestures/browser/keytip.ts | 10 ++++--- .../src/keyboard-layout/gesturePreviewHost.ts | 28 +++++++++++++++++++ .../osk/src/keyboard-layout/oskBaseKey.ts | 21 ++++++++++++++ web/src/resources/osk/kmwosk.css | 21 ++++++++++++++ 4 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts diff --git a/web/src/engine/osk/src/input/gestures/browser/keytip.ts b/web/src/engine/osk/src/input/gestures/browser/keytip.ts index 54eab1ee2c..9e96c69c3b 100644 --- a/web/src/engine/osk/src/input/gestures/browser/keytip.ts +++ b/web/src/engine/osk/src/input/gestures/browser/keytip.ts @@ -2,6 +2,7 @@ import OSKBaseKey from '../../../keyboard-layout/oskBaseKey.js'; import { KeyElement } from '../../../keyElement.js'; import KeyTipInterface from '../../../keytip.interface.js'; import VisualKeyboard from '../../../visualKeyboard.js'; +import { GesturePreviewHost } from '../../../keyboard-layout/gesturePreviewHost.js'; export default class KeyTip implements KeyTipInterface { public readonly element: HTMLDivElement; @@ -18,7 +19,7 @@ export default class KeyTip implements KeyTipInterface { private readonly cap: HTMLDivElement; private readonly tip: HTMLDivElement; - private readonly label: HTMLSpanElement; + private previewHost: HTMLDivElement; private readonly constrain: boolean; @@ -38,11 +39,10 @@ export default class KeyTip implements KeyTipInterface { tipElement.appendChild(this.tip = document.createElement('div')); tipElement.appendChild(this.cap = document.createElement('div')); - this.tip.appendChild(this.label = document.createElement('span')); + this.tip.appendChild(this.previewHost = document.createElement('div')); this.tip.className = 'kmw-keytip-tip'; this.cap.className = 'kmw-keytip-cap'; - this.label.className = 'kmw-keytip-label'; this.constrain = constrain; } @@ -105,7 +105,9 @@ export default class KeyTip implements KeyTipInterface { kts.fontSize = key.key.getIdealFontSize(vkbd, key.key.keyText, scaleStyle, true); } - this.label.textContent = kc.textContent; + const oldHost = this.previewHost; + this.previewHost = (new GesturePreviewHost(key)).element; + this.tip.replaceChild(this.previewHost, oldHost); // Adjust shape if at edges var xOverflow = (canvasWidth - xWidth) / 2; diff --git a/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts b/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts new file mode 100644 index 0000000000..13bb09cc42 --- /dev/null +++ b/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts @@ -0,0 +1,28 @@ +import { ActiveKey } from "@keymanapp/keyboard-processor"; +import { KeyElement } from "../keyElement.js"; + +export class GesturePreviewHost { + private readonly div: HTMLDivElement; + private readonly label: HTMLSpanElement; + + get element(): HTMLDivElement { + return this.div; + } + + constructor(key: KeyElement) { + const keySpec = key.key.spec; + + const base = this.div = document.createElement('div'); + base.className='kmw-gesture-preview'; + base.id = 'kmw-gesture-preview'; + + base.style.pointerEvents='none'; + const label = this.label = document.createElement('span'); + label.className='kmw-gesture-base-label kmw-key-text'; + label.id = label.className; + base.appendChild(label); + + // Re-use the text value from the base key's label. + label.textContent = key.key.label.textContent; + } +} \ No newline at end of file diff --git a/web/src/engine/osk/src/keyboard-layout/oskBaseKey.ts b/web/src/engine/osk/src/keyboard-layout/oskBaseKey.ts index 9c1fb1a019..e38090cb0a 100644 --- a/web/src/engine/osk/src/keyboard-layout/oskBaseKey.ts +++ b/web/src/engine/osk/src/keyboard-layout/oskBaseKey.ts @@ -6,10 +6,12 @@ import { KeyData, KeyElement, link } from '../keyElement.js'; import OSKRow from './oskRow.js'; import VisualKeyboard from '../visualKeyboard.js'; import { ParsedLengthStyle } from '../lengthStyle.js'; +import { GesturePreviewHost } from './gesturePreviewHost.js'; export default class OSKBaseKey extends OSKKey { private capLabel: HTMLDivElement; + private previewHost: HTMLDivElement; public readonly row: OSKRow; constructor(spec: ActiveKey, layer: string, row: OSKRow) { @@ -122,6 +124,10 @@ export default class OSKBaseKey extends OSKKey { // Add text to button and button to placeholder div kDiv.appendChild(btn); + this.previewHost = document.createElement('div'); + this.previewHost.style.display = 'none'; + btn.appendChild(this.previewHost); + // The 'return value' of this process. return this.square = kDiv; } @@ -163,6 +169,21 @@ export default class OSKBaseKey extends OSKKey { return skIcon; } + public highlight(on: boolean): void { + super.highlight(on); + + if(on) { + const oldPreviewHost = this.previewHost; + this.previewHost = (new GesturePreviewHost(this.btn)).element; + this.btn.replaceChild(this.previewHost, oldPreviewHost); + } else { + const oldPreviewHost = this.previewHost; + this.previewHost = document.createElement('div'); + this.previewHost.style.display = 'none'; + this.btn.replaceChild(this.previewHost, oldPreviewHost); + } + } + public refreshLayout(vkbd: VisualKeyboard) { let key = this.spec as ActiveKey; this.square.style.width = vkbd.layoutWidth.scaledBy(key.proportionalWidth).styleString; diff --git a/web/src/resources/osk/kmwosk.css b/web/src/resources/osk/kmwosk.css index 47476c832c..65ebb50efa 100644 --- a/web/src/resources/osk/kmwosk.css +++ b/web/src/resources/osk/kmwosk.css @@ -342,6 +342,25 @@ body div.kmw-key-shift-on span.kmw-key-text {font-family:SpecialOSK !important;f z-index: 10002; } +#kmw-gesture-preview { + position: absolute; + height: 100%; + width: 100%; + display: block; + z-index: 1; + top: 0; + left: 0; + background-color: inherit; +} + +#kmw-gesture-base-label { + top: 50%; + transform: translateY(-50%); + position: absolute; + display: block; + width: 100%; +} + /* Key preview styles */ div.ios div.kmw-keytip, div.android div.kmw-keytip { @@ -399,6 +418,8 @@ div.android div.kmw-keytip { div.android div.kmw-keytip-tip { border-radius: 6px; background: #999; + /* Needed in order to properly 'anchor' the gesture-preview */ + position: relative; } div.android div.kmw-keytip-cap { From 58da9612b0e130c9ee8afbfd9ebe1e7e57964a0c Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Fri, 20 Oct 2023 12:27:07 +0700 Subject: [PATCH 02/25] fix(web): minor goof in last commit --- web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts b/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts index 13bb09cc42..0077a8d072 100644 --- a/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts +++ b/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts @@ -19,7 +19,7 @@ export class GesturePreviewHost { base.style.pointerEvents='none'; const label = this.label = document.createElement('span'); label.className='kmw-gesture-base-label kmw-key-text'; - label.id = label.className; + label.id = 'kmw-gesture-base-label'; base.appendChild(label); // Re-use the text value from the base key's label. From 90f9baf4ddf19f1b8e0662eaaad48bff4a67948e Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Fri, 20 Oct 2023 12:59:25 +0700 Subject: [PATCH 03/25] feat(web): prototype flick preview styling --- .../src/keyboard-layout/gesturePreviewHost.ts | 44 +++++++++++++++++++ web/src/resources/osk/kmwosk.css | 6 +++ 2 files changed, 50 insertions(+) diff --git a/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts b/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts index 0077a8d072..c6fbfba300 100644 --- a/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts +++ b/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts @@ -1,10 +1,14 @@ import { ActiveKey } from "@keymanapp/keyboard-processor"; import { KeyElement } from "../keyElement.js"; +const FLICK_DIRS = ['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw'] as const; + export class GesturePreviewHost { private readonly div: HTMLDivElement; private readonly label: HTMLSpanElement; + private flickPreviews: HTMLDivElement[] = []; + get element(): HTMLDivElement { return this.div; } @@ -24,5 +28,45 @@ export class GesturePreviewHost { // Re-use the text value from the base key's label. label.textContent = key.key.label.textContent; + + for(const dir of FLICK_DIRS) { + const index = FLICK_DIRS.indexOf(dir); + const isDiag = (index % 2) == 1; + + const arrowEle = document.createElement('div'); + arrowEle.className = 'kmw-flick-preview'; + let angle: number; + + if(dir.includes('w')) { + arrowEle.style.left = isDiag ? '15%' : '5%'; + arrowEle.textContent = '\u2329'; + angle = (index - 6) * 45; + } else if(dir.includes('e')) { + arrowEle.style.right = isDiag ? '15%' : '5%'; + arrowEle.textContent = '\u232a'; + angle = (index - 2) * 45; + } else { + arrowEle.style.left = '50%'; + arrowEle.style.transform = 'translateX(-50%)'; + angle = 0; + } + + if(dir.includes('n')) { + arrowEle.style.top = isDiag ? '5%' : '-5%'; + arrowEle.textContent ||= '\ufe3f'; + } else if(dir.includes('s')) { + arrowEle.style.bottom = isDiag ? '5%' : '-5%'; + arrowEle.textContent ||= '\ufe40'; + } else { + arrowEle.style.top = '50%'; + arrowEle.style.transform = 'translateY(-50%) '; + } + + // const angle = (index - 2) * 45; + arrowEle.style.transform = arrowEle.style.transform + `rotate(${angle}deg)`; + + this.flickPreviews.push(arrowEle); + this.div.appendChild(arrowEle); + } } } \ No newline at end of file diff --git a/web/src/resources/osk/kmwosk.css b/web/src/resources/osk/kmwosk.css index 65ebb50efa..847ca08939 100644 --- a/web/src/resources/osk/kmwosk.css +++ b/web/src/resources/osk/kmwosk.css @@ -361,6 +361,12 @@ body div.kmw-key-shift-on span.kmw-key-text {font-family:SpecialOSK !important;f width: 100%; } +.kmw-flick-preview { + position: absolute; + font: 0.333em Arial; + color: #aaa; +} + /* Key preview styles */ div.ios div.kmw-keytip, div.android div.kmw-keytip { From 3f9fd8c99089b1a9c6cc3b34d9f57740f4d7439a Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Fri, 20 Oct 2023 14:44:34 +0700 Subject: [PATCH 04/25] change(web): puts flick previews in multitap ele to prevent overlay --- .../src/keyboard-layout/gesturePreviewHost.ts | 12 +++++++--- web/src/resources/osk/kmwosk.css | 24 ++++++++++++++++++- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts b/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts index c6fbfba300..5c4a44e28e 100644 --- a/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts +++ b/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts @@ -6,6 +6,7 @@ const FLICK_DIRS = ['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw'] as const; export class GesturePreviewHost { private readonly div: HTMLDivElement; private readonly label: HTMLSpanElement; + private readonly previewImgContainer: HTMLDivElement; private flickPreviews: HTMLDivElement[] = []; @@ -29,6 +30,11 @@ export class GesturePreviewHost { // Re-use the text value from the base key's label. label.textContent = key.key.label.textContent; + const multitapPreview = this.previewImgContainer = document.createElement('div'); + this.previewImgContainer.id = 'kmw-preview-img-container'; + this.previewImgContainer.className = 'kmw-multitap-preview'; // to indicate multitap presence. + this.div.appendChild(this.previewImgContainer); + for(const dir of FLICK_DIRS) { const index = FLICK_DIRS.indexOf(dir); const isDiag = (index % 2) == 1; @@ -52,10 +58,10 @@ export class GesturePreviewHost { } if(dir.includes('n')) { - arrowEle.style.top = isDiag ? '5%' : '-5%'; + arrowEle.style.top = isDiag ? '10%' : '0%'; arrowEle.textContent ||= '\ufe3f'; } else if(dir.includes('s')) { - arrowEle.style.bottom = isDiag ? '5%' : '-5%'; + arrowEle.style.bottom = isDiag ? '10%' : '0%'; arrowEle.textContent ||= '\ufe40'; } else { arrowEle.style.top = '50%'; @@ -66,7 +72,7 @@ export class GesturePreviewHost { arrowEle.style.transform = arrowEle.style.transform + `rotate(${angle}deg)`; this.flickPreviews.push(arrowEle); - this.div.appendChild(arrowEle); + multitapPreview.appendChild(arrowEle); } } } \ No newline at end of file diff --git a/web/src/resources/osk/kmwosk.css b/web/src/resources/osk/kmwosk.css index 847ca08939..ac0a7ef8d3 100644 --- a/web/src/resources/osk/kmwosk.css +++ b/web/src/resources/osk/kmwosk.css @@ -350,7 +350,7 @@ body div.kmw-key-shift-on span.kmw-key-text {font-family:SpecialOSK !important;f z-index: 1; top: 0; left: 0; - background-color: inherit; + /* background-color: inherit; */ } #kmw-gesture-base-label { @@ -367,6 +367,28 @@ body div.kmw-key-shift-on span.kmw-key-text {font-family:SpecialOSK !important;f color: #aaa; } +#kmw-preview-img-container { + position: absolute; + top: 0px; + left: 0px; + right: 0px; + bottom: 0px; + + transition: 0.5s linear all; +} + +#kmw-preview-img-container.kmw-multitap-preview { + position: absolute; + margin-top: 1px; + margin-left: 1px; + margin-bottom: 1px; + margin-right: 1px; + border-right: 2px darkgray dotted; + border-bottom: 2px darkgray dotted; + /* border-top: 1px lightgray dotted; */ + /* border-left: 1px lightgray dotted; */ +} + /* Key preview styles */ div.ios div.kmw-keytip, div.android div.kmw-keytip { From 55f004f4c6d82d6126c84627b34b013406220a5c Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Mon, 23 Oct 2023 09:34:01 +0700 Subject: [PATCH 05/25] feat(web): demo flag, styling nuances, ability to fade invalid entries --- .../osk/src/input/gestures/browser/keytip.ts | 18 +- .../osk/src/input/gestures/specsForLayout.ts | 4 + .../src/keyboard-layout/gesturePreviewHost.ts | 158 +++++++++++++----- .../osk/src/keyboard-layout/oskBaseKey.ts | 26 +-- web/src/engine/osk/tsconfig.json | 1 + web/src/resources/osk/kmwosk.css | 24 ++- 6 files changed, 172 insertions(+), 59 deletions(-) diff --git a/web/src/engine/osk/src/input/gestures/browser/keytip.ts b/web/src/engine/osk/src/input/gestures/browser/keytip.ts index 9e96c69c3b..4bbd05a65f 100644 --- a/web/src/engine/osk/src/input/gestures/browser/keytip.ts +++ b/web/src/engine/osk/src/input/gestures/browser/keytip.ts @@ -11,7 +11,7 @@ export default class KeyTip implements KeyTipInterface { // ----- // | | <-- tip - // | x | <-- label + // | x | <-- preview // |_ _| // | | // | | <-- cap @@ -19,7 +19,8 @@ export default class KeyTip implements KeyTipInterface { private readonly cap: HTMLDivElement; private readonly tip: HTMLDivElement; - private previewHost: HTMLDivElement; + private previewHost: GesturePreviewHost; + private preview: HTMLDivElement; private readonly constrain: boolean; @@ -39,7 +40,7 @@ export default class KeyTip implements KeyTipInterface { tipElement.appendChild(this.tip = document.createElement('div')); tipElement.appendChild(this.cap = document.createElement('div')); - this.tip.appendChild(this.previewHost = document.createElement('div')); + this.tip.appendChild(this.preview = document.createElement('div')); this.tip.className = 'kmw-keytip-tip'; this.cap.className = 'kmw-keytip-cap'; @@ -105,9 +106,10 @@ export default class KeyTip implements KeyTipInterface { kts.fontSize = key.key.getIdealFontSize(vkbd, key.key.keyText, scaleStyle, true); } - const oldHost = this.previewHost; - this.previewHost = (new GesturePreviewHost(key)).element; - this.tip.replaceChild(this.previewHost, oldHost); + const oldHost = this.preview; + this.previewHost = new GesturePreviewHost(key, true); + this.preview = this.previewHost.element; + this.tip.replaceChild(this.preview, oldHost); // Adjust shape if at edges var xOverflow = (canvasWidth - xWidth) / 2; @@ -145,6 +147,10 @@ export default class KeyTip implements KeyTipInterface { kts.display = 'block'; } else { // Hide the key preview this.element.style.display = 'none'; + this.previewHost = null; + const oldPreview = this.preview; + this.preview = document.createElement('div'); + this.tip.replaceChild(this.preview, oldPreview); } // Save the key preview state diff --git a/web/src/engine/osk/src/input/gestures/specsForLayout.ts b/web/src/engine/osk/src/input/gestures/specsForLayout.ts index 328c515d71..c7ae74db65 100644 --- a/web/src/engine/osk/src/input/gestures/specsForLayout.ts +++ b/web/src/engine/osk/src/input/gestures/specsForLayout.ts @@ -128,6 +128,10 @@ export function gestureSetForLayout(layerGroup: OSKLayerGroup, params: GesturePa // To be used among the `allowsInitialState` contact-model specifications as needed. const gestureKeyFilter = (key: KeyElement, gestureId: string) => { + if(!key) { + return false; + } + const keySpec = key.key.spec; switch(gestureId) { case 'modipress-start': diff --git a/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts b/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts index 5c4a44e28e..b50cf9470f 100644 --- a/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts +++ b/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts @@ -3,18 +3,35 @@ import { KeyElement } from "../keyElement.js"; const FLICK_DIRS = ['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw'] as const; +const FLICK_PREVIEW_CHAR = { + n: '\ufe3f', + s: '\ufe40', + nw: '\u2329', + w: '\u2329', + sw: '\u232a', + ne: '\u232a', + e: '\u232a', + se: '\u2329' +} + export class GesturePreviewHost { private readonly div: HTMLDivElement; private readonly label: HTMLSpanElement; private readonly previewImgContainer: HTMLDivElement; - private flickPreviews: HTMLDivElement[] = []; + private flickPreviews = new Map; + private lpPreview: HTMLDivElement = null; + private readonly mtStyling: boolean; get element(): HTMLDivElement { return this.div; } - constructor(key: KeyElement) { + constructor(key: KeyElement, isPhone: boolean) { + // Temporary "force all to be on" switch. Is within constructor so it can + // update during a demo. + const DEMO_ALL = false || window['GESTURE_DEMO']; + const keySpec = key.key.spec; const base = this.div = document.createElement('div'); @@ -22,57 +39,122 @@ export class GesturePreviewHost { base.id = 'kmw-gesture-preview'; base.style.pointerEvents='none'; + + // Should probably just be the base element at this point... + // Ah well, only figured this out after experimentation. + const previewImgContainer = this.previewImgContainer = document.createElement('div'); + this.previewImgContainer.id = 'kmw-preview-img-container'; + const label = this.label = document.createElement('span'); label.className='kmw-gesture-base-label kmw-key-text'; label.id = 'kmw-gesture-base-label'; - base.appendChild(label); + previewImgContainer.appendChild(label); // Re-use the text value from the base key's label. label.textContent = key.key.label.textContent; - const multitapPreview = this.previewImgContainer = document.createElement('div'); - this.previewImgContainer.id = 'kmw-preview-img-container'; - this.previewImgContainer.className = 'kmw-multitap-preview'; // to indicate multitap presence. + this.mtStyling = DEMO_ALL || keySpec.multitap; + if(this.mtStyling) { + // Shifts the layout to provide a rough multitap visualization + this.previewImgContainer.className = 'kmw-multitap-preview'; // to indicate multitap presence. + } + this.div.appendChild(this.previewImgContainer); - for(const dir of FLICK_DIRS) { - const index = FLICK_DIRS.indexOf(dir); - const isDiag = (index % 2) == 1; + if(DEMO_ALL || keySpec.flick) { + const flickSpec = keySpec.flick || {}; - const arrowEle = document.createElement('div'); - arrowEle.className = 'kmw-flick-preview'; - let angle: number; + for(const dir of FLICK_DIRS) { + if(DEMO_ALL || (flickSpec[dir])) { + const index = FLICK_DIRS.indexOf(dir); + const isDiag = (index % 2) == 1; - if(dir.includes('w')) { - arrowEle.style.left = isDiag ? '15%' : '5%'; - arrowEle.textContent = '\u2329'; - angle = (index - 6) * 45; - } else if(dir.includes('e')) { - arrowEle.style.right = isDiag ? '15%' : '5%'; - arrowEle.textContent = '\u232a'; - angle = (index - 2) * 45; - } else { - arrowEle.style.left = '50%'; - arrowEle.style.transform = 'translateX(-50%)'; - angle = 0; + const arrowEle = document.createElement('div'); + arrowEle.className = 'kmw-flick-preview'; + arrowEle.textContent = FLICK_PREVIEW_CHAR[dir]; + + let angle: number; + + // The different characters selected here are because of how each is + // spaced on its line; a pure rotation of a single variant fails to + // align the rendered glyphs of opposite sides correctly. + + if(dir.includes('w')) { + arrowEle.style.left = isDiag ? '15%' : '5%'; + angle = (index - 6) * 45; + arrowEle.style.marginTop = isDiag ? '0px' : '-1px'; + } else if(dir.includes('e')) { + arrowEle.style.right = isDiag ? '15%' : '5%'; + angle = (index - 2) * 45; + arrowEle.style.marginTop = isDiag ? '0px' : '-1px'; + } else { + arrowEle.style.left = '50%'; + arrowEle.style.transform = 'translateX(-50%)'; + angle = 0; + } + + const isSouthward = dir.includes('s'); + if(angle && isSouthward) { + angle += 180; + } + + // The two glyphs below may not render identically to their left & right + // variants on certain devices, unfortunately. + if(dir.includes('n')) { + arrowEle.style.top = isDiag ? '10%' : '0%'; + } else if(isSouthward) { + arrowEle.style.bottom = isDiag ? '10%' : '0%'; + } else { + arrowEle.style.top = '50%'; + arrowEle.style.transform = 'translateY(-50%) '; + } + + arrowEle.style.transform = arrowEle.style.transform + `rotate(${angle}deg)`; + + this.flickPreviews.set(dir, arrowEle); + previewImgContainer.appendChild(arrowEle); + } } + } - if(dir.includes('n')) { - arrowEle.style.top = isDiag ? '10%' : '0%'; - arrowEle.textContent ||= '\ufe3f'; - } else if(dir.includes('s')) { - arrowEle.style.bottom = isDiag ? '10%' : '0%'; - arrowEle.textContent ||= '\ufe40'; - } else { - arrowEle.style.top = '50%'; - arrowEle.style.transform = 'translateY(-50%) '; - } + // const neFlick = DEMO_ALL || keySpec.flick && keySpec.flick.ne; + if(DEMO_ALL || keySpec.sk) { + const skIcon = this.lpPreview = document.createElement('div'); + skIcon.className='kmw-key-popup-icon'; - // const angle = (index - 2) * 45; - arrowEle.style.transform = arrowEle.style.transform + `rotate(${angle}deg)`; + // Default positioning puts it far too close to the flick-preview bit. + let yAdjustment = DEMO_ALL || keySpec.multitap ? 1 : 0; + yAdjustment += isPhone ? 2 : 0; + skIcon.style.marginTop = `-${yAdjustment}px`; - this.flickPreviews.push(arrowEle); - multitapPreview.appendChild(arrowEle); + // b/c multitap's border forces position shifting + let xAdjustment = DEMO_ALL || keySpec.multitap ? 3 : 0; + skIcon.style.marginRight = `-${xAdjustment}px`; + + previewImgContainer.appendChild(skIcon); } } + + // These may not exist like this longterm. + private clearMultitap() { + if(this.mtStyling) { + this.previewImgContainer.classList.add('multitap-clear'); + } + } + + private clearFlick() { + for(const pair of this.flickPreviews.entries()) { + pair[1].classList.add('flick-clear'); + } + } + + private clearLongpress() { + this.lpPreview?.classList.add('longpress-clear'); + } + + private clearAll() { + this.clearMultitap(); + this.clearFlick(); + this.clearLongpress(); + } } \ No newline at end of file diff --git a/web/src/engine/osk/src/keyboard-layout/oskBaseKey.ts b/web/src/engine/osk/src/keyboard-layout/oskBaseKey.ts index e38090cb0a..26b17d1d6c 100644 --- a/web/src/engine/osk/src/keyboard-layout/oskBaseKey.ts +++ b/web/src/engine/osk/src/keyboard-layout/oskBaseKey.ts @@ -11,7 +11,9 @@ import { GesturePreviewHost } from './gesturePreviewHost.js'; export default class OSKBaseKey extends OSKKey { private capLabel: HTMLDivElement; - private previewHost: HTMLDivElement; + private previewHost: GesturePreviewHost; + private preview: HTMLDivElement; + public readonly row: OSKRow; constructor(spec: ActiveKey, layer: string, row: OSKRow) { @@ -124,9 +126,9 @@ export default class OSKBaseKey extends OSKKey { // Add text to button and button to placeholder div kDiv.appendChild(btn); - this.previewHost = document.createElement('div'); - this.previewHost.style.display = 'none'; - btn.appendChild(this.previewHost); + this.preview = document.createElement('div'); + this.preview.style.display = 'none'; + btn.appendChild(this.preview); // The 'return value' of this process. return this.square = kDiv; @@ -171,17 +173,17 @@ export default class OSKBaseKey extends OSKKey { public highlight(on: boolean): void { super.highlight(on); + const oldPreview = this.preview; - if(on) { - const oldPreviewHost = this.previewHost; - this.previewHost = (new GesturePreviewHost(this.btn)).element; - this.btn.replaceChild(this.previewHost, oldPreviewHost); + if(on && this.allowsKeyTip()) { + this.previewHost = new GesturePreviewHost(this.btn, false); + this.preview = this.previewHost.element; } else { - const oldPreviewHost = this.previewHost; - this.previewHost = document.createElement('div'); - this.previewHost.style.display = 'none'; - this.btn.replaceChild(this.previewHost, oldPreviewHost); + this.previewHost = null; + this.preview = document.createElement('div'); + this.preview.style.display = 'none'; } + this.btn.replaceChild(this.preview, oldPreview); } public refreshLayout(vkbd: VisualKeyboard) { diff --git a/web/src/engine/osk/tsconfig.json b/web/src/engine/osk/tsconfig.json index a8cfdcacae..325dae0d98 100644 --- a/web/src/engine/osk/tsconfig.json +++ b/web/src/engine/osk/tsconfig.json @@ -3,6 +3,7 @@ "compilerOptions": { "baseUrl": "./", + "downlevelIteration": true, "outDir": "../../../build/engine/osk/obj/", "tsBuildInfoFile": "../../../build/engine/osk/obj/tsconfig.tsbuildinfo", "rootDir": "./src" diff --git a/web/src/resources/osk/kmwosk.css b/web/src/resources/osk/kmwosk.css index ac0a7ef8d3..a81e99084d 100644 --- a/web/src/resources/osk/kmwosk.css +++ b/web/src/resources/osk/kmwosk.css @@ -350,7 +350,8 @@ body div.kmw-key-shift-on span.kmw-key-text {font-family:SpecialOSK !important;f z-index: 1; top: 0; left: 0; - /* background-color: inherit; */ + /* Hides the base key entirely; this prevents artifacting should positioning vary a bit. */ + background-color: inherit; } #kmw-gesture-base-label { @@ -367,6 +368,11 @@ body div.kmw-key-shift-on span.kmw-key-text {font-family:SpecialOSK !important;f color: #aaa; } +.kmw-flick-preview.flick-clear { + color: transparent; + transition: 0.5s linear all; +} + #kmw-preview-img-container { position: absolute; top: 0px; @@ -385,8 +391,20 @@ body div.kmw-key-shift-on span.kmw-key-text {font-family:SpecialOSK !important;f margin-right: 1px; border-right: 2px darkgray dotted; border-bottom: 2px darkgray dotted; - /* border-top: 1px lightgray dotted; */ - /* border-left: 1px lightgray dotted; */ +} + +#kmw-preview-img-container.kmw-multitap-preview.multitap-clear { + /* Because on some devices, 1px border may not equal 1px positioning! + * Use of full transparency removes the (visual) border but keeps + * altered positioning intact, avoiding any animation thereof. + */ + border-right: 2px transparent dotted; + border-bottom: 2px transparent dotted; +} + +.kmw-key-popup-icon.longpress-clear:before { + color: transparent; + transition: 0.5s linear all; } /* Key preview styles */ From efa4806912fb86e7e4da9b4a252e2ce70af38bc2 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Mon, 23 Oct 2023 10:12:19 +0700 Subject: [PATCH 06/25] feat(web): multitap + longpress styling statically positioned, flick part may scroll --- .../src/keyboard-layout/gesturePreviewHost.ts | 13 ++++++---- web/src/resources/osk/kmwosk.css | 24 ++++++++++--------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts b/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts index b50cf9470f..1d686fe1d1 100644 --- a/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts +++ b/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts @@ -40,8 +40,11 @@ export class GesturePreviewHost { base.style.pointerEvents='none'; - // Should probably just be the base element at this point... - // Ah well, only figured this out after experimentation. + // We want this to be distinct from the base element so that we can scroll it; + // this matters greatly for doing flick things. + // + // Note: should probably put multitap style & longpress subkey bit one layer up; + // it looks REALLY odd when the multitap style scrolls. const previewImgContainer = this.previewImgContainer = document.createElement('div'); this.previewImgContainer.id = 'kmw-preview-img-container'; @@ -56,7 +59,7 @@ export class GesturePreviewHost { this.mtStyling = DEMO_ALL || keySpec.multitap; if(this.mtStyling) { // Shifts the layout to provide a rough multitap visualization - this.previewImgContainer.className = 'kmw-multitap-preview'; // to indicate multitap presence. + base.classList.add('kmw-multitap-preview'); // to indicate multitap presence. } this.div.appendChild(this.previewImgContainer); @@ -131,14 +134,14 @@ export class GesturePreviewHost { let xAdjustment = DEMO_ALL || keySpec.multitap ? 3 : 0; skIcon.style.marginRight = `-${xAdjustment}px`; - previewImgContainer.appendChild(skIcon); + base.appendChild(skIcon); } } // These may not exist like this longterm. private clearMultitap() { if(this.mtStyling) { - this.previewImgContainer.classList.add('multitap-clear'); + this.div.classList.add('multitap-clear'); } } diff --git a/web/src/resources/osk/kmwosk.css b/web/src/resources/osk/kmwosk.css index a81e99084d..06fe29e51f 100644 --- a/web/src/resources/osk/kmwosk.css +++ b/web/src/resources/osk/kmwosk.css @@ -344,12 +344,14 @@ body div.kmw-key-shift-on span.kmw-key-text {font-family:SpecialOSK !important;f #kmw-gesture-preview { position: absolute; - height: 100%; - width: 100%; display: block; z-index: 1; - top: 0; - left: 0; + top: 0px; + left: 0px; + right: 0px; + bottom: 0px; + /* Clip anything that 'scrolls' past the preview's boundaries.*/ + overflow: hidden; /* Hides the base key entirely; this prevents artifacting should positioning vary a bit. */ background-color: inherit; } @@ -375,15 +377,15 @@ body div.kmw-key-shift-on span.kmw-key-text {font-family:SpecialOSK !important;f #kmw-preview-img-container { position: absolute; - top: 0px; - left: 0px; - right: 0px; - bottom: 0px; - transition: 0.5s linear all; + /* Facilitates scrolling animation to match a flick */ + height: 100%; + width: 100%; + + transition: 0.5s linear border; } -#kmw-preview-img-container.kmw-multitap-preview { +#kmw-gesture-preview.kmw-multitap-preview { position: absolute; margin-top: 1px; margin-left: 1px; @@ -393,7 +395,7 @@ body div.kmw-key-shift-on span.kmw-key-text {font-family:SpecialOSK !important;f border-bottom: 2px darkgray dotted; } -#kmw-preview-img-container.kmw-multitap-preview.multitap-clear { +#kmw-gesture-preview.kmw-multitap-preview.multitap-clear { /* Because on some devices, 1px border may not equal 1px positioning! * Use of full transparency removes the (visual) border but keeps * altered positioning intact, avoiding any animation thereof. From f6d93540c7fe56b7a402bc7c9890659ccde30c17 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Mon, 23 Oct 2023 10:18:30 +0700 Subject: [PATCH 07/25] fix(web): border-rounding for previews --- web/src/resources/osk/kmwosk.css | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/resources/osk/kmwosk.css b/web/src/resources/osk/kmwosk.css index 06fe29e51f..e4573ac842 100644 --- a/web/src/resources/osk/kmwosk.css +++ b/web/src/resources/osk/kmwosk.css @@ -346,6 +346,7 @@ body div.kmw-key-shift-on span.kmw-key-text {font-family:SpecialOSK !important;f position: absolute; display: block; z-index: 1; + border-radius: inherit; top: 0px; left: 0px; right: 0px; From 1ff8d680438a2f1e19e7731e86f7949733656493 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Tue, 24 Oct 2023 08:31:21 +0700 Subject: [PATCH 08/25] chore(web): post-rebase patchup --- web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts | 2 ++ web/src/resources/osk/kmwosk.css | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts b/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts index 1d686fe1d1..25635429c5 100644 --- a/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts +++ b/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts @@ -124,6 +124,8 @@ export class GesturePreviewHost { if(DEMO_ALL || keySpec.sk) { const skIcon = this.lpPreview = document.createElement('div'); skIcon.className='kmw-key-popup-icon'; + skIcon.textContent = '\u2022'; + skIcon.style.fontWeight='bold'; // Default positioning puts it far too close to the flick-preview bit. let yAdjustment = DEMO_ALL || keySpec.multitap ? 1 : 0; diff --git a/web/src/resources/osk/kmwosk.css b/web/src/resources/osk/kmwosk.css index e4573ac842..c9fd42b5eb 100644 --- a/web/src/resources/osk/kmwosk.css +++ b/web/src/resources/osk/kmwosk.css @@ -405,7 +405,7 @@ body div.kmw-key-shift-on span.kmw-key-text {font-family:SpecialOSK !important;f border-bottom: 2px transparent dotted; } -.kmw-key-popup-icon.longpress-clear:before { +.kmw-key-popup-icon.longpress-clear { color: transparent; transition: 0.5s linear all; } From 5be435b9d3d1408151becad11feaba490c50a7bf Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Tue, 31 Oct 2023 11:38:53 +0700 Subject: [PATCH 09/25] change(web): gesture-preview longevity, causality --- .../osk/src/input/gestures/browser/flick.ts | 8 +- .../osk/src/input/gestures/browser/keytip.ts | 16 ++- .../src/input/gestures/browser/multitap.ts | 1 + .../src/keyboard-layout/gesturePreviewHost.ts | 11 ++ .../osk/src/keyboard-layout/oskBaseKey.ts | 12 +- web/src/engine/osk/src/keytip.interface.ts | 3 +- web/src/engine/osk/src/visualKeyboard.ts | 106 ++++++++++++------ 7 files changed, 112 insertions(+), 45 deletions(-) diff --git a/web/src/engine/osk/src/input/gestures/browser/flick.ts b/web/src/engine/osk/src/input/gestures/browser/flick.ts index 69a98d4a01..e207208278 100644 --- a/web/src/engine/osk/src/input/gestures/browser/flick.ts +++ b/web/src/engine/osk/src/input/gestures/browser/flick.ts @@ -5,7 +5,8 @@ import { ActiveKey, ActiveKeyBase, ActiveSubKey, KeyDistribution } from '@keyman import { ConfigChangeClosure, CumulativePathStats, GestureRecognizerConfiguration, GestureSequence, PaddedZoneSource } from '@keymanapp/gesture-recognizer'; import { GestureHandler } from '../gestureHandler.js'; import { distributionFromDistanceMaps } from '@keymanapp/input-processor'; -import { DEFAULT_GESTURE_PARAMS, GestureParams } from '../specsForLayout.js'; +import { GestureParams } from '../specsForLayout.js'; +import { GesturePreviewHost } from '../../../keyboard-layout/gesturePreviewHost.js'; const OrderedFlickDirections = ['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw'] as const; @@ -40,12 +41,15 @@ export default class Flick implements GestureHandler { configChanger: ConfigChangeClosure, vkbd: VisualKeyboard, e: KeyElement, - gestureParams: GestureParams + gestureParams: GestureParams, + previewHost: GesturePreviewHost ) { this.sequence = sequence; this.gestureParams = gestureParams; this.baseSpec = e.key.spec as ActiveKey; + sequence.on('complete', () => previewHost.cancel()); + // May be worth a temporary alt config: global roaming, rather than auto-canceling. this.baseKeyDistances = vkbd.getSimpleTapCorrectionDistances(sequence.stageReports[0].sources[0].path.stats.initialSample, this.baseSpec) diff --git a/web/src/engine/osk/src/input/gestures/browser/keytip.ts b/web/src/engine/osk/src/input/gestures/browser/keytip.ts index 4bbd05a65f..94d2c9da29 100644 --- a/web/src/engine/osk/src/input/gestures/browser/keytip.ts +++ b/web/src/engine/osk/src/input/gestures/browser/keytip.ts @@ -48,7 +48,7 @@ export default class KeyTip implements KeyTipInterface { this.constrain = constrain; } - show(key: KeyElement, on: boolean, vkbd: VisualKeyboard) { + show(key: KeyElement, on: boolean, vkbd: VisualKeyboard, previewHost: GesturePreviewHost) { // Create and display the preview // If !key.offsetParent, the OSK is probably hidden. Either way, it's a half- // decent null-guard check. @@ -106,11 +106,6 @@ export default class KeyTip implements KeyTipInterface { kts.fontSize = key.key.getIdealFontSize(vkbd, key.key.keyText, scaleStyle, true); } - const oldHost = this.preview; - this.previewHost = new GesturePreviewHost(key, true); - this.preview = this.previewHost.element; - this.tip.replaceChild(this.preview, oldHost); - // Adjust shape if at edges var xOverflow = (canvasWidth - xWidth) / 2; if(xLeft < xOverflow) { @@ -145,6 +140,15 @@ export default class KeyTip implements KeyTipInterface { } kts.display = 'block'; + + const oldHost = this.preview; + this.previewHost = previewHost; + + if(previewHost) { + this.preview = this.previewHost.element; + this.tip.replaceChild(this.preview, oldHost); + previewHost.setCancellationHandler(() => this.show(null, false, vkbd, null)); + } } else { // Hide the key preview this.element.style.display = 'none'; this.previewHost = null; diff --git a/web/src/engine/osk/src/input/gestures/browser/multitap.ts b/web/src/engine/osk/src/input/gestures/browser/multitap.ts index 8f652e5a4f..58c9238cbf 100644 --- a/web/src/engine/osk/src/input/gestures/browser/multitap.ts +++ b/web/src/engine/osk/src/input/gestures/browser/multitap.ts @@ -101,6 +101,7 @@ export default class Multitap implements GestureHandler { } keyEvent.keyDistribution = this.currentStageKeyDistribution(baseDistances); + // TODO for future: multitap previews. vkbd.raiseKeyEvent(keyEvent, null); // Now that the key has been processed, with a layer possibly changed as a result... diff --git a/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts b/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts index 25635429c5..3c0fb68670 100644 --- a/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts +++ b/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts @@ -23,6 +23,8 @@ export class GesturePreviewHost { private lpPreview: HTMLDivElement = null; private readonly mtStyling: boolean; + private onCancel: () => void; + get element(): HTMLDivElement { return this.div; } @@ -140,6 +142,15 @@ export class GesturePreviewHost { } } + public cancel() { + this.onCancel?.(); + this.onCancel = null; + } + + public setCancellationHandler(handler: () => void) { + this.onCancel = handler; + } + // These may not exist like this longterm. private clearMultitap() { if(this.mtStyling) { diff --git a/web/src/engine/osk/src/keyboard-layout/oskBaseKey.ts b/web/src/engine/osk/src/keyboard-layout/oskBaseKey.ts index 26b17d1d6c..69e91efdd1 100644 --- a/web/src/engine/osk/src/keyboard-layout/oskBaseKey.ts +++ b/web/src/engine/osk/src/keyboard-layout/oskBaseKey.ts @@ -171,18 +171,22 @@ export default class OSKBaseKey extends OSKKey { return skIcon; } - public highlight(on: boolean): void { - super.highlight(on); + public setPreview(previewHost: GesturePreviewHost) { const oldPreview = this.preview; - if(on && this.allowsKeyTip()) { - this.previewHost = new GesturePreviewHost(this.btn, false); + if(previewHost) { + this.previewHost = previewHost; this.preview = this.previewHost.element; } else { this.previewHost = null; this.preview = document.createElement('div'); this.preview.style.display = 'none'; } + + previewHost.setCancellationHandler(() => { + this.setPreview(null); + }); + this.btn.replaceChild(this.preview, oldPreview); } diff --git a/web/src/engine/osk/src/keytip.interface.ts b/web/src/engine/osk/src/keytip.interface.ts index d3bb630d8b..f21eb74d63 100644 --- a/web/src/engine/osk/src/keytip.interface.ts +++ b/web/src/engine/osk/src/keytip.interface.ts @@ -1,3 +1,4 @@ +import { GesturePreviewHost } from "./keyboard-layout/gesturePreviewHost.js"; import { KeyElement } from "./keyElement.js"; import VisualKeyboard from "./visualKeyboard.js"; @@ -6,5 +7,5 @@ export default interface KeyTip { state: boolean; element?: HTMLDivElement; - show(key: KeyElement, on: boolean, vkbd: VisualKeyboard); + show(key: KeyElement, on: boolean, vkbd: VisualKeyboard, previewHost: GesturePreviewHost); } diff --git a/web/src/engine/osk/src/visualKeyboard.ts b/web/src/engine/osk/src/visualKeyboard.ts index f76e8adca8..3e4afa3911 100644 --- a/web/src/engine/osk/src/visualKeyboard.ts +++ b/web/src/engine/osk/src/visualKeyboard.ts @@ -56,6 +56,8 @@ import Multitap from './input/gestures/browser/multitap.js'; import { GestureHandler } from './input/gestures/gestureHandler.js'; import Modipress from './input/gestures/browser/modipress.js'; import Flick from './input/gestures/browser/flick.js'; +import { GesturePreviewHost } from './keyboard-layout/gesturePreviewHost.js'; +import OSKBaseKey from './keyboard-layout/oskBaseKey.js'; interface KeyRuleEffects { contextToken?: number, @@ -210,6 +212,7 @@ export default class VisualKeyboard extends EventEmitter implements Ke // Popup key management keytip: KeyTip; + gesturePreviewHost: GesturePreviewHost; globeHint: GlobeHint; activeGestures: GestureHandler[] = []; @@ -388,7 +391,8 @@ export default class VisualKeyboard extends EventEmitter implements Ke const sourceTrackingMap: Record, roamingHighlightHandler: (sample: InputSample) => void, - key: KeyElement + key: KeyElement, + previewHost: GesturePreviewHost }> = {}; const gestureHandlerMap = new Map, GestureHandler[]>(); @@ -396,6 +400,14 @@ export default class VisualKeyboard extends EventEmitter implements Ke // Now to set up event-handling links. // This handler should probably vary based on the keyboard: do we allow roaming touches or not? recognizer.on('inputstart', (source) => { + // Yay for closure-capture mechanics: we can "keep a lock" on this newly-starting + // gesture's highlighted key here. + const previewHost = this.highlightKey(source.currentSample.item, true); + if(previewHost) { + this.gesturePreviewHost?.cancel(); + this.gesturePreviewHost = previewHost; + } + // Make sure we're tracking the source and its currently-selected item (the latter, as we're // highlighting it) const trackingEntry = sourceTrackingMap[source.identifier] = { @@ -407,17 +419,21 @@ export default class VisualKeyboard extends EventEmitter implements Ke if(key != oldKey) { this.highlightKey(oldKey, false); - this.highlightKey(key, true); + this.gesturePreviewHost?.cancel(); + + const previewHost = this.highlightKey(key, true); + if(previewHost) { + this.gesturePreviewHost = previewHost; + } + + trackingEntry.previewHost = previewHost; sourceTrackingMap[source.identifier].key = key; } }, - key: source.currentSample.item + key: source.currentSample.item, + previewHost: previewHost } - // Yay for closure-capture mechanics: we can "keep a lock" on this newly-starting - // gesture's highlighted key here. - this.highlightKey(trackingEntry.key, true); - const endHighlighting = () => { if(trackingEntry.key) { this.highlightKey(trackingEntry.key, false); @@ -458,12 +474,18 @@ export default class VisualKeyboard extends EventEmitter implements Ke // Multitouch does reference tracking data for a source after its completion, // but only while still permitting new touches. If we're here, that time is over. for(let id of gestureSequence.allSourceIds) { + // If the original preview host lives on, ensure it's cancelled now. + sourceTrackingMap[id].previewHost?.cancel(); delete sourceTrackingMap[id]; } }); // This should probably vary based on the type of gesture. gestureSequence.on('stage', (gestureStage, configChanger) => { + const existingPreviewHost = gestureSequence.allSourceIds.map((id) => { + return sourceTrackingMap[id].previewHost; + }).find((obj) => !!obj); + let handlers: GestureHandler[] = gestureHandlerMap.get(gestureSequence); // Disable roaming-touch highlighting (and current highlighting) for all @@ -552,6 +574,9 @@ export default class VisualKeyboard extends EventEmitter implements Ke // specialized handlers for the remainder of the sequence. // Should work for modipresses, too... I think. if(gestureStage.matchedId == 'special-key-start' && gestureKey.key.spec.baseKeyID == 'K_BKSP') { + // There shouldn't be a preview host for special keys... but it doesn't hurt to add the check. + existingPreviewHost?.cancel(); + // Possible enhancement: maybe update the held location for the backspace if there's movement? // But... that seems pretty low-priority. // @@ -559,6 +584,8 @@ export default class VisualKeyboard extends EventEmitter implements Ke // handle everything that remains for the backspace from here. handlers = [new HeldRepeater(gestureSequence, () => this.modelKeyClick(gestureKey, coord))]; } else if(gestureStage.matchedId.indexOf('longpress') > -1) { + existingPreviewHost?.cancel(); + // Matches: 'longpress', 'longpress-reset'. // Likewise. handlers = [new SubkeyPopup( @@ -569,6 +596,10 @@ export default class VisualKeyboard extends EventEmitter implements Ke this.gestureParams )]; } else if(baseItem?.key.spec.multitap && (gestureStage.matchedId == 'initial-tap' || gestureStage.matchedId == 'multitap' || gestureStage.matchedId == 'modipress-start')) { + // For now, but worth changing later! + // Idea: if the preview weren't hosted by the key, but instead had a key-lookalike overlay. + // Then it would float above any layer, even after layer swaps. + existingPreviewHost?.cancel(); // Likewise - mere construction is enough. handlers = [new Multitap(gestureSequence, this, baseItem, keyResult.contextToken)]; } else if(gestureStage.matchedId.indexOf('flick') > -1) { @@ -577,11 +608,13 @@ export default class VisualKeyboard extends EventEmitter implements Ke configChanger, this, gestureSequence.stageReports[0].sources[0].baseItem, - this.gestureParams + this.gestureParams, + existingPreviewHost )]; - } + } else if(gestureStage.matchedId.includes('modipress') && gestureStage.matchedId.includes('-start')) { + // There shouldn't be a preview host for modipress keys... but it doesn't hurt to add the check. + existingPreviewHost?.cancel(); - if(gestureStage.matchedId.includes('modipress') && gestureStage.matchedId.includes('-start')) { if(this.layerLocked) { console.warn("Unexpected state: modipress start attempt during an active modipress"); } else { @@ -600,6 +633,9 @@ export default class VisualKeyboard extends EventEmitter implements Ke handlers.push(modipressHandler); this.activeModipress = modipressHandler; } + } else { + // Probably an initial-tap or a simple-tap. + existingPreviewHost?.cancel(); } if(handlers) { @@ -614,12 +650,9 @@ export default class VisualKeyboard extends EventEmitter implements Ke if(handler instanceof Modipress) { handler.cancel(); } - }) + }); }); } - - // TODO: depending upon the gesture type, what sort of UI shifts should happen to - // facilitate follow-up stages? }) }); @@ -1044,20 +1077,28 @@ export default class VisualKeyboard extends EventEmitter implements Ke * @param {Object} key key affected * @param {boolean} on add or remove highlighting **/ - highlightKey(key: KeyElement, on: boolean) { + highlightKey(key: KeyElement, on: boolean): GesturePreviewHost { // Do not change element class unless a key if (!key || !key.key || (key.className == '') || (key.className.indexOf('kmw-key-row') >= 0)) return; // For phones, use key preview rather than highlighting the key, - var usePreview = (this.keytip != null) && key.key.allowsKeyTip(); + const usePreview = key.key.allowsKeyTip(); + const modalVizActive = this.activeGestures.find((handler) => handler.hasModalVisualization); + + // If the subkey menu (or a different modal visualization) is active, do not show the key tip - + // even if for a different contact point. + on = modalVizActive ? false : on; + + if(!on) { + key.key.highlight(on); + return null; + } if (usePreview) { - this.showKeyTip(key, on); - } else { - // No key tip should be shown. In some cases (e.g. multitap), we - // may still have a tip visible so let's always hide in that case - this.showKeyTip(null, false); key.key.highlight(on); + return this.showGesturePreview(key); + } else { + return null; } } @@ -1447,25 +1488,26 @@ export default class VisualKeyboard extends EventEmitter implements Ke }; /** - * Add (or remove) the keytip preview (if KeymanWeb on a phone device) + * Add (or remove) the gesture preview (if KeymanWeb on a phone device) * * @param {Object} key HTML key element * @param {boolean} on show or hide + * @returns A GesturePreviewHost instance usable for visualizing a gesture. */ - showKeyTip(key: KeyElement, on: boolean) { - var tip = this.keytip; + showGesturePreview(key: KeyElement) { + const tip = this.keytip; + + const previewHost = new GesturePreviewHost(key, !!tip); if (tip == null) { + const baseKey = key.key as OSKBaseKey; + baseKey.setPreview(previewHost); return; + } else { + tip.show(key, true, this, previewHost); } - const modalVizActive = this.activeGestures.find((handler) => handler.hasModalVisualization); - - // If the subkey menu (or a different modal visualization) is active, do not show the key tip - - // even if for a different contact point. - on = modalVizActive ? false : on; - - tip.show(key, on, this); + return previewHost; }; /** @@ -1510,7 +1552,7 @@ export default class VisualKeyboard extends EventEmitter implements Ke window.clearTimeout(this.deleting); } - this.keytip?.show(null, false, this); + this.keytip?.show(null, false, this, null); } lockLayer(enable: boolean) { From 9ef0ad385fce5ce14b140385517ec71b6d1ec691 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Tue, 31 Oct 2023 12:05:22 +0700 Subject: [PATCH 10/25] fix(web): no roaming preview with flicks --- web/src/engine/osk/src/visualKeyboard.ts | 50 +++++++++++++----------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/web/src/engine/osk/src/visualKeyboard.ts b/web/src/engine/osk/src/visualKeyboard.ts index 3e4afa3911..3822f32808 100644 --- a/web/src/engine/osk/src/visualKeyboard.ts +++ b/web/src/engine/osk/src/visualKeyboard.ts @@ -412,7 +412,33 @@ export default class VisualKeyboard extends EventEmitter implements Ke // highlighting it) const trackingEntry = sourceTrackingMap[source.identifier] = { source: source, - roamingHighlightHandler: (sample) => { + roamingHighlightHandler: null, + key: source.currentSample.item, + previewHost: previewHost + } + + const endHighlighting = () => { + trackingEntry.previewHost?.cancel(); + if(trackingEntry.key) { + this.highlightKey(trackingEntry.key, false); + trackingEntry.key = null; + } + } + + // Note: GestureSource does not currently auto-terminate if there are no + // remaining matchable gestures. Though, we shouldn't facilitate roaming + // anyway if we've turned it off. + if(this.kbdLayout.hasFlicks) { + trackingEntry.roamingHighlightHandler = (sample) => { + const key = sample.item; + const oldKey = sourceTrackingMap[source.identifier].key; + + if(key != oldKey) { + endHighlighting(); + } + }; + } else { + trackingEntry.roamingHighlightHandler = (sample) => { // Maintain highlighting const key = sample.item; const oldKey = sourceTrackingMap[source.identifier].key; @@ -429,32 +455,12 @@ export default class VisualKeyboard extends EventEmitter implements Ke trackingEntry.previewHost = previewHost; sourceTrackingMap[source.identifier].key = key; } - }, - key: source.currentSample.item, - previewHost: previewHost - } - - const endHighlighting = () => { - if(trackingEntry.key) { - this.highlightKey(trackingEntry.key, false); - trackingEntry.key = null; } } source.path.on('invalidated', endHighlighting); source.path.on('complete', endHighlighting); - - // TODO: any other 'invalidated' / 'complete' handling needed? - // If so, separate handler - it likely needs to be disabled once the first gesture-component - // match happens, unlike the highlighting part. - source.path.on('step', trackingEntry.roamingHighlightHandler); - - source.path.on('step', (sample) => { - // // Do... something based on the potential gesture types that could arise, as appropriate. - // // Should be useful for selecting a hint type, etc. - // source.potentialModelMatchIds - }) }); // @@ -483,7 +489,7 @@ export default class VisualKeyboard extends EventEmitter implements Ke // This should probably vary based on the type of gesture. gestureSequence.on('stage', (gestureStage, configChanger) => { const existingPreviewHost = gestureSequence.allSourceIds.map((id) => { - return sourceTrackingMap[id].previewHost; + return sourceTrackingMap[id]?.previewHost; }).find((obj) => !!obj); let handlers: GestureHandler[] = gestureHandlerMap.get(gestureSequence); From db77fa1ada27879217a9f6be87dcbda85a98f14a Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Tue, 31 Oct 2023 12:13:08 +0700 Subject: [PATCH 11/25] change(web): trims previous preview styling --- .../src/keyboard-layout/gesturePreviewHost.ts | 61 ++++++------------- web/src/resources/osk/kmwosk.css | 21 +------ 2 files changed, 19 insertions(+), 63 deletions(-) diff --git a/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts b/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts index 3c0fb68670..cc3d782fea 100644 --- a/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts +++ b/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts @@ -20,8 +20,7 @@ export class GesturePreviewHost { private readonly previewImgContainer: HTMLDivElement; private flickPreviews = new Map; - private lpPreview: HTMLDivElement = null; - private readonly mtStyling: boolean; + private hintLabel: HTMLDivElement = null; private onCancel: () => void; @@ -30,10 +29,6 @@ export class GesturePreviewHost { } constructor(key: KeyElement, isPhone: boolean) { - // Temporary "force all to be on" switch. Is within constructor so it can - // update during a demo. - const DEMO_ALL = false || window['GESTURE_DEMO']; - const keySpec = key.key.spec; const base = this.div = document.createElement('div'); @@ -44,9 +39,6 @@ export class GesturePreviewHost { // We want this to be distinct from the base element so that we can scroll it; // this matters greatly for doing flick things. - // - // Note: should probably put multitap style & longpress subkey bit one layer up; - // it looks REALLY odd when the multitap style scrolls. const previewImgContainer = this.previewImgContainer = document.createElement('div'); this.previewImgContainer.id = 'kmw-preview-img-container'; @@ -58,19 +50,13 @@ export class GesturePreviewHost { // Re-use the text value from the base key's label. label.textContent = key.key.label.textContent; - this.mtStyling = DEMO_ALL || keySpec.multitap; - if(this.mtStyling) { - // Shifts the layout to provide a rough multitap visualization - base.classList.add('kmw-multitap-preview'); // to indicate multitap presence. - } - this.div.appendChild(this.previewImgContainer); - if(DEMO_ALL || keySpec.flick) { + if(keySpec.flick) { const flickSpec = keySpec.flick || {}; for(const dir of FLICK_DIRS) { - if(DEMO_ALL || (flickSpec[dir])) { + if(flickSpec[dir]) { const index = FLICK_DIRS.indexOf(dir); const isDiag = (index % 2) == 1; @@ -122,24 +108,20 @@ export class GesturePreviewHost { } } - // const neFlick = DEMO_ALL || keySpec.flick && keySpec.flick.ne; - if(DEMO_ALL || keySpec.sk) { - const skIcon = this.lpPreview = document.createElement('div'); - skIcon.className='kmw-key-popup-icon'; - skIcon.textContent = '\u2022'; - skIcon.style.fontWeight='bold'; + // const hintLabel = this.hintLabel = document.createElement('div'); + // hintLabel.className='kmw-key-popup-icon'; + // hintLabel.textContent = keySpec == keySpec.hintSrc ? keySpec.hint : keySpec.hintSrc?.text; + // hintLabel.style.fontWeight= hintLabel.textContent == '\u2022' ? 'bold' : ''; - // Default positioning puts it far too close to the flick-preview bit. - let yAdjustment = DEMO_ALL || keySpec.multitap ? 1 : 0; - yAdjustment += isPhone ? 2 : 0; - skIcon.style.marginTop = `-${yAdjustment}px`; + // // Default positioning puts it far too close to the flick-preview bit. + // let yAdjustment = 0; + // hintLabel.style.marginTop = `-${yAdjustment}px`; - // b/c multitap's border forces position shifting - let xAdjustment = DEMO_ALL || keySpec.multitap ? 3 : 0; - skIcon.style.marginRight = `-${xAdjustment}px`; + // // b/c multitap's border forces position shifting + // let xAdjustment = 0; + // hintLabel.style.marginRight = `-${xAdjustment}px`; - base.appendChild(skIcon); - } + // base.appendChild(hintLabel); } public cancel() { @@ -152,25 +134,18 @@ export class GesturePreviewHost { } // These may not exist like this longterm. - private clearMultitap() { - if(this.mtStyling) { - this.div.classList.add('multitap-clear'); - } - } - private clearFlick() { for(const pair of this.flickPreviews.entries()) { pair[1].classList.add('flick-clear'); } } - private clearLongpress() { - this.lpPreview?.classList.add('longpress-clear'); + private clearHint() { + this.hintLabel?.classList.add('hint-clear'); } - private clearAll() { - this.clearMultitap(); + public clearAll() { this.clearFlick(); - this.clearLongpress(); + this.clearHint(); } } \ No newline at end of file diff --git a/web/src/resources/osk/kmwosk.css b/web/src/resources/osk/kmwosk.css index c9fd42b5eb..c5679c3d0f 100644 --- a/web/src/resources/osk/kmwosk.css +++ b/web/src/resources/osk/kmwosk.css @@ -386,26 +386,7 @@ body div.kmw-key-shift-on span.kmw-key-text {font-family:SpecialOSK !important;f transition: 0.5s linear border; } -#kmw-gesture-preview.kmw-multitap-preview { - position: absolute; - margin-top: 1px; - margin-left: 1px; - margin-bottom: 1px; - margin-right: 1px; - border-right: 2px darkgray dotted; - border-bottom: 2px darkgray dotted; -} - -#kmw-gesture-preview.kmw-multitap-preview.multitap-clear { - /* Because on some devices, 1px border may not equal 1px positioning! - * Use of full transparency removes the (visual) border but keeps - * altered positioning intact, avoiding any animation thereof. - */ - border-right: 2px transparent dotted; - border-bottom: 2px transparent dotted; -} - -.kmw-key-popup-icon.longpress-clear { +.kmw-key-popup-icon.hint-clear { color: transparent; transition: 0.5s linear all; } From 34837810dd13994fc77cf07f4693e2bee65f9286 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Tue, 31 Oct 2023 13:11:14 +0700 Subject: [PATCH 12/25] feat(web): first scrolling flick preview --- .../osk/src/input/gestures/browser/flick.ts | 64 ++++++++--- .../src/keyboard-layout/gesturePreviewHost.ts | 107 ++++++++---------- web/src/engine/osk/src/visualKeyboard.ts | 5 +- web/src/resources/osk/kmwosk.css | 7 -- 4 files changed, 100 insertions(+), 83 deletions(-) diff --git a/web/src/engine/osk/src/input/gestures/browser/flick.ts b/web/src/engine/osk/src/input/gestures/browser/flick.ts index e207208278..0c439ed89a 100644 --- a/web/src/engine/osk/src/input/gestures/browser/flick.ts +++ b/web/src/engine/osk/src/input/gestures/browser/flick.ts @@ -8,7 +8,20 @@ import { distributionFromDistanceMaps } from '@keymanapp/input-processor'; import { GestureParams } from '../specsForLayout.js'; import { GesturePreviewHost } from '../../../keyboard-layout/gesturePreviewHost.js'; -const OrderedFlickDirections = ['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw'] as const; +export const OrderedFlickDirections = ['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw'] as const; + +const PI = Math.PI; + +export const FlickNameCoordMap = (() => { + const map = new Map(); + + const angleIncrement = PI / 4; + for(let i = 0; i < OrderedFlickDirections.length; i++) { + map.set(OrderedFlickDirections[i], [angleIncrement * i, 1]); + } + + return map; +})(); /** * The maximum angle-difference, in radians, allowed before a potential flick @@ -53,6 +66,7 @@ export default class Flick implements GestureHandler { // May be worth a temporary alt config: global roaming, rather than auto-canceling. this.baseKeyDistances = vkbd.getSimpleTapCorrectionDistances(sequence.stageReports[0].sources[0].path.stats.initialSample, this.baseSpec) + const baseSource = sequence.stageReports[0].sources[0].baseSource; const baseSource = sequence.stageReports[0].sources[0].baseSource; this.sequence.on('stage', (result) => { @@ -73,6 +87,32 @@ export default class Flick implements GestureHandler { vkbd.raiseKeyEvent(keyEvent, null); }); + const baseCoord = baseSource.path.coords[0]; + baseSource.path.on('step', (coord) => { + const deltaX = coord.targetX - baseCoord.targetX; + const deltaY = coord.targetY - baseCoord.targetY; + + const sqDist = deltaX * deltaX + deltaY * deltaY; + + /* + * Accomplishes two things: + * 1) Ensures the coordinates for flick-preview scrolling don't overshoot + * the preview key-cap + * 2) While allowing for _undershoot_ if "not quite there yet" + */ + let divisor = Math.sqrt(sqDist); + const FUDGE_FACTOR = 1.2; + const FULL_SCROLL_MAG = FUDGE_FACTOR * gestureParams.flick.triggerDist; + if(divisor < FULL_SCROLL_MAG) { + divisor = FULL_SCROLL_MAG; + } + + const previewX = deltaX / divisor; + const previewY = deltaY / divisor; + + previewHost?.scrollFlickPreview(previewX, previewY); + }); + // Be sure to extend roaming bounds a bit more than usual for flicks, as they can be quick motions. const altConfig = this.buildPopupRecognitionConfig(vkbd); configChanger({ @@ -142,20 +182,12 @@ export default class Flick implements GestureHandler { coord: [NaN, 0] }]; - const PI = Math.PI; - - const angleIncrement = PI / 4; - for(let i = 0; i < OrderedFlickDirections.length; i++) { - const spec = flickSet[OrderedFlickDirections[i]] as ActiveSubKey; - if(spec) { - keys.push({ - spec: spec, - // Greatest possible angle difference: Math.PI (180 degrees) - // So we'll scale the distance accordingly. - coord: [angleIncrement * i, 1] - }); - } - } + keys = keys.concat(Object.keys(flickSet).map((dir: (typeof OrderedFlickDirections[number])) => { + return { + spec: flickSet[dir] as ActiveSubKey, + coord: FlickNameCoordMap.get(dir) + }; + })); const angle = pathStats.angle; const TRIGGER_DIST = this.gestureParams.flick.triggerDist; @@ -170,7 +202,7 @@ export default class Flick implements GestureHandler { const coord = entry.coord; if(!isNaN(coord[0])) { const angleDelta1 = angle - coord[0]; - const angleDelta2 = 2*PI + coord[0] - angle; // because of angle wrap-around. + const angleDelta2 = 2 * PI + coord[0] - angle; // because of angle wrap-around. // NOTE: max linear angle dist: PI. angleDist = Math.min(angleDelta1 * angleDelta1, angleDelta2 * angleDelta2); diff --git a/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts b/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts index cc3d782fea..a93c93f639 100644 --- a/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts +++ b/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts @@ -1,19 +1,8 @@ import { ActiveKey } from "@keymanapp/keyboard-processor"; import { KeyElement } from "../keyElement.js"; +import { FlickNameCoordMap, OrderedFlickDirections } from "../input/gestures/browser/flick.js"; const FLICK_DIRS = ['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw'] as const; - -const FLICK_PREVIEW_CHAR = { - n: '\ufe3f', - s: '\ufe40', - nw: '\u2329', - w: '\u2329', - sw: '\u232a', - ne: '\u232a', - e: '\u232a', - se: '\u2329' -} - export class GesturePreviewHost { private readonly div: HTMLDivElement; private readonly label: HTMLSpanElement; @@ -21,6 +10,7 @@ export class GesturePreviewHost { private flickPreviews = new Map; private hintLabel: HTMLDivElement = null; + private flickEdgeLength: number; private onCancel: () => void; @@ -28,8 +18,9 @@ export class GesturePreviewHost { return this.div; } - constructor(key: KeyElement, isPhone: boolean) { + constructor(key: KeyElement, isPhone: boolean, edgeLength: number) { const keySpec = key.key.spec; + this.flickEdgeLength = edgeLength; const base = this.div = document.createElement('div'); base.className='kmw-gesture-preview'; @@ -55,57 +46,43 @@ export class GesturePreviewHost { if(keySpec.flick) { const flickSpec = keySpec.flick || {}; - for(const dir of FLICK_DIRS) { - if(flickSpec[dir]) { - const index = FLICK_DIRS.indexOf(dir); - const isDiag = (index % 2) == 1; + Object.keys(flickSpec).forEach((dir: typeof OrderedFlickDirections[number]) => { + const flickPreview = document.createElement('div'); + flickPreview.className = 'kmw-flick-preview kmw-key-text'; + flickPreview.textContent = flickSpec[dir].text; - const arrowEle = document.createElement('div'); - arrowEle.className = 'kmw-flick-preview'; - arrowEle.textContent = FLICK_PREVIEW_CHAR[dir]; + const ps /* preview style */ = flickPreview.style; - let angle: number; + const OVERFLOW_OFFSET = 1.41; //141; - // The different characters selected here are because of how each is - // spaced on its line; a pure rotation of a single variant fails to - // align the rendered glyphs of opposite sides correctly. + // is in polar coords, origin toward north, clockwise. + const coords = FlickNameCoordMap.get(dir); + const x = Math.sin(coords[0]); + const y = -Math.cos(coords[0]); - if(dir.includes('w')) { - arrowEle.style.left = isDiag ? '15%' : '5%'; - angle = (index - 6) * 45; - arrowEle.style.marginTop = isDiag ? '0px' : '-1px'; - } else if(dir.includes('e')) { - arrowEle.style.right = isDiag ? '15%' : '5%'; - angle = (index - 2) * 45; - arrowEle.style.marginTop = isDiag ? '0px' : '-1px'; - } else { - arrowEle.style.left = '50%'; - arrowEle.style.transform = 'translateX(-50%)'; - angle = 0; - } - - const isSouthward = dir.includes('s'); - if(angle && isSouthward) { - angle += 180; - } - - // The two glyphs below may not render identically to their left & right - // variants on certain devices, unfortunately. - if(dir.includes('n')) { - arrowEle.style.top = isDiag ? '10%' : '0%'; - } else if(isSouthward) { - arrowEle.style.bottom = isDiag ? '10%' : '0%'; - } else { - arrowEle.style.top = '50%'; - arrowEle.style.transform = 'translateY(-50%) '; - } - - arrowEle.style.transform = arrowEle.style.transform + `rotate(${angle}deg)`; - - this.flickPreviews.set(dir, arrowEle); - previewImgContainer.appendChild(arrowEle); + if(x < 0) { + ps.right = (-x * OVERFLOW_OFFSET * edgeLength) + 'px'; + } else if(x > 0) { + ps.left = ( x * OVERFLOW_OFFSET * edgeLength) + 'px'; + } else { + ps.left = '0px'; + ps.right = '0px'; + ps.textAlign = 'center'; } - } + + if(y < 0) { + ps.bottom = (-y * OVERFLOW_OFFSET * edgeLength) + 'px'; + } else if(y > 0) { + ps.top = ( y * OVERFLOW_OFFSET * edgeLength) + 'px'; + } else { + ps.top = '0px'; + ps.bottom = '0px'; + ps.lineHeight = '100%'; + } + + this.flickPreviews.set(dir, flickPreview); + previewImgContainer.appendChild(flickPreview); + }); } // const hintLabel = this.hintLabel = document.createElement('div'); @@ -133,8 +110,20 @@ export class GesturePreviewHost { this.onCancel = handler; } + public scrollFlickPreview(x: number, y: number) { + const scrollStyle = this.previewImgContainer.style; + const edge = this.flickEdgeLength; + + scrollStyle.marginLeft = `${edge * -x}px`; + scrollStyle.marginTop = `${edge * -y}px`; + } + // These may not exist like this longterm. private clearFlick() { + this.previewImgContainer.style.marginTop = '0px'; + this.previewImgContainer.style.marginLeft = '0px'; + // animate the return slightly? + for(const pair of this.flickPreviews.entries()) { pair[1].classList.add('flick-clear'); } diff --git a/web/src/engine/osk/src/visualKeyboard.ts b/web/src/engine/osk/src/visualKeyboard.ts index 3822f32808..82efffd09e 100644 --- a/web/src/engine/osk/src/visualKeyboard.ts +++ b/web/src/engine/osk/src/visualKeyboard.ts @@ -1503,7 +1503,10 @@ export default class VisualKeyboard extends EventEmitter implements Ke showGesturePreview(key: KeyElement) { const tip = this.keytip; - const previewHost = new GesturePreviewHost(key, !!tip); + const keyCS = getComputedStyle(key); + const parsedHeight = Number.parseInt(keyCS.height, 10); + const parsedWidth = Number.parseInt(keyCS.width, 10); + const previewHost = new GesturePreviewHost(key, !!tip, Math.max(parsedWidth, parsedHeight)); if (tip == null) { const baseKey = key.key as OSKBaseKey; diff --git a/web/src/resources/osk/kmwosk.css b/web/src/resources/osk/kmwosk.css index c5679c3d0f..feeeb6b4de 100644 --- a/web/src/resources/osk/kmwosk.css +++ b/web/src/resources/osk/kmwosk.css @@ -367,13 +367,6 @@ body div.kmw-key-shift-on span.kmw-key-text {font-family:SpecialOSK !important;f .kmw-flick-preview { position: absolute; - font: 0.333em Arial; - color: #aaa; -} - -.kmw-flick-preview.flick-clear { - color: transparent; - transition: 0.5s linear all; } #kmw-preview-img-container { From e5622533dee29e32aa28875bebb46649716764da Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Tue, 31 Oct 2023 13:22:13 +0700 Subject: [PATCH 13/25] feat(web): 'natural' scrolling, smoother scroll start --- .../osk/src/input/gestures/browser/flick.ts | 60 +++++++++++-------- .../src/keyboard-layout/gesturePreviewHost.ts | 8 +-- web/src/engine/osk/src/visualKeyboard.ts | 6 +- 3 files changed, 45 insertions(+), 29 deletions(-) diff --git a/web/src/engine/osk/src/input/gestures/browser/flick.ts b/web/src/engine/osk/src/input/gestures/browser/flick.ts index 0c439ed89a..b503cb17d6 100644 --- a/web/src/engine/osk/src/input/gestures/browser/flick.ts +++ b/web/src/engine/osk/src/input/gestures/browser/flick.ts @@ -2,7 +2,7 @@ import { type KeyElement } from '../../../keyElement.js'; import VisualKeyboard from '../../../visualKeyboard.js'; import { ActiveKey, ActiveKeyBase, ActiveSubKey, KeyDistribution } from '@keymanapp/keyboard-processor'; -import { ConfigChangeClosure, CumulativePathStats, GestureRecognizerConfiguration, GestureSequence, PaddedZoneSource } from '@keymanapp/gesture-recognizer'; +import { ConfigChangeClosure, CumulativePathStats, GestureRecognizerConfiguration, GestureSequence, GestureSource, InputSample, PaddedZoneSource } from '@keymanapp/gesture-recognizer'; import { GestureHandler } from '../gestureHandler.js'; import { distributionFromDistanceMaps } from '@keymanapp/input-processor'; import { GestureParams } from '../specsForLayout.js'; @@ -23,6 +23,40 @@ export const FlickNameCoordMap = (() => { return map; })(); +export function buildFlickScroller( + baseSource: GestureSource, + initialCoord: InputSample, + previewHost: GesturePreviewHost, + gestureParams: GestureParams +): (coord: InputSample) => void { + return (coord: InputSample) => { + baseSource.path.on('step', (coord) => { + const deltaX = coord.targetX - initialCoord.targetX; + const deltaY = coord.targetY - initialCoord.targetY; + + const sqDist = deltaX * deltaX + deltaY * deltaY; + + /* + * Accomplishes two things: + * 1) Ensures the coordinates for flick-preview scrolling don't overshoot + * the preview key-cap + * 2) While allowing for _undershoot_ if "not quite there yet" + */ + let divisor = Math.sqrt(sqDist); + const FUDGE_FACTOR = 1.1; + const FULL_SCROLL_MAG = FUDGE_FACTOR * gestureParams.flick.triggerDist; + if(divisor < FULL_SCROLL_MAG) { + divisor = FULL_SCROLL_MAG; + } + + const previewX = deltaX / divisor; + const previewY = deltaY / divisor; + + previewHost?.scrollFlickPreview(previewX, previewY); + }); + } +} + /** * The maximum angle-difference, in radians, allowed before a potential flick * is to be considered less likely than its base key. @@ -88,30 +122,8 @@ export default class Flick implements GestureHandler { }); const baseCoord = baseSource.path.coords[0]; - baseSource.path.on('step', (coord) => { - const deltaX = coord.targetX - baseCoord.targetX; - const deltaY = coord.targetY - baseCoord.targetY; + baseSource.path.on('step', buildFlickScroller(baseSource, baseCoord, previewHost, this.gestureParams)); - const sqDist = deltaX * deltaX + deltaY * deltaY; - - /* - * Accomplishes two things: - * 1) Ensures the coordinates for flick-preview scrolling don't overshoot - * the preview key-cap - * 2) While allowing for _undershoot_ if "not quite there yet" - */ - let divisor = Math.sqrt(sqDist); - const FUDGE_FACTOR = 1.2; - const FULL_SCROLL_MAG = FUDGE_FACTOR * gestureParams.flick.triggerDist; - if(divisor < FULL_SCROLL_MAG) { - divisor = FULL_SCROLL_MAG; - } - - const previewX = deltaX / divisor; - const previewY = deltaY / divisor; - - previewHost?.scrollFlickPreview(previewX, previewY); - }); // Be sure to extend roaming bounds a bit more than usual for flicks, as they can be quick motions. const altConfig = this.buildPopupRecognitionConfig(vkbd); diff --git a/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts b/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts index a93c93f639..6785f099e7 100644 --- a/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts +++ b/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts @@ -57,8 +57,8 @@ export class GesturePreviewHost { // is in polar coords, origin toward north, clockwise. const coords = FlickNameCoordMap.get(dir); - const x = Math.sin(coords[0]); - const y = -Math.cos(coords[0]); + const x = -Math.sin(coords[0]); // Put 'e' flick at left + const y = Math.cos(coords[0]); // Put 'n' flick at bottom if(x < 0) { ps.right = (-x * OVERFLOW_OFFSET * edgeLength) + 'px'; @@ -114,8 +114,8 @@ export class GesturePreviewHost { const scrollStyle = this.previewImgContainer.style; const edge = this.flickEdgeLength; - scrollStyle.marginLeft = `${edge * -x}px`; - scrollStyle.marginTop = `${edge * -y}px`; + scrollStyle.marginLeft = `${edge * x}px`; + scrollStyle.marginTop = `${edge * y}px`; } // These may not exist like this longterm. diff --git a/web/src/engine/osk/src/visualKeyboard.ts b/web/src/engine/osk/src/visualKeyboard.ts index 82efffd09e..7466eea728 100644 --- a/web/src/engine/osk/src/visualKeyboard.ts +++ b/web/src/engine/osk/src/visualKeyboard.ts @@ -55,7 +55,7 @@ import SubkeyPopup from './input/gestures/browser/subkeyPopup.js'; import Multitap from './input/gestures/browser/multitap.js'; import { GestureHandler } from './input/gestures/gestureHandler.js'; import Modipress from './input/gestures/browser/modipress.js'; -import Flick from './input/gestures/browser/flick.js'; +import Flick, { buildFlickScroller } from './input/gestures/browser/flick.js'; import { GesturePreviewHost } from './keyboard-layout/gesturePreviewHost.js'; import OSKBaseKey from './keyboard-layout/oskBaseKey.js'; @@ -429,7 +429,11 @@ export default class VisualKeyboard extends EventEmitter implements Ke // remaining matchable gestures. Though, we shouldn't facilitate roaming // anyway if we've turned it off. if(this.kbdLayout.hasFlicks) { + const flickScroller = buildFlickScroller(source, source.path.coords[0], previewHost, DEFAULT_GESTURE_PARAMS); + trackingEntry.roamingHighlightHandler = (sample) => { + flickScroller(sample); + const key = sample.item; const oldKey = sourceTrackingMap[source.identifier].key; From d3ad24e6453e169d4d56c81405485996e0c64204 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Tue, 31 Oct 2023 13:29:30 +0700 Subject: [PATCH 14/25] feat(web): flick-scroll clearing --- web/src/engine/osk/src/input/gestures/browser/flick.ts | 4 +++- .../engine/osk/src/keyboard-layout/gesturePreviewHost.ts | 7 ++----- web/src/engine/osk/src/visualKeyboard.ts | 3 +++ web/src/resources/osk/kmwosk.css | 6 ++++++ 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/web/src/engine/osk/src/input/gestures/browser/flick.ts b/web/src/engine/osk/src/input/gestures/browser/flick.ts index b503cb17d6..24cc94fec8 100644 --- a/web/src/engine/osk/src/input/gestures/browser/flick.ts +++ b/web/src/engine/osk/src/input/gestures/browser/flick.ts @@ -122,7 +122,9 @@ export default class Flick implements GestureHandler { }); const baseCoord = baseSource.path.coords[0]; - baseSource.path.on('step', buildFlickScroller(baseSource, baseCoord, previewHost, this.gestureParams)); + const flickScroller = buildFlickScroller(baseSource, baseCoord, previewHost, this.gestureParams); + flickScroller(baseSource.currentSample); + baseSource.path.on('step', flickScroller); // Be sure to extend roaming bounds a bit more than usual for flicks, as they can be quick motions. diff --git a/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts b/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts index 6785f099e7..67c194f576 100644 --- a/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts +++ b/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts @@ -119,14 +119,11 @@ export class GesturePreviewHost { } // These may not exist like this longterm. - private clearFlick() { + public clearFlick() { this.previewImgContainer.style.marginTop = '0px'; this.previewImgContainer.style.marginLeft = '0px'; - // animate the return slightly? - for(const pair of this.flickPreviews.entries()) { - pair[1].classList.add('flick-clear'); - } + this.previewImgContainer.classList.add('flick-clear'); } private clearHint() { diff --git a/web/src/engine/osk/src/visualKeyboard.ts b/web/src/engine/osk/src/visualKeyboard.ts index 7466eea728..9b6b3eea64 100644 --- a/web/src/engine/osk/src/visualKeyboard.ts +++ b/web/src/engine/osk/src/visualKeyboard.ts @@ -497,6 +497,9 @@ export default class VisualKeyboard extends EventEmitter implements Ke }).find((obj) => !!obj); let handlers: GestureHandler[] = gestureHandlerMap.get(gestureSequence); + if(!handlers && existingPreviewHost && !gestureStage.matchedId.includes('flick')) { + existingPreviewHost.clearFlick(); + } // Disable roaming-touch highlighting (and current highlighting) for all // touchpoints included in a gesture, even newly-included ones as they occur. diff --git a/web/src/resources/osk/kmwosk.css b/web/src/resources/osk/kmwosk.css index feeeb6b4de..86e5ef0fdc 100644 --- a/web/src/resources/osk/kmwosk.css +++ b/web/src/resources/osk/kmwosk.css @@ -379,6 +379,12 @@ body div.kmw-key-shift-on span.kmw-key-text {font-family:SpecialOSK !important;f transition: 0.5s linear border; } +#kmw-preview-img-container.flick-clear { + transition: margin 0.25s linear ease-in-out; + margin-left: 0px !important; + margin-top: 0px !important; +} + .kmw-key-popup-icon.hint-clear { color: transparent; transition: 0.5s linear all; From 6169ffe682df788d8e284f5f4b54f4ae87d2920b Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Tue, 31 Oct 2023 13:36:15 +0700 Subject: [PATCH 15/25] chore(web): post-rebase patchup --- web/src/engine/osk/src/input/gestures/browser/flick.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/web/src/engine/osk/src/input/gestures/browser/flick.ts b/web/src/engine/osk/src/input/gestures/browser/flick.ts index 24cc94fec8..d5a3925c6c 100644 --- a/web/src/engine/osk/src/input/gestures/browser/flick.ts +++ b/web/src/engine/osk/src/input/gestures/browser/flick.ts @@ -102,7 +102,6 @@ export default class Flick implements GestureHandler { this.baseKeyDistances = vkbd.getSimpleTapCorrectionDistances(sequence.stageReports[0].sources[0].path.stats.initialSample, this.baseSpec) const baseSource = sequence.stageReports[0].sources[0].baseSource; - const baseSource = sequence.stageReports[0].sources[0].baseSource; this.sequence.on('stage', (result) => { const pathStats = baseSource.path.stats; this.computedFlickDistribution = this.flickDistribution(pathStats); From bb2250fdf006598a5cbc37b64794021eaaa4d535 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Tue, 31 Oct 2023 14:10:56 +0700 Subject: [PATCH 16/25] fix(web): flick-scroll only for keys with flicks --- web/src/engine/osk/src/input/gestures/browser/flick.ts | 3 --- web/src/engine/osk/src/visualKeyboard.ts | 6 ++++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/web/src/engine/osk/src/input/gestures/browser/flick.ts b/web/src/engine/osk/src/input/gestures/browser/flick.ts index d5a3925c6c..1fb1258fb7 100644 --- a/web/src/engine/osk/src/input/gestures/browser/flick.ts +++ b/web/src/engine/osk/src/input/gestures/browser/flick.ts @@ -142,10 +142,7 @@ export default class Flick implements GestureHandler { const roamBounding = new PaddedZoneSource(vkbd.element, [ // top basePadding * 2, // be extra-loose for the top! - // left, right basePadding, - // bottom: ensure the recognition zone includes the row of the base key. - // basePadding is already negative, but bottomDistance isn't. basePadding ]); diff --git a/web/src/engine/osk/src/visualKeyboard.ts b/web/src/engine/osk/src/visualKeyboard.ts index 9b6b3eea64..4067bad1f8 100644 --- a/web/src/engine/osk/src/visualKeyboard.ts +++ b/web/src/engine/osk/src/visualKeyboard.ts @@ -432,7 +432,9 @@ export default class VisualKeyboard extends EventEmitter implements Ke const flickScroller = buildFlickScroller(source, source.path.coords[0], previewHost, DEFAULT_GESTURE_PARAMS); trackingEntry.roamingHighlightHandler = (sample) => { - flickScroller(sample); + if(source.baseItem.key.spec.flick) { + flickScroller(sample); + } const key = sample.item; const oldKey = sourceTrackingMap[source.identifier].key; @@ -1228,7 +1230,7 @@ export default class VisualKeyboard extends EventEmitter implements Ke paddingZone.updatePadding([-0.333 * this.currentLayer.rowHeight]); this.gestureParams.longpress.flickDist = 0.25 * this.currentLayer.rowHeight; - this.gestureParams.flick.startDist = 0.25 * this.currentLayer.rowHeight; + this.gestureParams.flick.startDist = 0.1 * this.currentLayer.rowHeight; this.gestureParams.flick.triggerDist = 0.75 * this.currentLayer.rowHeight; // Needs the refreshed layout info to work correctly. From 1715463fd61342c06af8f483bfcf78f04ab4e364 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Tue, 31 Oct 2023 14:11:11 +0700 Subject: [PATCH 17/25] fix(web): random viewport-size def bug --- .../src/engine/configuration/viewportZoneSource.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/web/gesture-recognizer/src/engine/configuration/viewportZoneSource.ts b/common/web/gesture-recognizer/src/engine/configuration/viewportZoneSource.ts index 4a9b6b3cd1..c558a2aaf7 100644 --- a/common/web/gesture-recognizer/src/engine/configuration/viewportZoneSource.ts +++ b/common/web/gesture-recognizer/src/engine/configuration/viewportZoneSource.ts @@ -8,8 +8,8 @@ export class ViewportZoneSource implements RecognitionZoneSource { return DOMRect.fromRect({ y: 0, x: 0, - height: Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0), - width: Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0) + width: Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0), + height: Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0) }); } } \ No newline at end of file From 3c9c036a9c7f3220542ec17576a997b3fcc91cb9 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Wed, 1 Nov 2023 15:48:06 +0700 Subject: [PATCH 18/25] fix(web): early off-the-top cancellation in app/webview mode --- .../engine/osk/src/input/gestures/browser/flick.ts | 8 +++++++- .../osk/src/input/gestures/browser/subkeyPopup.ts | 13 ++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/web/src/engine/osk/src/input/gestures/browser/flick.ts b/web/src/engine/osk/src/input/gestures/browser/flick.ts index 1fb1258fb7..cc68656568 100644 --- a/web/src/engine/osk/src/input/gestures/browser/flick.ts +++ b/web/src/engine/osk/src/input/gestures/browser/flick.ts @@ -146,9 +146,15 @@ export default class Flick implements GestureHandler { basePadding ]); + let safeBounds = vkbd.gestureEngine.config.safeBounds; + if(vkbd.isEmbedded) { + safeBounds = new PaddedZoneSource(safeBounds, [basePadding, 0, 0]); + } + return { ...vkbd.gestureEngine.config, - maxRoamingBounds: roamBounding + maxRoamingBounds: roamBounding, + safeBounds: safeBounds // if embedded, ensure top boundary extends outside the WebView! } } diff --git a/web/src/engine/osk/src/input/gestures/browser/subkeyPopup.ts b/web/src/engine/osk/src/input/gestures/browser/subkeyPopup.ts index 431d0bf04b..8cd25869cd 100644 --- a/web/src/engine/osk/src/input/gestures/browser/subkeyPopup.ts +++ b/web/src/engine/osk/src/input/gestures/browser/subkeyPopup.ts @@ -157,12 +157,13 @@ export default class SubkeyPopup implements GestureHandler { const subkeyStyle = this.subkeys[0].style; const subkeyHeight = Number.parseInt(subkeyStyle.height, 10); const basePadding = -0.666 * subkeyHeight; // extends bounds by the absolute value. + const topScalar = 3; const bottomDistance = underlyingKeyBounding.bottom - baseBounding.bottom; const roamBounding = new PaddedZoneSource(this.element, [ // top - basePadding * 2, // be extra-loose for the top! + basePadding * topScalar, // be extra-loose for the top! // left, right basePadding, // bottom: ensure the recognition zone includes the row of the base key. @@ -174,13 +175,19 @@ export default class SubkeyPopup implements GestureHandler { const topContainerBounding = topContainer.getBoundingClientRect(); // Uses the top boundary from `roamBounding` unless the OSK's main element has a more // permissive top boundary. - const topPadding = Math.min(baseBounding.top + basePadding - topContainerBounding.top, 0); - const sustainBounding = new PaddedZoneSource(topContainer, [topPadding, 0, 0]) + const topPadding = Math.min(baseBounding.top + basePadding * topScalar - topContainerBounding.top, 0); + const sustainBounding = new PaddedZoneSource(topContainer, [topPadding * topScalar, 0, 0]); + + let safeBounds = vkbd.gestureEngine.config.safeBounds; + if(vkbd.isEmbedded) { + safeBounds = new PaddedZoneSource(safeBounds, [topPadding * topScalar, 0, 0]); + } return { targetRoot: this.element, inputStartBounds: vkbd.element, maxRoamingBounds: sustainBounding, + safeBounds: safeBounds, // if embedded, ensure top boundary extends outside the WebView! itemIdentifier: (coord, target) => { const roamingRect = roamBounding.getBoundingClientRect(); From 1cef89fa819fb69e0776ba2e5e4373eaabe0712a Mon Sep 17 00:00:00 2001 From: jahorton Date: Thu, 2 Nov 2023 11:58:26 +0700 Subject: [PATCH 19/25] fix(web): app/webview globe-key key-up signalling --- web/src/engine/osk/src/visualKeyboard.ts | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/web/src/engine/osk/src/visualKeyboard.ts b/web/src/engine/osk/src/visualKeyboard.ts index 6dfbfd167b..9814cd7d2a 100644 --- a/web/src/engine/osk/src/visualKeyboard.ts +++ b/web/src/engine/osk/src/visualKeyboard.ts @@ -592,16 +592,20 @@ export default class VisualKeyboard extends EventEmitter implements Ke // Potential long-term idea: only handle the first stage; delegate future stages to // specialized handlers for the remainder of the sequence. // Should work for modipresses, too... I think. - if(gestureStage.matchedId == 'special-key-start' && gestureKey.key.spec.baseKeyID == 'K_BKSP') { - // There shouldn't be a preview host for special keys... but it doesn't hurt to add the check. - existingPreviewHost?.cancel(); + if(gestureStage.matchedId == 'special-key-start') { + if(gestureKey.key.spec.baseKeyID == 'K_BKSP') { + // There shouldn't be a preview host for special keys... but it doesn't hurt to add the check. + existingPreviewHost?.cancel(); - // Possible enhancement: maybe update the held location for the backspace if there's movement? - // But... that seems pretty low-priority. - // - // Merely constructing the instance is enough; it'll link into the sequence's events and - // handle everything that remains for the backspace from here. - handlers = [new HeldRepeater(gestureSequence, () => this.modelKeyClick(gestureKey, coord))]; + // Possible enhancement: maybe update the held location for the backspace if there's movement? + // But... that seems pretty low-priority. + // + // Merely constructing the instance is enough; it'll link into the sequence's events and + // handle everything that remains for the backspace from here. + handlers = [new HeldRepeater(gestureSequence, () => this.modelKeyClick(gestureKey, coord))]; + } else if(gestureKey.key.spec.baseKeyID == "K_LOPT") { + gestureSequence.on('complete', () => this.emit('globekey', gestureKey, false)); + } } else if(gestureStage.matchedId.indexOf('longpress') > -1) { existingPreviewHost?.cancel(); From 3e3e6edf6951ad1af04067b1444d783c13541a89 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Thu, 2 Nov 2023 16:22:26 +0700 Subject: [PATCH 20/25] fix(web): Android API 21 compat --- .../web/keyboard-processor/src/keyboards/activeLayout.ts | 8 ++++++-- web/src/engine/main/src/keymanEngine.ts | 5 ++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/common/web/keyboard-processor/src/keyboards/activeLayout.ts b/common/web/keyboard-processor/src/keyboards/activeLayout.ts index a46161eda3..feef636c42 100644 --- a/common/web/keyboard-processor/src/keyboards/activeLayout.ts +++ b/common/web/keyboard-processor/src/keyboards/activeLayout.ts @@ -260,8 +260,12 @@ export class ActiveKeyBase { // And now for generalized type validation. ----------------------------------------- - // Object.entries does require Android 54... but we do polyfill within the Android app. Should be 'fine'. - for(const [key, value] of Object.entries(KeyTypesOfKeyMap)) { + // WARNING: Object.values and Object.entries is NOT polyfilled by es6-shim and thus + // is NOT available within the Android app in extremely early APIs. + // Object.entries requires Android 54. + + for(const key of Object.keys(KeyTypesOfKeyMap)) { + const value = KeyTypesOfKeyMap[key as keyof typeof KeyTypesOfKeyMap]; switch(value) { case 'subkeys': const arr = rawKey[key] as LayoutSubKey[]; diff --git a/web/src/engine/main/src/keymanEngine.ts b/web/src/engine/main/src/keymanEngine.ts index 8f30261595..e541da4d15 100644 --- a/web/src/engine/main/src/keymanEngine.ts +++ b/web/src/engine/main/src/keymanEngine.ts @@ -306,7 +306,10 @@ export default class KeymanEngine< } this._osk = value; if(value) { - value.activeKeyboard = this.contextManager.activeKeyboard; + // Don't build an OSK if no keyboard is available yet; avoid the extra flash. + if(this.contextManager.activeKeyboard) { + value.activeKeyboard = this.contextManager.activeKeyboard; + } value.on('keyevent', this.keyEventListener); this.core.keyboardProcessor.layerStore.handler = value.layerChangeHandler; } From 31257d9ed4123f1e08fd52a4c884d9853c4445ad Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Fri, 3 Nov 2023 09:45:52 +0700 Subject: [PATCH 21/25] fix(web): fixes tablet preview-host cleanup --- web/src/engine/osk/src/keyboard-layout/oskBaseKey.ts | 2 +- web/src/engine/osk/src/visualKeyboard.ts | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/web/src/engine/osk/src/keyboard-layout/oskBaseKey.ts b/web/src/engine/osk/src/keyboard-layout/oskBaseKey.ts index 69e91efdd1..7e0902ca97 100644 --- a/web/src/engine/osk/src/keyboard-layout/oskBaseKey.ts +++ b/web/src/engine/osk/src/keyboard-layout/oskBaseKey.ts @@ -183,7 +183,7 @@ export default class OSKBaseKey extends OSKKey { this.preview.style.display = 'none'; } - previewHost.setCancellationHandler(() => { + previewHost?.setCancellationHandler(() => { this.setPreview(null); }); diff --git a/web/src/engine/osk/src/visualKeyboard.ts b/web/src/engine/osk/src/visualKeyboard.ts index 49a66c7c4e..371b8b1ae2 100644 --- a/web/src/engine/osk/src/visualKeyboard.ts +++ b/web/src/engine/osk/src/visualKeyboard.ts @@ -423,6 +423,10 @@ export default class VisualKeyboard extends EventEmitter implements Ke const endHighlighting = () => { trackingEntry.previewHost?.cancel(); + // If we ever allow concurrent previews, check if it exists and matches + // a VisualKeyboard-tracked entry; if so, clear that too. + this.gesturePreviewHost = null; + trackingEntry.previewHost = null; if(trackingEntry.key) { this.highlightKey(trackingEntry.key, false); trackingEntry.key = null; @@ -490,7 +494,8 @@ export default class VisualKeyboard extends EventEmitter implements Ke // Multitouch does reference tracking data for a source after its completion, // but only while still permitting new touches. If we're here, that time is over. for(let id of gestureSequence.allSourceIds) { - // If the original preview host lives on, ensure it's cancelled now. + // If the original preview host lives on, ensure it's cancelled now. + this.gesturePreviewHost = null; sourceTrackingMap[id].previewHost?.cancel(); delete sourceTrackingMap[id]; } @@ -1524,7 +1529,7 @@ export default class VisualKeyboard extends EventEmitter implements Ke if (tip == null) { const baseKey = key.key as OSKBaseKey; baseKey.setPreview(previewHost); - return; + return previewHost; } else { tip.show(key, true, this, previewHost); } From fab64bc2acdb996aa935b4d7bfaad25169c93a59 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Mon, 6 Nov 2023 10:30:55 +0700 Subject: [PATCH 22/25] chore(web): applies a few PR suggestions --- .../osk/src/keyboard-layout/gesturePreviewHost.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts b/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts index 67c194f576..933d00c287 100644 --- a/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts +++ b/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts @@ -23,8 +23,7 @@ export class GesturePreviewHost { this.flickEdgeLength = edgeLength; const base = this.div = document.createElement('div'); - base.className='kmw-gesture-preview'; - base.id = 'kmw-gesture-preview'; + base.className = base.id = 'kmw-gesture-preview'; base.style.pointerEvents='none'; @@ -53,7 +52,12 @@ export class GesturePreviewHost { const ps /* preview style */ = flickPreview.style; - const OVERFLOW_OFFSET = 1.41; //141; + /* With edge lengths of 1, to keep flick-text invisible at the start, the + * hypotenuse for an inter-cardinal path is sqrt(2). To keep a perfect circle + * for all flicks, then, requires the straight-edge length for pure cardinal + * paths to match - sqrt(2). + */ + const OVERFLOW_OFFSET = 1.4142; // is in polar coords, origin toward north, clockwise. const coords = FlickNameCoordMap.get(dir); From 1136b838fbde5e5cbd158b5eeb7a2c5bebbb9588 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Mon, 6 Nov 2023 11:40:45 +0700 Subject: [PATCH 23/25] fix(web): better consistency for flick-animation b/t phone and tablet --- .../src/keyboard-layout/gesturePreviewHost.ts | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts b/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts index 933d00c287..e60e26cbd2 100644 --- a/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts +++ b/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts @@ -3,6 +3,16 @@ import { KeyElement } from "../keyElement.js"; import { FlickNameCoordMap, OrderedFlickDirections } from "../input/gestures/browser/flick.js"; const FLICK_DIRS = ['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw'] as const; + + + +/**With edge lengths of 1, to keep flick-text invisible at the start, the + * hypotenuse for an inter-cardinal path is sqrt(2). To keep a perfect circle + * for all flicks, then, requires the straight-edge length for pure cardinal + * paths to match - sqrt(2). + */ +const FLICK_OVERFLOW_OFFSET = 1.4142; + export class GesturePreviewHost { private readonly div: HTMLDivElement; private readonly label: HTMLSpanElement; @@ -41,6 +51,8 @@ export class GesturePreviewHost { label.textContent = key.key.label.textContent; this.div.appendChild(this.previewImgContainer); + const width = Number.parseInt(getComputedStyle(this.div).width, 10) || this.flickEdgeLength; + const height = Number.parseInt(getComputedStyle(this.div).height, 10) || this.flickEdgeLength; if(keySpec.flick) { const flickSpec = keySpec.flick || {}; @@ -52,36 +64,30 @@ export class GesturePreviewHost { const ps /* preview style */ = flickPreview.style; - /* With edge lengths of 1, to keep flick-text invisible at the start, the - * hypotenuse for an inter-cardinal path is sqrt(2). To keep a perfect circle - * for all flicks, then, requires the straight-edge length for pure cardinal - * paths to match - sqrt(2). - */ - const OVERFLOW_OFFSET = 1.4142; - // is in polar coords, origin toward north, clockwise. const coords = FlickNameCoordMap.get(dir); const x = -Math.sin(coords[0]); // Put 'e' flick at left const y = Math.cos(coords[0]); // Put 'n' flick at bottom + ps.width = width + 'px'; + ps.textAlign = 'center'; + if(x < 0) { - ps.right = (-x * OVERFLOW_OFFSET * edgeLength) + 'px'; + ps.right = (-x * FLICK_OVERFLOW_OFFSET * edgeLength) + 'px'; } else if(x > 0) { - ps.left = ( x * OVERFLOW_OFFSET * edgeLength) + 'px'; + ps.left = ( x * FLICK_OVERFLOW_OFFSET * edgeLength) + 'px'; } else { ps.left = '0px'; - ps.right = '0px'; - ps.textAlign = 'center'; } + ps.height = height + 'px'; + ps.lineHeight = height + 'px'; if(y < 0) { - ps.bottom = (-y * OVERFLOW_OFFSET * edgeLength) + 'px'; + ps.bottom = (-y * FLICK_OVERFLOW_OFFSET * edgeLength) + 'px'; } else if(y > 0) { - ps.top = ( y * OVERFLOW_OFFSET * edgeLength) + 'px'; + ps.top = ( y * FLICK_OVERFLOW_OFFSET * edgeLength) + 'px'; } else { ps.top = '0px'; - ps.bottom = '0px'; - ps.lineHeight = '100%'; } this.flickPreviews.set(dir, flickPreview); @@ -116,7 +122,7 @@ export class GesturePreviewHost { public scrollFlickPreview(x: number, y: number) { const scrollStyle = this.previewImgContainer.style; - const edge = this.flickEdgeLength; + const edge = this.flickEdgeLength * FLICK_OVERFLOW_OFFSET; scrollStyle.marginLeft = `${edge * x}px`; scrollStyle.marginTop = `${edge * y}px`; From 8d6610abf4c118c80b3f1c5b735a900dd4a13a54 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Mon, 6 Nov 2023 13:20:37 +0700 Subject: [PATCH 24/25] fix(web): lm-worker null guard --- common/web/lm-worker/src/main/correction/context-tracker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/web/lm-worker/src/main/correction/context-tracker.ts b/common/web/lm-worker/src/main/correction/context-tracker.ts index ce235562e2..c8dfd9aab3 100644 --- a/common/web/lm-worker/src/main/correction/context-tracker.ts +++ b/common/web/lm-worker/src/main/correction/context-tracker.ts @@ -542,7 +542,7 @@ export class ContextTracker extends CircularArray { if(doublecheckContext.left != context.left) { continue; } - } else if(priorMatchState.taggedContext.left != context.left) { + } else if(priorMatchState.taggedContext?.left != context.left) { continue; } From b0b696d8664ab3a593c77e7c3e8a26804a093266 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Tue, 14 Nov 2023 09:51:52 +0700 Subject: [PATCH 25/25] chore(web): a bit more cleanup --- .../src/keyboard-layout/gesturePreviewHost.ts | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts b/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts index e60e26cbd2..786ad2f0ed 100644 --- a/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts +++ b/web/src/engine/osk/src/keyboard-layout/gesturePreviewHost.ts @@ -2,10 +2,6 @@ import { ActiveKey } from "@keymanapp/keyboard-processor"; import { KeyElement } from "../keyElement.js"; import { FlickNameCoordMap, OrderedFlickDirections } from "../input/gestures/browser/flick.js"; -const FLICK_DIRS = ['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw'] as const; - - - /**With edge lengths of 1, to keep flick-text invisible at the start, the * hypotenuse for an inter-cardinal path is sqrt(2). To keep a perfect circle * for all flicks, then, requires the straight-edge length for pure cardinal @@ -94,21 +90,6 @@ export class GesturePreviewHost { previewImgContainer.appendChild(flickPreview); }); } - - // const hintLabel = this.hintLabel = document.createElement('div'); - // hintLabel.className='kmw-key-popup-icon'; - // hintLabel.textContent = keySpec == keySpec.hintSrc ? keySpec.hint : keySpec.hintSrc?.text; - // hintLabel.style.fontWeight= hintLabel.textContent == '\u2022' ? 'bold' : ''; - - // // Default positioning puts it far too close to the flick-preview bit. - // let yAdjustment = 0; - // hintLabel.style.marginTop = `-${yAdjustment}px`; - - // // b/c multitap's border forces position shifting - // let xAdjustment = 0; - // hintLabel.style.marginRight = `-${xAdjustment}px`; - - // base.appendChild(hintLabel); } public cancel() {