Skip to content

Commit

Permalink
feat(web): add Keyboard and KMXKeyboard classes
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ermshiperete committed Dec 11, 2024
1 parent 27e7418 commit f8f844f
Show file tree
Hide file tree
Showing 26 changed files with 264 additions and 167 deletions.
13 changes: 8 additions & 5 deletions web/src/app/browser/src/beepHandler.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -17,9 +18,9 @@ class BeepData {
}

export class BeepHandler {
readonly keyboardInterface: JSKeyboardInterface;
readonly keyboardInterface: KeyboardMinimalInterface;

constructor(keyboardInterface: JSKeyboardInterface) {
constructor(keyboardInterface: KeyboardMinimalInterface) {
this.keyboardInterface = keyboardInterface;
}

Expand Down Expand Up @@ -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<this._BeepObjects.length;Lbo++) { // I1511 - array prototype extended
for(let Lbo=0;Lbo<this._BeepObjects.length;Lbo++) { // I1511 - array prototype extended
this._BeepObjects[Lbo].reset();
}
this._BeepObjects = [];
Expand Down
10 changes: 6 additions & 4 deletions web/src/app/browser/src/contextManager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type JSKeyboard, KeyboardScriptError } from 'keyman/engine/keyboard';
import { JSKeyboard, type Keyboard, KeyboardScriptError } from 'keyman/engine/keyboard';
import { type KeyboardStub } from 'keyman/engine/keyboard-storage';
import { CookieSerializer } from 'keyman/engine/dom-utils';
import { eventOutputTarget, outputTargetForElement, PageContextAttachment } from 'keyman/engine/attachment';
Expand All @@ -23,10 +23,12 @@ export interface KeyboardCookie {
* has the same directionality, text runs will be re-ordered which is confusing and causes
* incorrect caret positioning
*
* @param {Object} Ptarg Target element
* @param {Object} Ptarg Target element
* @param {Keyboard} activeKeyboard The active keyboard
*/
function _SetTargDir(Ptarg: HTMLElement, activeKeyboard: JSKeyboard) {
const elDir = activeKeyboard?.isRTL ? 'rtl' : 'ltr';
function _SetTargDir(Ptarg: HTMLElement, activeKeyboard: Keyboard) {
// TODO-web-core: do we need to support RTL in Core?
const elDir = activeKeyboard instanceof JSKeyboard && activeKeyboard?.isRTL ? 'rtl' : 'ltr';

if(Ptarg) {
if(Ptarg instanceof Ptarg.ownerDocument.defaultView.HTMLInputElement
Expand Down
28 changes: 21 additions & 7 deletions web/src/app/browser/src/keymanEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
VisualKeyboard
} from 'keyman/engine/osk';
import { ErrorStub, KeyboardStub, CloudQueryResult, toPrefixedKeyboardId as prefixed } from 'keyman/engine/keyboard-storage';
import { DeviceSpec, JSKeyboard } from "keyman/engine/keyboard";
import { DeviceSpec, JSKeyboard, Keyboard } from "keyman/engine/keyboard";
import KeyboardObject = KeymanWebKeyboard.KeyboardObject;

import * as views from './viewsAnchorpoint.js';
Expand Down Expand Up @@ -425,7 +425,7 @@ export default class KeymanEngine extends KeymanEngineBase<BrowserConfiguration,
* See https://help.keyman.com/developer/engine/web/current-version/reference/core/isCJK
*/
public isCJK(k0?: KeyboardObject | ReturnType<KeymanEngine['_GetKeyboardDetail']> /* [b/c Toolbar UI]*/) {
let kbd: JSKeyboard;
let kbd: Keyboard;
if(k0) {
let kbdDetail = k0 as ReturnType<KeymanEngine['_GetKeyboardDetail']>;
if(kbdDetail.KeyboardID){
Expand All @@ -437,7 +437,8 @@ export default class KeymanEngine extends KeymanEngineBase<BrowserConfiguration,
kbd = this.core.activeKeyboard;
}

return kbd && kbd.isCJK;
// TODO-web-core: implement for KMX keyboards if needed
return kbd && kbd instanceof JSKeyboard && kbd.isCJK;
}

/**
Expand All @@ -453,7 +454,12 @@ export default class KeymanEngine extends KeymanEngineBase<BrowserConfiguration,
const stub = this.keyboardRequisitioner.cache.getStub(PInternalName, PlgCode);
const keyboard = this.keyboardRequisitioner.cache.getKeyboardForStub(stub);

return stub && this._GetKeyboardDetail(stub, keyboard);
if (keyboard instanceof JSKeyboard) {
return stub && this._GetKeyboardDetail(stub, keyboard);
} else {
// TODO-web-core: implement for KMX keyboards if needed
return null;
}
}

/**
Expand All @@ -478,8 +484,12 @@ export default class KeymanEngine extends KeymanEngineBase<BrowserConfiguration,
// In Chrome, (including on Android), Array.prototype.find() requires Chrome 45.
// This is a later version than the default on our oldest-supported Android devices.
const Lkbd = cache.getKeyboardForStub(Lstub);
const Lrn = this._GetKeyboardDetail(Lstub, Lkbd); // I2078 - Full keyboard detail
Lr.push(Lrn);
if (Lkbd instanceof JSKeyboard) {
const Lrn = this._GetKeyboardDetail(Lstub, Lkbd); // I2078 - Full keyboard detail
Lr.push(Lrn);
} else {
// TODO-web-core: implement for KMX keyboards if needed
}
}
return Lr;
}
Expand Down Expand Up @@ -669,13 +679,17 @@ export default class KeymanEngine extends KeymanEngineBase<BrowserConfiguration,
argFormFactor?: DeviceSpec.FormFactor,
argLayerId?: string
): HTMLElement {
let PKbd: JSKeyboard = null;
let PKbd: Keyboard = null;

if(PInternalName != null) {
PKbd = this.keyboardRequisitioner.cache.getKeyboard(PInternalName);
}

PKbd = PKbd || this.core.activeKeyboard;
if (!(PKbd instanceof JSKeyboard)) {
// TODO-web-core: implement for KMX keyboards if needed
return null;
}
let Pstub = this.keyboardRequisitioner.cache.getStub(PKbd);

// help.keyman.com will set this function in place to specify the desired
Expand Down
9 changes: 6 additions & 3 deletions web/src/app/webview/src/contextManager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type JSKeyboard } from 'keyman/engine/keyboard';
import { JSKeyboard, Keyboard } from 'keyman/engine/keyboard';
import { Mock, OutputTarget, Transcription, findCommonSubstringEndIndex, isEmptyTransform, TextTransform } from 'keyman/engine/js-processor';
import { KeyboardStub } from 'keyman/engine/keyboard-storage';
import { ContextManagerBase } from 'keyman/engine/main';
Expand Down Expand Up @@ -192,7 +192,7 @@ export default class ContextManager extends ContextManagerBase<WebviewConfigurat
protected prepareKeyboardForActivation(
keyboardId: string,
languageCode?: string
): {keyboard: Promise<JSKeyboard>, metadata: KeyboardStub} {
): {keyboard: Promise<Keyboard>, metadata: KeyboardStub} {
const originalKeyboard = this.activeKeyboard;
const activatingKeyboard = super.prepareKeyboardForActivation(keyboardId, languageCode);

Expand All @@ -203,7 +203,10 @@ export default class ContextManager extends ContextManagerBase<WebviewConfigurat
// That said, 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()
// TODO-web-core: Do we need to refresh layouts for KMX keyboards also?
if (kbd instanceof JSKeyboard) {
kbd.refreshLayouts();
}
return kbd;
});
}
Expand Down
4 changes: 2 additions & 2 deletions web/src/app/webview/src/passthroughKeyboard.ts
Original file line number Diff line number Diff line change
@@ -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();
Expand Down
4 changes: 2 additions & 2 deletions web/src/engine/js-processor/src/jsKeyboardInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}

Expand Down
20 changes: 11 additions & 9 deletions web/src/engine/keyboard-storage/src/stubAndKeyboardCache.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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<EventMap> {
private stubSetTable: Record<string, Record<string, KeyboardStub>> = {};
private keyboardTable: Record<string, JSKeyboard | Promise<JSKeyboard>> = {};
private keyboardTable: Record<string, Keyboard | Promise<Keyboard>> = {};

private readonly keyboardLoader: KeyboardLoader;

Expand All @@ -52,11 +52,11 @@ export default class StubAndKeyboardCache extends EventEmitter<EventMap> {
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;
}
Expand Down Expand Up @@ -112,14 +112,14 @@ export default class StubAndKeyboardCache extends EventEmitter<EventMap> {
}
}

addKeyboard(keyboard: JSKeyboard) {
addKeyboard(keyboard: Keyboard) {
const keyboardID = prefixed(keyboard.id);
this.keyboardTable[keyboardID] = keyboard;

this.emit('keyboardadded', keyboard);
}

fetchKeyboardForStub(stub: KeyboardStub) : Promise<JSKeyboard> {
fetchKeyboardForStub(stub: KeyboardStub) : Promise<Keyboard> {
return this.fetchKeyboard(stub.KI);
}

Expand All @@ -134,7 +134,7 @@ export default class StubAndKeyboardCache extends EventEmitter<EventMap> {
return cachedEntry instanceof Promise;
}

fetchKeyboard(keyboardID: string): Promise<JSKeyboard> {
fetchKeyboard(keyboardID: string): Promise<Keyboard> {
if(!keyboardID) {
throw new Error("Keyboard ID must be specified");
}
Expand Down Expand Up @@ -166,7 +166,9 @@ export default class StubAndKeyboardCache extends EventEmitter<EventMap> {

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];
Expand Down
1 change: 1 addition & 0 deletions web/src/engine/keyboard/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
4 changes: 3 additions & 1 deletion web/src/engine/keyboard/src/index.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
21 changes: 17 additions & 4 deletions web/src/engine/keyboard/src/keyboards/keyboardLoaderBase.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<JSKeyboard> {
public loadKeyboardFromPath(uri: string): Promise<Keyboard> {
this.harness.install();
return this.loadKeyboardInternal(uri, new UriBasedErrorBuilder(uri));
}
Expand All @@ -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<JSKeyboard> {
public async loadKeyboardFromStub(stub: KeyboardStub): Promise<Keyboard> {
this.harness.install();
return this.loadKeyboardInternal(stub.filename, new StubBasedErrorBuilder(stub));
}

private async loadKeyboardInternal(uri: string, errorBuilder: KeyboardLoadErrorBuilder): Promise<JSKeyboard> {
private async loadKeyboardInternal(uri: string, errorBuilder: KeyboardLoadErrorBuilder): Promise<Keyboard> {
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;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Keyboard } from './keyboardLoaderBase.js';

export interface KeyboardMinimalInterface {
activeKeyboard: Keyboard;
}
24 changes: 24 additions & 0 deletions web/src/engine/keyboard/src/keyboards/kmxKeyboard.ts
Original file line number Diff line number Diff line change
@@ -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 '';
}
}
Loading

0 comments on commit f8f844f

Please sign in to comment.