diff --git a/web/src/engine/osk/src/banner/bannerGestureSet.ts b/web/src/engine/osk/src/banner/bannerGestureSet.ts index b5888cad86..57bc992c71 100644 --- a/web/src/engine/osk/src/banner/bannerGestureSet.ts +++ b/web/src/engine/osk/src/banner/bannerGestureSet.ts @@ -10,7 +10,7 @@ import { BannerSuggestion } from './suggestionBanner.js'; import { simpleTapModelWithReset } from "../input/gestures/specsForLayout.js"; export const BannerSimpleTap: gestures.specs.GestureModel = { - ...deepCopy(simpleTapModelWithReset()), + ...deepCopy(simpleTapModelWithReset(null)), resolutionAction: { type: 'complete', item: 'current' diff --git a/web/src/engine/osk/src/input/gestures/specsForLayout.ts b/web/src/engine/osk/src/input/gestures/specsForLayout.ts index bc0e683941..cc72d3ad13 100644 --- a/web/src/engine/osk/src/input/gestures/specsForLayout.ts +++ b/web/src/engine/osk/src/input/gestures/specsForLayout.ts @@ -87,7 +87,14 @@ export interface GestureParams { * The minimum _net_ touch-path distance after which the direction will be locked. */ dirLockDist: number - } + }, + /** + * Indicates whether roaming-touch oriented behaviors should be enabled. + * + * Note that run-time adjustments to this property after initialization will + * not take affect, unlike the other properties of the overall parameter object. + */ + roamingEnabled?: boolean; } export const DEFAULT_GESTURE_PARAMS: GestureParams = { @@ -153,6 +160,9 @@ let dummy2: LayoutGestureSupportFlags = dummy; * @param params A set of tweakable gesture parameters. It will be closure-captured * and referred to by reference; changes to its values will take * immediate effect during gesture processing. + * + * If params.roamingEnabled is unset, it will be initialized by this + * method based upon layout properties. * @returns */ export function gestureSetForLayout(flags: LayoutGestureSupportFlags, params: GestureParams): GestureModelDefs { @@ -185,9 +195,11 @@ export function gestureSetForLayout(flags: LayoutGestureSupportFlags, params: Ge } }; - const _initialTapModel: GestureModel = deepCopy(flags.hasFlicks ? initialTapModel(params) : initialTapModelWithReset(params)); - const _simpleTapModel: GestureModel = deepCopy(flags.hasFlicks ? simpleTapModel() : simpleTapModelWithReset()); - const longpressModel: GestureModel = deepCopy(longpressModelWithShortcut(params, true, !flags.hasFlicks)); + const doRoaming = params.roamingEnabled ||= !flags.hasFlicks; + + const _initialTapModel: GestureModel = deepCopy(!doRoaming ? initialTapModel(params) : initialTapModelWithReset(params)); + const _simpleTapModel: GestureModel = deepCopy(!doRoaming ? simpleTapModel(params) : simpleTapModelWithReset(params)); + const longpressModel: GestureModel = deepCopy(longpressModelWithShortcut(params, true, doRoaming)); // #region Functions for implementing and/or extending path initial-state checks function withKeySpecFiltering(model: GestureModel, contactIndices: number | number[]) { @@ -226,7 +238,7 @@ export function gestureSetForLayout(flags: LayoutGestureSupportFlags, params: Ge _initialTapModel, _simpleTapModel, withKeySpecFiltering(specialStartModel, 0), - specialKeyEndModel(), + specialKeyEndModel(params), subkeySelectModel(), withKeySpecFiltering(_modipressStartModel, 0), modipressHoldModel(params), @@ -241,7 +253,7 @@ export function gestureSetForLayout(flags: LayoutGestureSupportFlags, params: Ge longpressModel.id, _initialTapModel.id, _modipressStartModel.id, specialStartModel.id ]; - if(flags.hasFlicks) { + if(!doRoaming) { gestureModels.push(withKeySpecFiltering(flickStartModel(params), 0)); gestureModels.push(flickMidModel(params)); gestureModels.push(flickResetModel(params)); @@ -471,11 +483,17 @@ export function modipressContactEndModel(): ContactModel { }; } -export function simpleTapContactModel(): ContactModel { +export function simpleTapContactModel(params: GestureParams, isNotInitial?: boolean): ContactModel { + // Snapshot at model construction; do not update if changed. + const roamingEnabled = params?.roamingEnabled ?? true; // ?? true - used by the banner. + return { itemPriority: 0, - itemChangeAction: 'reject', + itemChangeAction: roamingEnabled ? 'reject' : undefined, pathResolutionAction: 'resolve', + // if roaming, a tap reset should set the base key. + // if not, block path resets. + pathInheritance: (!roamingEnabled && isNotInitial) ? 'full' : 'chop', pathModel: { evaluate: (path) => { if(path.isComplete && !path.wasCancelled) { @@ -531,14 +549,14 @@ export function specialKeyStartModel(): GestureModel { }; } -export function specialKeyEndModel(): GestureModel { +export function specialKeyEndModel(params: GestureParams): GestureModel { return { id: 'special-key-end', resolutionPriority: 0, contacts : [ { model: { - ...simpleTapContactModel(), + ...simpleTapContactModel(params), itemChangeAction: 'resolve' }, endOnResolve: true, @@ -763,7 +781,7 @@ export function multitapEndModel(params: GestureParams): GestureModel { contacts: [ { model: { - ...simpleTapContactModel(), + ...simpleTapContactModel(params), itemPriority: 1, timer: { duration: params.multitap.holdLength, @@ -797,7 +815,7 @@ export function initialTapModel(params: GestureParams): GestureModel { contacts: [ { model: { - ...simpleTapContactModel(), + ...simpleTapContactModel(params), pathInheritance: 'chop', itemPriority: 1, timer: { @@ -821,20 +839,19 @@ export function initialTapModel(params: GestureParams): GestureModel { resolutionAction: { type: 'chain', next: 'multitap-start', - item: 'current' + item: 'base' } } } -export function simpleTapModel(): GestureModel { +export function simpleTapModel(params: GestureParams): GestureModel { return { id: 'simple-tap', resolutionPriority: 1, contacts: [ { model: { - ...simpleTapContactModel(), - pathInheritance: 'chop', + ...simpleTapContactModel(params, true), itemPriority: 1 }, endOnResolve: true @@ -846,7 +863,7 @@ export function simpleTapModel(): GestureModel { sustainWhenNested: true, resolutionAction: { type: 'complete', - item: 'current' + item: 'base' } }; } @@ -865,8 +882,8 @@ export function initialTapModelWithReset(params: GestureParams): GestureModel { - const simpleModel = simpleTapModel(); +export function simpleTapModelWithReset(params: GestureParams): GestureModel { + const simpleModel = simpleTapModel(params); return { ...simpleModel, rejectionActions: { diff --git a/web/src/engine/osk/src/visualKeyboard.ts b/web/src/engine/osk/src/visualKeyboard.ts index ad3819c9d9..303b6ba724 100644 --- a/web/src/engine/osk/src/visualKeyboard.ts +++ b/web/src/engine/osk/src/visualKeyboard.ts @@ -371,7 +371,9 @@ export default class VisualKeyboard extends EventEmitter implements Ke mouseEventRoot: document.body, // Note: at this point in execution, the value will evaluate to NaN! Height hasn't been set yet. // BUT: we need to establish the instance now; we can update it later when height _is_ set. - maxRoamingBounds: new PaddedZoneSource(this.element, [NaN]), + // + // Allow keys to be preserved while the contact point is within banner space + a small fudge-factor. + maxRoamingBounds: new PaddedZoneSource(this.topContainer, [NaN]), // touchEventRoot: this.element, // is the default itemIdentifier: (sample, target) => { /* ALWAYS use the findNearestKey function. @@ -450,17 +452,14 @@ export default class VisualKeyboard extends EventEmitter implements Ke const key = sample.item; const oldKey = sourceTrackingMap[source.identifier].key; - if(key != oldKey) { + if(!this.kbdLayout.hasFlicks && key != oldKey) { this.highlightKey(oldKey, false); this.gesturePreviewHost?.cancel(); this.gesturePreviewHost = null; - if(!this.kbdLayout.hasFlicks) { - const previewHost = this.highlightKey(key, true); - if(previewHost) { - this.gesturePreviewHost = previewHost; - } - + const previewHost = this.highlightKey(key, true); + if(previewHost) { + this.gesturePreviewHost = previewHost; trackingEntry.previewHost = previewHost; sourceTrackingMap[source.identifier].key = key; } diff --git a/web/src/test/manual/web/prediction-mtnt/index.html b/web/src/test/manual/web/prediction-mtnt/index.html index 3613fe72aa..691f69cba4 100644 --- a/web/src/test/manual/web/prediction-mtnt/index.html +++ b/web/src/test/manual/web/prediction-mtnt/index.html @@ -45,6 +45,8 @@ kmw.addKeyboards('sil_euro_latin@en'); kmw.addKeyboards({id:'ye_old_ten_key',name:'Classic 10-key',languages:{id:'en',name:'English'}, filename:('../keyboards/ye_old_ten_key/build/ye_old_ten_key.js')}) + kmw.addKeyboards({id:'gesture_prototyping',name:'Gesture prototyping',languages:{id:'en',name:'English'}, + filename:('../keyboards/gesture_prototyping/build/gesture_prototyping.js')}) var pageRef = (window.location.protocol == 'file:') ? window.location.href.substr(0, window.location.href.lastIndexOf('/')+1)