chore(developer): Merge branch 'master' into change/developer/touch-layout-reorganize-files

This commit is contained in:
Marc Durdin 2026-09-24 09:05:12 +02:00
commit 7bdf62f0bf
58 changed files with 889 additions and 439 deletions

View file

@ -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)

View file

@ -1 +1 @@
19.0.289
19.0.291

2
android/.gitignore vendored
View file

@ -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

View file

@ -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";

View file

@ -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

View file

@ -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)}']`);

View file

@ -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-<KLID>.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-<KLID>-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.

View file

@ -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);

View file

@ -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);
}

View file

@ -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));

23
package-lock.json generated
View file

@ -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"
},

View file

@ -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
}

View file

@ -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/'
});

View file

@ -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');

View file

@ -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;

View file

@ -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"
}
}

View file

@ -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"
}

View file

@ -27,10 +27,16 @@ export interface CorrectionResultMapping<ResultType> {
readonly matchedResult: Readonly<ResultType>;
/**
* 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;
}

View file

@ -29,10 +29,14 @@ type CompleteSearchPath<MappingType> = {
export type PathResult<MappingType> = NullPath | IntermediateSearchPath | CompleteSearchPath<MappingType>;
export function CORRECTION_QUEUE_COMPARATOR<T extends {currentCost: number}>(a: T, b: T) {
export function PREDICTION_QUEUE_COMPARATOR<T extends {currentCost: number}>(a: T, b: T) {
return a.currentCost - b.currentCost;
}
export function CORRECTION_QUEUE_COMPARATOR<T extends {correctionCost: number}>(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

View file

@ -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<Correctable>(CORRECTION_QUEUE_COMPARATOR);
let spaceQueue = new PriorityQueue<Correctable>(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<ResultMapping>((a, b) => a.totalCost - b.totalCost);
const priorResultsQueue = new PriorityQueue<ResultMapping>(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;

View file

@ -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<SearchNode> = new PriorityQueue(CORRECTION_QUEUE_COMPARATOR);
private selectionQueue: PriorityQueue<SearchNode> = new PriorityQueue(PREDICTION_QUEUE_COMPARATOR);
private processed: SearchNode[] = [];
constructor(model: LexicalModel) {

View file

@ -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<SearchNode> = new PriorityQueue(CORRECTION_QUEUE_COMPARATOR);
private transposeQueue: PriorityQueue<SearchNode> = new PriorityQueue(PREDICTION_QUEUE_COMPARATOR);
private incomingTransposeRootNodes: TokenResultMapping[] = [];
public readonly insertLength: number;

View file

@ -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<SearchQuotientNode> = new PriorityQueue(CORRECTION_QUEUE_COMPARATOR);
private selectionQueue: PriorityQueue<SearchQuotientNode> = 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<SearchQuotientNode>(CORRECTION_QUEUE_COMPARATOR, entries.slice());
this.selectionQueue = new PriorityQueue<SearchQuotientNode>(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') {

View file

@ -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<SearchNode> = new PriorityQueue(CORRECTION_QUEUE_COMPARATOR);
private selectionQueue: PriorityQueue<SearchNode> = 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<SearchNode>(CORRECTION_QUEUE_COMPARATOR, entries);
this.selectionQueue = new PriorityQueue<SearchNode>(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;
}

View file

@ -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<SearchNode> {
* 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;
}

View file

@ -532,7 +532,7 @@ export function buildAndMapPredictions(
transition: ContextTransition,
tokenization: ContextTokenization,
// Originally, Readonly<TokenResultMapping> - 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<string, Distribution<Suggestion>> = {};
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;
}
}

View file

@ -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

View file

@ -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];

View file

@ -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.)

View file

@ -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;

View file

@ -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<Readonly<TokenResultMapping>, any, any>) => {
const firstIterResult = await iter.next(); // {value: <actual value>, done: <iteration complete?>}
assert.isFalse(firstIterResult.done);
const firstResult: TokenResultMapping = firstIterResult.value; // Retrieves <actual value>
// 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);
});
});
});

View file

@ -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);
});
});
});

View file

@ -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;
}

View file

@ -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
);

View file

@ -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

View file

@ -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
/// </summary>
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

View file

@ -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

View file

@ -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;

View file

@ -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

View file

@ -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);

View file

@ -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}

View file

@ -358,6 +358,7 @@
<Form>frmStartInstall</Form>
</DCCReference>
<DCCReference Include="util\Keyman.Configuration.Util.NetworkConnection.pas"/>
<DCCReference Include="main\Keyman.Configuration.System.BaseKeyboard.pas"/>
<None Include="Profiling\AQtimeModule1.aqt"/>
<BuildConfiguration Include="Debug">
<Key>Cfg_2</Key>
@ -419,6 +420,12 @@
<Platform value="Win64">False</Platform>
</Platforms>
<Deployment Version="3">
<DeployFile LocalName="bin\Win32\Debug\kmshell.exe" Configuration="Debug" Class="ProjectOutput">
<Platform Name="Win32">
<RemoteName>kmshell.exe</RemoteName>
<Overwrite>true</Overwrite>
</Platform>
</DeployFile>
<DeployFile LocalName="Profiling\AQtimeModule1.aqt" Configuration="Debug" Class="ProjectFile">
<Platform Name="Win32">
<RemoteDir>.\</RemoteDir>
@ -431,12 +438,6 @@
<Overwrite>true</Overwrite>
</Platform>
</DeployFile>
<DeployFile LocalName="bin\Win32\Debug\kmshell.exe" Configuration="Debug" Class="ProjectOutput">
<Platform Name="Win32">
<RemoteName>kmshell.exe</RemoteName>
<Overwrite>true</Overwrite>
</Platform>
</DeployFile>
<DeployClass Name="AdditionalDebugSymbols">
<Platform Name="OSX32">
<Operation>1</Operation>

View file

@ -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.

View file

@ -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.

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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.

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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 }

View file

@ -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;

View file

@ -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;