mirror of
https://github.com/keymanapp/keyman.git
synced 2026-10-16 02:47:31 +00:00
change(web): keyboard activation as part of ContextManager, related odds and ends
This commit is contained in:
parent
c55ad3f5a0
commit
acca3f0dbe
12 changed files with 401 additions and 573 deletions
|
|
@ -1,5 +1,6 @@
|
|||
import Keyboard from "./keyboard.js";
|
||||
import { KeyboardHarness } from "./keyboardHarness.js";
|
||||
import KeyboardProperties from "./keyboardProperties.js";
|
||||
|
||||
export default abstract class KeyboardLoaderBase {
|
||||
private _harness: KeyboardHarness;
|
||||
|
|
@ -19,5 +20,48 @@ export default abstract class KeyboardLoaderBase {
|
|||
return promise;
|
||||
}
|
||||
|
||||
protected abstract loadKeyboardInternal(uri: string): Promise<Keyboard>;
|
||||
public loadKeyboardFromStub(stub: KeyboardProperties & { filename: string }) {
|
||||
this.harness.install();
|
||||
let promise = this.loadKeyboardInternal(stub.filename, stub.id);
|
||||
|
||||
promise = promise.catch((err: Error) => {
|
||||
if(err == KeyboardLoaderBase.scriptErrorMessage(stub.filename)) {
|
||||
// Enhance the error message.
|
||||
err.message = KeyboardLoaderBase.scriptErrorMessage(stub);
|
||||
} else if(err == KeyboardLoaderBase.missingErrorMessage(stub.filename)) {
|
||||
// Same thing here.
|
||||
err.message = KeyboardLoaderBase.scriptErrorMessage(stub);
|
||||
}
|
||||
|
||||
throw err;
|
||||
})
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
protected abstract loadKeyboardInternal(uri: string, id?: string): Promise<Keyboard>;
|
||||
|
||||
protected static scriptErrorMessage(uri: string);
|
||||
protected static scriptErrorMessage(metadata: KeyboardProperties & { filename: string });
|
||||
protected static scriptErrorMessage(arg: string | (KeyboardProperties & { filename: string })) {
|
||||
if(typeof arg == "string") {
|
||||
const uri = arg;
|
||||
return `Error registering the keyboard script at ${uri}; it may contain an error.`
|
||||
} else {
|
||||
const stub = arg;
|
||||
return `Error registering the ${stub.name} keyboard for ${stub.langName}; keyboard script at ${stub.filename} may contain an error.`
|
||||
}
|
||||
}
|
||||
|
||||
protected static missingErrorMessage(uri: string);
|
||||
protected static missingErrorMessage(metadata: KeyboardProperties & { filename: string });
|
||||
protected static missingErrorMessage(arg: string | (KeyboardProperties & { filename: string })) {
|
||||
if(typeof arg == "string") {
|
||||
const uri = arg;
|
||||
return `Cannot find the keyboard at ${uri}.`
|
||||
} else {
|
||||
const stub = arg;
|
||||
return `Cannot find the ${stub.name} keyboard for ${stub.langName} at ${stub.filename}.`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -28,7 +28,7 @@ export class DOMKeyboardLoader extends KeyboardLoaderBase {
|
|||
this.performCacheBusting = cacheBust || false;
|
||||
}
|
||||
|
||||
protected loadKeyboardInternal(uri: string): Promise<Keyboard> {
|
||||
protected loadKeyboardInternal(uri: string, id?: string): Promise<Keyboard> {
|
||||
const promise = new ManagedPromise<Keyboard>();
|
||||
|
||||
if(this.performCacheBusting) {
|
||||
|
|
@ -38,15 +38,20 @@ export class DOMKeyboardLoader extends KeyboardLoaderBase {
|
|||
try {
|
||||
const document = this.harness._jsGlobal.document;
|
||||
const script = document.createElement('script');
|
||||
if(id) {
|
||||
script.id = id;
|
||||
}
|
||||
document.head.appendChild(script);
|
||||
script.onerror = promise.reject;
|
||||
script.onerror = () => {
|
||||
promise.reject(new Error(KeyboardLoaderBase.missingErrorMessage(uri)));
|
||||
}
|
||||
script.onload = () => {
|
||||
if(this.harness.loadedKeyboard) {
|
||||
const keyboard = this.harness.loadedKeyboard;
|
||||
this.harness.loadedKeyboard = null;
|
||||
promise.resolve(keyboard);
|
||||
} else {
|
||||
promise.reject();
|
||||
promise.reject(new Error(KeyboardLoaderBase.scriptErrorMessage(uri)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,15 +23,21 @@ export class NodeKeyboardLoader extends KeyboardLoaderBase {
|
|||
}
|
||||
|
||||
protected loadKeyboardInternal(uri: string): Promise<Keyboard> {
|
||||
// `fs` does not like 'file:///'; it IS "File System" oriented, after all, and wants a path, not a URI.
|
||||
if(uri.indexOf('file:///') == 0) {
|
||||
uri = uri.substring('file:///'.length);
|
||||
}
|
||||
|
||||
let script;
|
||||
try {
|
||||
script = new vm.Script(fs.readFileSync(uri).toString());
|
||||
} catch (err) {
|
||||
return Promise.reject(new Error(KeyboardLoaderBase.missingErrorMessage(uri)));
|
||||
}
|
||||
try {
|
||||
// `fs` does not like 'file:///'; it IS "File System" oriented, after all, and wants a path, not a URI.
|
||||
if(uri.indexOf('file:///') == 0) {
|
||||
uri = uri.substring('file:///'.length);
|
||||
}
|
||||
const script = new vm.Script(fs.readFileSync(uri).toString());
|
||||
script.runInContext(this.harness._jsGlobal);
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
return Promise.reject(new Error(KeyboardLoaderBase.scriptErrorMessage(uri)));
|
||||
}
|
||||
|
||||
const keyboard = this.harness.loadedKeyboard;
|
||||
|
|
|
|||
|
|
@ -4,12 +4,13 @@ import fs from 'fs';
|
|||
import { createRequire } from 'module';
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
import { KeyboardHarness, KeyboardInterface, MinimalKeymanGlobal, Mock } from '@keymanapp/keyboard-processor';
|
||||
import { KeyboardHarness, KeyboardInterface, KeyboardLoaderBase, MinimalKeymanGlobal, Mock } from '@keymanapp/keyboard-processor';
|
||||
import { NodeKeyboardLoader } from '@keymanapp/keyboard-processor/node-keyboard-loader';
|
||||
|
||||
describe('Headless keyboard loading', function() {
|
||||
const laoPath = require.resolve('@keymanapp/common-test-resources/keyboards/lao_2008_basic.js');
|
||||
const khmerPath = require.resolve('@keymanapp/common-test-resources/keyboards/khmer_angkor.js');
|
||||
const nonKeyboardPath = require.resolve('@keymanapp/common-test-resources/index.mjs');
|
||||
// Common test suite setup.
|
||||
|
||||
let device = {
|
||||
|
|
@ -101,5 +102,28 @@ describe('Headless keyboard loading', function() {
|
|||
|
||||
assert.equal(khmer_keyboard.id, "Keyboard_khmer_angkor");
|
||||
});
|
||||
|
||||
it('throws distinct errors', async function() {
|
||||
const invalidPath = 'totally_invalid_path.js';
|
||||
|
||||
let harness = new KeyboardInterface({}, MinimalKeymanGlobal);
|
||||
let keyboardLoader = new NodeKeyboardLoader(harness);
|
||||
let missingError;
|
||||
try {
|
||||
await keyboardLoader.loadKeyboardFromPath(invalidPath);
|
||||
} catch (err) {
|
||||
missingError = err;
|
||||
assert.equal(missingError.message, KeyboardLoaderBase.missingErrorMessage(invalidPath));
|
||||
}
|
||||
|
||||
let scriptLoadError;
|
||||
try {
|
||||
await keyboardLoader.loadKeyboardFromPath(nonKeyboardPath);
|
||||
} catch (err) {
|
||||
scriptLoadError = err;
|
||||
}
|
||||
|
||||
assert.notEqual(scriptLoadError.message, KeyboardLoaderBase.scriptErrorMessage(nonKeyboardPath));
|
||||
});
|
||||
})
|
||||
});
|
||||
|
|
@ -1,16 +1,31 @@
|
|||
import { type Keyboard, Mock, OutputTarget } from '@keymanapp/keyboard-processor';
|
||||
import { type KeyboardStub } from 'keyman/engine/package-cache';
|
||||
import { CookieSerializer } from 'keyman/engine/dom-utils';
|
||||
import {
|
||||
ContextManagerBase,
|
||||
type KeyboardInterface
|
||||
} from 'keyman/engine/main';
|
||||
import { BrowserConfiguration } from './configuration.js';
|
||||
|
||||
interface KeyboardCookie {
|
||||
current: string;
|
||||
}
|
||||
|
||||
export default class ContextManager extends ContextManagerBase {
|
||||
private _activeKeyboard: {keyboard: Keyboard, metadata: KeyboardStub};
|
||||
private config: BrowserConfiguration;
|
||||
private cookieManager = new CookieSerializer<KeyboardCookie>('KeymanWeb_Keyboard');
|
||||
|
||||
initialize(): void {
|
||||
this.on('keyboardasyncload', (stub, completion) => {
|
||||
// TODO: app/browser - display the loader UI if configured?
|
||||
// util.wait('Installing keyboard<br/>' + kbdName);
|
||||
|
||||
completion.then(() => {
|
||||
// Cancel the loader UI.
|
||||
});
|
||||
});
|
||||
|
||||
// TBD: keyman.domManager.init (the page-integration parts)
|
||||
// CTRL+F: `// Exit initialization here if we're using an embedded code path.`
|
||||
// EVERYTHING after that block will likely go here - DOMManager's role always
|
||||
|
|
@ -27,7 +42,7 @@ export default class ContextManager extends ContextManagerBase {
|
|||
return this._activeKeyboard;
|
||||
}
|
||||
|
||||
set activeKeyboard(kbd: {keyboard: Keyboard, metadata: KeyboardStub}) {
|
||||
setKeyboardActiveForTarget(kbd: {keyboard: Keyboard, metadata: KeyboardStub}, target: OutputTarget) {
|
||||
throw new Error('Method not implemented.');
|
||||
// depends on the target
|
||||
// if not set with an "independent keyboard", changes the global.
|
||||
|
|
@ -64,7 +79,7 @@ export default class ContextManager extends ContextManagerBase {
|
|||
* When `null`, such operations will affect the global default; otherwise, such operations
|
||||
* affect only the specified `target`.
|
||||
*/
|
||||
private get keyboardTarget(): OutputTarget {
|
||||
protected get keyboardTarget(): OutputTarget {
|
||||
// Remove `&& false` once the inlined section below is implemented.
|
||||
if(this.activeTarget /* has 'independent keyboard mode activated' */ && false) {
|
||||
return this.activeTarget;
|
||||
|
|
@ -73,39 +88,46 @@ export default class ContextManager extends ContextManagerBase {
|
|||
}
|
||||
}
|
||||
|
||||
async setTargetActiveKeyboardAsync(kbd: Promise<Keyboard>, metadata: KeyboardStub, target: OutputTarget): Promise<boolean> {
|
||||
if(!await this.deferredKeyboardActivationValid(kbd, metadata, target)) {
|
||||
return false;
|
||||
} else {
|
||||
let activatingKeyboard = {
|
||||
keyboard: await kbd,
|
||||
metadata: metadata
|
||||
};
|
||||
|
||||
if(target == this.keyboardTarget) {
|
||||
// TODO: 'beforekeyboardchange' event
|
||||
public async activateKeyboard(keyboardId: string, languageCode?: string, saveCookie?: boolean): Promise<boolean> {
|
||||
saveCookie ||= false;
|
||||
const originalKeyboardTarget = this.keyboardTarget;
|
||||
|
||||
try {
|
||||
let result = await super.activateKeyboard(keyboardId, languageCode, saveCookie);
|
||||
|
||||
if(saveCookie && !originalKeyboardTarget) { // if the active target uses global keyboard settings
|
||||
this.cookieManager.save({current: `${keyboardId}:${languageCode}`});
|
||||
}
|
||||
|
||||
// TODO: set THAT TARGET's active keyboard. May or may not be active!
|
||||
|
||||
if(target == this.keyboardTarget) {
|
||||
// TODO: 'keyboardchange' event
|
||||
// Only do these if the active keyboard-target still matches the original keyboard-target;
|
||||
// otherwise, maintain what's correct for the currently active one.
|
||||
if(originalKeyboardTarget == this.keyboardTarget) {
|
||||
// TODO: app/browser - _SetTargDir (within its ContextManager)
|
||||
// util.addStyleSheet(domManager.setAttachmentFontStyle(kbdStub.KF));
|
||||
// uiManager.justActivated = true; // TODO: Resolve without need for the cast.
|
||||
}
|
||||
|
||||
/*
|
||||
* Alternative to the three above TODOs:
|
||||
* if (same condition met)
|
||||
* this.activeKeyboard = activatingKeyboard
|
||||
* else
|
||||
* // manually set it for that one control; no events.
|
||||
*
|
||||
*/
|
||||
return result;
|
||||
} catch(err) {
|
||||
// non-embedded: if keyboard activation failed, deactivate the keyboard.
|
||||
|
||||
return true;
|
||||
// Make sure we don't infinite-recursion should the deactivate somehow fail.
|
||||
if(this.config.hostDevice.touchable) {
|
||||
// Fallback behavior - if on a touch device, we need to keep a keyboard visible.
|
||||
const defaultStub = this.keyboardCache.defaultStub;
|
||||
await this.activateKeyboard(defaultStub.id, defaultStub.langId, true).catch(() => {});
|
||||
} else {
|
||||
// Fallback behavior - if on a desktop device, the user still has a physical keyboard.
|
||||
// Just clear out the active keyboard & OSK.
|
||||
await this.activateKeyboard('', '', false).catch(() => {});
|
||||
}
|
||||
|
||||
if((this.config as BrowserConfiguration).shouldAlert) {
|
||||
// TODO: util.alert error report
|
||||
}
|
||||
|
||||
throw err; // since the site-dev consumer may want to do their own error-handling.
|
||||
}
|
||||
}
|
||||
|
||||
async setActiveKeyboardAsync(kbd: Promise<Keyboard>, metadata: KeyboardStub): Promise<boolean> {
|
||||
return this.setTargetActiveKeyboardAsync(kbd, metadata, this.keyboardTarget);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import { KeymanEngine as KeymanEngineBase } from 'keyman/engine/main';
|
||||
import { ProcessorInitOptions } from "@keymanapp/keyboard-processor";
|
||||
import { type KeyboardStub } from "keyman/engine/package-cache";
|
||||
|
||||
import { BrowserConfiguration } from './configuration.js';
|
||||
import ContextManager from './contextManager.js';
|
||||
|
|
@ -18,41 +17,4 @@ export class KeymanEngine extends KeymanEngineBase<ContextManager, KeyEventKeybo
|
|||
defaultOutputRules: new DefaultBrowserRules(this.contextManager)
|
||||
};
|
||||
};
|
||||
|
||||
protected async activateKeyboard(keyboardId: string, languageCode?: string, saveCookie?: boolean): Promise<void> {
|
||||
saveCookie ||= false;
|
||||
|
||||
try {
|
||||
await super.activateKeyboard(keyboardId, languageCode, saveCookie);
|
||||
|
||||
if(saveCookie /* && this.contextManager.activeTarget does not have independent-keyboard mode active */) {
|
||||
// TODO: persist the newly-activating keyboard's IDs for use as default upon page reload
|
||||
}
|
||||
|
||||
// TODO: app/browser - _SetTargDir (within its ContextManager)
|
||||
} catch(err) {
|
||||
// non-embedded: if keyboard activation failed, deactivate the keyboard.
|
||||
|
||||
// Make sure we don't infinite-recursion should the deactivate somehow fail.
|
||||
if(this.config.hostDevice.touchable) {
|
||||
// Fallback behavior - if on a touch device, we need to keep a keyboard visible.
|
||||
const defaultStub = this.keyboardRequisitioner.cache.defaultStub;
|
||||
await this.activateKeyboard(defaultStub.id, defaultStub.langId, true).catch(() => {});
|
||||
} else {
|
||||
// Fallback behavior - if on a desktop device, the user still has a physical keyboard.
|
||||
// Just clear out the active keyboard & OSK.
|
||||
await this.activateKeyboard('', '', false).catch(() => {});
|
||||
}
|
||||
|
||||
if((this.config as BrowserConfiguration).shouldAlert) {
|
||||
// TODO: util.alert error report
|
||||
}
|
||||
|
||||
throw err; // since the site-dev consumer may want to do their own error-handling.
|
||||
}
|
||||
}
|
||||
|
||||
protected onKeyboardAsyncLoadStart(requestedStub: KeyboardStub) {
|
||||
// TODO: app/browser - display the loader UI if configured?
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { type Keyboard, Mock } from '@keymanapp/keyboard-processor';
|
||||
import { type Keyboard, Mock, OutputTarget } from '@keymanapp/keyboard-processor';
|
||||
import { KeyboardStub } from 'keyman/engine/package-cache';
|
||||
import { ContextManagerBase, ContextManagerConfiguration } from 'keyman/engine/main';
|
||||
import { WebviewConfiguration } from './configuration.js';
|
||||
|
|
@ -51,42 +51,50 @@ export default class ContextManager extends ContextManagerBase {
|
|||
return this._activeKeyboard;
|
||||
}
|
||||
|
||||
set activeKeyboard(kbd: {keyboard: Keyboard, metadata: KeyboardStub}) {
|
||||
const priorEntry = this._activeKeyboard;
|
||||
|
||||
// Clone the stub before exposing it...
|
||||
if(!this.confirmKeyboardChange(new KeyboardStub(kbd.metadata))) {
|
||||
return;
|
||||
}
|
||||
setKeyboardActiveForTarget(kbd: {keyboard: Keyboard, metadata: KeyboardStub}, target: OutputTarget) {
|
||||
// `target` is irrelevant for `app/webview`, as it'll only ever use 'global' keyboard settings.
|
||||
|
||||
// Clone the object to prevent accidental by-reference changes.
|
||||
this._activeKeyboard = {...kbd};
|
||||
}
|
||||
|
||||
if(priorEntry.keyboard != kbd.keyboard || priorEntry.metadata != kbd.metadata) {
|
||||
this.emit('keyboardchange', kbd);
|
||||
this.resetContext();
|
||||
/**
|
||||
* Reflects the active 'target' upon which any `set activeKeyboard` operation will take place.
|
||||
* For app/webview... there's only one target, thus only a "global default" matters.
|
||||
*/
|
||||
protected get keyboardTarget(): Mock {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public async activateKeyboard(keyboardId: string, languageCode?: string, saveCookie?: boolean): Promise<boolean> {
|
||||
try {
|
||||
return await super.activateKeyboard(keyboardId, languageCode, saveCookie);
|
||||
} catch(err) {
|
||||
// Fallback behavior - we're embedded in a touch-device's webview, so we need to keep a keyboard visible.
|
||||
const defaultStub = this.keyboardCache.defaultStub;
|
||||
await this.activateKeyboard(defaultStub.id, defaultStub.langId, true).catch(() => {});
|
||||
|
||||
throw err; // since the consumer may want to do its own error-handling.
|
||||
}
|
||||
}
|
||||
|
||||
async setActiveKeyboardAsync(kbd: Promise<Keyboard>, metadata: KeyboardStub): Promise<boolean> {
|
||||
if(!this.confirmKeyboardChange) {
|
||||
return false;
|
||||
protected prepareKeyboardForActivation(
|
||||
keyboardId: string,
|
||||
languageCode?: string
|
||||
): {keyboard: Promise<Keyboard>, metadata: KeyboardStub} {
|
||||
const originalKeyboard = this.activeKeyboard;
|
||||
const activatingKeyboard = super.prepareKeyboardForActivation(keyboardId, languageCode);
|
||||
|
||||
// Probably isn't necessary at this point - osk.refreshLayout() exists - but
|
||||
// it's best to keep it around for now and verify later.
|
||||
if(originalKeyboard.metadata.id == activatingKeyboard.metadata.id) {
|
||||
activatingKeyboard.keyboard = activatingKeyboard.keyboard.then((kbd) => {
|
||||
kbd.refreshLayouts()
|
||||
return kbd;
|
||||
});
|
||||
}
|
||||
|
||||
// There is only the one target, so 'default global keyboard' use is fine.
|
||||
if(!await this.deferredKeyboardActivationValid(kbd, metadata, null)) {
|
||||
return false;
|
||||
} else {
|
||||
const activatingKeyboard = {
|
||||
keyboard: await kbd,
|
||||
metadata: metadata
|
||||
};
|
||||
|
||||
this.activeKeyboard = activatingKeyboard;
|
||||
|
||||
// The change may silently fail due to `set activeKeyboard`'s `confirmKeyboardChange` call.
|
||||
return this.activeKeyboard.keyboard == activatingKeyboard.keyboard
|
||||
&& this.activeKeyboard.metadata == activatingKeyboard.metadata;
|
||||
}
|
||||
return activatingKeyboard;
|
||||
}
|
||||
}
|
||||
|
|
@ -52,36 +52,6 @@ export class KeymanEngine extends KeymanEngineBase<ContextManager, PassthroughKe
|
|||
setupEmbeddedListeners(this, this.osk);
|
||||
}
|
||||
|
||||
protected async activateKeyboard(keyboardId: string, languageCode?: string, saveCookie?: boolean): Promise<void> {
|
||||
saveCookie ||= false;
|
||||
|
||||
try {
|
||||
await super.activateKeyboard(keyboardId, languageCode, saveCookie);
|
||||
} catch(err) {
|
||||
// Fallback behavior - we're embedded in a touch-device's webview, so we need to keep a keyboard visible.
|
||||
const defaultStub = this.keyboardRequisitioner.cache.defaultStub;
|
||||
await this.activateKeyboard(defaultStub.id, defaultStub.langId, true).catch(() => {});
|
||||
|
||||
throw err; // since the consumer may want to do its own error-handling.
|
||||
}
|
||||
}
|
||||
|
||||
protected async prepareKeyboardForActivation(
|
||||
keyboardId: string,
|
||||
languageCode?: string
|
||||
): Promise<{keyboard: Keyboard, metadata: KeyboardStub}> {
|
||||
const originalKeyboard = this.contextManager.activeKeyboard;
|
||||
const activatingKeyboard = await super.prepareKeyboardForActivation(keyboardId, languageCode);
|
||||
|
||||
// Probably isn't necessary at this point - osk.refreshLayout() exists - but
|
||||
// it's best to keep it around for now and verify later.
|
||||
if(originalKeyboard.keyboard == activatingKeyboard.keyboard) {
|
||||
activatingKeyboard.keyboard.refreshLayouts();
|
||||
}
|
||||
|
||||
return activatingKeyboard;
|
||||
}
|
||||
|
||||
// Functions that the old 'app/webview' equivalent had always provided to the WebView
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import EventEmitter from 'eventemitter3';
|
||||
import { type Keyboard, type KeyboardInterface, type OutputTarget } from '@keymanapp/keyboard-processor';
|
||||
import { type KeyboardStub } from 'keyman/engine/package-cache';
|
||||
import { DeviceSpec, ManagedPromise, type Keyboard, type KeyboardInterface, type OutputTarget } from '@keymanapp/keyboard-processor';
|
||||
import { StubAndKeyboardCache, type KeyboardStub } from 'keyman/engine/package-cache';
|
||||
import { PredictionContext } from '@keymanapp/input-processor';
|
||||
|
||||
interface EventMap {
|
||||
// target, then keyboard.
|
||||
'targetchange': (target: OutputTarget) => boolean;
|
||||
'beforekeyboardchange': (metadata: KeyboardStub, abortChange: () => void) => void;
|
||||
'keyboardasyncload': (metadata: KeyboardStub, onload: Promise<Error>) => void;
|
||||
'keyboardchange': (kbd: {keyboard: Keyboard, metadata: KeyboardStub}) => void;
|
||||
}
|
||||
|
||||
|
|
@ -18,13 +19,19 @@ export interface ContextManagerConfiguration {
|
|||
*
|
||||
* Does not reset option-stores, variable-stores, etc.
|
||||
*/
|
||||
readonly resetKeyState: (outputTarget?: OutputTarget) => void;
|
||||
readonly resetContext: (outputTarget?: OutputTarget) => void;
|
||||
|
||||
/**
|
||||
* A predictive-state management object that interfaces the predictive-text banner
|
||||
* with the active context.
|
||||
*/
|
||||
readonly predictionContext: PredictionContext;
|
||||
|
||||
/**
|
||||
* The stub & keyboard curation cache holding preloaded keyboards and metadata useable
|
||||
* to load those not yet loaded.
|
||||
*/
|
||||
readonly keyboardCache: StubAndKeyboardCache;
|
||||
}
|
||||
|
||||
interface PendingActivation {
|
||||
|
|
@ -34,12 +41,16 @@ interface PendingActivation {
|
|||
}
|
||||
|
||||
export abstract class ContextManagerBase extends EventEmitter<EventMap> {
|
||||
public static readonly TIMEOUT_THRESHOLD = 10000;
|
||||
|
||||
abstract initialize(): void;
|
||||
|
||||
abstract get activeTarget(): OutputTarget;
|
||||
|
||||
private _predictionContext: PredictionContext;
|
||||
private _resetKeyState: (outputTarget?: OutputTarget) => void;
|
||||
protected keyboardCache: StubAndKeyboardCache;
|
||||
private _resetContext: (outputTarget?: OutputTarget) => void;
|
||||
private _hostDevice: DeviceSpec;
|
||||
|
||||
private pendingActivations: PendingActivation[] = [];
|
||||
|
||||
|
|
@ -47,18 +58,15 @@ export abstract class ContextManagerBase extends EventEmitter<EventMap> {
|
|||
return this._predictionContext;
|
||||
}
|
||||
|
||||
protected get resetKeyState(): (outputTarget?: OutputTarget) => void {
|
||||
return this._resetKeyState;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
configure(config: ContextManagerConfiguration) {
|
||||
// TODO: Set in followup configuration method. Part of initialization?
|
||||
this._resetKeyState = config.resetKeyState;
|
||||
this._resetContext = config.resetContext;
|
||||
this._predictionContext = config.predictionContext;
|
||||
this.keyboardCache = config.keyboardCache;
|
||||
}
|
||||
|
||||
insertText(kbdInterface: KeyboardInterface, Ptext: string, PdeadKey: number) {
|
||||
|
|
@ -82,16 +90,20 @@ export abstract class ContextManagerBase extends EventEmitter<EventMap> {
|
|||
}
|
||||
|
||||
resetContext() {
|
||||
this._resetKeyState(this.activeTarget);
|
||||
this._resetContext(this.activeTarget);
|
||||
this.predictionContext.resetContext();
|
||||
}
|
||||
|
||||
abstract get activeKeyboard(): {keyboard: Keyboard, metadata: KeyboardStub};
|
||||
protected abstract get keyboardTarget(): OutputTarget;
|
||||
|
||||
// TODO: should `activateKeyboard` (on KeymanEngine) be relocated to within here?
|
||||
// It seems to make sense, and was originally a goal.
|
||||
abstract set activeKeyboard(kbd: {keyboard: Keyboard, metadata: KeyboardStub});
|
||||
abstract setActiveKeyboardAsync(kbd: Promise<Keyboard>, metadata: KeyboardStub): Promise<boolean>;
|
||||
/**
|
||||
* Ensures that newly activated keyboards are set correctly within managed context, possibly
|
||||
* against inactive output targets.
|
||||
* @param kbd
|
||||
* @param target
|
||||
*/
|
||||
protected abstract setKeyboardActiveForTarget(kbd: {keyboard: Keyboard, metadata: KeyboardStub}, target: OutputTarget);
|
||||
|
||||
/**
|
||||
* Checks the pending keyboard-activation array for an entry corresponding to the specified
|
||||
|
|
@ -100,7 +112,7 @@ export abstract class ContextManagerBase extends EventEmitter<EventMap> {
|
|||
* May be `null`, which corresponds to the global default Keyboard.
|
||||
* @returns `true` if pending activation is still valid, `false` otherwise.
|
||||
*/
|
||||
private findAndPopActivation(target: OutputTarget): boolean {
|
||||
private findAndPopActivation(target: OutputTarget): PendingActivation {
|
||||
// Array.findIndex requires Chrome 45+. :(
|
||||
let activationIndex;
|
||||
for(activationIndex = 0; activationIndex < this.pendingActivations.length; activationIndex++) {
|
||||
|
|
@ -110,11 +122,10 @@ export abstract class ContextManagerBase extends EventEmitter<EventMap> {
|
|||
}
|
||||
|
||||
if(activationIndex == this.pendingActivations.length) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
this.pendingActivations.splice(activationIndex, 1);
|
||||
return true;
|
||||
return this.pendingActivations.splice(activationIndex, 1)[0];
|
||||
}
|
||||
|
||||
protected confirmKeyboardChange(metadata: KeyboardStub): boolean {
|
||||
|
|
@ -127,7 +138,11 @@ export abstract class ContextManagerBase extends EventEmitter<EventMap> {
|
|||
return eventReturn.continue;
|
||||
}
|
||||
|
||||
protected async deferredKeyboardActivationValid(kbdPromise: Promise<Keyboard>, metadata: KeyboardStub, target: OutputTarget): Promise<boolean> {
|
||||
protected async deferredKeyboardActivation(
|
||||
kbdPromise: Promise<Keyboard>,
|
||||
metadata: KeyboardStub,
|
||||
target: OutputTarget
|
||||
): Promise<PendingActivation> {
|
||||
const activation: PendingActivation = {
|
||||
target: target,
|
||||
keyboard: kbdPromise,
|
||||
|
|
@ -140,6 +155,155 @@ export abstract class ContextManagerBase extends EventEmitter<EventMap> {
|
|||
await kbdPromise;
|
||||
|
||||
// The keyboard-load is complete; is the activation still desired?
|
||||
return this.findAndPopActivation(target);
|
||||
const activationAfterAwait = this.findAndPopActivation(target);
|
||||
if(activationAfterAwait == activation) {
|
||||
return activation;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change active keyboard to keyboard selected by (internal) name and language code
|
||||
*
|
||||
* Test if selected keyboard already loaded, and simply update active stub if so.
|
||||
* Otherwise, insert a script to download and insert the keyboard from the repository
|
||||
* or user-indicated file location.
|
||||
*
|
||||
* TODO: The old 'recorder' tool stubbed the old _SetActiveKeyboard method, but should now stub this
|
||||
* instead. Or, perhaps one of the newer internal events.
|
||||
* @param keyboardId
|
||||
* @param languageCode
|
||||
* @param saveCookie
|
||||
* @returns
|
||||
*/
|
||||
public async activateKeyboard(keyboardId: string, languageCode?: string, saveCookie?: boolean): Promise<boolean> {
|
||||
const activatingKeyboard = this.prepareKeyboardForActivation(keyboardId, languageCode);
|
||||
const originalKeyboardTarget = this.keyboardTarget;
|
||||
|
||||
// Triggers `beforeKeyboardChange` event
|
||||
// - this.osk.startHide(false), when needed, could be called via handler here...
|
||||
// and on 'onAsyncKeyboardLoad'
|
||||
// Also include an 'abort' check based upon it.
|
||||
if(!this.confirmKeyboardChange(activatingKeyboard.metadata)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const keyboard = await activatingKeyboard.keyboard;
|
||||
if(keyboard == null && activatingKeyboard.metadata) {
|
||||
// Cancelled - the activation was async and no longer valid.
|
||||
return false;
|
||||
}
|
||||
|
||||
this.setKeyboardActiveForTarget({
|
||||
keyboard: keyboard,
|
||||
metadata: activatingKeyboard.metadata
|
||||
}, this.keyboardTarget);
|
||||
|
||||
// Only trigger `keyboardchange` events when they will affect the active context.
|
||||
if(this.keyboardTarget == originalKeyboardTarget) {
|
||||
// Perform standard context-reset ops, including processNewContextEvent.
|
||||
this.resetContext();
|
||||
// Will trigger KeymanEngine handler that passes keyboard to the OSK, displays it.
|
||||
this.emit('keyboardchange', this.activeKeyboard);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on the provided keyboard id and language code, selects and (if necessary) loads the
|
||||
* corresponding keyboard but does not activate it.
|
||||
*
|
||||
* This acts as a helper to `activateKeyboard`, helping to centralize and DRY out the actual
|
||||
* activation of the requested keyboard.
|
||||
* @param keyboardId
|
||||
* @param languageCode
|
||||
* @returns
|
||||
*/
|
||||
protected prepareKeyboardForActivation(
|
||||
keyboardId: string,
|
||||
languageCode?: string
|
||||
): {keyboard: Promise<Keyboard>, metadata: KeyboardStub} {
|
||||
// Set default language code
|
||||
languageCode ||= '';
|
||||
|
||||
// Check that the saved keyboard is currently registered
|
||||
let requestedStub = this.keyboardCache.getStub(keyboardId, languageCode);
|
||||
|
||||
// Mobile device addition: force selection of the first keyboard if none set
|
||||
if(this._hostDevice.touchable && !requestedStub) {
|
||||
// Pick the oldest-registered stub as default.
|
||||
requestedStub = this.keyboardCache.defaultStub;
|
||||
} else if(!requestedStub) {
|
||||
return {
|
||||
keyboard: Promise.resolve(null),
|
||||
metadata: null
|
||||
};
|
||||
}
|
||||
|
||||
// Check if current keyboard matches requested keyboard, but not (necessarily) stub
|
||||
if(keyboardId == this.activeKeyboard.metadata.id) {
|
||||
const keyboard = this.activeKeyboard.keyboard;
|
||||
// In this case, the keyboard is loaded; just update the stub.
|
||||
|
||||
return {
|
||||
keyboard: Promise.resolve(keyboard),
|
||||
metadata: requestedStub
|
||||
};
|
||||
}
|
||||
|
||||
// Determine if the keyboard was previously loaded but is not active; use the cached, pre-loaded version if so.
|
||||
let keyboard: Keyboard;
|
||||
if(keyboard = this.keyboardCache.getKeyboardForStub(requestedStub)) {
|
||||
return {
|
||||
keyboard: Promise.resolve(keyboard),
|
||||
metadata: requestedStub
|
||||
};
|
||||
} else {
|
||||
// It's async time - the keyboard is not preloaded within the cache. Use the stub's data to load it.
|
||||
|
||||
// Provide a Promise for completion of the async load process.
|
||||
const completionPromise = new ManagedPromise<Error>();
|
||||
this.emit('keyboardasyncload', requestedStub, completionPromise.corePromise);
|
||||
|
||||
let keyboardPromise = this.keyboardCache.fetchKeyboardForStub(requestedStub);
|
||||
let timeoutPromise = new Promise<void>((resolve, reject) => {
|
||||
const timeoutMsg = `Sorry, the ${requestedStub.name} keyboard for ${requestedStub.langName} is not currently available.`;
|
||||
window.setTimeout(() => reject(new Error(timeoutMsg)), ContextManagerBase.TIMEOUT_THRESHOLD);
|
||||
});
|
||||
|
||||
let combinedPromise = Promise.race([keyboardPromise, timeoutPromise]);
|
||||
|
||||
// Ensure the async-load Promise completes properly.
|
||||
combinedPromise.then(() => completionPromise.resolve(null));
|
||||
combinedPromise.catch((err) => {
|
||||
completionPromise.resolve(err);
|
||||
throw err;
|
||||
});
|
||||
|
||||
// Now the fun part: note the original call's parameters as a pending activation.
|
||||
let promise = this.deferredKeyboardActivation(keyboardPromise, requestedStub, this.keyboardTarget);
|
||||
return {
|
||||
keyboard: promise.then(async (activation) => {
|
||||
// Is the activation we requested still pending, or was it cancelled in favor of a
|
||||
// different activation in some manner?
|
||||
if(!activation) {
|
||||
// If the user chose to load a different keyboard afterward that would affect the same
|
||||
// output target, the activation is no longer valid.
|
||||
return Promise.resolve(null);
|
||||
} else if(activation.target == this.keyboardTarget && !this.confirmKeyboardChange(requestedStub)) {
|
||||
// If still valid, but it would affect the active output target, we provide another chance
|
||||
// to cancel the keyboard change - after all, we're in an async op.
|
||||
return Promise.resolve(null);
|
||||
} else {
|
||||
// If still valid but it won't affect the currently-active output target, we don't ask to verify.
|
||||
// It wouldn't affect the active context, so a corresponding event would be too unclear / confusing.
|
||||
return keyboardPromise;
|
||||
}
|
||||
}),
|
||||
metadata: requestedStub
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -97,10 +97,11 @@ export default class KeymanEngine<
|
|||
this.processor = new InputProcessor(config.hostDevice, worker, this.processorConfiguration());
|
||||
|
||||
this.contextManager.configure({
|
||||
resetKeyState: (target) => {
|
||||
resetContext: (target) => {
|
||||
this.processor.keyboardProcessor.resetContext(target);
|
||||
},
|
||||
predictionContext: new PredictionContext(this.processor.languageProcessor, this.processor.keyboardProcessor)
|
||||
predictionContext: new PredictionContext(this.processor.languageProcessor, this.processor.keyboardProcessor),
|
||||
keyboardCache: this.keyboardRequisitioner.cache
|
||||
});
|
||||
|
||||
// TODO: configure that context-manager!
|
||||
|
|
@ -143,11 +144,44 @@ export default class KeymanEngine<
|
|||
contextManager.on('keyboardchange', (kbd) => {
|
||||
this.refreshModel();
|
||||
|
||||
// Hide OSK and do not update keyboard list if using internal keyboard (desktops).
|
||||
// Condition will not be met for touch form-factors; they force selection of a
|
||||
// default keyboard.
|
||||
if(kbd.keyboard == null && kbd.metadata == null) {
|
||||
this.osk.startHide(false);
|
||||
}
|
||||
|
||||
if(this.osk) {
|
||||
this.osk.setNeedsLayout();
|
||||
this.osk.activeKeyboard = kbd;
|
||||
this.osk.present();
|
||||
}
|
||||
});
|
||||
|
||||
contextManager.on('keyboardasyncload', (metadata) => {
|
||||
/* Original implementation pre-modularization:
|
||||
*
|
||||
* > Force OSK display for CJK keyboards (keyboards using a pick list)
|
||||
*
|
||||
* A matching subcondition in the block below will ensure that the OSK activates pre-load
|
||||
* for CJK keyboards. Yes, even before a CJK picker could ever show. We should be fine
|
||||
* without the CJK check so long as a picker keyboard's OSK is kept activated post-load,
|
||||
* when the picker actually needs to be kept persistently-active.
|
||||
* `metadata` would be relevant a the CJK-check, which was based on language codes.
|
||||
*
|
||||
* Of course, as mobile devices don't have guaranteed physical keyboards... we need to
|
||||
* keep the OSK visible for them, hence the actual block below.
|
||||
*/
|
||||
if(this.config.hostDevice.touchable && this.osk?.activationModel) {
|
||||
this.osk.activationModel.enabled = true;
|
||||
// Also note: the OSKView.mayDisable method returns false when hostDevice.touchable = false.
|
||||
// The .startHide() call below will check that method before actually starting an OSK hide.
|
||||
}
|
||||
|
||||
// Always (temporarily) hide the OSK when loading a new keyboard, to ensure
|
||||
// that a failure to load doesn't leave the current OSK displayed
|
||||
this.osk?.startHide(false);
|
||||
})
|
||||
// #endregion
|
||||
}
|
||||
|
||||
|
|
@ -235,111 +269,9 @@ export default class KeymanEngine<
|
|||
}
|
||||
}
|
||||
|
||||
async setActiveKeyboard(keyboardId: string, languageCode?: string): Promise<void> {
|
||||
return this.activateKeyboard(keyboardId, languageCode, true);
|
||||
async setActiveKeyboard(keyboardId: string, languageCode?: string): Promise<boolean> {
|
||||
return this.contextManager.activateKeyboard(keyboardId, languageCode, true);
|
||||
}
|
||||
|
||||
protected async activateKeyboard(keyboardId: string, languageCode?: string, saveCookie?: boolean): Promise<void> {
|
||||
saveCookie ||= false;
|
||||
|
||||
// TODO: beforeKeyboardChange
|
||||
// - this.osk.startHide(false), when needed, could be called via handler here...
|
||||
// and on 'onAsyncKeyboardLoad'
|
||||
// Also include an 'abort' check based upon it.
|
||||
|
||||
this.contextManager.activeKeyboard = await this.prepareKeyboardForActivation(keyboardId, languageCode);
|
||||
|
||||
// TODO: keyboardChange
|
||||
// - this.osk.present() could totally be part of the handler for the event.
|
||||
|
||||
this.osk.present();
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on the provided keyboard id and language code, selects and (if necessary) loads the
|
||||
* corresponding keyboard but does not activate it.
|
||||
*
|
||||
* This acts as a helper to `activateKeyboard`, helping to centralize and DRY out the actual
|
||||
* activation of the requested keyboard.
|
||||
* @param keyboardId
|
||||
* @param languageCode
|
||||
* @returns
|
||||
*/
|
||||
protected async prepareKeyboardForActivation(
|
||||
keyboardId: string,
|
||||
languageCode?: string
|
||||
): Promise<{keyboard: Keyboard, metadata: KeyboardStub}> {
|
||||
// Set default language code
|
||||
languageCode ||= '';
|
||||
|
||||
// Check that the saved keyboard is currently registered
|
||||
let requestedStub = this.keyboardRequisitioner.cache.getStub(keyboardId, languageCode);
|
||||
|
||||
// Mobile device addition: force selection of the first keyboard if none set
|
||||
if(this.config.softDevice.touchable && !requestedStub) {
|
||||
// Pick the oldest-registered stub as default.
|
||||
requestedStub = this.keyboardRequisitioner.cache.defaultStub;
|
||||
} else if(!requestedStub) {
|
||||
// Hide OSK and do not update keyboard list if using internal keyboard (desktops)
|
||||
this.osk?.startHide(false);
|
||||
|
||||
return Promise.resolve({
|
||||
keyboard: null,
|
||||
metadata: null
|
||||
});
|
||||
}
|
||||
|
||||
// Check if current keyboard matches requested keyboard, but not (necessarily) stub
|
||||
if(keyboardId == this.contextManager.activeKeyboard.metadata.id) {
|
||||
const keyboard = this.contextManager.activeKeyboard.keyboard;
|
||||
// In this case, the keyboard is loaded; just update the stub.
|
||||
|
||||
return Promise.resolve({
|
||||
keyboard: keyboard,
|
||||
metadata: requestedStub
|
||||
});
|
||||
}
|
||||
|
||||
// Determine if the keyboard was previously loaded but is not active and use the prior load if so.
|
||||
let keyboard: Keyboard;
|
||||
if(keyboard = this.keyboardRequisitioner.cache.getKeyboardForStub(requestedStub)) {
|
||||
return Promise.resolve({
|
||||
keyboard: keyboard,
|
||||
metadata: requestedStub
|
||||
});
|
||||
} else {
|
||||
// async time - the keyboard has not yet been loaded.
|
||||
|
||||
// Original implementation: also checked for CJK and kept OSKs activated pre-load,
|
||||
// before the picker could ever show. We should be fine without it so long as
|
||||
// a picker keyboard's OSK is kept activated post-load.
|
||||
if(this.config.hostDevice.touchable && this.osk?.activationModel) {
|
||||
this.osk.activationModel.enabled = true;
|
||||
}
|
||||
|
||||
this.osk?.startHide(false);
|
||||
|
||||
// TODO: Maybe make this an event of sorts? ... which means extending EventEmitter, etc.
|
||||
// We aren't adding a 'new' event API set quite yet, though.
|
||||
this.onKeyboardAsyncLoadStart(requestedStub);
|
||||
|
||||
let keyboardPromise = this.keyboardRequisitioner.cache.fetchKeyboardForStub(requestedStub);
|
||||
|
||||
let promise = this.contextManager.setActiveKeyboardAsync(keyboardPromise, requestedStub);
|
||||
return promise.then(async (stillValid) => {
|
||||
if(!stillValid) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
return {
|
||||
keyboard: await keyboardPromise,
|
||||
metadata: requestedStub
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected onKeyboardAsyncLoadStart(requestedStub: KeyboardStub) { }
|
||||
}
|
||||
|
||||
// Intent: define common behaviors for both primary app types; each then subclasses & extends where needed.
|
||||
|
|
@ -189,315 +189,6 @@ namespace com.keyman.keyboards {
|
|||
return p;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change active keyboard to keyboard selected by (internal) name and language code
|
||||
*
|
||||
* Test if selected keyboard already loaded, and simply update active stub if so.
|
||||
* Otherwise, insert a script to download and insert the keyboard from the repository
|
||||
* or user-indicated file location.
|
||||
*
|
||||
* Note that the test-case oriented 'recorder' stubs this method to provide active
|
||||
* keyboard stub information. If changing this function, please ensure the recorder is
|
||||
* not affected.
|
||||
*
|
||||
* @param {string} PInternalName
|
||||
* @param {string=} PLgCode
|
||||
* @param {boolean=} saveCookie
|
||||
*/
|
||||
_SetActiveKeyboard(PInternalName: string, PLgCode?: string, saveCookie?: boolean): Promise<void> {
|
||||
let n;
|
||||
|
||||
let keyman = com.keyman.singleton;
|
||||
|
||||
var util = keyman.util;
|
||||
var domManager = keyman.domManager;
|
||||
var osk = keyman.osk;
|
||||
|
||||
let activeKeyboard = keyman.core.activeKeyboard;
|
||||
|
||||
// Set default language code
|
||||
if(arguments.length < 2 || (!PLgCode)) {
|
||||
PLgCode='---';
|
||||
}
|
||||
|
||||
// Check that the saved keyboard is currently registered
|
||||
for(n=0; n < this.keyboardStubs.length; n++) {
|
||||
if(PInternalName == this.keyboardStubs[n]['KI']) {
|
||||
if(PLgCode == this.keyboardStubs[n]['KLC'] || PLgCode == '---') break;
|
||||
// This 'n' is used in the condition for the block below... and that's all.
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile device addition: force selection of the first keyboard if none set
|
||||
if(util.device.touchable && (PInternalName == '' || PInternalName == null || n >= this.keyboardStubs.length)) {
|
||||
if(this.keyboardStubs.length != 0) {
|
||||
PInternalName=this.keyboardStubs[0]['KI'];
|
||||
PLgCode=this.keyboardStubs[0]['KLC'];
|
||||
}
|
||||
}
|
||||
|
||||
// Save name of keyboard (with language code) as a cookie
|
||||
if(arguments.length > 2 && saveCookie) {
|
||||
this.saveCurrentKeyboard(PInternalName,PLgCode);
|
||||
}
|
||||
|
||||
// Check if requested keyboard and stub are currently active
|
||||
if(this.activeStub && activeKeyboard && activeKeyboard.id == PInternalName
|
||||
&& this.activeStub['KI'] == PInternalName //this part of test should not be necessary, but keep anyway
|
||||
&& this.activeStub['KLC'] == PLgCode && !this.keymanweb.mustReloadKeyboard
|
||||
) return Promise.resolve();
|
||||
|
||||
// Check if current keyboard matches requested keyboard, but not stub
|
||||
if(activeKeyboard && (activeKeyboard.id == PInternalName)) {
|
||||
// If so, simply update the active stub
|
||||
for(let Ln=0; Ln<this.keyboardStubs.length; Ln++) {
|
||||
if((this.keyboardStubs[Ln]['KI'] == PInternalName)
|
||||
&& (this.keyboardStubs[Ln]['KLC'] == PLgCode)) {
|
||||
this.activeStub = this.keyboardStubs[Ln];
|
||||
|
||||
// Append a stylesheet for this keyboard for keyboard specific styles
|
||||
// or if needed to specify an embedded font
|
||||
osk.vkbd?.appendStyleSheet();
|
||||
|
||||
// Re-initializate OSK before returning if required
|
||||
if(this.keymanweb.mustReloadKeyboard) {
|
||||
activeKeyboard.refreshLayouts();
|
||||
if(osk) {
|
||||
osk._Load();
|
||||
util.addStyleSheet(domManager.setAttachmentFontStyle(this.activeStub.KF));
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
// huh, does not verify that we matched a stub. Weird.
|
||||
}
|
||||
|
||||
keyman.core.activeKeyboard = null;
|
||||
this.activeStub = null;
|
||||
|
||||
// Hide OSK and do not update keyboard list if using internal keyboard (desktops)
|
||||
if(PInternalName == '') {
|
||||
osk.startHide(false);
|
||||
|
||||
if(!this.keymanweb.isEmbedded) {
|
||||
util.wait(false);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// Determine if the keyboard was previously loaded but is not active and use the prior load if so.
|
||||
for(let Ln=0; Ln<this.keyboards.length; Ln++) { // I1511 - array prototype extended
|
||||
if(this.keyboards[Ln]['KI'] == PInternalName) {
|
||||
keyman.core.activeKeyboard = new Keyboard(this.keyboards[Ln]);
|
||||
// As a rotation may have occurred since the keyboard was swapped out,
|
||||
// we should refresh its layouts.
|
||||
keyman.core.activeKeyboard.refreshLayouts();
|
||||
this.keymanweb.domManager._SetTargDir(this.keymanweb.domManager.lastActiveElement); // I2077 - LTR/RTL timing
|
||||
|
||||
// and update the active stub
|
||||
for(var Ls=0; Ls<this.keyboardStubs.length; Ls++) {
|
||||
if((this.keyboardStubs[Ls]['KI'] == PInternalName) &&
|
||||
(this.keyboardStubs[Ls]['KLC'] == PLgCode || PLgCode == '---')) {
|
||||
this.activeStub = this.keyboardStubs[Ls];
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we've reached this point, this is the first load request for the requested keyboard.
|
||||
if(keyman.core.activeKeyboard == null) {
|
||||
for(let Ln=0; Ln<this.keyboardStubs.length; Ln++) { // I1511 - array prototype extended
|
||||
if((this.keyboardStubs[Ln]['KI'] == PInternalName)
|
||||
&& ((this.keyboardStubs[Ln]['KLC'] == PLgCode) || (PLgCode == '---'))) {
|
||||
// Force OSK display for CJK keyboards (keyboards using a pick list)
|
||||
if(this.isCJK(this.keyboardStubs[Ln]) || util.device.touchable) {
|
||||
osk.displayIfActive = true;
|
||||
}
|
||||
|
||||
// Create a script to load from the server - when it finishes loading, it will register itself,
|
||||
// detect that it is active, and focus as appropriate. The second test is needed to allow recovery from a failed script load
|
||||
|
||||
// Ensure we're not already loading the keyboard.
|
||||
if(!this.keyboardStubs[Ln].asyncLoader) {
|
||||
// Always (temporarily) hide the OSK when loading a new keyboard, to ensure that a failure to load doesn't leave the current OSK displayed
|
||||
if(osk) {
|
||||
osk.startHide(false);
|
||||
}
|
||||
|
||||
var loadingStub = this.keyboardStubs[Ln];
|
||||
// Tag the stub so that we don't double-load the keyboard!
|
||||
loadingStub.asyncLoader = {};
|
||||
|
||||
var kbdName = loadingStub['KN'];
|
||||
var lngName = loadingStub['KL'];
|
||||
kbdName = kbdName.replace(/\s*keyboard\s*/i, '');
|
||||
|
||||
// Setup our default error-messaging callback if it should be implemented.
|
||||
loadingStub.asyncLoader.callback = function(altString, msgType) {
|
||||
var msg = altString || 'Sorry, the '+kbdName+' keyboard for '+lngName+' is not currently available.';
|
||||
|
||||
// Thanks, Closure errors.
|
||||
if(!this.keymanweb.isEmbedded) {
|
||||
util.wait(false);
|
||||
util.internalAlert(altString || msg, function() {
|
||||
this.keymanweb['setActiveKeyboard'](''); // The API call!
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
switch(msgType) { // in case we extend this later.
|
||||
case 'err':
|
||||
console.error(msg);
|
||||
break;
|
||||
case 'warn':
|
||||
default:
|
||||
console.warn(msg);
|
||||
break;
|
||||
}
|
||||
|
||||
if(Ln > 0) {
|
||||
var Ps = this.keyboardStubs[0];
|
||||
this._SetActiveKeyboard(Ps['KI'], Ps['KLC'], true);
|
||||
}
|
||||
}.bind(this);
|
||||
|
||||
loadingStub.asyncLoader.timer = window.setTimeout(loadingStub.asyncLoader.callback, 10000);
|
||||
|
||||
//Display the loading delay bar (Note: only append 'keyboard' if not included in name.)
|
||||
if(!this.keymanweb.isEmbedded) {
|
||||
util.wait('Installing keyboard<br/>' + kbdName);
|
||||
}
|
||||
|
||||
// Installing the script immediately does not work reliably if two keyboards are
|
||||
// loaded in succession if there is any delay in downloading the script.
|
||||
// It works much more reliably if deferred (KMEW-101, build 356)
|
||||
// The effect of a delay can also be tested, for example, by setting the timeout to 5000
|
||||
var manager = this;
|
||||
loadingStub.asyncLoader.promise = new Promise<void>(function(resolve, reject) {
|
||||
window.setTimeout(function(){
|
||||
manager.installKeyboard(resolve, reject, loadingStub);
|
||||
// To be modularized: activation after the keyboard's loaded.
|
||||
},0);
|
||||
});
|
||||
}
|
||||
this.activeStub=this.keyboardStubs[Ln];
|
||||
return this.keyboardStubs[Ln].asyncLoader.promise;
|
||||
}
|
||||
}
|
||||
this.keymanweb.domManager._SetTargDir(this.keymanweb.domManager.lastActiveElement); // I2077 - LTR/RTL timing
|
||||
}
|
||||
|
||||
// Initialize the OSK (provided that the base code has been loaded)
|
||||
if(osk) {
|
||||
osk._Load();
|
||||
util.addStyleSheet(domManager.setAttachmentFontStyle(this.activeStub.KF));
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a keyboard script that has been downloaded from a keyboard server
|
||||
* Operates as the core of a Promise, hence the 'resolve' and 'reject' parameters.
|
||||
*
|
||||
* @param {Object} kbdStub keyboard stub to be loaded.
|
||||
*
|
||||
**/
|
||||
installKeyboard(resolve: () => void, reject: (message?: string) => void, kbdStub: KeyboardStub) {
|
||||
var util = this.keymanweb.util;
|
||||
var osk = this.keymanweb.osk;
|
||||
|
||||
var Lscript = util._CreateElement('script');
|
||||
Lscript.charset="UTF-8"; // KMEW-89
|
||||
Lscript.type = 'text/javascript';
|
||||
|
||||
// Preserve any namespaced IDs by use of the script's id tag attribute!
|
||||
if(this.keymanweb.isEmbedded) {
|
||||
Lscript.id = kbdStub['KI'];
|
||||
}
|
||||
|
||||
var kbdFile = kbdStub['KF'];
|
||||
var kbdLang = kbdStub['KL'];
|
||||
var kbdName = kbdStub['KN'];
|
||||
|
||||
const scriptSrc = this.keymanweb.getKeyboardPath(kbdFile);
|
||||
|
||||
var manager = this;
|
||||
let core = com.keyman.singleton.core;
|
||||
let domManager = com.keyman.singleton.domManager;
|
||||
|
||||
// Add a handler for cases where the new <script> block fails to load.
|
||||
Lscript.addEventListener('error', function() {
|
||||
// Omitted: parts that are now handled modularly.
|
||||
});
|
||||
|
||||
|
||||
// The load event will activate a newly-loaded keyboard if successful and report an error if it is not.
|
||||
Lscript.addEventListener('load', function() {
|
||||
|
||||
// Omitted: parts that are now handled modularly.
|
||||
|
||||
// To determine if the load was successful, we'll need to check the keyboard array for our desired keyboard.
|
||||
// Test if keyboard already loaded
|
||||
var kbd = manager.getKeyboardByID(kbdStub['KI']);
|
||||
if(kbd) { // Is cleared upon a successful load.
|
||||
|
||||
//Activate keyboard, if it's still the active stub.
|
||||
if(kbdStub == manager.activeStub) {
|
||||
manager.doBeforeKeyboardChange(kbd['KI'],kbdStub['KLC']);
|
||||
core.activeKeyboard=new Keyboard(kbd);
|
||||
|
||||
if(manager.keymanweb.domManager.lastActiveElement != null) { // TODO: Resolve without need for the cast.
|
||||
manager.keymanweb.uiManager.justActivated = true; // TODO: Resolve without need for the cast.
|
||||
manager.keymanweb.domManager._SetTargDir(manager.keymanweb.domManager.lastActiveElement);
|
||||
}
|
||||
|
||||
manager.saveCurrentKeyboard(kbd['KI'], kbdStub['KLC']);
|
||||
|
||||
// Prepare and show the OSK for this keyboard
|
||||
if(osk) {
|
||||
osk._Load();
|
||||
util.addStyleSheet(domManager.setAttachmentFontStyle(kbdStub.KF));
|
||||
}
|
||||
|
||||
if(manager.keymanweb.domManager.lastActiveElement != null) {
|
||||
core.processNewContextEvent(dom.Utils.getOutputTarget(manager.keymanweb.domManager.lastActiveElement));
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the wait message, if defined
|
||||
if(!manager.keymanweb.isEmbedded) {
|
||||
util.wait(false);
|
||||
}
|
||||
|
||||
kbdStub.asyncLoader = null;
|
||||
resolve();
|
||||
// A handler portion for cases where the new <script> block loads, but fails to process.
|
||||
} else { // Output error messages even when embedded - they're useful when debugging the apps and KMEA/KMEI engines.
|
||||
// Omitted: parts that are now handled modularly.
|
||||
}
|
||||
}, false);
|
||||
|
||||
// Some browsers may instantly start loading the file when assigned to an element, so we do this after the rest
|
||||
// of our setup. This method is not relocated here (yet) b/c it varies based upon 'native' vs 'embedded'.
|
||||
Lscript.src = scriptSrc;
|
||||
|
||||
try {
|
||||
document.body.appendChild(Lscript);
|
||||
}
|
||||
catch(ex) {
|
||||
try {
|
||||
document.getElementsByTagName('head')[0].appendChild(Lscript);
|
||||
} catch(ex2) {
|
||||
reject('Error registering script ' + scriptSrc + ': ' + ex2);
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.linkedScripts.push(Lscript);
|
||||
}
|
||||
|
||||
/* TODO: why not use util.loadCookie and saveCookie?? */
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ export default class StubAndKeyboardCache extends EventEmitter<EventMap> {
|
|||
throw new Error(`The registered stub for ${withoutPrefix(keyboardID)} lacks a path to the main keyboard file`);
|
||||
}
|
||||
|
||||
const promise = this.keyboardLoader.loadKeyboardFromPath(stub.filename);
|
||||
const promise = this.keyboardLoader.loadKeyboardFromStub(stub);
|
||||
this.keyboardTable[keyboardID] = promise;
|
||||
|
||||
promise.then((kbd) => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue