diff --git a/HISTORY.md b/HISTORY.md index 7cf85efc25..3d8156b69b 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,15 @@ # Keyman Version History +## 19.0.290 alpha 2026-09-23 + +* fix(windows): set basekeyboard as current user not the admin user on an elevated process (#16162) +* fix(windows): add non-admin user install with different base keyboard (#16528) + +## 19.0.289 alpha 2026-09-21 + +* fix(android): suppress autocorrection of hardware keystroke input (#16606) +* change(web): add lexical weighting to prediction search (#16600) + ## 19.0.288 alpha 2026-09-18 * change(web): remove TokenizationCorrector class only used in epic/boundary-correction (#16581) @@ -1739,6 +1749,27 @@ * refactor(windows): rename `TKeymanMutex.MutexOwned` to `TakeOwnership` and add `ReleaseOwnership` (#13168) * chore: increment to alpha 19.0 (#13187) +## 18.0.252 stable 2026-09-22 + +* change(android): use webview asset loader (#16628) +* chore: revert "fix(web): retry importScripts NetworkErrors in attempt to mitigate errors loading models" (#16630) + +## 18.0.251 stable 2026-09-21 + +* maint(android): update Android Target API to 36 (#16620) +* +## 18.0.250 stable 2026-09-21 + +* fix(linux): fix memory problem (#15828) +* chore: update fast-xml-parser to 5.8.0 (#15953) +* fix(linux): add dependency on `procps` (#15980) +* chore(ios): update first voices distribution certificate (#16007) +* chore(linux): release v18 on Ubuntu 26.04 Resolute (#16078) +* chore: update multi-labeler to 5.0.0 (#16101) +* chore(android): log legacy cloud keyboards (#16243) +* fix(web): handle 'unload' message while attempting model 'load' (#16609) +* fix(web): retry importScripts NetworkErrors in attempt to mitigate errors loading models (#16604) + ## 18.0.249 stable 2026-03-27 * chore(linux): Update debian changelog (#15717) diff --git a/VERSION.md b/VERSION.md index 4ae56c84c6..cdb78fdb4f 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -19.0.289 \ No newline at end of file +19.0.291 \ No newline at end of file diff --git a/android/.gitignore b/android/.gitignore index e822a25c2b..7c298cad5c 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -37,10 +37,8 @@ KMEA/**/assets/keymanandroid.js KMEA/**/assets/keyman.js.map KMEA/**/assets/keymanweb-webview.js KMEA/**/assets/keymanweb-webview.js.map -KMEA/**/assets/map-polyfill.js KMEA/**/assets/sentry.min.js KMEA/**/assets/keyman-sentry.js -KMEA/**/assets/es6-shim.min.js KMEA/**/assets/kmwosk.css KMEA/**/assets/globe-hint.css KMEA/**/assets/resources diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java b/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java index 80701ad683..b0e8661b95 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java @@ -380,9 +380,7 @@ public final class KMManager { protected static final String KMFilename_KmwCss = "kmwosk.css"; protected static final String KMFilename_KmwGlobeHintCss = "globe-hint.css"; protected static final String KMFilename_Osk_Ttf_Font = "keymanweb-osk.ttf"; - protected static final String KMFilename_JSPolyfill = "es6-shim.min.js"; protected static final String KMFilename_JSPolyfill2 = "other-polyfills.js"; - protected static final String KMFilename_JSPolyfill3 = "map-polyfill.js"; // Deprecated by KeyboardController.KMFilename_Installed_KeyboardsList public static final String KMFilename_KeyboardsList = "keyboards_list.dat"; diff --git a/android/KMEA/build.sh b/android/KMEA/build.sh index 66471c0bbb..14dc821f19 100755 --- a/android/KMEA/build.sh +++ b/android/KMEA/build.sh @@ -55,16 +55,12 @@ do_build() { 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" - builder_echo "Copying es6-shim polyfill" - cp "$KEYMAN_ROOT/node_modules/es6-shim/es6-shim.min.js" "$ENGINE_ASSETS/" - builder_echo "BUILD_FLAGS $BUILD_FLAGS" # Build without test diff --git a/common/test/resources/test-runner-TC-reporter.mjs b/common/test/resources/test-runner-TC-reporter.mjs index 33198592af..054f6b2aa4 100644 --- a/common/test/resources/test-runner-TC-reporter.mjs +++ b/common/test/resources/test-runner-TC-reporter.mjs @@ -63,12 +63,12 @@ export default function teamcityReporter({ name="Web Test Runner JavaScript test } else { summary.failed++; const message = test.error ? `message='${e(test.error.message)}'` : ''; - const details = test.error ? `\ndetails='${e(test.error.stack)}\n`: ''; + const details = test.error ? `details='${e(test.error.stack)}'`: ''; if(test.error?.actual !== undefined && test.error?.expected !== undefined) { - logger.log(`##teamcity[testFailed type='comparisonFailure' name='${e(test.name)}' ${e(message)}] ${e(details)} expected='${e(test.error?.expected)}' actual='${e(test.error?.actual)}']`); + logger.log(`##teamcity[testFailed type='comparisonFailure' name='${e(test.name)}' ${message} ${details} expected='${e(test.error?.expected)}' actual='${e(test.error?.actual)}']`); } - logger.log(`##teamcity[testFailed name='${e(test.name)}' ${e(message)}] ${e(details)}']`); + logger.log(`##teamcity[testFailed name='${e(test.name)}' ${message} ${details}]`); } logger.log(`##teamcity[testFinished name='${e(test.name)}' duration='${e(test.duration ?? 0)}']`); diff --git a/common/windows/delphi/general/utilfiletypes.pas b/common/windows/delphi/general/utilfiletypes.pas index f34be0933d..28faa8804c 100644 --- a/common/windows/delphi/general/utilfiletypes.pas +++ b/common/windows/delphi/general/utilfiletypes.pas @@ -91,6 +91,27 @@ function IsProjectFile(const FileName: string): Boolean; function IsKeyboardFile(const FileName: string): Boolean; function RemoveFileExtension(Filename, Extension: string): string; +(** + * Builds the compiled keyboard filename by inserting the base keyboard ID + * before the .kmx extension. + * + * @param KeyboardFileName Keyboard filename, in the form '[path\]keyboardid[.kmx]' + * @param BaseKeyboardIDHex Base keyboard KLID in eight digit hexadecimal form + * @return Compiled keyboard filename, in the form '[path\]keyboardid-.kmx' + *) +function BuildKeyboardFilenameWithBaseKeyboardID(const KeyboardFileName: string; BaseKeyboardIDHex: string): string; + +(** + * Builds the dead-key compiled keyboard filename by inserting the base + * keyboard ID and -d suffix before the .kmx extension. + * + * @param KeyboardFileName Keyboard filename, in the form '[path]\keyboardid[.kmx]' + * @param BaseKeyboardIDHex Base keyboard KLID in eight digit hexadecimal form + * @return Dead-key compiled keyboard filename, in the form '[path\]keyboardid--d.kmx' + *) +function BuildKeyboardFilenameWithBaseKeyboardIDAndDeadkey(const KeyboardFileName: string; BaseKeyboardIDHex: string): string; + + type TKeymanFileTypeInfo = class public @@ -208,4 +229,14 @@ begin SameText(ExtractFileExt(Filename), ExtractFileExt(PackageFile_Welcome))); end; +function BuildKeyboardFilenameWithBaseKeyboardID(const KeyboardFileName: string; BaseKeyboardIDHex: string): string; +begin + Result := ChangeFileExt(KeyboardFileName, '') + '-' + BaseKeyboardIDHex + '.kmx'; +end; + +function BuildKeyboardFilenameWithBaseKeyboardIDAndDeadkey(const KeyboardFileName: string; BaseKeyboardIDHex: string): string; +begin + Result := ChangeFileExt(KeyboardFileName, '') + '-' + BaseKeyboardIDHex + '-d.kmx'; +end; + end. diff --git a/common/windows/delphi/general/utilstr.pas b/common/windows/delphi/general/utilstr.pas index 01e085b8a8..4cd175e360 100644 --- a/common/windows/delphi/general/utilstr.pas +++ b/common/windows/delphi/general/utilstr.pas @@ -1,18 +1,18 @@ (* Name: utilstr Copyright: Copyright (C) SIL International. - Documentation: - Description: + Documentation: + Description: Create Date: 1 Aug 2006 Modified Date: 8 Jun 2012 Authors: mcdurdin - Related Files: - Dependencies: + Related Files: + Dependencies: - Bugs: - Todo: - Notes: + Bugs: + Todo: + Notes: History: 01 Aug 2006 - mcdurdin - Refactor util functions into multiple units 23 Aug 2006 - mcdurdin - Add StringToExtString and WideQuotedStr functions 14 Sep 2006 - mcdurdin - Add RectToString, StringToRect, use widestrings for some functions @@ -58,8 +58,6 @@ function GetTokenFromCaret(line: string; var selx, sellen: Integer): string; function WideQuotedStr(const str: WideString): WideString; deprecated; // I3310 - - implementation uses @@ -78,7 +76,7 @@ begin Result := ''; Exit; end; - + if s[1] = '"' then begin Delete(s,1,1); diff --git a/developer/src/tike/xml/layoutbuilder/src/builder.js b/developer/src/tike/xml/layoutbuilder/src/builder.js index b81707fa3c..486bce81f9 100644 --- a/developer/src/tike/xml/layoutbuilder/src/builder.js +++ b/developer/src/tike/xml/layoutbuilder/src/builder.js @@ -508,8 +508,16 @@ $(function() { this.selectLayer = function (val) { let selection = builder.saveSelection(); - if(val) $('#selLayer').val(val); + if(val !== null && val !== undefined) { + $('#selLayer').val(val); + } builder.lastLayerIndex = $('#selLayer').val(); + if(builder.lastLayerIndex === null) { + // If the layer has just been deleted, go to the first layer + builder.lastLayerIndex = 0; + builder.lastLayers[builder.lastPlatform] = 0; + $('#selLayer').val(builder.lastLayerIndex); + } builder.prepareLayer(); builder.restoreSelection(selection); } diff --git a/developer/src/tike/xml/layoutbuilder/src/layer-controls.js b/developer/src/tike/xml/layoutbuilder/src/layer-controls.js index 765aa5c2bd..e1cf11ac34 100644 --- a/developer/src/tike/xml/layoutbuilder/src/layer-controls.js +++ b/developer/src/tike/xml/layoutbuilder/src/layer-controls.js @@ -64,7 +64,7 @@ $(function() { key.sk.forEach(k => fixup(k)); } if (key.flick) { - key.flick.forEach(k => fixup(k)); + Object.keys(key.flick).forEach(k => fixup(key.flick[k])); } if (key.multitap) { key.multitap.forEach(k => fixup(k)); diff --git a/package-lock.json b/package-lock.json index 88b8c7f08b..e473b783b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2844,23 +2844,6 @@ "node": ">= 14" } }, - "node_modules/@npmcli/agent/node_modules/debug": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", - "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/@npmcli/agent/node_modules/https-proxy-agent": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", @@ -7607,10 +7590,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es6-shim": { - "version": "0.35.6", - "license": "MIT" - }, "node_modules/esbuild": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", @@ -15286,7 +15265,6 @@ "@keymanapp/keyman-version": "*", "@keymanapp/models-templates": "*", "@keymanapp/models-wordbreakers": "*", - "es6-shim": "^0.35.5", "string.prototype.codepointat": "^0.2.1" }, "devDependencies": { @@ -15302,7 +15280,6 @@ "@keymanapp/keyman-version": "*", "@keymanapp/models-templates": "*", "@keymanapp/models-wordbreakers": "*", - "es6-shim": "^0.35.5", "string.prototype.codepointat": "^0.2.1", "string.prototype.startswith": "^0.2.0" }, diff --git a/web/src/app/webview/build.sh b/web/src/app/webview/build.sh index cce218ba6d..dceaf19715 100755 --- a/web/src/app/webview/build.sh +++ b/web/src/app/webview/build.sh @@ -83,8 +83,6 @@ compile_and_copy() { "$script" "$sourcemap" --clean done - node map-polyfill-bundler.js - # For dependent test pages. builder_launch /web/src/test/manual/embed/android-harness/build.sh configure,build } diff --git a/web/src/app/webview/map-polyfill-bundler.js b/web/src/app/webview/map-polyfill-bundler.js deleted file mode 100644 index 257f897e70..0000000000 --- a/web/src/app/webview/map-polyfill-bundler.js +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Note: while this file is not meant to exist long-term, it provides a nice - * low-level proof-of-concept for esbuild bundling of the various Web submodules. - * - * Add some extra code at the end of src/index.ts and run it to verify successful bundling! - */ - -import esbuild from 'esbuild'; -import { iifeConfiguration, prepareTslibTreeshaking } from '../../../../common/tools/es-bundling/build/index.mjs'; - -const commonConfig = { - ...iifeConfiguration, - entryPoints: { - 'index': '../../../build/app/webview/obj/polyfill/map.js', - }, - outfile: '../../../build/app/webview/debug/map-polyfill.js', - // `esbuild`'s sourcemap output puts relative paths to the original sources from the - // directory of the build output. The following keeps repo structure intact and - // puts our code under a common 'namespace' of sorts. - sourceRoot: '@keymanapp/keyman/web/build/app/webview/debug/' -}; - -await prepareTslibTreeshaking(commonConfig, /worker-main\.wrapped(?:\.min)?\.js/); - -await esbuild.build(commonConfig); - -await esbuild.build({ - ...commonConfig, - minify: true, - outfile: '../../../build/app/webview/release/map-polyfill.js', - // `esbuild`'s sourcemap output puts relative paths to the original sources from the - // directory of the build output. The following keeps repo structure intact and - // puts our code under a common 'namespace' of sorts. - sourceRoot: '@keymanapp/keyman/web/build/app/webview/release/' -}); \ No newline at end of file diff --git a/web/src/app/webview/src/passthroughKeyboard.ts b/web/src/app/webview/src/passthroughKeyboard.ts index 293be9883b..f20d80dbb7 100644 --- a/web/src/app/webview/src/passthroughKeyboard.ts +++ b/web/src/app/webview/src/passthroughKeyboard.ts @@ -39,6 +39,8 @@ export class PassthroughKeyboard extends HardKeyboardBase { isSynthetic: false // is not an OSK keystroke. }); + Lkc.suppressAutocorrection = true; + // 'us' is our default base layout; our distributed engine doesn't support // changing the assumed base keyboard for mnemonics. Lkc = processForMnemonicsAndLegacy(Lkc, this.activeKeyboard, 'us'); diff --git a/web/src/app/webview/src/polyfill/map.ts b/web/src/app/webview/src/polyfill/map.ts deleted file mode 100644 index a9299df769..0000000000 --- a/web/src/app/webview/src/polyfill/map.ts +++ /dev/null @@ -1,11 +0,0 @@ -// A separate build-product needed to support KMW when running in legacy ES5 mode for -// non-updated Chrome WebViews (like with Android API 21 / 5.0) -// @ts-ignore -import { default as Map } from "core-js/stable/map"; -// @ts-ignore -import { default as Symbol } from "core-js/stable/symbol"; - -// @ts-ignore -Window['Symbol'] = Symbol; -// @ts-ignore -Window['Map'] = Map; \ No newline at end of file diff --git a/web/src/engine/predictive-text/worker-main/package.json b/web/src/engine/predictive-text/worker-main/package.json index dc6301e113..1ae13a4011 100644 --- a/web/src/engine/predictive-text/worker-main/package.json +++ b/web/src/engine/predictive-text/worker-main/package.json @@ -47,10 +47,9 @@ "typescript": "^5.4.5" }, "dependencies": { + "@keymanapp/keyman-version": "*", "@keymanapp/models-templates": "*", "@keymanapp/models-wordbreakers": "*", - "@keymanapp/keyman-version": "*", - "es6-shim": "^0.35.5", "string.prototype.codepointat": "^0.2.1" } } diff --git a/web/src/engine/predictive-text/worker-thread/package.json b/web/src/engine/predictive-text/worker-thread/package.json index 7263f20b7c..af87bce811 100644 --- a/web/src/engine/predictive-text/worker-thread/package.json +++ b/web/src/engine/predictive-text/worker-thread/package.json @@ -39,7 +39,6 @@ "@keymanapp/keyman-version": "*", "@keymanapp/models-templates": "*", "@keymanapp/models-wordbreakers": "*", - "es6-shim": "^0.35.5", "string.prototype.codepointat": "^0.2.1", "string.prototype.startswith": "^0.2.0" } diff --git a/web/src/engine/predictive-text/worker-thread/src/main/correction/correction-result-mapping.ts b/web/src/engine/predictive-text/worker-thread/src/main/correction/correction-result-mapping.ts index 67e8fa62b6..d1903b6caf 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/correction/correction-result-mapping.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/correction/correction-result-mapping.ts @@ -27,10 +27,16 @@ export interface CorrectionResultMapping { readonly matchedResult: Readonly; /** - * Gets the "total cost" of the edge, which should be considered as the + * Gets the "correction cost" of the edge, which should be considered as the * negative log-likelihood of the input path taken to reach the node * multiplied by the 'probability' induced by needed Damerau-Levenshtein edits * to the resulting output. */ - readonly totalCost: number; + readonly correctionCost: number; + + /** + * The "total cost" of the edge - comprised of both the correction cost and the + * prediction cost based on the model's frequency data for the word. + */ + readonly currentCost: number; } \ No newline at end of file diff --git a/web/src/engine/predictive-text/worker-thread/src/main/correction/correction-searchable.ts b/web/src/engine/predictive-text/worker-thread/src/main/correction/correction-searchable.ts index 74ca206e1e..648734c3ef 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/correction/correction-searchable.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/correction/correction-searchable.ts @@ -29,10 +29,14 @@ type CompleteSearchPath = { export type PathResult = NullPath | IntermediateSearchPath | CompleteSearchPath; -export function CORRECTION_QUEUE_COMPARATOR(a: T, b: T) { +export function PREDICTION_QUEUE_COMPARATOR(a: T, b: T) { return a.currentCost - b.currentCost; } +export function CORRECTION_QUEUE_COMPARATOR(a: T, b: T) { + return a.correctionCost - b.correctionCost; +} + /** * Represents objects that support correction search via the `getBestMatches` * method, providing metadata relative to optimizing the search process for diff --git a/web/src/engine/predictive-text/worker-thread/src/main/correction/distance-modeler.ts b/web/src/engine/predictive-text/worker-thread/src/main/correction/distance-modeler.ts index b85f6b98c0..d3ffc2e0a4 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/correction/distance-modeler.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/correction/distance-modeler.ts @@ -4,7 +4,7 @@ import { PriorityQueue } from 'keyman/common/web-utils'; import { LexicalModelTypes } from '@keymanapp/common-types'; import { ClassicalDistanceCalculation } from './classical-calculation.js'; -import { CORRECTION_QUEUE_COMPARATOR, CorrectionSearchable } from './correction-searchable.js'; +import { PREDICTION_QUEUE_COMPARATOR, CorrectionSearchable } from './correction-searchable.js'; import { CorrectionResultMapping } from './correction-result-mapping.js'; import { ExecutionTimer, STANDARD_TIME_BETWEEN_DEFERS } from './execution-timer.js'; import { SearchQuotientNode } from './search-quotient-node.js'; @@ -262,7 +262,7 @@ export class SearchNode { * The correction search evaluates Nodes in cost-ascending order based on this property's * return value. */ - get currentCost(): number { + get correctionCost(): number { // - We reintrepret 'known cost' as a psuedo-probability. // - Noting that 1/e = 0.367879441, an edit-distance cost of 1 may be intepreted as -ln(1/e) - a log-space 'likelihood'. // - Not exactly normalized, though. @@ -278,6 +278,14 @@ export class SearchNode { return EDIT_DISTANCE_COST_SCALE * this.editCount + this.inputSamplingCost; } + get predictionCost(): number { + return -Math.log(this.currentTraversal.p); + } + + get currentCost(): number { + return this.correctionCost + this.predictionCost; + } + addEdit() { this.addedEditCost++; } @@ -625,7 +633,7 @@ export async function *getBestMatches< // If no filter function is provided, default to one that always returns true. filter ??= () => true; - let spaceQueue = new PriorityQueue(CORRECTION_QUEUE_COMPARATOR); + let spaceQueue = new PriorityQueue(PREDICTION_QUEUE_COMPARATOR); // Stage 1 - if we already have extracted results, build a queue just for them // and iterate over it first. @@ -633,7 +641,7 @@ export async function *getBestMatches< // Does not get any results that another iterator pulls up after this is // created - and those results won't come up later in stage 2, either. Only // intended for restarting a search, not searching twice in parallel. - const priorResultsQueue = new PriorityQueue((a, b) => a.totalCost - b.totalCost); + const priorResultsQueue = new PriorityQueue(PREDICTION_QUEUE_COMPARATOR); priorResultsQueue.enqueueAll(searchModules.map((space) => space.previousResults).flat()); // With potential prior results re-queued, NOW enqueue. (Not before - the heap may reheapify!) @@ -642,7 +650,7 @@ export async function *getBestMatches< // Stage 2: the fun part; actually searching! do { const entry: ResultMapping = timer.time(() => { - if((priorResultsQueue.peek()?.totalCost ?? Number.POSITIVE_INFINITY) <= spaceQueue.peek().currentCost) { + if((priorResultsQueue.peek()?.currentCost ?? Number.POSITIVE_INFINITY) <= spaceQueue.peek().currentCost) { const result = priorResultsQueue.dequeue(); // There's no guarantee that the filter closure is the same instance as @@ -669,7 +677,7 @@ export async function *getBestMatches< let lowestCostSource = spaceQueue.dequeue(); const newResult = lowestCostSource.handleNextNode(); spaceQueue.enqueue(lowestCostSource); - spaceQueue = new PriorityQueue(CORRECTION_QUEUE_COMPARATOR, spaceQueue.toArray()); + spaceQueue = new PriorityQueue(PREDICTION_QUEUE_COMPARATOR, spaceQueue.toArray()); if(newResult.type == 'none') { return null; diff --git a/web/src/engine/predictive-text/worker-thread/src/main/correction/legacy-quotient-root.ts b/web/src/engine/predictive-text/worker-thread/src/main/correction/legacy-quotient-root.ts index 7b381890be..dc73a02be8 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/correction/legacy-quotient-root.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/correction/legacy-quotient-root.ts @@ -1,7 +1,7 @@ import { PriorityQueue } from 'keyman/common/web-utils'; import { LexicalModelTypes } from '@keymanapp/common-types'; -import { CORRECTION_QUEUE_COMPARATOR, PathResult } from './correction-searchable.js'; +import { PREDICTION_QUEUE_COMPARATOR, PathResult } from './correction-searchable.js'; import { SearchQuotientNode } from './search-quotient-node.js'; import { SearchQuotientRoot } from './search-quotient-root.js'; import { SearchNode } from './distance-modeler.js'; @@ -10,7 +10,7 @@ import LexicalModel = LexicalModelTypes.LexicalModel; import { TokenResultMapping } from './token-result-mapping.js'; export class LegacyQuotientRoot extends SearchQuotientRoot { - private selectionQueue: PriorityQueue = new PriorityQueue(CORRECTION_QUEUE_COMPARATOR); + private selectionQueue: PriorityQueue = new PriorityQueue(PREDICTION_QUEUE_COMPARATOR); private processed: SearchNode[] = []; constructor(model: LexicalModel) { diff --git a/web/src/engine/predictive-text/worker-thread/src/main/correction/legacy-quotient-spur.ts b/web/src/engine/predictive-text/worker-thread/src/main/correction/legacy-quotient-spur.ts index a1134ddd3e..99e97b7f84 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/correction/legacy-quotient-spur.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/correction/legacy-quotient-spur.ts @@ -11,7 +11,7 @@ import { LexicalModelTypes } from '@keymanapp/common-types'; import { KMWString, PriorityQueue } from 'keyman/common/web-utils'; -import { CORRECTION_QUEUE_COMPARATOR, PathResult } from './correction-searchable.js'; +import { PREDICTION_QUEUE_COMPARATOR, PathResult } from './correction-searchable.js'; import { SearchNode } from './distance-modeler.js'; import { SearchQuotientNode, PathInputProperties } from './search-quotient-node.js'; import { SearchQuotientSpur } from './search-quotient-spur.js'; @@ -24,7 +24,7 @@ import Transform = LexicalModelTypes.Transform; // The set of search spaces corresponding to the same 'context' for search. // Whenever a wordbreak boundary is crossed, a new instance should be made. export class LegacyQuotientSpur extends SearchQuotientSpur { - private transposeQueue: PriorityQueue = new PriorityQueue(CORRECTION_QUEUE_COMPARATOR); + private transposeQueue: PriorityQueue = new PriorityQueue(PREDICTION_QUEUE_COMPARATOR); private incomingTransposeRootNodes: TokenResultMapping[] = []; public readonly insertLength: number; diff --git a/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-cluster.ts b/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-cluster.ts index 4a5ee0723f..ba625b5d81 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-cluster.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-cluster.ts @@ -11,7 +11,7 @@ import { PriorityQueue } from 'keyman/common/web-utils'; import { LexicalModelTypes } from '@keymanapp/common-types'; -import { CORRECTION_QUEUE_COMPARATOR, PathResult } from './correction-searchable.js'; +import { PREDICTION_QUEUE_COMPARATOR, PathResult } from './correction-searchable.js'; import { LegacyQuotientRoot } from './legacy-quotient-root.js'; import { generateSpaceSeed, InputSegment, SearchQuotientNode } from './search-quotient-node.js'; import { SearchQuotientSpur } from './search-quotient-spur.js'; @@ -20,7 +20,7 @@ import { TokenResultMapping } from './token-result-mapping.js'; // The set of search spaces corresponding to the same 'context' for search. // Whenever a wordbreak boundary is crossed, a new instance should be made. export class SearchQuotientCluster extends SearchQuotientNode { - private selectionQueue: PriorityQueue = new PriorityQueue(CORRECTION_QUEUE_COMPARATOR); + private selectionQueue: PriorityQueue = new PriorityQueue(PREDICTION_QUEUE_COMPARATOR); readonly spaceId: number; // We use an array and not a PriorityQueue b/c batch-heapifying at a single @@ -102,7 +102,7 @@ export class SearchQuotientCluster extends SearchQuotientNode { entries.forEach((path) => path.increaseMaxEditDistance()); // Since we just modified the stored instances, and the costs may have shifted, we need to re-heapify. - this.selectionQueue = new PriorityQueue(CORRECTION_QUEUE_COMPARATOR, entries.slice()); + this.selectionQueue = new PriorityQueue(PREDICTION_QUEUE_COMPARATOR, entries.slice()); } /** @@ -131,7 +131,7 @@ export class SearchQuotientCluster extends SearchQuotientNode { const bestPath = this.selectionQueue.dequeue(); const baseResult = bestPath.handleNextNode(); this.selectionQueue.enqueue(bestPath); - this.selectionQueue = new PriorityQueue(CORRECTION_QUEUE_COMPARATOR, this.selectionQueue.toArray()); + this.selectionQueue = new PriorityQueue(PREDICTION_QUEUE_COMPARATOR, this.selectionQueue.toArray()); let finalResult = baseResult; if(baseResult.type == 'complete') { diff --git a/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-spur.ts b/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-spur.ts index dbc0742287..78fa68eb6e 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-spur.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-spur.ts @@ -12,7 +12,7 @@ import { KMWString, PriorityQueue } from 'keyman/common/web-utils'; import { LexicalModelTypes } from '@keymanapp/common-types'; import { buildMergedTransform } from '@keymanapp/models-templates'; -import { CORRECTION_QUEUE_COMPARATOR, PathResult } from './correction-searchable.js'; +import { PREDICTION_QUEUE_COMPARATOR, PathResult } from './correction-searchable.js'; import { EDIT_DISTANCE_COST_SCALE, SearchNode } from './distance-modeler.js'; import { generateSpaceSeed, InputSegment, PathInputProperties, SearchQuotientNode } from './search-quotient-node.js'; import { generateSubsetId } from './tokenization-subsets.js'; @@ -34,7 +34,7 @@ export const MAX_EDIT_THRESHOLD_FACTOR = 2.5; // The set of search spaces corresponding to the same 'context' for search. // Whenever a wordbreak boundary is crossed, a new instance should be made. export abstract class SearchQuotientSpur extends SearchQuotientNode { - private selectionQueue: PriorityQueue = new PriorityQueue(CORRECTION_QUEUE_COMPARATOR); + private selectionQueue: PriorityQueue = new PriorityQueue(PREDICTION_QUEUE_COMPARATOR); /** * Holds all incoming Nodes generated from a parent `SearchSpace` that have not yet been @@ -152,7 +152,7 @@ export abstract class SearchQuotientSpur extends SearchQuotientNode { entries.forEach(function(edge) { edge.calculation = edge.calculation.increaseMaxDistance(); }); // Since we just modified the stored instances, and the costs may have shifted, we need to re-heapify. - this.selectionQueue = new PriorityQueue(CORRECTION_QUEUE_COMPARATOR, entries); + this.selectionQueue = new PriorityQueue(PREDICTION_QUEUE_COMPARATOR, entries); } /** @@ -419,7 +419,7 @@ export abstract class SearchQuotientSpur extends SearchQuotientNode { // Allows a little 'wiggle room' + 2 "hard" edits. // Can be important if needed characters don't actually exist on the keyboard // ... or even just not the then-current layer of the keyboard. - if(currentNode.currentCost > this.lowestPossibleSingleCost + MAX_EDIT_THRESHOLD_FACTOR * EDIT_DISTANCE_COST_SCALE) { + if(currentNode.correctionCost > this.lowestPossibleSingleCost + MAX_EDIT_THRESHOLD_FACTOR * EDIT_DISTANCE_COST_SCALE) { return unmatchedResult; } diff --git a/web/src/engine/predictive-text/worker-thread/src/main/correction/token-result-mapping.ts b/web/src/engine/predictive-text/worker-thread/src/main/correction/token-result-mapping.ts index 37e9d1d00b..d37f02ce37 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/correction/token-result-mapping.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/correction/token-result-mapping.ts @@ -33,8 +33,8 @@ export function initTokenResultFilterer() { return false; } - if((priorReturnCosts.get(searchResult.matchString) ?? Number.MAX_VALUE) > searchResult.totalCost) { - priorReturnCosts.set(searchResult.matchString, searchResult.totalCost); + if((priorReturnCosts.get(searchResult.matchString) ?? Number.MAX_VALUE) > searchResult.correctionCost) { + priorReturnCosts.set(searchResult.matchString, searchResult.correctionCost); return true; } else { @@ -118,6 +118,14 @@ export class TokenResultMapping implements CorrectionResultMapping { * multiplied by the 'probability' induced by needed Damerau-Levenshtein edits * to the resulting output. */ + get correctionCost(): number { + return this.node.correctionCost; + } + + get currentCost(): number { + return this.node.currentCost; + } + get totalCost(): number { return this.node.currentCost; } diff --git a/web/src/engine/predictive-text/worker-thread/src/main/predict-helpers.ts b/web/src/engine/predictive-text/worker-thread/src/main/predict-helpers.ts index 2ba308fdea..dd1146793c 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/predict-helpers.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/predict-helpers.ts @@ -532,7 +532,7 @@ export function buildAndMapPredictions( transition: ContextTransition, tokenization: ContextTokenization, // Originally, Readonly - but we only need these three components here. - match: Readonly<{matchString: string, totalCost: number, editCount: number}>, + match: Readonly<{matchString: string, correctionCost: number, editCount: number}>, costFactor: number ): CorrectionPredictionTuple[] { const model = transition.final.model; @@ -545,7 +545,7 @@ export function buildAndMapPredictions( // --- to move into predictFromCorrections --- let correction = match.matchString; - let rootCost = match.totalCost; + let rootCost = match.correctionCost; // Replace the existing context with the correction. const correctionTransform: Transform = { @@ -656,7 +656,7 @@ export async function correctAndEnumerate( // Only run the correction search when corrections are enabled. let rawPredictions: CorrectionPredictionTuple[] = []; - let bestCorrectionCost: number; + let bestTotalCost: number; const correctionPredictionMap: Record> = {}; for await(const match of getBestTokenMatches(searchModules, timer)) { // Corrections obtained: now to predict from them! @@ -700,8 +700,8 @@ export async function correctAndEnumerate( const predictions = buildAndMapPredictions(transition, tokenization, match, costFactor); // Only set 'best correction' cost when a correction ACTUALLY YIELDS predictions. - if(predictions.length > 0 && bestCorrectionCost === undefined) { - bestCorrectionCost = match.totalCost * costFactor; + if(predictions.length > 0 && bestTotalCost === undefined) { + bestTotalCost = match.totalCost * costFactor; } // If we're getting the same prediction again, it's lower-cost. Update! @@ -714,7 +714,7 @@ export async function correctAndEnumerate( rawPredictions = rawPredictions.concat(predictions); - if(shouldStopSearchingEarly(bestCorrectionCost, match.totalCost, rawPredictions)) { + if(shouldStopSearchingEarly(bestTotalCost, match.totalCost, rawPredictions)) { break; } } @@ -738,20 +738,15 @@ export function shouldStopSearchingEarly( return true; // If enough have been found, we're safe to terminate earlier. } else if(rawPredictions.length >= ModelCompositor.MAX_SUGGESTIONS) { - if(currentCorrectionCost >= bestCorrectionCost + CORRECTION_SEARCH_THRESHOLDS.REPLACEMENT_SEARCH_THRESHOLD) { - // Very useful for stopping 'sooner' when words reach a sufficient length. - return true; - } else { - // Sort the prediction list; we need them in descending probability order - // for the next check. - rawPredictions.sort((a, b) => b.totalProb - a.totalProb); + // Sort the prediction list; we need them in descending probability order + // for the next check. + rawPredictions.sort((a, b) => b.totalProb - a.totalProb); - // If the best result at the current state of the search fails to beat the worst - // pending suggestion from previous tiers, assume all further corrections will - // similarly fail to win; terminate the search-loop. - if(rawPredictions[ModelCompositor.MAX_SUGGESTIONS-1].totalProb > Math.exp(-currentCorrectionCost)) { - return true; - } + // If the best result at the current state of the search fails to beat the worst + // pending suggestion from previous tiers, assume all further corrections will + // similarly fail to win; terminate the search-loop. + if(rawPredictions[ModelCompositor.MAX_SUGGESTIONS-1].totalProb > Math.exp(-currentCorrectionCost)) { + return true; } } diff --git a/web/src/engine/src/keyboard/keyEvent.ts b/web/src/engine/src/keyboard/keyEvent.ts index 149b169bbe..0561d72225 100644 --- a/web/src/engine/src/keyboard/keyEvent.ts +++ b/web/src/engine/src/keyboard/keyEvent.ts @@ -111,6 +111,11 @@ export class KeyEvent implements KeyEventSpec { */ isSynthetic: boolean = true; + /** + * Set to `true` to prevent autocorrect from activating from this keystroke. + */ + suppressAutocorrection: boolean = false; + public constructor(keyEventSpec: KeyEventSpec) { for(const key in keyEventSpec) { // @ts-ignore diff --git a/web/src/engine/src/keyboard/keyboards/activeLayout.ts b/web/src/engine/src/keyboard/keyboards/activeLayout.ts index 0aad7376fd..8952ba8ebc 100644 --- a/web/src/engine/src/keyboard/keyboards/activeLayout.ts +++ b/web/src/engine/src/keyboard/keyboards/activeLayout.ts @@ -311,9 +311,8 @@ export class ActiveKeyBase { // And now for generalized type validation. ----------------------------------------- - // WARNING: Object.values and Object.entries is NOT polyfilled by es6-shim and thus - // is NOT available within the Android app in extremely early APIs. - // Object.entries requires Android 54. + // WARNING: Object.values and Object.entries is NOT available within the + // Android app in extremely early APIs. Object.entries requires Android 54. for(const key of Object.keys(KeyTypesOfKeyMap)) { const value = KeyTypesOfKeyMap[key as keyof typeof KeyTypesOfKeyMap]; diff --git a/web/src/engine/src/main/headless/inputProcessor.ts b/web/src/engine/src/main/headless/inputProcessor.ts index 5018e4de02..b2f4531a5b 100644 --- a/web/src/engine/src/main/headless/inputProcessor.ts +++ b/web/src/engine/src/main/headless/inputProcessor.ts @@ -334,6 +334,9 @@ export class InputProcessor { } const keyEvent = processorAction.transcription.keystroke; + if(keyEvent.suppressAutocorrection) { + return null; + } // ...and is this immediately after a Suggestion with an appended Transform was applied? // (If not, don't consider reverting an appended transform.) diff --git a/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/distance-modeler.tests.ts b/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/distance-modeler.tests.ts index dbd9f54a1d..97260c24ee 100644 --- a/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/distance-modeler.tests.ts +++ b/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/distance-modeler.tests.ts @@ -13,7 +13,7 @@ import { PriorityQueue } from 'keyman/common/web-utils'; import { jsonFixture } from '@keymanapp/common-test-resources/model-helpers.mjs'; import { LexicalModelTypes } from '@keymanapp/common-types'; -import { CORRECTION_QUEUE_COMPARATOR, models, SearchNode } from '@keymanapp/lm-worker/test-index'; +import { CORRECTION_QUEUE_COMPARATOR, models, PREDICTION_QUEUE_COMPARATOR, SearchNode } from '@keymanapp/lm-worker/test-index'; import SENTINEL_CODE_UNIT = models.SENTINEL_CODE_UNIT; import Distribution = LexicalModelTypes.Distribution; @@ -50,15 +50,6 @@ function edgeHasChars(edge: SearchNode, input: string, match: string) { return lastEntry(edge.calculation.matchSequence) == match; } -function findEdgesWithChars(edgeArray: SearchNode[], match: string) { - let results = edgeArray.filter(function(value) { - return lastEntry(value.calculation.matchSequence) == match; - }); - - assert.isAtLeast(results.length, 1); - return results; -} - function fetchCommonTENode() { const rootSeed = SEARCH_EDGE_SEED++; const rootNode = new SearchNode(testModel.traverseFromRoot(), rootSeed, toKey); @@ -130,7 +121,9 @@ describe('Correction Distance Modeler', () => { assert.equal(rootNode.editCount, 0); assert.equal(rootNode.inputSamplingCost, 0); - assert.equal(rootNode.currentCost, 0); + assert.equal(rootNode.correctionCost, 0); + assert.isAbove(rootNode.predictionCost, 0); + assert.isAbove(rootNode.currentCost, 0); assert.equal((rootNode.currentTraversal as TrieTraversal).prefix, ''); assert.isFalse(rootNode.hasPartialInput); @@ -151,7 +144,7 @@ describe('Correction Distance Modeler', () => { assert.equal(clonedNode.editCount, 0); assert.equal(clonedNode.inputSamplingCost, 0); - assert.equal(clonedNode.currentCost, 0); + assert.equal(clonedNode.correctionCost, 0); assert.equal((clonedNode.currentTraversal as TrieTraversal).prefix, ''); assert.isFalse(clonedNode.hasPartialInput); @@ -167,6 +160,9 @@ describe('Correction Distance Modeler', () => { // Verify aliasing for properties holding immutable objects assert.equal(clonedNode.calculation, originalNode.calculation); assert.equal(clonedNode.currentTraversal, originalNode.currentTraversal); + + // Verify local values are properly copied. + assert.equal(clonedNode.currentCost, originalNode.currentCost); }); it('properly deep-copies fully-processed nodes later in the search path', () => { @@ -222,12 +218,12 @@ describe('Correction Distance Modeler', () => { // ***** - function assertSourceNodeProps(node: SearchNode) { + function assertExpectedNodeProps(node: SearchNode) { assert.equal(node.resultKey, 'te'); assert.equal(node.editCount, 0); assert.equal(node.inputSamplingCost, -Math.log(firstLayerTransforms[0].p) - Math.log(secondLayerTransforms[0].p)); - assert.equal(node.currentCost, node.inputSamplingCost); + assert.equal(node.correctionCost, node.inputSamplingCost); assert.isFalse(node.hasPartialInput); assert.isFalse(node.isFullReplacement) @@ -238,12 +234,12 @@ describe('Correction Distance Modeler', () => { assert.equal(node.spaceId, secondSpaceId); } - assertSourceNodeProps(teNode); + assertExpectedNodeProps(teNode); const clonedNode = new SearchNode(teNode); // Root node properties; may as well re-assert 'em. - assertSourceNodeProps(clonedNode); + assertExpectedNodeProps(clonedNode); // Avoid aliasing for properties holding mutable objects assert.notEqual(clonedNode.priorInput, teNode.priorInput); @@ -308,12 +304,12 @@ describe('Correction Distance Modeler', () => { // ***** - function assertSourceNodeProps(node: SearchNode) { + function assertExpectedNodeProbs(node: SearchNode) { assert.equal(node.resultKey, 'te'); assert.equal(node.editCount, 0); assert.equal(node.inputSamplingCost, -Math.log(firstLayerTransforms[0].p) - Math.log(secondLayerTransforms[0].p)); - assert.equal(node.currentCost, node.inputSamplingCost); + assert.equal(node.correctionCost, node.inputSamplingCost); assert.isTrue(node.hasPartialInput); assert.isFalse(node.isFullReplacement) @@ -324,12 +320,12 @@ describe('Correction Distance Modeler', () => { assert.equal(node.spaceId, secondLayerId); } - assertSourceNodeProps(teNode); + assertExpectedNodeProbs(teNode); const clonedNode = new SearchNode(teNode); // Root node properties; may as well re-assert 'em. - assertSourceNodeProps(clonedNode); + assertExpectedNodeProbs(clonedNode); // Avoid aliasing for properties holding mutable objects assert.notEqual(clonedNode.priorInput, teNode.priorInput); @@ -577,7 +573,7 @@ describe('Correction Distance Modeler', () => { // Allow a little value wiggle due to double-precision limitations. assert.approximately(subsetNodes[i].inputSamplingCost, expectedCosts[i], 1e-8); // No actual edit-tracking is done yet, so these should also match. - assert.approximately(subsetNodes[i].currentCost, expectedCosts[i], 1e-8); + assert.approximately(subsetNodes[i].correctionCost, expectedCosts[i], 1e-8); } }); @@ -605,7 +601,7 @@ describe('Correction Distance Modeler', () => { assert.equal(lastEntry(ins1_dl0[1].calculation.matchSequence), 'h'); assert.equal(ins1_dl0[1].editCount, 0); assert.isBelow(ins1_dl0[0].inputSamplingCost, ins1_dl0[1].inputSamplingCost); - assert.isBelow(ins1_dl0[0].currentCost, ins1_dl0[1].currentCost); + assert.isBelow(ins1_dl0[0].correctionCost, ins1_dl0[1].correctionCost); // Correction of _other_ input characters to the 't' and the 'h' come // after ALL other corrections - these don't get both 't' and 'h' input @@ -616,13 +612,13 @@ describe('Correction Distance Modeler', () => { assert.equal(lastEntry(ins1_dl0[FIRST_CHAR_VARIANTS].calculation.matchSequence), 'h'); assert.equal(ins1_dl0[FIRST_CHAR_VARIANTS].editCount, 1); assert.isBelow(ins1_dl0[FIRST_CHAR_VARIANTS-1].inputSamplingCost, ins1_dl0[FIRST_CHAR_VARIANTS].inputSamplingCost); - assert.isBelow(ins1_dl0[FIRST_CHAR_VARIANTS-1].currentCost, ins1_dl0[FIRST_CHAR_VARIANTS].currentCost); + assert.isBelow(ins1_dl0[FIRST_CHAR_VARIANTS-1].correctionCost, ins1_dl0[FIRST_CHAR_VARIANTS].correctionCost); assert.equal(lastEntry(ins1_dl0[FIRST_CHAR_VARIANTS+1].calculation.inputSequence), SENTINEL_CODE_UNIT); assert.equal(lastEntry(ins1_dl0[FIRST_CHAR_VARIANTS+1].calculation.matchSequence), 't'); assert.equal(ins1_dl0[FIRST_CHAR_VARIANTS+1].editCount, 1); assert.isBelow(ins1_dl0[FIRST_CHAR_VARIANTS].inputSamplingCost, ins1_dl0[FIRST_CHAR_VARIANTS+1].inputSamplingCost); - assert.isBelow(ins1_dl0[FIRST_CHAR_VARIANTS].currentCost, ins1_dl0[FIRST_CHAR_VARIANTS+1].currentCost); + assert.isBelow(ins1_dl0[FIRST_CHAR_VARIANTS].correctionCost, ins1_dl0[FIRST_CHAR_VARIANTS+1].correctionCost); // For everything in between... well, the input-sampling weight is uniform, and // all require a full edit. @@ -649,7 +645,7 @@ describe('Correction Distance Modeler', () => { assert.equal(ins0_dl1[0].editCount, 0); assert.isUndefined(lastEntry(ins0_dl1[0].calculation.inputSequence)); assert.equal(ins0_dl1[0].inputSamplingCost, subsetNodes[3].inputSamplingCost); - assert.equal(ins0_dl1[0].currentCost, subsetNodes[3].currentCost); + assert.equal(ins0_dl1[0].correctionCost, subsetNodes[3].correctionCost); // ************ // Set 1: set for ins 2, dl 1 - 'tr' + 'th'. @@ -667,8 +663,8 @@ describe('Correction Distance Modeler', () => { assert.equal(lastEntry(ins2_dl1[0].calculation.matchSequence), 't'); assert.equal(ins2_dl1[0].editCount, 0); // The subset hasn't yet split! - assert.equal(ins2_dl1[0].currentCost, subsetNodes[1].currentCost); - assert.isBelow(ins2_dl1[0].currentCost, ins2_dl1[1].currentCost); + assert.equal(ins2_dl1[0].correctionCost, subsetNodes[1].correctionCost); + assert.isBelow(ins2_dl1[0].correctionCost, ins2_dl1[1].correctionCost); // All other (non-'t') entries get full subset probability with edit count 1; // they're all substitutions, as they fail to match against a non-'t' path. @@ -699,8 +695,8 @@ describe('Correction Distance Modeler', () => { assert.equal(lastEntry(ins2_dl0[0].calculation.matchSequence), 'c'); assert.equal(ins2_dl0[0].editCount, 0); // The subset won't split. - assert.equal(ins2_dl0[0].currentCost, subsetNodes[2].currentCost); - assert.isBelow(ins2_dl0[0].currentCost, ins2_dl0[1].currentCost); + assert.equal(ins2_dl0[0].correctionCost, subsetNodes[2].correctionCost); + assert.isBelow(ins2_dl0[0].correctionCost, ins2_dl0[1].correctionCost); // All other (non-'c') entries get full subset probability with edit count 1; // they're all substitutions, as they fail to match against a non-'t' path. @@ -816,7 +812,7 @@ describe('Correction Distance Modeler', () => { const subsetNodes = teNode.buildSubstitutionEdges(synthDistribution, SEARCH_EDGE_SEED++); assert.equal(subsetNodes.length, 4); subsetNodes.sort(CORRECTION_QUEUE_COMPARATOR); - const expectedCosts = [0.5, .25, 0.15, 0.1].map(x => -Math.log(x) + teNode.currentCost); + const expectedCosts = [0.5, .25, 0.15, 0.1].map(x => -Math.log(x) + teNode.correctionCost); // The known subs for the subsets defined above. for(let i=0; i < expectedCosts.length; i++) { assert.isTrue(subsetNodes[i].hasPartialInput); @@ -826,7 +822,7 @@ describe('Correction Distance Modeler', () => { // Allow a little value wiggle due to double-precision limitations. assert.approximately(subsetNodes[i].inputSamplingCost, expectedCosts[i], 1e-8); // No actual edit-tracking is done yet, so these should also match. - assert.approximately(subsetNodes[i].currentCost, expectedCosts[i], 1e-8); + assert.approximately(subsetNodes[i].correctionCost, expectedCosts[i], 1e-8); } }); @@ -862,13 +858,13 @@ describe('Correction Distance Modeler', () => { assert.equal(lastEntry(ins1_dl0[TE_CHILD_PATH_COUNT].calculation.matchSequence), 'l'); assert.equal(ins1_dl0[TE_CHILD_PATH_COUNT].editCount, 1); assert.isBelow(ins1_dl0[TE_CHILD_PATH_COUNT-1].inputSamplingCost, ins1_dl0[TE_CHILD_PATH_COUNT].inputSamplingCost); - assert.isBelow(ins1_dl0[TE_CHILD_PATH_COUNT-1].currentCost, ins1_dl0[TE_CHILD_PATH_COUNT].currentCost); + assert.isBelow(ins1_dl0[TE_CHILD_PATH_COUNT-1].correctionCost, ins1_dl0[TE_CHILD_PATH_COUNT].correctionCost); assert.equal(lastEntry(ins1_dl0[TE_CHILD_PATH_COUNT+1].calculation.inputSequence), SENTINEL_CODE_UNIT); assert.equal(lastEntry(ins1_dl0[TE_CHILD_PATH_COUNT+1].calculation.matchSequence), 'r'); assert.equal(ins1_dl0[TE_CHILD_PATH_COUNT+1].editCount, 1); assert.isBelow(ins1_dl0[TE_CHILD_PATH_COUNT].inputSamplingCost, ins1_dl0[TE_CHILD_PATH_COUNT+1].inputSamplingCost); - assert.isBelow(ins1_dl0[TE_CHILD_PATH_COUNT].currentCost, ins1_dl0[TE_CHILD_PATH_COUNT+1].currentCost); + assert.isBelow(ins1_dl0[TE_CHILD_PATH_COUNT].correctionCost, ins1_dl0[TE_CHILD_PATH_COUNT+1].correctionCost); // For everything in between... well, the input-sampling weight is uniform, and // all require a full edit. @@ -912,8 +908,8 @@ describe('Correction Distance Modeler', () => { assert.equal(lastEntry(ins2_dl1[0].calculation.matchSequence), 'a'); assert.equal(ins2_dl1[0].editCount, 0); // The subset hasn't yet split! - assert.equal(ins2_dl1[0].currentCost, subsetNodes[1].currentCost); - assert.isBelow(ins2_dl1[0].currentCost, ins2_dl1[1].currentCost); + assert.equal(ins2_dl1[0].correctionCost, subsetNodes[1].correctionCost); + assert.isBelow(ins2_dl1[0].correctionCost, ins2_dl1[1].correctionCost); // All other (non-'t') entries get full subset probability with edit count 1; // they're all substitutions, as they fail to match against a non-'t' path. @@ -943,8 +939,8 @@ describe('Correction Distance Modeler', () => { assert.equal(lastEntry(ins2_dl0[0].calculation.matchSequence), 'c'); assert.equal(ins2_dl0[0].editCount, 0); // The subset won't split. - assert.equal(ins2_dl0[0].currentCost, subsetNodes[2].currentCost); - assert.isBelow(ins2_dl0[0].currentCost, ins2_dl0[1].currentCost); + assert.equal(ins2_dl0[0].correctionCost, subsetNodes[2].correctionCost); + assert.isBelow(ins2_dl0[0].correctionCost, ins2_dl0[1].correctionCost); // All other (non-'c') entries get full subset probability with edit count 1; // they're all substitutions, as they fail to match against a non-'t' path. @@ -1058,7 +1054,7 @@ describe('Correction Distance Modeler', () => { const layer1Edges = rootNode.buildSubstitutionEdges(synthDistribution1, layer1Id) // No 2+ inserts here; we're fine with just one call. .flatMap(e => e.processSubsetEdge()); - const layer1Queue = new PriorityQueue(CORRECTION_QUEUE_COMPARATOR, layer1Edges); + const layer1Queue = new PriorityQueue(PREDICTION_QUEUE_COMPARATOR, layer1Edges); const tEdge = layer1Queue.dequeue(); assertEdgeChars(tEdge, 't', 't'); @@ -1068,7 +1064,7 @@ describe('Correction Distance Modeler', () => { const layer2Edges = tEdge.buildSubstitutionEdges(synthDistribution2, layer2Id) // No 2+ inserts here; we're fine with just one call. .flatMap(e => e.processSubsetEdge()); - const layer2Queue = new PriorityQueue(CORRECTION_QUEUE_COMPARATOR, layer2Edges); + const layer2Queue = new PriorityQueue(PREDICTION_QUEUE_COMPARATOR, layer2Edges); const eEdge = layer2Queue.dequeue(); assertEdgeChars(eEdge, 'e', 'e'); @@ -1078,21 +1074,16 @@ describe('Correction Distance Modeler', () => { assertEdgeChars(hEdge, 'h', 'h'); assert.equal(hEdge.spaceId, layer2Id); - // Needed for a proper e <-> h transposition. - const ehEdge = findEdgesWithChars(layer2Edges, 'h')[0]; - - assert.isOk(ehEdge); // Final round: we'll use three nodes and throw all of their results into the same priority queue. + // Note: as we're constructing these directly, we're not modeling transpositions. const layer3Id = SEARCH_EDGE_SEED++; const layer3eEdges = eEdge.buildSubstitutionEdges(synthDistribution3, layer3Id) // No 2+ inserts here; we're fine with just one call. .flatMap(e => e.processSubsetEdge()); const layer3hEdges = hEdge.buildSubstitutionEdges(synthDistribution3, layer3Id) .flatMap(e => e.processSubsetEdge()); - const layer3ehEdges = ehEdge.buildSubstitutionEdges(synthDistribution3, layer3Id) - .flatMap(e => e.processSubsetEdge()); - const layer3Queue = new PriorityQueue(CORRECTION_QUEUE_COMPARATOR, layer3eEdges.concat(layer3hEdges).concat(layer3ehEdges)); + const layer3Queue = new PriorityQueue(PREDICTION_QUEUE_COMPARATOR, layer3eEdges.concat(layer3hEdges)); // Find the first result with an actual word directly represented. let bestEdge; diff --git a/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/getBestMatches.tests.ts b/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/getBestMatches.tests.ts index 4bccef2ce9..dcc3cf3f0c 100644 --- a/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/getBestMatches.tests.ts +++ b/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/getBestMatches.tests.ts @@ -18,7 +18,8 @@ import { models, LegacyQuotientRoot, SearchQuotientCluster, - TokenResultMapping + TokenResultMapping, + CORRECTION_QUEUE_COMPARATOR } from '@keymanapp/lm-worker/test-index'; import TrieModel = models.TrieModel; @@ -32,74 +33,31 @@ function buildTestTimer() { describe('Correction Searching', () => { describe('without multi-tokenization; using a single SearchPath sequence', () => { const checkRepeatableResults_teh = async (iter: AsyncGenerator, any, any>) => { - const firstIterResult = await iter.next(); // {value: , done: } - assert.isFalse(firstIterResult.done); - - const firstResult: TokenResultMapping = firstIterResult.value; // Retrieves - // No checks on the first set's cost. - assert.equal(firstResult.matchString, "ten"); - - // All start with 'te' but one, and invoke one edit of the same cost. - // 'th' has an 'h' at the same cost (input 3) of the 'e' (input 2). - const secondBatch = [ - 'tec', 'tel', 'tem', - 'ter', 'tes', 'th', - 'te' + const expectedFirstTwenty = [ + 'ten', // no edits required whatsoever + 'th', // one edit (deletion), but gets to ignore the cost of a keystroke and still prefixes 'the' + 'the', // one edit (transposition), incurs the cost of all three keystrokes + 'te', // one edit (deletion), but gets to ignore the cost of a keystroke + 'tel', 'beh', // both cost one edit (hard character replacement: n/h -> l vs t -> b) + // Other edits, generally of one edit cost, predicting words of varying frequency. + 'ter', 'tha', 'thi', 'thr', 'tho', 'tem', 'thu', 'then', 'men', 'wen', 'gen', 'en', 'sen', 'tec' ]; - async function checkBatch(batch: string[], prevCost: number) { - let cost; - while(batch.length > 0) { - const iter_result = await iter.next(); - assert.isFalse(iter_result.done); - - const result = iter_result.value; - assert.isAbove(result.totalCost, prevCost); - if(cost !== undefined) { - assert.equal(result.totalCost, cost); - } else { - cost = result.totalCost; - } - - const matchIndex = batch.findIndex((entry) => entry == result.matchString); - assert.notEqual(matchIndex, -1, `'${result.matchString}' received as prediction too early`); - batch.splice(matchIndex, 1); - } - - return cost; + let results: TokenResultMapping[] = []; + for(let i = 0; i < expectedFirstTwenty.length; i++) { + const iterResult = await iter.next(); + results.push(iterResult.value as TokenResultMapping); } - const secondCost = await checkBatch(secondBatch, firstResult.totalCost); + assert.sameOrderedMembers(results.map((r) => r.matchString), expectedFirstTwenty); + for(let i=0; i < expectedFirstTwenty.length - 1; i++) { + assert.isAtLeast(results[i+1].totalCost, results[i].totalCost); + } - // Single hard edit, all other input probability aspects are equal - const thirdBatch = [ - // 't' -> 'b' (sub) - 'beh', - // '' -> 'c' (insertion) - 'tech', - // 'eh' -> 'he' (transposition) - 'the' - ]; - - await checkBatch(thirdBatch, secondCost); - - // All replace the low-likelihood case for the third input. - const fourthBatch = [ - 'thi', 'tho', 'thr', - 'thu', 'tha' - ]; - - await checkBatch(fourthBatch, secondCost); - - // Replace the _first_ input's char OR insert an extra char, - // also matching the low-likelihood third-char option. - const fifthBatch = [ - 'cen', 'en', 'gen', - 'ken', 'len', 'men', - 'sen', 'then', 'wen' - ]; - - await checkBatch(fifthBatch, secondCost); + // The results will not be in the order as raw correction likelihood because some words + // are more frequent than others. + results.sort(CORRECTION_QUEUE_COMPARATOR); + assert.notSameOrderedMembers(results.map((r) => r.matchString), expectedFirstTwenty); } it('Empty search root, loaded model', async () => { @@ -113,12 +71,13 @@ describe('Correction Searching', () => { // While there's no input, insertion operations can produce suggestions. const resultState = await iter.next(); - const result = resultState.value; + const result = resultState.value as TokenResultMapping; // Just one suggestion root should be returned as the first result. - assert.equal(result.totalCost, 0); // Gives a perfect match + assert.equal(result.correctionCost, 0); // Gives a perfect match assert.equal(result.matchString, ''); // an empty match string. assert.isFalse(resultState.done); + assert.isAbove(result.totalCost, 0); }); // Hmm... how best to update this... @@ -423,9 +382,9 @@ describe('Correction Searching', () => { // paths of lower total cost. pathsResults.push(nextFromPaths); - assert.isAtLeast(nextFromCluster.totalCost, baseCost); - assert.isAtLeast(nextFromPaths.totalCost, baseCost); - baseCost = Math.max(baseCost, nextFromCluster.totalCost, nextFromPaths.totalCost); + assert.isAtLeast(nextFromCluster.correctionCost, baseCost); + assert.isAtLeast(nextFromPaths.correctionCost, baseCost); + baseCost = Math.max(baseCost, nextFromCluster.correctionCost, nextFromPaths.correctionCost); } assert.deepEqual(genResults.map(r => r.matchString), pathsResults.map(r => r.matchString)); @@ -434,22 +393,13 @@ describe('Correction Searching', () => { assert.sameDeepMembers(pathsResults.slice(0, 3).map(r => r.matchString), ['th', 'to', 'tr']); // These involve likely-enough corrections that should show, given the model fixture. assert.includeDeepMembers(pathsResults.map(r => r.matchString), [ - 'ty', // 'type' is quite frequent according to the text fixture. 't', // Deleting the second keystroke outright lands here. 'oth', // What if we insert an 'o' early on? 'other' is a very common English word - 'ti' // 'time' is pretty common too. + 'ti', // 'time' is pretty common too. + 'thi', // 'this' is common enough to show up early despite inserting the 'i'. + 'wh', // "which" is a super-frequent English word, worthy of being a forced correction + 'sh' // "she" is also strong enough to force an early appearance. ]); - - // NOTE: this level of corrections does not yet consider the word likelihood - only - // the raw correction cost. No ordering of "likely word" to "unlikely word" should - // occur yet. - - // 'time': weight 934 - // 'type': weight 540 - const timeResult = pathsResults.find(r => r.matchString == 'ti'); - const typeResult = pathsResults.find(r => r.matchString == 'ty'); - // Correction to either should be equally likely. - assert.equal(timeResult.totalCost, typeResult.totalCost); }); }); }); diff --git a/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/legacy-quotient-spur.tests.ts b/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/legacy-quotient-spur.tests.ts index ccc38467a5..74b2af3e43 100644 --- a/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/legacy-quotient-spur.tests.ts +++ b/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/legacy-quotient-spur.tests.ts @@ -427,7 +427,7 @@ describe('LegacyQuotientSpur', () => { const entry_the = thirdResults.find((entry) => entry.matchString == 'the' && entry.editCount == 1); assert.isOk(entry_the); - thirdResults.sort((a, b) => a.totalCost - b.totalCost); + thirdResults.sort((a, b) => a.correctionCost - b.correctionCost); const the_index = thirdResults.findIndex((entry) => entry.matchString == 'the' && entry.editCount == 1); // `teh` should appear fairly early as a viable correction. assert.isBelow(the_index, 10); @@ -438,7 +438,7 @@ describe('LegacyQuotientSpur', () => { // We want to make sure we don't auto-ignore transposition cases by // accident by failing that conditional. const the_entry = thirdResults[the_index]; - assert.isBelow(the_entry.totalCost - thirdResults[0].totalCost, CORRECTION_SEARCH_THRESHOLDS.REPLACEMENT_SEARCH_THRESHOLD); + assert.isBelow(the_entry.correctionCost - thirdResults[0].correctionCost, CORRECTION_SEARCH_THRESHOLDS.REPLACEMENT_SEARCH_THRESHOLD); }); }); }); \ No newline at end of file diff --git a/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/quotient-node-finalizer.tests.ts b/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/quotient-node-finalizer.tests.ts index ee6ee088e6..1a89a68321 100644 --- a/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/quotient-node-finalizer.tests.ts +++ b/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/quotient-node-finalizer.tests.ts @@ -99,9 +99,9 @@ describe('QuotientNodeFinalizer', () => { assert.equal(searchResult.type, 'complete'); if(searchResult.type == 'complete') { - assert.equal(searchResult.mapping.totalCost, -Math.log(therefo.bestExample.p)); + assert.equal(searchResult.mapping.correctionCost, -Math.log(therefo.bestExample.p)); assert.isNotNaN(searchResult.cost); - assert.equal(searchResult.cost, searchResult.mapping.totalCost); + assert.isAtLeast(searchResult.cost, searchResult.mapping.totalCost); } else { return; } @@ -129,9 +129,9 @@ describe('QuotientNodeFinalizer', () => { assert.equal(searchResult.type, 'complete'); if(searchResult.type == 'complete') { - assert.isAbove(searchResult.mapping.totalCost, -Math.log(therefo.bestExample.p)); + assert.isAbove(searchResult.mapping.correctionCost, -Math.log(therefo.bestExample.p)); assert.isNotNaN(searchResult.cost); - assert.equal(searchResult.cost, searchResult.mapping.totalCost); + assert.isAtLeast(searchResult.cost, searchResult.mapping.totalCost); } else { return; } diff --git a/web/src/test/auto/headless/engine/predictive-text/worker-thread/prediction-helpers/build-and-map-predictions.tests.ts b/web/src/test/auto/headless/engine/predictive-text/worker-thread/prediction-helpers/build-and-map-predictions.tests.ts index 5051c2acbf..72876e4ca1 100644 --- a/web/src/test/auto/headless/engine/predictive-text/worker-thread/prediction-helpers/build-and-map-predictions.tests.ts +++ b/web/src/test/auto/headless/engine/predictive-text/worker-thread/prediction-helpers/build-and-map-predictions.tests.ts @@ -58,7 +58,7 @@ describe('buildAndMapPredictions', () => { const mappedPredictions = buildAndMapPredictions( transition, transition.base.displayTokenization, - {matchString: 'the', totalCost: 0, editCount: 0}, + {matchString: 'the', correctionCost: 0, editCount: 0}, 1 ); @@ -104,7 +104,7 @@ describe('buildAndMapPredictions', () => { const mappedPredictions = buildAndMapPredictions( transition, transition.base.displayTokenization, - {matchString: '', totalCost: 0, editCount: 0}, + {matchString: '', correctionCost: 0, editCount: 0}, 1 ); @@ -148,7 +148,7 @@ describe('buildAndMapPredictions', () => { const mappedPredictions = buildAndMapPredictions( transition, transition.base.displayTokenization, - {matchString: '', totalCost: 0, editCount: 0}, + {matchString: '', correctionCost: 0, editCount: 0}, 1 ); @@ -210,7 +210,7 @@ describe('buildAndMapPredictions', () => { const mappedPredictions = buildAndMapPredictions( transition, transition.final.displayTokenization, - {matchString: '', totalCost: 0, editCount: 0}, + {matchString: '', correctionCost: 0, editCount: 0}, 1 ); diff --git a/web/src/test/manual/embed/android-harness/build.sh b/web/src/test/manual/embed/android-harness/build.sh index 81d141bc23..e67d359e7a 100755 --- a/web/src/test/manual/embed/android-harness/build.sh +++ b/web/src/test/manual/embed/android-harness/build.sh @@ -46,7 +46,6 @@ if builder_start_action build; then # We shouldn't rely on a prior Android build for these files! We can retrieve them separately. cp "$KEYMAN_ROOT/common/web/sentry-manager/build/lib/index.js" "host/keyman-sentry.js" - cp "$KEYMAN_ROOT/node_modules/es6-shim/es6-shim.min.js" "host/es6-shim.min.js" # Test keyboards diff --git a/windows/src/desktop/kmshell/install/Keyman.Configuration.UI.InstallFile.pas b/windows/src/desktop/kmshell/install/Keyman.Configuration.UI.InstallFile.pas index ad12104026..479c924149 100644 --- a/windows/src/desktop/kmshell/install/Keyman.Configuration.UI.InstallFile.pas +++ b/windows/src/desktop/kmshell/install/Keyman.Configuration.UI.InstallFile.pas @@ -18,9 +18,9 @@ type FPackage: IKeymanPackageInstalled; const BCP47: string); static; public class function BrowseAndInstallKeyboardFromFile(Owner: TComponent): Boolean; static; - class function Execute(KeyboardFileNames: TStrings; const FirstKeyboardFileName: string; FSilent, FNoWelcome: Boolean; const LogFile: string): Boolean; overload; static; - class function Execute(Owner: TComponent; const FileName: string; ASilent, ANoWelcome: Boolean; const LogFile, BCP47: string): Boolean; overload; static; - class function Execute(Owner: TComponent; const FileNames: TStrings; ASilent: Boolean): Boolean; overload; static; + class function Execute(KeyboardFileNames: TStrings; const FirstKeyboardFileName: string; FSilent, FNoWelcome: Boolean; const LogFile: string; BaseKeyboardID: Integer): Boolean; overload; static; + class function Execute(Owner: TComponent; const FileName: string; ASilent, ANoWelcome: Boolean; const LogFile, BCP47: string; BaseKeyboardID: Integer): Boolean; overload; static; + class function Execute(Owner: TComponent; const FileNames: TStrings; ASilent: Boolean; BaseKeyboardID: Integer): Boolean; overload; static; end; implementation @@ -35,30 +35,31 @@ uses Keyman.Configuration.UI.KeymanProtocolHandler, Keyman.Configuration.UI.MitigationForWin10_1803, kmint, + KeymanOptionNames, Keyman.System.KeymanSentryClient, UfrmHTML, UfrmInstallKeyboard; class function TInstallFile.Execute(KeyboardFileNames: TStrings; const FirstKeyboardFileName: string; FSilent, FNoWelcome: Boolean; - const LogFile: string): Boolean; + const LogFile: string; BaseKeyboardID: Integer): Boolean; begin if TKeymanProtocolHandler.CanHandle(FirstKeyboardFileName) then begin - Result := TKeymanProtocolHandler.Handle(nil, FirstKeyboardFileName, FSilent, FNoWelcome, LogFile); + Result := TKeymanProtocolHandler.Handle(nil, FirstKeyboardFileName, FSilent, FNoWelcome, LogFile, BaseKeyboardID); end else if (KeyboardFileNames.Count > 1) or (Pos('=', FirstKeyboardFileName) > 0) then begin - Result := TInstallFile.Execute(nil, KeyboardFileNames, FSilent) + Result := TInstallFile.Execute(nil, KeyboardFileNames, FSilent, BaseKeyboardID) end // TODO: support bare package ids from command line (if it does not include a file extension, assume it is a .kmp and try and download it) // else if IsNotPackageOrKeyboardFile then else begin - Result := TInstallFile.Execute(nil, FirstKeyboardFileName, FSilent, FNoWelcome, LogFile, ''); + Result := TInstallFile.Execute(nil, FirstKeyboardFileName, FSilent, FNoWelcome, LogFile, '', BaseKeyboardID); end; end; -class function TInstallFile.Execute(Owner: TComponent; const FileName: string; ASilent, ANoWelcome: Boolean; const LogFile, BCP47: string): Boolean; +class function TInstallFile.Execute(Owner: TComponent; const FileName: string; ASilent, ANoWelcome: Boolean; const LogFile, BCP47: string; BaseKeyboardID: Integer): Boolean; var n: Integer; InstalledKeyboards: array of IKeymanKeyboardInstalled; @@ -78,7 +79,7 @@ begin begin if ASilent then begin - InstallKeyboard(LogFile, BCP47); + InstallKeyboard(LogFile, BCP47, BaseKeyboardID); Result := True; end else @@ -140,7 +141,7 @@ end; /// This is the handler for the `-i` parameter, e.g. /// kmshell -i khmer_angkor.kmp c:\temp\sil_euro_latin.kmp=fr /// -class function TInstallFile.Execute(Owner: TComponent; const FileNames: TStrings; ASilent: Boolean): Boolean; +class function TInstallFile.Execute(Owner: TComponent; const FileNames: TStrings; ASilent: Boolean; BaseKeyboardID: Integer): Boolean; var i, j: Integer; FPackage: IKeymanPackageInstalled; @@ -172,7 +173,7 @@ begin end; if IsPackage then begin - FPackage := (kmcom.Packages as IKeymanPackagesInstalled2).Install2(FileName, True); + FPackage := (kmcom.Packages as IKeymanPackagesInstalled3).Install3(FileName, True, BaseKeyboardID); if Length(FilenameBCP47) > 1 then RegisterKeyboardPackageLanguage(FPackage, FilenameBCP47[1]) else RegisterKeyboardPackageLanguage(FPackage, ''); @@ -180,7 +181,7 @@ begin end else begin - FKeyboard := (kmcom.Keyboards as IKeymanKeyboardsInstalled2).Install2(FileName, True); + FKeyboard := (kmcom.Keyboards as IKeymanKeyboardsInstalled3).Install3(FileName, True, BaseKeyboardID); if (Length(FilenameBCP47) > 1) and (Trim(FilenameBCP47[1]) <> '') then BCP47Tag := FilenameBCP47[1] else BCP47Tag := TTIPMaintenance.GetFirstLanguage(FKeyboard); @@ -231,6 +232,7 @@ end; class function TInstallFile.BrowseAndInstallKeyboardFromFile(Owner: TComponent): Boolean; var dlgOpen: TOpenDialog; + BaseKeyboardID : Integer; begin dlgOpen := TOpenDialog.Create(nil); try @@ -238,9 +240,9 @@ begin 'Keyman files (*.kmx, *.kmp)|*.kmx;*.kmp|Keyman keyboards (*.kmx)' + '|*.kmx|Keyman packages (*.kmp)|*.kmp|All files (*.*)|*.*'; dlgOpen.Title := 'Install Keyman Keyboard'; - + BaseKeyboardID := kmcom.Options[KeymanOptionName(koBaseLayout)].Value; if dlgOpen.Execute then - Result := Execute(Owner, dlgOpen.FileName, False, False, '', '') + Result := Execute(Owner, dlgOpen.FileName, False, False, '', '', BaseKeyboardID) else Result := False; finally diff --git a/windows/src/desktop/kmshell/install/Keyman.Configuration.UI.KeymanProtocolHandler.pas b/windows/src/desktop/kmshell/install/Keyman.Configuration.UI.KeymanProtocolHandler.pas index 6f13f6777b..60ac8ae43c 100644 --- a/windows/src/desktop/kmshell/install/Keyman.Configuration.UI.KeymanProtocolHandler.pas +++ b/windows/src/desktop/kmshell/install/Keyman.Configuration.UI.KeymanProtocolHandler.pas @@ -17,13 +17,13 @@ type FDownloadURL: string; frmDownloadProgress: TfrmDownloadProgress; function DoHandle(Owner: TComponent; const url: string; ASilent, - ANoWelcome: Boolean; const ALogFile: string): Boolean; + ANoWelcome: Boolean; const ALogFile: string; BaseKeyboardID: Integer): Boolean; procedure DoDownload(AOwner: TfrmDownloadProgress; var Result: Boolean); procedure HttpReceiveData(const Sender: TObject; AContentLength, AReadCount: Int64; var Abort: Boolean); public class function CanHandle(const url: string): Boolean; static; - class function Handle(Owner: TComponent; const url: string; ASilent, ANoWelcome: Boolean; const ALogFile: string): Boolean; static; + class function Handle(Owner: TComponent; const url: string; ASilent, ANoWelcome: Boolean; const ALogFile: string; BaseKeyboardID: Integer): Boolean; static; end; implementation @@ -51,13 +51,13 @@ end; class function TKeymanProtocolHandler.Handle(Owner: TComponent; const url: string; ASilent, ANoWelcome: Boolean; - const ALogFile: string): Boolean; + const ALogFile: string; BaseKeyboardID: Integer): Boolean; var h: TKeymanProtocolHandler; begin h := TKeymanProtocolHandler.Create; try - Result := h.DoHandle(Owner, url, ASilent, ANoWelcome, ALogFile); + Result := h.DoHandle(Owner, url, ASilent, ANoWelcome, ALogFile, BaseKeyboardID); finally h.Free; end; @@ -65,7 +65,7 @@ end; function TKeymanProtocolHandler.DoHandle(Owner: TComponent; const url: string; ASilent, ANoWelcome: Boolean; - const ALogFile: string): Boolean; + const ALogFile: string; BaseKeyboardID: Integer): Boolean; var FTempDir: string; PackageID, BCP47: string; @@ -98,7 +98,7 @@ begin end; // TODO: this makes a circular dependency, refactor it out! - Result := TInstallFile.Execute(nil, FDownloadFilename, False, False, '', BCP47); + Result := TInstallFile.Execute(nil, FDownloadFilename, False, False, '', BCP47, BaseKeyboardID); finally if FileExists(FDownloadFilename) then diff --git a/windows/src/desktop/kmshell/install/UfrmInstallKeyboard.pas b/windows/src/desktop/kmshell/install/UfrmInstallKeyboard.pas index 6d79811a80..5833fc5460 100644 --- a/windows/src/desktop/kmshell/install/UfrmInstallKeyboard.pas +++ b/windows/src/desktop/kmshell/install/UfrmInstallKeyboard.pas @@ -74,6 +74,7 @@ uses Vcl.StdCtrls, keymanapi_TLB, + KeymanOptionNames, UfrmKeymanBase, UfrmWebContainer; @@ -102,7 +103,7 @@ type protected procedure FireCommand(const command: WideString; params: TStringList); override; public - procedure InstallKeyboard(const ALogFile, BCP47Tag: string); + procedure InstallKeyboard(const ALogFile, BCP47Tag: string; BaseKeyboardID: Integer); property DefaultBCP47Tag: string read FDefaultBCP47Tag write SetDefaultBCP47Tag; property InstallFile: string read FInstallFile write SetInstallFile; property Silent: Boolean read FSilent write FSilent; @@ -271,6 +272,7 @@ end; procedure TfrmInstallKeyboard.FireCommand(const command: WideString; params: TStringList); var BCP47Tag: string; + BaseKeyboardID :Integer; begin BCP47Tag := ''; if (command = 'keyboard_install') and kmcom.SystemInfo.IsAdministrator then // I4172 @@ -283,7 +285,8 @@ begin Manager.Title := 'Installing Keyboard'; Manager.CanCancel := False; Manager.UpdateProgress('Installing Keyboard', 0, 0); - InstallKeyboard('', BCP47Tag); + BaseKeyboardID := kmcom.Options[KeymanOptionName(koBaseLayout)].Value; + InstallKeyboard('', BCP47Tag, BaseKeyboardID); Result := True; end ); @@ -299,6 +302,7 @@ begin var t: TTempFile; ExecParams: string; + BaseKeyboardString :string; begin KL.MethodEnter(Self, '"keyboard_install"', [params.Text]); try @@ -306,8 +310,9 @@ begin Manager.CanCancel := False; Manager.UpdateProgress('Installing Keyboard', 0, 0); t := TTempFileManager.Get('.log'); + BaseKeyboardString := IntToHex(kmcom.Options[KeymanOptionName(koBaseLayout)].Value, 8); try - ExecParams := '-log "'+t.Name+'" -s -i "'+FInstallFile+'='+BCP47Tag+'"'+ + ExecParams := '-log "'+t.Name+'" -bklid "'+BaseKeyboardString+'" -s -i "'+FInstallFile+'='+BCP47Tag+'"'+ ' -nowelcome '+TTIPMaintenance.GetUserDefaultLangParameterString; KL.Log('Calling elevated kmshell %s', [ExecParams]); if WaitForElevatedConfiguration(GetForegroundWindow, ExecParams) = 0 then @@ -358,7 +363,7 @@ end; ------------------------------------------------------------------------------} // TODO: move this to TInstallFile -procedure TfrmInstallKeyboard.InstallKeyboard(const ALogFile, BCP47Tag: string); +procedure TfrmInstallKeyboard.InstallKeyboard(const ALogFile, BCP47Tag: string; BaseKeyboardID: Integer); var i: Integer; kbd: IKeymanKeyboardInstalled; @@ -400,7 +405,7 @@ begin kbd := nil; kmcom.Keyboards.Apply; kmcom.Keyboards.Refresh; - FInstalledKeyboard := (FKeyboard as IKeymanKeyboardFile2).Install2(True); + FInstalledKeyboard := (FKeyboard as IKeymanKeyboardFile3).Install3(True, BaseKeyboardID); if not InstallTipForKeyboard(BCP47Tag) then begin // TODO can we return a failure code? @@ -461,7 +466,7 @@ begin kmcom.Keyboards.Apply; kmcom.Keyboards.Refresh; // I2169 - (FPackage as IKeymanPackageFile2).Install2(True); + (FPackage as IKeymanPackageFile3).Install3(True, BaseKeyboardID); kmcom.Refresh; diff --git a/windows/src/desktop/kmshell/install/UfrmInstallKeyboardFromWeb.pas b/windows/src/desktop/kmshell/install/UfrmInstallKeyboardFromWeb.pas index aff3251037..ed236d489d 100644 --- a/windows/src/desktop/kmshell/install/UfrmInstallKeyboardFromWeb.pas +++ b/windows/src/desktop/kmshell/install/UfrmInstallKeyboardFromWeb.pas @@ -87,6 +87,7 @@ uses Keyman.Configuration.UI.InstallFile, Keyman.System.LocaleStrings, kmint, + KeymanOptionNames, MessageIdentifierConsts, Upload_Settings, utilfiletypes, @@ -213,6 +214,7 @@ end; procedure TfrmInstallKeyboardFromWeb.DownloadAndInstallPackage(const PackageID, BCP47: string); var FTempDir: string; + BaseKeyboardID: Integer; begin FTempDir := IncludeTrailingPathDelimiter(CreateTempPath); // I1679 try @@ -230,8 +232,8 @@ begin finally frmDownloadProgress.Free; end; - - if TInstallFile.Execute(Self, FDownloadFilename, False, False, '', BCP47) then + BaseKeyboardID := kmcom.Options[KeymanOptionName(koBaseLayout)].Value; + if TInstallFile.Execute(Self, FDownloadFilename, False, False, '', BCP47, BaseKeyboardID) then ModalResult := mrOk; finally diff --git a/windows/src/desktop/kmshell/install/UpgradeMnemonicLayout.pas b/windows/src/desktop/kmshell/install/UpgradeMnemonicLayout.pas index 5ff65fd8d3..4386c46aff 100644 --- a/windows/src/desktop/kmshell/install/UpgradeMnemonicLayout.pas +++ b/windows/src/desktop/kmshell/install/UpgradeMnemonicLayout.pas @@ -46,7 +46,8 @@ uses kmint, RegistryKeys, utilexecute, - utilkmshell; + utilkmshell, + utilfiletypes; const { CurrentMnemonicLayoutVersion = 476; // First 9.0 build with fixes for mnemonic layouts } @@ -170,7 +171,7 @@ begin FBaseKeyboardIDHex := IntToHex(BaseKeyboardID, 8); FBaseFileName := Keyboard.Filename; FDestFileName := OutputFileName; - FDestDeadkeyFileName := ChangeFileExt(FDestFileName, '') + '-d.kmx'; // I4552 + FDestDeadkeyFileName := BuildKeyboardFilenameWithBaseKeyboardIDAndDeadkey(FBaseFileName, FBaseKeyboardIDHex); // I4552 FMCompilePath := TKeymanPaths.KeymanEngineInstallPath(TKeymanPaths.S_MCompileExe); FDestPath := ExtractFileDir(Keyboard.Filename); diff --git a/windows/src/desktop/kmshell/kmshell.dpr b/windows/src/desktop/kmshell/kmshell.dpr index e2d28b88d2..124b0357ce 100644 --- a/windows/src/desktop/kmshell/kmshell.dpr +++ b/windows/src/desktop/kmshell/kmshell.dpr @@ -183,7 +183,8 @@ uses Keyman.System.DownloadUpdate in 'main\Keyman.System.DownloadUpdate.pas', Keyman.System.ExecutionHistory in '..\..\..\..\common\windows\delphi\general\Keyman.System.ExecutionHistory.pas', Keyman.Configuration.UI.UfrmStartInstall in 'main\Keyman.Configuration.UI.UfrmStartInstall.pas' {frmStartInstall}, - Keyman.Configuration.Util.NetworkConnection in 'util\Keyman.Configuration.Util.NetworkConnection.pas'; + Keyman.Configuration.Util.NetworkConnection in 'util\Keyman.Configuration.Util.NetworkConnection.pas', + Keyman.Configuration.System.BaseKeyboard in 'main\Keyman.Configuration.System.BaseKeyboard.pas'; {$R VERSION.RES} {$R manifest.res} diff --git a/windows/src/desktop/kmshell/kmshell.dproj b/windows/src/desktop/kmshell/kmshell.dproj index 371ce4cd49..abcb521597 100644 --- a/windows/src/desktop/kmshell/kmshell.dproj +++ b/windows/src/desktop/kmshell/kmshell.dproj @@ -358,6 +358,7 @@
frmStartInstall
+ Cfg_2 @@ -419,6 +420,12 @@ False + + + kmshell.exe + true + + .\ @@ -431,12 +438,6 @@ true - - - kmshell.exe - true - - 1 diff --git a/windows/src/desktop/kmshell/main/Keyman.Configuration.System.BaseKeyboard.pas b/windows/src/desktop/kmshell/main/Keyman.Configuration.System.BaseKeyboard.pas new file mode 100644 index 0000000000..c4fb428b49 --- /dev/null +++ b/windows/src/desktop/kmshell/main/Keyman.Configuration.System.BaseKeyboard.pas @@ -0,0 +1,108 @@ +(* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by Ross Cruickshank on 2026-09-12 + * + * + * This unit assists in setting the base keyboard configuration, + * including compiling the installed keyboard layouts against + * the selected base keyboard. + *) +unit Keyman.Configuration.System.BaseKeyboard; + +interface + +uses + Winapi.Windows, + System.SysUtils, + keymanapi_TLB; + +(** + * Returns true if the keyboard files need to be compiled for the specified KLID. + * @param BaseKeyboardID KLID of the base keyboard to compile. + * @returns True If the keyboard files need to be compiled. + *) +function BaseKeyboardNeedsMCompile(BaseKeyboardID: Integer): Boolean; + +(** + * Sets the base keyboard KLID for the current user and compiles the keyboard + * layout files if necessary. In the case the compiled keyboard files are + *not present, it will require elevation. + * @param WindowHandle Window handle to own the elevation prompt. + * @param BaseKeyboardID KLID of the base keyboard KLID to set. + * @returns True when the base keyboard setting has been applied. + *) +function SetBaseKeyboard(WindowHandle: THandle; BaseKeyboardID: Integer): Boolean; + +(** + * Compiles the installed keyboard layouts for the specified KLID. + * Must run elevated. + * + * @param BaseKeyboardID KLID of the base keyboard to compile. + * @returns True when the compilation is successful. + *) +function MCompileBaseKeyboard(BaseKeyboardID: Integer): Boolean; + +implementation + +uses + kmint, + utilkmshell, + utilfiletypes; + +function BaseKeyboardNeedsMCompile(BaseKeyboardID: Integer): Boolean; +var + I: Integer; + Keyboard: IKeymanKeyboardInstalled; + KeyboardFileName: string; + BaseKeyboardIDHex: string; +begin + BaseKeyboardIDHex := IntToHex(BaseKeyboardID, 8); + for I := 0 to kmcom.Keyboards.Count - 1 do + begin + Keyboard := kmcom.Keyboards.Items[I]; + KeyboardFileName := Keyboard.Filename; + if FileExists(KeyboardFileName) and + (not FileExists(BuildKeyboardFilenameWithBaseKeyboardID(KeyboardFileName, BaseKeyboardIDHex)) or + not FileExists(BuildKeyboardFilenameWithBaseKeyboardIDAndDeadkey(KeyboardFileName, BaseKeyboardIDHex))) then + Exit(True); + end; + Result := False; +end; + +function SetBaseKeyboard(WindowHandle: THandle; BaseKeyboardID: Integer): Boolean; +begin + Result := True; + if BaseKeyboardNeedsMCompile(BaseKeyboardID) then + begin + if not kmcom.SystemInfo.IsAdministrator then + begin + Result := WaitForElevatedConfiguration(WindowHandle, '-mcompilekbds ' + IntToHex(BaseKeyboardID, 8)) = 0; + end + else + Result := MCompileBaseKeyboard(BaseKeyboardID); + end; + if not Result then + Exit; + kmcom.Options['koBaseLayout'].Value := BaseKeyboardID; + kmcom.Options.Apply; +end; + +function MCompileBaseKeyboard(BaseKeyboardID: Integer): Boolean; +var + i: Integer; + kbd: IKeymanKeyboardInstalled; +begin + Result := False; + // can be called from command line so test for admin + if not kmcom.SystemInfo.IsAdministrator then + Exit; + for i := 0 to kmcom.Keyboards.Count - 1 do + begin + kbd := kmcom.Keyboards[i]; + (kbd as IKeymanKeyboardInstalled2).MCompileForBaseKeyboard(BaseKeyboardID); + end; + Result := True; +end; + +end. diff --git a/windows/src/desktop/kmshell/main/UfrmBaseKeyboard.pas b/windows/src/desktop/kmshell/main/UfrmBaseKeyboard.pas index 40a65e31ce..7d8467730c 100644 --- a/windows/src/desktop/kmshell/main/UfrmBaseKeyboard.pas +++ b/windows/src/desktop/kmshell/main/UfrmBaseKeyboard.pas @@ -11,13 +11,21 @@ type TfrmBaseKeyboard = class(TfrmWebContainer) procedure TntFormCreate(Sender: TObject); private + FBaseKeyboardID: Integer; procedure Footer_Cancel; procedure Footer_OK(params: TStringList); protected procedure FireCommand(const command: WideString; params: TStringList); override; end; -function ConfigureBaseKeyboard: Boolean; +(** + * Displays a form for the user to select a base keyboard. If the user selects a base + * keyboard, the KLID is used to Set the Base Keyboard. + * + * @returns True if the user selected base keyboard has been set. + *) +function ConfigureAndSetBaseKeyboard(WindowHandle: THandle): Boolean; + implementation @@ -25,15 +33,21 @@ implementation uses BaseKeyboards, - kmint; + ErrorControlledRegistry, + RegistryKeys, + keymanapi_TLB, + Keyman.Configuration.System.BaseKeyboard, + kmint, + utilkmshell; -function ConfigureBaseKeyboard: Boolean; + +function ConfigureAndSetBaseKeyboard(WindowHandle: THandle): Boolean; begin with TfrmBaseKeyboard.Create(nil) do try Result := ShowModal = mrOk; if Result then - kmcom.Apply; + SetBaseKeyboard(WindowHandle, FBaseKeyboardID) finally Free; end; @@ -65,9 +79,9 @@ var v: Integer; begin if not TryStrToInt('$'+params.Values['id'], v) then Exit; - kmcom.Options['koBaseLayout'].Value := v; - kmcom.Options.Apply; + FBaseKeyboardID := v; ModalResult := mrOk; end; + end. diff --git a/windows/src/desktop/kmshell/main/UfrmMain.pas b/windows/src/desktop/kmshell/main/UfrmMain.pas index a8fdfe9845..b6ccccddd6 100644 --- a/windows/src/desktop/kmshell/main/UfrmMain.pas +++ b/windows/src/desktop/kmshell/main/UfrmMain.pas @@ -174,6 +174,7 @@ uses Hints, HotkeyUtils, initprog, + Keyman.Configuration.System.BaseKeyboard, Keyman.Configuration.System.TIPMaintenance, Keyman.Configuration.UI.UfrmDiagnosticTests, KeymanOptionNames, @@ -198,6 +199,7 @@ uses Keyman.Configuration.UI.UfrmStartInstall, RegistryKeys, SupportXMLRenderer, + UfrmBaseKeyboard, UfrmChangeHotkey, UfrmHTML, UfrmInstallKeyboardFromWeb, @@ -665,8 +667,11 @@ end; procedure TfrmMain.Options_BaseKeyboard; // I4169 begin - WaitForElevatedConfiguration(Handle, '-basekeyboard'); - // Refresh will be triggered by elevated process + if ConfigureAndSetBaseKeyboard(Handle) then + begin + DoRefresh; + end; + end; procedure TfrmMain.Options_SettingsManager; diff --git a/windows/src/desktop/kmshell/main/initprog.pas b/windows/src/desktop/kmshell/main/initprog.pas index 39c4cc50e8..c1bec21508 100644 --- a/windows/src/desktop/kmshell/main/initprog.pas +++ b/windows/src/desktop/kmshell/main/initprog.pas @@ -90,6 +90,7 @@ type fmKeyboardWelcome, // I2569 fmKeyboardPrint, // I2329 fmBaseKeyboard, // I4169 + fmMCompileKbds, fmUpgradeMnemonicLayout, // I4553 fmRepair, fmKeepInTouch, @@ -114,6 +115,7 @@ uses GetOsVersion, help, HTMLHelpViewer, + Keyman.Configuration.System.BaseKeyboard, Keyman.Configuration.UI.InstallFile, Keyman.Configuration.System.TIPMaintenance, Keyman.Configuration.System.UImportOlderVersionKeyboards11To13, @@ -205,7 +207,7 @@ end; function Init(var FMode: TKMShellMode; KeyboardFileNames: TStrings; var FSilent, FForce, FNoWelcome: Boolean; var FLogFile, FQuery: string; var FDisablePackages, FDefaultUILanguage: string; var FStartWithConfiguration: Boolean; - var FParentWindow: THandle; var FDefaultBCP47: string; var FDefaultLangID: Integer): Boolean; + var FParentWindow: THandle; var FDefaultBCP47: string; var FDefaultLangID, FBaseKeyboard: Integer): Boolean; var s: string; i: Integer; @@ -220,6 +222,7 @@ begin FQuery := ''; FMode := fmStart; KeyboardFileNames.Clear; + FBaseKeyboard := 0; i := 1; while i <= ParamCount do @@ -262,6 +265,15 @@ begin else if s = '-bd' then FMode := fmBackgroundDownload else if s = '-an' then FMode := fmApplyInstallNow else if s = '-basekeyboard' then FMode := fmBaseKeyboard // I4169 + else if s = '-bklid' then begin Inc(i); FBaseKeyboard := StrToIntDef('$' + ParamStr(i), 0); end + else if s = '-mcompilekbds' then + begin + // Requires elevated context + FMode := fmMCompileKbds; + Inc(i); + if i > ParamCount then Exit; + FBaseKeyboard := StrToIntDef('$' + ParamStr(i), 0); + end else if s = '-nowelcome' then FNoWelcome := True else if s = '-kw' then FMode := fmKeyboardWelcome // I2569 else if s = '-kp' then FMode := fmKeyboardPrint // I2329 @@ -321,7 +333,7 @@ end; procedure RunKMCOM(FMode: TKMShellMode; KeyboardFileNames: TStrings; FSilent, FForce, FNoWelcome: Boolean; FLogFile, FQuery: string; FDisablePackages, FDefaultUILanguage: string; FStartWithConfiguration: Boolean; FParentWindow: THandle; - const FDefaultBCP47: string; FDefaultLangID: Integer); forward; + const FDefaultBCP47: string; FDefaultLangID, FBaseKeyboard: Integer); forward; procedure Run; var @@ -332,7 +344,7 @@ var FForce: Boolean; FParentWindow: THandle; FLogFile: string; - FDefaultLangID: Integer; + FDefaultLangID, FBaseKeyboard: Integer; FDefaultBCP47, FDisablePackages, FDefaultUILanguage: string; FStartWithConfiguration: Boolean; begin @@ -341,7 +353,7 @@ begin KeyboardFileNames := TStringList.Create; try FParentWindow := 0; - if not Init(FMode, KeyboardFileNames, FSilent, FForce, FNoWelcome, FLogFile, FQuery, FDisablePackages, FDefaultUILanguage, FStartWithConfiguration, FParentWindow, FDefaultBCP47, FDefaultLangID) then + if not Init(FMode, KeyboardFileNames, FSilent, FForce, FNoWelcome, FLogFile, FQuery, FDisablePackages, FDefaultUILanguage, FStartWithConfiguration, FParentWindow, FDefaultBCP47, FDefaultLangID, FBaseKeyboard) then begin //TODO: TUtilExecute.Shell(PChar('hh.exe mk:@MSITStore:'+ExtractFilePath(KMShellExe)+'keyman.chm::/context/keyman_usage.html'), SW_SHOWNORMAL); Exit; @@ -349,7 +361,7 @@ begin if not LoadKMCOM then Exit; try - RunKMCOM(FMode, KeyboardFileNames, FSilent, FForce, FNoWelcome, FLogFile, FQuery, FDisablePackages, FDefaultUILanguage, FStartWithConfiguration, FParentWindow, FDefaultBCP47, FDefaultLangID); + RunKMCOM(FMode, KeyboardFileNames, FSilent, FForce, FNoWelcome, FLogFile, FQuery, FDisablePackages, FDefaultUILanguage, FStartWithConfiguration, FParentWindow, FDefaultBCP47, FDefaultLangID, FBaseKeyboard); finally kmcom := nil; end; @@ -388,7 +400,7 @@ end; procedure RunKMCOM(FMode: TKMShellMode; KeyboardFileNames: TStrings; FSilent, FForce, FNoWelcome: Boolean; FLogFile, FQuery: string; FDisablePackages, FDefaultUILanguage: string; FStartWithConfiguration: Boolean; - FParentWindow: THandle; const FDefaultBCP47: string; FDefaultLangID: Integer); + FParentWindow: THandle; const FDefaultBCP47: string; FDefaultLangID, FBaseKeyboard: Integer); var kdl: IKeymanDefaultLanguage; FIcon: string; @@ -540,12 +552,17 @@ begin end; fmBaseKeyboard: // I4169 - if ConfigureBaseKeyboard + if ConfigureAndSetBaseKeyboard(0) + then ExitCode := 0 + else ExitCode := 1; + + fmMCompileKbds: + if MCompileBaseKeyboard(FBaseKeyboard) then ExitCode := 0 else ExitCode := 1; fmInstall: - if TInstallFile.Execute(KeyboardFileNames, FirstKeyboardFileName, FSilent, FNoWelcome, FLogFile) + if TInstallFile.Execute(KeyboardFileNames, FirstKeyboardFileName, FSilent, FNoWelcome, FLogFile, FBaseKeyboard) then ExitCode := 0 else ExitCode := 1; diff --git a/windows/src/engine/kmcomapi/com/keyboards/keymankeyboardfile.pas b/windows/src/engine/kmcomapi/com/keyboards/keymankeyboardfile.pas index 5ea1e74265..b7255869b7 100644 --- a/windows/src/engine/kmcomapi/com/keyboards/keymankeyboardfile.pas +++ b/windows/src/engine/kmcomapi/com/keyboards/keymankeyboardfile.pas @@ -37,7 +37,7 @@ uses keymankeyboard, keymancontext, Classes, PackageInfo, keymankeyboardlanguagesfile; type - TKeymanKeyboardFile = class(TKeymanKeyboard, IKeymanKeyboardFile, IKeymanKeyboardFile2) + TKeymanKeyboardFile = class(TKeymanKeyboard, IKeymanKeyboardFile, IKeymanKeyboardFile2, IKeymanKeyboardFile3) private FFileName: WideString; FError: Boolean; @@ -52,6 +52,7 @@ type { IKeymanKeyboardFile } procedure Install(Force: WordBool); safecall; function Install2(Force: WordBool): IKeymanKeyboardInstalled; safecall; + function Install3(Force: WordBool; BaseKeyboardID: Integer): IKeymanKeyboardInstalled; safecall; { IKeymanKeyboard } function Get_Copyright: WideString; override; safecall; @@ -252,7 +253,7 @@ procedure TKeymanKeyboardFile.Install(Force: WordBool); begin with TKPInstallKeyboard.Create(Context) do try - Execute(FFileName, '', [ikLegacyRegisterAndInstallProfiles], nil, Force); + Execute(FFileName, '', [ikLegacyRegisterAndInstallProfiles], nil, Force, 0); finally Free; end; @@ -264,7 +265,23 @@ var begin with TKPInstallKeyboard.Create(Context) do try - Execute(FFileName, '', [], nil, Force); + Execute(FFileName, '', [], nil, Force, 0); + finally + Free; + end; + + kki := Context.Keyboards as IKeymanKeyboardsInstalled; + kki.Refresh; + Result := kki.Items[FFileName]; +end; + +function TKeymanKeyboardFile.Install3(Force: WordBool; BaseKeyboardID: Integer): IKeymanKeyboardInstalled; +var + kki: IKeymanKeyboardsInstalled; +begin + with TKPInstallKeyboard.Create(Context) do + try + Execute(FFileName, '', [], nil, Force, BaseKeyboardID); finally Free; end; diff --git a/windows/src/engine/kmcomapi/com/keyboards/keymankeyboardinstalled.pas b/windows/src/engine/kmcomapi/com/keyboards/keymankeyboardinstalled.pas index f920c35461..260612e738 100644 --- a/windows/src/engine/kmcomapi/com/keyboards/keymankeyboardinstalled.pas +++ b/windows/src/engine/kmcomapi/com/keyboards/keymankeyboardinstalled.pas @@ -63,7 +63,8 @@ type TKeymanKeyboardInstalled = class( // I3581 TKeymanKeyboard, IIntKeymanKeyboardInstalled, - IKeymanKeyboardInstalled) + IKeymanKeyboardInstalled, + IKeymanKeyboardInstalled2) private FRegKeyboard: TRegKeyboard; FVisualKeyboard: IKeymanVisualKeyboard; @@ -109,9 +110,11 @@ type { IIntKeymanKeyboardInstalled } function RegKeyboard: TRegKeyboard; procedure ClearVisualKeyboard; - procedure UpdateBaseLayout; // I4169 procedure RefreshInstallation; + { IKeymanKeyboardInstalled2 } + procedure MCompileForBaseKeyboard(KLID: Integer); safecall; + public constructor Create(AContext: TKeymanContext; const Name: string); destructor Destroy; override; @@ -151,17 +154,6 @@ begin end; end; -procedure TKeymanKeyboardInstalled.UpdateBaseLayout; // I4169 -begin - if FRegKeyboard.MnemonicLayout and FileExists(FRegKeyboard.KeymanFile) then // I4615 - with TKPRecompileMnemonicKeyboard.Create(Context) do - try - Execute(FRegKeyboard.KeymanFile, FRegKeyboard.PackageName); - finally - Free; - end; -end; - function TKeymanKeyboardInstalled.Get_Copyright: WideString; begin Result := FRegKeyboard.Copyright; @@ -475,5 +467,21 @@ begin Result := FRegKeyboard; end; +{ IKeymanKeyboardInstalled2 } +procedure TKeymanKeyboardInstalled.MCompileForBaseKeyboard(KLID: Integer); safecall; +var + RecompileMnemonicKeyboard: TKPRecompileMnemonicKeyboard; +begin + if FRegKeyboard.MnemonicLayout and FileExists(FRegKeyboard.KeymanFile) then + begin + RecompileMnemonicKeyboard := TKPRecompileMnemonicKeyboard.Create(Context); + try + RecompileMnemonicKeyboard.Execute(FRegKeyboard.KeymanFile, FRegKeyboard.PackageName, KLID); + finally + RecompileMnemonicKeyboard.Free; + end; + end; +end; + end. diff --git a/windows/src/engine/kmcomapi/com/keyboards/keymankeyboardsinstalled.pas b/windows/src/engine/kmcomapi/com/keyboards/keymankeyboardsinstalled.pas index f5f8b74448..b37e0dfce0 100644 --- a/windows/src/engine/kmcomapi/com/keyboards/keymankeyboardsinstalled.pas +++ b/windows/src/engine/kmcomapi/com/keyboards/keymankeyboardsinstalled.pas @@ -37,7 +37,8 @@ uses keymanerrorcodes, keymankeyboardinstalled, keymankeyboard, internalinterfaces; type - TKeymanKeyboardsInstalled = class(TKeymanAutoCollectionObject, IKeymanKeyboardsInstalled, IKeymanKeyboardsInstalled2, IIntKeymanKeyboardsInstalled) // I4376 + TKeymanKeyboardsInstalled = class(TKeymanAutoCollectionObject, IKeymanKeyboardsInstalled, + IKeymanKeyboardsInstalled2, IIntKeymanKeyboardsInstalled, IKeymanKeyboardsInstalled3) // I4376 private FKeyboards: TKeyboardList; procedure TriggerWindowsLanguageSync; @@ -55,6 +56,7 @@ type procedure Install(const Filename: WideString; Force: WordBool); safecall; procedure Apply; safecall; function Install2(const Filename: WideString; Force: WordBool): IKeymanKeyboardInstalled; safecall; + function Install3(const Filename: WideString; Force: WordBool; BaseKeyboardID: Integer): IKeymanKeyboardInstalled; safecall; procedure RefreshInstalledKeyboards; safecall; { IIntKeymanKeyboardsInstalled } @@ -101,7 +103,7 @@ procedure TKeymanKeyboardsInstalled.Install(const Filename: WideString; Force: W begin with TKPInstallKeyboard.Create(Context) do try - Execute(FileName, '', [ikLegacyRegisterAndInstallProfiles], nil, Force); + Execute(FileName, '', [ikLegacyRegisterAndInstallProfiles], nil, Force, 0); finally Free; end; @@ -112,7 +114,21 @@ function TKeymanKeyboardsInstalled.Install2(const Filename: WideString; begin with TKPInstallKeyboard.Create(Context) do try - Execute(FileName, '', [], nil, Force); + Execute(FileName, '', [], nil, Force, 0); + finally + Free; + end; + + DoRefresh; + Result := Get_Items(FileName); +end; + +function TKeymanKeyboardsInstalled.Install3(const Filename: WideString; + Force: WordBool; BaseKeyboardID: Integer): IKeymanKeyboardInstalled; +begin + with TKPInstallKeyboard.Create(Context) do + try + Execute(FileName, '', [], nil, Force, BaseKeyboardID); finally Free; end; diff --git a/windows/src/engine/kmcomapi/com/options/keymanoptions.pas b/windows/src/engine/kmcomapi/com/options/keymanoptions.pas index 7bea91450b..1848428033 100644 --- a/windows/src/engine/kmcomapi/com/options/keymanoptions.pas +++ b/windows/src/engine/kmcomapi/com/options/keymanoptions.pas @@ -1,18 +1,18 @@ (* Name: keymanoptions Copyright: Copyright (C) SIL International. - Documentation: - Description: + Documentation: + Description: Create Date: 20 Jun 2006 Modified Date: 6 Feb 2015 Authors: mcdurdin - Related Files: - Dependencies: + Related Files: + Dependencies: - Bugs: - Todo: - Notes: + Bugs: + Todo: + Notes: History: 20 Jun 2006 - mcdurdin - Initial version 01 Aug 2006 - mcdurdin - Add AutoRefershKeyman call 12 Aug 2008 - mcdurdin - Avoid crash with missing options @@ -67,6 +67,7 @@ uses ErrorControlledRegistry, RegistryKeys, Glossary, + isadmin, Keyman.System.BaseKeyboard, KeymanOptionNames, keymanerrorcodes; @@ -111,29 +112,8 @@ begin end; procedure TKeymanOptions.Apply; -var - I, FOldBaseLayout: Integer; begin - with TRegistryErrorControlled.Create do // I3717 - try - if OpenKey(SRegKey_KeymanEngine_CU, True) then - begin - if ValueExists(SRegValue_UnderlyingLayout) - then FOldBaseLayout := StrToIntDef('$'+ReadString(SRegValue_UnderlyingLayout),0) // I3759 - else FOldBaseLayout := TBaseKeyboard.GetDefaultBaseLayoutID; - end - else - FOldBaseLayout := TBaseKeyboard.GetDefaultBaseLayoutID; - finally - Free; - end; - FInternalOptions.Save(Context); - - if FOldBaseLayout <> Get_Items('koBaseLayout').Value then - for I := 0 to Context.Keyboards.Count - 1 do // I4169 - (Context.Keyboards.Items[I] as IIntKeymanKeyboardInstalled).UpdateBaseLayout; - Context.Control.AutoApplyKeyman; end; diff --git a/windows/src/engine/kmcomapi/com/packages/keymanpackagefile.pas b/windows/src/engine/kmcomapi/com/packages/keymanpackagefile.pas index 14b207e229..28ef2d064a 100644 --- a/windows/src/engine/kmcomapi/com/packages/keymanpackagefile.pas +++ b/windows/src/engine/kmcomapi/com/packages/keymanpackagefile.pas @@ -1,18 +1,18 @@ (* Name: keymanpackagefile Copyright: Copyright (C) SIL International. - Documentation: - Description: + Documentation: + Description: Create Date: 20 Jun 2006 Modified Date: 29 Mar 2010 Authors: mcdurdin - Related Files: - Dependencies: + Related Files: + Dependencies: - Bugs: - Todo: - Notes: + Bugs: + Todo: + Notes: History: 20 Jun 2006 - mcdurdin - Initial version 01 Aug 2006 - mcdurdin - Avoid processmessages in unzip 04 Dec 2006 - mcdurdin - Add Serialize function, support ShortcutRootPath in installation @@ -37,7 +37,7 @@ uses keymanpackagecontentfiles, StdVcl, kmpinffile, KeymanContext, Graphics, Classes, internalinterfaces; type - TKeymanPackageFile = class(TKeymanAutoObject, IKeymanPackage, IKeymanPackageFile, IKeymanPackageFile2) + TKeymanPackageFile = class(TKeymanAutoObject, IKeymanPackage, IKeymanPackageFile, IKeymanPackageFile2, IKeymanPackageFile3) private FSourcePath: string; FSubFiles: IKeymanPackageContentFiles; @@ -74,6 +74,7 @@ type { IKeymanPackageFile } procedure Install(Force: WordBool); safecall; function Install2(Force: WordBool): IKeymanPackageInstalled; safecall; + function Install3(Force: WordBool; BaseKeyboardID: Integer): IKeymanPackageInstalled; safecall; public constructor Create(AContext: TKeymanContext; const Filename: Widestring); destructor Destroy; override; @@ -175,7 +176,7 @@ begin o := [ipLegacyRegisterAndInstallProfiles]; if Force then Include(o, ipForce); - Execute(FFileName, o); + Execute(FFileName, o, 0); finally Free; end; @@ -191,7 +192,27 @@ begin o := []; if Force then Include(o, ipForce); - Execute(FFileName, o); + Execute(FFileName, o, 0); + finally + Free; + end; + + kpi := Context.Packages as IKeymanPackagesInstalled; + kpi.Refresh; + Result := kpi.Items[FFileName]; +end; + +function TKeymanPackageFile.Install3(Force: WordBool; BaseKeyboardID: Integer): IKeymanPackageInstalled; +var + o: TKPInstallPackageOptions; + kpi: IKeymanPackagesInstalled; +begin + with TKPInstallPackage.Create(Context) do + try + o := []; + if Force then + Include(o, ipForce); + Execute(FFileName, o, BaseKeyboardID); finally Free; end; @@ -211,7 +232,7 @@ var begin if not FileExists(FFileName) then raise Exception.Create('File '+FFileName+' does not exist.'); - + if GetTempPath(260, buf) = 0 then raise Exception.Create('Unable to get temporary path: ' + IntToHex(GetLastError, 8) + ' ' + SysErrorMessage(GetLastError)); FTempOutPath := buf; diff --git a/windows/src/engine/kmcomapi/com/packages/keymanpackagesinstalled.pas b/windows/src/engine/kmcomapi/com/packages/keymanpackagesinstalled.pas index 9dd9f1a52a..821a38bdc5 100644 --- a/windows/src/engine/kmcomapi/com/packages/keymanpackagesinstalled.pas +++ b/windows/src/engine/kmcomapi/com/packages/keymanpackagesinstalled.pas @@ -38,7 +38,8 @@ type property Items[Index: Integer]: IIntKeymanPackageInstalled read GetItem write SetItem; default; end; - TKeymanPackagesInstalled = class(TKeymanAutoCollectionObject, IKeymanPackagesInstalled, IKeymanPackagesInstalled2) + TKeymanPackagesInstalled = class(TKeymanAutoCollectionObject, + IKeymanPackagesInstalled, IKeymanPackagesInstalled2, IKeymanPackagesInstalled3) private FPackages: TPackageList; protected @@ -51,6 +52,7 @@ type function IndexOf(const ID: WideString): Integer; safecall; procedure Install(const Filename: WideString; Force: WordBool); safecall; function Install2(const Filename: WideString; Force: WordBool): IKeymanPackageInstalled; safecall; + function Install3(const Filename: WideString; Force: WordBool; BaseKeyboardID: Integer): IKeymanPackageInstalled; safecall; public constructor Create(AContext: TKeymanContext); destructor Destroy; override; @@ -118,7 +120,7 @@ begin o := [ipLegacyRegisterAndInstallProfiles]; if Force then Include(o, ipForce); - Execute(Filename, o); + Execute(Filename, o, 0); finally Free; end; @@ -136,7 +138,29 @@ begin o := []; if Force then Include(o, ipForce); - Execute(Filename, o); + Execute(Filename, o, 0); + finally + Free; + end; + + DoRefresh; + Result := Get_Items(Filename); + + KL.MethodExit(Self, 'Install2'); +end; + +function TKeymanPackagesInstalled.Install3(const Filename: WideString; + Force: WordBool; BaseKeyboardID: Integer): IKeymanPackageInstalled; +var + o: TKPInstallPackageOptions; +begin + KL.MethodEnter(Self, 'Install2', [Filename, Force]); + with TKPInstallPackage.Create(Context) do + try + o := []; + if Force then + Include(o, ipForce); + Execute(Filename, o, BaseKeyboardID); finally Free; end; diff --git a/windows/src/engine/kmcomapi/keymanapi_TLB.pas b/windows/src/engine/kmcomapi/keymanapi_TLB.pas index 3ceb5b15cd..b0530cb94a 100644 --- a/windows/src/engine/kmcomapi/keymanapi_TLB.pas +++ b/windows/src/engine/kmcomapi/keymanapi_TLB.pas @@ -12,7 +12,7 @@ unit keymanapi_TLB; // ************************************************************************ // // $Rev: 52393 $ -// File generated on 16/09/2021 6:54:44 PM from Type Library described below. +// File generated on 31/08/2026 5:28:13 PM from Type Library described below. // ************************************************************************ // // Type Lib: C:\Projects\keyman\app\windows\src\engine\kmcomapi\kmcomapi (1) @@ -87,11 +87,16 @@ const IID_IKeymanKeyboardLanguagesInstalled: TGUID = '{7DC22BC0-85BB-45C0-8EDB-A2F4BD1D500B}'; IID_IKeymanKeyboardLanguagesFile: TGUID = '{5F90BCDA-F1C1-433A-8FD0-B498299D3C30}'; IID_IKeymanKeyboardsInstalled2: TGUID = '{EA57C94F-C140-485E-941A-3F1D5A229024}'; + IID_IKeymanKeyboardInstalled2: TGUID = '{3086C85C-932A-4726-BF76-2D74DD133AC9}'; IID_IKeymanPackagesInstalled2: TGUID = '{F23B9848-2AEF-4A2B-BC3A-292E3A00D691}'; IID_IKeymanKeyboardFile2: TGUID = '{EDE4326B-51F4-42D5-8251-B20B71993EC8}'; IID_IKeymanPackageFile2: TGUID = '{9B43B6BC-C622-47EF-915E-6780CF53BAAA}'; IID_IKeymanKeyboardLanguageInstalled2: TGUID = '{414C26E6-BFAC-4A70-9EA1-E525BA9BBA7E}'; IID_IKeymanKeyboardLanguagesInstalled2: TGUID = '{628FF2E6-B490-462E-8FC7-7AE53B9D392C}'; + IID_IKeymanKeyboardFile3: TGUID = '{8F4B2D91-6C37-4A05-BE82-1D9F7C53A6E4}'; + IID_IKeymanPackageFile3: TGUID = '{C27A6E4B-9D13-47F8-A052-6B8E31D4F9C7}'; + IID_IKeymanKeyboardsInstalled3: TGUID = '{B7D3A8F1-6C42-4E95-AB17-93F0C2D8E641}'; + IID_IKeymanPackagesInstalled3: TGUID = '{3F8C2D71-94A6-4B0E-87D5-C1E3A9F62458}'; CLASS_Keyman: TGUID = '{CF46549D-4D2D-4679-A2E1-23A815F172F8}'; IID_IKeymanDefaultLanguage: TGUID = '{77BAB934-B7DF-4304-AFA6-B8F6BEC16516}'; @@ -248,6 +253,8 @@ type IKeymanKeyboardLanguagesFileDisp = dispinterface; IKeymanKeyboardsInstalled2 = interface; IKeymanKeyboardsInstalled2Disp = dispinterface; + IKeymanKeyboardInstalled2 = interface; + IKeymanKeyboardInstalled2Disp = dispinterface; IKeymanPackagesInstalled2 = interface; IKeymanPackagesInstalled2Disp = dispinterface; IKeymanKeyboardFile2 = interface; @@ -258,6 +265,14 @@ type IKeymanKeyboardLanguageInstalled2Disp = dispinterface; IKeymanKeyboardLanguagesInstalled2 = interface; IKeymanKeyboardLanguagesInstalled2Disp = dispinterface; + IKeymanKeyboardFile3 = interface; + IKeymanKeyboardFile3Disp = dispinterface; + IKeymanPackageFile3 = interface; + IKeymanPackageFile3Disp = dispinterface; + IKeymanKeyboardsInstalled3 = interface; + IKeymanKeyboardsInstalled3Disp = dispinterface; + IKeymanPackagesInstalled3 = interface; + IKeymanPackagesInstalled3Disp = dispinterface; IKeymanDefaultLanguage = interface; IKeymanDefaultLanguageDisp = dispinterface; @@ -1590,6 +1605,51 @@ type out References: OleVariant): WideString; dispid 401; end; +// *********************************************************************// +// Interface: IKeymanKeyboardInstalled2 +// Flags: (4416) Dual OleAutomation Dispatchable +// GUID: {3086C85C-932A-4726-BF76-2D74DD133AC9} +// *********************************************************************// + IKeymanKeyboardInstalled2 = interface(IKeymanKeyboardInstalled) + ['{3086C85C-932A-4726-BF76-2D74DD133AC9}'] + procedure MCompileForBaseKeyboard(KLID: Integer); safecall; + end; + +// *********************************************************************// +// DispIntf: IKeymanKeyboardInstalled2Disp +// Flags: (4416) Dual OleAutomation Dispatchable +// GUID: {3086C85C-932A-4726-BF76-2D74DD133AC9} +// *********************************************************************// + IKeymanKeyboardInstalled2Disp = dispinterface + ['{3086C85C-932A-4726-BF76-2D74DD133AC9}'] + procedure MCompileForBaseKeyboard(KLID: Integer); dispid 288; + property IconFilename: WideString readonly dispid 257; + procedure InstallVisualKeyboard(const Filename: WideString); dispid 258; + property KeymanID: Integer readonly dispid 259; + property Languages: IKeymanKeyboardLanguagesInstalled readonly dispid 260; + property Loaded: WordBool dispid 261; + property Options: IKeymanKeyboardOptions readonly dispid 262; + property OwnerPackage: IKeymanPackageInstalled readonly dispid 263; + property VisualKeyboard: IKeymanVisualKeyboard readonly dispid 264; + procedure Uninstall; dispid 265; + property Bitmap: IPicture readonly dispid 1; + property Copyright: WideString readonly dispid 2; + property DefaultBCP47Languages: WideString readonly dispid 3; + property DefaultPrimaryLanguage: Integer readonly dispid 4; + property DefaultWindowsLanguages: WideString readonly dispid 5; + property DefaultHotkey: IKeymanHotkey readonly dispid 6; + property Encodings: KeymanKeyboardEncodings readonly dispid 7; + property Filename: WideString readonly dispid 8; + function GetCharsUsed: WideString; dispid 9; + property ID: WideString readonly dispid 10; + property LayoutType: KeymanKeyboardLayoutType readonly dispid 11; + property Message: WideString readonly dispid 12; + property Name: WideString readonly dispid 13; + property Version: WideString readonly dispid 14; + function SerializeXML(Flags: tagKeymanSerializeFlags; const ImagePath: WideString; + out References: OleVariant): WideString; dispid 401; + end; + // *********************************************************************// // Interface: IKeymanPackagesInstalled2 // Flags: (4416) Dual OleAutomation Dispatchable @@ -1768,6 +1828,143 @@ type out References: OleVariant): WideString; dispid 401; end; +// *********************************************************************// +// Interface: IKeymanKeyboardFile3 +// Flags: (4416) Dual OleAutomation Dispatchable +// GUID: {8F4B2D91-6C37-4A05-BE82-1D9F7C53A6E4} +// *********************************************************************// + IKeymanKeyboardFile3 = interface(IKeymanKeyboardFile) + ['{8F4B2D91-6C37-4A05-BE82-1D9F7C53A6E4}'] + function Install3(Force: WordBool; BaseKeyboardID: Integer): IKeymanKeyboardInstalled; safecall; + end; + +// *********************************************************************// +// DispIntf: IKeymanKeyboardFile3Disp +// Flags: (4416) Dual OleAutomation Dispatchable +// GUID: {8F4B2D91-6C37-4A05-BE82-1D9F7C53A6E4} +// *********************************************************************// + IKeymanKeyboardFile3Disp = dispinterface + ['{8F4B2D91-6C37-4A05-BE82-1D9F7C53A6E4}'] + function Install3(Force: WordBool; BaseKeyboardID: Integer): IKeymanKeyboardInstalled; dispid 289; + procedure Install(Force: WordBool); dispid 256; + property Languages: IKeymanKeyboardLanguagesFile readonly dispid 402; + property Bitmap: IPicture readonly dispid 1; + property Copyright: WideString readonly dispid 2; + property DefaultBCP47Languages: WideString readonly dispid 3; + property DefaultPrimaryLanguage: Integer readonly dispid 4; + property DefaultWindowsLanguages: WideString readonly dispid 5; + property DefaultHotkey: IKeymanHotkey readonly dispid 6; + property Encodings: KeymanKeyboardEncodings readonly dispid 7; + property Filename: WideString readonly dispid 8; + function GetCharsUsed: WideString; dispid 9; + property ID: WideString readonly dispid 10; + property LayoutType: KeymanKeyboardLayoutType readonly dispid 11; + property Message: WideString readonly dispid 12; + property Name: WideString readonly dispid 13; + property Version: WideString readonly dispid 14; + function SerializeXML(Flags: tagKeymanSerializeFlags; const ImagePath: WideString; + out References: OleVariant): WideString; dispid 401; + end; + +// *********************************************************************// +// Interface: IKeymanPackageFile3 +// Flags: (4416) Dual OleAutomation Dispatchable +// GUID: {C27A6E4B-9D13-47F8-A052-6B8E31D4F9C7} +// *********************************************************************// + IKeymanPackageFile3 = interface(IKeymanPackageFile) + ['{C27A6E4B-9D13-47F8-A052-6B8E31D4F9C7}'] + function Install3(Force: WordBool; BaseKeyboardID: Integer): IKeymanPackageInstalled; safecall; + end; + +// *********************************************************************// +// DispIntf: IKeymanPackageFile3Disp +// Flags: (4416) Dual OleAutomation Dispatchable +// GUID: {C27A6E4B-9D13-47F8-A052-6B8E31D4F9C7} +// *********************************************************************// + IKeymanPackageFile3Disp = dispinterface + ['{C27A6E4B-9D13-47F8-A052-6B8E31D4F9C7}'] + function Install3(Force: WordBool; BaseKeyboardID: Integer): IKeymanPackageInstalled; dispid 292; + procedure Install(Force: WordBool); dispid 256; + property Author: WideString readonly dispid 1; + property AuthorEmail: WideString readonly dispid 2; + property Copyright: WideString readonly dispid 3; + property Filename: WideString readonly dispid 4; + property Files: IKeymanPackageContentFiles readonly dispid 5; + property Fonts: IKeymanPackageContentFonts readonly dispid 6; + property Graphic: IPicture readonly dispid 7; + property GraphicFile: IKeymanPackageContentFile readonly dispid 8; + property ID: WideString readonly dispid 9; + property KeyboardOptionsFile: IKeymanPackageContentFile readonly dispid 10; + property Keyboards: IKeymanPackageContentKeyboards readonly dispid 11; + property Name: WideString readonly dispid 12; + property ReadmeFile: IKeymanPackageContentFile readonly dispid 13; + property UsageFile: IKeymanPackageContentFile readonly dispid 14; + property Version: WideString readonly dispid 15; + property WelcomeFile: IKeymanPackageContentFile readonly dispid 16; + property Website: WideString readonly dispid 17; + function SerializeXML(Flags: tagKeymanSerializeFlags; const ImagePath: WideString; + out References: OleVariant): WideString; dispid 401; + end; + +// *********************************************************************// +// Interface: IKeymanKeyboardsInstalled3 +// Flags: (4416) Dual OleAutomation Dispatchable +// GUID: {B7D3A8F1-6C42-4E95-AB17-93F0C2D8E641} +// *********************************************************************// + IKeymanKeyboardsInstalled3 = interface(IKeymanKeyboardsInstalled) + ['{B7D3A8F1-6C42-4E95-AB17-93F0C2D8E641}'] + function Install3(const Filename: WideString; Force: WordBool; BaseKeyboardID: Integer): IKeymanKeyboardInstalled; safecall; + end; + +// *********************************************************************// +// DispIntf: IKeymanKeyboardsInstalled3Disp +// Flags: (4416) Dual OleAutomation Dispatchable +// GUID: {B7D3A8F1-6C42-4E95-AB17-93F0C2D8E641} +// *********************************************************************// + IKeymanKeyboardsInstalled3Disp = dispinterface + ['{B7D3A8F1-6C42-4E95-AB17-93F0C2D8E641}'] + function Install3(const Filename: WideString; Force: WordBool; BaseKeyboardID: Integer): IKeymanKeyboardInstalled; dispid 290; + property Items[Index: OleVariant]: IKeymanKeyboardInstalled readonly dispid 0; default; + function GetKeyboardFromFile(const Filename: WideString): IKeymanKeyboardFile; dispid 16; + procedure Install(const Filename: WideString; Force: WordBool); dispid 17; + procedure Apply; dispid 18; + function IndexOf(const ID: WideString): Integer; dispid 5; + property Count: Integer readonly dispid 1; + property _NewEnum: IUnknown readonly dispid -4; + procedure Refresh; dispid 2; + function SerializeXML(Flags: tagKeymanSerializeFlags; const ImagePath: WideString; + out References: OleVariant): WideString; dispid 401; + end; + +// *********************************************************************// +// Interface: IKeymanPackagesInstalled3 +// Flags: (4416) Dual OleAutomation Dispatchable +// GUID: {3F8C2D71-94A6-4B0E-87D5-C1E3A9F62458} +// *********************************************************************// + IKeymanPackagesInstalled3 = interface(IKeymanPackagesInstalled) + ['{3F8C2D71-94A6-4B0E-87D5-C1E3A9F62458}'] + function Install3(const Filename: WideString; Force: WordBool; BaseKeyboardID: Integer): IKeymanPackageInstalled; safecall; + end; + +// *********************************************************************// +// DispIntf: IKeymanPackagesInstalled3Disp +// Flags: (4416) Dual OleAutomation Dispatchable +// GUID: {3F8C2D71-94A6-4B0E-87D5-C1E3A9F62458} +// *********************************************************************// + IKeymanPackagesInstalled3Disp = dispinterface + ['{3F8C2D71-94A6-4B0E-87D5-C1E3A9F62458}'] + function Install3(const Filename: WideString; Force: WordBool; BaseKeyboardID: Integer): IKeymanPackageInstalled; dispid 291; + property Items[Index: OleVariant]: IKeymanPackageInstalled readonly dispid 0; default; + function GetPackageFromFile(const Filename: WideString): IKeymanPackageFile; dispid 16; + procedure Install(const Filename: WideString; Force: WordBool); dispid 17; + function IndexOf(const ID: WideString): Integer; dispid 18; + property Count: Integer readonly dispid 1; + property _NewEnum: IUnknown readonly dispid -4; + procedure Refresh; dispid 2; + function SerializeXML(Flags: tagKeymanSerializeFlags; const ImagePath: WideString; + out References: OleVariant): WideString; dispid 401; + end; + // *********************************************************************// // Interface: IKeymanDefaultLanguage // Flags: (4416) Dual OleAutomation Dispatchable diff --git a/windows/src/engine/kmcomapi/kmcomapi.ridl b/windows/src/engine/kmcomapi/kmcomapi.ridl index ac20310a87..0b2366c144 100644 --- a/windows/src/engine/kmcomapi/kmcomapi.ridl +++ b/windows/src/engine/kmcomapi/kmcomapi.ridl @@ -6,7 +6,7 @@ // However, when applying changes via the Editor this file will be regenerated // and comments or formatting changes will be lost. // ************************************************************************ // -// File generated on 16/09/2021 6:54:45 PM (- $Rev: 12980 $, 32940875). +// File generated on 31/08/2026 5:28:15 PM (- $Rev: 12980 $, 11059984). [ uuid(F16E2A9A-DA46-4EA3-BFF3-BA46B480C961), @@ -60,11 +60,16 @@ library keymanapi interface IKeymanKeyboardLanguagesInstalled; interface IKeymanKeyboardLanguagesFile; interface IKeymanKeyboardsInstalled2; + interface IKeymanKeyboardInstalled2; interface IKeymanPackagesInstalled2; interface IKeymanKeyboardFile2; interface IKeymanPackageFile2; interface IKeymanKeyboardLanguageInstalled2; interface IKeymanKeyboardLanguagesInstalled2; + interface IKeymanKeyboardFile3; + interface IKeymanPackageFile3; + interface IKeymanKeyboardsInstalled3; + interface IKeymanPackagesInstalled3; interface IKeymanBCP47Canonicalization; interface IKeymanDefaultLanguage; @@ -936,6 +941,19 @@ library keymanapi HRESULT _stdcall RefreshInstalledKeyboards(void); }; + [ + uuid(3086C85C-932A-4726-BF76-2D74DD133AC9), + version(19.0), + helpstring("https://help.keyman.com/developer/engine/windows/19.0/api/IKeymanKeyboardInstalled2"), + dual, + oleautomation + ] + interface IKeymanKeyboardInstalled2: IKeymanKeyboardInstalled + { + [id(0x00000120)] + HRESULT _stdcall MCompileForBaseKeyboard(long KLID); + }; + [ uuid(F23B9848-2AEF-4A2B-BC3A-292E3A00D691), version(14.0), @@ -1007,6 +1025,58 @@ library keymanapi HRESULT _stdcall Add([in] BSTR BCP47Tag, [out, retval] IKeymanKeyboardLanguageInstalled** Result); }; + [ + uuid(8F4B2D91-6C37-4A05-BE82-1D9F7C53A6E4), + version(19.0), + helpstring("http://help.keyman.com/developer/engine/desktop/19.0/api/IKeymanKeyboardFile3"), + dual, + oleautomation + ] + interface IKeymanKeyboardFile3: IKeymanKeyboardFile + { + [id(0x00000121)] + HRESULT _stdcall Install3([in] VARIANT_BOOL Force, [in] long BaseKeyboardID, [out, retval] IKeymanKeyboardInstalled** KeyboardResult); + }; + + [ + uuid(C27A6E4B-9D13-47F8-A052-6B8E31D4F9C7), + version(19.0), + helpstring("http://help.keyman.com/developer/engine/desktop/19.0/api/IKeymanPackageFile3"), + dual, + oleautomation + ] + interface IKeymanPackageFile3: IKeymanPackageFile + { + [id(0x00000124)] + HRESULT _stdcall Install3([in] VARIANT_BOOL Force, [in] long BaseKeyboardID, [out, retval] IKeymanPackageInstalled** PackageResult); + }; + + [ + uuid(B7D3A8F1-6C42-4E95-AB17-93F0C2D8E641), + version(19.0), + helpstring("https://help.keyman.com/developer/engine/windows/19.0/api/IKeymanKeyboardInstalled2\x03"), + dual, + oleautomation + ] + interface IKeymanKeyboardsInstalled3: IKeymanKeyboardsInstalled + { + [id(0x00000122)] + HRESULT _stdcall Install3([in] BSTR Filename, [in] VARIANT_BOOL Force, [in] long BaseKeyboardID, [out, retval] IKeymanKeyboardInstalled** KeyboardResult); + }; + + [ + uuid(3F8C2D71-94A6-4B0E-87D5-C1E3A9F62458), + version(19.0), + helpstring("http://help.keyman.com/developer/engine/desktop/19.0/api/IKeymanPackagesInstalled3"), + dual, + oleautomation + ] + interface IKeymanPackagesInstalled3: IKeymanPackagesInstalled + { + [id(0x00000123)] + HRESULT _stdcall Install3([in] BSTR Filename, [in] VARIANT_BOOL Force, [in] long BaseKeyboardID, [out, retval] IKeymanPackageInstalled** PackageResult); + }; + [ uuid(CA3B3B00-EA42-4EED-9043-D1A1F1842D52), dual, diff --git a/windows/src/engine/kmcomapi/processes/keyboard/kpinstallkeyboard.pas b/windows/src/engine/kmcomapi/processes/keyboard/kpinstallkeyboard.pas index 26c92faa90..1d198264cb 100644 --- a/windows/src/engine/kmcomapi/processes/keyboard/kpinstallkeyboard.pas +++ b/windows/src/engine/kmcomapi/processes/keyboard/kpinstallkeyboard.pas @@ -64,7 +64,7 @@ type ikLegacyRegisterAndInstallProfiles); TKPInstallKeyboard = class(TKPBase) - procedure Execute(const FileName, PackageID: string; FInstallOptions: TKPInstallKeyboardOptions; Languages: TPackageKeyboardLanguageList; Force: Boolean); + procedure Execute(const FileName: string; const PackageID: string; FInstallOptions: TKPInstallKeyboardOptions; Languages: TPackageKeyboardLanguageList; Force: Boolean; BaseKeyboardID: Integer); procedure RegisterProfiles(const FileName, PackageID: string; FInstallOptions: TKPInstallKeyboardOptions; PackageLanguageMetadata: TPackageKeyboardLanguageList); private procedure LegacyRegisterAndInstallLanguageProfile(Langs: array of Integer; @@ -112,7 +112,7 @@ uses utiltsf, keymanapi_TLB; -procedure TKPInstallKeyboard.Execute(const FileName, PackageID: string; FInstallOptions: TKPInstallKeyboardOptions; Languages: TPackageKeyboardLanguageList; Force: Boolean); +procedure TKPInstallKeyboard.Execute(const FileName: string; const PackageID: string; FInstallOptions: TKPInstallKeyboardOptions; Languages: TPackageKeyboardLanguageList; Force: Boolean; BaseKeyboardID: Integer); var ki: TKeyboardInfo; FDestPath: string; @@ -125,6 +125,9 @@ var FExitCode: Integer; FKVKName: WideString; FCreatedIcon: Boolean; + ElevatedBaseKeyboardID: Integer; + KeymanContext: TKeymanContext; + RecompileMnemonicKeyboard: TKPRecompileMnemonicKeyboard; begin KL.MethodEnter(Self, 'Execute', [FileName,PackageID,ikPartOfPackage in FInstallOptions ,Force]); try @@ -245,14 +248,21 @@ begin KL.Log(FLogText); end; - // Recompile a mnemonic layout to the user's selected base layout + // Recompile a mnemonic layout to the user's selected base layout. If + // the baselayout has not been passed through (=0) then use the current + // process configured value which is likely the Admin user if ki.MnemonicLayout then // I4169 begin - with TKPRecompileMnemonicKeyboard.Create(Context) do + KeymanContext := Context as TKeymanContext; + ElevatedBaseKeyboardID := (KeymanContext.Options as IKeymanOptions).Items['koBaseLayout'].Value; + RecompileMnemonicKeyboard := TKPRecompileMnemonicKeyboard.Create(Context); try - Execute(FDestFileName, PackageID); + if (BaseKeyboardID = 0) then + RecompileMnemonicKeyboard.Execute(FDestFileName, PackageID, ElevatedBaseKeyboardID) + else + RecompileMnemonicKeyboard.Execute(FDestFileName, PackageID, BaseKeyboardID); finally - Free; + RecompileMnemonicKeyboard.Free; end; end; finally diff --git a/windows/src/engine/kmcomapi/processes/keyboard/kprecompilemnemonickeyboard.pas b/windows/src/engine/kmcomapi/processes/keyboard/kprecompilemnemonickeyboard.pas index 30d0c275cc..52139f5839 100644 --- a/windows/src/engine/kmcomapi/processes/keyboard/kprecompilemnemonickeyboard.pas +++ b/windows/src/engine/kmcomapi/processes/keyboard/kprecompilemnemonickeyboard.pas @@ -1,18 +1,18 @@ (* Name: kprecompilemnemonickeyboard Copyright: Copyright (C) SIL International. - Documentation: - Description: + Documentation: + Description: Create Date: 24 Apr 2014 Modified Date: 13 Mar 2015 Authors: mcdurdin - Related Files: - Dependencies: + Related Files: + Dependencies: - Bugs: - Todo: - Notes: + Bugs: + Todo: + Notes: History: 24 Apr 2014 - mcdurdin - I4174 - V9 - mcompile logs should be stored in diag folder 06 Feb 2015 - mcdurdin - I4552 - V9.0 - Add mnemonic recompile option to ignore deadkeys 13 Mar 2015 - mcdurdin - I4615 - CrashID:kmshell.exe_9.0.481.0_2C6795CE_EOleException @@ -27,7 +27,7 @@ uses type TKPRecompileMnemonicKeyboard = class(TKPBase) - procedure Execute(const FileName: string; const PackageName: string); + procedure Execute(const FileName: string; const PackageName: string; BaseKeyboardID: Cardinal); end; implementation @@ -42,13 +42,12 @@ uses Winapi.Windows, errorcontrolledregistry, - keymancontext, keymanerrorcodes, KeymanPaths, - keymanapi_TLB, RegistryKeys, utilexecute, utilkeyman, + utilfiletypes, utilsystem; function GetKeyboardLayoutFileName(id: Integer): string; @@ -67,7 +66,7 @@ begin Result := ''; end; -procedure TKPRecompileMnemonicKeyboard.Execute(const FileName,PackageName: string); +procedure TKPRecompileMnemonicKeyboard.Execute(const FileName,PackageName: string; BaseKeyboardID: Cardinal); var FDestPath, FDestFileName: string; FBaseKeyboardIDHex: string; @@ -76,7 +75,6 @@ var FExitCode: Integer; FMCompilePath: string; FBaseKeyboardFileName: string; - BaseKeyboardID: Cardinal; FDestDeadkeyFileName: string; FCommand: string; begin @@ -84,13 +82,10 @@ begin then FDestPath := GetPackageInstallPath(PackageName) // I3581 else FDestPath := GetKeyboardInstallPath(FileName); // I3581 - with Context as TKeymanContext do - BaseKeyboardID := (Options as IKeymanOptions).Items['koBaseLayout'].Value; - FBaseKeyboardIDHex := IntToHex(BaseKeyboardID, 8); FBaseFileName := FDestPath + '\' + ExtractFileName(FileName); // I3581 - FDestFileName := ChangeFileExt(FBaseFileName, '') + '-'+FBaseKeyboardIDHex + '.kmx'; - FDestDeadkeyFileName := ChangeFileExt(FBaseFileName, '') + '-'+FBaseKeyboardIDHex + '-d.kmx'; // I4552 + FDestFileName := BuildKeyboardFilenameWithBaseKeyboardID(FBaseFileName, FBaseKeyboardIDHex); + FDestDeadkeyFileName := BuildKeyboardFilenameWithBaseKeyboardIDAndDeadkey(FBaseFileName, FBaseKeyboardIDHex); // I4552 FMCompilePath := TKeymanPaths.KeymanEngineInstallPath(TKeymanPaths.S_MCompileExe); { Recompile with the traditional deadkey behaviour } diff --git a/windows/src/engine/kmcomapi/processes/package/kpinstallpackage.pas b/windows/src/engine/kmcomapi/processes/package/kpinstallpackage.pas index 2c1c5131cb..51756a2152 100644 --- a/windows/src/engine/kmcomapi/processes/package/kpinstallpackage.pas +++ b/windows/src/engine/kmcomapi/processes/package/kpinstallpackage.pas @@ -50,7 +50,7 @@ type TKPInstallPackage = class(TKPBase) public - procedure Execute(const FileName: string; Options: TKPInstallPackageOptions); + procedure Execute(const FileName: string; Options: TKPInstallPackageOptions; BaseKeyboardID: Integer); end; implementation @@ -85,7 +85,7 @@ uses { TKPInstallPackage } -procedure TKPInstallPackage.Execute(const FileName: string; Options: TKPInstallPackageOptions); +procedure TKPInstallPackage.Execute(const FileName: string; Options: TKPInstallPackageOptions; BaseKeyboardID: Integer); function GetHHIcon: string; var buf: array[0..260] of char; @@ -112,7 +112,7 @@ var FErrorValue: Cardinal; FSrcFileName: string; - procedure InstallKeyboard(FileName: string); + procedure InstallKeyboard(FileName: string; BaseKeyboardID: Integer); var FOptions: TKPInstallKeyboardOptions; kbd: TPackageKeyboard; @@ -132,7 +132,7 @@ var with TKPInstallKeyboard.Create(Context) do try - Execute(FileName, PackageName, FOptions, FLanguages, ipForce in Options); + Execute(FileName, PackageName, FOptions, FLanguages, ipForce in Options, BaseKeyboardID); finally Free; end; @@ -168,8 +168,8 @@ begin except on E:EZipException do begin - ErrorFmt(KMN_E_Install_InvalidFile, VarArrayOf([ExtractFileName(FileName), E.Message, 0])); - raise; + ErrorFmt(KMN_E_Install_InvalidFile, VarArrayOf([ExtractFileName(FileName), E.Message, 0])); + raise; end; end; @@ -262,12 +262,12 @@ begin begin case inf.Files[i].FileType of ftKeymanFile: - InstallKeyboard(dest + inf.Files[i].FileName); + InstallKeyboard(dest + inf.Files[i].FileName, BaseKeyboardID); ftPackageFile: with TKPInstallPackage.Create(Context) do try - Execute(dest + inf.Files[i].FileName, Options); + Execute(dest + inf.Files[i].FileName, Options, BaseKeyboardID); finally Free; end; diff --git a/windows/src/engine/kmcomapi/util/internalinterfaces.pas b/windows/src/engine/kmcomapi/util/internalinterfaces.pas index 1759468e00..edfc65bf28 100644 --- a/windows/src/engine/kmcomapi/util/internalinterfaces.pas +++ b/windows/src/engine/kmcomapi/util/internalinterfaces.pas @@ -1,24 +1,24 @@ (* Name: internalinterfaces Copyright: Copyright (C) 2003-2017 SIL International. - Documentation: - Description: + Documentation: + Description: Create Date: 25 Jan 2011 Modified Date: 17 Aug 2014 Authors: mcdurdin - Related Files: - Dependencies: + Related Files: + Dependencies: - Bugs: - Todo: - Notes: + Bugs: + Todo: + Notes: History: 25 Jan 2011 - mcdurdin - I2569 - Keyboard welcome should always shown from kmshell 01 Jan 2013 - mcdurdin - I3717 - V9.0 - Need ability to select base keyboard in Keyman Configuration 16 Apr 2014 - mcdurdin - I4169 - V9.0 - Mnemonic layouts should be recompiled to positional based on user-selected base keyboard 17 Aug 2014 - mcdurdin - I4376 - V9.0 - Unticked keyboards in configuration should be removed from language profile 17 Aug 2014 - mcdurdin - I4381 - V9.0 - Keyman keyboards should be removed from language bar when Keyman exits - + *) unit internalinterfaces; @@ -41,7 +41,7 @@ type IIntKeymanInterface = interface ['{D1EBBED5-B9E3-4807-969D-DCF9E1FFB287}'] function XMLClassName: WideString; - function Serialize(Flags: TOleEnum; const ImagePath: WideString; References: TStrings): WideString; + function Serialize(Flags: TOleEnum; const ImagePath: WideString; References: TStrings): WideString; function DoSerialize(Flags: TOleEnum; const ImagePath: WideString; References: TStrings): WideString; // Wraps serialize with tag end; @@ -67,7 +67,6 @@ type ['{4876E6DF-C557-46E2-84F4-787BE5F55DDA}'] function RegKeyboard: TRegKeyboard; procedure ClearVisualKeyboard; - procedure UpdateBaseLayout; // I4169 procedure RefreshInstallation; end;