From bd827f83951e00e42fb308a4e23914772c07a3b6 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Fri, 4 Sep 2026 21:55:58 +0200 Subject: [PATCH 1/3] refactor(web): clarify function scope and update some function comments --- web/src/app/ui/kmwuitoggle.ts | 36 ++++++++++++++--------------------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/web/src/app/ui/kmwuitoggle.ts b/web/src/app/ui/kmwuitoggle.ts index bc5afb1c7f0..c07065c50a9 100644 --- a/web/src/app/ui/kmwuitoggle.ts +++ b/web/src/app/ui/kmwuitoggle.ts @@ -258,7 +258,7 @@ if(!keyman) { * // and the like are defined on individual instances later. * // It thinks they're always null. **/ - button(_src: string, _caption: string, _selected: boolean) { + private button(_src: string, _caption: string, _selected: boolean) { /** * Only ui.controllerHovered is referenced here: it'd be easy enough to toggle it via closure * and extract this inner class into its own definition outside of `class ToggleUI`. @@ -401,11 +401,9 @@ if(!keyman) { }; /** - * Function Initialize - * Scope Private - * Description Initialize Toggle User Interface + * Initialize Toggle User Interface **/ - initialize() { + public initialize() { //Never initialize before KMW! if(!keyman.initialized || util.isTouchDevice()) { return; @@ -482,12 +480,10 @@ if(!keyman) { } /** - * Function updateKeyboardList - * Scope Private - * Description Rebuild the UI and keyboard list + * Rebuild the UI and keyboard list **/ - readonly updateKeyboardList = () => { - if(!(keyman.initialized || this.initialized)) { + public readonly updateKeyboardList = () => { + if (!(keyman.initialized || this.initialized)) { return; //TODO: may want to restart the timer?? } @@ -562,10 +558,9 @@ if(!keyman) { // var _SelectedMenuItem; /** - * Function selecKbd - * Scope Private + * Select a keyboard from the drop down menu + * * @param {number} kbdIndex - * Description Select a keyboard from the drop down menu **/ private async selectKbd(kbdIndex: number): Promise { let name: string, languageCode: string; @@ -588,13 +583,12 @@ if(!keyman) { }; /** - * Function updateMenu - * Scope Private + * Updates the menu selection when a change is required + * * @param {string} kbdName * @param {?string=} lgCode - * Description Updates the menu selection when a change is required **/ - updateMenu(kbdName: string, lgCode: string) { + public updateMenu(kbdName: string, lgCode: string) { let _k=document.getElementById('KMWSel_$'); for(let i=0; i < this.keyboards.length; i++) { @@ -629,7 +623,7 @@ if(!keyman) { } } - get stylingCSS() { + private get stylingCSS() { return ` #KeymanWeb_KbdList { display: block; @@ -713,11 +707,9 @@ if(!keyman) { } /** - * Function createMenu - * Scope Private - * Description Create the drop down menu and populate with loaded KeymanWeb keyboards + * Create the drop down menu and populate with loaded KeymanWeb keyboards **/ - createMenu() { + private createMenu() { if(typeof(this.keyboardMenu) == 'undefined') { // I2403 - Allow toggle design to be loaded twice this.keyboardMenu = util.createElement('ul'); this.keyboardMenu.id='KeymanWeb_KbdList'; From 04a5cd13c9bd233788c8f575b867d1bb5c2c3138 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Fri, 4 Sep 2026 18:10:23 +0200 Subject: [PATCH 2/3] chore(web): minor improvements and fixes Build-bot: skip build:web Test-bot: skip --- web/src/app/browser/src/contextManager.ts | 7 +++++- web/src/app/browser/src/keymanEngine.ts | 23 ++++++++++--------- web/src/app/webview/src/contextManager.ts | 2 +- web/src/engine/src/main/contextManagerBase.ts | 4 ++-- 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/web/src/app/browser/src/contextManager.ts b/web/src/app/browser/src/contextManager.ts index 52bace4f87b..d1d428c1ae9 100644 --- a/web/src/app/browser/src/contextManager.ts +++ b/web/src/app/browser/src/contextManager.ts @@ -232,7 +232,7 @@ export class ContextManager extends ContextManagerBase { } } - public setActiveTextStore(textStore: AbstractElementTextStore, sendEvents?: boolean) { + public setActiveTextStore(textStore: AbstractElementTextStore, sendEvents?: boolean): void { const previousTextStore = this.mostRecentTextStore; const originalTextStore = this.activeTextStore; // may differ, depending on focus state. @@ -410,6 +410,11 @@ export class ContextManager extends ContextManagerBase { * activates the keyboard if the specified control represents the * currently-active context. * + * If kbdId and langId are both null, the control will use the global + * keyboard. If both are the empty string, the control will use the + * system keyboard (on desktop), or the first installed keyboard (on + * touch devices). + * * This is the core method that backs * https://help.keyman.com/developer/engine/web/current-version/reference/core/setKeyboardForControl. * @param textStore diff --git a/web/src/app/browser/src/keymanEngine.ts b/web/src/app/browser/src/keymanEngine.ts index 8f1f7ac54ca..9b56247cdd1 100644 --- a/web/src/app/browser/src/keymanEngine.ts +++ b/web/src/app/browser/src/keymanEngine.ts @@ -315,14 +315,14 @@ export class KeymanEngine extends KeymanEngineBase { return this._activeKeyboard; } - activateKeyboardForTextStore(kbd: { keyboard: Keyboard, metadata: KeyboardStub }, textStore: TextStore) { + protected activateKeyboardForTextStore(kbd: { keyboard: Keyboard, metadata: KeyboardStub }, textStore: TextStore) { // `textStore` is irrelevant for `app/webview`, as it'll only ever use 'global' keyboard settings. // Clone the object to prevent accidental by-reference changes. diff --git a/web/src/engine/src/main/contextManagerBase.ts b/web/src/engine/src/main/contextManagerBase.ts index 8554c124bc8..1d5fc942501 100644 --- a/web/src/engine/src/main/contextManagerBase.ts +++ b/web/src/engine/src/main/contextManagerBase.ts @@ -248,7 +248,7 @@ export abstract class ContextManagerBase const wasNull = !this.activeKeyboard; // If there was a previous activation attempt set and still active for the specified keyboard textStore, - // cancel it. For exmaple, if the user selects a preloaded keyboard after having tried to select one + // cancel it. For example, if the user selects a preloaded keyboard after having tried to select one // still async-loading, we should go with the later setting - the preloaded one. this.findAndPopActivation(this.currentKeyboardSrcTextStore()); @@ -322,7 +322,7 @@ export abstract class ContextManagerBase if(keyboardId) { requestedStub = this.keyboardCache.getStub(keyboardId, languageCode); } else { - languageCode == ''; + languageCode = ''; } if(!requestedStub) { From d2abc847ff119588c437803173d06c92812c038b Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Mon, 7 Sep 2026 18:31:26 +0200 Subject: [PATCH 3/3] refactor(web): simplify logic of `prepareKeyboardForActivation` Co-authored-by: Marc Durdin --- web/src/engine/src/main/contextManagerBase.ts | 126 +++++++++--------- web/src/engine/src/main/keymanEngineBase.ts | 6 +- 2 files changed, 63 insertions(+), 69 deletions(-) diff --git a/web/src/engine/src/main/contextManagerBase.ts b/web/src/engine/src/main/contextManagerBase.ts index 1d5fc942501..931c18c039b 100644 --- a/web/src/engine/src/main/contextManagerBase.ts +++ b/web/src/engine/src/main/contextManagerBase.ts @@ -314,31 +314,25 @@ export abstract class ContextManagerBase keyboardId: string, languageCode?: string ): {keyboard: Promise, metadata: KeyboardStub} { + if (!keyboardId) { + return { + keyboard: Promise.resolve(null), + metadata: null + } + } + // Set default language code languageCode ||= ''; // Check that the saved keyboard is currently registered - let requestedStub: KeyboardStub = null; - if(keyboardId) { - requestedStub = this.keyboardCache.getStub(keyboardId, languageCode); - } else { - languageCode = ''; - } - + const requestedStub: KeyboardStub = this.keyboardCache.getStub(keyboardId, languageCode); if(!requestedStub) { - if(keyboardId) { - const availableStubList = this.keyboardCache.getStubList().map(stub => `${stub.KI}@${stub.KLC}`); - throw new Error(`No matching stub has been registered for keyboard ${keyboardId}. Available stubs: ${JSON.stringify(availableStubList)}`); - } else { - return { - keyboard: Promise.resolve(null), - metadata: null - } - } + const availableStubList = this.keyboardCache.getStubList().map(stub => `${stub.KI}@${stub.KLC}`); + throw new Error(`No matching stub has been registered for keyboard ${keyboardId}. Available stubs: ${JSON.stringify(availableStubList)}`); } // Check if current keyboard matches requested keyboard, but not (necessarily) stub - if (this.activeKeyboard?.metadata && keyboardId == this.activeKeyboard.metadata.id) { + if (keyboardId == this.activeKeyboard?.metadata?.id) { const {keyboard} = this.activeKeyboard; // In this case, the keyboard is loaded; just update the stub. @@ -349,61 +343,61 @@ 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: Keyboard; - if(keyboard = this.keyboardCache.getKeyboardForStub(requestedStub)) { + const keyboard: Keyboard = this.keyboardCache.getKeyboardForStub(requestedStub); + if (keyboard) { 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. - - // `beforeKeyboardChange` - first call - this.emit('beforekeyboardchange', requestedStub); - - const defermentPromise = this.engineConfig.deferForInitialization.then(() => { - // Provide a Promise for completion of the async load process. - const completionPromise = new ManagedPromise(); - this.emit('keyboardasyncload', requestedStub, completionPromise.corePromise); - - const keyboardPromise = this.keyboardCache.fetchKeyboard(requestedStub.KI); - const timeoutPromise = new Promise((resolve, reject) => { - const timeoutMsg = `Download of ${requestedStub.KI} for language ${requestedStub.langId} timed out.`; - window.setTimeout(() => reject(new Error(timeoutMsg)), ContextManagerBase.TIMEOUT_THRESHOLD); - }); - - const combinedPromise = Promise.race([keyboardPromise, timeoutPromise]); - - // Ensure the async-load Promise completes properly. - combinedPromise.then(() => { - completionPromise.resolve(null); - // Prevent any 'unhandled Promise rejection' events that may otherwise occur from the timeout promise. - timeoutPromise.catch(() => {}); - }); - combinedPromise.catch((err) => { - completionPromise.resolve(err); - throw err; - }); - - return combinedPromise; + } + + // It's async time - the keyboard is not preloaded within the cache. Use the stub's data to load it. + + // `beforeKeyboardChange` - first call + this.emit('beforekeyboardchange', requestedStub); + + const defermentPromise = this.engineConfig.deferForInitialization.then(() => { + // Provide a Promise for completion of the async load process. + const completionPromise = new ManagedPromise(); + this.emit('keyboardasyncload', requestedStub, completionPromise.corePromise); + + const keyboardPromise = this.keyboardCache.fetchKeyboard(requestedStub.KI); + const timeoutPromise = new Promise((resolve, reject) => { + const timeoutMsg = `Download of ${requestedStub.KI} for language ${requestedStub.langId} timed out.`; + window.setTimeout(() => reject(new Error(timeoutMsg)), ContextManagerBase.TIMEOUT_THRESHOLD); }); - // Now the fun part: note the original call's parameters as a pending activation. - const promise = this.deferredKeyboardActivation(defermentPromise, requestedStub, this.currentKeyboardSrcTextStore()); - 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 - // textStore, the activation is no longer valid. - return Promise.resolve(null); - } else { - return defermentPromise; - } - }), - metadata: requestedStub - } + const combinedPromise = Promise.race([keyboardPromise, timeoutPromise]); + + // Ensure the async-load Promise completes properly. + combinedPromise.then(() => { + completionPromise.resolve(null); + // Prevent any 'unhandled Promise rejection' events that may otherwise occur from the timeout promise. + timeoutPromise.catch(() => {}); + }); + combinedPromise.catch((err) => { + completionPromise.resolve(err); + throw err; + }); + + return combinedPromise; + }); + + // Now the fun part: note the original call's parameters as a pending activation. + const promise = this.deferredKeyboardActivation(defermentPromise, requestedStub, this.currentKeyboardSrcTextStore()); + 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 + // textStore, the activation is no longer valid. + return Promise.resolve(null); + } else { + return defermentPromise; + } + }), + metadata: requestedStub } } } diff --git a/web/src/engine/src/main/keymanEngineBase.ts b/web/src/engine/src/main/keymanEngineBase.ts index 793f1bcc773..2ee22a2b370 100644 --- a/web/src/engine/src/main/keymanEngineBase.ts +++ b/web/src/engine/src/main/keymanEngineBase.ts @@ -543,8 +543,8 @@ export class KeymanEngineBase< /** * Allow to change active keyboard by (internal) keyboard name * - * @param {string} PInternalName Internal name - * @param {string} PLgCode Language code + * @param {string} keyboardId Keyboard name + * @param {string} languageCode Language code * * See https://help.keyman.com/developer/engine/web/current-version/reference/core/setActiveKeyboard */ @@ -632,4 +632,4 @@ export class KeymanEngineBase< }; } -// Intent: define common behaviors for both primary app types; each then subclasses & extends where needed. \ No newline at end of file +// Intent: define common behaviors for both primary app types; each then subclasses & extends where needed.