From f8f844f27edf2e84fd3a590920e117aefe1f8b97 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Wed, 4 Dec 2024 18:19:02 +0100 Subject: [PATCH] feat(web): add `Keyboard` and `KMXKeyboard` classes The `Keyboard` class can be either a JS or KMX keyboard. Also make use of the `Keyboard` class where it makes sense and add TODOs in the places that still need to be implemented for KMX keyboard support. --- web/src/app/browser/src/beepHandler.ts | 13 +-- web/src/app/browser/src/contextManager.ts | 10 ++- web/src/app/browser/src/keymanEngine.ts | 28 +++++-- web/src/app/webview/src/contextManager.ts | 9 ++- .../app/webview/src/passthroughKeyboard.ts | 4 +- .../js-processor/src/jsKeyboardInterface.ts | 4 +- .../src/stubAndKeyboardCache.ts | 20 ++--- web/src/engine/keyboard/build.sh | 1 + web/src/engine/keyboard/src/index.ts | 4 +- .../src/keyboards/keyboardLoaderBase.ts | 21 ++++- .../src/keyboards/keyboardMinimalInterface.ts | 5 ++ .../keyboard/src/keyboards/kmxKeyboard.ts | 24 ++++++ web/src/engine/main/src/contextManagerBase.ts | 20 ++--- web/src/engine/main/src/hardKeyboard.ts | 80 ++++++++++--------- .../main/src/headless/inputProcessor.ts | 11 ++- web/src/engine/main/src/keymanEngine.ts | 38 +++++---- web/src/engine/osk/src/index.ts | 2 +- web/src/engine/osk/src/views/oskView.ts | 20 ++--- .../cases/keyboard/domKeyboardLoader.tests.ts | 52 ++++++------ web/src/test/auto/dom/kbdLoader.ts | 7 +- .../engine/js-processor/kbdInterface.tests.ts | 14 ++-- .../engine/keyboard/keyboard.tests.ts | 5 +- .../keyboard/keyboardLoaderBase.tests.ts | 7 +- .../integrated/web-test-runner.config.mjs | 2 +- .../testing/bulk_rendering/renderer_core.ts | 28 ++++--- .../tools/testing/recorder/browserDriver.ts | 2 +- 26 files changed, 264 insertions(+), 167 deletions(-) create mode 100644 web/src/engine/keyboard/src/keyboards/keyboardMinimalInterface.ts create mode 100644 web/src/engine/keyboard/src/keyboards/kmxKeyboard.ts diff --git a/web/src/app/browser/src/beepHandler.ts b/web/src/app/browser/src/beepHandler.ts index 5427b5024a8..1c1de463a8c 100644 --- a/web/src/app/browser/src/beepHandler.ts +++ b/web/src/app/browser/src/beepHandler.ts @@ -1,4 +1,5 @@ import { type JSKeyboardInterface } from 'keyman/engine/js-processor'; +import { JSKeyboard, type KeyboardMinimalInterface } from 'keyman/engine/keyboard'; import { DesignIFrame, OutputTarget } from 'keyman/engine/element-wrappers'; // Utility object used to handle beep (keyboard error response) operations. @@ -17,9 +18,9 @@ class BeepData { } export class BeepHandler { - readonly keyboardInterface: JSKeyboardInterface; + readonly keyboardInterface: KeyboardMinimalInterface; - constructor(keyboardInterface: JSKeyboardInterface) { + constructor(keyboardInterface: KeyboardMinimalInterface) { this.keyboardInterface = keyboardInterface; } @@ -75,11 +76,13 @@ export class BeepHandler { * Description Reset/terminate beep or flash (not currently used: Aug 2011) */ readonly reset = () => { - this.keyboardInterface.resetContextCache(); + // TODO-web-core: implement for KMX keyboards if needed + if (this.keyboardInterface.activeKeyboard instanceof JSKeyboard) { + (this.keyboardInterface as JSKeyboardInterface).resetContextCache(); + } - var Lbo; this._BeepTimeout = 0; - for(Lbo=0;Lbo /* [b/c Toolbar UI]*/) { - let kbd: JSKeyboard; + let kbd: Keyboard; if(k0) { let kbdDetail = k0 as ReturnType; if(kbdDetail.KeyboardID){ @@ -437,7 +437,8 @@ export default class KeymanEngine extends KeymanEngineBase, metadata: KeyboardStub} { + ): {keyboard: Promise, metadata: KeyboardStub} { const originalKeyboard = this.activeKeyboard; const activatingKeyboard = super.prepareKeyboardForActivation(keyboardId, languageCode); @@ -203,7 +203,10 @@ export default class ContextManager extends ContextManagerBase { - kbd.refreshLayouts() + // TODO-web-core: Do we need to refresh layouts for KMX keyboards also? + if (kbd instanceof JSKeyboard) { + kbd.refreshLayouts(); + } return kbd; }); } diff --git a/web/src/app/webview/src/passthroughKeyboard.ts b/web/src/app/webview/src/passthroughKeyboard.ts index 8ce8864293d..25dec7a8e68 100644 --- a/web/src/app/webview/src/passthroughKeyboard.ts +++ b/web/src/app/webview/src/passthroughKeyboard.ts @@ -1,10 +1,10 @@ -import { DeviceSpec, JSKeyboard, KeyEvent, ManagedPromise } from 'keyman/engine/keyboard'; +import { DeviceSpec, Keyboard, KeyEvent, ManagedPromise } from 'keyman/engine/keyboard'; import { HardKeyboard, processForMnemonicsAndLegacy } from 'keyman/engine/main'; export default class PassthroughKeyboard extends HardKeyboard { readonly baseDevice: DeviceSpec; - public activeKeyboard: JSKeyboard; + public activeKeyboard: Keyboard; constructor(baseDevice: DeviceSpec) { super(); diff --git a/web/src/engine/js-processor/src/jsKeyboardInterface.ts b/web/src/engine/js-processor/src/jsKeyboardInterface.ts index 2ce481c2be2..7d0311c904a 100644 --- a/web/src/engine/js-processor/src/jsKeyboardInterface.ts +++ b/web/src/engine/js-processor/src/jsKeyboardInterface.ts @@ -222,7 +222,7 @@ export class JSKeyboardInterface extends KeyboardHarness { /** * Function registerKeyboard KR * Scope Public - * @param {Object} Pk JSKeyboard object + * @param {Object} Pk Keyboard object * Description Registers a keyboard with KeymanWeb once its script has fully loaded. * * In web-core, this also activates the keyboard; in other modules, this method @@ -231,7 +231,7 @@ export class JSKeyboardInterface extends KeyboardHarness { registerKeyboard(Pk: any): void { // NOTE: This implementation is web-core specific and is intentionally replaced, whole-sale, // by DOM-aware code. - let keyboard = new JSKeyboard(Pk); + const keyboard = new JSKeyboard(Pk); this.loadedKeyboard = keyboard; } diff --git a/web/src/engine/keyboard-storage/src/stubAndKeyboardCache.ts b/web/src/engine/keyboard-storage/src/stubAndKeyboardCache.ts index 523da41fbf9..d9db65dc0ae 100644 --- a/web/src/engine/keyboard-storage/src/stubAndKeyboardCache.ts +++ b/web/src/engine/keyboard-storage/src/stubAndKeyboardCache.ts @@ -1,4 +1,4 @@ -import { JSKeyboard, KeyboardLoaderBase as KeyboardLoader } from "keyman/engine/keyboard"; +import { type Keyboard, JSKeyboard, KeyboardLoaderBase as KeyboardLoader } from "keyman/engine/keyboard"; import { EventEmitter } from "eventemitter3"; import KeyboardStub from "./keyboardStub.js"; @@ -38,12 +38,12 @@ interface EventMap { /** * Indicates that the specified Keyboard has just been added to the cache. */ - keyboardadded: (keyboard: JSKeyboard) => void; + keyboardadded: (keyboard: Keyboard) => void; } export default class StubAndKeyboardCache extends EventEmitter { private stubSetTable: Record> = {}; - private keyboardTable: Record> = {}; + private keyboardTable: Record> = {}; private readonly keyboardLoader: KeyboardLoader; @@ -52,11 +52,11 @@ export default class StubAndKeyboardCache extends EventEmitter { this.keyboardLoader = keyboardLoader; } - getKeyboardForStub(stub: KeyboardStub): JSKeyboard { + getKeyboardForStub(stub: KeyboardStub): Keyboard { return stub ? this.getKeyboard(stub.KI) : null; } - getKeyboard(keyboardID: string): JSKeyboard { + getKeyboard(keyboardID: string): Keyboard { if(!keyboardID) { return null; } @@ -112,14 +112,14 @@ export default class StubAndKeyboardCache extends EventEmitter { } } - addKeyboard(keyboard: JSKeyboard) { + addKeyboard(keyboard: Keyboard) { const keyboardID = prefixed(keyboard.id); this.keyboardTable[keyboardID] = keyboard; this.emit('keyboardadded', keyboard); } - fetchKeyboardForStub(stub: KeyboardStub) : Promise { + fetchKeyboardForStub(stub: KeyboardStub) : Promise { return this.fetchKeyboard(stub.KI); } @@ -134,7 +134,7 @@ export default class StubAndKeyboardCache extends EventEmitter { return cachedEntry instanceof Promise; } - fetchKeyboard(keyboardID: string): Promise { + fetchKeyboard(keyboardID: string): Promise { if(!keyboardID) { throw new Error("Keyboard ID must be specified"); } @@ -166,7 +166,9 @@ export default class StubAndKeyboardCache extends EventEmitter { promise.then((kbd) => { // Overrides the built-in ID in case of keyboard namespacing. - kbd.scriptObject["KI"] = keyboardID; + if (kbd instanceof JSKeyboard) { + kbd.scriptObject["KI"] = keyboardID; + } this.addKeyboard(kbd); }).catch((err) => { delete this.keyboardTable[keyboardID]; diff --git a/web/src/engine/keyboard/build.sh b/web/src/engine/keyboard/build.sh index 6390243b9a3..b3e6ba81337 100755 --- a/web/src/engine/keyboard/build.sh +++ b/web/src/engine/keyboard/build.sh @@ -22,6 +22,7 @@ builder_describe \ "@/web/src/tools/testing/recorder-core test" \ "@/web/src/tools/es-bundling" \ "@/web/src/engine/common/web-utils" \ + "@/web/src/engine/core-processor" \ configure \ clean \ build \ diff --git a/web/src/engine/keyboard/src/index.ts b/web/src/engine/keyboard/src/index.ts index 7bd587d303e..8ba9aaa8899 100644 --- a/web/src/engine/keyboard/src/index.ts +++ b/web/src/engine/keyboard/src/index.ts @@ -1,8 +1,10 @@ export { ActiveKeyBase, ActiveKey, ActiveSubKey, ActiveRow, ActiveLayer, ActiveLayout } from "./keyboards/activeLayout.js"; export { ButtonClass, ButtonClasses, LayoutLayer, LayoutFormFactor, LayoutRow, LayoutKey, LayoutSubKey, Layouts } from "./keyboards/defaultLayouts.js"; export { JSKeyboard, LayoutState, VariableStoreDictionary } from "./keyboards/jsKeyboard.js"; +export { KeyboardMinimalInterface } from './keyboards/keyboardMinimalInterface.js'; +export { KMXKeyboard } from './keyboards/kmxKeyboard.js'; export { KeyboardHarness, KeyboardKeymanGlobal, MinimalCodesInterface, MinimalKeymanGlobal } from "./keyboards/keyboardHarness.js"; -export { KeyboardLoaderBase } from "./keyboards/keyboardLoaderBase.js"; +export { Keyboard, KeyboardLoaderBase } from "./keyboards/keyboardLoaderBase.js"; export { KeyboardLoadErrorBuilder, KeyboardMissingError, KeyboardScriptError, KeyboardDownloadError, InvalidKeyboardError } from './keyboards/keyboardLoadError.js' export { CloudKeyboardFont, diff --git a/web/src/engine/keyboard/src/keyboards/keyboardLoaderBase.ts b/web/src/engine/keyboard/src/keyboards/keyboardLoaderBase.ts index 660b19f0325..bafa59160f7 100644 --- a/web/src/engine/keyboard/src/keyboards/keyboardLoaderBase.ts +++ b/web/src/engine/keyboard/src/keyboards/keyboardLoaderBase.ts @@ -1,12 +1,16 @@ +import { MainModule as KmCoreModule, KM_CORE_STATUS } from 'keyman/engine/core-processor'; import { JSKeyboard } from "./jsKeyboard.js"; +import { KMXKeyboard } from './kmxKeyboard.js'; import { KeyboardHarness } from "./keyboardHarness.js"; import KeyboardProperties from "./keyboardProperties.js"; import { KeyboardLoadErrorBuilder, StubBasedErrorBuilder, UriBasedErrorBuilder } from './keyboardLoadError.js'; export type KeyboardStub = KeyboardProperties & { filename: string }; +export type Keyboard = JSKeyboard | KMXKeyboard; export abstract class KeyboardLoaderBase { private _harness: KeyboardHarness; + protected _km_core: KmCoreModule; public get harness(): KeyboardHarness { return this._harness; @@ -16,13 +20,17 @@ export abstract class KeyboardLoaderBase { this._harness = harness; } + public set coreModule(km_core: KmCoreModule) { + this._km_core = km_core; + } + /** * Load a keyboard from a remote or local URI. * * @param uri The URI of the keyboard to load. * @returns A Promise that resolves to the loaded keyboard. */ - public loadKeyboardFromPath(uri: string): Promise { + public loadKeyboardFromPath(uri: string): Promise { this.harness.install(); return this.loadKeyboardInternal(uri, new UriBasedErrorBuilder(uri)); } @@ -33,17 +41,22 @@ export abstract class KeyboardLoaderBase { * @param stub The stub of the keyboard to load. * @returns A Promise that resolves to the loaded keyboard. */ - public async loadKeyboardFromStub(stub: KeyboardStub): Promise { + public async loadKeyboardFromStub(stub: KeyboardStub): Promise { this.harness.install(); return this.loadKeyboardInternal(stub.filename, new StubBasedErrorBuilder(stub)); } - private async loadKeyboardInternal(uri: string, errorBuilder: KeyboardLoadErrorBuilder): Promise { + private async loadKeyboardInternal(uri: string, errorBuilder: KeyboardLoadErrorBuilder): Promise { const byteArray = await this.loadKeyboardBlob(uri, errorBuilder); if (byteArray.slice(0, 4) == Uint8Array.from([0x4b, 0x58, 0x54, 0x53])) { // 'KXTS' // KMX or LDML (KMX+) keyboard - console.error("KMX keyboard loading is not yet implemented!"); + const result = this._km_core.keyboard_load_from_blob(uri, byteArray); + if (result.status == KM_CORE_STATUS.OK) { + // extract keyboard name from URI + const id = uri.split('#')[0].split('?')[0].split('/').pop().split('.')[0]; + return new KMXKeyboard(id, result.object); + } return null; } diff --git a/web/src/engine/keyboard/src/keyboards/keyboardMinimalInterface.ts b/web/src/engine/keyboard/src/keyboards/keyboardMinimalInterface.ts new file mode 100644 index 00000000000..564a7b2acb9 --- /dev/null +++ b/web/src/engine/keyboard/src/keyboards/keyboardMinimalInterface.ts @@ -0,0 +1,5 @@ +import { Keyboard } from './keyboardLoaderBase.js'; + +export interface KeyboardMinimalInterface { + activeKeyboard: Keyboard; +} \ No newline at end of file diff --git a/web/src/engine/keyboard/src/keyboards/kmxKeyboard.ts b/web/src/engine/keyboard/src/keyboards/kmxKeyboard.ts new file mode 100644 index 00000000000..34ffd5f9f07 --- /dev/null +++ b/web/src/engine/keyboard/src/keyboards/kmxKeyboard.ts @@ -0,0 +1,24 @@ +import { km_core_keyboard } from 'keyman/engine/core-processor'; + +/** + * Acts as a wrapper class for KMX(+) Keyman keyboards + */ +export class KMXKeyboard { + + constructor(id: string, keyboard: km_core_keyboard) { + this.id = id; + this.keyboard = keyboard; + } + + id: string; + keyboard: km_core_keyboard; + + get isMnemonic(): boolean { + return false; + } + + get version(): string { + // TODO-web-core: get version from `km_core_keyboard_get_attrs` + return ''; + } +} diff --git a/web/src/engine/main/src/contextManagerBase.ts b/web/src/engine/main/src/contextManagerBase.ts index 9abc503c65c..98ce5e6d605 100644 --- a/web/src/engine/main/src/contextManagerBase.ts +++ b/web/src/engine/main/src/contextManagerBase.ts @@ -1,5 +1,5 @@ import { EventEmitter } from 'eventemitter3'; -import { ManagedPromise, type JSKeyboard } from 'keyman/engine/keyboard'; +import { ManagedPromise, type Keyboard } from 'keyman/engine/keyboard'; import { type JSKeyboardInterface, type OutputTarget } from 'keyman/engine/js-processor'; import { StubAndKeyboardCache, type KeyboardStub } from 'keyman/engine/keyboard-storage'; import { PredictionContext } from 'keyman/engine/interfaces'; @@ -37,7 +37,7 @@ interface EventMap { * @param kbd * @returns */ - 'keyboardchange': (kbd: {keyboard: JSKeyboard, metadata: KeyboardStub}) => void; + 'keyboardchange': (kbd: {keyboard: Keyboard, metadata: KeyboardStub}) => void; } export interface ContextManagerConfiguration { @@ -65,7 +65,7 @@ export interface ContextManagerConfiguration { interface PendingActivation { target: OutputTarget, - keyboard: Promise, + keyboard: Promise, stub: KeyboardStub; } @@ -124,7 +124,7 @@ export abstract class ContextManagerBase this.predictionContext.resetContext(); } - abstract get activeKeyboard(): {keyboard: JSKeyboard, metadata: KeyboardStub}; + abstract get activeKeyboard(): {keyboard: Keyboard, metadata: KeyboardStub}; /** * Determines the 'target' currently used to determine which keyboard should be active. @@ -143,7 +143,7 @@ export abstract class ContextManagerBase * @param kbd * @param target */ - protected abstract activateKeyboardForTarget(kbd: {keyboard: JSKeyboard, metadata: KeyboardStub}, target: OutputTarget): void; + protected abstract activateKeyboardForTarget(kbd: {keyboard: Keyboard, metadata: KeyboardStub}, target: OutputTarget): void; /** * Checks the pending keyboard-activation array for an entry corresponding to the specified @@ -178,7 +178,7 @@ export abstract class ContextManagerBase * @returns */ protected async deferredKeyboardActivation( - kbdPromise: Promise, + kbdPromise: Promise, metadata: KeyboardStub, target: OutputTarget ): Promise { @@ -263,7 +263,7 @@ export abstract class ContextManagerBase this.emit('beforekeyboardchange', activatingKeyboard.metadata); } - let kbdStubPair: { keyboard: JSKeyboard, metadata: KeyboardStub } = null; + let kbdStubPair: { keyboard: Keyboard, metadata: KeyboardStub } = null; if(keyboard) { kbdStubPair = { keyboard: keyboard, @@ -298,7 +298,7 @@ export abstract class ContextManagerBase protected prepareKeyboardForActivation( keyboardId: string, languageCode?: string - ): {keyboard: Promise, metadata: KeyboardStub} { + ): {keyboard: Promise, metadata: KeyboardStub} { // Set default language code languageCode ||= ''; @@ -333,7 +333,7 @@ export abstract class ContextManagerBase } // Determine if the keyboard was previously loaded but is not active; use the cached, pre-loaded version if so. - let keyboard: JSKeyboard; + let keyboard: Keyboard; if(keyboard = this.keyboardCache.getKeyboardForStub(requestedStub)) { return { keyboard: Promise.resolve(keyboard), @@ -351,7 +351,7 @@ export abstract class ContextManagerBase this.emit('keyboardasyncload', requestedStub, completionPromise.corePromise); let keyboardPromise = this.keyboardCache.fetchKeyboardForStub(requestedStub); - let timeoutPromise = new Promise((resolve, reject) => { + let timeoutPromise = new Promise((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); }); diff --git a/web/src/engine/main/src/hardKeyboard.ts b/web/src/engine/main/src/hardKeyboard.ts index 62cc91e63bb..09806caa21c 100644 --- a/web/src/engine/main/src/hardKeyboard.ts +++ b/web/src/engine/main/src/hardKeyboard.ts @@ -1,5 +1,5 @@ import { EventEmitter } from "eventemitter3"; -import { JSKeyboard, KeyMapping, KeyEvent, Codes } from "keyman/engine/keyboard"; +import { JSKeyboard, Keyboard, KeyMapping, KeyEvent, Codes } from "keyman/engine/keyboard"; import { type RuleBehavior } from 'keyman/engine/js-processor'; import { KeyEventSourceInterface } from 'keyman/engine/osk'; import { ModifierKeyConstants } from '@keymanapp/common-types'; @@ -13,47 +13,55 @@ interface EventMap { export default class HardKeyboard extends EventEmitter implements KeyEventSourceInterface { } -export function processForMnemonicsAndLegacy(s: KeyEvent, activeKeyboard: JSKeyboard, baseLayout: string): KeyEvent { - // Mnemonic handling. - if(activeKeyboard && activeKeyboard.isMnemonic) { - // The following will never set a code corresponding to a modifier key, so it's fine to do this, - // which may change the value of Lcode, here. - - s.setMnemonicCode(!!(s.Lmodifiers & ModifierKeyConstants.K_SHIFTFLAG), !!(s.Lmodifiers & ModifierKeyConstants.CAPITALFLAG)); +export function processForMnemonicsAndLegacy(s: KeyEvent, activeKeyboard: Keyboard, baseLayout: string): KeyEvent { + if (!activeKeyboard) { + return s; } - // Other minor physical-keyboard adjustments - if(activeKeyboard && !activeKeyboard.isMnemonic) { - // Positional Layout + if (activeKeyboard instanceof JSKeyboard) { + // Mnemonic handling. + if (activeKeyboard.isMnemonic) { + // The following will never set a code corresponding to a modifier key, so it's fine to do this, + // which may change the value of Lcode, here. - /* 13/03/2007 MCD: Swedish: Start mapping of keystroke to US keyboard */ - var Lbase = KeyMapping.languageMap[baseLayout]; - if(Lbase && Lbase['k'+s.Lcode]) { - s.Lcode=Lbase['k'+s.Lcode]; + s.setMnemonicCode(!!(s.Lmodifiers & ModifierKeyConstants.K_SHIFTFLAG), !!(s.Lmodifiers & ModifierKeyConstants.CAPITALFLAG)); } - /* 13/03/2007 MCD: Swedish: End mapping of keystroke to US keyboard */ + // Other minor physical-keyboard adjustments + if (!activeKeyboard.isMnemonic) { + // Positional Layout + + /* 13/03/2007 MCD: Swedish: Start mapping of keystroke to US keyboard */ + const Lbase = KeyMapping.languageMap[baseLayout]; + if (Lbase && Lbase['k' + s.Lcode]) { + s.Lcode = Lbase['k' + s.Lcode]; + } + /* 13/03/2007 MCD: Swedish: End mapping of keystroke to US keyboard */ - // The second conditional component (re 0x60): if CTRL or ALT is held down... - // Do not remap for legacy keyboard compatibility, do not pass Go, do not collect $200. - // This effectively only permits `default` and `shift` for legacy keyboards. - // - // Third: DO, however, track direct presses of any main modifier key. The OSK should - // reflect the current modifier state even for legacy keyboards. - if(!activeKeyboard.definesPositionalOrMnemonic && - !(s.Lmodifiers & Codes.modifierBitmasks.NON_LEGACY) && - !s.isModifier) { - // Support version 1.0 KeymanWeb keyboards that do not define positional vs mnemonic - s = new KeyEvent({ - Lcode: KeyMapping._USKeyCodeToCharCode(s), - Lmodifiers: 0, - LisVirtualKey: false, - vkCode: s.Lcode, // Helps to merge OSK and physical keystroke control paths. - Lstates: s.Lstates, - kName: '', - device: s.device, - isSynthetic: false - }); + // The second conditional component (re 0x60): if CTRL or ALT is held down... + // Do not remap for legacy keyboard compatibility, do not pass Go, do not collect $200. + // This effectively only permits `default` and `shift` for legacy keyboards. + // + // Third: DO, however, track direct presses of any main modifier key. The OSK should + // reflect the current modifier state even for legacy keyboards. + if (!activeKeyboard.definesPositionalOrMnemonic && + !(s.Lmodifiers & Codes.modifierBitmasks.NON_LEGACY) && + !s.isModifier) { + // Support version 1.0 KeymanWeb keyboards that do not define positional vs mnemonic + s = new KeyEvent({ + Lcode: KeyMapping._USKeyCodeToCharCode(s), + Lmodifiers: 0, + LisVirtualKey: false, + vkCode: s.Lcode, // Helps to merge OSK and physical keystroke control paths. + Lstates: s.Lstates, + kName: '', + device: s.device, + isSynthetic: false + }); + } } + } else { + // KMX keyboard + // TODO-web-core: forward to Core } return s; diff --git a/web/src/engine/main/src/headless/inputProcessor.ts b/web/src/engine/main/src/headless/inputProcessor.ts index 750f17c6700..560d6d4c326 100644 --- a/web/src/engine/main/src/headless/inputProcessor.ts +++ b/web/src/engine/main/src/headless/inputProcessor.ts @@ -7,11 +7,10 @@ import { globalObject, DeviceSpec } from "@keymanapp/web-utils"; import { CoreFactory, MainModule as KmCoreModule } from 'keyman/engine/core-processor'; -import { Codes, type JSKeyboard, type KeyEvent } from "keyman/engine/keyboard"; +import { Codes, JSKeyboard, KeyboardMinimalInterface, type Keyboard, type KeyEvent } from "keyman/engine/keyboard"; import { type Alternate, isEmptyTransform, - JSKeyboardInterface, JSKeyboardProcessor, Mock, type OutputTarget, @@ -66,7 +65,7 @@ export class InputProcessor { return this.kbdProcessor; } - public get keyboardInterface(): JSKeyboardInterface { + public get keyboardInterface(): KeyboardMinimalInterface { return this.keyboardProcessor.keyboardInterface; } @@ -74,11 +73,11 @@ export class InputProcessor { return this.km_core; } - public get activeKeyboard(): JSKeyboard { + public get activeKeyboard(): Keyboard { return this.keyboardInterface.activeKeyboard; } - public set activeKeyboard(keyboard: JSKeyboard) { + public set activeKeyboard(keyboard: Keyboard) { this.keyboardInterface.activeKeyboard = keyboard; // All old deadkeys and keyboard-specific cache should immediately be invalidated @@ -156,7 +155,7 @@ export class InputProcessor { // The default OSK layout for desktop devices does not include nextlayer info, relying on modifier detection here. // It's the OSK equivalent to doModifierPress on 'desktop' form factors. - if((formFactor == DeviceSpec.FormFactor.Desktop || !this.activeKeyboard || this.activeKeyboard.usesDesktopLayoutOnDevice(keyEvent.device)) && fromOSK) { + if((formFactor == DeviceSpec.FormFactor.Desktop || !this.activeKeyboard || (this.activeKeyboard instanceof JSKeyboard && this.activeKeyboard.usesDesktopLayoutOnDevice(keyEvent.device))) && fromOSK) { // If it's a desktop OSK style and this triggers a layer change, // a modifier key was clicked. No output expected, so it's safe to instantly exit. if(this.keyboardProcessor.selectLayer(keyEvent)) { diff --git a/web/src/engine/main/src/keymanEngine.ts b/web/src/engine/main/src/keymanEngine.ts index e04145a361d..5ebc98fa822 100644 --- a/web/src/engine/main/src/keymanEngine.ts +++ b/web/src/engine/main/src/keymanEngine.ts @@ -1,8 +1,8 @@ -import { type KeyEvent, type JSKeyboard, KeyboardKeymanGlobal } from "keyman/engine/keyboard"; +import { type KeyEvent, JSKeyboard, Keyboard, KeyboardProperties, KeyboardKeymanGlobal } from "keyman/engine/keyboard"; import { OutputTarget, ProcessorInitOptions, RuleBehavior } from 'keyman/engine/js-processor'; import { DOMKeyboardLoader as KeyboardLoader } from "keyman/engine/keyboard/dom-keyboard-loader"; import { InputProcessor } from './headless/inputProcessor.js'; -import { OSKView } from "keyman/engine/osk"; +import { OSKView, JSKeyboardData } from "keyman/engine/osk"; import { KeyboardRequisitioner, ModelCache, toUnprefixedKeyboardId as unprefixed } from "keyman/engine/keyboard-storage"; import { ModelSpec, PredictionContext } from "keyman/engine/interfaces"; @@ -146,11 +146,11 @@ export default class KeymanEngine< }); }); - this.contextManager.on('keyboardchange', (kbd) => { + this.contextManager.on('keyboardchange', (kbdData: { keyboard: Keyboard, metadata: KeyboardProperties }) => { // 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) { + if(!kbdData) { this.osk.startHide(false); } @@ -158,11 +158,11 @@ export default class KeymanEngine< this.refreshModel(); // Triggers context resets that can trigger layout stuff. // It's not the final such context-reset, though. - this.core.activeKeyboard = kbd?.keyboard; + this.core.activeKeyboard = kbdData?.keyboard; this.legacyAPIEvents.callEvent('keyboardchange', { - internalName: kbd?.metadata.id ?? '', - languageCode: kbd?.metadata.langId ?? '' + internalName: kbdData?.metadata.id ?? '', + languageCode: kbdData?.metadata.langId ?? '' }); } @@ -174,10 +174,10 @@ export default class KeymanEngine< If possible, we want to only perform layout operations once the correct layer is set to active. */ - if(this.osk) { + if(this.osk && (!kbdData || kbdData.keyboard instanceof JSKeyboard)) { // TODO-web-core: add support for OSK for KMX keyboards this.osk.batchLayoutAfter(() => { prepareKeyboardSwap(); - this.osk.activeKeyboard = kbd; + this.osk.activeKeyboard = kbdData ? { keyboard: kbdData.keyboard as JSKeyboard, metadata: kbdData.metadata } : undefined; // Note: when embedded within the mobile apps, the keyboard will still be visible // at this time. @@ -243,6 +243,7 @@ export default class KeymanEngine< // Since we're not sandboxing keyboard loads yet, we just use `window` as the jsGlobal object. // All components initialized below require a properly-configured `config.paths` or similar. const keyboardLoader = new KeyboardLoader(this.interface, config.applyCacheBusting); + keyboardLoader.coreModule = this.core.keymanCore; this.keyboardRequisitioner = new KeyboardRequisitioner(keyboardLoader, new DOMCloudRequester(), this.config.paths); this.modelCache = new ModelCache(); const kbdCache = this.keyboardRequisitioner.cache; @@ -382,7 +383,7 @@ export default class KeymanEngine< this.core.keyboardProcessor.contextDevice = value?.targetDevice ?? this.config.softDevice; if(value) { // Don't build an OSK if no keyboard is available yet; avoid the extra flash. - if(this.contextManager.activeKeyboard) { + if (this.contextManager.activeKeyboard && this.contextManager.activeKeyboard instanceof JSKeyboardData) { // TODO-web-core: add support for OSK for KMX keyboards value.activeKeyboard = this.contextManager.activeKeyboard; } value.on('keyevent', this.keyEventListener); @@ -555,22 +556,29 @@ export default class KeymanEngine< * See https://help.keyman.com/developer/engine/web/current-version/reference/core/isChiral */ public isChiral(k0?: string | JSKeyboard) { - let kbd: JSKeyboard; + let jsKbd: JSKeyboard; if(k0) { if(typeof k0 == 'string') { const kbdObj = this.keyboardRequisitioner.cache.getKeyboard(k0); - if(!kbdObj) { + if (!kbdObj) { throw new Error(`Keyboard '${k0}' has not been loaded.`); + } else if (!(kbdObj instanceof JSKeyboard)) { + return false; // TODO-web-core: implement for KMX keyboards } else { k0 = kbdObj; } } - kbd = k0; + jsKbd = k0; } else { - kbd = this.core.activeKeyboard; + const kbd = this.core.activeKeyboard; + if (kbd instanceof JSKeyboard) { + jsKbd = kbd; + } else { + return false; // TODO-web-core: implement for KMX keyboards + } } - return kbd.isChiral; + return jsKbd.isChiral; } /** diff --git a/web/src/engine/osk/src/index.ts b/web/src/engine/osk/src/index.ts index 121b8f43375..4140e1537dd 100644 --- a/web/src/engine/osk/src/index.ts +++ b/web/src/engine/osk/src/index.ts @@ -1,6 +1,6 @@ export { Codes, DeviceSpec, JSKeyboard, KeyboardProperties, SpacebarText } from 'keyman/engine/keyboard'; -export { default as OSKView } from './views/oskView.js'; +export { default as OSKView, JSKeyboardData } from './views/oskView.js'; export { default as FloatingOSKView, FloatingOSKViewConfiguration } from './views/floatingOskView.js'; export { default as AnchoredOSKView } from './views/anchoredOskView.js'; export { default as InlinedOSKView } from './views/inlinedOskView.js'; diff --git a/web/src/engine/osk/src/views/oskView.ts b/web/src/engine/osk/src/views/oskView.ts index 480ed5f99c2..43123fef2c9 100644 --- a/web/src/engine/osk/src/views/oskView.ts +++ b/web/src/engine/osk/src/views/oskView.ts @@ -59,6 +59,11 @@ export interface LegacyOSKEventMap { }): void; } +export class JSKeyboardData { + keyboard: JSKeyboard; + metadata: KeyboardProperties; +}; + /** * For now, these will serve as undocumented, internal events. We need a proper * design round and discussion before we consider promoting them to long-term, @@ -168,10 +173,7 @@ export default abstract class OSKView private _boxBaseTouchStart: (e: TouchEvent) => boolean; private _boxBaseTouchEventCancel: (e: TouchEvent) => boolean; - private keyboardData: { - keyboard: JSKeyboard, - metadata: KeyboardProperties - }; + private keyboardData: JSKeyboardData; /** * Provides the current parameterization for timings and distances used by @@ -532,17 +534,11 @@ export default abstract class OSKView } } - public get activeKeyboard(): { - keyboard: JSKeyboard, - metadata: KeyboardProperties - } { + public get activeKeyboard(): JSKeyboardData { return this.keyboardData; } - public set activeKeyboard(keyboardData: { - keyboard: JSKeyboard, - metadata: KeyboardProperties - }) { + public set activeKeyboard(keyboardData: JSKeyboardData) { this.keyboardData = keyboardData; this.loadActiveKeyboard(); diff --git a/web/src/test/auto/dom/cases/keyboard/domKeyboardLoader.tests.ts b/web/src/test/auto/dom/cases/keyboard/domKeyboardLoader.tests.ts index f23f87f31e2..55cd21037d0 100644 --- a/web/src/test/auto/dom/cases/keyboard/domKeyboardLoader.tests.ts +++ b/web/src/test/auto/dom/cases/keyboard/domKeyboardLoader.tests.ts @@ -1,7 +1,7 @@ import { assert } from 'chai'; import { DOMKeyboardLoader } from 'keyman/engine/keyboard/dom-keyboard-loader'; -import { extendString, KeyboardHarness, JSKeyboard, MinimalKeymanGlobal, DeviceSpec, KeyboardKeymanGlobal, KeyboardDownloadError, KeyboardScriptError } from 'keyman/engine/keyboard'; +import { extendString, KeyboardHarness, JSKeyboard, MinimalKeymanGlobal, DeviceSpec, KeyboardKeymanGlobal, KeyboardDownloadError, KeyboardScriptError, Keyboard } from 'keyman/engine/keyboard'; import { JSKeyboardInterface, Mock } from 'keyman/engine/js-processor'; import { assertThrowsAsync } from 'keyman/tools/testing/test-utils'; @@ -49,12 +49,14 @@ describe('Keyboard loading in DOM', function() { it('`window`, disabled rule processing', async () => { const harness = new KeyboardHarness(window, MinimalKeymanGlobal); let keyboardLoader = new DOMKeyboardLoader(harness); - let keyboard: JSKeyboard = await keyboardLoader.loadKeyboardFromPath('/common/test/resources/keyboards/khmer_angkor.js'); + let keyboard: Keyboard = await keyboardLoader.loadKeyboardFromPath('/common/test/resources/keyboards/khmer_angkor.js'); assert.isOk(keyboard); - assert.equal(keyboard.id, 'Keyboard_khmer_angkor'); - assert.isTrue(keyboard.isChiral); - assert.isFalse(keyboard.isCJK); + assert.instanceOf(keyboard, JSKeyboard); + const jsKeyboard = keyboard as JSKeyboard; + assert.equal(jsKeyboard.id, 'Keyboard_khmer_angkor'); + assert.isTrue(jsKeyboard.isChiral); + assert.isFalse(jsKeyboard.isCJK); assert.isOk(KeymanWeb); assert.isOk(keyman); assert.isOk(keyman.osk); @@ -65,24 +67,26 @@ describe('Keyboard loading in DOM', function() { }); it('`window`, enabled rule processing', async () => { - const harness = new JSKeyboardInterface(window, MinimalKeymanGlobal); - const keyboardLoader = new DOMKeyboardLoader(harness); - const keyboard: JSKeyboard = await keyboardLoader.loadKeyboardFromPath('/common/test/resources/keyboards/khmer_angkor.js'); - harness.activeKeyboard = keyboard; + const jsHarness = new JSKeyboardInterface(window, MinimalKeymanGlobal); + const keyboardLoader = new DOMKeyboardLoader(jsHarness); + const keyboard: Keyboard = await keyboardLoader.loadKeyboardFromPath('/common/test/resources/keyboards/khmer_angkor.js'); + const jsKeyboard = keyboard as JSKeyboard; + jsHarness.activeKeyboard = jsKeyboard; assert.isOk(keyboard); - assert.equal(keyboard.id, 'Keyboard_khmer_angkor'); - assert.isTrue(keyboard.isChiral); - assert.isFalse(keyboard.isCJK); + assert.instanceOf(keyboard, JSKeyboard); + assert.equal(jsKeyboard.id, 'Keyboard_khmer_angkor'); + assert.isTrue(jsKeyboard.isChiral); + assert.isFalse(jsKeyboard.isCJK); assert.isOk(KeymanWeb); assert.isOk(keyman); assert.isOk(keyman.osk); assert.isOk(keyman.osk.keyCodes); // TODO: verify actual rule processing. - const nullKeyEvent = keyboard.constructNullKeyEvent(device); + const nullKeyEvent = jsKeyboard.constructNullKeyEvent(device); const mock = new Mock(); - const result = harness.processKeystroke(mock, nullKeyEvent); + const result = jsHarness.processKeystroke(mock, nullKeyEvent); assert.isOk(result); assert.isOk(KeymanWeb); @@ -91,17 +95,19 @@ describe('Keyboard loading in DOM', function() { assert.isOk(keyman.osk.keyCodes); // Should be cleared post-keyboard-load. - assert.isNotOk(harness.loadedKeyboard); + assert.isNotOk(jsHarness.loadedKeyboard); }); it('load keyboards successfully in parallel without side effects', async () => { - let harness = new JSKeyboardInterface(window, MinimalKeymanGlobal); - let keyboardLoader = new DOMKeyboardLoader(harness); + let jsHarness = new JSKeyboardInterface(window, MinimalKeymanGlobal); + let keyboardLoader = new DOMKeyboardLoader(jsHarness); // Preload a keyboard and make it active. - const test_kbd: JSKeyboard = await keyboardLoader.loadKeyboardFromPath('/common/test/resources/keyboards/test_917.js'); - harness.activeKeyboard = test_kbd; - assert.isNotOk(harness.loadedKeyboard); + const test_kbd: Keyboard = await keyboardLoader.loadKeyboardFromPath('/common/test/resources/keyboards/test_917.js'); + assert.instanceOf(test_kbd, JSKeyboard); + const test_jskbd = test_kbd as JSKeyboard; + jsHarness.activeKeyboard = test_jskbd; + assert.isNotOk(jsHarness.loadedKeyboard); // With an active keyboard, load three keyboards but activate none of them. const lao_keyboard_promise = keyboardLoader.loadKeyboardFromPath('/common/test/resources/keyboards/lao_2008_basic.js'); @@ -113,8 +119,8 @@ describe('Keyboard loading in DOM', function() { const lao_keyboard = await lao_keyboard_promise; const khmer_keyboard = await khmer_keyboard_promise; - assert.strictEqual(test_kbd, harness.activeKeyboard); - assert.isNotOk(harness.loadedKeyboard); + assert.strictEqual(test_kbd, jsHarness.activeKeyboard); + assert.isNotOk(jsHarness.loadedKeyboard); assert.isOk(lao_keyboard); assert.isOk(chiral_keyboard); @@ -125,6 +131,6 @@ describe('Keyboard loading in DOM', function() { assert.equal(khmer_keyboard.id, "Keyboard_khmer_angkor"); assert.equal(chiral_keyboard.id, "Keyboard_test_chirality"); - harness.activeKeyboard = lao_keyboard; + jsHarness.activeKeyboard = lao_keyboard as JSKeyboard; }); }); diff --git a/web/src/test/auto/dom/kbdLoader.ts b/web/src/test/auto/dom/kbdLoader.ts index 92968460f29..a9a01dfe2c8 100644 --- a/web/src/test/auto/dom/kbdLoader.ts +++ b/web/src/test/auto/dom/kbdLoader.ts @@ -37,7 +37,12 @@ export function loadKeyboardsFromStubs(apiStubs: any, baseDir: string) { const overwriteLoader = (id: number, path: string) => { const loader = loadKeyboardFromPath(path); return loader.then(kbd => { - keyboards[stub.id].keyboard = kbd; + if (kbd instanceof JSKeyboard) { + keyboards[stub.id].keyboard = kbd; + } else { + // TODO-web-core: implement for KMX keyboards if needed + keyboards[stub.id].keyboard = null; + } }); } keyboards[stub.id] = { diff --git a/web/src/test/auto/headless/engine/js-processor/kbdInterface.tests.ts b/web/src/test/auto/headless/engine/js-processor/kbdInterface.tests.ts index e015a9ee784..b8a68dd55c2 100644 --- a/web/src/test/auto/headless/engine/js-processor/kbdInterface.tests.ts +++ b/web/src/test/auto/headless/engine/js-processor/kbdInterface.tests.ts @@ -3,7 +3,7 @@ import { assert } from 'chai'; import { createRequire } from 'module'; const require = createRequire(import.meta.url); -import { DeviceSpec, MinimalKeymanGlobal } from 'keyman/engine/keyboard'; +import { DeviceSpec, JSKeyboard, MinimalKeymanGlobal } from 'keyman/engine/keyboard'; import { JSKeyboardInterface, Mock } from 'keyman/engine/js-processor'; import { NodeKeyboardLoader } from 'keyman/engine/keyboard/node-keyboard-loader'; @@ -24,10 +24,10 @@ describe('Headless keyboard loading', function () { describe('Full harness loading', () => { it('successfully loads', async function () { // -- START: Standard Recorder-based unit test loading boilerplate -- - const harness = new JSKeyboardInterface({}, MinimalKeymanGlobal); - const keyboardLoader = new NodeKeyboardLoader(harness); + const jsHarness = new JSKeyboardInterface({}, MinimalKeymanGlobal); + const keyboardLoader = new NodeKeyboardLoader(jsHarness); const keyboard = await keyboardLoader.loadKeyboardFromPath(laoPath); - harness.activeKeyboard = keyboard; + jsHarness.activeKeyboard = keyboard as JSKeyboard; // -- END: Standard Recorder-based unit test loading boilerplate -- // This part provides assurance that the keyboard properly loaded. @@ -39,11 +39,11 @@ describe('Headless keyboard loading', function () { const harness = new JSKeyboardInterface({}, MinimalKeymanGlobal); const keyboardLoader = new NodeKeyboardLoader(harness); const keyboard = await keyboardLoader.loadKeyboardFromPath(laoPath); - harness.activeKeyboard = keyboard; + harness.activeKeyboard = keyboard as JSKeyboard; // -- END: Standard Recorder-based unit test loading boilerplate -- // Runs a blank KeyEvent through the keyboard's rule processing. - harness.processKeystroke(new Mock(), keyboard.constructNullKeyEvent(device)); + harness.processKeystroke(new Mock(), (keyboard as JSKeyboard).constructNullKeyEvent(device)); }); it('does not change the active kehboard', async function () { @@ -56,7 +56,7 @@ describe('Headless keyboard loading', function () { // This part provides assurance that the keyboard properly loaded. assert.equal(lao_keyboard.id, "Keyboard_lao_2008_basic"); - harness.activeKeyboard = lao_keyboard; + harness.activeKeyboard = lao_keyboard as JSKeyboard; const khmer_keyboard = await keyboardLoader.loadKeyboardFromPath(khmerPath); assert.strictEqual(lao_keyboard, harness.activeKeyboard); diff --git a/web/src/test/auto/headless/engine/keyboard/keyboard.tests.ts b/web/src/test/auto/headless/engine/keyboard/keyboard.tests.ts index e089b64fe7b..3ebeaaa58f4 100644 --- a/web/src/test/auto/headless/engine/keyboard/keyboard.tests.ts +++ b/web/src/test/auto/headless/engine/keyboard/keyboard.tests.ts @@ -3,7 +3,7 @@ import { assert } from 'chai'; import { createRequire } from 'module'; const require = createRequire(import.meta.url); -import { KeyboardHarness, MinimalKeymanGlobal, DeviceSpec } from 'keyman/engine/keyboard'; +import { KeyboardHarness, MinimalKeymanGlobal, DeviceSpec, JSKeyboard } from 'keyman/engine/keyboard'; import { NodeKeyboardLoader } from 'keyman/engine/keyboard/node-keyboard-loader'; @@ -14,7 +14,8 @@ describe('Keyboard tests', function () { // -- START: Standard Recorder-based unit test loading boilerplate -- const harness = new KeyboardHarness({}, MinimalKeymanGlobal); const keyboardLoader = new NodeKeyboardLoader(harness); - const km_keyboard = await keyboardLoader.loadKeyboardFromPath(khmerPath); + const keyboard = await keyboardLoader.loadKeyboardFromPath(khmerPath); + const km_keyboard = keyboard as JSKeyboard; // -- END: Standard Recorder-based unit test loading boilerplate -- // `khmer_angkor` - supports longpresses, but not flicks or multitaps. diff --git a/web/src/test/auto/headless/engine/keyboard/keyboardLoaderBase.tests.ts b/web/src/test/auto/headless/engine/keyboard/keyboardLoaderBase.tests.ts index 01b66544ec4..76d5fdc1b1c 100644 --- a/web/src/test/auto/headless/engine/keyboard/keyboardLoaderBase.tests.ts +++ b/web/src/test/auto/headless/engine/keyboard/keyboardLoaderBase.tests.ts @@ -3,7 +3,7 @@ import { assert } from 'chai'; import { createRequire } from 'module'; const require = createRequire(import.meta.url); -import { KeyboardHarness, MinimalKeymanGlobal, KeyboardDownloadError, DeviceSpec, InvalidKeyboardError } from 'keyman/engine/keyboard'; +import { KeyboardHarness, MinimalKeymanGlobal, KeyboardDownloadError, DeviceSpec, InvalidKeyboardError, JSKeyboard } from 'keyman/engine/keyboard'; import { JSKeyboardInterface, Mock } from 'keyman/engine/js-processor'; import { NodeKeyboardLoader } from 'keyman/engine/keyboard/node-keyboard-loader'; import { assertThrowsAsync, assertThrows } from 'keyman/tools/testing/test-utils'; @@ -76,14 +76,15 @@ describe('Headless keyboard loading', function() { const keyboard = await keyboardLoader.loadKeyboardFromPath(laoPath); // -- END: Standard Recorder-based unit test loading boilerplate -- + assert.instanceOf(keyboard, JSKeyboard); // Runs a blank KeyEvent through the keyboard's rule processing... // but via separate harness configured with a different captured global. // This shows an important detail: the 'global' object is effectively // closure-captured. (Similar constraints may occur when experimenting with // 'sandboxed' keyboard loading in the DOM!) const ruleHarness = new JSKeyboardInterface({}, MinimalKeymanGlobal); - ruleHarness.activeKeyboard = keyboard; - assertThrows(() => ruleHarness.processKeystroke(new Mock(), keyboard.constructNullKeyEvent(device)), 'k.KKM is not a function'); + ruleHarness.activeKeyboard = keyboard as JSKeyboard; + assertThrows(() => ruleHarness.processKeystroke(new Mock(), (keyboard as JSKeyboard).constructNullKeyEvent(device)), 'k.KKM is not a function'); }); }); }); diff --git a/web/src/test/auto/integrated/web-test-runner.config.mjs b/web/src/test/auto/integrated/web-test-runner.config.mjs index 5aa4c749220..50ffd75da5a 100644 --- a/web/src/test/auto/integrated/web-test-runner.config.mjs +++ b/web/src/test/auto/integrated/web-test-runner.config.mjs @@ -24,7 +24,7 @@ export default { concurrency: 10, nodeResolve: true, files: [ - 'web/build/test/integrated//**/*.tests.mjs', + 'web/build/test/integrated/**/*.tests.mjs', // '**/*.tests.html' ], middleware: [ diff --git a/web/src/tools/testing/bulk_rendering/renderer_core.ts b/web/src/tools/testing/bulk_rendering/renderer_core.ts index 50c5ca24948..5bdd1527d1d 100644 --- a/web/src/tools/testing/bulk_rendering/renderer_core.ts +++ b/web/src/tools/testing/bulk_rendering/renderer_core.ts @@ -3,7 +3,7 @@ import { DeviceDetector } from 'keyman/engine/main'; import { type DeviceSpec } from '@keymanapp/web-utils'; import type { KeymanEngine } from 'keyman/app/browser'; -import type { FloatingOSKView } from 'keyman/engine/osk'; +import { JSKeyboard, type FloatingOSKView } from 'keyman/engine/osk'; declare var keyman: KeymanEngine; @@ -114,19 +114,23 @@ export class BatchRenderer { eleDescription.appendChild(document.createElement('br')); const keyboard = keyman.core.activeKeyboard; - // Some keyboards, such as sil_euro_latin and sil_ipa, no longer specify this property. - if(keyboard['_legacyLayoutSpec']) { - eleDescription.appendChild(document.createTextNode('Font: ' + keyboard['_legacyLayoutSpec'].F)); - } else { - // They instead specify only the post-KMW-10 touch-layout format. - const layout = keyboard.layout(formFactor); - if(!layout) { - eleDescription.appendChild(document.createTextNode('Keyboard is help-text based')); + if (keyboard instanceof JSKeyboard) { + // Some keyboards, such as sil_euro_latin and sil_ipa, no longer specify this property. + if (keyboard['_legacyLayoutSpec']) { + eleDescription.appendChild(document.createTextNode('Font: ' + keyboard['_legacyLayoutSpec'].F)); } else { - eleDescription.appendChild(document.createTextNode('Font: ' + layout.font)); + // They instead specify only the post-KMW-10 touch-layout format. + const layout = keyboard.layout(formFactor); + if (!layout) { + eleDescription.appendChild(document.createTextNode('Keyboard is help-text based')); + } else { + eleDescription.appendChild(document.createTextNode('Font: ' + layout.font)); + } } + } else { + // TODO-web-core: implement for KMX keyboards + eleDescription.appendChild(document.createTextNode('Keyboard is help-text based')); } - } else { eleDescription.appendChild(document.createTextNode('Unable to load this keyboard!')); } @@ -168,7 +172,7 @@ export class BatchRenderer { // Uses 'private' APIs that may be subject to change in the future. Keep it updated! let layers: string[]; - if(!isMobile) { + if(!isMobile && keyman.core.activeKeyboard instanceof JSKeyboard) { // The desktop OSK will be overpopulated, with a number of blank layers to display in most cases. // We instead rely upon the KLS definition to ensure we keep the renders sparse. // diff --git a/web/src/tools/testing/recorder/browserDriver.ts b/web/src/tools/testing/recorder/browserDriver.ts index 9cab03e5a54..f3aaded3009 100644 --- a/web/src/tools/testing/recorder/browserDriver.ts +++ b/web/src/tools/testing/recorder/browserDriver.ts @@ -8,7 +8,7 @@ import { ManagedPromise, timedPromise } from "@keymanapp/web-utils"; import { type KeymanEngine } from 'keyman/app/browser'; -declare var keyman: KeymanEngine; +declare let keyman: KeymanEngine; export type Mutable = { -readonly [Property in keyof Type]: Type[Property];