mirror of
https://github.com/keymanapp/keyman.git
synced 2026-08-15 21:29:23 +00:00
feat(web): restores subkey-selection, extends accepted path range
This commit is contained in:
parent
cf82a2f840
commit
df8a14f87c
12 changed files with 293 additions and 226 deletions
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -72,10 +72,12 @@ interface PopConfig {
|
|||
count: number
|
||||
}
|
||||
|
||||
export type ConfigChangeClosure<Type> = (configStackCommand: PushConfig<Type> | PopConfig) => void;
|
||||
|
||||
interface EventMap<Type> {
|
||||
stage: (
|
||||
stageReport: GestureStageReport<Type>,
|
||||
changeConfiguration: (configStackCommand: PushConfig<Type> | PopConfig) => void
|
||||
changeConfiguration: ConfigChangeClosure<Type>
|
||||
) => void;
|
||||
complete: () => void;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -159,6 +159,9 @@ export class MatcherSelector<Type> extends EventEmitter<EventMap<Type>> {
|
|||
}
|
||||
|
||||
const matchPromise = new ManagedPromise<MatcherSelection<Type>>();
|
||||
matchPromise.finally(() => {
|
||||
this._sourceSelector = this._sourceSelector.filter((source) => sources.indexOf(source.source) == -1);
|
||||
});
|
||||
|
||||
/*
|
||||
* First...
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export abstract class InputEventEngine<HoveredItemType, StateToken> extends Inpu
|
|||
abstract registerEventHandlers(): void;
|
||||
abstract unregisterEventHandlers(): void;
|
||||
|
||||
protected buildSampleFor(clientX: number, clientY: number, target: EventTarget, timestamp: number, stateToken: StateToken): InputSample<HoveredItemType, StateToken> {
|
||||
protected buildSampleFor(clientX: number, clientY: number, target: EventTarget, timestamp: number, source: GestureSource<HoveredItemType, StateToken>): InputSample<HoveredItemType, StateToken> {
|
||||
const targetRect = this.config.targetRoot.getBoundingClientRect();
|
||||
const sample: InputSample<HoveredItemType, StateToken> = {
|
||||
clientX: clientX,
|
||||
|
|
@ -14,10 +14,11 @@ export abstract class InputEventEngine<HoveredItemType, StateToken> 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<HoveredItemType, StateToken> 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
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ export class MouseEventEngine<HoveredItemType, StateToken = any> 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) {
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ export class TouchEventEngine<HoveredItemType, StateToken = any> 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) {
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ class ActiveKeyBase {
|
|||
nextlayer: string;
|
||||
sp?: ButtonClass;
|
||||
|
||||
_baseKeyEvent: KeyEvent;
|
||||
private _baseKeyEvent: KeyEvent;
|
||||
isMnemonic: boolean = false;
|
||||
|
||||
proportionalPad: number;
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<SubkeyPopup>;
|
||||
// /**
|
||||
// * (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<SubkeyPopup>;
|
||||
|
||||
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<SubkeyPopup>(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<SubkeyPopup>(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));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -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<KeyEvent>;
|
||||
|
||||
// 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<KeyEvent>(function(resolve) {
|
||||
_this.resolver = resolve;
|
||||
})
|
||||
|
||||
this.vkbd = vkbd;
|
||||
constructor(
|
||||
source: GestureSequence<KeyElement>,
|
||||
configChanger: ConfigChangeClosure<KeyElement>,
|
||||
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<nKeys; i++) {
|
||||
var needsTopMargin = false;
|
||||
let nRow=Math.floor(i/nCols);
|
||||
|
|
@ -99,8 +112,9 @@ export default class SubkeyPopup implements RealizedGesture {
|
|||
}
|
||||
let keyGenerator = new OSKSubKey(subKeySpec[i], layer);
|
||||
let kDiv = keyGenerator.construct(vkbd, <KeyElement> 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<KeyElement>) {
|
||||
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<KeyElement, string> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<EventMap> 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<EventMap> 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<KeyElement, string> = 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<KeyElement, string> = 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<EventMap> 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<EventMap> 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<EventMap> 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.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue