From df8a14f87cb4931ce56530fbfb9c280171bd2e0c Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Fri, 6 Oct 2023 08:52:42 +0700 Subject: [PATCH] feat(web): restores subkey-selection, extends accepted path range --- .../engine/configuration/paddedZoneSource.ts | 1 + .../gestures/matchers/gestureSequence.ts | 4 +- .../gestures/matchers/matcherSelector.ts | 3 + .../gesture-recognizer/src/engine/index.ts | 2 +- .../src/engine/inputEventEngine.ts | 9 +- .../src/engine/mouseEventEngine.ts | 2 +- .../src/engine/touchEventEngine.ts | 2 +- .../src/keyboards/activeLayout.ts | 2 +- .../src/input/gestures/browser/oskSubKey.ts | 4 +- .../gestures/browser/pendingLongpress.ts | 126 ++++++------ .../src/input/gestures/browser/subkeyPopup.ts | 172 ++++++++++------ web/src/engine/osk/src/visualKeyboard.ts | 192 ++++++++++-------- 12 files changed, 293 insertions(+), 226 deletions(-) diff --git a/common/web/gesture-recognizer/src/engine/configuration/paddedZoneSource.ts b/common/web/gesture-recognizer/src/engine/configuration/paddedZoneSource.ts index 98a9f21d49..fa9cfd37e1 100644 --- a/common/web/gesture-recognizer/src/engine/configuration/paddedZoneSource.ts +++ b/common/web/gesture-recognizer/src/engine/configuration/paddedZoneSource.ts @@ -107,6 +107,7 @@ export class PaddedZoneSource implements RecognitionZoneSource { w: 2 * edgePadding[1], h: edgePadding[0] + edgePadding[2] }; + break; case 4: // top, right, bottom, left this._edgePadding = { diff --git a/common/web/gesture-recognizer/src/engine/headless/gestures/matchers/gestureSequence.ts b/common/web/gesture-recognizer/src/engine/headless/gestures/matchers/gestureSequence.ts index 8364ed2a2e..9577b40b19 100644 --- a/common/web/gesture-recognizer/src/engine/headless/gestures/matchers/gestureSequence.ts +++ b/common/web/gesture-recognizer/src/engine/headless/gestures/matchers/gestureSequence.ts @@ -72,10 +72,12 @@ interface PopConfig { count: number } +export type ConfigChangeClosure = (configStackCommand: PushConfig | PopConfig) => void; + interface EventMap { stage: ( stageReport: GestureStageReport, - changeConfiguration: (configStackCommand: PushConfig | PopConfig) => void + changeConfiguration: ConfigChangeClosure ) => void; complete: () => void; } diff --git a/common/web/gesture-recognizer/src/engine/headless/gestures/matchers/matcherSelector.ts b/common/web/gesture-recognizer/src/engine/headless/gestures/matchers/matcherSelector.ts index 3a4c0d1a5f..50793d4bc1 100644 --- a/common/web/gesture-recognizer/src/engine/headless/gestures/matchers/matcherSelector.ts +++ b/common/web/gesture-recognizer/src/engine/headless/gestures/matchers/matcherSelector.ts @@ -159,6 +159,9 @@ export class MatcherSelector extends EventEmitter> { } const matchPromise = new ManagedPromise>(); + matchPromise.finally(() => { + this._sourceSelector = this._sourceSelector.filter((source) => sources.indexOf(source.source) == -1); + }); /* * First... diff --git a/common/web/gesture-recognizer/src/engine/index.ts b/common/web/gesture-recognizer/src/engine/index.ts index 2ced8a26ef..8ffad3a579 100644 --- a/common/web/gesture-recognizer/src/engine/index.ts +++ b/common/web/gesture-recognizer/src/engine/index.ts @@ -6,7 +6,7 @@ export { GestureRecognizerConfiguration } from "./configuration/gestureRecognize export { InputEngineBase } from "./headless/inputEngineBase.js"; export { InputSample } from "./headless/inputSample.js"; export { SerializedGesturePath, GesturePath } from "./headless/gesturePath.js"; -export { GestureStageReport, GestureSequence } from "./headless/gestures/matchers/gestureSequence.js"; +export { ConfigChangeClosure, GestureStageReport, GestureSequence } from "./headless/gestures/matchers/gestureSequence.js"; export { SerializedGestureSource, GestureSource, buildGestureMatchInspector } from "./headless/gestureSource.js"; export { MouseEventEngine } from "./mouseEventEngine.js"; export { PathSegmenter, Subsegmentation } from "./headless/subsegmentation/pathSegmenter.js"; diff --git a/common/web/gesture-recognizer/src/engine/inputEventEngine.ts b/common/web/gesture-recognizer/src/engine/inputEventEngine.ts index 472c333059..19dc4e2355 100644 --- a/common/web/gesture-recognizer/src/engine/inputEventEngine.ts +++ b/common/web/gesture-recognizer/src/engine/inputEventEngine.ts @@ -6,7 +6,7 @@ export abstract class InputEventEngine extends Inpu abstract registerEventHandlers(): void; abstract unregisterEventHandlers(): void; - protected buildSampleFor(clientX: number, clientY: number, target: EventTarget, timestamp: number, stateToken: StateToken): InputSample { + protected buildSampleFor(clientX: number, clientY: number, target: EventTarget, timestamp: number, source: GestureSource): InputSample { const targetRect = this.config.targetRoot.getBoundingClientRect(); const sample: InputSample = { clientX: clientX, @@ -14,10 +14,11 @@ export abstract class InputEventEngine extends Inpu targetX: clientX - targetRect.left, targetY: clientY - targetRect.top, t: timestamp, - stateToken: stateToken + stateToken: source?.stateToken ?? this.stateToken }; - const hoveredItem = this.config.itemIdentifier(sample, target); + const itemIdentifier = source?.currentRecognizerConfig.itemIdentifier ?? this.config.itemIdentifier; + const hoveredItem = itemIdentifier(sample, target); sample.item = hoveredItem; return sample; @@ -68,7 +69,7 @@ export abstract class InputEventEngine extends Inpu } const lastEntry = touchpoint.path.coords[touchpoint.path.coords.length-1]; - const sample = this.buildSampleFor(lastEntry.clientX, lastEntry.clientY, target, lastEntry.t, lastEntry.stateToken); + const sample = this.buildSampleFor(lastEntry.clientX, lastEntry.clientY, target, lastEntry.t, touchpoint); /* While an 'end' event immediately follows a 'move' if it occurred simultaneously, * this is decidedly _not_ the case if the touchpoint was held for a while without diff --git a/common/web/gesture-recognizer/src/engine/mouseEventEngine.ts b/common/web/gesture-recognizer/src/engine/mouseEventEngine.ts index da643c3dab..42baf53299 100644 --- a/common/web/gesture-recognizer/src/engine/mouseEventEngine.ts +++ b/common/web/gesture-recognizer/src/engine/mouseEventEngine.ts @@ -69,7 +69,7 @@ export class MouseEventEngine extends InputEv private buildSampleFromEvent(event: MouseEvent, identifier: number) { // WILL be null for newly-starting `GestureSource`s / contact points. const source = this.getTouchpointWithId(identifier); - return this.buildSampleFor(event.clientX, event.clientY, event.target, performance.now(), source?.stateToken ?? this.stateToken); + return this.buildSampleFor(event.clientX, event.clientY, event.target, performance.now(), source); } onMouseStart(event: MouseEvent) { diff --git a/common/web/gesture-recognizer/src/engine/touchEventEngine.ts b/common/web/gesture-recognizer/src/engine/touchEventEngine.ts index b3a8fa8a93..c3412e382f 100644 --- a/common/web/gesture-recognizer/src/engine/touchEventEngine.ts +++ b/common/web/gesture-recognizer/src/engine/touchEventEngine.ts @@ -86,7 +86,7 @@ export class TouchEventEngine extends InputEv private buildSampleFromTouch(touch: Touch, timestamp: number) { // WILL be null for newly-starting `GestureSource`s / contact points. const source = this.getTouchpointWithId(touch.identifier); - return this.buildSampleFor(touch.clientX, touch.clientY, touch.target, timestamp, source?.stateToken ?? this.stateToken); + return this.buildSampleFor(touch.clientX, touch.clientY, touch.target, timestamp, source); } onTouchStart(event: TouchEvent) { diff --git a/common/web/keyboard-processor/src/keyboards/activeLayout.ts b/common/web/keyboard-processor/src/keyboards/activeLayout.ts index 230366e4f0..9eeddffd54 100644 --- a/common/web/keyboard-processor/src/keyboards/activeLayout.ts +++ b/common/web/keyboard-processor/src/keyboards/activeLayout.ts @@ -59,7 +59,7 @@ class ActiveKeyBase { nextlayer: string; sp?: ButtonClass; - _baseKeyEvent: KeyEvent; + private _baseKeyEvent: KeyEvent; isMnemonic: boolean = false; proportionalPad: number; diff --git a/web/src/engine/osk/src/input/gestures/browser/oskSubKey.ts b/web/src/engine/osk/src/input/gestures/browser/oskSubKey.ts index 0743bce568..8cfc21d454 100644 --- a/web/src/engine/osk/src/input/gestures/browser/oskSubKey.ts +++ b/web/src/engine/osk/src/input/gestures/browser/oskSubKey.ts @@ -25,7 +25,9 @@ export default class OSKSubKey extends OSKKey { let ks=kDiv.style; for(var tp in tKey) { - if(typeof spec[tp] != 'string') { + // The subkey has already had its _baseKeyEvent field constructed; don't overwrite it! + // Layout properties, however, are safe to overwrite. + if(typeof spec[tp] != 'string' && tp != '_baseKeyEvent') { spec[tp]=tKey[tp]; } } diff --git a/web/src/engine/osk/src/input/gestures/browser/pendingLongpress.ts b/web/src/engine/osk/src/input/gestures/browser/pendingLongpress.ts index 9435976147..50504eec2c 100644 --- a/web/src/engine/osk/src/input/gestures/browser/pendingLongpress.ts +++ b/web/src/engine/osk/src/input/gestures/browser/pendingLongpress.ts @@ -1,73 +1,73 @@ -import { type KeyElement } from '../../../keyElement.js'; -import VisualKeyboard from '../../../visualKeyboard.js'; -import PendingGesture from '../pendingGesture.interface.js'; -import SubkeyPopup from './subkeyPopup.js'; +// import { type KeyElement } from '../../../keyElement.js'; +// import VisualKeyboard from '../../../visualKeyboard.js'; +// import PendingGesture from '../pendingGesture.interface.js'; +// import SubkeyPopup from './subkeyPopup.js'; -/** - * (Conceptually) represents a finite-state-machine that determines - * whether or not a series of touch events corresponds to a longpress - * touch input. The `resolve` method may be used to trigger the - * subkey menu early, as with the upward quick-display shortcut. - * - * This is the default implementation of longpress behavior for KMW. - * Alterate implementations are modeled through the `embedded` - * namespace's equivalent, which is designed to facilitate custom - * modeling for such gestures. - * - * Once the conditions to recognize a longpress gesture have been - * fulfilled, this class's `promise` will resolve with a `SubkeyPopup` - * matching the gesture's 'base' key, which itself provides a - * `promise` field that will resolve to a `KeyEvent` once the touch - * sequence is completed. - */ -export default class PendingLongpress implements PendingGesture { - public readonly baseKey: KeyElement; - public readonly promise: Promise; +// /** +// * (Conceptually) represents a finite-state-machine that determines +// * whether or not a series of touch events corresponds to a longpress +// * touch input. The `resolve` method may be used to trigger the +// * subkey menu early, as with the upward quick-display shortcut. +// * +// * This is the default implementation of longpress behavior for KMW. +// * Alterate implementations are modeled through the `embedded` +// * namespace's equivalent, which is designed to facilitate custom +// * modeling for such gestures. +// * +// * Once the conditions to recognize a longpress gesture have been +// * fulfilled, this class's `promise` will resolve with a `SubkeyPopup` +// * matching the gesture's 'base' key, which itself provides a +// * `promise` field that will resolve to a `KeyEvent` once the touch +// * sequence is completed. +// */ +// export default class PendingLongpress implements PendingGesture { +// public readonly baseKey: KeyElement; +// public readonly promise: Promise; - public readonly subkeyUI: SubkeyPopup; +// public readonly subkeyUI: SubkeyPopup; - private readonly vkbd: VisualKeyboard; - private resolver: (subkeyPopup: SubkeyPopup) => void; +// private readonly vkbd: VisualKeyboard; +// private resolver: (subkeyPopup: SubkeyPopup) => void; - private timerId: number; - private popupDelay: number = 500; +// private timerId: number; +// private popupDelay: number = 500; - constructor(vkbd: VisualKeyboard, baseKey: KeyElement) { - this.vkbd = vkbd; - this.baseKey = baseKey; +// constructor(vkbd: VisualKeyboard, baseKey: KeyElement) { +// this.vkbd = vkbd; +// this.baseKey = baseKey; - let _this = this; - this.promise = new Promise(function(resolve, reject) { - _this.resolver = resolve; - // After the timeout, it's no longer deferred; it's being fulfilled. - // Even if the actual subkey itself is still async. - _this.timerId = window.setTimeout(_this.resolve.bind(_this), _this.popupDelay); - }); - } +// let _this = this; +// this.promise = new Promise(function(resolve, reject) { +// _this.resolver = resolve; +// // After the timeout, it's no longer deferred; it's being fulfilled. +// // Even if the actual subkey itself is still async. +// _this.timerId = window.setTimeout(_this.resolve.bind(_this), _this.popupDelay); +// }); +// } - public cancel() { - if(this.timerId) { - window.clearTimeout(this.timerId); - this.timerId = null; - } +// public cancel() { +// if(this.timerId) { +// window.clearTimeout(this.timerId); +// this.timerId = null; +// } - if(this.resolver) { - this.resolver(null); - this.resolver = null; - } - } +// if(this.resolver) { +// this.resolver(null); +// this.resolver = null; +// } +// } - public resolve() { - // User has flicked up to get to the longpress, before - // the timeout has expired. We need to cancel the timeout. - // See #5950 - if(this.timerId) { - window.clearTimeout(this.timerId); - this.timerId = null; - } +// public resolve() { +// // User has flicked up to get to the longpress, before +// // the timeout has expired. We need to cancel the timeout. +// // See #5950 +// if(this.timerId) { +// window.clearTimeout(this.timerId); +// this.timerId = null; +// } - if(this.resolver) { - this.resolver(new SubkeyPopup(this.vkbd, this.baseKey)); - } - } -} +// if(this.resolver) { +// this.resolver(new SubkeyPopup(this.vkbd, this.baseKey)); +// } +// } +// } 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 6f4ba17e7a..131dc960f6 100644 --- a/web/src/engine/osk/src/input/gestures/browser/subkeyPopup.ts +++ b/web/src/engine/osk/src/input/gestures/browser/subkeyPopup.ts @@ -1,12 +1,10 @@ import OSKSubKey from './oskSubKey.js'; -import RealizedGesture from '../realizedGesture.interface.js'; import { type KeyElement } from '../../../keyElement.js'; import OSKBaseKey from '../../../keyboard-layout/oskBaseKey.js'; import VisualKeyboard from '../../../visualKeyboard.js'; -import InputEventCoordinate from '../../../input/inputEventCoordinate.js'; import { DeviceSpec, KeyEvent, ActiveSubkey } from '@keymanapp/keyboard-processor'; -import { InputSample } from '@keymanapp/gesture-recognizer'; +import { ConfigChangeClosure, GestureRecognizerConfiguration, GestureSequence, PaddedZoneSource } from '@keymanapp/gesture-recognizer'; /** * Represents a 'realized' longpress gesture's default implementation @@ -24,11 +22,10 @@ import { InputSample } from '@keymanapp/gesture-recognizer'; * The `Promise` may also resolve to `null` if the user indicates * the desire to cancel subkey selection. */ -export default class SubkeyPopup implements RealizedGesture { +export default class SubkeyPopup { public readonly element: HTMLDivElement; public readonly shim: HTMLDivElement; - private vkbd: VisualKeyboard; private currentSelection: KeyElement; private callout: HTMLDivElement; @@ -36,19 +33,34 @@ export default class SubkeyPopup implements RealizedGesture { public readonly baseKey: KeyElement; public readonly promise: Promise; - // Resolves the promise that generated this SubkeyPopup. - private resolver: (keyEvent: KeyEvent) => void; + public readonly subkeys: KeyElement[]; - constructor(vkbd: VisualKeyboard, e: KeyElement) { - let _this = this; - - this.promise = new Promise(function(resolve) { - _this.resolver = resolve; - }) - - this.vkbd = vkbd; + constructor( + source: GestureSequence, + configChanger: ConfigChangeClosure, + vkbd: VisualKeyboard, + e: KeyElement + ) { this.baseKey = e; + source.on('complete', () => { + this.currentSelection.key.highlight(false); + this.clear(); + }); + + // From here, we want to make decisions based on only the subkey-menu portion of the gesture path. + const subkeyComponent = source.stageReports[0].sources[0].constructSubview(true, false); + + // Watch for touchpoint selection of new keys. + subkeyComponent.path.on('step', (sample) => { + // Require a fudge-factor before dropping the default key. + if(subkeyComponent.path.stats.netDistance >= 4) { + this.currentSelection.key.highlight(false); + sample.item.key.highlight(true); + this.currentSelection = sample.item; + } + }); + // If the user doesn't move their finger and releases, we'll output the base key // by default. this.currentSelection = e; @@ -60,15 +72,15 @@ export default class SubkeyPopup implements RealizedGesture { // The holder is position:fixed, but the keys do not need to be, as no scrolling // is possible while the array is visible. So it is simplest to let the keys have // position:static and display:inline-block - var subKeys = this.element = document.createElement('div'); + const elements = this.element = document.createElement('div'); var i; - subKeys.id='kmw-popup-keys'; + elements.id='kmw-popup-keys'; // #3718: No longer prepend base key to popup array // Must set position dynamically, not in CSS - var ss=subKeys.style; + var ss=elements.style; // Set key font according to layout, or defaulting to OSK font // (copied, not inherited, since OSK is not a parent of popup keys) @@ -85,6 +97,7 @@ export default class SubkeyPopup implements RealizedGesture { ss.width=(nCols*e.offsetWidth+nCols*5)+'px'; // Add nested button elements for each sub-key + this.subkeys = []; for(i=0; i e, needsTopMargin); + this.subkeys.push(kDiv.firstChild as KeyElement); - subKeys.appendChild(kDiv); + elements.appendChild(kDiv); } // And add a filter to fade main keyboard @@ -109,20 +123,82 @@ export default class SubkeyPopup implements RealizedGesture { // Highlight the duplicated base key or ideal subkey (if a phone) if(vkbd.device.formFactor == DeviceSpec.FormFactor.Phone) { - this.selectDefaultSubkey(vkbd, e, subKeys /* == this.element */); + this.selectDefaultSubkey(vkbd, e, elements /* == this.element */); } + + vkbd.topContainer.appendChild(this.element); + vkbd.topContainer.appendChild(this.shim); + + // Must be placed after its `.element` has been inserted into the DOM. + this.reposition(vkbd); + + const config = this.buildPopupRecognitionConfig(vkbd); + configChanger({ + type: 'push', + config: config + }); } - finalize(input: InputSample) { - if(this.resolver) { - let keyEvent: KeyEvent = null; - if(this.currentSelection) { - keyEvent = this.vkbd.initKeyEvent(this.currentSelection, input); - this.currentSelection.key.highlight(false); + private buildPopupRecognitionConfig(vkbd: VisualKeyboard): GestureRecognizerConfiguration { + const baseBounding = this.element.getBoundingClientRect(); + const underlyingKeyBounding = this.baseKey.getBoundingClientRect(); + + const subkeyStyle = this.subkeys[0].style; + const subkeyHeight = Number.parseInt(subkeyStyle.height, 10); + const basePadding = -0.333 * subkeyHeight; // extends bounds by the absolute value. + + const bottomDistance = underlyingKeyBounding.bottom - baseBounding.bottom; + + const roamBounding = new PaddedZoneSource(this.element, [ + // top + basePadding, + // left, right + basePadding, + // bottom: ensure the recognition zone includes the row of the base key. + // basePadding is already negative, but bottomDistance isn't. + bottomDistance > basePadding ? -bottomDistance : basePadding + ]); + + return { + targetRoot: this.element, + inputStartBounds: vkbd.element, + maxRoamingBounds: roamBounding, + itemIdentifier: (coord, target) => { + let bestMatchKey: KeyElement = null; + let bestYdist = Number.MAX_VALUE; + let bestXdist = Number.MAX_VALUE; + + for(let key of this.subkeys) { + const keyBounds = key.getBoundingClientRect(); + + let xDist = Number.MAX_VALUE; + let yDist = Number.MAX_VALUE; + + if(keyBounds.left <= coord.clientX && coord.clientX < keyBounds.right) { + xDist = 0; + } else { + xDist = (keyBounds.left >= coord.clientX) ? keyBounds.left - coord.clientX : coord.clientX - keyBounds.right; + } + + if(keyBounds.top <= coord.clientY && coord.clientY < keyBounds.bottom) { + yDist = 0; + } else { + yDist = (keyBounds.top >= coord.clientY) ? keyBounds.top - coord.clientY : coord.clientY - keyBounds.bottom; + } + + if(xDist == 0 && yDist == 0) { + // Perfect match! + return key; + } else if(xDist < bestXdist || (xDist == bestXdist && yDist < bestYdist)) { + bestXdist = xDist; + bestMatchKey = key; + bestYdist = yDist; + } + } + + return bestMatchKey; } - this.resolver(keyEvent); } - this.resolver = null; } reposition(vkbd: VisualKeyboard) { @@ -168,7 +244,7 @@ export default class SubkeyPopup implements RealizedGesture { // Add the callout if(vkbd.device.formFactor == DeviceSpec.FormFactor.Phone && vkbd.device.OS == DeviceSpec.OperatingSystem.iOS) { - this.callout = this.addCallout(e, delta); + this.callout = this.addCallout(e, delta, vkbd.topContainer); } } @@ -178,9 +254,7 @@ export default class SubkeyPopup implements RealizedGesture { * @param {Object} key HTML key element * @return {Object} callout object */ - addCallout(key: KeyElement, delta?: number): HTMLDivElement { - const _Box = this.vkbd.topContainer; - + addCallout(key: KeyElement, delta: number, _Box: HTMLElement): HTMLDivElement { delta = delta || 0; let calloutHeight = key.offsetHeight - delta + 6; @@ -248,11 +322,6 @@ export default class SubkeyPopup implements RealizedGesture { } clear() { - // Discard the reference to the Promise's resolve method, allowing - // GC to clean it up. The corresponding Promise's contract allows - // passive cancellation. - this.resolver = null; - // Remove the displayed subkey array, if any if(this.element.parentNode) { this.element.parentNode.removeChild(this.element); @@ -266,33 +335,4 @@ export default class SubkeyPopup implements RealizedGesture { this.callout.parentNode.removeChild(this.callout); } } - - updateTouch(input: InputEventCoordinate) { - this.currentSelection = null; - this.baseKey.key.highlight(false); - - for(let i=0; i < this.baseKey['subKeys'].length; i++) { - try { - let sk = this.element.childNodes[i].firstChild as KeyElement; - - let onKey = sk.key.isUnderTouch(input); - if(onKey) { - this.currentSelection = sk; - } - sk.key.highlight(onKey); - } catch(ex) { - if(ex.message) { - console.error("Unexpected error when attempting to update selected subkey:" + ex.message); - } else { - console.error("Unexpected error (and error type) when attempting to update selected subkey."); - } - } - } - - // Use the popup duplicate of the base key if a phone with a visible popup array - if(!this.currentSelection && this.baseKey.key.isUnderTouch(input)) { - this.baseKey.key.highlight(true); - this.currentSelection = this.baseKey; - } - } } diff --git a/web/src/engine/osk/src/visualKeyboard.ts b/web/src/engine/osk/src/visualKeyboard.ts index 762bccc759..9a3adfc1ee 100644 --- a/web/src/engine/osk/src/visualKeyboard.ts +++ b/web/src/engine/osk/src/visualKeyboard.ts @@ -39,7 +39,6 @@ import RealizedGesture from './input/gestures/realizedGesture.interface.js'; import { defaultFontSize, getFontSizeStyle } from './fontSizeUtils.js'; import PendingMultiTap, { PendingMultiTapState } from './input/gestures/browser/pendingMultiTap.js'; import InternalSubkeyPopup from './input/gestures/browser/subkeyPopup.js'; -import InternalPendingLongpress from './input/gestures/browser/pendingLongpress.js'; import InternalKeyTip from './input/gestures/browser/keytip.js'; import CommonConfiguration from './config/commonConfiguration.js'; @@ -47,6 +46,7 @@ import { gestureSetForLayout } from './input/gestures/specsForLayout.js'; import { getViewportScale } from './screenUtils.js'; import { HeldRepeater } from './input/gestures/heldRepeater.js'; +import SubkeyPopup from './input/gestures/browser/subkeyPopup.js'; export interface VisualKeyboardConfiguration extends CommonConfiguration { /** @@ -355,6 +355,9 @@ export default class VisualKeyboard extends EventEmitter implements Ke * > The read-only target property of the Touch interface returns the (EventTarget) on which the touch contact * started when it was first placed on the surface, even if the touch point has since moved outside the * interactive area of that element[...] + * + * Therefore, `target` is for the initial element, not necessarily the one currently under + * the touchpoint - which matters during a 'touchmove'. */ return this.layerGroup.findNearestKey(sample); @@ -442,44 +445,59 @@ export default class VisualKeyboard extends EventEmitter implements Ke // First, if we've configured the gesture to generate a keystroke, let's handle that. const gestureKey = gestureStage.item; + let coordSource = gestureStage.sources[0]; + let coord: InputSample = null; + if(coordSource) { + // TODO: should probably vary depending upon `gestureStage.matchedId` + // (certain types should probably use the base coord... or even from + // a prior stage of the sequence as appropriate.) + // + // This is the coordinate used as the basis for fat-finger calculations. + coord = coordSource.currentSample; + } if(gestureKey) { - let coordSource = gestureStage.sources[0]; - let coord: InputSample = null; - if(coordSource) { - // TODO: should probably vary depending upon `gestureStage.matchedId` - // (certain types should probably use the base coord... or even from - // a prior stage of the sequence as appropriate.) - // - // This is the coordinate used as the basis for fat-finger calculations. - coord = coordSource.currentSample; - } if(gestureStage.matchedId == 'multitap') { // TODO: examine sequence, determine rota-style index to apply; select THAT item instead. } - // Once the best coord to use for fat-finger calculations has been determined: - this.modelKeyClick(gestureStage.item, coord); - - // -- Scratch-space as gestures start becoming integrated -- - // Reordering may follow at some point. - // - // 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') { - // 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. - new HeldRepeater(gestureSequence, () => this.modelKeyClick(gestureKey, coord)); + if(gestureStage.matchedId == 'subkey-select') { + // TODO: examine subkey menu, determine proper set of fat-finger alternates. } - // TODO: depending upon the gesture type, what sort of UI shifts should happen to - // facilitate follow-up stages? + // Once the best coord to use for fat-finger calculations has been determined: + this.modelKeyClick(gestureStage.item, coord); } + + // Outside of passing keys along... the handling of later stages is delegated + // to gesture-specific handling classes. + if(gestureSequence.stageReports.length > 1) { + return; + } + + // So, if this is the first stage, this is where we need to perform that delegation. + + // -- Scratch-space as gestures start becoming integrated -- + // Reordering may follow at some point. + // + // 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') { + // 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. + new HeldRepeater(gestureSequence, () => this.modelKeyClick(gestureKey, coord)); + } else if(gestureStage.matchedId == 'longpress') { + // Likewise. + new SubkeyPopup(gestureSequence, configChanger, this, gestureSequence.stageReports[0].sources[0].baseItem); + } + + // TODO: depending upon the gesture type, what sort of UI shifts should happen to + // facilitate follow-up stages? }) }); @@ -1764,30 +1782,30 @@ export default class VisualKeyboard extends EventEmitter implements Ke } } - /** - * Starts an implementation-specific longpress gesture. Separately implemented for - * in-browser and embedded modes. - * @param key The base key of the longpress. - * @returns - */ - startLongpress(key: KeyElement): PendingGesture { - // First-level object/Promise: will produce a subkey popup when the longpress gesture completes. - // 'Returns' a second-level object/Promise: resolves when a subkey is selected or is cancelled. - let pendingLongpress = new InternalPendingLongpress(this, key); - pendingLongpress.promise.then((subkeyPopup) => { - // In-browser-specific handling. - if (subkeyPopup) { - // Append the touch-hold (subkey) array to the OSK - this.topContainer.appendChild(subkeyPopup.element); - this.topContainer.appendChild(subkeyPopup.shim); + // /** + // * Starts an implementation-specific longpress gesture. Separately implemented for + // * in-browser and embedded modes. + // * @param key The base key of the longpress. + // * @returns + // */ + // startLongpress(key: KeyElement): PendingGesture { + // // First-level object/Promise: will produce a subkey popup when the longpress gesture completes. + // // 'Returns' a second-level object/Promise: resolves when a subkey is selected or is cancelled. + // let pendingLongpress = new InternalPendingLongpress(this, key); + // pendingLongpress.promise.then((subkeyPopup) => { + // // In-browser-specific handling. + // if (subkeyPopup) { + // // Append the touch-hold (subkey) array to the OSK + // this.topContainer.appendChild(subkeyPopup.element); + // this.topContainer.appendChild(subkeyPopup.shim); - // Must be placed after its `.element` has been inserted into the DOM. - subkeyPopup.reposition(this); - } - }); + // // Must be placed after its `.element` has been inserted into the DOM. + // subkeyPopup.reposition(this); + // } + // }); - return pendingLongpress; - } + // return pendingLongpress; + // } /** * Initializes all supported gestures given a base key and the triggering touch coordinates. @@ -1821,36 +1839,36 @@ export default class VisualKeyboard extends EventEmitter implements Ke } - if (key['subKeys']) { - let _this = this; + // if (key['subKeys']) { + // let _this = this; - let pendingLongpress = this.startLongpress(key); - if (pendingLongpress == null) { - return; - } - this.pendingSubkey = pendingLongpress; + // let pendingLongpress = this.startLongpress(key); + // if (pendingLongpress == null) { + // return; + // } + // this.pendingSubkey = pendingLongpress; - pendingLongpress.promise.then(function (subkeyPopup) { - if (_this.pendingSubkey == pendingLongpress) { - _this.pendingSubkey = null; - } + // pendingLongpress.promise.then(function (subkeyPopup) { + // if (_this.pendingSubkey == pendingLongpress) { + // _this.pendingSubkey = null; + // } - if (subkeyPopup) { - // Clear key preview if any - _this.showKeyTip(null, false); + // if (subkeyPopup) { + // // Clear key preview if any + // _this.showKeyTip(null, false); - _this.subkeyGesture = subkeyPopup; - subkeyPopup.promise.then(function (keyEvent: KeyEvent) { - // Allow active cancellation, even if the source should allow passive. - // It's an easy and cheap null guard. - if (keyEvent) { - _this.raiseKeyEvent(keyEvent, null); - } - _this.clearPopup(); - }); - } - }); - } + // _this.subkeyGesture = subkeyPopup; + // subkeyPopup.promise.then(function (keyEvent: KeyEvent) { + // // Allow active cancellation, even if the source should allow passive. + // // It's an easy and cheap null guard. + // if (keyEvent) { + // _this.raiseKeyEvent(keyEvent, null); + // } + // _this.clearPopup(); + // }); + // } + // }); + // } } /** @@ -1885,16 +1903,16 @@ export default class VisualKeyboard extends EventEmitter implements Ke this.currentTarget = null; - // If popup is visible, need to move over popup, not over main keyboard - // Could be turned into a browser-longpress specific implementation within browser.PendingLongpress? - if (key1 && key1['subKeys'] != null && this.initTouchCoord) { - if(this.pendingSubkey && this.pendingSubkey instanceof InternalPendingLongpress) { - // Show popup keys immediately if touch moved up towards key array (KMEW-100, Build 353) - if (this.initTouchCoord.y - input.y > this.getLongpressFlickThreshold()) { - this.pendingSubkey.resolve(); - } - } - } + // // If popup is visible, need to move over popup, not over main keyboard + // // Could be turned into a browser-longpress specific implementation within browser.PendingLongpress? + // if (key1 && key1['subKeys'] != null && this.initTouchCoord) { + // if(this.pendingSubkey && this.pendingSubkey instanceof InternalPendingLongpress) { + // // Show popup keys immediately if touch moved up towards key array (KMEW-100, Build 353) + // if (this.initTouchCoord.y - input.y > this.getLongpressFlickThreshold()) { + // this.pendingSubkey.resolve(); + // } + // } + // } // If there is an active popup menu (which can occur from the previous block), // a subkey popup exists; do not allow base key output.