mirror of
https://github.com/keymanapp/keyman.git
synced 2026-09-05 23:27:40 +00:00
Observed while debugging another issue on iOS. The VisualKeyboard.waitForFonts function sets up a busy timer to wait for font loading. This functionality is not even needed on the iOS app, because it's really only used for the touch alias elements. However, it was being called, potentially multiple times, leading to the timer never being removed. On Android, the timer was correctly removed. This aligns the iOS code path with the Android code path.
443 lines
16 KiB
TypeScript
443 lines
16 KiB
TypeScript
// Since 'web' compilation is the path recognized by VSCode, we need to make references here to prevent TS errors.
|
|
// 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
|
|
|
|
/*****************************************/
|
|
/* */
|
|
/* Embedded application-specific code */
|
|
/* */
|
|
/*****************************************/
|
|
|
|
namespace com.keyman.osk {
|
|
VisualKeyboard.prototype.optionKey = function(this: VisualKeyboard, e: KeyElement, keyName: string, keyDown: boolean) {
|
|
let keyman = com.keyman.singleton;
|
|
|
|
if(keyName.indexOf('K_LOPT') >= 0) {
|
|
if(keyDown) {
|
|
this.menuEvent = e;
|
|
if(typeof keyman['showKeyboardList'] == 'function') {
|
|
keyman['showKeyboardList']();
|
|
}
|
|
} else {
|
|
if(this.menuEvent) {
|
|
this.highlightKey(this.menuEvent, false);
|
|
}
|
|
if(typeof(window['menuKeyUp']) == 'function') {
|
|
window['menuKeyUp']();
|
|
}
|
|
this.menuEvent = null;
|
|
}
|
|
} else if(keyName.indexOf('K_ROPT') >= 0) {
|
|
if(keyDown) {
|
|
this.highlightKey(e,false);
|
|
if(typeof keyman['hideKeyboard'] == 'function') {
|
|
keyman['hideKeyboard']();
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// iOS now relies upon native-mode popup key management, so we only implement these hybrid-targeted
|
|
// methods when embedding in Android.
|
|
let device = com.keyman.singleton.util.device;
|
|
|
|
if(device.OS == 'Android') { // assumption - if this file is being loaded, keyman.isEmbedded == true.
|
|
// Send the subkey array to iOS, with centre,top of base key position
|
|
/**
|
|
* Create a popup key array natively
|
|
*
|
|
* @param {Object} key base key element
|
|
*/
|
|
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
|
|
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;
|
|
}
|
|
};
|
|
|
|
// Create a keytip (dummy call - actual keytip handled by native code)
|
|
VisualKeyboard.prototype.createKeyTip = function(this: VisualKeyboard) {
|
|
if(com.keyman.singleton.util.device.formFactor == 'phone') {
|
|
this.keytip = new osk.embedded.KeyTip(window['oskCreateKeyPreview'], window['oskClearKeyPreview']);
|
|
}
|
|
};
|
|
}
|
|
|
|
VisualKeyboard.prototype.waitForFonts = function(this: VisualKeyboard, kfd, ofd) {
|
|
// a dummy function; it's only really used for 'native' KMW.
|
|
return true;
|
|
}
|
|
|
|
SuggestionManager.prototype.platformHold = function(this: SuggestionManager, suggestionObj: BannerSuggestion, isCustom: boolean) {
|
|
// Parallels VisualKeyboard.prototype.touchHold, but for predictive suggestions instead of keystrokes.
|
|
let suggestionEle = suggestionObj.div;
|
|
|
|
// Need to know if it's a <keep> option or not!
|
|
|
|
var xBase = dom.Utils.getAbsoluteX(suggestionEle) - dom.Utils.getAbsoluteX(suggestionEle.parentElement) + suggestionEle.offsetWidth/2,
|
|
yBase = dom.Utils.getAbsoluteY(suggestionEle) - dom.Utils.getAbsoluteY(suggestionEle.parentElement);
|
|
|
|
window['suggestionPopup'](suggestionObj.suggestion, isCustom, xBase, yBase, suggestionEle.offsetWidth, suggestionEle.offsetHeight);
|
|
}
|
|
}
|
|
|
|
namespace com.keyman.text {
|
|
let nativeForBaseKeys = DefaultOutput.forBaseKeys;
|
|
|
|
// Overrides the 'native'-mode implementation with in-app friendly defaults prioritized over 'native' defaults.
|
|
DefaultOutput.forBaseKeys = function(Lkc: KeyEvent): string {
|
|
let Codes = com.keyman.text.Codes;
|
|
let code = Lkc.Lcode;
|
|
|
|
// Intentionally not assigning K_TAB or K_ENTER so KMW will pass them back
|
|
// to the mobile apps to handle (insert characters or navigate forms).
|
|
if (code == Codes.keyCodes.K_oE2) {
|
|
// Using defaults of English US layout for the 102nd key
|
|
if (Lkc.Lmodifiers == Codes.modifierCodes['SHIFT']) {
|
|
return '|';
|
|
} else {
|
|
return '\\';
|
|
}
|
|
}
|
|
|
|
// Use 'native'-mode defaults, determining the character from the OSK
|
|
return nativeForBaseKeys(Lkc);
|
|
}
|
|
}
|
|
|
|
(function() {
|
|
// Declare KeymanWeb and related objects
|
|
var keymanweb=window['keyman'], util=keymanweb['util'],device=util.device;
|
|
var dom = com.keyman.dom;
|
|
|
|
// Allow definition of application name
|
|
keymanweb.options['app']='';
|
|
|
|
// Flag to control refreshing of a keyboard that is already loaded
|
|
keymanweb.mustReloadKeyboard = true;
|
|
|
|
// Skip full page initialization - skips native-mode only code
|
|
keymanweb.isEmbedded = true;
|
|
|
|
// Set default device options
|
|
keymanweb.setDefaultDeviceOptions = function(opt: com.keyman.OptionType) {
|
|
opt['attachType'] = 'manual';
|
|
device.app=opt['app'];
|
|
device.touchable=true;
|
|
device.formFactor='phone';
|
|
if(navigator && navigator.userAgent && navigator.userAgent.indexOf('iPad') >= 0) device.formFactor='tablet';
|
|
if(device.app.indexOf('Mobile') >= 0) device.formFactor='phone';
|
|
if(device.app.indexOf('Tablet') >= 0) device.formFactor='tablet';
|
|
device.browser='native';
|
|
};
|
|
|
|
// Get default style sheet path
|
|
keymanweb.getStyleSheetPath = function(ssName) {
|
|
return keymanweb.rootPath+ssName;
|
|
};
|
|
|
|
// Get KMEI, KMEA keyboard path (overrides default function, allows direct app control of paths)
|
|
keymanweb.getKeyboardPath = function(Lfilename, packageID) {
|
|
return Lfilename + "?v=" + (new Date()).getTime(); /*cache buster*/
|
|
};
|
|
|
|
// Establishes keyboard namespacing.
|
|
keymanweb.namespaceID = function(Pstub) {
|
|
if(typeof(Pstub['KP']) != 'undefined') {
|
|
// An embedded use case wants to utilize package-namespacing.
|
|
Pstub['KI'] = Pstub['KP'] + "::" + Pstub['KI'];
|
|
}
|
|
}
|
|
|
|
// In conjunction with the KeyboardManager's installKeyboard method and script IDs, preserves a keyboard's
|
|
// namespaced ID.
|
|
keymanweb.preserveID = function(Pk) {
|
|
var trueID;
|
|
|
|
// Find the currently-executing script tag; KR is called directly from each keyboard's definition script.
|
|
if(document.currentScript) {
|
|
trueID = document.currentScript.id;
|
|
} else {
|
|
var scripts = document.getElementsByTagName('script');
|
|
var currentScript = scripts[scripts.length-1];
|
|
|
|
trueID = currentScript.id;
|
|
}
|
|
|
|
// Final check that the script tag is valid and appropriate for the loading keyboard.
|
|
if(trueID.indexOf(Pk['KI']) != -1) {
|
|
Pk['KI'] = trueID; // Take the script's version of the ID, which may include package namespacing.
|
|
} else {
|
|
console.error("Error when registering keyboard: current SCRIPT tag's ID does not match!");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Force reload of resource
|
|
*
|
|
* @param {string} s unmodified URL
|
|
* @return {string} modified URL
|
|
*/
|
|
util.unCached = function(s) {
|
|
var t=(new Date().getTime());
|
|
s = s + '?v=' + t;
|
|
return s;
|
|
};
|
|
|
|
util.wait = function() {
|
|
// Empty stub - this function should not be implemented or used within embedded code routes.
|
|
console.warn("util.wait() call attempted in embedded mode!"); // Sends log message to embedding app.
|
|
};
|
|
|
|
util.alert = function() {
|
|
// Empty stub - this function should not be implemented or used within embedded code routes.
|
|
console.warn("util.alert() call attempted in embedded mode!"); // Sends log message to embedding app.
|
|
};
|
|
|
|
/**
|
|
* Refresh element content after change of text (if required)
|
|
*
|
|
* @param {Object} Pelem input element
|
|
*/
|
|
keymanweb.refreshElementContent = function(Pelem)
|
|
{
|
|
if('ontextchange' in keymanweb) keymanweb['ontextchange'](Pelem);
|
|
};
|
|
|
|
/**
|
|
* Set target element text direction (LTR or RTL): not functional for KMEI, KMEA
|
|
*
|
|
* @param {Object} Ptarg Target element
|
|
*/
|
|
keymanweb.domManager._SetTargDir = function(Ptarg){};
|
|
|
|
/**
|
|
* Align input fields (should not be needed with KMEI, KMEA)
|
|
*
|
|
* @param {object} eleList A list of specific elements to align. If nil, selects all elements.
|
|
*
|
|
**/
|
|
keymanweb.alignInputs = function(eleList: HTMLElement[]) {};
|
|
|
|
/**
|
|
* Use rotation events to adjust OSK element positions and scaling if necessary
|
|
*/
|
|
keymanweb.handleRotationEvents = function() {};
|
|
|
|
/**
|
|
* Caret position always determined from the active (but hidden) element
|
|
*
|
|
* @return {boolean}
|
|
**/
|
|
keymanweb.isPositionSynthesized = function()
|
|
{
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* correctOSKTextSize handles rotation event -- currently rebuilds keyboard and adjusts font sizes
|
|
*/
|
|
keymanweb['correctOSKTextSize']=function() {
|
|
let osk: com.keyman.osk.AnchoredOSKView = keymanweb.osk;
|
|
if(osk?.vkbd) {
|
|
osk._Load(); // TODO: replace with osk.refreshLayout() in the future once it can perfectly
|
|
// handle rotations.
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Register a lexical model
|
|
*
|
|
* @param {com.keyman.text.prediction.ModelSpec} model Spec of the lexical model
|
|
*/
|
|
keymanweb['registerModel']=function(model: com.keyman.text.prediction.ModelSpec) {
|
|
keymanweb.modelManager.register(model);
|
|
};
|
|
|
|
/**
|
|
* 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) {
|
|
let osk = keymanweb.osk;
|
|
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();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Return position of language menu key to KeymanTouch
|
|
*
|
|
* @return {string} comma-separated x,y,w,h of language menu key
|
|
*
|
|
**/
|
|
keymanweb['touchMenuPos'] = function() {
|
|
let osk = keymanweb.osk;
|
|
if(osk == null || osk.vkbd == null || osk.vkbd.lgKey == null) {
|
|
return '';
|
|
}
|
|
|
|
var key: HTMLElement = osk.vkbd.lgKey;
|
|
// A CSS change of kmd-key-square from position:fixed to position:static was needed
|
|
// for Android 4.3 to display the OSK correctly, but resulted in the position of
|
|
// the menu key not being returned correctly. The following line gets the
|
|
// key element, instead of the key-square element, fixes this. It should be
|
|
// removed again when the key-square elements are all removed as planned.
|
|
if(typeof key.firstChild != 'undefined' && key.firstChild != null && util.hasClass(key.firstChild,'kmw-key')) {
|
|
key = <HTMLElement> key.firstChild;
|
|
}
|
|
|
|
var w=key.offsetWidth,
|
|
h=key.offsetHeight,
|
|
// Since the full OSKManager '_Box' is displayed within the keyboards' WebViews,
|
|
// these calculations should be performed with respect to that, rather than osk.vkbd.kbdDiv.
|
|
x=dom.Utils.getAbsoluteX(key) - dom.Utils.getAbsoluteX(osk._Box) + w/2,
|
|
y=dom.Utils.getAbsoluteY(key) - dom.Utils.getAbsoluteY(osk._Box);
|
|
|
|
return x+','+y+','+w+','+h;
|
|
};
|
|
|
|
/**
|
|
* Accept an external key ID (from KeymanTouch) and pass to the keyboard mapping
|
|
*
|
|
* @param {string} keyName key identifier which could contain a display layer and a "functional" layer
|
|
* e.g: 'shift-K_E+rightalt-shift'
|
|
**/
|
|
keymanweb['executePopupKey'] = function(keyName: string) {
|
|
let osk = keymanweb.osk;
|
|
var origArg = keyName;
|
|
if(!keymanweb.core.activeKeyboard || !osk.vkbd) {
|
|
return false;
|
|
}
|
|
|
|
/* Clear any pending (non-popup) key */
|
|
osk.vkbd.keyPending = null;
|
|
|
|
// Changes for Build 353 to resolve KMEI popup key issues
|
|
keyName=keyName.replace('popup-',''); //remove popup prefix if present (unlikely)
|
|
|
|
// Regex for 'display layer'-'virtual key name'+'optional functional layer'
|
|
// Can't just split on '-' because some layers like ctrl-shift contain it.
|
|
// Virtual key name starts with T_, K_, or U_
|
|
// matches[1]: displayLayer (not used)
|
|
// matches[2]: keyId
|
|
// matches[3]: optional functionalLayer
|
|
let matches = keyName.match(/^(.+)-([TKU]_[^+]+)\+?(.+)?$/);
|
|
if (matches == null) {
|
|
return false;
|
|
}
|
|
keyName = matches[2] + (matches[3] ? '+' + matches[3] : '');
|
|
|
|
// Note: this assumes Lelem is properly attached and has an element interface.
|
|
// Currently true in the Android and iOS apps.
|
|
var Lelem=keymanweb.domManager.lastActiveElement;
|
|
keymanweb.domManager.initActiveElement(Lelem);
|
|
|
|
// 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.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 + "'");
|
|
}
|
|
};
|
|
|
|
/**
|
|
* API endpoint for hardware keystroke events from Android external keyboards
|
|
*
|
|
* @param {number} code key identifier
|
|
* @param {number} shift shift state (0x01=left ctrl 0x02=right ctrl 0x04=left alt 0x08=right alt
|
|
* 0x10=shift 0x20=ctrl 0x40=alt)
|
|
* @param {number} lstates lock state (0x0200=no caps 0x0400=num 0x0800=no num 0x1000=scroll 0x2000=no scroll locks)
|
|
* @return {boolean} false when KMW _has_ fully handled the event and true when not.
|
|
**/
|
|
keymanweb['executeHardwareKeystroke'] = function(code, shift, lstates = 0): boolean {
|
|
if(!keymanweb.core.activeKeyboard || code == 0) {
|
|
return false;
|
|
}
|
|
|
|
// Clear any pending (non-popup) key
|
|
keymanweb.osk.vkbd.keyPending = null;
|
|
|
|
// Note: this assumes Lelem is properly attached and has an element interface.
|
|
// Currently true in the Android and iOS apps.
|
|
var Lelem = keymanweb.domManager.lastActiveElement;
|
|
|
|
keymanweb.domManager.initActiveElement(Lelem);
|
|
|
|
// Check the virtual key
|
|
var Lkc: com.keyman.text.KeyEvent = {
|
|
Lmodifiers: shift,
|
|
vkCode: code,
|
|
Lcode: code,
|
|
Lstates: lstates,
|
|
LisVirtualKey: true,
|
|
kName: '',
|
|
device: keymanweb.util.physicalDevice.coreSpec, // As we're executing a hardware keystroke.
|
|
isSynthetic: false
|
|
};
|
|
|
|
try {
|
|
// Now that we've manually constructed a proper keystroke-sourced KeyEvent, pass control
|
|
// off to the processor for its actual execution.
|
|
|
|
// Should return 'false' when KMW _has_ fully handled the event and 'true' when not.
|
|
const ruleBehavior: com.keyman.text.RuleBehavior = keymanweb.core.processKeyEvent(Lkc, com.keyman.dom.Utils.getOutputTarget(Lelem));
|
|
|
|
return !ruleBehavior || ruleBehavior.triggerKeyDefault;
|
|
} catch (err) {
|
|
console.error(err.message, err);
|
|
return false;
|
|
}
|
|
};
|
|
})();
|