mirror of
https://github.com/keymanapp/keyman.git
synced 2026-09-24 00:27:40 +00:00
Merge pull request #5387 from keymanapp/refactor/web/osk-centralized-gestures
refactor(web): longpresses, groundwork for additional gestures 🍕
This commit is contained in:
commit
75e47c1ccc
11 changed files with 636 additions and 306 deletions
|
|
@ -221,10 +221,10 @@ namespace com.keyman.keyboards {
|
|||
return Lkc;
|
||||
}
|
||||
|
||||
public getSubkey(id: string): ActiveKey {
|
||||
public getSubkey(coreID: string): ActiveKey {
|
||||
if(this.sk) {
|
||||
for(let key of this.sk) {
|
||||
if(key.id == id) {
|
||||
if(key.coreID == coreID) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// References the base Keyman object (and consequently, the rest of the core objects).
|
||||
/// <reference path="kmwbase.ts" />
|
||||
/// <reference path="osk/embedded/keytip.ts" />
|
||||
/// <reference path="osk/embedded/pendingLongpress.ts" />
|
||||
|
||||
// KeymanWeb 11.0
|
||||
// Copyright 2019 SIL International
|
||||
|
|
@ -52,17 +53,22 @@ namespace com.keyman.osk {
|
|||
*
|
||||
* @param {Object} key base key element
|
||||
*/
|
||||
VisualKeyboard.prototype.touchHold = function(this: VisualKeyboard, key: KeyElement) {
|
||||
let util = com.keyman.singleton.util;
|
||||
if(key['subKeys'] && (typeof(window['oskCreatePopup']) == 'function')) {
|
||||
VisualKeyboard.prototype.startLongpress = function(this: VisualKeyboard, key: KeyElement): PendingGesture {
|
||||
if(typeof(window['oskCreatePopup']) == 'function') {
|
||||
var xBase = dom.Utils.getAbsoluteX(key) - dom.Utils.getAbsoluteX(this.kbdDiv) + key.offsetWidth/2,
|
||||
yBase = dom.Utils.getAbsoluteY(key);
|
||||
|
||||
// #3718: No longer prepend base key to subkey array
|
||||
|
||||
this.popupBaseKey = key;
|
||||
this.popupPending=true;
|
||||
window['oskCreatePopup'](key['subKeys'], xBase, yBase, key.offsetWidth, key.offsetHeight);
|
||||
|
||||
return new embedded.PendingLongpress(this, key);
|
||||
} else {
|
||||
// When embedded within our Android app, we expect the `oskCreatePopup` function to
|
||||
// exist; all subkey control is delegated to the app.
|
||||
//
|
||||
// No function = big problem.
|
||||
console.error("Missing `oskCreatePopup` function for engine integration.");
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -73,10 +79,6 @@ namespace com.keyman.osk {
|
|||
}
|
||||
};
|
||||
|
||||
VisualKeyboard.prototype.highlightSubKeys = function(this: VisualKeyboard, k, x, y) {
|
||||
// a dummy function; it's only really used for 'native' KMW.
|
||||
}
|
||||
|
||||
VisualKeyboard.prototype.waitForFonts = function(this: VisualKeyboard, kfd, ofd) {
|
||||
// a dummy function; it's only really used for 'native' KMW.
|
||||
return true;
|
||||
|
|
@ -134,8 +136,6 @@ namespace com.keyman.text {
|
|||
// Skip full page initialization - skips native-mode only code
|
||||
keymanweb.isEmbedded = true;
|
||||
|
||||
com.keyman.osk.VisualKeyboard.prototype.popupDelay = 400; // Delay must be less than native touch-hold delay
|
||||
|
||||
// Set default device options
|
||||
keymanweb.setDefaultDeviceOptions = function(opt: com.keyman.OptionType) {
|
||||
opt['attachType'] = 'manual';
|
||||
|
|
@ -270,14 +270,45 @@ namespace com.keyman.text {
|
|||
};
|
||||
|
||||
/**
|
||||
* Function called by Android and iOS when a device-implemented keyboard popup is displayed or hidden
|
||||
* Function called by Android and iOS when a device-implemented keyboard popup
|
||||
* is displayed or hidden. As this is controlled by the app, we use it as a
|
||||
* trigger for 'embedded'-mode gesture state management.
|
||||
*
|
||||
* @param {boolean} isVisible
|
||||
*
|
||||
**/
|
||||
keymanweb['popupVisible'] = function(isVisible)
|
||||
{
|
||||
osk.vkbd.popupVisible = isVisible;
|
||||
keymanweb['popupVisible'] = function(isVisible) {
|
||||
let gesture = osk.vkbd.subkeyGesture as com.keyman.osk.embedded.SubkeyDelegator;
|
||||
let pendingLongpress = osk.vkbd.pendingSubkey;
|
||||
|
||||
/*
|
||||
* If a longpress popup was visible, but is no longer, this means that the
|
||||
* associated longpress gesture was cancelled. It is possible for the base
|
||||
* key to emit if selected at this time; detecton of this is managed by
|
||||
* the `SubkeyDelegator` class.
|
||||
*/
|
||||
if(!isVisible) {
|
||||
if(gesture) {
|
||||
gesture.resolve(null);
|
||||
osk.vkbd.subkeyGesture = null;
|
||||
} else if(pendingLongpress) {
|
||||
pendingLongpress.cancel();
|
||||
osk.vkbd.pendingSubkey = null;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* If the popup was not visible, but now is, that means our previously-pending
|
||||
* longpress is now 'realized' (complete). The OSK relies upon this state
|
||||
* information, which will be properly updated by `resolve`.
|
||||
*
|
||||
* Prominent uses of such state info helps prevent change of base key, key
|
||||
* previews, and key output from occurring while a subkey popup remains active.
|
||||
*/
|
||||
if(isVisible && pendingLongpress) {
|
||||
// Fulfills the first-stage promise.
|
||||
pendingLongpress.resolve();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -318,8 +349,6 @@ namespace com.keyman.text {
|
|||
* @param {string} keyName key identifier
|
||||
**/
|
||||
keymanweb['executePopupKey'] = function(keyName: string) {
|
||||
let core = (<KeymanBase> keymanweb).core;
|
||||
|
||||
var origArg = keyName;
|
||||
if(!keymanweb.core.activeKeyboard || !osk.vkbd) {
|
||||
return false;
|
||||
|
|
@ -333,99 +362,24 @@ namespace com.keyman.text {
|
|||
|
||||
// Can't just split on '-' because some layers like ctrl-shift contain it.
|
||||
let separatorIndex = keyName.lastIndexOf('-');
|
||||
var layer = core.keyboardProcessor.layerId;
|
||||
|
||||
if (separatorIndex > 0) {
|
||||
layer = keyName.substring(0, separatorIndex);
|
||||
keyName = keyName.substring(separatorIndex+1);
|
||||
}
|
||||
if(layer == 'undefined') {
|
||||
layer=core.keyboardProcessor.layerId;
|
||||
}
|
||||
|
||||
// Note: this assumes Lelem is properly attached and has an element interface.
|
||||
// Currently true in the Android and iOS apps.
|
||||
var Lelem=keymanweb.domManager.getLastActiveElement(),keyShiftState=com.keyman.text.KeyboardProcessor.getModifierState(layer);
|
||||
|
||||
var Lelem=keymanweb.domManager.getLastActiveElement();
|
||||
keymanweb.domManager.initActiveElement(Lelem);
|
||||
|
||||
var nextLayer: string;
|
||||
|
||||
// This should be set if we're within this method... but it's best to guard against nulls here, just in case.
|
||||
if(osk.vkbd.popupBaseKey && osk.vkbd.popupBaseKey['key']) {
|
||||
// This is set with the base key of our current subkey elsewhere within the engine.
|
||||
var baseKey: com.keyman.osk.OSKKeySpec = osk.vkbd.popupBaseKey['key'].spec;
|
||||
var found = false;
|
||||
|
||||
if(baseKey.coreID == keyName) {
|
||||
nextLayer = baseKey.nextlayer;
|
||||
found = true;
|
||||
} else {
|
||||
// Search for the specified subkey so we can retrieve its useful properties.
|
||||
// It should be within the popupBaseKey's subkey list.
|
||||
for(let subKey of baseKey.sk) {
|
||||
if(subKey.coreID == keyName) {
|
||||
// ... to consider: why are we not just taking the keyspec wholesale right here?
|
||||
nextLayer = subKey.nextlayer;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!found) {
|
||||
console.warn("Could not find subkey '" + origArg + "' under the current base key '" + baseKey.coreID + "'!");
|
||||
}
|
||||
if(osk.vkbd.subkeyGesture) {
|
||||
let gesture = osk.vkbd.subkeyGesture as com.keyman.osk.embedded.SubkeyDelegator;
|
||||
gesture.resolve(keyName);
|
||||
osk.vkbd.subkeyGesture = null;
|
||||
} else {
|
||||
console.warn("No base key exists for the subkey being executed: '" + origArg + "'");
|
||||
}
|
||||
|
||||
let Codes = com.keyman.text.Codes;
|
||||
|
||||
// Check the virtual key
|
||||
let Lkc: com.keyman.text.KeyEvent = {
|
||||
Lmodifiers: keyShiftState,
|
||||
Lstates: 0,
|
||||
Lcode: Codes.keyCodes[keyName],
|
||||
LisVirtualKey: true,
|
||||
kName: keyName,
|
||||
kNextLayer: nextLayer,
|
||||
vkCode: null, // was originally undefined
|
||||
isSynthetic: true,
|
||||
device: keymanweb.util.device.coreSpec
|
||||
};
|
||||
|
||||
// Process modifier key action
|
||||
if(core.keyboardProcessor.selectLayer(Lkc, true)) { // ignores key's 'nextLayer' property for this check
|
||||
return true;
|
||||
}
|
||||
|
||||
// While we can't source the base KeyEvent properties for embedded subkeys the same way as native,
|
||||
// we can handle many other pre-processing steps the same way with this common method.
|
||||
core.keyboardProcessor.setSyntheticEventDefaults(Lkc);
|
||||
|
||||
//if(!Lkc.Lcode) return false; // Value is now zero if not known (Build 347)
|
||||
//Build 353: revert to prior test to try to fix lack of KMEI output, May 1, 2014
|
||||
if(isNaN(Lkc.Lcode) || !Lkc.Lcode) {
|
||||
// Addresses modifier SHIFT keys.
|
||||
if(nextLayer) {
|
||||
core.keyboardProcessor.selectLayer(Lkc);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Lkc.vkCode=Lkc.Lcode;
|
||||
|
||||
// Now that we have a valid key event, hand it off to the Processor for execution.
|
||||
// This allows the Processor to also handle any predictive-text tasks necessary.
|
||||
let retVal = com.keyman.osk.PreProcessor.handleClick(Lkc, com.keyman.dom.Utils.getOutputTarget(Lelem), null);
|
||||
|
||||
// Special case for embedded to pass K_TAB back to device to process
|
||||
if(Lkc.Lcode == Codes.keyCodes["K_TAB"] || Lkc.Lcode == Codes.keyCodes["K_TABBACK"]
|
||||
|| Lkc.Lcode == Codes.keyCodes["K_TABFWD"]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return retVal;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -493,7 +493,7 @@ namespace com.keyman.osk {
|
|||
// Utilized by the mobile apps; allows them to 'take over' touch handling,
|
||||
// blocking it within KMW when the apps are already managing an ongoing touch-hold.
|
||||
let keyman = com.keyman.singleton;
|
||||
return keyman['osk'].vkbd.popupVisible;
|
||||
return keyman['osk'].vkbd.subkeyGesture && keyman.isEmbedded;
|
||||
}
|
||||
|
||||
protected dealiasSubTarget(target: HTMLDivElement): HTMLDivElement {
|
||||
|
|
|
|||
65
web/source/osk/browser/pendingLongpress.ts
Normal file
65
web/source/osk/browser/pendingLongpress.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
/// <reference path="subkeyPopup.ts" />
|
||||
/// <reference path="../pendingGesture.interface.ts" />
|
||||
|
||||
namespace com.keyman.osk.browser {
|
||||
/**
|
||||
* (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 class PendingLongpress implements PendingGesture {
|
||||
public readonly baseKey: KeyElement;
|
||||
public readonly promise: Promise<SubkeyPopup>;
|
||||
|
||||
public readonly subkeyUI: SubkeyPopup;
|
||||
|
||||
private readonly vkbd: VisualKeyboard;
|
||||
private resolver: (subkeyPopup: SubkeyPopup) => void;
|
||||
|
||||
private timerId: number;
|
||||
private popupDelay: number = 500;
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
public cancel() {
|
||||
if(this.timerId) {
|
||||
window.clearTimeout(this.timerId);
|
||||
this.timerId = null;
|
||||
}
|
||||
|
||||
if(this.resolver) {
|
||||
this.resolver(null);
|
||||
this.resolver = null;
|
||||
}
|
||||
}
|
||||
|
||||
public resolve() {
|
||||
if(this.resolver) {
|
||||
this.resolver(new SubkeyPopup(this.vkbd, this.baseKey));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,54 @@
|
|||
/// <reference path="oskSubKey.ts" />
|
||||
/// <reference path="../realizedGesture.interface.ts" />
|
||||
|
||||
namespace com.keyman.osk.browser {
|
||||
export class SubkeyPopup {
|
||||
/**
|
||||
* Represents a 'realized' longpress gesture's default implementation
|
||||
* within KeymanWeb. Once a touch sequence has been confirmed to
|
||||
* correspond to a longpress gesture, implementations of this class
|
||||
* provide the following:
|
||||
* * The UI needed to present a subkey menu
|
||||
* * The state management needed to present feedback about the
|
||||
* currently-selected subkey to the user
|
||||
* * A `Promise` that will resolve to the user's selected subkey
|
||||
* once the longpress operation is complete.
|
||||
*
|
||||
* As selection of the subkey occurs after the subkey popup is
|
||||
* displayed, selection of the subkey is inherently asynchronous.
|
||||
* The `Promise` may also resolve to `null` if the user indicates
|
||||
* the desire to cancel subkey selection.
|
||||
*/
|
||||
export class SubkeyPopup implements RealizedGesture {
|
||||
public readonly element: HTMLDivElement;
|
||||
public readonly shim: HTMLDivElement;
|
||||
|
||||
private baseKey: KeyElement;
|
||||
|
||||
private vkbd: VisualKeyboard;
|
||||
private currentSelection: KeyElement;
|
||||
|
||||
private callout: HTMLDivElement;
|
||||
|
||||
public readonly baseKey: KeyElement;
|
||||
public readonly promise: Promise<text.KeyEvent>;
|
||||
|
||||
// Resolves the promise that generated this SubkeyPopup.
|
||||
private resolver: (keyEvent: text.KeyEvent) => void;
|
||||
|
||||
constructor(vkbd: VisualKeyboard, e: KeyElement) {
|
||||
let keyman = com.keyman.singleton;
|
||||
let _this = this;
|
||||
|
||||
this.promise = new Promise<text.KeyEvent>(function(resolve) {
|
||||
_this.resolver = resolve;
|
||||
})
|
||||
|
||||
this.vkbd = vkbd;
|
||||
this.baseKey = e;
|
||||
|
||||
// If the user doesn't move their finger and releases, we'll output the base key
|
||||
// by default.
|
||||
this.currentSelection = e;
|
||||
e.key.highlight(true);
|
||||
|
||||
// A tag we directly set on a key element during its construction.
|
||||
let subKeySpec: OSKKeySpec[] = e['subKeys'];
|
||||
|
||||
|
|
@ -18,7 +56,7 @@ namespace com.keyman.osk.browser {
|
|||
// 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');
|
||||
this.baseKey = e;
|
||||
|
||||
var i;
|
||||
subKeys.id='kmw-popup-keys';
|
||||
|
||||
|
|
@ -72,6 +110,18 @@ namespace com.keyman.osk.browser {
|
|||
}
|
||||
}
|
||||
|
||||
finalize(touch: Touch) {
|
||||
if(this.resolver) {
|
||||
let keyEvent: text.KeyEvent = null;
|
||||
if(this.currentSelection) {
|
||||
keyEvent = this.vkbd.initKeyEvent(this.currentSelection, touch);
|
||||
this.currentSelection.key.highlight(false);
|
||||
}
|
||||
this.resolver(keyEvent);
|
||||
}
|
||||
this.resolver = null;
|
||||
}
|
||||
|
||||
reposition(vkbd: VisualKeyboard) {
|
||||
let keyman = com.keyman.singleton;
|
||||
|
||||
|
|
@ -181,7 +231,16 @@ namespace com.keyman.osk.browser {
|
|||
}
|
||||
}
|
||||
|
||||
isVisible(): boolean {
|
||||
return this.element.style.visibility == 'visible';
|
||||
}
|
||||
|
||||
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);
|
||||
|
|
@ -195,5 +254,34 @@ namespace com.keyman.osk.browser {
|
|||
this.callout.parentNode.removeChild(this.callout);
|
||||
}
|
||||
}
|
||||
|
||||
updateTouch(touch: Touch) {
|
||||
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(touch);
|
||||
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(touch)) {
|
||||
this.baseKey.key.highlight(true);
|
||||
this.currentSelection = this.baseKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
48
web/source/osk/embedded/pendingLongpress.ts
Normal file
48
web/source/osk/embedded/pendingLongpress.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
/// <reference path="subkeyDelegator.ts" />
|
||||
/// <reference path="../pendingGesture.interface.ts" />
|
||||
|
||||
namespace com.keyman.osk.embedded {
|
||||
/**
|
||||
* As control over the subkey display timer and the subkey popup are
|
||||
* both handled by the host app within the Android app, this class
|
||||
* serves mostly to communicate longpress state management from the
|
||||
* app to the VisualKeyboard.
|
||||
*
|
||||
* The `resolve()` function should be triggered by the host app
|
||||
* whenever it has recognized a completed longpress and has thus
|
||||
* begun displaying its subkey popup. (Should these two events
|
||||
* ever become separated in time, the latter is the more critical
|
||||
* aspect.)
|
||||
*/
|
||||
export class PendingLongpress implements PendingGesture {
|
||||
private resolver: (delegator: SubkeyDelegator) => void;
|
||||
private readonly vkbd: VisualKeyboard;
|
||||
|
||||
public readonly baseKey: KeyElement;
|
||||
public readonly promise: Promise<SubkeyDelegator>;
|
||||
|
||||
constructor(vkbd: VisualKeyboard, e: KeyElement) {
|
||||
this.vkbd = vkbd;
|
||||
let _this = this;
|
||||
|
||||
this.promise = new Promise<SubkeyDelegator>(function(resolve) {
|
||||
_this.resolver = resolve;
|
||||
});
|
||||
this.baseKey = e;
|
||||
}
|
||||
|
||||
public resolve() {
|
||||
if(this.resolver) {
|
||||
this.resolver(new SubkeyDelegator(this.vkbd, this.baseKey));
|
||||
}
|
||||
this.resolver = null;
|
||||
}
|
||||
|
||||
public cancel() {
|
||||
if(this.resolver) {
|
||||
this.resolver(null);
|
||||
this.resolver = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
110
web/source/osk/embedded/subkeyDelegator.ts
Normal file
110
web/source/osk/embedded/subkeyDelegator.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
/// <reference path="../realizedGesture.interface.ts" />
|
||||
|
||||
namespace com.keyman.osk.embedded {
|
||||
/**
|
||||
* As the subkey popup view is handled by the host app when in embedded mode
|
||||
* within our Android app, this class represents the fact that KMW has
|
||||
* "delegated" subkey UI and selection to the host app. Hence, "Delegator",
|
||||
* rather than "Popup".
|
||||
*
|
||||
* The `resolve` method should be triggered, in some fashion, by the host app
|
||||
* whenever the user has completed their longpress, potentially selecting
|
||||
* a subkey.
|
||||
*
|
||||
* This class will also track the ongoing touch event in case the base key is
|
||||
* reselected, which _is_ managed by this class, not the host app.
|
||||
*/
|
||||
export class SubkeyDelegator implements RealizedGesture {
|
||||
private resolver: (keyEvent: text.KeyEvent) => void;
|
||||
private readonly vkbd: VisualKeyboard;
|
||||
|
||||
public readonly baseKey: KeyElement;
|
||||
public readonly promise: Promise<text.KeyEvent>;
|
||||
|
||||
private movedFromBaseKey: boolean = false;
|
||||
private baseKeySelected: boolean = false;
|
||||
|
||||
constructor(vkbd: VisualKeyboard, e: KeyElement) {
|
||||
this.vkbd = vkbd;
|
||||
|
||||
let _this = this;
|
||||
this.promise = new Promise<text.KeyEvent>(function(resolve) {
|
||||
_this.resolver = resolve;
|
||||
});
|
||||
|
||||
this.baseKey = e;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the ongoing longpress -> subkey gesture, fulfilling this
|
||||
* `SubkeyDelegator`'s `promise` of a `KeyEvent`.
|
||||
*
|
||||
* If no subkey is selected but the original base key is, `resolve(null)`
|
||||
* will return a key event corresponding to the base key.
|
||||
*
|
||||
* @param keyCoreID {string} The 'core ID' (id + modifier layer) of
|
||||
* a selected subkey. May be `null`.
|
||||
*/
|
||||
public resolve(keyCoreID: string) {
|
||||
if(this.resolver) {
|
||||
let keyEvent: text.KeyEvent = null;
|
||||
|
||||
if(keyCoreID == null && this.baseKeySelected) {
|
||||
// Handle selection of base key underneath the subkey array.
|
||||
keyEvent = this.vkbd.keyEventFromSpec(this.baseKey.key.spec as keyboards.ActiveKey, null);
|
||||
this.baseKey.key.highlight(false);
|
||||
} else if(keyCoreID != null) {
|
||||
// This is set with the base key of our current subkey elsewhere within the engine.
|
||||
let baseKey: OSKKeySpec = this.baseKey.key.spec;
|
||||
let selectedKey: OSKKeySpec;
|
||||
|
||||
if(baseKey.coreID == keyCoreID) {
|
||||
selectedKey = baseKey;
|
||||
} else {
|
||||
// ... yeah, there are some funky type shenanigans between the two.
|
||||
// OSKKeySpec is the OSK's... reinterpretation of the ActiveKey type.
|
||||
selectedKey = (baseKey as keyboards.ActiveKey).getSubkey(keyCoreID) as OSKKeySpec;
|
||||
}
|
||||
|
||||
if(!selectedKey) {
|
||||
// While we can't complete successfully, the subkey operation is done; we
|
||||
// should still signal that and update related gesture state management.
|
||||
this.resolver(null);
|
||||
console.error("Could not find subkey '" + keyCoreID + "' under base key '" + baseKey.coreID + "'!");
|
||||
return;
|
||||
}
|
||||
|
||||
keyEvent = this.vkbd.keyEventFromSpec(selectedKey as keyboards.ActiveKey, null);
|
||||
keyEvent.vkCode=keyEvent.Lcode;
|
||||
} // else /* if(keyCoreID == null) */ keyEvent = null; // As initialized at the top.
|
||||
|
||||
this.resolver(keyEvent);
|
||||
}
|
||||
this.resolver = null;
|
||||
}
|
||||
|
||||
public isVisible(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
public clear() {
|
||||
// no-op; it's fully controlled on the app side.
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows this class to detect if the user may have changed their mind and
|
||||
* re-selected the base key.
|
||||
* @param touch
|
||||
*/
|
||||
updateTouch(touch: Touch) {
|
||||
this.baseKeySelected = this.baseKey.key.isUnderTouch(touch);
|
||||
|
||||
// Prevent highlighting & selection before the touch has moved from the base key.
|
||||
if(this.movedFromBaseKey) {
|
||||
this.baseKey.key.highlight(this.baseKeySelected);
|
||||
} else {
|
||||
this.movedFromBaseKey = !this.baseKeySelected;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -430,5 +430,19 @@ namespace com.keyman.osk {
|
|||
|
||||
return t;
|
||||
}
|
||||
|
||||
public isUnderTouch(touch: Touch): boolean {
|
||||
let x = touch.clientX;
|
||||
let y = touch.clientY;
|
||||
|
||||
let btn = this.btn;
|
||||
let x0 = dom.Utils.getAbsoluteX(btn);
|
||||
let y0 = dom.Utils.getAbsoluteY(btn);//-document.body.scrollTop;
|
||||
|
||||
let x1 = x0 + btn.offsetWidth;
|
||||
let y1 = y0 + btn.offsetHeight;
|
||||
|
||||
return (x > x0 && x < x1 && y > y0 && y < y1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
45
web/source/osk/pendingGesture.interface.ts
Normal file
45
web/source/osk/pendingGesture.interface.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
namespace com.keyman.osk {
|
||||
/**
|
||||
* Used for evaluating potential gestures. Classes adhering to this interface
|
||||
* should be instantiated whenever the (implied) state-machine allows a new
|
||||
* touch event to mark the start of a potential new gesture.
|
||||
*
|
||||
* For example, whenever a user touches a base key and there are no "realized"
|
||||
* (fully-completed, but as-of-yet unresolved) gestures, that state allows the
|
||||
* start of a potential new longpress event.
|
||||
*
|
||||
* The role of the `PendingGesture` is complete whenever all touch-events and
|
||||
* conditions necessary for a modeled gesture have been met. As this point,
|
||||
* it should be `resolve`d, fulfilling its `promise`. This results in a
|
||||
* `RealizedGesture` appropriate for the gesture type that is used to obtain
|
||||
* the final `KeyEvent` for the overall gesture sequence.
|
||||
*
|
||||
* For example, a "longpress" is considered resolved once the user has maintained
|
||||
* an active, stationary touch point on the same key for a sufficiently long
|
||||
* period without releasing it.
|
||||
* * Were it released earlier, that would result in selection of a base key.
|
||||
*
|
||||
* Alternatively, a "flick" might be considered resolved if:
|
||||
* * a user has rapidly moved a touch point in a consistent direction
|
||||
* * for a long enough distance
|
||||
* * and _then_ releases that touch point within a short timeframe.
|
||||
*
|
||||
* The pending gesture should only `resolve` to a realized gesture once
|
||||
* _all_ such conditions are met, confirming that this specific gesture,
|
||||
* and _only_ this specific gesture, could have resulted from the active
|
||||
* touch sequence.
|
||||
*
|
||||
* The `RealizedGesture` that results and is 'returned' via the Promise will
|
||||
* be handled by the `VisualKeyboard` class, which will retrieve and forward
|
||||
* any `KeyEvent` that results from the overall gesture input sequence.
|
||||
*
|
||||
* @see `RealizedGesture`
|
||||
*/
|
||||
export interface PendingGesture {
|
||||
readonly baseKey: KeyElement;
|
||||
readonly promise: Promise<RealizedGesture>;
|
||||
|
||||
cancel(): void;
|
||||
resolve?(): void;
|
||||
}
|
||||
}
|
||||
34
web/source/osk/realizedGesture.interface.ts
Normal file
34
web/source/osk/realizedGesture.interface.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
namespace com.keyman.osk {
|
||||
/**
|
||||
* Implementations of this interface allow individual types of gestures to
|
||||
* specify any additional user interaction and functionality (which may
|
||||
* include UI elements) appropriate for obtaining a key event that may be
|
||||
* produced by the modeled gesture type. These should only be instantiated
|
||||
* once the associated `PendingLongpress` is no longer 'pending' - once it
|
||||
* has become clear that the input touch-event sequence could only correspond
|
||||
* to the modeled gesture.
|
||||
*
|
||||
* For example, when a longpress gesture completes - and hence, the user has
|
||||
* kept their finger stationary on the same key for a long enough period -
|
||||
* we display a popup view presenting subkeys corresponding to the gesture's
|
||||
* underlying element. This popup view accepts touch input and completes only
|
||||
* upon release of the ongoing touch sequence.
|
||||
*
|
||||
* Gestures are events that occur over intervals of time, and since some of them
|
||||
* will require time and user interaction after becoming 'realized', these cases
|
||||
* will be inherently async. The simplest way to model this is with `Promise`s.
|
||||
*
|
||||
* If appropriate for the modeled gesture type, an implementation may supply an
|
||||
* instantly-resolving `Promise`. This may be appropriate for modeling "flick"
|
||||
* or "swipe" gestures in the future, which may require no additional input once
|
||||
* such a gesture is fully realized.
|
||||
*/
|
||||
export interface RealizedGesture {
|
||||
readonly baseKey: KeyElement;
|
||||
readonly promise: Promise<text.KeyEvent>;
|
||||
|
||||
clear(): void;
|
||||
isVisible(): boolean;
|
||||
updateTouch(touch: Touch): void;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
/// <reference path="oskBaseKey.ts" />
|
||||
/// <reference path="keytip.interface.ts" />
|
||||
/// <reference path="browser/keytip.ts" />
|
||||
/// <reference path="browser/subkeyPopup.ts" />
|
||||
/// <reference path="browser/pendingLongpress.ts" />
|
||||
|
||||
namespace com.keyman.osk {
|
||||
|
||||
|
|
@ -41,7 +41,6 @@ namespace com.keyman.osk {
|
|||
|
||||
// State-related properties
|
||||
ddOSK: boolean = false;
|
||||
popupVisible: boolean;
|
||||
keyPending: KeyElement;
|
||||
touchPending: Touch;
|
||||
deleteKey: KeyElement;
|
||||
|
|
@ -60,14 +59,13 @@ namespace com.keyman.osk {
|
|||
touchCount: number;
|
||||
currentTarget: KeyElement;
|
||||
|
||||
// Popup key management
|
||||
popupBaseKey: KeyElement;
|
||||
popupPending: boolean = false;
|
||||
subkeyDelayTimer: number;
|
||||
popupDelay: number = 500;
|
||||
// Used by embedded-mode's globe key
|
||||
menuEvent: KeyElement; // Used by embedded-mode.
|
||||
|
||||
// Popup key management
|
||||
keytip: KeyTip;
|
||||
subkeyPopup: browser.SubkeyPopup;
|
||||
pendingSubkey: PendingGesture;
|
||||
subkeyGesture: RealizedGesture;
|
||||
|
||||
get layerId(): string {
|
||||
return this._layerId;
|
||||
|
|
@ -426,7 +424,7 @@ namespace com.keyman.osk {
|
|||
let kbdAspectRatio = layerGroup.offsetWidth / this.kbdDiv.offsetHeight;
|
||||
let baseKeyProbabilities = this.layout.getLayer(this.layerId).getTouchProbabilities(touchKbdPos, kbdAspectRatio);
|
||||
|
||||
if(!this.popupBaseKey || !this.popupBaseKey.key) {
|
||||
if(!this.subkeyGesture || !this.subkeyGesture.baseKey.key) {
|
||||
return baseKeyProbabilities;
|
||||
} else {
|
||||
// A temp-hack, as this was noted just before 14.0's release.
|
||||
|
|
@ -438,7 +436,7 @@ namespace com.keyman.osk {
|
|||
let baseMass = 1.0;
|
||||
|
||||
let baseKeyMass = 1.0;
|
||||
let baseKeyID = this.popupBaseKey.key.spec.coreID;
|
||||
let baseKeyID = this.subkeyGesture.baseKey.key.spec.coreID;
|
||||
|
||||
let popupKeyMass = 0.0;
|
||||
let popupKeyID: string = null;
|
||||
|
|
@ -514,8 +512,7 @@ namespace com.keyman.osk {
|
|||
this.cancelDelete();
|
||||
|
||||
// Prevent multi-touch if popup displayed
|
||||
var sk = document.getElementById('kmw-popup-keys');
|
||||
if((sk && sk.style.visibility == 'visible') || this.popupVisible) {
|
||||
if(this.subkeyGesture && this.subkeyGesture.isVisible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -561,14 +558,20 @@ namespace com.keyman.osk {
|
|||
} else {
|
||||
if(this.keyPending) {
|
||||
this.highlightKey(this.keyPending, false);
|
||||
this.modelKeyClick(this.keyPending, this.touchPending);
|
||||
this.clearPopup();
|
||||
|
||||
if(this.subkeyGesture && this.subkeyGesture instanceof browser.SubkeyPopup) {
|
||||
let subkeyPopup = this.subkeyGesture as browser.SubkeyPopup;
|
||||
subkeyPopup.updateTouch(e.changedTouches[0]);
|
||||
subkeyPopup.finalize(e.changedTouches[0]);
|
||||
} else {
|
||||
this.modelKeyClick(this.keyPending, this.touchPending);
|
||||
}
|
||||
// Decrement the number of unreleased touch points to prevent
|
||||
// sending the keystroke again when the key is actually released
|
||||
this.touchCount--;
|
||||
} else {
|
||||
// If this key has subkey, start timer to display subkeys after delay, set up release
|
||||
this.touchHold(key);
|
||||
this.initGestures(key, e.changedTouches[0]);
|
||||
}
|
||||
this.keyPending = key;
|
||||
this.touchPending = e.changedTouches[0];
|
||||
|
|
@ -583,32 +586,25 @@ namespace com.keyman.osk {
|
|||
**/
|
||||
release: (e: TouchEvent) => void = function(this: VisualKeyboard, e: TouchEvent) {
|
||||
// Prevent incorrect multi-touch behaviour if native or device popup visible
|
||||
var sk = document.getElementById('kmw-popup-keys'), t = this.currentTarget;
|
||||
var t = this.currentTarget;
|
||||
|
||||
// Clear repeated backspace if active, preventing 'sticky' behavior.
|
||||
this.cancelDelete();
|
||||
|
||||
if((sk && sk.style.visibility == 'visible')) {
|
||||
if((this.subkeyGesture && this.subkeyGesture.isVisible())) {
|
||||
// Ignore release if a multiple touch
|
||||
if(e.touches.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Cancel (but do not execute) pending key if neither a popup key or the base key
|
||||
if((t == null) || ((t.id.indexOf('popup') < 0) && (t.id != this.popupBaseKey.id))) {
|
||||
this.highlightKey(this.keyPending,false);
|
||||
this.clearPopup();
|
||||
this.keyPending = null;
|
||||
this.touchPending = null;
|
||||
if(this.subkeyGesture instanceof browser.SubkeyPopup) {
|
||||
let subkeyPopup = this.subkeyGesture as browser.SubkeyPopup;
|
||||
subkeyPopup.finalize(e.changedTouches[0]);
|
||||
}
|
||||
}
|
||||
this.highlightKey(this.keyPending,false);
|
||||
this.keyPending = null;
|
||||
this.touchPending = null;
|
||||
|
||||
// Only set when embedded in our Android/iOS app. Signals that the device is handling
|
||||
// subkeys, so we shouldn't allow output for the base key.
|
||||
//
|
||||
// Note that on iOS (at least), this.release() will trigger before kmwembedded.ts's
|
||||
// executePopupKey() function.
|
||||
if(this.popupVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -631,7 +627,6 @@ namespace com.keyman.osk {
|
|||
// Process and clear highlighting of pending target
|
||||
if(this.keyPending) {
|
||||
this.highlightKey(this.keyPending,false);
|
||||
|
||||
// Output character unless moved off key
|
||||
if(this.keyPending.className.indexOf('hidden') < 0 && tc > 0 && !beyondEdge) {
|
||||
this.modelKeyClick(this.keyPending, e.changedTouches[0]);
|
||||
|
|
@ -702,77 +697,29 @@ namespace com.keyman.osk {
|
|||
return;
|
||||
}
|
||||
|
||||
// Do not move over keys if device popup visible
|
||||
if(this.popupVisible) {
|
||||
if(key1 == null) {
|
||||
if(key0) {
|
||||
this.highlightKey(key0,false);
|
||||
}
|
||||
this.keyPending=null;
|
||||
this.touchPending=null;
|
||||
} else {
|
||||
if(key1 == this.popupBaseKey) {
|
||||
if(!key1.classList.contains('kmw-key-touched')) {
|
||||
this.highlightKey(key1,true);
|
||||
}
|
||||
this.keyPending = key1;
|
||||
this.touchPending = e.touches[0];
|
||||
} else {
|
||||
if(key0) {
|
||||
this.highlightKey(key0,false);
|
||||
}
|
||||
this.keyPending = null;
|
||||
this.touchPending = null;
|
||||
}
|
||||
}
|
||||
// Update all gesture tracking. The function returns true if further input processing
|
||||
// should be blocked.
|
||||
if(this.updateGestures(key1, key0, e.changedTouches[0])) {
|
||||
return;
|
||||
}
|
||||
|
||||
var sk=document.getElementById('kmw-popup-keys');
|
||||
|
||||
// Use the popup duplicate of the base key if a phone with a visible popup array
|
||||
if(sk && sk.style.visibility == 'visible' && this.device.formFactor == 'phone' && key1 == this.popupBaseKey) {
|
||||
key1 = <KeyElement> sk.childNodes[0].firstChild;
|
||||
}
|
||||
|
||||
// Identify current touch position (to manage off-key release)
|
||||
this.currentTarget = key1;
|
||||
|
||||
// Clear previous key highlighting
|
||||
if(key0 && key1 && key1 !== key0) {
|
||||
// _Box has (most of) the useful client values.
|
||||
let _Box = this.kbdDiv.parentElement ? this.kbdDiv.parentElement : keyman.osk._Box;
|
||||
let height = this.kbdDiv.offsetHeight;
|
||||
// We need to adjust the offset properties by any offsets related to the active banner.
|
||||
|
||||
// Determine the y-threshold at which touch-cancellation should automatically occur.
|
||||
let rowCount = this.layers[this.layerIndex].row.length;
|
||||
let yBufferThreshold = (0.333 * height / rowCount); // Allows vertical movement by 1/3 the height of a row.
|
||||
var yMin = (this.kbdDiv && _Box) ? Math.max(5, this.kbdDiv.offsetTop - yBufferThreshold) : 5;
|
||||
if(key0 && e.touches[0].pageY < yMin) {
|
||||
this.highlightKey(key0,false);
|
||||
}
|
||||
|
||||
// If popup is visible, need to move over popup, not over main keyboard
|
||||
this.highlightSubKeys(key1,x,y);
|
||||
|
||||
if(sk && sk.style.visibility == 'visible') {
|
||||
// Once a subkey array is displayed, do not allow changing the base key.
|
||||
// Keep that array visible and accept no other options until the touch ends.
|
||||
if(key1 && key1.id.indexOf('popup') < 0 && key1 != this.popupBaseKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Highlight the base key on devices that do not append it to the subkey array.
|
||||
if(key1 && key1 == this.popupBaseKey && key1.className.indexOf('kmw-key-touched') < 0) {
|
||||
this.highlightKey(key1,true);
|
||||
}
|
||||
// Cancel touch if moved up and off keyboard, unless popup keys visible
|
||||
} else {
|
||||
// _Box has (most of) the useful client values.
|
||||
let _Box = this.kbdDiv.parentElement ? this.kbdDiv.parentElement : keyman.osk._Box;
|
||||
let height = this.kbdDiv.offsetHeight;
|
||||
|
||||
// Determine the y-threshold at which touch-cancellation should automatically occur.
|
||||
let rowCount = this.layers[this.layerIndex].row.length;
|
||||
let yBufferThreshold = (0.333 * height / rowCount); // Allows vertical movement by 1/3 the height of a row.
|
||||
var yMin = (this.kbdDiv && _Box) ? Math.max(5, this.kbdDiv.offsetTop - yBufferThreshold) : 5;
|
||||
if(key0 && e.touches[0].pageY < yMin) {
|
||||
this.highlightKey(key0,false);
|
||||
this.showKeyTip(null,false);
|
||||
this.keyPending = null;
|
||||
this.touchPending = null;
|
||||
}
|
||||
this.showKeyTip(null,false);
|
||||
this.keyPending = null;
|
||||
this.touchPending = null;
|
||||
}
|
||||
|
||||
// Replace the target key, if any, by the new target key
|
||||
|
|
@ -787,29 +734,6 @@ namespace com.keyman.osk {
|
|||
this.highlightKey(key1,true);
|
||||
}
|
||||
}
|
||||
|
||||
if(key0 && key1 && (key1 != key0) && (key1.id != '')) {
|
||||
// Display the touch-hold keys (after a pause)
|
||||
this.touchHold(key1);
|
||||
/*
|
||||
// Clear and restart the popup timer
|
||||
if(this.subkeyDelayTimer)
|
||||
{
|
||||
window.clearTimeout(this.subkeyDelayTimer);
|
||||
this.subkeyDelayTimer = null;
|
||||
}
|
||||
if(key1.subKeys != null)
|
||||
{
|
||||
this.subkeyDelayTimer = window.setTimeout(
|
||||
function()
|
||||
{
|
||||
this.clearPopup();
|
||||
this.showSubKeys(key1);
|
||||
}.bind(this),
|
||||
this.popupDelay);
|
||||
}
|
||||
*/
|
||||
}
|
||||
}.bind(this);
|
||||
|
||||
/**
|
||||
|
|
@ -945,8 +869,6 @@ namespace com.keyman.osk {
|
|||
// Turn off key highlighting (or preview)
|
||||
this.highlightKey(e,false);
|
||||
|
||||
let core = com.keyman.singleton.core; // only singleton-based ref currently needed here.
|
||||
|
||||
// Future note: we need to refactor osk.OSKKeySpec to instead be a 'tag field' for
|
||||
// keyboards.ActiveKey. (Prob with generics, allowing the Web-only parts to
|
||||
// be fully specified within the tag.)
|
||||
|
|
@ -958,7 +880,13 @@ namespace com.keyman.osk {
|
|||
console.error("OSK key with ID '" + e.id + "', keyID '" + e.keyId + "' missing needed specification");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Return the event object.
|
||||
return this.keyEventFromSpec(keySpec, touch);
|
||||
}
|
||||
|
||||
keyEventFromSpec(keySpec: keyboards.ActiveKey, touch?: Touch) {
|
||||
let core = com.keyman.singleton.core; // only singleton-based ref currently needed here.
|
||||
|
||||
// Start: mirrors _GetKeyEventProperties
|
||||
|
||||
|
|
@ -1074,42 +1002,15 @@ namespace com.keyman.osk {
|
|||
|
||||
clearPopup() {
|
||||
// Remove the displayed subkey array, if any, and cancel popup request
|
||||
if(this.subkeyPopup) {
|
||||
this.subkeyPopup.clear();
|
||||
this.subkeyPopup = null;
|
||||
if(this.subkeyGesture) {
|
||||
this.subkeyGesture.clear();
|
||||
this.subkeyGesture = null;
|
||||
}
|
||||
|
||||
if(this.subkeyDelayTimer) {
|
||||
window.clearTimeout(this.subkeyDelayTimer);
|
||||
this.subkeyDelayTimer = null;
|
||||
if(this.pendingSubkey) {
|
||||
this.pendingSubkey.cancel();
|
||||
this.pendingSubkey = null;
|
||||
}
|
||||
this.popupBaseKey = null;
|
||||
}
|
||||
|
||||
//#region 'native'-mode subkey handling
|
||||
/**
|
||||
* Display touch-hold array of 'sub-keys' above the currently touched key
|
||||
* @param {Object} e primary key element
|
||||
*/
|
||||
showSubKeys(e: KeyElement) {
|
||||
// Do not show subkeys if key already released
|
||||
if(this.keyPending == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear key preview if any
|
||||
this.showKeyTip(null,false);
|
||||
this.popupBaseKey = e;
|
||||
|
||||
let subKeys = this.subkeyPopup = new browser.SubkeyPopup(this, e);
|
||||
|
||||
// Otherwise append the touch-hold (subkey) array to the OSK
|
||||
let keyman = com.keyman.singleton;
|
||||
keyman.osk._Box.appendChild(subKeys.element);
|
||||
keyman.osk._Box.appendChild(subKeys.shim);
|
||||
|
||||
// Must be placed after its `.element` has been inserted into the DOM.
|
||||
subKeys.reposition(this);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
|
@ -1692,26 +1593,123 @@ namespace com.keyman.osk {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Touch hold key display management
|
||||
*
|
||||
* @param {Object} key base key object
|
||||
*/
|
||||
touchHold(key: KeyElement) {
|
||||
// Clear and restart the popup timer
|
||||
if(this.subkeyDelayTimer) {
|
||||
window.clearTimeout(this.subkeyDelayTimer);
|
||||
this.subkeyDelayTimer = null;
|
||||
/**
|
||||
* 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 {
|
||||
let _this = this;
|
||||
|
||||
// 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 browser.PendingLongpress(this, key);
|
||||
pendingLongpress.promise.then(function(subkeyPopup) {
|
||||
// In-browser-specific handling.
|
||||
if(subkeyPopup) {
|
||||
// Append the touch-hold (subkey) array to the OSK
|
||||
let keyman = com.keyman.singleton;
|
||||
keyman.osk._Box.appendChild(subkeyPopup.element);
|
||||
keyman.osk._Box.appendChild(subkeyPopup.shim);
|
||||
|
||||
// Must be placed after its `.element` has been inserted into the DOM.
|
||||
subkeyPopup.reposition(_this);
|
||||
}
|
||||
});
|
||||
|
||||
return pendingLongpress;
|
||||
}
|
||||
|
||||
if(typeof key['subKeys'] != 'undefined' && key['subKeys'] != null) {
|
||||
this.subkeyDelayTimer = window.setTimeout(
|
||||
function(this: VisualKeyboard) {
|
||||
this.clearPopup();
|
||||
this.showSubKeys(key);
|
||||
}.bind(this), this.popupDelay);
|
||||
/**
|
||||
* Initializes all supported gestures given a base key and the triggering touch coordinates.
|
||||
* @param key The gesture's base key
|
||||
* @param touch The starting touch coordinates for the gesture
|
||||
* @returns
|
||||
*/
|
||||
initGestures(key: KeyElement, touch: Touch) {
|
||||
if(key['subKeys']) {
|
||||
let _this = this;
|
||||
|
||||
let pendingLongpress = this.startLongpress(key);
|
||||
if(pendingLongpress == null) {
|
||||
return;
|
||||
}
|
||||
this.pendingSubkey = pendingLongpress;
|
||||
|
||||
pendingLongpress.promise.then(function(subkeyPopup) {
|
||||
if(_this.pendingSubkey == pendingLongpress) {
|
||||
_this.pendingSubkey = null;
|
||||
}
|
||||
|
||||
if(subkeyPopup) {
|
||||
// Clear key preview if any
|
||||
_this.showKeyTip(null,false);
|
||||
|
||||
_this.subkeyGesture = subkeyPopup;
|
||||
subkeyPopup.promise.then(function(keyEvent: text.KeyEvent) {
|
||||
// Allow active cancellation, even if the source should allow passive.
|
||||
// It's an easy and cheap null guard.
|
||||
if(keyEvent) {
|
||||
PreProcessor.raiseKeyEvent(keyEvent);
|
||||
}
|
||||
_this.clearPopup();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates all currently-pending and activated gestures.
|
||||
*
|
||||
* @param currentKey The key currently underneath the most recent touch coordinate
|
||||
* @param previousKey The previously-selected key
|
||||
* @param touch The current touch-coordinate for the gesture
|
||||
* @returns true if should fully capture input, false if input should 'fall through'.
|
||||
*/
|
||||
updateGestures(currentKey: KeyElement, previousKey: KeyElement, touch: Touch): boolean {
|
||||
let key0 = previousKey;
|
||||
let key1 = currentKey;
|
||||
|
||||
// Clear previous key highlighting, allow subkey controller to highlight as appropriate.
|
||||
if(this.subkeyGesture) {
|
||||
if(key0) {
|
||||
key0.key.highlight(false);
|
||||
}
|
||||
this.subkeyGesture.updateTouch(touch);
|
||||
|
||||
this.keyPending = null;
|
||||
this.touchPending = null;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
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) {
|
||||
// Show popup keys immediately if touch moved up towards key array (KMEW-100, Build 353)
|
||||
if((this.touchY - touch.pageY > 5) && this.pendingSubkey && this.pendingSubkey instanceof browser.PendingLongpress) {
|
||||
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.
|
||||
if(this.subkeyGesture) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if(key0 && key1 && (key1 != key0) && (key1.id != '')) {
|
||||
// While there may not be an active subkey menu, we should probably update which base key
|
||||
// is being highlighted by the current touch & start a pending longpress for it.
|
||||
this.clearPopup();
|
||||
this.initGestures(key1, touch);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
optionKey(e: KeyElement, keyName: string, keyDown: boolean) {
|
||||
let keyman = com.keyman.singleton;
|
||||
|
|
@ -1731,32 +1729,6 @@ namespace com.keyman.osk {
|
|||
}
|
||||
};
|
||||
|
||||
// Manage popup key highlighting
|
||||
highlightSubKeys(k: KeyElement, x: number, y: number) {
|
||||
// Test for subkey array, return if none
|
||||
|
||||
// Issue: if `k` is itself a subkey, this won't do subkey highlighting correctly.
|
||||
// That "common case" is actually handled through _standard_ key highlighting.
|
||||
if(k == null || k['subKeys'] == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Highlight key at touch position (and clear other highlighting)
|
||||
var skBox=document.getElementById('kmw-popup-keys');
|
||||
|
||||
//#region This section fills a different role than the method name would suggest.
|
||||
// Might correspond better to a 'checkInstantSubkeys' or something.
|
||||
|
||||
// Show popup keys immediately if touch moved up towards key array (KMEW-100, Build 353)
|
||||
if((this.touchY-y > 5) && skBox == null) {
|
||||
if(this.subkeyDelayTimer) {
|
||||
window.clearTimeout(this.subkeyDelayTimer);
|
||||
}
|
||||
this.showSubKeys(k);
|
||||
}
|
||||
//#endregion
|
||||
};
|
||||
|
||||
/**
|
||||
* Add (or remove) the keytip preview (if KeymanWeb on a phone device)
|
||||
*
|
||||
|
|
@ -1771,8 +1743,8 @@ namespace com.keyman.osk {
|
|||
return;
|
||||
}
|
||||
|
||||
var sk=this.subkeyPopup,
|
||||
popup = (sk && sk.element.style.visibility == 'visible')
|
||||
let sk = this.subkeyGesture;
|
||||
let popup = (sk && sk.isVisible());
|
||||
|
||||
// If popup keys are active, do not show the key tip.
|
||||
on = popup ? false : on;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue