diff --git a/web/src/engine/src/osk/input/gestures/gestureHandler.ts b/web/src/engine/src/osk/input/gestures/gestureHandler.ts index 3c585de646..8bb099a07a 100644 --- a/web/src/engine/src/osk/input/gestures/gestureHandler.ts +++ b/web/src/engine/src/osk/input/gestures/gestureHandler.ts @@ -1,8 +1,22 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by jahorton on 2023-10-16. + * + * The GestureHandler interface defines methods common to all OSK + * gesture-handling classes, providing a common abstraction for common-path + * gesture support. + */ + import { ActiveKeyBase, KeyDistribution } from "keyman/engine/keyboard"; export interface GestureHandler { /** - * Triggers cancellation of any further processing for the gesture being handled. + * Triggers cancellation of any further processing for the gesture being + * handled. + * + * The method should be able to safely handle the `cancel` method being called + * multiple times, much like `clearTimeout` in JS. */ cancel(): void; diff --git a/web/src/engine/src/osk/input/gestures/heldRepeater.ts b/web/src/engine/src/osk/input/gestures/heldRepeater.ts index 253d57930c..5feb87c198 100644 --- a/web/src/engine/src/osk/input/gestures/heldRepeater.ts +++ b/web/src/engine/src/osk/input/gestures/heldRepeater.ts @@ -1,3 +1,13 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by jahorton on 2023-10-04. + * + * The HeldRepeater class models key input that is repeated when its + * corresponding key is held. At this time, the class is mostly used to model a + * repeatable Backspace input. + */ + import { GestureSequence } from "keyman/engine/gesture-processor"; import { KeyDistribution } from "keyman/engine/keyboard"; @@ -5,48 +15,47 @@ import { KeyElement } from "../../keyElement.js"; import { GestureHandler } from './gestureHandler.js'; export class HeldRepeater implements GestureHandler { - readonly directlyEmitsKeys = true; + public readonly directlyEmitsKeys = true; + public readonly hasModalVisualization = false; - static readonly INITIAL_DELAY = 500; - static readonly REPEAT_DELAY = 100; + public static readonly INITIAL_DELAY = 500; // msec + public static readonly REPEAT_DELAY = 100; // msec - readonly source: GestureSequence; - readonly hasModalVisualization = false; - readonly repeatClosure: () => void; + private readonly source: GestureSequence; + private readonly baseKey: KeyElement + private readonly actionToRepeat: () => void; + private timerHandle: number; - timerHandle: number; - - constructor(source: GestureSequence, closureToRepeat: () => void) { + constructor(source: GestureSequence, actionToRepeat: () => void) { this.source = source; - const baseKey = source.stageReports[0].item; - baseKey.key.highlight(true); + this.baseKey = source.stageReports[0].item; + this.baseKey.key.highlight(true); + this.actionToRepeat = actionToRepeat; - this.repeatClosure = () => { - closureToRepeat(); - // The repeat-closure may cancel key highlighting. This restores it afterward. - baseKey.key.highlight(true); - } - - - this.timerHandle = window.setTimeout(this.deleteRepeater, HeldRepeater.INITIAL_DELAY); + this.timerHandle = window.setTimeout(() => this.repeatAction(), HeldRepeater.INITIAL_DELAY); this.source.on('complete', () => { - window.clearTimeout(this.timerHandle); - this.timerHandle = undefined; - baseKey.key.highlight(false); + this.cancel(); }); } cancel() { - this.deleteRepeater(); + if(this.timerHandle !== undefined) { + window.clearTimeout(this.timerHandle); + delete this.timerHandle; + } + + this.baseKey.key.highlight(false); this.source.cancel(); } - readonly deleteRepeater = () => { - this.repeatClosure(); - - this.timerHandle = window.setTimeout(this.deleteRepeater, HeldRepeater.REPEAT_DELAY); + private repeatAction() { + this.actionToRepeat(); + // In case the action to repeat cancels key highlighting, we restore it + // afterward. + this.baseKey.key.highlight(true); + this.timerHandle = window.setTimeout(() => this.repeatAction(), HeldRepeater.REPEAT_DELAY); } currentStageKeyDistribution(): KeyDistribution { diff --git a/web/src/engine/src/osk/visualKeyboard.ts b/web/src/engine/src/osk/visualKeyboard.ts index 07bb794110..bbec8144f2 100644 --- a/web/src/engine/src/osk/visualKeyboard.ts +++ b/web/src/engine/src/osk/visualKeyboard.ts @@ -1162,6 +1162,10 @@ export class VisualKeyboard extends EventEmitter implements KeyboardVi return; } + // A re-layout operation can break an ongoing gesture operation. + // See #15226 - this is surprisingly relevant for backspaces! + this.activeGestures.forEach((g) => g.cancel()); + /* Phase 1: calculations possible at the start without triggering _any_ additional layout reflow. (A single, initial reflow may happen depending on DOM manipulations before this method...,