change(web): gesture-preview longevity, causality

This commit is contained in:
Joshua A. Horton 2023-10-31 11:38:53 +07:00
parent 1ff8d68043
commit 5be435b9d3
7 changed files with 112 additions and 45 deletions

View file

@ -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<KeyElement>,
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)

View file

@ -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;

View file

@ -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...

View file

@ -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) {

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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<EventMap> implements Ke
// Popup key management
keytip: KeyTip;
gesturePreviewHost: GesturePreviewHost;
globeHint: GlobeHint;
activeGestures: GestureHandler[] = [];
@ -388,7 +391,8 @@ export default class VisualKeyboard extends EventEmitter<EventMap> implements Ke
const sourceTrackingMap: Record<string, {
source: GestureSource<KeyElement, string>,
roamingHighlightHandler: (sample: InputSample<KeyElement, string>) => void,
key: KeyElement
key: KeyElement,
previewHost: GesturePreviewHost
}> = {};
const gestureHandlerMap = new Map<GestureSequence<KeyElement>, GestureHandler[]>();
@ -396,6 +400,14 @@ export default class VisualKeyboard extends EventEmitter<EventMap> 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<EventMap> 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<EventMap> 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<EventMap> 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<EventMap> 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<EventMap> 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<EventMap> 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<EventMap> 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<EventMap> 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<EventMap> 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<EventMap> 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<EventMap> implements Ke
window.clearTimeout(this.deleting);
}
this.keytip?.show(null, false, this);
this.keytip?.show(null, false, this, null);
}
lockLayer(enable: boolean) {