Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(web): add Keyboard and KMXKeyboard classes 🎼 #12825

Draft
wants to merge 1 commit into
base: refactor/web/jskeyboards
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading