From 25a40ab903e04458796be87f12774555d075aee8 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Tue, 23 Jun 2026 10:41:40 +0200 Subject: [PATCH 01/44] fix(developer): resync model and keyboard after device switch in Server When a new OSK is attached, we need to set its active keyboard and lexical model. These are undocumented API contracts between Keyman Engine for Web and Keyman Developer Server, so this appears to have changed in v.19 for keyboards (although the sync is also necessary for v.18). Fixes: #15217 Test-bot: skip --- developer/src/server/src/site/test.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/developer/src/server/src/site/test.js b/developer/src/server/src/site/test.js index eeefd21fced..000a2006323 100644 --- a/developer/src/server/src/site/test.js +++ b/developer/src/server/src/site/test.js @@ -262,6 +262,11 @@ function setupKeyman() { newOSK.setSize(targetDevice.dimensions[0]+'px', targetDevice.dimensions[1]+'px'); } document.getElementById('osk-host').appendChild(newOSK.element); + + // After attaching a new OSK, we also need to set the OSK's active keyboard + // and model + newOSK.activeKeyboard = keyman.contextManager.activeKeyboard; // Note: undocumented KeymanWeb API refs on both sides + selectModel(keyman.core.activeModel?.id); } setOSK(); From d86604902e4c95e1ce8674bfac9cd588cf153082 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Wed, 24 Jun 2026 14:19:36 +0200 Subject: [PATCH 02/44] chore(developer): consolidate api-extractor usage in Developer * Centralize the api-extractor.json files, update usage * Support @since * Tidy up a few warnings relating to API documentation content Fixes: #14838 Test-bot: skip --- .gitignore | 4 ++ common/tools/api-extractor/README.md | 35 +++++++++++++ .../api-extractor}/api-extractor.base.json | 6 +-- .../api-extractor/api-extractor.template.json | 15 ++++++ .../tools/api-extractor/tsdoc.template.json | 15 ++++++ developer/docs/api/etc/kmc-copy.api.md | 2 +- developer/docs/api/etc/kmc-kmn.api.md | 12 ++--- developer/src/kmc-analyze/build.sh | 2 +- .../src/kmc-analyze/config/api-extractor.json | 12 ----- .../src/osk-character-use/index.ts | 4 +- developer/src/kmc-copy/build.sh | 2 +- .../src/kmc-copy/config/api-extractor.json | 12 ----- .../src/kmc-copy/src/KeymanProjectCopier.ts | 22 +++++---- developer/src/kmc-generate/build.sh | 2 +- .../kmc-generate/config/api-extractor.json | 12 ----- developer/src/kmc-keyboard-info/build.sh | 2 +- .../config/api-extractor.json | 12 ----- developer/src/kmc-kmn/build.sh | 2 +- .../src/kmc-kmn/config/api-extractor.json | 12 ----- developer/src/kmc-kmn/src/compiler/osk.ts | 36 +++++++++++++- developer/src/kmc-ldml/build.sh | 2 +- .../src/kmc-ldml/config/api-extractor.json | 12 ----- developer/src/kmc-model-info/build.sh | 2 +- .../kmc-model-info/config/api-extractor.json | 12 ----- developer/src/kmc-model/build.sh | 2 +- .../src/kmc-model/config/api-extractor.json | 12 ----- developer/src/kmc-package/build.sh | 2 +- .../src/kmc-package/config/api-extractor.json | 12 ----- resources/build/typescript.inc.sh | 49 +++++++++++++++++++ 29 files changed, 187 insertions(+), 139 deletions(-) create mode 100644 common/tools/api-extractor/README.md rename {developer/config => common/tools/api-extractor}/api-extractor.base.json (99%) create mode 100644 common/tools/api-extractor/api-extractor.template.json create mode 100644 common/tools/api-extractor/tsdoc.template.json delete mode 100644 developer/src/kmc-analyze/config/api-extractor.json delete mode 100644 developer/src/kmc-copy/config/api-extractor.json delete mode 100644 developer/src/kmc-generate/config/api-extractor.json delete mode 100644 developer/src/kmc-keyboard-info/config/api-extractor.json delete mode 100644 developer/src/kmc-kmn/config/api-extractor.json delete mode 100644 developer/src/kmc-ldml/config/api-extractor.json delete mode 100644 developer/src/kmc-model-info/config/api-extractor.json delete mode 100644 developer/src/kmc-model/config/api-extractor.json delete mode 100644 developer/src/kmc-package/config/api-extractor.json diff --git a/.gitignore b/.gitignore index 7b872195880..eaf8f5f3ebf 100644 --- a/.gitignore +++ b/.gitignore @@ -186,3 +186,7 @@ lcov.info # flag file for build script .configured + +# see common/tools/api-extractor/README.md +tsdoc.json +api-extractor.json diff --git a/common/tools/api-extractor/README.md b/common/tools/api-extractor/README.md new file mode 100644 index 00000000000..e9367309294 --- /dev/null +++ b/common/tools/api-extractor/README.md @@ -0,0 +1,35 @@ +# api-extractor.template.json + +* Reference: https://api-extractor.com + +api-extractor.template.json contains a template for api-extractor; these parameters +cannot be passed in to the tool, so we modify this template as needed with the following +parameters: + +* `$keyman_root`: the `$KEYMAN_ROOT` variable, with backslash \ translated to forward slash / +* `$index_d_ts`: the filename `index.d.ts` or the corresponding filename for the + entry point of the project +* `$project_path`: the path for the module, relative to the base of the repo +* `$report_temp`: a temporary path for output files for api-extractor +* `$report_folder`: target folder for completed api-extractor API documentation + +# tsdoc.template.json + +* Reference: https://tsdoc.org/pages/packages/tsdoc-config/ + +tsdoc.template.json is copied (unmodified) from this folder into tsdoc.json in +project folders (alongside tsconfig.json) before running api-extractor and +removed again afterwards; there is no way to specify an alternate location for +the file. + +tsdoc.template.json includes a definition for "@since" which has been proposed in +https://github.com/microsoft/tsdoc/issues/136. + +# Notes + +These files are used by `typescript_run_api_extractor()` in typescript.inc.sh. + +This is setup only for Developer projects at this time as outputs go into +developer/docs and developer/build; future generalization requires changing only +`$report_temp` and `$report_folder` parameters in +`typescript_run_api_extractor()`. diff --git a/developer/config/api-extractor.base.json b/common/tools/api-extractor/api-extractor.base.json similarity index 99% rename from developer/config/api-extractor.base.json rename to common/tools/api-extractor/api-extractor.base.json index 2c11f6402db..4ba43a64513 100644 --- a/developer/config/api-extractor.base.json +++ b/common/tools/api-extractor/api-extractor.base.json @@ -161,7 +161,7 @@ * SUPPORTED TOKENS: , , * DEFAULT VALUE: "/etc/" */ - "reportFolder": "../docs/api/etc/", + // "reportFolder": "../docs/api/etc/", /** * Specifies the folder where the temporary report file is written. The file name portion is determined by @@ -176,7 +176,7 @@ * SUPPORTED TOKENS: , , * DEFAULT VALUE: "/temp/" */ - "reportTempFolder": "../build/api/", + // "reportTempFolder": "../build/api/", /** * Whether "forgotten exports" should be included in the API report file. Forgotten exports are declarations @@ -206,7 +206,7 @@ * SUPPORTED TOKENS: , , * DEFAULT VALUE: "/build/temp/.api.json" */ - "apiJsonFilePath": "../build/api/.api.json", + // "apiJsonFilePath": "../build/api/.api.json", /** * Whether "forgotten exports" should be included in the doc model file. Forgotten exports are declarations diff --git a/common/tools/api-extractor/api-extractor.template.json b/common/tools/api-extractor/api-extractor.template.json new file mode 100644 index 00000000000..ccc76623cd2 --- /dev/null +++ b/common/tools/api-extractor/api-extractor.template.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "extends": "$keyman_root/common/tools/api-extractor/api-extractor.base.json", + "mainEntryPointFilePath": "/build/src/$index_d_ts", + "docModel": { + "enabled": true, + "projectFolderUrl": "http://github.com/keymanapp/keyman/tree/master/$project_path", + "apiJsonFilePath": "$report_temp/.api.json" + }, + "apiReport": { + "enabled": true, + "reportFolder": "$report_folder/", + "reportTempFolder": "$report_temp/" + } +} diff --git a/common/tools/api-extractor/tsdoc.template.json b/common/tools/api-extractor/tsdoc.template.json new file mode 100644 index 00000000000..2a5a8d130b8 --- /dev/null +++ b/common/tools/api-extractor/tsdoc.template.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json", + "extends": ["@microsoft/api-extractor/extends/tsdoc-base.json"], + "tagDefinitions": [ + { + "tagName": "@since", + "syntaxKind": "block", + "allowMultiple": false + } + ], + + "supportForTags": { + "@since": true + } +} \ No newline at end of file diff --git a/developer/docs/api/etc/kmc-copy.api.md b/developer/docs/api/etc/kmc-copy.api.md index f61dd59e108..3ec2789a6c3 100644 --- a/developer/docs/api/etc/kmc-copy.api.md +++ b/developer/docs/api/etc/kmc-copy.api.md @@ -214,7 +214,7 @@ export interface CopierOptions extends CompilerBaseOptions { relocateExternalFiles?: boolean; } -// @public (undocumented) +// @public export class KeymanProjectCopier implements KeymanCompiler { // Warning: (ae-forgotten-export) The symbol "CopierAsyncCallbacks" needs to be exported by the entry point main.d.ts // diff --git a/developer/docs/api/etc/kmc-kmn.api.md b/developer/docs/api/etc/kmc-kmn.api.md index 0805075e56a..b4372a99c25 100644 --- a/developer/docs/api/etc/kmc-kmn.api.md +++ b/developer/docs/api/etc/kmc-kmn.api.md @@ -933,21 +933,21 @@ declare namespace Osk { } export { Osk } -// @public (undocumented) +// @public function parseMapping(mapping: any): PuaMap; -// @public (undocumented) +// @public type PuaMap = { [index: string]: string; }; -// @public (undocumented) +// @public function remapTouchLayout(source: TouchLayout.TouchLayoutFile, map: PuaMap): boolean; -// @public (undocumented) +// @public function remapVisualKeyboard(vk: VisualKeyboard.VisualKeyboard, map: PuaMap): boolean; -// @public (undocumented) +// @public interface StringRef { // (undocumented) str: string; @@ -955,7 +955,7 @@ interface StringRef { usages: StringRefUsage[]; } -// @public (undocumented) +// @public interface StringRefUsage { // (undocumented) count: number; diff --git a/developer/src/kmc-analyze/build.sh b/developer/src/kmc-analyze/build.sh index f28c822b5a3..1bfde715855 100755 --- a/developer/src/kmc-analyze/build.sh +++ b/developer/src/kmc-analyze/build.sh @@ -27,5 +27,5 @@ builder_parse "$@" builder_run_action clean rm -rf ./build/ builder_run_action configure node_select_version_and_npm_ci builder_run_action build tsc --build -builder_run_action api api-extractor run --local --verbose +builder_run_action api typescript_run_api_extractor developer/src/kmc-analyze index.d.ts builder_run_action test typescript_run_eslint_mocha_tests 75 diff --git a/developer/src/kmc-analyze/config/api-extractor.json b/developer/src/kmc-analyze/config/api-extractor.json deleted file mode 100644 index 855dc1f4471..00000000000 --- a/developer/src/kmc-analyze/config/api-extractor.json +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Config file for API Extractor. For more info, please visit: https://api-extractor.com - */ -{ - "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", - "extends": "../../../config/api-extractor.base.json", - "mainEntryPointFilePath": "/build/src/index.d.ts", - "docModel": { - "enabled": true, - "projectFolderUrl": "http://github.com/keymanapp/keyman/tree/master/developer/src/kmc-analyze" - } -} diff --git a/developer/src/kmc-analyze/src/osk-character-use/index.ts b/developer/src/kmc-analyze/src/osk-character-use/index.ts index 7058ad67760..4343a3b8c87 100644 --- a/developer/src/kmc-analyze/src/osk-character-use/index.ts +++ b/developer/src/kmc-analyze/src/osk-character-use/index.ts @@ -304,7 +304,7 @@ export class AnalyzeOskCharacterUse { * * - .json: returns the final aggregated data as an array of strings, which * can be joined to form a JSON blob of an object with a single member, - * `map`, which is an array of {@link Osk.StringResult} objects. + * `map`, which is an array of {@link @keymanapp/kmc-kmn#Osk.StringResult} objects. * * @param format - file format to return - can be '.txt', '.md', or '.json' * @returns an array of strings, formatted according to the `format` @@ -324,7 +324,7 @@ export class AnalyzeOskCharacterUse { /** * Load a JSON-format result file to merge from - * @param filename + * @param filename - the full path to the JSON result file to load * @returns */ private loadPreviousMap(filename: string): Osk.StringResult[] { diff --git a/developer/src/kmc-copy/build.sh b/developer/src/kmc-copy/build.sh index fe9ffc10868..f3bc50c85a0 100755 --- a/developer/src/kmc-copy/build.sh +++ b/developer/src/kmc-copy/build.sh @@ -33,7 +33,7 @@ builder_parse "$@" builder_run_action clean rm -rf ./build/ builder_run_action configure node_select_version_and_npm_ci builder_run_action build tsc --build -builder_run_action api api-extractor run --local --verbose +builder_run_action api typescript_run_api_extractor developer/src/kmc-copy main.d.ts # note: `export TEST_SAVE_ARTIFACTS=1` to save a copy of artifacts to temp path # note: `export TEST_SAVE_FIXTURES=1` to get a copy of cloud-based fixtures saved to online/ diff --git a/developer/src/kmc-copy/config/api-extractor.json b/developer/src/kmc-copy/config/api-extractor.json deleted file mode 100644 index 12a33719245..00000000000 --- a/developer/src/kmc-copy/config/api-extractor.json +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Config file for API Extractor. For more info, please visit: https://api-extractor.com - */ -{ - "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", - "extends": "../../../config/api-extractor.base.json", - "mainEntryPointFilePath": "/build/src/main.d.ts", - "docModel": { - "enabled": true, - "projectFolderUrl": "http://github.com/keymanapp/keyman/tree/master/developer/src/kmc-copy" - } -} diff --git a/developer/src/kmc-copy/src/KeymanProjectCopier.ts b/developer/src/kmc-copy/src/KeymanProjectCopier.ts index 4218cc6e941..d9bd33ae91b 100644 --- a/developer/src/kmc-copy/src/KeymanProjectCopier.ts +++ b/developer/src/kmc-copy/src/KeymanProjectCopier.ts @@ -81,6 +81,10 @@ export interface CopierResult extends KeymanCompilerResult { artifacts: CopierArtifacts; }; +/** + * @public + * Copy a project and rename internal references + */ export class KeymanProjectCopier implements KeymanCompiler { options: CopierOptions; callbacks: CompilerCallbacks; @@ -111,8 +115,8 @@ export class KeymanProjectCopier implements KeymanCompiler { * artifacts on success. The files are passed in by name, and the compiler * will use callbacks as passed to the {@link KeymanProjectCopier.init} * function to read any input files by disk. - * @param source Source file or folder to copy. Can be a local file or folder, https://github.com/.../repo[/path], or cloud:id - * @returns Binary artifacts on success, null on failure. + * @param source - Source file or folder to copy. Can be a local file or folder, https://github.com/.../repo[/path], or cloud:id + * @returns Binary artifacts on success, null on failure. */ public async run(source: string): Promise { @@ -174,7 +178,7 @@ export class KeymanProjectCopier implements KeymanCompiler { /** * Resolve the source project file to either a local filesystem file, * or a reference on GitHub - * @param source + * @param source - URI to a project file * @returns path to .kpj (either local or remote) */ private async getSourceProject(source: string): Promise { @@ -196,7 +200,7 @@ export class KeymanProjectCopier implements KeymanCompiler { /** * Resolve source path to the contained project file; the project * file must have the same basename as the folder in this case - * @param source + * @param source - local file path to a .kpj project file * @returns */ private getLocalFolderProject(source: string): string { @@ -212,7 +216,7 @@ export class KeymanProjectCopier implements KeymanCompiler { /** * Resolve source path to the input .kpj filename, folder name * is not relevant when .kpj filename is passed in - * @param source + * @param source - local file path to a .kpj project file * @returns */ private getLocalFileProject(source: string): string { @@ -224,7 +228,7 @@ export class KeymanProjectCopier implements KeymanCompiler { * `[https://]github.com/owner/repo/branch/path/to/kpj` * The path must be fully qualified, referencing the .kpj file; it * cannot just be the folder where the .kpj is found - * @param source + * @param source - URL to a .kpj project file on GitHub * @returns a promise: GitHub reference to the source for the keyboard, or null on failure */ private async getGitHubSourceProject(source: string): Promise { @@ -273,7 +277,7 @@ export class KeymanProjectCopier implements KeymanCompiler { * The `keyboard_id` parameter should be a valid id (a-z0-9_), as found at * https://keyman.com/keyboards; alternatively if it is a model_id, it should * have the format author.bcp47.uniq - * @param source + * @param source - a reference to a keyboard or model project on Keyman Cloud * @returns a promise: GitHub reference to the source for the keyboard, or null on failure */ private async getCloudSourceProject(source: string): Promise { @@ -613,8 +617,8 @@ export class KeymanProjectCopier implements KeymanCompiler { /** * renames matching filename to the output filename pattern, and prepends the * outputPath - * @param filename - * @param outputPath + * @param filename - input filename to rename + * @param outputPath - target filename path * @returns */ private generateNewFilename(filename: string, outputPath: string): string { diff --git a/developer/src/kmc-generate/build.sh b/developer/src/kmc-generate/build.sh index d67cbaf21a1..fd4c71de6e3 100755 --- a/developer/src/kmc-generate/build.sh +++ b/developer/src/kmc-generate/build.sh @@ -39,5 +39,5 @@ do_build() { builder_run_action clean rm -rf ./build/ ./tsconfig.tsbuildinfo builder_run_action configure node_select_version_and_npm_ci builder_run_action build do_build -builder_run_action api api-extractor run --local --verbose +builder_run_action api typescript_run_api_extractor developer/src/kmc-copy main.d.ts builder_run_action test typescript_run_eslint_mocha_tests diff --git a/developer/src/kmc-generate/config/api-extractor.json b/developer/src/kmc-generate/config/api-extractor.json deleted file mode 100644 index 5ab7b50e162..00000000000 --- a/developer/src/kmc-generate/config/api-extractor.json +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Config file for API Extractor. For more info, please visit: https://api-extractor.com - */ -{ - "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", - "extends": "../../../config/api-extractor.base.json", - "mainEntryPointFilePath": "/build/src/main.d.ts", - "docModel": { - "enabled": true, - "projectFolderUrl": "http://github.com/keymanapp/keyman/tree/master/developer/src/kmc-generate" - } -} diff --git a/developer/src/kmc-keyboard-info/build.sh b/developer/src/kmc-keyboard-info/build.sh index fe8ecfac583..58b415b47f6 100755 --- a/developer/src/kmc-keyboard-info/build.sh +++ b/developer/src/kmc-keyboard-info/build.sh @@ -32,7 +32,7 @@ builder_parse "$@" builder_run_action clean rm -rf ./build/ ./tsconfig.tsbuildinfo builder_run_action configure node_select_version_and_npm_ci builder_run_action build tsc --build -builder_run_action api api-extractor run --local --verbose +builder_run_action api typescript_run_api_extractor developer/src/kmc-copy index.d.ts builder_run_action test typescript_run_eslint_mocha_tests #------------------------------------------------------------------------------------------------------------------- diff --git a/developer/src/kmc-keyboard-info/config/api-extractor.json b/developer/src/kmc-keyboard-info/config/api-extractor.json deleted file mode 100644 index b98feb9963c..00000000000 --- a/developer/src/kmc-keyboard-info/config/api-extractor.json +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Config file for API Extractor. For more info, please visit: https://api-extractor.com - */ -{ - "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", - "extends": "../../../config/api-extractor.base.json", - "mainEntryPointFilePath": "/build/src/index.d.ts", - "docModel": { - "enabled": true, - "projectFolderUrl": "http://github.com/keymanapp/keyman/tree/master/developer/src/kmc-keyboard-info" - } -} diff --git a/developer/src/kmc-kmn/build.sh b/developer/src/kmc-kmn/build.sh index 27fb9584867..bb64f5c48dd 100755 --- a/developer/src/kmc-kmn/build.sh +++ b/developer/src/kmc-kmn/build.sh @@ -65,5 +65,5 @@ function do_test() { } builder_run_action build do_build -builder_run_action api api-extractor run --local --verbose +builder_run_action api typescript_run_api_extractor developer/src/kmc-copy main.d.ts builder_run_action test do_test diff --git a/developer/src/kmc-kmn/config/api-extractor.json b/developer/src/kmc-kmn/config/api-extractor.json deleted file mode 100644 index 5df197da3e9..00000000000 --- a/developer/src/kmc-kmn/config/api-extractor.json +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Config file for API Extractor. For more info, please visit: https://api-extractor.com - */ -{ - "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", - "extends": "../../../config/api-extractor.base.json", - "mainEntryPointFilePath": "/build/src/main.d.ts", - "docModel": { - "enabled": true, - "projectFolderUrl": "http://github.com/keymanapp/keyman/tree/master/developer/src/kmc-kmn" - } -} diff --git a/developer/src/kmc-kmn/src/compiler/osk.ts b/developer/src/kmc-kmn/src/compiler/osk.ts index e26c124b15b..f9194c1642f 100644 --- a/developer/src/kmc-kmn/src/compiler/osk.ts +++ b/developer/src/kmc-kmn/src/compiler/osk.ts @@ -2,17 +2,27 @@ import { TouchLayout } from "@keymanapp/common-types"; import { VisualKeyboard } from "@keymanapp/common-types"; import { SchemaValidators } from "@keymanapp/common-types"; +/** + * @public + * Records the number of references to an OSK key cap string for a specific + * file + */ export interface StringRefUsage { filename: string; count: number; }; +/** + * @public + * Tracks usage of a single OSK key cap string across multiple files + */ export interface StringRef { str: string; usages: StringRefUsage[]; }; /** + * @public * Represents a single key cap found by `AnalyzeOskCharacterUse` */ export interface StringResult { @@ -23,13 +33,23 @@ export interface StringResult { /** hexadecimal single character in PUA range, without 'U+' prefix, e.g. 'F100' */ pua: string; /** files in which the string is referenced; will be an array of - * {@link StringRefUsage} if includeCounts is true, otherwise will be an array + * {@link @keymanapp/kmc-kmn#Osk.StringRefUsage} if includeCounts is true, otherwise will be an array * of strings listing files in which the key cap may be found */ usages: StringRefUsage[] | string[]; }; +/** + * @public + * Maps a source OSK key cap string to a PUA character + */ export type PuaMap = {[index:string]: string}; +/** + * @public + * Parse a map object loaded from a displaymap file into a PuaMap + * @param mapping - source object to parse, must be in displayMap JSON format + * @returns + */ export function parseMapping(mapping: any) { if(!SchemaValidators.default.displayMap(mapping)) /* c8 ignore next 3 */ @@ -60,6 +80,13 @@ function remap(text: string, map: PuaMap) { return text; } +/** + * @public + * Remap key caps in the `vk` visual keyboard object to use PUA characters from `map` + * @param vk - source visual keyboard object to remap, updated in place + * @param map - PUA string mapping to apply + * @returns + */ export function remapVisualKeyboard(vk: VisualKeyboard.VisualKeyboard, map: PuaMap): boolean { let dirty = false; for(const key of vk.keys) { @@ -73,6 +100,13 @@ export function remapVisualKeyboard(vk: VisualKeyboard.VisualKeyboard, map: PuaM return dirty; } +/** + * @public + * Remap key caps in the `source` touch layout object to use PUA characters from `map` + * @param source - source touch layout object to remap, updated in place + * @param map - PUA string mapping to apply + * @returns + */ export function remapTouchLayout(source: TouchLayout.TouchLayoutFile, map: PuaMap) { let dirty = false; const scanKey = (key: TouchLayout.TouchLayoutKey | TouchLayout.TouchLayoutSubKey) => { diff --git a/developer/src/kmc-ldml/build.sh b/developer/src/kmc-ldml/build.sh index 708a4c9b3d7..627286551c0 100755 --- a/developer/src/kmc-ldml/build.sh +++ b/developer/src/kmc-ldml/build.sh @@ -85,5 +85,5 @@ builder_run_action clean do_clean builder_run_action configure do_configure builder_run_action build do_build builder_run_action build-fixtures do_build_fixtures -builder_run_action api api-extractor run --local --verbose +builder_run_action api typescript_run_api_extractor developer/src/kmc-copy main.d.ts builder_run_action test typescript_run_eslint_mocha_tests 90 diff --git a/developer/src/kmc-ldml/config/api-extractor.json b/developer/src/kmc-ldml/config/api-extractor.json deleted file mode 100644 index 9022460bf8a..00000000000 --- a/developer/src/kmc-ldml/config/api-extractor.json +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Config file for API Extractor. For more info, please visit: https://api-extractor.com - */ -{ - "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", - "extends": "../../../config/api-extractor.base.json", - "mainEntryPointFilePath": "/build/src/main.d.ts", - "docModel": { - "enabled": true, - "projectFolderUrl": "http://github.com/keymanapp/keyman/tree/master/developer/src/kmc-ldml" - } -} diff --git a/developer/src/kmc-model-info/build.sh b/developer/src/kmc-model-info/build.sh index fb4743700f4..deb29bcd018 100755 --- a/developer/src/kmc-model-info/build.sh +++ b/developer/src/kmc-model-info/build.sh @@ -29,5 +29,5 @@ builder_parse "$@" builder_run_action clean rm -rf ./build/ ./tsconfig.tsbuildinfo builder_run_action configure node_select_version_and_npm_ci builder_run_action build tsc --build -builder_run_action api api-extractor run --local --verbose +builder_run_action api typescript_run_api_extractor developer/src/kmc-copy index.d.ts builder_run_action test typescript_run_eslint_mocha_tests 55 diff --git a/developer/src/kmc-model-info/config/api-extractor.json b/developer/src/kmc-model-info/config/api-extractor.json deleted file mode 100644 index 73bd2798926..00000000000 --- a/developer/src/kmc-model-info/config/api-extractor.json +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Config file for API Extractor. For more info, please visit: https://api-extractor.com - */ -{ - "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", - "extends": "../../../config/api-extractor.base.json", - "mainEntryPointFilePath": "/build/src/index.d.ts", - "docModel": { - "enabled": true, - "projectFolderUrl": "http://github.com/keymanapp/keyman/tree/master/developer/src/kmc-model-info" - } -} diff --git a/developer/src/kmc-model/build.sh b/developer/src/kmc-model/build.sh index 4b5f15cc307..ab32fbd1536 100755 --- a/developer/src/kmc-model/build.sh +++ b/developer/src/kmc-model/build.sh @@ -36,5 +36,5 @@ function do_build() { builder_run_action clean rm -rf ./build/ ./tsconfig.tsbuildinfo builder_run_action configure node_select_version_and_npm_ci builder_run_action build do_build -builder_run_action api api-extractor run --local --verbose +builder_run_action api typescript_run_api_extractor developer/src/kmc-copy main.d.ts builder_run_action test typescript_run_eslint_mocha_tests diff --git a/developer/src/kmc-model/config/api-extractor.json b/developer/src/kmc-model/config/api-extractor.json deleted file mode 100644 index de898e137df..00000000000 --- a/developer/src/kmc-model/config/api-extractor.json +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Config file for API Extractor. For more info, please visit: https://api-extractor.com - */ -{ - "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", - "extends": "../../../config/api-extractor.base.json", - "mainEntryPointFilePath": "/build/src/main.d.ts", - "docModel": { - "enabled": true, - "projectFolderUrl": "http://github.com/keymanapp/keyman/tree/master/developer/src/kmc-model" - } -} diff --git a/developer/src/kmc-package/build.sh b/developer/src/kmc-package/build.sh index f1d6ba8dcc0..d435bb59198 100755 --- a/developer/src/kmc-package/build.sh +++ b/developer/src/kmc-package/build.sh @@ -34,5 +34,5 @@ builder_parse "$@" builder_run_action clean rm -rf ./build/ ./tsconfig.tsbuildinfo builder_run_action configure node_select_version_and_npm_ci builder_run_action build tsc --build -builder_run_action api api-extractor run --local --verbose +builder_run_action api typescript_run_api_extractor developer/src/kmc-copy main.d.ts builder_run_action test typescript_run_eslint_mocha_tests diff --git a/developer/src/kmc-package/config/api-extractor.json b/developer/src/kmc-package/config/api-extractor.json deleted file mode 100644 index 6ba3ee01fd1..00000000000 --- a/developer/src/kmc-package/config/api-extractor.json +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Config file for API Extractor. For more info, please visit: https://api-extractor.com - */ -{ - "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", - "extends": "../../../config/api-extractor.base.json", - "mainEntryPointFilePath": "/build/src/main.d.ts", - "docModel": { - "enabled": true, - "projectFolderUrl": "http://github.com/keymanapp/keyman/tree/master/developer/src/kmc-package" - } -} diff --git a/resources/build/typescript.inc.sh b/resources/build/typescript.inc.sh index ef6f58d68a6..fe39d870d1e 100644 --- a/resources/build/typescript.inc.sh +++ b/resources/build/typescript.inc.sh @@ -58,3 +58,52 @@ typescript_run_eslint_mocha_tests() { echo "##teamcity[flowFinished flowId='unit_tests']" fi } + +# +# Run api-extractor, preparing config files for each folder. As the config files +# are largely identical for each project, we copy them in rather than +# duplicating them across the repo (as that is hard to maintain over time) +# +# NOTE: this is setup only for Developer projects at this time as outputs go +# into developer/docs and developer/build; future generalization requires +# changing only report_temp and report_folder parameters. +# +# See also: /common/tools/api-extractor/README.md +# +typescript_run_api_extractor() { + project_path="$1" + index_d_ts="$2" + + # tsdoc config file must be in same folder as tsconfig.json + cp "${KEYMAN_ROOT}/common/tools/api-extractor/tsdoc.template.json" "${THIS_SCRIPT_PATH}/tsdoc.json" + + # api-extractor configuration must be stored in a file, so patch the + # file with the relevant parameters + + if builder_is_windows; then + # replace \ with / on Windows in KEYMAN_ROOT path, so we don't end up with + # silly unescaped strings in JSON + keyman_root="$(echo "$KEYMAN_ROOT" | sed "s=\\\=/=g")" + else + keyman_root="${KEYMAN_ROOT}" + fi + + # For now, these two variables are Developer-specific + report_temp="$keyman_root/developer/build/api" + report_folder="$keyman_root/developer/docs/api/etc" + export project_path index_d_ts keyman_root report_temp report_folder + + envsubst "\$keyman_root,\$project_path,\$index_d_ts,\$report_temp,\$report_folder" \ + < "${KEYMAN_ROOT}/common/tools/api-extractor/api-extractor.template.json" \ + > "${THIS_SCRIPT_PATH}/api-extractor.json" + + export -n project_path index_d_ts keyman_root report_temp report_folder + + api-extractor run \ + --local \ + --verbose \ + --config "${THIS_SCRIPT_PATH}/api-extractor.json" + + rm "${THIS_SCRIPT_PATH}/tsdoc.json" + rm "${THIS_SCRIPT_PATH}/api-extractor.json" +} \ No newline at end of file From 77ad99ee256ce8bd71c5e627f1a80849269f2b85 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Thu, 25 Jun 2026 11:02:46 +0200 Subject: [PATCH 03/44] feat(android): add keyman-version to package-version check For Keyman for Android, add the new `keyman-version` parameter to the api.keyman.com/package-version call so that updates to packages that are not supported on the current version of Keyman will not be offered. This supports the scenario where an updated keyboard or lexical model depends on a newer version of Keyman. Note that an older version of the keyboard or lexical model package will not be offered (the user can still download and install an older version manually, but generally, the recommended solution is to upgrade Keyman; there would be significant cost to add support for querying and installation of older version keyboards on the server side, for limited benefit). Relates-to: keymanapp/api.keyman.com#325 --- .../src/main/java/com/keyman/engine/data/CloudRepository.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/data/CloudRepository.java b/android/KMEA/app/src/main/java/com/keyman/engine/data/CloudRepository.java index 4676d59c775..5bd4d8d6849 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/data/CloudRepository.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/data/CloudRepository.java @@ -45,7 +45,7 @@ public class CloudRepository { public static final String API_STAGING_HOST = "api.keyman.com"; // #7227 disabling: "api.keyman-staging.com"; public static final String API_MODEL_LANGUAGE_FORMATSTR = "https://%s/model?q=bcp47:%s"; - public static final String API_PACKAGE_VERSION_FORMATSTR = "https://%s/package-version?platform=android%s%s"; + public static final String API_PACKAGE_VERSION_FORMATSTR = "https://%s/package-version?platform=android&keyman-version=%s%s%s"; private Dataset memCachedDataset; private Calendar lastLoad; // To be used for Dataset caching. @@ -205,7 +205,7 @@ private CloudApiTypes.CloudApiParam prepareResourcesUpdateQuery(Context aContext } } - String queryURL = String.format(API_PACKAGE_VERSION_FORMATSTR, getHost(), keyboardQuery, lexicalModelQuery); + String queryURL = String.format(API_PACKAGE_VERSION_FORMATSTR, getHost(), KMManager.getVersion(), keyboardQuery, lexicalModelQuery); return new CloudApiTypes.CloudApiParam( CloudApiTypes.ApiTarget.PackageVersion, queryURL).setType(CloudApiTypes.JSONType.Object); } From bbeb799ac2a686b27ae28a43c2d3eef874ce1d67 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Thu, 25 Jun 2026 10:53:31 +0200 Subject: [PATCH 04/44] feat(ios): add keyman-version to package-version check For Keyman for iOS, add the new `keyman-version` parameter to the api.keyman.com/package-version call so that updates to packages that are not supported on the current version of Keyman will not be offered. This supports the scenario where an updated keyboard or lexical model depends on a newer version of Keyman. Note that an older version of the keyboard or lexical model package will not be offered (the user can still download and install an older version manually, but generally, the recommended solution is to upgrade Keyman; there would be significant cost to add support for querying and installation of older version keyboards on the server side, for limited benefit). Also change order of handling for response so that error responses are recognized even if non-error fields are present. (The current api endpoint, before keymanapp/api.keyman.com#325 lands, can return `kmp` and `version` fields even when an `error` field was present, and the iOS code would see that as a valid response for upgrade, when it shouldn't. The keymanapp/api.keyman.com#325 change ensures that the additional fields are not set if there is an `error` field.) Relates-to: keymanapp/api.keyman.com#325 --- .../Classes/Queries/Queries+PackageVersion.swift | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Queries/Queries+PackageVersion.swift b/ios/engine/KMEI/KeymanEngine/Classes/Queries/Queries+PackageVersion.swift index 69ca1e15a91..769089fdbc5 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Queries/Queries+PackageVersion.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Queries/Queries+PackageVersion.swift @@ -52,10 +52,10 @@ extension Queries { func dictionaryReducer(container: KeyedDecodingContainer, category: String) -> ([String: ResultComponent], Queries.IDCodingKey) throws -> [String: ResultComponent] { return { (dict, id) -> [String: ResultComponent] in var dict = dict - if let entry = try? container.decode(ResultEntry.self, forKey: id) { - dict[id.stringValue] = entry - } else if let error = try? container.decode(ResultError.self, forKey: id) { + if let error = try? container.decode(ResultError.self, forKey: id) { dict[id.stringValue] = error + } else if let entry = try? container.decode(ResultEntry.self, forKey: id) { + dict[id.stringValue] = entry } else { throw FetchError.decodingError(category, id.stringValue) } @@ -116,7 +116,10 @@ extension Queries { return URLQueryItem(name: resourceField, value: key.id) } - urlComponents.queryItems = queryItems + [URLQueryItem(name: "platform", value: "ios")] + urlComponents.queryItems = queryItems + [ + URLQueryItem(name: "platform", value: "ios"), + URLQueryItem(name: "keyman-version", value: Version.current.plainString) + ] let message = "Querying package versions through API endpoint: \(urlComponents.url!)" os_log("%{public}s", log:KeymanEngineLogger.resources, type: .info, message) SentryManager.breadcrumb(message) From 7155e9d7223ee21d249df1d3698bc00f8ffb5fab Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Mon, 29 Jun 2026 09:50:51 +0200 Subject: [PATCH 05/44] fix(android): refactor `KMLog` - simplification and added resilience * Clean up `KMLog` -- simpler code paths, DRY out common validation, remove redundant re-entrancy checks. * Use Sentry `setTag` API and scopes instead of `setExtra`. * Wrap all potential failure points in exception handlers for extra resilience -- do our best to make sure errors are reported in as many cases as possible. Fixes: #16122 Test-bot: skip --- .../java/com/keyman/engine/util/KMLog.java | 189 +++++++++--------- 1 file changed, 94 insertions(+), 95 deletions(-) diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/util/KMLog.java b/android/KMEA/app/src/main/java/com/keyman/engine/util/KMLog.java index 1ce55bf0ce4..e6eb05a96ee 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/util/KMLog.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/util/KMLog.java @@ -24,16 +24,11 @@ public final class KMLog { private static final String TAG = "KMLog"; - private static final String KEYBOARD_TAG = "keyboardId"; - private static final String KEYBOARD_COUNT_TAG = "installedKeyboardCount"; - private static final String MODEL_TAG = "modelId"; - private static final String LANGCODE_TAG = "languageCode"; - - // Some of the methods used to generate debug logging information can, themselves, - // trigger errors that can also trigger the same logging. We must not get - // caught in an infinite loop / stack-overflow; this field helps us avoid states - // that would otherwise cause error-looping, etc. - private static boolean isLogging = false; + private static final String KEYBOARD_TAG = "keyman.keyboardId"; + private static final String KEYBOARD_COUNT_TAG = "keyman.installedKeyboardCount"; + private static final String MODEL_TAG = "keyman.modelId"; + private static final String LANGCODE_TAG = "keyman.languageCode"; + private static final String DEBUG_LOGGING_ERROR_TAG = "keyman.debugLoggingError"; private static void tagDebugInfo() { String kbdId = ""; @@ -57,13 +52,16 @@ private static void tagDebugInfo() { } } } catch (Exception ex) { - String msg = ex.getMessage() == null ? "" : ex.getMessage(); - Sentry.setExtra("debugLoggingError", msg); + Sentry.setTag(DEBUG_LOGGING_ERROR_TAG, ex.getMessage() == null ? "" : ex.getMessage()); } - Sentry.setExtra(KEYBOARD_TAG, kbdId); - Sentry.setExtra(KEYBOARD_COUNT_TAG, "" + kbdCount); - Sentry.setExtra(LANGCODE_TAG, lngCode); - Sentry.setExtra(MODEL_TAG, modelId); + Sentry.setTag(KEYBOARD_TAG, kbdId); + Sentry.setTag(KEYBOARD_COUNT_TAG, "" + kbdCount); + Sentry.setTag(LANGCODE_TAG, lngCode); + Sentry.setTag(MODEL_TAG, modelId); + } + + private static boolean canLogToSentry() { + return DependencyUtil.libraryExists(LibraryType.SENTRY) && Sentry.isEnabled(); } /** @@ -72,19 +70,20 @@ private static void tagDebugInfo() { * @param msg String of the info message */ public static void LogInfo(String tag, String msg) { - if(isLogging) { + if (msg == null || msg.isEmpty()) { return; } - isLogging = true; - if (msg != null && !msg.isEmpty()) { - Log.i(tag, msg); - if (DependencyUtil.libraryExists(LibraryType.SENTRY) && Sentry.isEnabled()) { - tagDebugInfo(); - Sentry.captureMessage(msg, SentryLevel.INFO); - } + Log.i(tag, msg); + + if (!canLogToSentry()) { + return; } - isLogging = false; + + Sentry.withScope(scope -> { + tagDebugInfo(); + Sentry.captureMessage(msg, SentryLevel.INFO); + }); } /** @@ -98,15 +97,9 @@ public static void LogBreadcrumb(String tag, String msg, boolean addStackTrace) return; } - if(isLogging) { - return; - } - isLogging = true; - Log.i(tag, msg); - if (!DependencyUtil.libraryExists(LibraryType.SENTRY) || !Sentry.isEnabled()) { - isLogging = false; + if (!canLogToSentry()) { return; } @@ -114,27 +107,30 @@ public static void LogBreadcrumb(String tag, String msg, boolean addStackTrace) crumb.setMessage(msg); crumb.setLevel(SentryLevel.INFO); - if(addStackTrace) { - StackTraceElement[] rawTrace = Thread.currentThread().getStackTrace(); - - // The call that gets us the stack-trace above... shows up in the - // stack trace, so we'll skip the first few (redundant) entries. - int skipCount = 3; - - // Sentry does limit the size of messages... so let's just - // keep 10 entries and call it a day. - int limit = Math.min(rawTrace.length, 10 + skipCount); - if(rawTrace.length > skipCount) { - String[] trace = new String[limit - skipCount]; - for (int i = skipCount; i < limit; i++) { - trace[i-skipCount] = rawTrace[i].toString(); + try { + if(addStackTrace) { + StackTraceElement[] rawTrace = Thread.currentThread().getStackTrace(); + + // The call that gets us the stack-trace above... shows up in the + // stack trace, so we'll skip the first few (redundant) entries. + int skipCount = 3; + + // Sentry does limit the size of messages... so let's just + // keep 10 entries and call it a day. + int limit = Math.min(rawTrace.length, 10 + skipCount); + if(rawTrace.length > skipCount) { + String[] trace = new String[limit - skipCount]; + for (int i = skipCount; i < limit; i++) { + trace[i-skipCount] = rawTrace[i].toString(); + } + crumb.setData("stacktrace", trace); } - crumb.setData("stacktrace", trace); } + } catch (Exception e) { + Sentry.captureException(e); } - tagDebugInfo(); + Sentry.addBreadcrumb(crumb); - isLogging = false; } /** @@ -143,23 +139,32 @@ public static void LogBreadcrumb(String tag, String msg, boolean addStackTrace) * @param msg String of the error message */ public static void LogError(String tag, String msg) { - if(isLogging) { + if (msg == null || msg.isEmpty()) { return; } - isLogging = true; - if (msg != null && !msg.isEmpty()) { - Log.e(tag, msg); + Log.e(tag, msg); + + try { + // On alpha and beta tiers, we pop an error toast so testers can be aware + // of the error if (KMManager.getTier(BuildConfig.KEYMAN_ENGINE_VERSION_NAME) != KMManager.Tier.STABLE) { BaseActivity.makeToast(null, msg, Toast.LENGTH_LONG); } - - if (DependencyUtil.libraryExists(LibraryType.SENTRY) && Sentry.isEnabled()) { - tagDebugInfo(); - Sentry.captureMessage(msg, SentryLevel.ERROR); + } catch(Exception e) { + if(canLogToSentry()) { + Sentry.captureException(e); } } - isLogging = false; + + if (!canLogToSentry()) { + return; + } + + Sentry.withScope(scope -> { + tagDebugInfo(); + Sentry.captureMessage(msg, SentryLevel.ERROR); + }); } /** @@ -169,28 +174,7 @@ public static void LogError(String tag, String msg) { * @param e Throwable exception */ public static void LogException(String tag, String msg, Throwable e) { - if(isLogging) { - return; - } - isLogging = true; - String errorMsg = ""; - if (msg != null && !msg.isEmpty()) { - errorMsg = msg + "\n" + e; - } else if (e != null) { - errorMsg = e.getMessage(); - } - Log.e(tag, errorMsg, e); - - if (KMManager.getTier(BuildConfig.KEYMAN_ENGINE_VERSION_NAME) != KMManager.Tier.STABLE) { - BaseActivity.makeToast(null, errorMsg, Toast.LENGTH_LONG); - } - - if (DependencyUtil.libraryExists(LibraryType.SENTRY) && Sentry.isEnabled()) { - tagDebugInfo(); - Sentry.addBreadcrumb(errorMsg); - Sentry.captureException(e); - } - isLogging = false; + KMLog.LogExceptionWithData(tag, msg, null, null, e); } /** @@ -203,25 +187,40 @@ public static void LogException(String tag, String msg, Throwable e) { */ public static void LogExceptionWithData(String tag, String msg, String objName, Object obj, Throwable e) { - if(isLogging) { + String errorMsg = ""; + try { + if (msg != null && !msg.isEmpty()) { + errorMsg = msg + "\n" + e.toString(); + } else if (e != null) { + errorMsg = e.toString(); + } + + // On alpha and beta tiers, we pop an error toast so testers can be aware + // of the exception + if (KMManager.getTier(BuildConfig.KEYMAN_ENGINE_VERSION_NAME) != KMManager.Tier.STABLE) { + BaseActivity.makeToast(null, errorMsg, Toast.LENGTH_LONG); + } + } catch (Exception innerE) { + if (canLogToSentry()) { + Sentry.captureException(innerE); + } + } + + if (!canLogToSentry()) { return; } - isLogging = true; - if (obj != null && DependencyUtil.libraryExists(LibraryType.SENTRY) && Sentry.isEnabled()) { + + Sentry.addBreadcrumb(errorMsg); + Sentry.withScope(scope -> { tagDebugInfo(); - String objStr = null; try { - objStr = obj.toString(); - Sentry.setExtra(objName, objStr); - } catch (Exception innerE) { - LogException(TAG, "Sentry.setExtra failed for " + objName, innerE); + if(obj != null && objName != null) { + Sentry.setTag(objName, obj.toString()); + } + } catch(Exception innerE) { + Sentry.captureException(innerE); } - // Report the original exception - LogException(tag, msg, e); - // And remove the exception-specific tagged data, lest it also be - // tracked on subsequent errors not associated with the current call. - Sentry.removeExtra(objName); - } - isLogging = false; + Sentry.captureException(e); + }); } } From b535203e366311cba22f7d50b5ef08a6a17c8aaa Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Mon, 29 Jun 2026 16:34:39 +0200 Subject: [PATCH 06/44] chore(developer): fix docs, api_extractor params in build.sh's Test-bot: skip Build-bot: skip release:developer --- common/tools/api-extractor/README.md | 36 ++++++++++++++---------- developer/src/kmc-generate/build.sh | 2 +- developer/src/kmc-keyboard-info/build.sh | 2 +- developer/src/kmc-kmn/build.sh | 2 +- developer/src/kmc-ldml/build.sh | 2 +- developer/src/kmc-model-info/build.sh | 2 +- developer/src/kmc-model/build.sh | 2 +- developer/src/kmc-package/build.sh | 2 +- 8 files changed, 28 insertions(+), 22 deletions(-) diff --git a/common/tools/api-extractor/README.md b/common/tools/api-extractor/README.md index e9367309294..0462e78bcb4 100644 --- a/common/tools/api-extractor/README.md +++ b/common/tools/api-extractor/README.md @@ -1,19 +1,29 @@ -# api-extractor.template.json +# Introduction + +These files are used by `typescript_run_api_extractor()` in typescript.inc.sh. + +This is setup only for Developer projects at this time as outputs go into +developer/docs and developer/build; future generalization requires changing only +`$report_temp` and `$report_folder` parameters in +`typescript_run_api_extractor()`. + +## api-extractor.template.json * Reference: https://api-extractor.com -api-extractor.template.json contains a template for api-extractor; these parameters -cannot be passed in to the tool, so we modify this template as needed with the following -parameters: +api-extractor.template.json contains a template for api-extractor; these +parameters cannot be passed in to the tool, so we modify this template as needed +with the following parameters: -* `$keyman_root`: the `$KEYMAN_ROOT` variable, with backslash \ translated to forward slash / +* `$keyman_root`: the `$KEYMAN_ROOT` variable, with backslash \ translated to + forward slash / * `$index_d_ts`: the filename `index.d.ts` or the corresponding filename for the entry point of the project * `$project_path`: the path for the module, relative to the base of the repo * `$report_temp`: a temporary path for output files for api-extractor * `$report_folder`: target folder for completed api-extractor API documentation -# tsdoc.template.json +## tsdoc.template.json * Reference: https://tsdoc.org/pages/packages/tsdoc-config/ @@ -22,14 +32,10 @@ project folders (alongside tsconfig.json) before running api-extractor and removed again afterwards; there is no way to specify an alternate location for the file. -tsdoc.template.json includes a definition for "@since" which has been proposed in -https://github.com/microsoft/tsdoc/issues/136. - -# Notes +tsdoc.template.json includes a definition for "@since" which has been proposed +in https://github.com/microsoft/tsdoc/issues/136. -These files are used by `typescript_run_api_extractor()` in typescript.inc.sh. +## Notes -This is setup only for Developer projects at this time as outputs go into -developer/docs and developer/build; future generalization requires changing only -`$report_temp` and `$report_folder` parameters in -`typescript_run_api_extractor()`. +See also .github/workflows/api-verification.yml which does API validation of +Core for desktop platforms. \ No newline at end of file diff --git a/developer/src/kmc-generate/build.sh b/developer/src/kmc-generate/build.sh index fd4c71de6e3..323c78937b6 100755 --- a/developer/src/kmc-generate/build.sh +++ b/developer/src/kmc-generate/build.sh @@ -39,5 +39,5 @@ do_build() { builder_run_action clean rm -rf ./build/ ./tsconfig.tsbuildinfo builder_run_action configure node_select_version_and_npm_ci builder_run_action build do_build -builder_run_action api typescript_run_api_extractor developer/src/kmc-copy main.d.ts +builder_run_action api typescript_run_api_extractor developer/src/kmc-generate main.d.ts builder_run_action test typescript_run_eslint_mocha_tests diff --git a/developer/src/kmc-keyboard-info/build.sh b/developer/src/kmc-keyboard-info/build.sh index 58b415b47f6..abd4604d96a 100755 --- a/developer/src/kmc-keyboard-info/build.sh +++ b/developer/src/kmc-keyboard-info/build.sh @@ -32,7 +32,7 @@ builder_parse "$@" builder_run_action clean rm -rf ./build/ ./tsconfig.tsbuildinfo builder_run_action configure node_select_version_and_npm_ci builder_run_action build tsc --build -builder_run_action api typescript_run_api_extractor developer/src/kmc-copy index.d.ts +builder_run_action api typescript_run_api_extractor developer/src/kmc-keyboard-info index.d.ts builder_run_action test typescript_run_eslint_mocha_tests #------------------------------------------------------------------------------------------------------------------- diff --git a/developer/src/kmc-kmn/build.sh b/developer/src/kmc-kmn/build.sh index bb64f5c48dd..2f1dd214ed3 100755 --- a/developer/src/kmc-kmn/build.sh +++ b/developer/src/kmc-kmn/build.sh @@ -65,5 +65,5 @@ function do_test() { } builder_run_action build do_build -builder_run_action api typescript_run_api_extractor developer/src/kmc-copy main.d.ts +builder_run_action api typescript_run_api_extractor developer/src/kmc-kmn main.d.ts builder_run_action test do_test diff --git a/developer/src/kmc-ldml/build.sh b/developer/src/kmc-ldml/build.sh index 627286551c0..b9e259e8dfa 100755 --- a/developer/src/kmc-ldml/build.sh +++ b/developer/src/kmc-ldml/build.sh @@ -85,5 +85,5 @@ builder_run_action clean do_clean builder_run_action configure do_configure builder_run_action build do_build builder_run_action build-fixtures do_build_fixtures -builder_run_action api typescript_run_api_extractor developer/src/kmc-copy main.d.ts +builder_run_action api typescript_run_api_extractor developer/src/kmc-ldml main.d.ts builder_run_action test typescript_run_eslint_mocha_tests 90 diff --git a/developer/src/kmc-model-info/build.sh b/developer/src/kmc-model-info/build.sh index deb29bcd018..75b1d5d17e2 100755 --- a/developer/src/kmc-model-info/build.sh +++ b/developer/src/kmc-model-info/build.sh @@ -29,5 +29,5 @@ builder_parse "$@" builder_run_action clean rm -rf ./build/ ./tsconfig.tsbuildinfo builder_run_action configure node_select_version_and_npm_ci builder_run_action build tsc --build -builder_run_action api typescript_run_api_extractor developer/src/kmc-copy index.d.ts +builder_run_action api typescript_run_api_extractor developer/src/kmc-model-info index.d.ts builder_run_action test typescript_run_eslint_mocha_tests 55 diff --git a/developer/src/kmc-model/build.sh b/developer/src/kmc-model/build.sh index ab32fbd1536..c01a4cdb765 100755 --- a/developer/src/kmc-model/build.sh +++ b/developer/src/kmc-model/build.sh @@ -36,5 +36,5 @@ function do_build() { builder_run_action clean rm -rf ./build/ ./tsconfig.tsbuildinfo builder_run_action configure node_select_version_and_npm_ci builder_run_action build do_build -builder_run_action api typescript_run_api_extractor developer/src/kmc-copy main.d.ts +builder_run_action api typescript_run_api_extractor developer/src/kmc-model main.d.ts builder_run_action test typescript_run_eslint_mocha_tests diff --git a/developer/src/kmc-package/build.sh b/developer/src/kmc-package/build.sh index d435bb59198..fe2102ff404 100755 --- a/developer/src/kmc-package/build.sh +++ b/developer/src/kmc-package/build.sh @@ -34,5 +34,5 @@ builder_parse "$@" builder_run_action clean rm -rf ./build/ ./tsconfig.tsbuildinfo builder_run_action configure node_select_version_and_npm_ci builder_run_action build tsc --build -builder_run_action api typescript_run_api_extractor developer/src/kmc-copy main.d.ts +builder_run_action api typescript_run_api_extractor developer/src/kmc-package main.d.ts builder_run_action test typescript_run_eslint_mocha_tests From 809a3cad4a42df5324d002af1c8362cbd2f2bc00 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Tue, 30 Jun 2026 15:12:59 +0200 Subject: [PATCH 07/44] chore(android): address review comments Also add logging if unit test fails --- .../main/java/com/keyman/engine/util/KMLog.java | 14 +++++++++++--- .../java/com/keyman/engine/util/FileUtilsTest.java | 2 +- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/util/KMLog.java b/android/KMEA/app/src/main/java/com/keyman/engine/util/KMLog.java index e6eb05a96ee..633f5c853b8 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/util/KMLog.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/util/KMLog.java @@ -117,8 +117,8 @@ public static void LogBreadcrumb(String tag, String msg, boolean addStackTrace) // Sentry does limit the size of messages... so let's just // keep 10 entries and call it a day. - int limit = Math.min(rawTrace.length, 10 + skipCount); if(rawTrace.length > skipCount) { + int limit = Math.min(rawTrace.length, 10 + skipCount); String[] trace = new String[limit - skipCount]; for (int i = skipCount; i < limit; i++) { trace[i-skipCount] = rawTrace[i].toString(); @@ -190,7 +190,7 @@ public static void LogExceptionWithData(String tag, String msg, String errorMsg = ""; try { if (msg != null && !msg.isEmpty()) { - errorMsg = msg + "\n" + e.toString(); + errorMsg = msg + "\n" + e; } else if (e != null) { errorMsg = e.toString(); } @@ -206,6 +206,8 @@ public static void LogExceptionWithData(String tag, String msg, } } + Log.e(tag, errorMsg); + if (!canLogToSentry()) { return; } @@ -215,12 +217,18 @@ public static void LogExceptionWithData(String tag, String msg, tagDebugInfo(); try { if(obj != null && objName != null) { - Sentry.setTag(objName, obj.toString()); + Sentry.setExtra(objName, obj.toString()); } } catch(Exception innerE) { Sentry.captureException(innerE); } Sentry.captureException(e); + + if(obj != null && objName != null) { + // And remove the exception-specific tagged data, lest it also be + // tracked on subsequent errors not associated with the current call. + Sentry.removeExtra(objName); + } }); } } diff --git a/android/KMEA/app/src/test/java/com/keyman/engine/util/FileUtilsTest.java b/android/KMEA/app/src/test/java/com/keyman/engine/util/FileUtilsTest.java index 5118159c9b4..9541a59afe2 100644 --- a/android/KMEA/app/src/test/java/com/keyman/engine/util/FileUtilsTest.java +++ b/android/KMEA/app/src/test/java/com/keyman/engine/util/FileUtilsTest.java @@ -23,7 +23,7 @@ public void test_download() { List logs = ShadowLog.getLogs(); // The logs contain type 4, but we only care about type 6 for connection messages - Assert.assertEquals(2, logs.size()); + Assert.assertEquals("Size did not match 2: " + logs.toString(), 2, logs.size()); Assert.assertEquals("Connection", logs.get(0).tag); Assert.assertEquals("Initialization failed:\njava.net.MalformedURLException: no protocol: invalidURL", logs.get(0).msg); From 0fc869faa4f2dff6f8dcdb47813724424a6a56ce Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Wed, 1 Jul 2026 07:38:57 +0200 Subject: [PATCH 08/44] fix(web): define IME interfaces in `KeyboardInterfaceBase` Ensure that the IME interface methods are all defined for both browser and webview modes. Deprecate 'Element' methods and replace with 'TextStore' to clarify return type. Add deprecation flags where needed and update documentation accordingly. Fixes: #16144 --- .../compatibility/FocusLastActiveElement.md | 26 ------ .../compatibility/GetLastActiveElement.md | 26 ------ .../reference/compatibility/HideHelp.md | 25 ------ .../reference/compatibility/ShowPinnedHelp.md | 26 ------ .../engine/reference/compatibility/index.md | 33 ------- web/docs/engine/reference/index.md | 3 - .../interface/FocusLastActiveElement.md | 38 ++++++++ .../interface/GetLastActiveElement.md | 39 +++++++++ .../engine/reference/interface/HideHelp.md | 30 +++++++ .../{compatibility => interface}/ShowHelp.md | 13 ++- .../reference/interface/ShowPinnedHelp.md | 31 +++++++ .../interface/focusLastActiveTextStore.md | 24 +++++ .../interface/getLastActiveTextStore.md | 26 ++++++ web/docs/engine/reference/interface/index.md | 28 ++++++ web/src/app/browser/src/keyboardInterface.ts | 24 ++--- web/src/app/browser/src/keymanEngine.ts | 2 +- .../engine/src/main/keyboardInterfaceBase.ts | 87 +++++++++++++++++++ 17 files changed, 325 insertions(+), 156 deletions(-) delete mode 100644 web/docs/engine/reference/compatibility/FocusLastActiveElement.md delete mode 100644 web/docs/engine/reference/compatibility/GetLastActiveElement.md delete mode 100644 web/docs/engine/reference/compatibility/HideHelp.md delete mode 100644 web/docs/engine/reference/compatibility/ShowPinnedHelp.md delete mode 100644 web/docs/engine/reference/compatibility/index.md create mode 100644 web/docs/engine/reference/interface/FocusLastActiveElement.md create mode 100644 web/docs/engine/reference/interface/GetLastActiveElement.md create mode 100644 web/docs/engine/reference/interface/HideHelp.md rename web/docs/engine/reference/{compatibility => interface}/ShowHelp.md (65%) create mode 100644 web/docs/engine/reference/interface/ShowPinnedHelp.md create mode 100644 web/docs/engine/reference/interface/focusLastActiveTextStore.md create mode 100644 web/docs/engine/reference/interface/getLastActiveTextStore.md diff --git a/web/docs/engine/reference/compatibility/FocusLastActiveElement.md b/web/docs/engine/reference/compatibility/FocusLastActiveElement.md deleted file mode 100644 index 49cb6dfed40..00000000000 --- a/web/docs/engine/reference/compatibility/FocusLastActiveElement.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: FocusLastActiveElement ---- - -## Summary - -Restore the focus to the element active before input was moved to -KeymanWeb. - -## Syntax - -``` -keyman.FocusLastActiveElement() -``` - -### Parameters - -None. - -### Return Value - -`undefined` - -### Replaced By - -[`keyman.focusLastActiveElement()`](../core/focusLastActiveElement) diff --git a/web/docs/engine/reference/compatibility/GetLastActiveElement.md b/web/docs/engine/reference/compatibility/GetLastActiveElement.md deleted file mode 100644 index 32ca21c1035..00000000000 --- a/web/docs/engine/reference/compatibility/GetLastActiveElement.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: GetLastActiveElement ---- - -## Summary - -Returns the last active target element before the current KMW operation. - -## Syntax - -``` -keyman.GetLastActiveElement(); -``` - -### Parameters - -None. - -### Return Value - -`Element` -: The requested element. - -### Replaced By - -[`keyman.getLastActiveElement()`](../core/getLastActiveElement) diff --git a/web/docs/engine/reference/compatibility/HideHelp.md b/web/docs/engine/reference/compatibility/HideHelp.md deleted file mode 100644 index 4c916c02a06..00000000000 --- a/web/docs/engine/reference/compatibility/HideHelp.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: HideHelp ---- - -## Summary - -Hides the OSK (visual help). - -## Syntax - -``` -keyman.HideHelp(); -``` - -### Parameters - -None. - -### Return Value - -`undefined` - -### Replaced By - -[`keyman.osk.hide()`](../osk/hide) diff --git a/web/docs/engine/reference/compatibility/ShowPinnedHelp.md b/web/docs/engine/reference/compatibility/ShowPinnedHelp.md deleted file mode 100644 index 43b3e40946b..00000000000 --- a/web/docs/engine/reference/compatibility/ShowPinnedHelp.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: ShowPinnedHelp ---- - -## Summary - -Shows the on-screen keyboard and locks it at the resulting location. - -## Syntax - -``` -keyman.ShowPinnedHelp(x, y); -``` - -### Parameters - -None. - -### Return Value - -`undefined` - -### Replaced by - -[`keyman.osk.setRect()`](../osk/setRect), -[`keyman.osk.show()`](../osk/show) diff --git a/web/docs/engine/reference/compatibility/index.md b/web/docs/engine/reference/compatibility/index.md deleted file mode 100644 index 98f4e37813e..00000000000 --- a/web/docs/engine/reference/compatibility/index.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: Compatibility Functions ---- - -The following *KeymanWeb* core functions -have been retained for compatibility with existing custom keyboards, but -should not be used in any new keyboards or user interfaces. Equivalent -new function calls are indicated. - -[`GetLastActiveElement` Function](GetLastActiveElement) -: [`keyman.getLastActiveElement()`](../core/getLastActiveElement) - - - -[`FocusLastActiveElement` Function](FocusLastActiveElement) -: [`keyman.focusLastActiveElement()`](../core/focusLastActiveElement) - - - -[`HideHelp` Function](HideHelp) -: [`keyman.osk.hide()`](../osk/hide) - - - -[`ShowHelp` Function](ShowHelp) -: [`keyman.osk.setPos()`](../osk/setPos), - [`keyman.osk.show()`](../osk/show) - - - -[`ShowPinnedHelp` Function](ShowPinnedHelp) -: [`keyman.osk.setRect()`](../osk/setRect), - [`keyman.osk.show()`](../osk/show) diff --git a/web/docs/engine/reference/index.md b/web/docs/engine/reference/index.md index f6e5a3a8def..1002a802e6c 100644 --- a/web/docs/engine/reference/index.md +++ b/web/docs/engine/reference/index.md @@ -17,9 +17,6 @@ title: KeymanWeb Reference [KeymanWeb Output Functions](interface) : KeymanWeb exposes its keyboard interface object through `window.keyman.interface`, providing a number of functions for low-level processing of input, context and output. -[KeymanWeb Compatibility Functions](compatibility) -: The following KeymanWeb core functions have been retained for compatibility with existing custom keyboards, but should not be used in any new keyboards or user interfaces. Equivalent new function calls are indicated. - [Desktop User Interfaces](ui) : Four different KeymanWeb user interfaces for desktop browsers are included, allowing users to select and enable keyboard mapping from a list of installed keyboards, and to control the visibility of the On-Screen Keyboard. diff --git a/web/docs/engine/reference/interface/FocusLastActiveElement.md b/web/docs/engine/reference/interface/FocusLastActiveElement.md new file mode 100644 index 00000000000..a06e8867810 --- /dev/null +++ b/web/docs/engine/reference/interface/FocusLastActiveElement.md @@ -0,0 +1,38 @@ +--- +title: FocusLastActiveElement (deprecated) +--- + +## Summary + +Restore the focus to the text store active before input was moved to KeymanWeb. + +Note that the name is misleading: this function move focus to the last active +`TextStore`, not the last active `HTMLElement` (in practice, this is equivalent +in a web browser context). + +## Syntax + +```js +keyman.interface.FocusLastActiveElement(); +keyman.interface.focusLastActiveElement(); +KeymanWeb.FocusLastActiveElement(); +KeymanWeb.focusLastActiveElement(); +``` + +### Parameters + +None. + +### Return Value + +`undefined` + +### Replaced By + +This function is deprecated; see instead: + +* [`keyman.interface.focusLastActiveTextStore()`](focusLastActiveTextStore) + +### See also + +* [`keyman.focusLastActiveElement()`](../core/focusLastActiveElement) diff --git a/web/docs/engine/reference/interface/GetLastActiveElement.md b/web/docs/engine/reference/interface/GetLastActiveElement.md new file mode 100644 index 00000000000..ee6eaf0e8d8 --- /dev/null +++ b/web/docs/engine/reference/interface/GetLastActiveElement.md @@ -0,0 +1,39 @@ +--- +title: GetLastActiveElement (deprecated) +--- + +## Summary + +Returns the last active target text store before the current KMW operation. + +Note that the name is misleading: this function returns the last active +`TextStore`, not the last active `HTMLElement`. Use instead +[`keyman.interface.getLastActiveTextStore()`](getLastActiveTextStore). + +## Syntax + +```js +keyman.interface.GetLastActiveElement(); +keyman.interface.getLastActiveElement(); +KeymanWeb.GetLastActiveElement(); +KeymanWeb.getLastActiveElement(); +``` + +### Parameters + +None. + +### Return Value + +`TextStore` +: The requested text store. + +### Replaced By + +This function is deprecated; see instead: + +* [`keyman.interface.getLastActiveTextStore()`](getLastActiveTextStore) + +### See also + +* [`keyman.getLastActiveElement()`](../core/getLastActiveElement) diff --git a/web/docs/engine/reference/interface/HideHelp.md b/web/docs/engine/reference/interface/HideHelp.md new file mode 100644 index 00000000000..f5f7a6c0a18 --- /dev/null +++ b/web/docs/engine/reference/interface/HideHelp.md @@ -0,0 +1,30 @@ +--- +title: HideHelp (deprecated) +--- + +## Summary + +Hides the OSK (visual help). + +## Syntax + +```js +keyman.interface.HideHelp(); +keyman.interface.hideHelp(); +KeymanWeb.HideHelp(); +KeymanWeb.hideHelp(); +``` + +### Parameters + +None. + +### Return Value + +`undefined` + +### Replaced By + +This function is deprecated; see instead: + +[`keyman.osk.hide()`](../osk/hide) diff --git a/web/docs/engine/reference/compatibility/ShowHelp.md b/web/docs/engine/reference/interface/ShowHelp.md similarity index 65% rename from web/docs/engine/reference/compatibility/ShowHelp.md rename to web/docs/engine/reference/interface/ShowHelp.md index 9647ec6b166..3ded765ff24 100644 --- a/web/docs/engine/reference/compatibility/ShowHelp.md +++ b/web/docs/engine/reference/interface/ShowHelp.md @@ -1,15 +1,18 @@ --- -title: ShowHelp +title: ShowHelp (deprecated) --- - + ## Summary Shows the on-screen keyboard at the requested (x, y) coordinates. ## Syntax -``` -keyman.ShowHelp(x, y); +```js +keyman.interface.ShowHelp(x, y); +keyman.interface.showHelp(x, y); +KeymanWeb.ShowHelp(x, y); +KeymanWeb.showHelp(x, y); ``` ### Parameters @@ -28,5 +31,7 @@ keyman.ShowHelp(x, y); ### Replaced by +This function is deprecated; see instead: + [`keyman.osk.setPos()`](../osk/setPos), [`keyman.osk.show()`](../osk/show) diff --git a/web/docs/engine/reference/interface/ShowPinnedHelp.md b/web/docs/engine/reference/interface/ShowPinnedHelp.md new file mode 100644 index 00000000000..7a03d2dc91f --- /dev/null +++ b/web/docs/engine/reference/interface/ShowPinnedHelp.md @@ -0,0 +1,31 @@ +--- +title: ShowPinnedHelp (deprecated) +--- + +## Summary + +Shows the on-screen keyboard and locks it at the resulting location. + +## Syntax + +```js +keyman.interface.ShowPinnedHelp(x, y); +keyman.interface.showPinnedHelp(x, y); +KeymanWeb.ShowPinnedHelp(x, y); +KeymanWeb.showPinnedHelp(x, y); +``` + +### Parameters + +None. + +### Return Value + +`undefined` + +### Replaced by + +This function is deprecated; see instead: + +* [`keyman.osk.setRect()`](../osk/setRect), +* [`keyman.osk.show()`](../osk/show) diff --git a/web/docs/engine/reference/interface/focusLastActiveTextStore.md b/web/docs/engine/reference/interface/focusLastActiveTextStore.md new file mode 100644 index 00000000000..d0ab7127db7 --- /dev/null +++ b/web/docs/engine/reference/interface/focusLastActiveTextStore.md @@ -0,0 +1,24 @@ +--- +title: focusLastActiveTextStore +--- + +## Summary + +Set focus to the currently-focused or most recently focused text store. This is +for use, for example, by IME keyboards to return the focus to the text store +after the user clicks on an IME element, without resetting context. + +## Syntax + +```js +keyman.interface.focusLastActiveTextStore(); +KeymanWeb.focusLastActiveTextStore(); +``` + +### Parameters + +None. + +### Return Value + +`undefined` diff --git a/web/docs/engine/reference/interface/getLastActiveTextStore.md b/web/docs/engine/reference/interface/getLastActiveTextStore.md new file mode 100644 index 00000000000..7a0ab7a9b39 --- /dev/null +++ b/web/docs/engine/reference/interface/getLastActiveTextStore.md @@ -0,0 +1,26 @@ +--- +title: getLastActiveTextStore +--- + +## Summary + +Get the currently or most recently focused text store. This is for use by IME +keyboards, to allow for more complex focus interactions when clicking IME +elements. + +## Syntax + +```js +keyman.interface.getLastActiveTextStore(); +KeymanWeb.getLastActiveTextStore(); +``` + +### Parameters + +None. + +### Return Value + +`TextStore` +: The last active text store. + diff --git a/web/docs/engine/reference/interface/index.md b/web/docs/engine/reference/interface/index.md index d257b7e3e33..10e412d3451 100644 --- a/web/docs/engine/reference/interface/index.md +++ b/web/docs/engine/reference/interface/index.md @@ -42,10 +42,16 @@ Custom user interfaces would not normally use these functions, but they are desc : Context deletion - removes the specified number of deadkeys and characters from the left of the caret. : Shorthand name: `KeymanWeb.KDC` +[`focusLastActiveTextStore` Function](focusLastActiveTextStore) +: Set focus to the currently-focused or most recently focused text store. + [`fullContextMatch` Function](fullContextMatch) : Context matching: Returns `true` if the current context matches the specified rule context specification. : Shorthand name: `KeymanWeb.KFCM` +[`getLastActiveTextStore` Function](getLastActiveTextStore) +: Get the currently or most recently focused text store. This is for use by IME keyboards. + [`ifStore` Function](ifStore) : `ifStore` compares the content of a [system `store`](/developer/language/guide/stores#toc-system-stores) with a string value. : Shorthand name: `KeymanWeb.KIFS` @@ -105,3 +111,25 @@ Custom user interfaces would not normally use these functions, but they are desc [`stateMatch` Function](stateMatch) : State-key matching: Returns `true` if the event matches the rule's state-key requirements. : Shorthand name: `KeymanWeb.KSM` + +## Deprecated functions for IMEs + +The following functions have been retained for compatibility with existing IME +keyboards, but should not be used in any new keyboards or user interfaces. + +[`GetLastActiveElement` or `getLastActiveElement` Function (Deprecated)](GetLastActiveElement) +: Use [`keyman.interface.getLastActiveTextStore()`](getLastActiveTextStore) + +[`FocusLastActiveElement` or `focusLastActiveElement` Function (Deprecated)](FocusLastActiveElement) +: Use [`keyman.interface.focusLastActiveTextStore()`](focusLastActiveTextStore) + +[`HideHelp` or `hideHelp` Function (Deprecated)](HideHelp) +: Use [`keyman.osk.hide()`](../osk/hide) + +[`ShowHelp` or `showHelp` Function (Deprecated)](ShowHelp) +: Use [`keyman.osk.setPos()`](../osk/setPos) + Use [`keyman.osk.show()`](../osk/show) + +[`ShowPinnedHelp` or `showPinnedHelp` Function (Deprecated)](ShowPinnedHelp) +: Use [`keyman.osk.setRect()`](../osk/setRect) +: Use [`keyman.osk.show()`](../osk/show) diff --git a/web/src/app/browser/src/keyboardInterface.ts b/web/src/app/browser/src/keyboardInterface.ts index 4da0579a007..e66fc70565c 100644 --- a/web/src/app/browser/src/keyboardInterface.ts +++ b/web/src/app/browser/src/keyboardInterface.ts @@ -26,23 +26,27 @@ export class KeyboardInterface extends KeyboardInterfaceBase { this.engine.contextManager.focusAssistant._IgnoreNextSelChange = 1; } - /** - * Legacy entry points (non-standard names)- included only to allow existing IME keyboards to continue to be used - */ - getLastActiveElement(): AbstractElementTextStore { + getLastActiveTextStore(): AbstractElementTextStore { return this.engine.contextManager.lastActiveTextStore; } - focusLastActiveElement(): void { + focusLastActiveTextStore(): void { this.engine.contextManager.restoreLastActiveTextStore(); } //The following entry points are defined but should not normally be used in a keyboard, as OSK display is no longer determined by the keyboard + + /** + * @deprecated + */ hideHelp(): void { const osk = this.engine.osk; osk.startHide(true); } + /** + * @deprecated + */ showHelp(Px: number, Py: number): void { const osk = this.engine.osk; @@ -53,6 +57,9 @@ export class KeyboardInterface extends KeyboardInterfaceBase { } } + /** + * @deprecated + */ showPinnedHelp(): void { const osk = this.engine.osk; @@ -74,13 +81,6 @@ export class KeyboardInterface extends KeyboardInterfaceBase { // pinned position. osk.present(); } - - // Also needed for some legacy CJK keyboards. - readonly GetLastActiveElement = this.getLastActiveElement; - readonly FocusLastActiveElement = this.focusLastActiveElement; - readonly HideHelp = this.hideHelp; - readonly ShowHelp = this.showHelp; - readonly ShowPinnedHelp = this.showPinnedHelp; } (function() { diff --git a/web/src/app/browser/src/keymanEngine.ts b/web/src/app/browser/src/keymanEngine.ts index af3bae2d207..0b3d54aebb6 100644 --- a/web/src/app/browser/src/keymanEngine.ts +++ b/web/src/app/browser/src/keymanEngine.ts @@ -543,7 +543,7 @@ export class KeymanEngine extends KeymanEngineBase> extends JSKeyboardInterface { protected readonly engine: KeymanEngineBase; @@ -118,6 +119,92 @@ export class KeyboardInterfaceBase Date: Wed, 1 Jul 2026 08:06:26 +0200 Subject: [PATCH 09/44] docs(web): cleanup of index pages and page titles Fixes: #16169 Test-bot: skip --- web/docs/engine/reference/core/index.md | 2 +- web/docs/engine/reference/events/index.md | 4 +- web/docs/engine/reference/index.md | 41 ++++++------ web/docs/engine/reference/interface/index.md | 68 ++++++++++++-------- web/docs/engine/reference/layouts.md | 7 -- web/docs/engine/reference/osk/index.md | 2 +- web/docs/engine/reference/overview.md | 12 +++- web/docs/engine/reference/ui/index.md | 4 +- web/docs/engine/reference/util/index.md | 2 +- 9 files changed, 80 insertions(+), 62 deletions(-) delete mode 100644 web/docs/engine/reference/layouts.md diff --git a/web/docs/engine/reference/core/index.md b/web/docs/engine/reference/core/index.md index 8ce45877fde..ff474d539bf 100644 --- a/web/docs/engine/reference/core/index.md +++ b/web/docs/engine/reference/core/index.md @@ -1,5 +1,5 @@ --- -title: Core Module +title: `keyman` - Core Module --- The KeymanWeb core module is exposed to the developer as `window.keyman`. diff --git a/web/docs/engine/reference/events/index.md b/web/docs/engine/reference/events/index.md index df7538c2c19..962c09d795f 100644 --- a/web/docs/engine/reference/events/index.md +++ b/web/docs/engine/reference/events/index.md @@ -7,8 +7,8 @@ interface to control the appearance and behavior of user interface elements. Standard event-processing requires all arguments to be passed as an array (object) with named member variables. -Two components of Keyman Engine for Web specify events: -* `keyman` object -- the main component +Two components of Keyman Engine for Web specify events: +* `keyman` object -- the core component * `keyman.osk` object -- the on-screen keyboard component Object events are handled in user code by passing the handler entry to diff --git a/web/docs/engine/reference/index.md b/web/docs/engine/reference/index.md index 1002a802e6c..2445ec292a2 100644 --- a/web/docs/engine/reference/index.md +++ b/web/docs/engine/reference/index.md @@ -5,35 +5,38 @@ title: KeymanWeb Reference [KeymanWeb Overview](overview) : KeymanWeb is a cross-browser JavaScript input method solution. -[KeymanWeb Core Module](core) -: The KeymanWeb core module is exposed to the developer as `window.keyman`. +# API Reference -[On-Screen Keyboard Module](osk) -: The On-Screen Keyboard module is exposed to the developer as `window.keyman.osk`. +[`keyman`](core) +: Core APIs for KeymanWeb, including initialization, adding keyboards, focus and element control. -[KeymanWeb Utility Module](util) -: The KeymanWeb Utility Function module is exposed to the developer as `window.keyman.util`. +[`keyman.osk`](osk) +: On-Screen Keyboard module. -[KeymanWeb Output Functions](interface) -: KeymanWeb exposes its keyboard interface object through `window.keyman.interface`, providing a number of functions for low-level processing of input, context and output. +[`keyman.util`](util) +: Utility functions. -[Desktop User Interfaces](ui) +[`keyman.interface` (also `KeymanWeb`)](interface) +: API surface for keyboards, providing a number of functions for low-level processing of input, context and output. + +[`keyman.ui` - Desktop User Interfaces](ui) : Four different KeymanWeb user interfaces for desktop browsers are included, allowing users to select and enable keyboard mapping from a list of installed keyboards, and to control the visibility of the On-Screen Keyboard. -[KeymanWeb Initialization Options](core/init#init_options) -: The following options can be defined by the site designer in the initialization call to KeymanWeb. +# API Guides -[KeymanWeb Events](events) -: A number of KeymanWeb events are exposed to allow the designer of a user interface to control the appearance and behavior of user interface elements. Standard event-processing requires all arguments to be passed as an array (object) with named member variables. +[Initialization Options](core/init#init_options) +: The following options can be defined by the site designer in the initialization call to KeymanWeb. -[KeymanWeb Keyboard Properties](keyboard_properties) -: Most keyboards are generated automatically from the Keyman keyboard source by Keyman Developer and contain properties used by KeymanWeb during keyboard mapping. +[Events](events) +: A number of KeymanWeb events are exposed to allow the designer of a user interface to control the appearance and behavior of user interface elements. -[KeymanWeb Layout Designer](layouts) -: One of the main features of KeymanWeb is its ability to support distinct, user-customizable layouts for touch-screen keyboards on phones and tablets. +[Keyboard Properties](keyboard_properties) +: Standard properties for a Keyboard object returned from a keyboard .js file. -[KeymanWeb Layout Specifications](layoutspec) +[Touch Layout Specifications](layoutspec) : Touch-screen layouts for KeymanWeb are specified as JSON objects containing a member object for each specified device type. -[KeymanWeb Keyboard Development Reference](/developer/language/guide/multi-platform) +# See Also + +[Keyboard Development Reference](/developer/language/guide/multi-platform) : Information on how to explicitly support KeymanWeb in custom keyboards. diff --git a/web/docs/engine/reference/interface/index.md b/web/docs/engine/reference/interface/index.md index 10e412d3451..fc32fa9cdde 100644 --- a/web/docs/engine/reference/interface/index.md +++ b/web/docs/engine/reference/interface/index.md @@ -1,116 +1,128 @@ --- -title: Output Functions - the Keyboard API +title: `keyman.interface` - the Keyboard API --- -The KeymanWeb core object `window.keyman.interface` (with legacy-oriented, deprecated alias `KeymanWeb`) exposes a number of functions for low-level processing of input, context and output. These functions are designed for use by keyboards compiled through Keyman Developer in order to facilitate input text processing and will also work for custom-coded KeymanWeb keyboards. As such, most of these functions should only be called by keyboard code, and a good understanding of the [Keyman Keyboard Language](/developer/language) will prove extremely beneficial toward understanding the keyboard API functions enumerated in this section. +The KeymanWeb core object `window.keyman.interface` exposes a number of +functions for low-level processing of input, context and output. These functions +are designed for use by keyboards compiled through Keyman Developer in order to +facilitate input text processing and will also work for custom-coded KeymanWeb +keyboards. These functions should only be called by keyboard code, and a good +understanding of the [Keyman Keyboard Language](/developer/language) will prove +extremely beneficial toward understanding the keyboard API functions enumerated +in this section. -Custom user interfaces would not normally use these functions, but they are described here as some custom keyboards, such as IME-style keyboards, may need to interact with the user interface. +The KeymanWeb Keyboard API publishes a deprecated global `KeymanWeb` which is +the same as `keyman.interface`. Use `keyman.interface` in preference to +`KeymanWeb`. + +Custom user interfaces should not use these functions. These functions are intended +for use in a keyboard context, in particular during a keystroke event cycle. [`any` Function (Deprecated)](any) : Returns whether or not the char `ch` is found within the [`any`](/developer/language/reference/any)([`store`](/developer/language/reference/store)) string, setting an internally-tracked index for use in the `indexOutput` function. -: Shorthand name: `KeymanWeb.KA` +: Shorthand name: `keyman.interface.KA` [`beep` Function](beep) : Flash body or element as substitute for an audible feedback [`beep`](/developer/language/reference/beep). -: Shorthand name: `KeymanWeb.KB` +: Shorthand name: `keyman.interface.KB` [`beepReset` Function](beepReset) : Cancels a previous feedback [`beep`](/developer/language/reference/beep) operation on a page element. -: Shorthand name: `KeymanWeb.KBR` +: Shorthand name: `keyman.interface.KBR` [`context` Function (Deprecated)](context) : Gets [`context`](/developer/language/reference/context) for an ongoing keyboard operation relative to the caret's present position. -: Shorthand name: `KeymanWeb.KC` +: Shorthand name: `keyman.interface.KC` [`contextExOutput` Function](contextExOutput) : Emits the character or object at `contextOffset` from the current matched rule's context. -: Shorthand name: `KeymanWeb.KCXO` +: Shorthand name: `keyman.interface.KCXO` [`contextMatch` Function (Deprecated)](contextMatch) : Context matching: Returns `true` if the specified `context` call matches a provided string. -: Shorthand name: `KeymanWeb.KCM` +: Shorthand name: `keyman.interface.KCM` [`deadkeyMatch` Function (Deprecated)](deadkeyMatch) : Deadkey matching: Seeks to match the [`deadkey`](/developer/language/reference/deadkey) state `dk` at the relative caret position `n`. -: Shorthand name: `KeymanWeb.KDM` +: Shorthand name: `keyman.interface.KDM` [`deadkeyOutput` Function](deadkeyOutput) : Deadkey output: Associates the [`deadkey`](/developer/language/reference/deadkey) state `dk` with the element at the current caret position, after overwriting `nd` characters. -: Shorthand name: `KeymanWeb.KDO` +: Shorthand name: `keyman.interface.KDO` [`deleteContext` Function](deleteContext) : Context deletion - removes the specified number of deadkeys and characters from the left of the caret. -: Shorthand name: `KeymanWeb.KDC` +: Shorthand name: `keyman.interface.KDC` [`focusLastActiveTextStore` Function](focusLastActiveTextStore) : Set focus to the currently-focused or most recently focused text store. [`fullContextMatch` Function](fullContextMatch) : Context matching: Returns `true` if the current context matches the specified rule context specification. -: Shorthand name: `KeymanWeb.KFCM` +: Shorthand name: `keyman.interface.KFCM` [`getLastActiveTextStore` Function](getLastActiveTextStore) : Get the currently or most recently focused text store. This is for use by IME keyboards. [`ifStore` Function](ifStore) : `ifStore` compares the content of a [system `store`](/developer/language/guide/stores#toc-system-stores) with a string value. -: Shorthand name: `KeymanWeb.KIFS` +: Shorthand name: `keyman.interface.KIFS` [`indexOutput` Function](indexOutput) : Index-based output: Outputs a mapped character according to a previous selection from a `keyman.interface.any()` call upon a [`store`](/developer/language/reference/store) string, after deleting `nd` characters. -: Shorthand name: `KeymanWeb.KIO` +: Shorthand name: `keyman.interface.KIO` [`insertText` Function](insertText) : Inserts a text string and optional [`deadkey`](/developer/language/reference/deadkey) into the active output element. -: Shorthand name: `KeymanWeb.KT` +: Shorthand name: `keyman.interface.KT` [`isKeypress` Function](isKeypress) : Returns `true` if the input event corresponds to a keypress event resulting in character output. -: Shorthand name: `KeymanWeb.KIK` +: Shorthand name: `keyman.interface.KIK` [`keyInformation` Function](keyInformation) : Returns an object with extended information about a specified keystroke event. -: Shorthand name: `KeymanWeb.KKI` +: Shorthand name: `keyman.interface.KKI` [`keyMatch` Function](keyMatch) : Keystroke matching: Returns `true` if the event matches the rule's shift mask and key code. -: Shorthand name: `KeymanWeb.KKM` +: Shorthand name: `keyman.interface.KKM` [`loadStore` Function](loadStore) : Load an option [`store`](/developer/language/guide/stores) value from a cookie or default value if no prior stored value exists. -: Shorthand name: `KeymanWeb.KLOAD` +: Shorthand name: `keyman.interface.KLOAD` [`nul` Function](_nul) : [`nul`](/developer/language/reference/nul) context check: Returns `true` if the length of the [`context`](/developer/language/reference/context) is less than or equal to `n` characters. -: Shorthand name: `KeymanWeb.KN` +: Shorthand name: `keyman.interface.KN` [`output` Function](output) : Outputs the specified string to an element, overwriting characters before the caret if specified. -: Shorthand name: `KeymanWeb.KO` +: Shorthand name: `keyman.interface.KO` [`registerKeyboard` Function](registerKeyboard) : Register the keyboard stub and load the keyboard. -: Shorthand name: `KeymanWeb.KR` +: Shorthand name: `keyman.interface.KR` [`registerStub` Function](registerStub) : Register the keyboard stub, return true if already registered. -: Shorthand name: `KeymanWeb.KRS` +: Shorthand name: `keyman.interface.KRS` [`saveFocus` Function](saveFocus) : Save focus: Temporarily saves keyboard processing data for the currently-focused control. -: Shorthand name: `KeymanWeb.KSF` +: Shorthand name: `keyman.interface.KSF` [`saveStore` Function](saveStore) : Save an option [`store`](/developer/language/guide/stores) value to a cookie for the active keyboard. -: Shorthand name: `KeymanWeb.KSAVE` +: Shorthand name: `keyman.interface.KSAVE` [`setStore` Function](setStore) : `setStore` sets the value of a [system `store`](/developer/language/guide/stores#toc-system-stores) to a string. -: Shorthand name: `KeymanWeb.KSETS` +: Shorthand name: `keyman.interface.KSETS` [`stateMatch` Function](stateMatch) : State-key matching: Returns `true` if the event matches the rule's state-key requirements. -: Shorthand name: `KeymanWeb.KSM` +: Shorthand name: `keyman.interface.KSM` ## Deprecated functions for IMEs diff --git a/web/docs/engine/reference/layouts.md b/web/docs/engine/reference/layouts.md deleted file mode 100644 index 56d1e40d625..00000000000 --- a/web/docs/engine/reference/layouts.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Layout Designer ---- - -One of the main features of *KeymanWeb* is its ability to support distinct, user-customizable -layouts for touch-screen keyboards on phones and tablets. *Keyman Developer* includes a layout -designer to simplify the process of creating custom layouts for any keyboard. diff --git a/web/docs/engine/reference/osk/index.md b/web/docs/engine/reference/osk/index.md index 999a25c504c..d95aeaa5d0d 100644 --- a/web/docs/engine/reference/osk/index.md +++ b/web/docs/engine/reference/osk/index.md @@ -1,5 +1,5 @@ --- -title: On-Screen Keyboard Module +title: `keyman.osk` - On-Screen Keyboard Module --- The On-Screen Keyboard module is exposed to the developer as `window.keyman.osk`. diff --git a/web/docs/engine/reference/overview.md b/web/docs/engine/reference/overview.md index 712a20640bf..4fef7b48af4 100644 --- a/web/docs/engine/reference/overview.md +++ b/web/docs/engine/reference/overview.md @@ -11,7 +11,7 @@ the *On-Screen Keyboard* module, a *Utility function* library, or one of the sta Interface* modules. A *KeymanWeb* instance is automatically constructed when you include the compiled KeymanWeb -script (kmw-release.js) in your web page source. +script in your web page source. The *KeymanWeb API* comprises the following objects: @@ -26,3 +26,13 @@ The *KeymanWeb API* comprises the following objects: - *User Interface* : Exposed as [`keyman.ui`](ui). + +The *KeymanWeb Keyboard API* provides endpoints for KeymanWeb keyboards to +interact with KeymanWeb. This API should not be used by website code. + +- *Interface* +: Exposed as [`keyman.interface`](interface) + +The KeymanWeb Keyboard API publishes a deprecated global `KeymanWeb` which is +the same as `keyman.interface`. Use `keyman.interface` in preference to +`KeymanWeb`. \ No newline at end of file diff --git a/web/docs/engine/reference/ui/index.md b/web/docs/engine/reference/ui/index.md index 74788baf516..53a555996d5 100644 --- a/web/docs/engine/reference/ui/index.md +++ b/web/docs/engine/reference/ui/index.md @@ -1,7 +1,7 @@ --- -title: Desktop User Interfaces +title: `keyman.ui` - Desktop User Interfaces --- - + Four different KeymanWeb user interfaces for desktop browsers are included, allowing users to select and enable keyboard mapping from a list of installed keyboards, and to control the visibility of the diff --git a/web/docs/engine/reference/util/index.md b/web/docs/engine/reference/util/index.md index f599e3d5d6c..eb860adaa01 100644 --- a/web/docs/engine/reference/util/index.md +++ b/web/docs/engine/reference/util/index.md @@ -1,5 +1,5 @@ --- -title: Utility Module +title: `keyman.util` - Utility Module --- The KeymanWeb Utility Function module is exposed to the developer as `window.keyman.util`. From e6d86d7e042472f4a9cbfdb9bcbd071b991bd17c Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Wed, 1 Jul 2026 09:05:18 +0200 Subject: [PATCH 10/44] refactor(web): rename `keymanweb` to `keyman` in UI modules The variable name `keymanweb` used in the UI modules is confusing because `KeymanWeb` also exists and points to something else. Rename to `keyman` which just mirrors `window.keyman`. Also fixup type declaration for toolbar `lastDismissalCallback`, which did not match the assigned type. Fixes: #16168 Test-bot: skip --- web/src/app/ui/kmwuibutton.ts | 66 ++++++++++++++-------------- web/src/app/ui/kmwuifloat.ts | 80 +++++++++++++++++----------------- web/src/app/ui/kmwuitoggle.ts | 80 +++++++++++++++++----------------- web/src/app/ui/kmwuitoolbar.ts | 66 ++++++++++++++-------------- 4 files changed, 146 insertions(+), 146 deletions(-) diff --git a/web/src/app/ui/kmwuibutton.ts b/web/src/app/ui/kmwuibutton.ts index 16377d4f3da..d375dae5cab 100644 --- a/web/src/app/ui/kmwuibutton.ts +++ b/web/src/app/ui/kmwuibutton.ts @@ -11,14 +11,14 @@ declare global { } } -const keymanweb=window.keyman; +const keyman=window.keyman; // If a UI module has been loaded, we can rely on the publically-published 'name' property // having been set as a way to short-out a UI reload. Its parent object always exists by // this point in the build process. -if(!keymanweb) { +if(!keyman) { throw new Error("`keyman` global is missing; Keyman Engine for Web script has not been loaded"); -} else if(!keymanweb.ui?.name) { +} else if(!keyman.ui?.name) { /********************************/ /* */ @@ -37,8 +37,8 @@ if(!keymanweb) { try { // Declare KeymanWeb, OnScreen keyboard and Util objects - const util=keymanweb.util; - // var dbg=keymanweb['debug']; + const util=keyman.util; + // var dbg=keyman['debug']; // Disable UI for touch devices if(util.isTouchDevice()) { @@ -78,8 +78,8 @@ if(!keymanweb) { * Highlight the currently active keyboard in the list of keyboards **/ private _ShowSelected() { - const kbd=keymanweb.getActiveKeyboard(); - const lgc=keymanweb.getActiveLanguage(); + const kbd=keyman.getActiveKeyboard(); + const lgc=keyman.getActiveLanguage(); const kList = this._KeymanWeb_KbdList.childNodes; const _r = /^KMWSel_(.*)\$(.*)$/; @@ -135,13 +135,13 @@ if(!keymanweb) { _k.className='selected'; } this._KMWSel = _k; - await keymanweb.setActiveKeyboard(_name,_lgc); + await keyman.setActiveKeyboard(_name,_lgc); } else { _name=null; } - keymanweb.focusLastActiveElement(); - const osk = keymanweb.osk; + keyman.focusLastActiveElement(); + const osk = keyman.osk; if(osk && osk.isEnabled()) { osk.show(true); } @@ -156,17 +156,17 @@ if(!keymanweb) { * @param {Event} e event */ readonly _SelectorMouseDown = (e: MouseEvent) => { - const x=keymanweb.getLastActiveElement(); + const x=keyman.getLastActiveElement(); // Set the focus to an input field, to get correct OSK display behaviour if(!x) { this._FocusFirstInput(); } else { - keymanweb.focusLastActiveElement(); + keyman.focusLastActiveElement(); } - if(keymanweb.activatingUI) { - keymanweb.activatingUI(1); + if(keyman.activatingUI) { + keyman.activatingUI(1); } } @@ -176,13 +176,13 @@ if(!keymanweb) { * @param {Event} e event */ readonly _SelectorMouseUp = (e: MouseEvent) => { - const x=keymanweb.getLastActiveElement(); + const x=keyman.getLastActiveElement(); // Set the focus to an input field, to get correct OSK display behaviour if(!x) { this._FocusFirstInput(); } else { - keymanweb.focusLastActiveElement(); + keyman.focusLastActiveElement(); } } @@ -195,8 +195,8 @@ if(!keymanweb) { // highlight the currently active keyboard this._ShowSelected(); - if(keymanweb.activatingUI) { - keymanweb.activatingUI(1); + if(keyman.activatingUI) { + keyman.activatingUI(1); } document.getElementById("kmwico_li").className="sfhover"; @@ -253,8 +253,8 @@ if(!keymanweb) { * @param {Event} e event */ private readonly _SelectorMouseOut = (e: MouseEvent) => { - if(keymanweb.activatingUI) { - keymanweb.activatingUI(0); + if(keyman.activatingUI) { + keyman.activatingUI(0); } document.getElementById("kmwico_li").className="sfunhover"; } @@ -265,24 +265,24 @@ if(!keymanweb) { * @param {?string=} _name current keyboard name */ private _ShowKeyboardButton(_name?: string) { - let kbdName = keymanweb.getActiveKeyboard(); + let kbdName = keyman.getActiveKeyboard(); const kbdId = document.getElementById("KMW_Keyboard"); if(arguments.length > 0) { kbdName = _name; } if(kbdId) { - if((kbdName == '') || keymanweb.isCJK()) { + if((kbdName == '') || keyman.isCJK()) { kbdId.className='kmw_disabled'; } else { - const osk = keymanweb.osk; + const osk = keyman.osk; kbdId.className = osk && osk.isEnabled() ? 'kmw_show' : 'kmw_hide'; } } } registerEvents() { - const osk = keymanweb.osk; + const osk = keyman.osk; if(!osk) { return; } @@ -290,7 +290,7 @@ if(!keymanweb) { * UI Functions called by KeymanWeb or OSK */ osk.addEventListener('show', (oskPosition) => { - const t=keymanweb.getLastActiveElement(); + const t=keyman.getLastActiveElement(); if(t) { if(!oskPosition['userLocated']) { oskPosition['x'] = util.getAbsoluteX(t); @@ -321,7 +321,7 @@ if(!keymanweb) { **/ readonly _ShowKeymanWebKeyboard = () => { const kbdId=document.getElementById("KMW_Keyboard"); - const osk = keymanweb.osk; + const osk = keyman.osk; if((kbdId.className!='kmw_disabled') && osk && osk.show) { if(osk.isEnabled()) { @@ -334,7 +334,7 @@ if(!keymanweb) { window.event.returnValue=false; } - keymanweb.focusLastActiveElement(); + keyman.focusLastActiveElement(); return false; } @@ -348,7 +348,7 @@ if(!keymanweb) { } //Never initialize UI before KMW (parameters will be undefined) - if(!keymanweb.initialized) { + if(!keyman.initialized) { this.initTimer = window.setTimeout(this.initialize, 50); return; } @@ -417,7 +417,7 @@ if(!keymanweb) { // Even tag `release-web-stable-2.0` turns up results only for this specific sourcefile. // Thus, in essence: if(true) { /* ... */ } // @ts-ignore - if(!keymanweb['iOS']) { + if(!keyman['iOS']) { const _li = util.createElement('li'); const _a = util.createElement('a'); const _img = util.createElement('img'); @@ -468,7 +468,7 @@ if(!keymanweb) { util.attachDOMEvent(_sfEl,'mouseup',this._SelectorMouseUp); this.registerEvents(); - keymanweb.focusLastActiveElement(); //TODO: this needs to be extended - if no element is active, try and identify an enabled input element + keyman.focusLastActiveElement(); //TODO: this needs to be extended - if no element is active, try and identify an enabled input element } shutdown() { @@ -493,7 +493,7 @@ if(!keymanweb) { this._KeymanWeb_KbdList.removeChild(this._KeymanWeb_KbdList.childNodes[i-1]); } - const kbds=keymanweb.getKeyboards(); + const kbds=keyman.getKeyboards(); if(kbds.length > 0) { for(let i:number=0; i { - keymanweb.activatingUI(true); - const osk = keymanweb.osk; + keyman.activatingUI(true); + const osk = keyman.osk; if(osk && osk.show) { if(osk.isEnabled()) { osk.hide(); @@ -99,8 +99,8 @@ if(!keymanweb) { if(window.event) { window.event.returnValue=false; } - keymanweb.focusLastActiveElement(); - keymanweb.activatingUI(false); + keyman.focusLastActiveElement(); + keyman.activatingUI(false); return false; } @@ -116,7 +116,7 @@ if(!keymanweb) { } // Must always initialize after keymanWeb itself, otherwise options are undefined - if(!keymanweb.initialized) { + if(!keyman.initialized) { this.initTimer = window.setTimeout(this.initialize, 50); return; } @@ -239,7 +239,7 @@ if(!keymanweb) { } // Loop over installed keyboards and add to selection list - const Lkbds=keymanweb.getKeyboards(); + const Lkbds=keyman.getKeyboards(); for(let Ln=0; Ln { + keyman.addEventListener('keyboardregistered', (p) => { this.updateList = true; if(this.updateTimer) { clearTimeout(this.updateTimer); @@ -371,8 +371,8 @@ if(!keymanweb) { * Note: Cannot simply set it to the loaded keyboard, * as more than one language may be supported by that keyboard. */ - keymanweb.addEventListener('keyboardloaded', (p) => { - const sk = keymanweb.getSavedKeyboard().split(':'); + keyman.addEventListener('keyboardloaded', (p) => { + const sk = keyman.getSavedKeyboard().split(':'); if(sk.length > 1) { this.updateMenu(sk[0],sk[1]); } @@ -383,7 +383,7 @@ if(!keymanweb) { * * Update menu selection and control OSK display appropriately */ - keymanweb.addEventListener('keyboardchange', (p) => { + keyman.addEventListener('keyboardchange', (p) => { // Update the keyboard selector whenever a keyboard is loaded this.updateMenu(p.internalName, p.languageCode); @@ -391,7 +391,7 @@ if(!keymanweb) { this.addButtonOSK(); }); - const osk = keymanweb.osk; + const osk = keyman.osk; if(!osk) { return; } @@ -422,8 +422,8 @@ if(!keymanweb) { * Description Set KMW UI activation state on mouse click */ readonly _SelectorMouseDown = (e: MouseEvent) => { - if(keymanweb.activatingUI) { - keymanweb.activatingUI(1); + if(keyman.activatingUI) { + keyman.activatingUI(1); } } @@ -434,8 +434,8 @@ if(!keymanweb) { * Description Set KMW UI activation state on mouse over */ readonly _SelectorMouseOver = (e: MouseEvent) => { - if(keymanweb.activatingUI) { - keymanweb.activatingUI(1); + if(keyman.activatingUI) { + keyman.activatingUI(1); } } @@ -446,8 +446,8 @@ if(!keymanweb) { * Description Clear KMW UI activation state on mouse out */ readonly _SelectorMouseOut = (e: MouseEvent) => { - if(keymanweb.activatingUI) { - keymanweb.activatingUI(0); + if(keyman.activatingUI) { + keyman.activatingUI(0); } } @@ -458,19 +458,19 @@ if(!keymanweb) { * Description Change active keyboard in response to user selection event */ private readonly SelectKeyboardChange = async (e: Event): Promise => { - keymanweb.activatingUI(true); + keyman.activatingUI(true); if(this.KeyboardSelector.value != '-') { const i=this.KeyboardSelector.selectedIndex; const t=this.KeyboardSelector.options[i].value.split(':'); - await keymanweb.setActiveKeyboard(t[0],t[1]); + await keyman.setActiveKeyboard(t[0],t[1]); } else { - await keymanweb.setActiveKeyboard(''); + await keyman.setActiveKeyboard(''); } //if(osk['show']) osk['show'](osk['isEnabled']()); handled by keyboard change event??? - keymanweb.focusLastActiveElement(); - keymanweb.activatingUI(false); + keyman.focusLastActiveElement(); + keyman.activatingUI(false); this.selecting = true; } @@ -482,7 +482,7 @@ if(!keymanweb) { */ readonly SelectBlur = (e: Event) => { if(!this.selecting) { - keymanweb.focusLastActiveElement(); + keyman.focusLastActiveElement(); } this.selecting = false; } @@ -511,7 +511,7 @@ if(!keymanweb) { this.addButtonOSK(); // Set the language selection to the currently active keyboard, if listed - this.updateMenu(keymanweb.getActiveKeyboard(), keymanweb.getActiveLanguage()); + this.updateMenu(keyman.getActiveKeyboard(), keyman.getActiveLanguage()); } /** @@ -534,12 +534,12 @@ if(!keymanweb) { */ readonly addButtonOSK = () => { if(this.oskButton != null) { - if(keymanweb.isCJK() || (this.KeyboardSelector.selectedIndex==0)) { + if(keyman.isCJK() || (this.KeyboardSelector.selectedIndex==0)) { this.oskButton.style.display = 'none'; this.outerDiv.style.width = this.KeyboardSelector.offsetWidth+30+'px'; } else { this.oskButton.style.display = 'block'; - const osk = keymanweb.osk; + const osk = keyman.osk; if(osk) { this.oskButtonState(osk.isEnabled()); } else { @@ -559,7 +559,7 @@ if(!keymanweb) { */ readonly _Resize = (e: Event) => { if(this.outerDiv.style.display =='block') { - const elem = keymanweb.getLastActiveElement(); + const elem = keyman.getLastActiveElement(); if(this.floatRight) { // I1296 this.ShowInterface(util.getAbsoluteX(elem) + elem.offsetWidth + 1, util.getAbsoluteY(elem) + 1); } else { @@ -570,13 +570,13 @@ if(!keymanweb) { } } - const ui=keymanweb.ui = new UIFloat(); + const ui=keyman.ui = new UIFloat(); //TODO: had to expose properties of params - what does that do? (focus event doesn't normally include these properties?) - keymanweb.addEventListener('controlfocused', (params) => { + keyman.addEventListener('controlfocused', (params) => { // ... this check shouldn't need to check _kmwAttachment directly. if(params.activeControl == null || params.activeControl['_kmwAttachment']) { - /*if(keymanweb.domManager._IsEditableIframe(Ltarg)) + /*if(keyman.domManager._IsEditableIframe(Ltarg)) Ltarg = Ltarg.defaultView.frameElement;*/ if(ui.floatRight) { // I1296 ui.ShowInterface(util.getAbsoluteX(params.target) + params.target.offsetWidth + 1, util.getAbsoluteY(params.target) + 1); @@ -588,7 +588,7 @@ if(!keymanweb) { return true; }); - keymanweb.addEventListener('controlblurred', (params) => { + keyman.addEventListener('controlblurred', (params) => { if(!params.event) { return true; // I2404 - Manage IE events in IFRAMEs } @@ -608,7 +608,7 @@ if(!keymanweb) { ui.initialize(); // is/was never actually raised. Note that the `shutdown` method likely fulfills a similar role. - // keymanweb.addEventListener('unloaduserinterface', ui._UnloadUserInterface); + // keyman.addEventListener('unloaduserinterface', ui._UnloadUserInterface); } catch(err){} diff --git a/web/src/app/ui/kmwuitoggle.ts b/web/src/app/ui/kmwuitoggle.ts index 79b78293220..dc449c28e52 100644 --- a/web/src/app/ui/kmwuitoggle.ts +++ b/web/src/app/ui/kmwuitoggle.ts @@ -22,14 +22,14 @@ interface Owned { _owningObject?: T; } -const keymanweb=window.keyman; +const keyman=window.keyman; // If a UI module has been loaded, we can rely on the publically-published 'name' property // having been set as a way to short-out a UI reload. Its parent object always exists by // this point in the build process. -if(!keymanweb) { +if(!keyman) { throw new Error("`keyman` global is missing; Keyman Engine for Web script has not been loaded"); -} else if(!keymanweb.ui?.name) { +} else if(!keyman.ui?.name) { /********************************/ /* */ /* Toggle User Interface Code */ @@ -47,8 +47,8 @@ if(!keymanweb) { try { // Declare KeymanWeb, OnScreen Keyboard and Util objects - //var dbg=keymanweb['debug']; - const util = keymanweb.util; + //var dbg=keyman['debug']; + const util = keyman.util; // Disable UI for touch devices if(util.isTouchDevice()) { @@ -99,14 +99,14 @@ if(!keymanweb) { // We don't want to shift the controller to something that's not an input element, // but do want to account for window.event's data when legitimate. - if(window.event && keymanweb.isAttached(window.event.srcElement as HTMLElement)) { + if(window.event && keyman.isAttached(window.event.srcElement as HTMLElement)) { someElement=window.event.srcElement as HTMLElement; } if(focusing) { this.controller.style.display = 'block'; } else { - if(!(keymanweb.getUIState().activationPending) && !this.controllerHovered) { + if(!(keyman.getUIState().activationPending) && !this.controllerHovered) { this.controller.style.display = 'none'; } } @@ -145,7 +145,7 @@ if(!keymanweb) { } registerEvents() { - const osk = keymanweb.osk; + const osk = keyman.osk; if(!osk) { return; @@ -171,13 +171,13 @@ if(!keymanweb) { **/ readonly switchOsk = () => { // Check that user control of OSK is allowed - if((keymanweb.getActiveKeyboard() == '') || keymanweb.isCJK()) { + if((keyman.getActiveKeyboard() == '') || keyman.isCJK()) { return; } - if(keymanweb.osk) { - const newState = !keymanweb.osk.isEnabled(); - keymanweb.osk.show(newState); + if(keyman.osk) { + const newState = !keyman.osk.isEnabled(); + keyman.osk.show(newState); // Also, indicate that the OSK is intentionally hidden. this.oskButton._setSelected(newState); @@ -188,7 +188,7 @@ if(!keymanweb) { * Toggle a single keyboard on or off - KMW button control event **/ readonly switchSingleKbd = async () => { - const _v = keymanweb.getActiveKeyboard() == ''; + const _v = keyman.getActiveKeyboard() == ''; let nLastKbd=0, kbdName='', lgCode=''; if(_v) { @@ -202,10 +202,10 @@ if(!keymanweb) { kbdName = this.keyboards[nLastKbd]._InternalName; lgCode = this.keyboards[nLastKbd]._LanguageCode; - await keymanweb.setActiveKeyboard(kbdName,lgCode); + await keyman.setActiveKeyboard(kbdName,lgCode); this.lastActiveKeyboard = nLastKbd; } else { - await keymanweb.setActiveKeyboard(''); + await keyman.setActiveKeyboard(''); } if(this.kbdButton) { @@ -217,7 +217,7 @@ if(!keymanweb) { * Switch to the next keyboard in the list - KMW button control event **/ readonly switchNextKbd = async () => { - let _v = (keymanweb.getActiveKeyboard() == ''); + let _v = (keyman.getActiveKeyboard() == ''); let kbdName='', lgCode=''; if(_v) { @@ -227,16 +227,16 @@ if(!keymanweb) { kbdName = this.keyboards[0]._InternalName; lgCode = this.keyboards[0]._LanguageCode; - await keymanweb.setActiveKeyboard(kbdName,lgCode); + await keyman.setActiveKeyboard(kbdName,lgCode); this.lastActiveKeyboard = 0; } else { if(this.lastActiveKeyboard == this.keyboards.length-1) { - await keymanweb.setActiveKeyboard(''); + await keyman.setActiveKeyboard(''); _v = false; } else { kbdName = this.keyboards[++this.lastActiveKeyboard]._InternalName; lgCode = this.keyboards[this.lastActiveKeyboard]._LanguageCode; - await keymanweb.setActiveKeyboard(kbdName,lgCode); + await keyman.setActiveKeyboard(kbdName,lgCode); _v = true; } } @@ -329,7 +329,7 @@ if(!keymanweb) { }; private __click = () => { - keymanweb.activatingUI(false); // Clear activating UI flag once click is acknowledged + keyman.activatingUI(false); // Clear activating UI flag once click is acknowledged if(this._onclick != null) { return this._onclick(); } @@ -337,7 +337,7 @@ if(!keymanweb) { }; private __mousedown = () => { - keymanweb.activatingUI(true); // Set activating UI flag (to manage focus/blur) on any UI mouse down event + keyman.activatingUI(true); // Set activating UI flag (to manage focus/blur) on any UI mouse down event this._down = true; this.__updatestyle(); return false; @@ -350,7 +350,7 @@ if(!keymanweb) { _setSelected(_value: boolean) { - keymanweb.activatingUI(false); // Always clear activating UI flag after selecting UI + keyman.activatingUI(false); // Always clear activating UI flag after selecting UI this._selected = _value; this.__updatestyle(); }; @@ -408,7 +408,7 @@ if(!keymanweb) { **/ initialize() { //Never initialize before KMW! - if(!keymanweb.initialized || util.isTouchDevice()) { + if(!keyman.initialized || util.isTouchDevice()) { return; } @@ -488,13 +488,13 @@ if(!keymanweb) { * Description Rebuild the UI and keyboard list **/ readonly updateKeyboardList = () => { - if(!(keymanweb.initialized || this.initialized)) { + if(!(keyman.initialized || this.initialized)) { return; //TODO: may want to restart the timer?? } this.updateList = false; - const _kbds=keymanweb.getKeyboards(); + const _kbds=keyman.getKeyboards(); const imgPath=util.getOption('resources') +'ui/toggle/'; // Check the number of installed keyboards to determine whether or not we will have a dropdown @@ -542,14 +542,14 @@ if(!keymanweb) { } // Highlight the last active keyboard - let activeKeyboard = keymanweb.getActiveKeyboard(); + let activeKeyboard = keyman.getActiveKeyboard(); let activeLanguage = ''; if (activeKeyboard) { - activeLanguage = keymanweb.getActiveLanguage(); + activeLanguage = keyman.getActiveLanguage(); } else { // savedKeyboard is only correct if we use global keyboard settings, // otherwise it's set to the first keyboard in the list - const savedKeyboard = keymanweb.getSavedKeyboard().split(':'); + const savedKeyboard = keyman.getSavedKeyboard().split(':'); activeKeyboard = savedKeyboard[0]; activeLanguage = savedKeyboard[1]; } @@ -578,8 +578,8 @@ if(!keymanweb) { languageCode = this.keyboards[kbdIndex]._LanguageCode; } - await keymanweb.setActiveKeyboard(name, languageCode); - keymanweb.focusLastActiveElement(); + await keyman.setActiveKeyboard(name, languageCode); + keyman.focusLastActiveElement(); this.kbdButton._setSelected(name != ''); if (kbdIndex >= 0) { this.lastActiveKeyboard = kbdIndex; @@ -734,7 +734,7 @@ if(!keymanweb) { // Add loaded keyboards to the menu this.keyboards = []; - const keyboards = keymanweb.getKeyboards(); + const keyboards = keyman.getKeyboards(); const added: Record = {}; for (let kbdIndex = 0; kbdIndex < keyboards.length; kbdIndex++) { if (!added[keyboards[kbdIndex].InternalName]) { @@ -761,25 +761,25 @@ if(!keymanweb) { // Actually assign our UI module to the active Keyman engine. - const ui = keymanweb.ui = new ToggleUI(); + const ui = keyman.ui = new ToggleUI(); // CTRL-K_SLASH: toggles to and from default keyboard - keymanweb.addHotKey(191, 0x20, ui.switchSingleKbd); + keyman.addHotKey(191, 0x20, ui.switchSingleKbd); // SHIFT-CTRL-K_SLASH: cycles among available keyboards in sequence - keymanweb.addHotKey(191, 0x30, ui.switchNextKbd); + keyman.addHotKey(191, 0x30, ui.switchNextKbd); // ALT-K_SLASH: Hides the OSK - keymanweb.addHotKey(191, 0x40, ui.switchOsk); + keyman.addHotKey(191, 0x40, ui.switchOsk); // // Initialize after KMW is fully initialized - // keymanweb['addEventListener']('loaduserinterface', ui.initialize); + // keyman['addEventListener']('loaduserinterface', ui.initialize); - keymanweb.addEventListener('controlfocused',function(params){ + keyman.addEventListener('controlfocused',function(params){ ui.doFocus(params.target, true, params.activeControl); }); - keymanweb.addEventListener('controlblurred',function(params){ + keyman.addEventListener('controlblurred',function(params){ ui.doFocus(params.target, false, null); }); @@ -789,7 +789,7 @@ if(!keymanweb) { * Set a timer to update the UI keyboard list on timeout after each keyboard is registered, * thus updating only once when only if multiple keyboards are registered together */ - keymanweb.addEventListener('keyboardregistered', function(p) { + keyman.addEventListener('keyboardregistered', function(p) { ui.updateList = true; if(ui.updateTimer) { clearTimeout(ui.updateTimer); @@ -803,7 +803,7 @@ if(!keymanweb) { * * Update menu selection and control OSK display appropriately */ - keymanweb.addEventListener('keyboardchange', function(p) { + keyman.addEventListener('keyboardchange', function(p) { ui.updateMenu(p.internalName, p.languageCode); }); diff --git a/web/src/app/ui/kmwuitoolbar.ts b/web/src/app/ui/kmwuitoolbar.ts index 6ed8d82e231..f121bb66cd0 100644 --- a/web/src/app/ui/kmwuitoolbar.ts +++ b/web/src/app/ui/kmwuitoolbar.ts @@ -52,14 +52,14 @@ interface ListedKeyboard { aNode: HTMLAnchorElement; } -const keymanweb=window.keyman; +const keyman=window.keyman; // If a UI module has been loaded, we can rely on the publically-published 'name' property // having been set as a way to short-out a UI reload. Its parent object always exists by // this point in the build process. -if(!keymanweb) { +if(!keyman) { throw new Error("`keyman` global is missing; Keyman Engine for Web script has not been loaded"); -} else if(!keymanweb.ui?.name) { +} else if(!keyman.ui?.name) { /********************************/ /* */ /* Toolbar User Interface */ @@ -77,7 +77,7 @@ if(!keymanweb) { try { // Declare KeymanWeb, OnScreen keyboard and Util objects - const util=keymanweb.util; + const util=keyman.util; // Disable UI for touch devices if(util.isTouchDevice()) { @@ -228,7 +228,7 @@ if(!keymanweb) { * Initialize toolbar UI */ initialize() { - if(!keymanweb.initialized || this.init) { + if(!keyman.initialized || this.init) { return; } @@ -367,7 +367,7 @@ if(!keymanweb) { aNode.href = '#'; aNode.onclick = this.showOSK; aNode.onmousedown = function() { - keymanweb.activatingUI(true); + keyman.activatingUI(true); }; aNode.title = this.ToolBar_Text.ShowOSK; aNode.appendChild(this.createNode('div', 'kmw_img_osk', 'kmw_img')); @@ -403,7 +403,7 @@ if(!keymanweb) { this.registerEvents(); // Restore focus - keymanweb.focusLastActiveElement(); + keyman.focusLastActiveElement(); } shutdown() { @@ -433,7 +433,7 @@ if(!keymanweb) { this.regionLanguageListNodes = {}; // Build list of keyboards by region and language - const Keyboards = keymanweb.getKeyboards(); + const Keyboards = keyman.getKeyboards(); // Sort the keyboards by region and language Keyboards.sort(this.sortKeyboards); @@ -465,7 +465,7 @@ if(!keymanweb) { } // Get JUST the language code for this section. BCP-47 codes can include more! - const bcpSubtags: string[] = keymanweb.util.getLanguageCodes(Keyboards[j].LanguageCode); + const bcpSubtags: string[] = keyman.util.getLanguageCodes(Keyboards[j].LanguageCode); if(bcpSubtags[0] == languageCode) { continue; // Same language as previous keyboard } @@ -489,7 +489,7 @@ if(!keymanweb) { continue; // Not this region } - const bcpSubtags: string[] = keymanweb.util.getLanguageCodes(Keyboards[j].LanguageCode); + const bcpSubtags: string[] = keyman.util.getLanguageCodes(Keyboards[j].LanguageCode); if(bcpSubtags[0] == languageCode) { // Same language as previous keyboard, so add it to that entry const x = this.languages[languageCode].keyboards; @@ -551,7 +551,7 @@ if(!keymanweb) { } // Restore focus - keymanweb.focusLastActiveElement(); + keyman.focusLastActiveElement(); } /** @@ -819,7 +819,7 @@ if(!keymanweb) { * @return {boolean} **/ private async selectKeyboard(event: Event, lang: LanguageEntry, kbd: KeyboardDetails, updateKeyman: boolean): Promise { - keymanweb.activatingUI(true); + keyman.activatingUI(true); if(this.selectedLanguage) { const found = this.findListedKeyboard(this.selectedLanguage); @@ -839,7 +839,7 @@ if(!keymanweb) { // Return focus to input area and activate the selected keyboard this.addKeyboardToList(lang, kbd); if(updateKeyman) { - await keymanweb.setActiveKeyboard(kbd.InternalName, kbd.LanguageCode).then(() => { + await keyman.setActiveKeyboard(kbd.InternalName, kbd.LanguageCode).then(() => { // Restore focus _after_ the keyboard finishes loading. this.setLastFocus(); }); @@ -850,7 +850,7 @@ if(!keymanweb) { this.saveCookie(); this.enableControls(); - keymanweb.activatingUI(false); + keyman.activatingUI(false); return this.hideKeyboardsPopup(event) || this.hideKeyboardsForLanguage(event); } @@ -869,7 +869,7 @@ if(!keymanweb) { ]; let hideOskButton=false; - if(keymanweb.isCJK(this.selectedKeyboard)) { + if(keyman.isCJK(this.selectedKeyboard)) { hideOskButton = true; } else if(this.selectedKeyboard == null) { hideOskButton = (elems[2].style.display == 'none'); @@ -903,7 +903,7 @@ if(!keymanweb) { * Restore the focus to the last focused element **/ setLastFocus() { - keymanweb.focusLastActiveElement(); + keyman.focusLastActiveElement(); } /** @@ -915,13 +915,13 @@ if(!keymanweb) { * @return {boolean} **/ readonly showOSK = (event: Event) => { - const osk = keymanweb.osk; + const osk = keyman.osk; if(!osk) { return false; } - keymanweb.activatingUI(true); + keyman.activatingUI(true); //Toggle OSK on or off - if(osk && keymanweb.getActiveKeyboard() != '') { + if(osk && keyman.getActiveKeyboard() != '') { if(osk.isEnabled()) { osk.hide(); } else { @@ -929,7 +929,7 @@ if(!keymanweb) { } } this.setLastFocus(); - keymanweb.activatingUI(false); + keyman.activatingUI(false); return this.eventCapture(event); } @@ -957,7 +957,7 @@ if(!keymanweb) { // Return the focus to the input area and set the active keyboard to nothing this.setLastFocus(); - await keymanweb.setActiveKeyboard('',''); + await keyman.setActiveKeyboard('',''); //Save current state when deselecting a keyboard (may not be needed) this.saveCookie(); @@ -1082,10 +1082,10 @@ if(!keymanweb) { } } else { if(this.selectedKeyboard != null) { - if(keymanweb.isCJK()) { + if(keyman.isCJK()) { this.oskButtonNode.style.display = this.oskBarNode.style.display = 'none'; } else { - const osk = keymanweb.osk; + const osk = keyman.osk; this.oskButtonNode.className = (osk && osk.isEnabled()) ? 'kmw_button_selected' : 'kmw_button'; } } @@ -1154,7 +1154,7 @@ if(!keymanweb) { // https://help.keyman.com/developer/engine/web/current-version/reference/events/kmw.keyboardchange this.lastSelectedKeyboard = null; const kbName=p.internalName, - lgName=keymanweb.util.getLanguageCodes(p.languageCode)[0]; + lgName=keyman.util.getLanguageCodes(p.languageCode)[0]; if(lgName != '' && kbName != '') { const lg = this.languages[lgName]; if(lg != null) { @@ -1200,7 +1200,7 @@ if(!keymanweb) { } registerEvents() { - const osk = keymanweb.osk; + const osk = keyman.osk; if(!osk) { return; } @@ -1282,7 +1282,7 @@ if(!keymanweb) { * for the popup, we stash the old event listener here and restore * it when we're done. */ - lastDismissalCallback: (event: MouseEvent) => any = null; + lastDismissalCallback: (this: GlobalEventHandlers, ev: MouseEvent) => any = null; /** * Function PopupDismissal @@ -1392,8 +1392,8 @@ if(!keymanweb) { } else { // If language list and keyboard list have not yet been saved as a cookie, // initialize to the current (default) language and keyboard, if set by KMW - const kbName=keymanweb.getActiveKeyboard(); - const lgName=keymanweb.getActiveLanguage(); + const kbName=keyman.getActiveKeyboard(); + const lgName=keyman.getActiveLanguage(); if(lgName != '' && kbName != '') { const lg = this.languages[lgName]; if(lg != null) { @@ -1429,12 +1429,12 @@ if(!keymanweb) { } - const ui = keymanweb.ui = new ToolbarUI(); + const ui = keyman.ui = new ToolbarUI(); - keymanweb.addEventListener('keyboardregistered', ui.registerKeyboard); - keymanweb.addEventListener('controlfocused', ui.focusControlEvent); - keymanweb.addEventListener('controlblurred', ui.blurControlEvent); - keymanweb.addEventListener('keyboardchange', ui.changeKeyboardEvent); + keyman.addEventListener('keyboardregistered', ui.registerKeyboard); + keyman.addEventListener('controlfocused', ui.focusControlEvent); + keyman.addEventListener('controlblurred', ui.blurControlEvent); + keyman.addEventListener('keyboardchange', ui.changeKeyboardEvent); // Initialize when everything defined (replaces unreliable onload event handler) // In case the toolbar script loads a bit later than the main KMW script From cda7dc6d2428870ce90d2499a1ea9fbe02bced47 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Wed, 1 Jul 2026 10:27:22 +0200 Subject: [PATCH 11/44] chore: remove deprecated "X-UA-Compatible" and "apple-mobile-web-app-capable" metas For Keyman Developer Server, add app.webmanifest so that it can be saved to the home screen; other pages should not require this. Fixes: #16172 Test-bot: skip --- common/test/resources/keyboards/test_8568_deadkeys.html | 6 ------ developer/src/server/src/site/app.webmanifest | 9 +++++++++ developer/src/server/src/site/index.html | 2 +- developer/src/tike/xml/app/editor/index.html | 4 ---- oem/firstvoices/windows/src/xml/onlineupdate.xsl | 1 - web/index.html | 4 ---- web/src/samples/complex/root/pages/index.html | 6 ------ web/src/samples/index.html | 4 ---- web/src/samples/simplest/index.html | 6 ------ web/src/samples/subfolder_toggle/index.html | 6 ------ web/src/samples/subfolder_toolbar/index.html | 4 ---- .../dom/cases/gesture-processor/host-page.tests.html | 3 --- web/src/test/auto/e2e/baseline/baseline.tests.html | 2 -- web/src/test/manual/embed/android-harness/index.html | 6 ------ web/src/test/manual/embed/index.html | 4 ---- web/src/test/manual/web/attachment-api/index.html | 6 ------ web/src/test/manual/web/basic-iframe/index.html | 6 ------ web/src/test/manual/web/build-visual-keyboard/index.html | 6 ------ web/src/test/manual/web/chirality/index.html | 6 ------ web/src/test/manual/web/default-subkey/index.html | 6 ------ web/src/test/manual/web/desktop-ui/button.html | 6 ------ web/src/test/manual/web/desktop-ui/float.html | 6 ------ web/src/test/manual/web/desktop-ui/toggle.html | 6 ------ web/src/test/manual/web/desktop-ui/toolbar.html | 6 ------ web/src/test/manual/web/empty-row/index.html | 6 ------ web/src/test/manual/web/gesture-recognizer/index.html | 3 --- web/src/test/manual/web/index.html | 4 ---- web/src/test/manual/web/init-race-10743/index.html | 6 ------ web/src/test/manual/web/inline-osk/index.html | 6 ------ web/src/test/manual/web/issue005/index.html | 6 ------ web/src/test/manual/web/issue103/index.html | 6 ------ web/src/test/manual/web/issue115/index.html | 6 ------ web/src/test/manual/web/issue116/index.html | 6 ------ web/src/test/manual/web/issue11785/index.html | 6 ------ web/src/test/manual/web/issue1332/index.html | 6 ------ web/src/test/manual/web/issue160/index.html | 6 ------ web/src/test/manual/web/issue266/index.html | 6 ------ web/src/test/manual/web/issue271/index.html | 6 ------ web/src/test/manual/web/issue29/index.html | 6 ------ web/src/test/manual/web/issue3701/index.html | 6 ------ web/src/test/manual/web/issue382/index.html | 6 ------ web/src/test/manual/web/issue53/index.html | 6 ------ web/src/test/manual/web/issue6005/index.html | 6 ------ web/src/test/manual/web/issue62/index.html | 6 ------ web/src/test/manual/web/issue63/index.html | 6 ------ .../manual/web/issue917-context-and-notany/index.html | 6 ------ web/src/test/manual/web/issue920/index.html | 6 ------ web/src/test/manual/web/issue9469/index.html | 6 ------ web/src/test/manual/web/keyboard-errors/index.html | 6 ------ web/src/test/manual/web/kmxkeyboard.html | 6 ------ web/src/test/manual/web/mnemonic/index.html | 3 --- web/src/test/manual/web/options-with-save/index.html | 3 --- web/src/test/manual/web/osk-event-buttons/index.html | 6 ------ web/src/test/manual/web/osk-movement/index.html | 6 ------ web/src/test/manual/web/osk/scratchspace/index.html | 6 ------ web/src/test/manual/web/platform/index.html | 6 ------ web/src/test/manual/web/pr10506/index.html | 6 ------ web/src/test/manual/web/prediction-mtnt/index.html | 6 ------ web/src/test/manual/web/prediction-ui/index.html | 6 ------ web/src/test/manual/web/promise-api/index.html | 6 ------ web/src/test/manual/web/regression-tests/index.html | 3 --- web/src/test/manual/web/regression-tests/test.html | 5 +---- web/src/test/manual/web/sentry-integration/index.html | 6 ------ web/src/test/manual/web/test-updateLayer/index.html | 6 ------ web/src/test/manual/web/unminified - manual.html | 6 ------ web/src/test/manual/web/unminified.html | 6 ------ web/src/tools/testing/bulk_rendering/index.html | 6 ------ web/src/tools/testing/bulk_rendering/local-renderer.html | 6 ------ .../gesture-processor/host-fixture/host-fixture.html | 3 --- .../testing/gesture-processor/recorder/src/index.html | 3 --- web/src/tools/testing/index.html | 4 ---- web/src/tools/testing/recorder/index.html | 6 ------ windows/src/desktop/kmshell/xml/onlineupdate.xsl | 1 - 73 files changed, 11 insertions(+), 376 deletions(-) create mode 100644 developer/src/server/src/site/app.webmanifest diff --git a/common/test/resources/keyboards/test_8568_deadkeys.html b/common/test/resources/keyboards/test_8568_deadkeys.html index 054f19b4f81..4ce169551f5 100644 --- a/common/test/resources/keyboards/test_8568_deadkeys.html +++ b/common/test/resources/keyboards/test_8568_deadkeys.html @@ -6,12 +6,6 @@ - - - - - - KeymanWeb #8568 diff --git a/developer/src/server/src/site/app.webmanifest b/developer/src/server/src/site/app.webmanifest new file mode 100644 index 00000000000..288b3059630 --- /dev/null +++ b/developer/src/server/src/site/app.webmanifest @@ -0,0 +1,9 @@ +{ + "name": "Keyman Developer Server", + "display": "standalone", + "id": "com.keyman.developer.server.self-hosted", + "icons": [{ + "src": "keyman-developer.svg", + "sizes": "any" + }] +} \ No newline at end of file diff --git a/developer/src/server/src/site/index.html b/developer/src/server/src/site/index.html index 8147a849a46..cd9cb5ae02f 100644 --- a/developer/src/server/src/site/index.html +++ b/developer/src/server/src/site/index.html @@ -5,7 +5,7 @@ - + Keyman Developer Keyboard Test Site diff --git a/developer/src/tike/xml/app/editor/index.html b/developer/src/tike/xml/app/editor/index.html index b30fa799cf6..9054aea37fc 100644 --- a/developer/src/tike/xml/app/editor/index.html +++ b/developer/src/tike/xml/app/editor/index.html @@ -9,10 +9,6 @@ - - - - Text Editor diff --git a/oem/firstvoices/windows/src/xml/onlineupdate.xsl b/oem/firstvoices/windows/src/xml/onlineupdate.xsl index 931c0c106bc..95024605ac7 100644 --- a/oem/firstvoices/windows/src/xml/onlineupdate.xsl +++ b/oem/firstvoices/windows/src/xml/onlineupdate.xsl @@ -10,7 +10,6 @@ - <xsl:value-of select="$locale/string[@name='S_Update_Title']"/> diff --git a/web/index.html b/web/index.html index 7dc31756496..87a7fe5cb53 100644 --- a/web/index.html +++ b/web/index.html @@ -6,10 +6,6 @@ - - - - KeymanWeb Testing + + + +

javascript_error

+ +

+Keyboard with a script error in `gs()`, taken originally from release/b/banne. The .js and .kmp files in modified_build/ have been manually tweaked to throw an error. +

+ + + diff --git a/common/test/keyboards/invalid/engine/javascript_error/source/welcome.htm b/common/test/keyboards/invalid/engine/javascript_error/source/welcome.htm new file mode 100644 index 00000000000..acc98782a02 --- /dev/null +++ b/common/test/keyboards/invalid/engine/javascript_error/source/welcome.htm @@ -0,0 +1,178 @@ + + + + + + Start Using Banne + + + + +

Start Using Banne

+ +

+Keyboard with a script error in `gs()`, taken originally from release/b/banne. The .js and .kmp files in modified_build/ have been manually tweaked to throw an error. +

+ +

Keyboard Layout

+

Keystrokes

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Base+Modifier=Result
s+'=š
S+'=Š
+

Punctuation

+

+ The ' key has been modified to cycle between (U+2039) and (U+203A) from the second keypress onwards. +
+ The first keypress will produce the ' (U+0027) character as expected, and pressing it again will turn the ' into a . Pressing the ' key one more time will produce a . +

+

+ The " key has been modified to automatically type the character « (U+00AB). To cancel this, pressing the ; key before typing a " will produce a normal ". +
+ Pressing the " key repeatedly will cycle between the » (U+00BB) and « characters. +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Base+Modifier=Result
'+'=
<+'=
>+'=
" =«
<+<=«
>+>=»
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Base+Modifier=Result
+'=
+'=
«+"=»
»+"=«
;+"="
+
+ + + \ No newline at end of file From ab2bfc90479bd7c316a1c8f7209897762d8e9ac1 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Mon, 13 Jul 2026 11:05:42 +0200 Subject: [PATCH 33/44] maint(resources): add extra debug reporting for builder Relates-to: #16137 Test-bot: skip Build-bot: skip build:all release:android --- resources/build/builder-basic.inc.sh | 42 ++++--- resources/build/test/build.sh | 5 + resources/build/test/test.sh | 5 +- resources/builder.inc.sh | 119 +++++++++++++++--- .../android/keyman-android-test-samples.sh | 2 +- 5 files changed, 138 insertions(+), 35 deletions(-) diff --git a/resources/build/builder-basic.inc.sh b/resources/build/builder-basic.inc.sh index a2077b081aa..8e0954f5c52 100644 --- a/resources/build/builder-basic.inc.sh +++ b/resources/build/builder-basic.inc.sh @@ -177,19 +177,33 @@ function _builder_basic_print_build_number_for_teamcity() { } function _builder_basic_print_version_utils_debug() { - echo "KEYMAN_ROOT: $KEYMAN_ROOT" - echo "KEYMAN_VERSION: $KEYMAN_VERSION" - echo "KEYMAN_VERSION_WIN: $KEYMAN_VERSION_WIN" - echo "KEYMAN_VERSION_RELEASE: $KEYMAN_VERSION_RELEASE" - echo "KEYMAN_VERSION_MAJOR: $KEYMAN_VERSION_MAJOR" - echo "KEYMAN_VERSION_MINOR: $KEYMAN_VERSION_MINOR" - echo "KEYMAN_VERSION_PATCH: $KEYMAN_VERSION_PATCH" - echo "KEYMAN_TIER: $KEYMAN_TIER" - echo "KEYMAN_VERSION_TAG: $KEYMAN_VERSION_TAG" - echo "KEYMAN_VERSION_WITH_TAG: $KEYMAN_VERSION_WITH_TAG" - echo "KEYMAN_VERSION_GIT_TAG: $KEYMAN_VERSION_GIT_TAG" - echo "KEYMAN_VERSION_ENVIRONMENT: $KEYMAN_VERSION_ENVIRONMENT" - echo "KEYMAN_VERSION_FOR_FILENAME: $KEYMAN_VERSION_FOR_FILENAME" + if ! builder_is_debug_internal && ! builder_is_ci_build; then + return + fi + + if [[ ${_builder_basic_environment_printed:-false} == true ]]; then + return + fi + + builder_echo start builder_basic_debug "Builder internal environment variables" + + echo " KEYMAN_ROOT: $KEYMAN_ROOT" + echo " KEYMAN_VERSION: $KEYMAN_VERSION" + echo " KEYMAN_VERSION_WIN: $KEYMAN_VERSION_WIN" + echo " KEYMAN_VERSION_RELEASE: $KEYMAN_VERSION_RELEASE" + echo " KEYMAN_VERSION_MAJOR: $KEYMAN_VERSION_MAJOR" + echo " KEYMAN_VERSION_MINOR: $KEYMAN_VERSION_MINOR" + echo " KEYMAN_VERSION_PATCH: $KEYMAN_VERSION_PATCH" + echo " KEYMAN_TIER: $KEYMAN_TIER" + echo " KEYMAN_VERSION_TAG: $KEYMAN_VERSION_TAG" + echo " KEYMAN_VERSION_WITH_TAG: $KEYMAN_VERSION_WITH_TAG" + echo " KEYMAN_VERSION_GIT_TAG: $KEYMAN_VERSION_GIT_TAG" + echo " KEYMAN_VERSION_ENVIRONMENT: $KEYMAN_VERSION_ENVIRONMENT" + echo " KEYMAN_VERSION_FOR_FILENAME: $KEYMAN_VERSION_FOR_FILENAME" + + export _builder_basic_environment_printed=true + + builder_echo end builder_basic_debug success "Builder internal environment variables" } # TODO: consolidate with buildLevel, see #14285 @@ -284,8 +298,8 @@ _builder_basic_find_keyman_root _builder_basic_find_tier _builder_basic_find_version -# _builder_basic_print_version_utils_debug _builder_basic_print_build_number_for_teamcity +_builder_basic_print_version_utils_debug _builder_basic_find_should_sentry_release diff --git a/resources/build/test/build.sh b/resources/build/test/build.sh index d1dec4a5057..6c4c4dec2c0 100755 --- a/resources/build/test/build.sh +++ b/resources/build/test/build.sh @@ -1,5 +1,10 @@ #!/usr/bin/env bash +# Avoid timing reports and internal debugging in unit tests. These conflict with +# the printing of outputs and cause tests to fail. +export _builder_debug_internal=false +export _builder_timings=false + ## START STANDARD BUILD SCRIPT INCLUDE # adjust relative paths as necessary THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" diff --git a/resources/build/test/test.sh b/resources/build/test/test.sh index f13a68c7271..746ad480277 100755 --- a/resources/build/test/test.sh +++ b/resources/build/test/test.sh @@ -2,7 +2,10 @@ set -eu -# Avoid timing reports in unit tests + +# Avoid timing reports and internal debugging in unit tests. These conflict with +# the printing of outputs and cause tests to fail. +export _builder_debug_internal=false export _builder_timings=false ## START STANDARD BUILD SCRIPT INCLUDE diff --git a/resources/builder.inc.sh b/resources/builder.inc.sh index 6ec2fbcebff..f316b5ade50 100755 --- a/resources/builder.inc.sh +++ b/resources/builder.inc.sh @@ -425,7 +425,7 @@ _builder_failure_trap() { # _builder_cleanup_deps() { if ! builder_is_dep_build && ! builder_is_child_build && [[ ! -z ${_builder_deps_built+x} ]]; then - if $_builder_debug_internal; then + if builder_is_debug_internal; then builder_echo_debug "Dependencies that were built:" cat "$_builder_deps_built" fi @@ -1722,18 +1722,6 @@ _builder_parse_expanded_parameters() { _builder_add_chosen_action_target_dependencies fi - if $_builder_debug_internal; then - builder_echo_debug "Selected actions and targets:" - for e in "${_builder_chosen_action_targets[@]}"; do - builder_echo_debug "* $e" - done - builder_echo_debug - builder_echo_debug "Selected options:" - for e in "${_builder_chosen_options[@]}"; do - builder_echo_debug "* $e" - done - fi - if builder_is_dep_build; then _builder_verify_expected_sub_process_variables elif builder_is_child_build; then @@ -1773,6 +1761,8 @@ _builder_parse_expanded_parameters() { BUILDER_CONFIGURATION=release fi + _builder_print_internal_debug_info + # Now that we've successfully parsed options adhering to the _builder spec, we may activate our # action_failure and action_hanging traps. (We don't want them active on scripts not yet using # said script.) @@ -2080,7 +2070,7 @@ _builder_do_build_deps() { $_builder_offline \ $_builder_build_deps \ --builder-dep-parent "$THIS_SCRIPT_IDENTIFIER" && ( - if $_builder_debug_internal; then + if builder_is_debug_internal; then builder_echo success "## Dependency $dep$dep_target for $_builder_matched_action_name completed successfully" fi ) || ( @@ -2149,6 +2139,9 @@ builder_is_full_dep_build() { # returns `0` if the current build script has at least one dependency. # builder_has_dependencies() { + if [[ ${_builder_deps:-false} == false ]]; then + return 1 + fi if [[ ${#_builder_deps[@]} -eq 0 ]]; then return 1 fi @@ -2245,17 +2238,29 @@ _builder_report_dependencies() { # returns `0` if we should be verbose in output # builder_verbose() { - if [[ $builder_verbose == --verbose ]]; then + if [[ ${builder_verbose:-} == --verbose ]]; then return 0 fi return 1 } # -# returns `0` if we are doing a debug build +# Returns `0` if we are doing a debug build. Not the same as +# `builder_is_debug_internal`. # builder_is_debug_build() { - if [[ $builder_debug == --debug ]]; then + if [[ ${builder_debug:-} == --debug ]]; then + return 0 + fi + return 1 +} + +# +# Returns `0` if builder internal debug reporting is enabled. This is not the +# same as a debug build. +# +builder_is_debug_internal() { + if $_builder_debug_internal; then return 0 fi return 1 @@ -2539,6 +2544,82 @@ _builder_get_operating_system() { readonly BUILDER_OS } +function _builder_echo_function_result() { + if $1; then + printf " %-40s%s\n" "$1:" true + else + printf " %-40s%s\n" "$1:" false + fi +} + +function _builder_print_internal_debug_info() { + if ! builder_is_debug_internal && ! builder_is_ci_build; then + return + fi + + if [[ ${_builder_internal_debug_info_printed:-false} == false ]]; then + builder_echo start builder_debug "Builder internal debug information" + + # For CI builds, we report this only once per build; for internal debug + # builds, it prints for every child/dep build also + echo -e "${COLOR_TEAL}Selected actions and targets${COLOR_RESET}" + for e in "${_builder_chosen_action_targets[@]}"; do + echo " $e" + done + + echo + echo -e "${COLOR_TEAL}Selected options${COLOR_RESET}" + for e in "${_builder_chosen_options[@]}"; do + echo " $e" + done + + echo + echo -e "${COLOR_TEAL}Builder configuration${COLOR_RESET}" + _builder_echo_function_result builder_is_debug_build + _builder_echo_function_result builder_verbose + + echo + echo -e "${COLOR_TEAL}Builder CI information${COLOR_RESET}" + _builder_echo_function_result builder_is_ci_build + _builder_echo_function_result builder_is_ci_release_build + _builder_echo_function_result builder_is_ci_test_build + _builder_echo_function_result builder_is_ci_build_level_release + _builder_echo_function_result builder_is_ci_build_level_build + + echo + echo -e "${COLOR_TEAL}Builder platform information${COLOR_RESET}" + _builder_echo_function_result builder_is_running_on_docker + _builder_echo_function_result builder_is_running_on_gha + _builder_echo_function_result builder_is_running_on_teamcity + _builder_echo_function_result builder_is_windows + _builder_echo_function_result builder_is_macos + _builder_echo_function_result builder_is_linux + fi + + if builder_is_debug_internal; then + # This may change in sub-project builds, so we print it every time, + # but we won't print it in CI to prevent logs exploding; note that + # the builder_debug header will not be printed each time; that is + # "by design" + echo + echo -e "${COLOR_TEAL}Builder dependency internal data${COLOR_RESET}" + _builder_echo_function_result builder_is_dep_build + _builder_echo_function_result builder_is_child_build + _builder_echo_function_result builder_is_quick_dep_build + _builder_echo_function_result builder_is_full_dep_build + _builder_echo_function_result builder_has_dependencies + fi + + if [[ ${_builder_internal_debug_info_printed:-false} == false ]]; then + builder_echo end builder_debug success "Builder internal debug information" + + if builder_is_ci_build; then + # In CI builds, we will only print the debug info once + export _builder_internal_debug_info_printed=true + fi + fi +} + ################################################################################ # Final initialization ################################################################################ @@ -2557,6 +2638,6 @@ if [ -z ${_builder_debug_internal+x} ]; then _builder_debug_internal=false fi -if $_builder_debug_internal; then - builder_echo_debug "Command line: $0 $@" +if builder_is_debug_internal; then + builder_echo_debug "Command line: $0 $*" fi diff --git a/resources/teamcity/android/keyman-android-test-samples.sh b/resources/teamcity/android/keyman-android-test-samples.sh index 675d339dcf2..c79612a3ca8 100755 --- a/resources/teamcity/android/keyman-android-test-samples.sh +++ b/resources/teamcity/android/keyman-android-test-samples.sh @@ -29,7 +29,7 @@ builder_parse "$@" cd "${KEYMAN_ROOT}/android" function do_build() { - builder_launch /android/build.sh configure,build:engine,sample1,sample2,keyboardharness + builder_launch /android/build.sh configure,build:sample1,sample2,keyboardharness } if builder_has_action all; then From a1c8e63cc8f0dc702a206e79df740b7743f9d539 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Mon, 13 Jul 2026 16:28:33 +0200 Subject: [PATCH 34/44] chore(android): cleanup Android build scripts and artifact filenames * Specify clearer filenames for Android samples and test KeyboardHarness * Cleanup android/*/build.sh to match current patterns * Remove obsolete jcenter dependency * Export BUILDER_CONFIGURATION in builder.inc.sh Fixes: #16137 Fixes: #16181 Test-bot: skip Build-bot: skip release:android --- android/KMAPro/build.sh | 96 ++++++++---------- android/KMEA/app/build.gradle | 2 +- android/KMEA/build.sh | 97 +++++++------------ android/Samples/KMSample1/app/build.gradle | 17 +++- android/Samples/KMSample1/build.gradle | 2 - android/Samples/KMSample1/build.sh | 61 ++---------- android/Samples/KMSample2/app/build.gradle | 16 ++- android/Samples/KMSample2/build.gradle | 2 - android/Samples/KMSample2/build.sh | 61 ++---------- .../Tests/KeyboardHarness/app/build.gradle | 3 +- android/Tests/KeyboardHarness/build.sh | 72 ++------------ android/android-utils.inc.sh | 20 ++++ android/build.sh | 14 +-- android/version.gradle | 11 +++ oem/firstvoices/android/build.gradle | 1 - resources/builder.inc.sh | 4 +- 16 files changed, 175 insertions(+), 304 deletions(-) create mode 100644 android/android-utils.inc.sh diff --git a/android/KMAPro/build.sh b/android/KMAPro/build.sh index 06370c06d23..ba3ea5aa597 100755 --- a/android/KMAPro/build.sh +++ b/android/KMAPro/build.sh @@ -1,5 +1,6 @@ #!/usr/bin/env bash -# Build Keyman for Android app (KMAPro) +# Keyman is copyright (C) SIL Global. MIT License. +# ## START STANDARD BUILD SCRIPT INCLUDE # adjust relative paths as necessary THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" @@ -9,111 +10,92 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" . "$KEYMAN_ROOT/resources/build/utils.inc.sh" . "$KEYMAN_ROOT/resources/build/build-help.inc.sh" . "$KEYMAN_ROOT/resources/build/build-download-resources.sh" - . "$KEYMAN_ROOT/android/KMAPro/build-play-store-notes.inc.sh" # ################################ Main script ################################ -# Definition of global compile constants - -CONFIG="release" -BUILD_FLAGS="build -x lint -x test" # Gradle build w/o test -TEST_FLAGS="-x assembleRelease lintRelease testRelease" # Gradle test w/o build -DAEMON_FLAG= - builder_describe "Builds Keyman for Android app." \ "@/android/KMEA" \ "clean" \ "configure" \ "build" \ - "test Runs lint and unit tests." \ - "publish-symbols Publishes symbols to Sentry." \ + "test Runs lint and unit tests." \ + "publish-symbols Publishes symbols to Sentry." \ "publish-play-store Publishes the APK to the Play Store." -# parse before describe_outputs to check debug flags builder_parse "$@" if builder_is_debug_build; then - builder_heading "### Debug config ####" - CONFIG="debug" BUILD_FLAGS="assembleDebug -x lintDebug -x testDebug" TEST_FLAGS="-x assembleDebug lintDebug testDebug" +else + BUILD_FLAGS="build -x lint -x test" # Gradle build w/o test + TEST_FLAGS="-x assembleRelease lintRelease testRelease" # Gradle test w/o build fi +if builder_is_ci_build; then + GRADLE_DAEMON=--no-daemon +else + GRADLE_DAEMON= +fi + +# Locations for bundled keyboard and lexical model + KEYBOARD_PACKAGE_ID="sil_euro_latin" KEYBOARDS_TARGET="android/KMAPro/kMAPro/src/main/assets/${KEYBOARD_PACKAGE_ID}.kmp" MODEL_PACKAGE_ID="nrc.en.mtnt" MODELS_TARGET="android/KMAPro/kMAPro/src/main/assets/${MODEL_PACKAGE_ID}.model.kmp" + builder_describe_outputs \ configure "/${MODELS_TARGET}" \ - build /android/KMAPro/kMAPro/build/outputs/apk/$CONFIG/keyman-${KEYMAN_VERSION_FOR_FILENAME}.apk - -#### Build - - -# Parse args - -if builder_is_ci_build; then - DAEMON_FLAG=--no-daemon -fi + build /android/KMAPro/kMAPro/build/outputs/apk/$BUILDER_CONFIGURATION/keyman-${KEYMAN_VERSION_FOR_FILENAME}.apk #### Build action definitions #### -# Check about cleaning artifact paths -if builder_start_action clean; then - rm -rf "$KEYMAN_ROOT/android/KMAPro/kMAPro/build/outputs" - builder_finish_action success clean -fi - -if builder_start_action configure; then - +do_configure() { downloadKeyboardPackage "$KEYBOARD_PACKAGE_ID" "${KEYMAN_ROOT}/$KEYBOARDS_TARGET" downloadModelPackage "$MODEL_PACKAGE_ID" "${KEYMAN_ROOT}/$MODELS_TARGET" +} - builder_finish_action success configure -fi - -if builder_start_action build; then - +do_build() { # Copy Keyman Engine for Android - cp "${KEYMAN_ROOT}/android/KMEA/app/build/outputs/aar/keyman-engine-${CONFIG}.aar" "${KEYMAN_ROOT}/android/KMAPro/kMAPro/libs/keyman-engine.aar" + cp "${KEYMAN_ROOT}/android/KMEA/app/build/outputs/aar/keyman-engine-${BUILDER_CONFIGURATION}.aar" "${KEYMAN_ROOT}/android/KMAPro/kMAPro/libs/keyman-engine.aar" # Convert markdown to html for offline help build_help_html android KMAPro/kMAPro/src/main/assets/info - echo "BUILD_FLAGS $BUILD_FLAGS" - ./gradlew $DAEMON_FLAG clean $BUILD_FLAGS + builder_echo "BUILD_FLAGS $BUILD_FLAGS" + ./gradlew $GRADLE_DAEMON $BUILD_FLAGS - mv "${KEYMAN_ROOT}/android/KMAPro/kMAPro/build/outputs/apk/$CONFIG/keyman-${KEYMAN_VERSION}.apk" "${KEYMAN_ROOT}/android/KMAPro/kMAPro/build/outputs/apk/$CONFIG/keyman-${KEYMAN_VERSION_FOR_FILENAME}.apk" - - builder_finish_action success build -fi - -if builder_start_action test; then - - echo "TEST_FLAGS $TEST_FLAGS" - ./gradlew $DAEMON_FLAG $TEST_FLAGS + mv "${KEYMAN_ROOT}/android/KMAPro/kMAPro/build/outputs/apk/$BUILDER_CONFIGURATION/keyman-${KEYMAN_VERSION}.apk" "${KEYMAN_ROOT}/android/KMAPro/kMAPro/build/outputs/apk/$BUILDER_CONFIGURATION/keyman-${KEYMAN_VERSION_FOR_FILENAME}.apk" +} - builder_finish_action success test -fi +do_test() { + builder_echo "TEST_FLAGS $TEST_FLAGS" + ./gradlew $GRADLE_DAEMON $TEST_FLAGS +} -publish_symbols() { +do_publish_symbols() { if builder_is_ci_build && builder_is_ci_build_level_release; then # TODO: what does publishSentry even do? - ./gradlew $DAEMON_FLAG publishSentry + ./gradlew $GRADLE_DAEMON publishSentry builder_echo "Making a Sentry release for tag $KEYMAN_VERSION_GIT_TAG" sentry-cli upload-dif -p keyman-android --include-sources sentry-cli releases -p keyman-android files $KEYMAN_VERSION_GIT_TAG upload-sourcemaps ./ fi } -publish_play_store() { +do_publish_play_store() { if builder_is_ci_build && builder_is_ci_build_level_release; then generateReleaseNotes # Publish Keyman for Android to Play Store - ./gradlew $DAEMON_FLAG publishReleaseApk + ./gradlew $GRADLE_DAEMON publishReleaseApk fi } -builder_run_action publish-symbols publish_symbols -builder_run_action publish-play-store publish_play_store +builder_run_action clean rm -rf "$KEYMAN_ROOT/android/KMAPro/kMAPro/build" +builder_run_action configure do_configure +builder_run_action build do_build +builder_run_action test do_test +builder_run_action publish-symbols do_publish_symbols +builder_run_action publish-play-store do_publish_play_store diff --git a/android/KMEA/app/build.gradle b/android/KMEA/app/build.gradle index 4bb05106e30..8f77d155f01 100644 --- a/android/KMEA/app/build.gradle +++ b/android/KMEA/app/build.gradle @@ -8,7 +8,7 @@ ext.rootPath = '../../' apply from: "$rootPath/version.gradle" base { - // Specify library filename keyman-engine-$CONFIG.aar + // Specify library filename keyman-engine-$BUILDER_CONFIGURATION.aar archivesName = "keyman-engine" } diff --git a/android/KMEA/build.sh b/android/KMEA/build.sh index 28d9877285c..85a1894672b 100755 --- a/android/KMEA/build.sh +++ b/android/KMEA/build.sh @@ -1,4 +1,6 @@ #!/usr/bin/env bash +# Keyman is copyright (C) SIL Global. MIT License. +# # Build Keyman Engine for Android using Keyman Web artifacts # ## START STANDARD BUILD SCRIPT INCLUDE @@ -7,20 +9,9 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" . "${THIS_SCRIPT%/*}/../../resources/build/builder-full.inc.sh" ## END STANDARD BUILD SCRIPT INCLUDE -. "$KEYMAN_ROOT/resources/build/utils.inc.sh" # ################################ Main script ################################ -# Definition of global compile constants - -KEYMAN_ANDROID_ROOT="$KEYMAN_ROOT/android" -KEYMAN_WEB_ROOT="$KEYMAN_ROOT/web" -ENGINE_ASSETS="$KEYMAN_ANDROID_ROOT/KMEA/app/src/main/assets" -CONFIG="release" -BUILD_FLAGS="aR -x lint -x test" # Gradle build w/o test -TEST_FLAGS="-x aR lintRelease testRelease" # Gradle test w/o build -JUNIT_RESULTS="##teamcity[importData type='junit' path='keyman\android\KMEA\app\build\test-results\testReleaseUnitTest\']" - builder_describe "Builds Keyman Engine for Android." \ "@/web/src/app/webview" \ "@/common/web/sentry-manager" \ @@ -30,78 +21,64 @@ builder_describe "Builds Keyman Engine for Android." \ "test Runs lint and unit tests." \ ":engine Builds Engine" -# parse before describe_outputs to check debug flags builder_parse "$@" +# Definition of global compile constants + if builder_is_debug_build; then - builder_heading "### Debug config ####" - CONFIG="debug" BUILD_FLAGS="assembleDebug -x lintDebug -x testDebug" TEST_FLAGS="-x assembleDebug lintDebug testDebug" JUNIT_RESULTS="##teamcity[importData type='junit' path='keyman\android\KMEA\app\build\test-results\testDebugUnitTest\']" +else + BUILD_FLAGS="assembleRelease -x lint -x test" # Gradle build w/o test + TEST_FLAGS="-x assembleRelease lintRelease testRelease" # Gradle test w/o build + JUNIT_RESULTS="##teamcity[importData type='junit' path='keyman\android\KMEA\app\build\test-results\testReleaseUnitTest\']" fi -builder_describe_outputs \ - build:engine /android/KMEA/app/build/outputs/aar/keyman-engine-${CONFIG}.aar - -#### Build - - -# Parse args - -DAEMON_FLAG= if builder_is_ci_build; then - DAEMON_FLAG=--no-daemon + GRADLE_DAEMON=--no-daemon +else + GRADLE_DAEMON= fi -#### Build action definitions #### - -# Check about cleaning artifact paths -if builder_start_action clean:engine; then - rm -rf "$KEYMAN_ROOT/android/KMEA/app/build/outputs" - builder_finish_action success clean:engine -fi - -if builder_start_action configure:engine; then - - builder_finish_action success configure:engine -fi - -# Destinations that will need the keymanweb artifacts +builder_describe_outputs \ + build:engine /android/KMEA/app/build/outputs/aar/keyman-engine-${BUILDER_CONFIGURATION}.aar +#### Build action definitions #### -if builder_start_action build:engine; then +do_build() { + local ENGINE_ASSETS="$KEYMAN_ROOT/android/KMEA/app/src/main/assets" # Copy KeymanWeb artifacts - echo "Copying Keyman Web artifacts" - cp "$KEYMAN_WEB_ROOT/build/app/webview/$CONFIG/keymanweb-webview.js" "$ENGINE_ASSETS/keymanweb-webview.js" - cp "$KEYMAN_WEB_ROOT/build/app/webview/$CONFIG/keymanweb-webview.js.map" "$ENGINE_ASSETS/keymanweb-webview.js.map" - cp "$KEYMAN_WEB_ROOT/build/app/webview/$CONFIG/map-polyfill.js" "$ENGINE_ASSETS/map-polyfill.js" - cp "$KEYMAN_WEB_ROOT/build/app/resources/osk/ajax-loader.gif" "$ENGINE_ASSETS/ajax-loader.gif" - cp "$KEYMAN_WEB_ROOT/build/app/resources/osk/kmwosk.css" "$ENGINE_ASSETS/kmwosk.css" - cp "$KEYMAN_WEB_ROOT/build/app/resources/osk/globe-hint.css" "$ENGINE_ASSETS/globe-hint.css" - cp "$KEYMAN_WEB_ROOT/build/app/resources/osk/keymanweb-osk.ttf" "$ENGINE_ASSETS/keymanweb-osk.ttf" + builder_echo "Copying Keyman Web artifacts" + cp "$KEYMAN_ROOT/web/build/app/webview/$BUILDER_CONFIGURATION/keymanweb-webview.js" "$ENGINE_ASSETS/" + cp "$KEYMAN_ROOT/web/build/app/webview/$BUILDER_CONFIGURATION/keymanweb-webview.js.map" "$ENGINE_ASSETS/" + cp "$KEYMAN_ROOT/web/build/app/webview/$BUILDER_CONFIGURATION/map-polyfill.js" "$ENGINE_ASSETS/" + cp "$KEYMAN_ROOT/web/build/app/resources/osk/ajax-loader.gif" "$ENGINE_ASSETS/" + cp "$KEYMAN_ROOT/web/build/app/resources/osk/kmwosk.css" "$ENGINE_ASSETS/" + cp "$KEYMAN_ROOT/web/build/app/resources/osk/globe-hint.css" "$ENGINE_ASSETS/" + cp "$KEYMAN_ROOT/web/build/app/resources/osk/keymanweb-osk.ttf" "$ENGINE_ASSETS/" cp "$KEYMAN_ROOT/common/web/sentry-manager/build/lib/index.js" "$ENGINE_ASSETS/keyman-sentry.js" - echo "Copying es6-shim polyfill" - cp "$KEYMAN_ROOT/node_modules/es6-shim/es6-shim.min.js" "$ENGINE_ASSETS/es6-shim.min.js" - - echo "BUILD_FLAGS $BUILD_FLAGS" - # Build without test - ./gradlew $DAEMON_FLAG clean $BUILD_FLAGS + builder_echo "Copying es6-shim polyfill" + cp "$KEYMAN_ROOT/node_modules/es6-shim/es6-shim.min.js" "$ENGINE_ASSETS/" - builder_finish_action success build:engine -fi + builder_echo "BUILD_FLAGS $BUILD_FLAGS" -if builder_start_action test:engine; then + # Build without test + ./gradlew $GRADLE_DAEMON $BUILD_FLAGS +} +do_test() { if builder_is_ci_build; then # Report JUnit test results to CI echo "$JUNIT_RESULTS" fi - echo "TEST_FLAGS: $TEST_FLAGS" - ./gradlew $DAEMON_FLAG $TEST_FLAGS + builder_echo "TEST_FLAGS: $TEST_FLAGS" + ./gradlew $GRADLE_DAEMON $TEST_FLAGS +} - builder_finish_action success test:engine -fi +builder_run_action clean:engine rm -rf build app/build +builder_run_action build:engine do_build +builder_run_action test:engine do_test diff --git a/android/Samples/KMSample1/app/build.gradle b/android/Samples/KMSample1/app/build.gradle index 395d6a69e55..4643d0e796e 100644 --- a/android/Samples/KMSample1/app/build.gradle +++ b/android/Samples/KMSample1/app/build.gradle @@ -2,6 +2,9 @@ plugins { id 'com.android.application' } +ext.rootPath = '../../../' +apply from: "$rootPath/version.gradle" + java { toolchain { languageVersion = JavaLanguageVersion.of(21) @@ -17,13 +20,22 @@ android { noCompress "kmp" } + buildFeatures { + buildConfig = true + } + defaultConfig { applicationId "com.keyman.kmsample1" minSdkVersion 21 targetSdkVersion 35 - versionCode 1 - versionName "1.0" + + // KEYMAN_VERSION_CODE, KEYMAN_VERSION_NAME, KEYMAN_VERSION_FOR_FILENAME from version.gradle + versionCode KEYMAN_VERSION_CODE as Integer + versionName KEYMAN_VERSION_NAME + buildConfigField "String", "KEYMAN_VERSION_ENVIRONMENT", "\""+KEYMAN_VERSION_ENVIRONMENT+"\"" + archivesBaseName = "KMSample1-$KEYMAN_VERSION_FOR_FILENAME" } + buildTypes { release { minifyEnabled false @@ -40,7 +52,6 @@ android { repositories { google() - } dependencies { diff --git a/android/Samples/KMSample1/build.gradle b/android/Samples/KMSample1/build.gradle index 18d4c46cd56..43ca9c592c9 100644 --- a/android/Samples/KMSample1/build.gradle +++ b/android/Samples/KMSample1/build.gradle @@ -3,7 +3,6 @@ buildscript { repositories { google() - jcenter() mavenCentral() } dependencies { @@ -17,7 +16,6 @@ buildscript { allprojects { repositories { google() - jcenter() mavenCentral() } } diff --git a/android/Samples/KMSample1/build.sh b/android/Samples/KMSample1/build.sh index d1d41bc2003..d6fca146531 100755 --- a/android/Samples/KMSample1/build.sh +++ b/android/Samples/KMSample1/build.sh @@ -1,21 +1,16 @@ #!/usr/bin/env bash +# Keyman is copyright (C) SIL Global. MIT License. # -# Samples: KMsample1 ## START STANDARD BUILD SCRIPT INCLUDE # adjust relative paths as necessary THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" . "${THIS_SCRIPT%/*}/../../../resources/build/builder-full.inc.sh" ## END STANDARD BUILD SCRIPT INCLUDE -. "$KEYMAN_ROOT/resources/build/utils.inc.sh" +. "${KEYMAN_ROOT}/android/android-utils.inc.sh" ################################ Main script ################################ -# Definition of global compile constants - -CONFIG="release" -SAMPLE_FLAGS="build" - builder_describe "Build KMSample1 app for Android." \ "@/android/KMEA" \ "clean" \ @@ -24,55 +19,19 @@ builder_describe "Build KMSample1 app for Android." \ "test" \ ":app KMSample1" -# parse before describe_outputs to check debug flags builder_parse "$@" -ARTIFACT="app-release-unsigned.apk" - -if builder_is_debug_build; then - builder_heading "### Debug config ####" - CONFIG="debug" - SAMPLE_FLAGS="assembleDebug" - ARTIFACT="app-$CONFIG.apk" -fi - +android_set_gradle_environment builder_describe_outputs \ - build:app /android/Samples/KMSample1/app/build/outputs/apk/$CONFIG/$ARTIFACT - - - -# Parse args - -if builder_is_ci_build; then - SAMPLE_FLAGS="$SAMPLE_FLAGS -no-daemon" -fi + build:app "/android/Samples/KMSample1/app/build/outputs/apk/${BUILDER_CONFIGURATION}/KMSample1-${KEYMAN_VERSION_FOR_FILENAME}-${ARCHIVE_TARGET}.apk" #### Build action definitions #### -# Check about cleaning artifact paths -if builder_start_action clean:app; then - rm -rf "$KEYMAN_ROOT/android/Samples/KMSample1/app/build/outputs" - builder_finish_action success clean:app -fi - -if builder_start_action configure:app; then - - builder_finish_action success configure:app -fi - -# Building KMSample1 -if builder_start_action build:app; then - - # Copy Keyman Engine for Android - cp "$KEYMAN_ROOT/android/KMEA/app/build/outputs/aar/keyman-engine-${CONFIG}.aar" "$KEYMAN_ROOT/android/Samples/KMSample1/app/libs/keyman-engine.aar" - - ./gradlew clean $SAMPLE_FLAGS - - builder_finish_action success build:app -fi +do_build() { + cp "$KEYMAN_ROOT/android/KMEA/app/build/outputs/aar/keyman-engine-${BUILDER_CONFIGURATION}.aar" "$KEYMAN_ROOT/android/Samples/KMSample1/app/libs/keyman-engine.aar" + ./gradlew $GRADLE_DAEMON $GRADLE_TARGET +} -if builder_start_action test:app; then - # TODO: define tests - builder_finish_action success test:app -fi +builder_run_action clean:app rm -rf build app/build +builder_run_action build:app do_build diff --git a/android/Samples/KMSample2/app/build.gradle b/android/Samples/KMSample2/app/build.gradle index 40e30c0025e..a3c9658056b 100644 --- a/android/Samples/KMSample2/app/build.gradle +++ b/android/Samples/KMSample2/app/build.gradle @@ -2,6 +2,9 @@ plugins { id 'com.android.application' } +ext.rootPath = '../../../' +apply from: "$rootPath/version.gradle" + java { toolchain { languageVersion = JavaLanguageVersion.of(21) @@ -17,13 +20,22 @@ android { noCompress "kmp" } + buildFeatures { + buildConfig = true + } + defaultConfig { applicationId "com.keyman.kmsample2" minSdkVersion 21 targetSdkVersion 35 - versionCode 1 - versionName "1.0" + + // KEYMAN_VERSION_CODE, KEYMAN_VERSION_NAME, KEYMAN_VERSION_FOR_FILENAME from version.gradle + versionCode KEYMAN_VERSION_CODE as Integer + versionName KEYMAN_VERSION_NAME + buildConfigField "String", "KEYMAN_VERSION_ENVIRONMENT", "\""+KEYMAN_VERSION_ENVIRONMENT+"\"" + archivesBaseName = "KMSample2-$KEYMAN_VERSION_FOR_FILENAME" } + buildTypes { release { minifyEnabled false diff --git a/android/Samples/KMSample2/build.gradle b/android/Samples/KMSample2/build.gradle index 18d4c46cd56..43ca9c592c9 100644 --- a/android/Samples/KMSample2/build.gradle +++ b/android/Samples/KMSample2/build.gradle @@ -3,7 +3,6 @@ buildscript { repositories { google() - jcenter() mavenCentral() } dependencies { @@ -17,7 +16,6 @@ buildscript { allprojects { repositories { google() - jcenter() mavenCentral() } } diff --git a/android/Samples/KMSample2/build.sh b/android/Samples/KMSample2/build.sh index 3c60795e8d5..14a9a49507a 100755 --- a/android/Samples/KMSample2/build.sh +++ b/android/Samples/KMSample2/build.sh @@ -1,21 +1,16 @@ #!/usr/bin/env bash +# Keyman is copyright (C) SIL Global. MIT License. # -# Samples: KMSample2 ## START STANDARD BUILD SCRIPT INCLUDE # adjust relative paths as necessary THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" . "${THIS_SCRIPT%/*}/../../../resources/build/builder-full.inc.sh" ## END STANDARD BUILD SCRIPT INCLUDE -. "$KEYMAN_ROOT/resources/build/utils.inc.sh" +. "${KEYMAN_ROOT}/android/android-utils.inc.sh" ################################ Main script ################################ -# Definition of global compile constants - -CONFIG="release" -SAMPLE_FLAGS="build" - builder_describe "Build KMSample2 app for Android." \ "@/android/KMEA" \ "clean" \ @@ -24,55 +19,19 @@ builder_describe "Build KMSample2 app for Android." \ "test" \ ":app KMSample2" -# parse before describe_outputs to check debug flags builder_parse "$@" -ARTIFACT="app-release-unsigned.apk" - -if builder_is_debug_build; then - builder_heading "### Debug config ####" - CONFIG="debug" - SAMPLE_FLAGS="assembleDebug" - ARTIFACT="app-$CONFIG.apk" -fi - - +android_set_gradle_environment builder_describe_outputs \ - build:app /android/Samples/KMSample2/app/build/outputs/apk/$CONFIG/$ARTIFACT - - - -# Parse args - -if builder_is_ci_build; then - SAMPLE_FLAGS="$SAMPLE_FLAGS -no-daemon" -fi + build:app "/android/Samples/KMSample2/app/build/outputs/apk/${BUILDER_CONFIGURATION}/KMSample2-${KEYMAN_VERSION_FOR_FILENAME}-${ARCHIVE_TARGET}.apk" #### Build action definitions #### -# Check about cleaning artifact paths -if builder_start_action clean:app; then - rm -rf "$KEYMAN_ROOT/android/Samples/KMSample2/app/build/outputs" - builder_finish_action success clean:app -fi - -if builder_start_action configure:app; then - - builder_finish_action success configure:app -fi - -# Building KMSample2 -if builder_start_action build:app; then - # Copy Keyman Engine for Android - cp "$KEYMAN_ROOT/android/KMEA/app/build/outputs/aar/keyman-engine-${CONFIG}.aar" "$KEYMAN_ROOT/android/Samples/KMSample2/app/libs/keyman-engine.aar" - - ./gradlew clean $SAMPLE_FLAGS - - builder_finish_action success build:app -fi +do_build() { + cp "$KEYMAN_ROOT/android/KMEA/app/build/outputs/aar/keyman-engine-${BUILDER_CONFIGURATION}.aar" "$KEYMAN_ROOT/android/Samples/KMSample2/app/libs/keyman-engine.aar" + ./gradlew $GRADLE_DAEMON $GRADLE_TARGET +} -if builder_start_action test:app; then - # TODO: define tests - builder_finish_action success test:app -fi +builder_run_action clean:app rm -rf build app/build +builder_run_action build:app do_build diff --git a/android/Tests/KeyboardHarness/app/build.gradle b/android/Tests/KeyboardHarness/app/build.gradle index d9502cfa647..070ee5c63b1 100644 --- a/android/Tests/KeyboardHarness/app/build.gradle +++ b/android/Tests/KeyboardHarness/app/build.gradle @@ -29,10 +29,11 @@ android { minSdkVersion 21 targetSdkVersion 35 - // KEYMAN_VERSION_CODE and KEYMAN_VERSION_NAME from version.gradle + // KEYMAN_VERSION_CODE, KEYMAN_VERSION_NAME, KEYMAN_VERSION_FOR_FILENAME from version.gradle versionCode KEYMAN_VERSION_CODE as Integer versionName KEYMAN_VERSION_NAME buildConfigField "String", "KEYMAN_VERSION_ENVIRONMENT", "\""+KEYMAN_VERSION_ENVIRONMENT+"\"" + archivesBaseName = "KeyboardHarness-$KEYMAN_VERSION_FOR_FILENAME" } buildTypes { release { diff --git a/android/Tests/KeyboardHarness/build.sh b/android/Tests/KeyboardHarness/build.sh index ccb7c99bc2f..2eb9a2f35f9 100755 --- a/android/Tests/KeyboardHarness/build.sh +++ b/android/Tests/KeyboardHarness/build.sh @@ -1,21 +1,16 @@ #!/usr/bin/env bash +# Keyman is copyright (C) SIL Global. MIT License. # -# Build Test app: KeyboardHarness ## START STANDARD BUILD SCRIPT INCLUDE # adjust relative paths as necessary THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" . "${THIS_SCRIPT%/*}/../../../resources/build/builder-full.inc.sh" ## END STANDARD BUILD SCRIPT INCLUDE -. "$KEYMAN_ROOT/resources/build/utils.inc.sh" +. "${KEYMAN_ROOT}/android/android-utils.inc.sh" ################################ Main script ################################ -# Definition of global compile constants -CONFIG="release" -BUILD_FLAGS="aR -x lint -x test" # Gradle build w/o test -TEST_FLAGS="-x aR lintRelease testRelease" # Gradle test w/o build - builder_describe "Build KeyboardHarness test app for Android." \ "@/android/KMEA" \ "clean" \ @@ -24,66 +19,19 @@ builder_describe "Build KeyboardHarness test app for Android." \ "test" \ ":app KeyboardHarness" -# parse before describe outputs to check debug flags builder_parse "$@" -ARTIFACT="app-release-unsigned.apk" - -if builder_is_debug_build; then - builder_heading "### Debug config ####" - CONFIG="debug" - BUILD_FLAGS="assembleDebug -x lintDebug -x testDebug" - TEST_FLAGS="-x assembleDebug lintDebug testDebug" - ARTIFACT="app-$CONFIG.apk" -fi - +android_set_gradle_environment builder_describe_outputs \ - build:app /android/Tests/KeyboardHarness/app/build/outputs/apk/$CONFIG/${ARTIFACT} - -#### Build - - -# -# Prevents 'clear' on exit of mingw64 bash shell -# -SHLVL=0 - - -# Parse args - -# Build flags that apply to all targets -if builder_is_ci_build; then - BUILD_FLAGS="$BUILD_FLAGS -no-daemon" - TEST_FLAGS="$TEST_FLAGS -no-daemon" -fi + build:app "/android/Tests/KeyboardHarness/app/build/outputs/apk/${BUILDER_CONFIGURATION}/KeyboardHarness-${KEYMAN_VERSION_FOR_FILENAME}-${ARCHIVE_TARGET}.apk" #### Build action definitions #### -# Check about cleaning artifact paths -if builder_start_action clean:app; then - rm -rf "$KEYMAN_ROOT/android/Tests/KeyboardHarness/app/build/outputs" - builder_finish_action success clean:app -fi - -if builder_start_action configure:app; then - - builder_finish_action success configure:app -fi - -# Building KeyboardHarness -if builder_start_action build:app; then - - # Copy Keyman Engine for Android - cp "$KEYMAN_ROOT/android/KMEA/app/build/outputs/aar/keyman-engine-${CONFIG}.aar" "$KEYMAN_ROOT/android/Tests/KeyboardHarness/app/libs/keyman-engine.aar" - - echo "BUILD_FLAGS: $BUILD_FLAGS" - ./gradlew clean $BUILD_FLAGS - builder_finish_action success build:app -fi +do_build() { + cp "$KEYMAN_ROOT/android/KMEA/app/build/outputs/aar/keyman-engine-${BUILDER_CONFIGURATION}.aar" ./app/libs/keyman-engine.aar + ./gradlew $GRADLE_DAEMON $GRADLE_TARGET +} -if builder_start_action test:app; then - echo "TEST_FLAGS $TEST_FLAGS" - # TODO: define tests - builder_finish_action success test:app -fi +builder_run_action clean:app rm -rf build app/build +builder_run_action build:app do_build diff --git a/android/android-utils.inc.sh b/android/android-utils.inc.sh new file mode 100644 index 00000000000..2e790b6e4ea --- /dev/null +++ b/android/android-utils.inc.sh @@ -0,0 +1,20 @@ +# shellcheck shell=bash +# Keyman is copyright (C) SIL Global. MIT License. + +function android_set_gradle_environment() { + if builder_is_debug_build; then + GRADLE_TARGET=assembleDebug + ARCHIVE_TARGET=debug + else + GRADLE_TARGET=build + ARCHIVE_TARGET=release-unsigned + fi + + if builder_is_ci_build; then + GRADLE_DAEMON=-no-daemon + else + GRADLE_DAEMON= + fi + + export GRADLE_TARGET GRADLE_DAEMON ARCHIVE_TARGET +} \ No newline at end of file diff --git a/android/build.sh b/android/build.sh index 54e61dd9a5b..926549101d5 100755 --- a/android/build.sh +++ b/android/build.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# Keyman is copyright (C) SIL Global. MIT License. # # Build Keyman Engine for Android, Keyman for Android, OEM FirstVoices Android app, # Samples: KMsample1 and KMSample2, Test - KeyboardHarness @@ -50,11 +51,6 @@ builder_describe \ builder_parse "$@" -function do_clean() { - builder_heading "Cleanup /android/upload" - rm -rf "${KEYMAN_ROOT}/android/upload" -} - function do_test_help() { check-markdown "${KEYMAN_ROOT}/android/docs/help" check-markdown "${KEYMAN_ROOT}/android/docs/engine" @@ -62,7 +58,6 @@ function do_test_help() { function archive_artifacts() { local UPLOAD_PATH KEYMAN_ENGINE_ANDROID_ZIP KEYMAN_APK FIRSTVOICES_APK - local KEYMAN_FULLY_VERSIONED_APK FIRSTVOICES_FULLY_VERSIONED_APK UPLOAD_PATH="${KEYMAN_ROOT}/android/upload/${KEYMAN_VERSION}" KEYMAN_ENGINE_ANDROID_ZIP="keyman-engine-android-${KEYMAN_VERSION}.zip" @@ -124,12 +119,13 @@ android_set_java_home # This script also responsible for cleaning up /android/upload builder_run_child_actions clean -builder_run_action clean do_clean +builder_run_action clean rm -rf "${KEYMAN_ROOT}/android/upload" builder_run_child_actions configure build test -builder_run_action test:help do_test_help +builder_run_action test:help do_test_help +# TODO: merge publish-* and archive to publish action? builder_run_child_actions publish-symbols publish-play-store -builder_run_action archive archive_artifacts +builder_run_action archive archive_artifacts diff --git a/android/version.gradle b/android/version.gradle index ea7342166c9..677f4926eff 100644 --- a/android/version.gradle +++ b/android/version.gradle @@ -88,6 +88,16 @@ def getVersionGitTag = { -> return "release@$KEYMAN_VERSION_NAME" } } + +def getVersionForFilename = { -> + String env_version_for_filename = System.getenv("KEYMAN_VERSION_FOR_FILENAME") + if(env_version_for_filename != null) { + return env_version_for_filename + } + // Building probably from IDE + return "$KEYMAN_VERSION_NAME" +} + ext { KEYMAN_VERSION_CODE=getVersionCode() KEYMAN_VERSION_MD=getVersionMD() @@ -95,6 +105,7 @@ ext { KEYMAN_VERSION_ENVIRONMENT=getVersionEnvironment() KEYMAN_VERSION_TAG=System.getenv("KEYMAN_VERSION_TAG") KEYMAN_VERSION_GIT_TAG=getVersionGitTag() + KEYMAN_VERSION_FOR_FILENAME=getVersionForFilename() } //println "version.gradle: KEYMAN_VERSION_TAG: " + KEYMAN_VERSION_TAG \ No newline at end of file diff --git a/oem/firstvoices/android/build.gradle b/oem/firstvoices/android/build.gradle index 3f20182cf69..f24c9cdc53a 100644 --- a/oem/firstvoices/android/build.gradle +++ b/oem/firstvoices/android/build.gradle @@ -16,7 +16,6 @@ buildscript { allprojects { repositories { google() - jcenter() mavenCentral() } } diff --git a/resources/builder.inc.sh b/resources/builder.inc.sh index f316b5ade50..2b81b7f52fd 100755 --- a/resources/builder.inc.sh +++ b/resources/builder.inc.sh @@ -1756,9 +1756,9 @@ _builder_parse_expanded_parameters() { fi if builder_is_debug_build; then - BUILDER_CONFIGURATION=debug + export BUILDER_CONFIGURATION=debug else - BUILDER_CONFIGURATION=release + export BUILDER_CONFIGURATION=release fi _builder_print_internal_debug_info From d295e0faf5bc41a78dc0846c8315e8ecd10cedb0 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Mon, 13 Jul 2026 16:47:48 +0200 Subject: [PATCH 35/44] maint(resources): add comment for debug printing Co-authored-by: Eberhard Beilharz --- resources/build/builder-basic.inc.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/build/builder-basic.inc.sh b/resources/build/builder-basic.inc.sh index 8e0954f5c52..c3108ede0e7 100644 --- a/resources/build/builder-basic.inc.sh +++ b/resources/build/builder-basic.inc.sh @@ -182,6 +182,7 @@ function _builder_basic_print_version_utils_debug() { fi if [[ ${_builder_basic_environment_printed:-false} == true ]]; then + # report this only once per build return fi From 896387a15a536f95dad22e873d7a155de0065315 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Mon, 13 Jul 2026 18:21:35 +0200 Subject: [PATCH 36/44] chore(linux): remove questing, add stonking Ubuntu 25.10 Questing is EOL. Instead add upcoming Ubuntu 26.10 Stonking for Launchpad builds. Fixes: #16235 Build-bot: skip Test-bot: skip --- .github/workflows/deb-packaging.yml | 2 +- linux/scripts/launchpad.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deb-packaging.yml b/.github/workflows/deb-packaging.yml index 9a990634b20..6ce42cf6d56 100644 --- a/.github/workflows/deb-packaging.yml +++ b/.github/workflows/deb-packaging.yml @@ -137,7 +137,7 @@ jobs: strategy: fail-fast: true matrix: - dist: [jammy, noble, questing] + dist: [jammy, noble, resolute] steps: - name: Checkout diff --git a/linux/scripts/launchpad.sh b/linux/scripts/launchpad.sh index b516d1eb53b..2789c9ec7ba 100755 --- a/linux/scripts/launchpad.sh +++ b/linux/scripts/launchpad.sh @@ -98,7 +98,7 @@ function upload_with_retry() { return 1 } -distributions="${DIST:-jammy noble questing resolute}" +distributions="${DIST:-jammy noble resolute stonking}" packageversion="${PACKAGEVERSION:-1~sil1}" retries="${RETRIES:-5}" From b04e28a43824ecd48e8c4bd4215c46bea9cf72e8 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Mon, 13 Jul 2026 18:44:32 +0200 Subject: [PATCH 37/44] chore(android): log legacy cloud keyboards This adds a Sentry log if a user is still using legacy cloud keyboards. We do this so that we will learn if there are still users out there or if we can remove the code that deals with those keyboards. Test-bot: skip --- .../java/com/keyman/engine/data/Keyboard.java | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/data/Keyboard.java b/android/KMEA/app/src/main/java/com/keyman/engine/data/Keyboard.java index 844959fb547..c0a275bbef2 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/data/Keyboard.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/data/Keyboard.java @@ -49,6 +49,9 @@ public class Keyboard extends LanguageResource implements Serializable { */ public Keyboard(JSONObject installedObj) { this.fromJSON(installedObj); + if (!FileUtils.hasFontExtension(this.font)) { + logLegacyKeyboard(this.font); + } } /** @@ -79,6 +82,10 @@ public Keyboard(JSONObject languageJSON, JSONObject keyboardJSON) { this.helpLink = keyboardJSON.optString(KMManager.KMKey_CustomHelpLink, KMString.format(HELP_URL_FORMATSTR, HELP_URL_HOST, this.resourceID, this.version)); + + if (!FileUtils.hasFontExtension(this.font)) { + logLegacyKeyboard(this.font); + } } catch (JSONException e) { KMLog.LogException(TAG, "Keyboard exception parsing JSON: ", e); } @@ -96,7 +103,11 @@ public Keyboard(String packageID, String keyboardID, String keyboardName, this.isNewKeyboard = isNewKeyboard; this.font = (font != null) ? font : ""; this.oskFont = (oskFont != null) ? oskFont : ""; - } + + if (!FileUtils.hasFontExtension(this.font)) { + logLegacyKeyboard(this.font); + } + } public Keyboard(Keyboard k) { super(k.getPackageID(), k.getKeyboardID(), k.getKeyboardName(), @@ -108,6 +119,23 @@ public Keyboard(Keyboard k) { this.displayName = k.getDisplayName(); } + private void logLegacyKeyboard(String font) { + if (font == null || font.isEmpty()) { + return; + } + try { + // Create a Sentry log entry if there are still users out there that use + // legacy cloud keyboard. See KMKeyboard.makeFontObject(). + JSONObject fontObj = new JSONObject(font); + Object obj = fontObj.get(KMManager.KMKey_FontFiles); + if (obj instanceof String || obj instanceof JSONArray) { + KMLog.LogInfo(TAG, "Constructing legacy keyboard: " + this.packageID + "/" + this.resourceID); + } + } catch (JSONException e) { + // Not a JSON object, so it's not a legacy keyboard. + } + } + public String getKeyboardID() { return getResourceID(); } public String getKeyboardName() { return getResourceName(); } From 82ab6477e54b89851bd1f628449daabc8e5b380a Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Mon, 13 Jul 2026 19:15:10 +0200 Subject: [PATCH 38/44] chore(android): keep handling of legacy keyboards Reverts part of commit d0e16496304a877933cd78661e73fa17f9857df4 until we know if there are still users out there that are using legacy cloud keyboards. See #16237. Build-bot: skip build:android --- .../java/com/keyman/engine/KMKeyboard.java | 86 ++++++++++++++----- 1 file changed, 64 insertions(+), 22 deletions(-) diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java index ca9edff90b5..94c1aaa2aee 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java @@ -1036,39 +1036,81 @@ public void onDismiss() { } /** - * Create a JSON object consisting of the font family and the URL of the - * font file on the local device. + * Create a JSON object consisting of the font family and the URLs of the + * font files on the local device. * - * @param fontFileName A string containing the font filename (with an - * extension recognized as font) - * @param packageID The package ID of the keyboard + * The `font` parameter can either be the filename of the font (with an + * extension recognized as font), or a Font object or JSON string. + * In the former case a new JSON object is created with the font family + * derived from the filename, and the font filename prefixed with path + * to the fonts. + * In the latter case the legacy `sources` key is renamed to `files`. + * If `files` is a single string it will be prefixed with the path to the + * fonts. If `files` is an array, the array is iterated until finding + * the first file with a font extension which is then prefixed with the + * path to the fonts. * - * @return JSONObject of modified font information with full path. If fontFileName + * @param font A string containing either the font filename or a font + * JSON object as a string + * @param packageID The package ID of the keyboard + * + * @return JSONObject of modified font information with full paths. If font * is invalid, return `null`. */ - private JSONObject makeFontObject(String fontFileName, String packageID) { - - if(fontFileName == null || fontFileName.equals("")) { - return null; - } + private JSONObject makeFontObject(String font, String packageID) { - if (!FileUtils.hasFontExtension(fontFileName)) { - KMLog.LogInfo(TAG, "makeFontObject: Got font without font extension: " + fontFileName); + if(font == null || font.equals("")) { return null; } try { - JSONObject font = new JSONObject(); - font.put(KMManager.KMKey_FontFamily, fontFileName.substring(0, fontFileName.length()-4)); - JSONArray files = new JSONArray(); - String fontRoot = KMManager.isDefaultFont(fontFileName) ? getDataRootUrl() : getPackageRootUrl(packageID); - files.put(fontRoot + fontFileName); - font.put(KMManager.KMKey_FontFiles, files); - return font; + if (FileUtils.hasFontExtension(font)) { + JSONObject jfont = new JSONObject(); + jfont.put(KMManager.KMKey_FontFamily, font.substring(0, font.length()-4)); + JSONArray jfiles = new JSONArray(); + String fontRoot = KMManager.isDefaultFont(font) ? getDataRootUrl() : getPackageRootUrl(packageID); + jfiles.put(fontRoot + font); + jfont.put(KMManager.KMKey_FontFiles, jfiles); + return jfont; + } + + // REVIEW: Why do we need the complicated code below? Can this still + // happen, or can we remove it? (see also getFontFilename) + KMLog.LogInfo(TAG, "Got font without font extension: " + font); + + JSONObject fontObj = new JSONObject(font); + + // Replace "sources" key with "files" + if (fontObj.has(KMManager.KMKey_FontSource)) { + fontObj.put(KMManager.KMKey_FontFiles, fontObj.get(KMManager.KMKey_FontSource)); + fontObj.remove(KMManager.KMKey_FontSource); + } + + Object obj = fontObj.get(KMManager.KMKey_FontFiles); + if (obj instanceof String) { + String fontFile = fontObj.getString(KMManager.KMKey_FontFiles); + String fontRoot = KMManager.isDefaultFont(fontFile) ? getDataRootUrl() : getPackageRootUrl(packageID); + fontObj.put(KMManager.KMKey_FontFiles, fontRoot + obj); + return fontObj; + } else if (obj instanceof JSONArray) { + JSONArray sourceArray = fontObj.optJSONArray(KMManager.KMKey_FontFiles); + if (sourceArray != null) { + for (int i = 0; i < sourceArray.length(); i++) { + String fontFile = sourceArray.getString(i); + if (FileUtils.hasFontExtension(fontFile)) { + String fontRoot = KMManager.isDefaultFont(fontFile) ? getDataRootUrl() : getPackageRootUrl(packageID); + fontObj.put(KMManager.KMKey_FontFiles, fontRoot + fontFile); + fontObj.remove(KMManager.KMKey_FontSource); + return fontObj; + } + } + } + } } catch (JSONException e) { - KMLog.LogException(TAG, "Failed to make font for '"+fontFileName+"'", e); - return null; + KMLog.LogException(TAG, "Failed to make font for '"+font+"'", e); } + + return null; } protected void showHelpBubble() { From 88731e51fc6df7ffcff5d7334b737690d90322c6 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Mon, 13 Jul 2026 19:29:21 +0200 Subject: [PATCH 39/44] chore(android): more cleanup --- .../main/java/com/keyman/engine/KMKeyboard.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java index 94c1aaa2aee..af948f1a45d 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java @@ -926,22 +926,22 @@ private void saveCurrentKeyboardIndex() { /** * Return the full path to the font file. If the font is invalid, return empty string. - * @param font String - Font filename + * @param fontFilename String - Font filename * @param packageID String - Package ID - * @return String - Full local path to the font file. If font is invalid, return "". + * @return String - Full path to the font file. If fontFilename is invalid, return "". */ - private String getFontFilename(String font, String packageID) { - if(font == null || font.equals("")) { + private String getFontFilename(String fontFilename, String packageID) { + if(fontFilename == null || fontFilename.equals("")) { return ""; } - if (!FileUtils.hasFontExtension(font)) { + if (!FileUtils.hasFontExtension(fontFilename)) { // QUESTION: do we log this? return ""; } - String fontRoot = KMManager.isDefaultFont(font) ? getDataRootPath() : getPackageRootPath(packageID); - return fontRoot + font; + String fontRoot = KMManager.isDefaultFont(fontFilename) ? getDataRootPath() : getPackageRootPath(packageID); + return fontRoot + fontFilename; } @SuppressLint("InflateParams") From 1174752071fc1d6718fa8b37abfe6f331f9e8093 Mon Sep 17 00:00:00 2001 From: Keyman Build Agent Date: Mon, 13 Jul 2026 13:01:55 -0500 Subject: [PATCH 40/44] auto: increment master version to 19.0.257 Test-bot: skip Build-bot: skip --- HISTORY.md | 5 +++++ VERSION.md | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 82497b52a93..0ef4503a94a 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,10 @@ # Keyman Version History +## 19.0.256 alpha 2026-07-13 + +* fix(android): add permissions for Sentry and set default keyboard (#16216) +* maint(resources): add extra debug reporting for builder (#16230) + ## 19.0.255 alpha 2026-07-11 * maint(android): use KEYMAN_TIER instead of TIER.md (#16219) diff --git a/VERSION.md b/VERSION.md index 3b84a539089..83562dee3b7 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -19.0.256 \ No newline at end of file +19.0.257 \ No newline at end of file From 9bdfe0eaa9cea0bc09b0733e4f427a56ebe0843f Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Tue, 14 Jul 2026 16:48:46 +0200 Subject: [PATCH 41/44] chore(android): address code review comments Legacy cloud installed keyboards don't have a package, so we can check for `KMDefault_UndefinedPackageID` to identify them. --- .../java/com/keyman/engine/data/Keyboard.java | 31 +++++-------------- 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/data/Keyboard.java b/android/KMEA/app/src/main/java/com/keyman/engine/data/Keyboard.java index c0a275bbef2..896fb23e965 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/data/Keyboard.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/data/Keyboard.java @@ -49,9 +49,7 @@ public class Keyboard extends LanguageResource implements Serializable { */ public Keyboard(JSONObject installedObj) { this.fromJSON(installedObj); - if (!FileUtils.hasFontExtension(this.font)) { - logLegacyKeyboard(this.font); - } + logIfLegacyKeyboard(); } /** @@ -83,9 +81,7 @@ public Keyboard(JSONObject languageJSON, JSONObject keyboardJSON) { this.helpLink = keyboardJSON.optString(KMManager.KMKey_CustomHelpLink, KMString.format(HELP_URL_FORMATSTR, HELP_URL_HOST, this.resourceID, this.version)); - if (!FileUtils.hasFontExtension(this.font)) { - logLegacyKeyboard(this.font); - } + logIfLegacyKeyboard(); } catch (JSONException e) { KMLog.LogException(TAG, "Keyboard exception parsing JSON: ", e); } @@ -104,10 +100,8 @@ public Keyboard(String packageID, String keyboardID, String keyboardName, this.font = (font != null) ? font : ""; this.oskFont = (oskFont != null) ? oskFont : ""; - if (!FileUtils.hasFontExtension(this.font)) { - logLegacyKeyboard(this.font); - } - } + logIfLegacyKeyboard(); + } public Keyboard(Keyboard k) { super(k.getPackageID(), k.getKeyboardID(), k.getKeyboardName(), @@ -119,20 +113,9 @@ public Keyboard(Keyboard k) { this.displayName = k.getDisplayName(); } - private void logLegacyKeyboard(String font) { - if (font == null || font.isEmpty()) { - return; - } - try { - // Create a Sentry log entry if there are still users out there that use - // legacy cloud keyboard. See KMKeyboard.makeFontObject(). - JSONObject fontObj = new JSONObject(font); - Object obj = fontObj.get(KMManager.KMKey_FontFiles); - if (obj instanceof String || obj instanceof JSONArray) { - KMLog.LogInfo(TAG, "Constructing legacy keyboard: " + this.packageID + "/" + this.resourceID); - } - } catch (JSONException e) { - // Not a JSON object, so it's not a legacy keyboard. + private void logIfLegacyKeyboard() { + if (this.packageID.equals(KMManager.KMDefault_UndefinedPackageID)) { + KMLog.LogInfo(TAG, "Constructing legacy keyboard: " + this.resourceID); } } From a93467a3a01e31a3225ac7fc9c924e217a966a51 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Tue, 14 Jul 2026 18:23:38 +0200 Subject: [PATCH 42/44] chore(android): remove doc for non-existing deprecated functions Several functions got deprecated and replaced with a function with same name and parameters but different return type. The original function no longer exists. This change removes the documentation for those no longer existing deprecated functions. Also update the documentation for two deprecated functions to show what should be used instead. Build-bot: skip Test-bot: skip --- android/docs/engine/KMManager/addKeyboard.md | 33 ------------------- .../KMManager/getCurrentKeyboardInfo.md | 28 ---------------- .../KMManager/getKeyboardFontFilename.md | 3 ++ .../KMManager/getKeyboardFontTypeface.md | 3 ++ .../docs/engine/KMManager/getKeyboardInfo.md | 29 ---------------- .../docs/engine/KMManager/getKeyboardsList.md | 27 --------------- 6 files changed, 6 insertions(+), 117 deletions(-) diff --git a/android/docs/engine/KMManager/addKeyboard.md b/android/docs/engine/KMManager/addKeyboard.md index f03048da470..7cbb355b7f7 100644 --- a/android/docs/engine/KMManager/addKeyboard.md +++ b/android/docs/engine/KMManager/addKeyboard.md @@ -32,39 +32,6 @@ can be selected from the keyboards menu. If the keyboard with same keyboard ID and language ID exists, it updates the existing keyboard info. - - ------------------------------------------------------------------------- - -## Syntax (Deprecated) - -``` javascript -KMManager.addKeyboard(Context context, HashMap keyboardInfo) -``` - -### Parameters - -`context` -: The context. - -`keyboardInfo` -: A dictionary of keyboard information with keys and values defined as - `HashMap`. - -### Returns - -Returns `true` if the keyboard was added successfully, `false` -otherwise. - -## Description - -Use this method to include a keyboard in the keyboards list so that it -can be selected from the keyboards menu. If the keyboard with same -keyboard ID and language ID exists, it updates the existing keyboard -info. - - - ------------------------------------------------------------------------ ## Examples diff --git a/android/docs/engine/KMManager/getCurrentKeyboardInfo.md b/android/docs/engine/KMManager/getCurrentKeyboardInfo.md index 4eb789e736d..9aceb3653b4 100644 --- a/android/docs/engine/KMManager/getCurrentKeyboardInfo.md +++ b/android/docs/engine/KMManager/getCurrentKeyboardInfo.md @@ -28,34 +28,6 @@ Use this method to get details of the currently selected keyboard. Details include package ID, keyboard ID, language ID, keyboard name, language name and fonts. - - ------------------------------------------------------------------------- - -## Syntax (Deprecated) - -``` javascript -KMManager.getCurrentKeyboardInfo(Context context) -``` - -### Parameters - -`context` -: The context. - -### Returns - -(Deprecated) Returns an information dictionary of the current keyboard -with keys and values defined as `HashMap`. - -## Description - -Use this method to get details of the currently selected keyboard. -Details include keyboard ID, language ID, keyboard name, language name -and fonts. - - - ------------------------------------------------------------------------ ## Examples diff --git a/android/docs/engine/KMManager/getKeyboardFontFilename.md b/android/docs/engine/KMManager/getKeyboardFontFilename.md index 086922224d4..005e6264a35 100644 --- a/android/docs/engine/KMManager/getKeyboardFontFilename.md +++ b/android/docs/engine/KMManager/getKeyboardFontFilename.md @@ -22,6 +22,8 @@ empty string otherwise. Use this method to get the font filename of the selected keyboard. +Deprecated. Use `getKeyboardTextFontFilename()` instead. + ## Examples ### Example: Using `getKeyboardFontFilename()` @@ -34,5 +36,6 @@ The following script illustrate the use of `getKeyboardFontFilename()`: ## See also +- [`getKeyboardTextFontFilename()`](getKeyboardTextFontFilename) - [`getKeyboardFontTypeface()` (Deprecated)](getKeyboardFontTypeface) - [`getFontTypeface()`](getFontTypeface) diff --git a/android/docs/engine/KMManager/getKeyboardFontTypeface.md b/android/docs/engine/KMManager/getKeyboardFontTypeface.md index 1f414ce14fd..a2858141e06 100644 --- a/android/docs/engine/KMManager/getKeyboardFontTypeface.md +++ b/android/docs/engine/KMManager/getKeyboardFontTypeface.md @@ -28,6 +28,8 @@ exists, `null` otherwise. Use this method to create a new typeface from the selected keyboard's font if it has any. +Deprecated. Use `getKeyboardTextFontFilename()` instead. + ## Examples ### Example: Using `getKeyboardFontTypeface()` @@ -44,3 +46,4 @@ The following script illustrate the use of `getKeyboardFontTypeface()`: - [`getFontTypeface()`](getFontTypeface) - [`getKeyboardFontFilename()` (Deprecated)](getKeyboardFontFilename) +- [`getKeyboardTextFontFilename()`](getKeyboardTextFontFilename) diff --git a/android/docs/engine/KMManager/getKeyboardInfo.md b/android/docs/engine/KMManager/getKeyboardInfo.md index 5838e0ca233..859c4b3bb7c 100644 --- a/android/docs/engine/KMManager/getKeyboardInfo.md +++ b/android/docs/engine/KMManager/getKeyboardInfo.md @@ -31,35 +31,6 @@ Use this method to get details of the keyboard at given position in keyboards list. Details include keyboard ID, language ID, keyboard name, language name and fonts. - - ------------------------------------------------------------------------- - -## Syntax (Deprecated) - -``` javascript -KMManager.getKeyboardInfo(Context context, int index) -``` - -### Parameters - -`context` -: The context. - -`index` -: 0-based position of the keyboard in keyboards list. - -### Returns - -(Deprecated) Returns an information dictionary of the specified keyboard -with keys and values defined as `HashMap`. - -## Description - -Use this method to get details of the keyboard at given position in -keyboards list. Details include keyboard ID, language ID, keyboard name, -language name and fonts. - ## Examples ### Example: Using `getKeyboardInfo()` diff --git a/android/docs/engine/KMManager/getKeyboardsList.md b/android/docs/engine/KMManager/getKeyboardsList.md index 26c520953b7..4f38761665c 100644 --- a/android/docs/engine/KMManager/getKeyboardsList.md +++ b/android/docs/engine/KMManager/getKeyboardsList.md @@ -26,33 +26,6 @@ otherwise. Use this method to get details of all keyboard's in keyboards menu. - - ------------------------------------------------------------------------- - -## Syntax (Deprecated) - -``` javascript -KMManager.getKeyboardsList(Context context) -``` - -### Parameters - -`context` -: The context. - -### Returns - -(Deprecated) Returns keyboards list as -`ArrayList>` if it exists, `null` -otherwise. - -## Description - -Use this method to get details of all keyboard's in keyboards menu. - - - ------------------------------------------------------------------------ ## Examples From b0a4b441639d20c635d99ba0ba569b087d93757f Mon Sep 17 00:00:00 2001 From: Keyman Build Agent Date: Tue, 14 Jul 2026 13:02:40 -0500 Subject: [PATCH 43/44] auto: increment master version to 19.0.258 Test-bot: skip Build-bot: skip --- HISTORY.md | 9 +++++++++ VERSION.md | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 0ef4503a94a..01941648ba8 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,14 @@ # Keyman Version History +## 19.0.257 alpha 2026-07-14 + +* chore(linux): remove questing, add stonking (#16236) +* chore(android): cleanup Android build scripts and artifact filenames (#16234) +* chore(web): declare scope for stubAndKeyboardCache members (#16221) +* fix(android): improve clarity of keyboard script error popup (#16228) +* chore(android): log legacy cloud keyboards (#16237) +* chore(android): cleanup font variable names, remove obsolete code (#16211) + ## 19.0.256 alpha 2026-07-13 * fix(android): add permissions for Sentry and set default keyboard (#16216) diff --git a/VERSION.md b/VERSION.md index 83562dee3b7..24c70fd57c1 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -19.0.257 \ No newline at end of file +19.0.258 \ No newline at end of file From 043e1cadd5a3a74115a22dcdab94b59d0c1a90f5 Mon Sep 17 00:00:00 2001 From: Keyman Build Agent Date: Wed, 15 Jul 2026 13:01:29 -0500 Subject: [PATCH 44/44] auto: increment master version to 19.0.259 Test-bot: skip Build-bot: skip --- HISTORY.md | 4 ++++ VERSION.md | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 01941648ba8..399e3077f2b 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,9 @@ # Keyman Version History +## 19.0.258 alpha 2026-07-15 + +* chore(android): remove doc for non-existing deprecated functions (#16244) + ## 19.0.257 alpha 2026-07-14 * chore(linux): remove questing, add stonking (#16236) diff --git a/VERSION.md b/VERSION.md index 24c70fd57c1..48c4023fd88 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -19.0.258 \ No newline at end of file +19.0.259 \ No newline at end of file