diff --git a/.github/workflows/deb-packaging.yml b/.github/workflows/deb-packaging.yml index 52c485aa65..bf1bfca2fa 100644 --- a/.github/workflows/deb-packaging.yml +++ b/.github/workflows/deb-packaging.yml @@ -116,7 +116,7 @@ jobs: strategy: fail-fast: true matrix: - dist: [focal, jammy, noble, oracular] + dist: [jammy, noble, oracular] steps: - name: Checkout diff --git a/HISTORY.md b/HISTORY.md index 1815302cac..544a275dea 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,84 @@ # Keyman Version History +## 19.0.2 alpha 2025-02-12 + +* chore(linux): remove support of Ubuntu 20.04 Focal (#13203) + +## 19.0.1 alpha 2025-02-11 + +* refactor(windows): rename `TKeymanMutex.MutexOwned` to `TakeOwnership` and add `ReleaseOwnership` (#13168) +* chore: increment to alpha 19.0 (#13187) + +## 18.0.188 alpha 2025-02-10 + +* fix(windows): check the params status flag equals ucrsUpdateReady before attempting to download the keyman setup file (#13154) +* feat(windows): background updates go from downloading to waiting for a restart except if the `apply now` flag is set (#13159) +* feat(developer): verify package version number format in kmc-package (#13118) +* fix(developer): support non-US base keyboard layouts in debuggers (#13131) +* feat(developer): improve compiler messages and user interface (#13156) +* feat(developer): verify that packages do not contain themselves in kmc-package (#13157) +* fix(developer): link welcome.htm in package for new projects; use v17 project format for new models (#13161) +* fix(windows): use Automatically check for updates and download for the english xml (#13162) +* change(linux): Implement ordered output without patched ibus (#11535) + +## 18.0.187 alpha 2025-02-07 + +* docs(web): relocate gesture docs to web/docs/internal (#13139) +* chore(mac): remove empty options tab from configuration (#13160) +* chore(linux): Update debian changelog (#13143) + +## 18.0.186 alpha 2025-02-06 + +* feat(windows): check updates and automatic update merged (#13115) + +## 18.0.185 alpha 2025-02-05 + +* fix(developer): make kmc log options consistent across all commands (#13075) +* feat(developer): add user interface for `kmc copy` to TIKE (#13076) +* chore(developer): add verbose logs for project copier (#13080) +* chore(mac, ios): build and run with xcode 16 (#13077) + +## 18.0.184 alpha 2025-02-04 + +* fix(common): handle undefined input in `@keymanapp/langtags` lookups (#13117) +* chore: update docs to reference node 20.0 (#13124) +* feat(developer): include command line in kmc sentry reports (#13113) +* fix(common/web): replace invalid StrsItem identity checks with isEqual() (#13122) + +## 18.0.183 alpha 2025-02-03 + +* change(ios): set host-app bundle identifier in embedded Web debug-report (#13105) +* fix(ios): prevent message-handler collision (#13037) +* change(web): add context tracker bksp handling, alignment-offset calculations (#12911) +* fix(web): correct edit-path ordering at source (#12925) +* fix(web): correctly track context transitions caused by applying suggestions (#12927) +* chore(linux): preserve log files when running tests under Docker (#13107) + +## 18.0.182 alpha 2025-01-31 + +* feat(windows): report on processor type in tsysinfo (#13084) +* fix(common): push branch when creating PR in version history writer (#13082) +* refactor: move langtags to @keymanapp/langtags npm module (#13046) +* chore(developer): use langtags.json for kmc-generate language names (#13047) +* feat(developer): support relocation of external files in kmc-copy (#13061) +* feat(windows): kmshell switch handling for the installing state (#12956) +* feat(windows): add boot switch (#12995) +* docs(web): remove `.md` in help links (#13051) +* chore: add android platform to titles (#13033) +* chore: add the windows platform to titles (#13026) +* fix(mac): properly manage Input Method lifecycle (#13006) +* chore(developer): handle forward slashes in paths in package editor (#13081) +* chore(developer,core,resources): LDML v46 update and remove workaround for import base (#13099) + +## 18.0.181 alpha 2025-01-30 + +* feat(windows): add localized button captions and form labels (#13025) +* fix(web): disabled correction state should prevent predictive corrections (#13066) +* fix(web): disabled autoaccept should not autohighlight suggestions (#13068) +* chore(common): add 17.0.334 to version history (#13065) +* fix(windows): add api GetContext and TIPProcessKeyEx with IsTextSelected bool (#13023) +* fix(web): keyboard query error states should always provide informative Error objects, reports (#13048) + ## 18.0.180 alpha 2025-01-29 * chore(common): Update crowdin strings for Portuguese (#13056) @@ -1242,6 +1321,18 @@ * chore(common): move to 18.0 alpha (#10713) * chore: move to 18.0 alpha +## 17.0.335 alpha 2025-02-06 + +* fix(android): improve resource-update tool handling of host Activity's closure (#13057) +* fix(ios): prevent message-handler collision (#13058) +* chore(linux): improve compatibility with Gentoo Linux (#12889) +* chore(linux): Update debian changelog (#13062) +* fix(web): keyboard query error states should always provide informative Error objects, reports (#13079) +* chore: push the branch when creating PR in version history writer (#13085) +* fix(web): disabled correction state should prevent predictive corrections (#13074) +* change(ios): set host-app bundle identifier in embedded Web debug-report (#13112) +* chore(mac, ios): build and run with xcode 16 (#13121) + ## 17.0.334 stable 2025-01-27 * chore(linux): update copyright year (#12919) diff --git a/VERSION.md b/VERSION.md index 3428a04a07..dd7d796646 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -18.0.181 \ No newline at end of file +19.0.3 \ No newline at end of file diff --git a/android/docs/engine/guides/in-app/index.md b/android/docs/engine/guides/in-app/index.md index bfa37d4622..538094ddbb 100644 --- a/android/docs/engine/guides/in-app/index.md +++ b/android/docs/engine/guides/in-app/index.md @@ -1,13 +1,13 @@ --- -title: Guide: build an in-app keyboard +title: Guide: build an in-app keyboard for Android with Keyman Engine --- Keyman Engine for Android allows you to use any Keyman touch keyboard in your Android app, or even to create your own system keyboard app for purchase in the Play Store.
This guide will walk you through the steps for creating your first Android app with Keyman Engine for Android. -If you are not familiar with Android development, you will find the -[Android Developer online training](https://developer.android.com/training/index.html) an invaluable +If you are not familiar with Android development, you will find the +[Android Developer online training](https://developer.android.com/training/index.html) an invaluable resource, and working through some of their tutorials first will help you with the rest of this guide. ### 1. Install Free Tools @@ -131,5 +131,5 @@ And there you have it: your first Keyman Engine for Android app! ## See Also * [Guide: Build a system keyboard app](../system-keyboard/) * [Keyman Developer Documentation](/developer/17.0/) -* [Keyman Engine for Android Documentation](/developer/engine/android/18.0/) +* [Keyman Engine for Android Documentation](/developer/engine/android/19.0/) * [Android Developer Home](https://developer.android.com/index.html) diff --git a/android/docs/engine/guides/system-keyboard/index.md b/android/docs/engine/guides/system-keyboard/index.md index 227bb4f33c..7a4901e166 100644 --- a/android/docs/engine/guides/system-keyboard/index.md +++ b/android/docs/engine/guides/system-keyboard/index.md @@ -1,5 +1,5 @@ --- -title: Guide: build a system keyboard app +title: Guide: build a system keyboard app for Android with Keyman Engine --- In [part 1](../in-app/) of this series, we looked at the steps involved diff --git a/android/docs/engine/index.md b/android/docs/engine/index.md index fa549e4b34..38f255592b 100644 --- a/android/docs/engine/index.md +++ b/android/docs/engine/index.md @@ -4,7 +4,7 @@ title: Keyman Engine for Android ## Overview -Keyman Engine for Android 18.0 is a Java library for Android 5.0 and later versions which enables a fully customisable keyboard layout, both within an app and system-wide. +Keyman Engine for Android 19.0 is a Java library for Android 5.0 and later versions which enables a fully customisable keyboard layout, both within an app and system-wide. Keyboard layouts for Keyman Engine can be created with [Keyman Developer](/developer/17.0), and a [library of existing keyboard layouts](http://keyman.com/developer/keymanweb/keyboards) is also available.

diff --git a/android/docs/engine/whatsnew.md b/android/docs/engine/whatsnew.md index af3f8932d6..5d6f717f8a 100644 --- a/android/docs/engine/whatsnew.md +++ b/android/docs/engine/whatsnew.md @@ -1,11 +1,6 @@ --- -title: What's New in Keyman Engine 18.0 for Android +title: What's New in Keyman Engine 19.0 for Android --- -## Keyman Engine for Android Breaking Changes: ## - -* Change package name from `com.tavultesoft.kmea` to `com.keyman.engine` #7881 -* Update to Java 11 #8543 - ## See Also * [Keyman Engine for Android Documentation](index) \ No newline at end of file diff --git a/android/docs/help/about/index.md b/android/docs/help/about/index.md index 787c0c9d4d..4428a4bb75 100644 --- a/android/docs/help/about/index.md +++ b/android/docs/help/about/index.md @@ -1,5 +1,5 @@ --- -title: About Keyman +title: About Keyman for Android --- * [Welcome to Keyman](welcome) diff --git a/android/docs/help/about/whatsnew.md b/android/docs/help/about/whatsnew.md index 0f0e891e42..d0ca85e93f 100644 --- a/android/docs/help/about/whatsnew.md +++ b/android/docs/help/about/whatsnew.md @@ -1,9 +1,6 @@ --- -title: What's New in Keyman 18.0 for Android +title: What's New in Keyman 19.0 for Android --- -Here are some of the new features we have added to Keyman 18.0 for Android: +Here are some of the new features we have added to Keyman 19.0 for Android: -* New menu to adjust longpress delay time (#12170, #12185) -* Support localizations for right-to-left languages (#12215) -* Handle additional actions for ENTER key (#12125, #12315) diff --git a/android/docs/help/basic/index.md b/android/docs/help/basic/index.md index 02d942ca0e..4a0d7aafe0 100644 --- a/android/docs/help/basic/index.md +++ b/android/docs/help/basic/index.md @@ -1,5 +1,5 @@ --- -title: Basic Help +title: Basic Help - Keyman for Android --- * [Using the Banner on the Keyboard](using-the-banner) diff --git a/android/docs/help/context/gestures.md b/android/docs/help/context/gestures.md index d46ab541f6..ec3fc690bd 100644 --- a/android/docs/help/context/gestures.md +++ b/android/docs/help/context/gestures.md @@ -1,5 +1,5 @@ --- -title: Gesture Hints and Tips +title: Gesture Hints and Tips - Keyman for Android --- ## Using Gestures on the Keyboard Keys diff --git a/android/docs/help/index.md b/android/docs/help/index.md index 62df93195b..360af91e6d 100644 --- a/android/docs/help/index.md +++ b/android/docs/help/index.md @@ -1,5 +1,5 @@ --- -title: Keyman for Android 18.0 Help +title: Keyman for Android 19.0 Help --- ## [About Keyman](about/) diff --git a/android/docs/help/start/enabling-system-keyboard.md b/android/docs/help/start/enabling-system-keyboard.md index 381fbb3e27..dcf16b97fb 100644 --- a/android/docs/help/start/enabling-system-keyboard.md +++ b/android/docs/help/start/enabling-system-keyboard.md @@ -1,5 +1,5 @@ --- -title: How To - Enabling Keyman as a System-Wide Keyboard +title: How To - Enabling Keyman for Android as a System-Wide Keyboard --- ## Enabling System Wide Keyboards diff --git a/android/docs/help/start/index.md b/android/docs/help/start/index.md index 4787ce9f9b..7d46903c58 100644 --- a/android/docs/help/start/index.md +++ b/android/docs/help/start/index.md @@ -1,5 +1,5 @@ --- -title: Getting Started +title: Getting Started with Keyman for Android --- * [How To - Download and Install a Keyman Keyboard](installing-keyboards) diff --git a/android/docs/help/troubleshooting/grant-storage-permission.md b/android/docs/help/troubleshooting/grant-storage-permission.md index 7b47cdf4ea..82f1e0d1aa 100644 --- a/android/docs/help/troubleshooting/grant-storage-permission.md +++ b/android/docs/help/troubleshooting/grant-storage-permission.md @@ -1,5 +1,5 @@ --- -title: How To - Granting Storage Permission +title: How To - Granting Storage Permission on Keyman for Android --- ## Granting Storage Permission diff --git a/common/web/build.sh b/common/web/build.sh index 798db3b231..2370e3ecf4 100755 --- a/common/web/build.sh +++ b/common/web/build.sh @@ -10,6 +10,7 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" builder_describe "Keyman common web modules" \ :keyman-version \ + :langtags \ :types \ clean \ configure \ diff --git a/common/web/langtags/.gitignore b/common/web/langtags/.gitignore new file mode 100644 index 0000000000..a0d996af40 --- /dev/null +++ b/common/web/langtags/.gitignore @@ -0,0 +1 @@ +src/imports/ diff --git a/common/web/langtags/README.md b/common/web/langtags/README.md new file mode 100644 index 0000000000..d55707a510 --- /dev/null +++ b/common/web/langtags/README.md @@ -0,0 +1,38 @@ +# Keyman langtags + +This package provides a wrapper for the +[langtags.json](https://ldml.api.sil.org/langtags.json) dataset from +https://github.com/silnrsi/langtags. + +This package is published in sync with Keyman, so changes in langtags may not +propagate immediately. + +## Usage + +* `getLangtagByTag(tag)`: Find a language tag from a given tag; matches on tag, + tags, full properties, with a case-insensitive search. Returns unmodified + object from langtags.json on match. + +* `metadata.conformance()`: Returns script and region conformance data from + langtags.json + +* `metadata.globalvar()`: Returns global variant data from langtags.json + +* `metadata.phonvar()`: Returns phonetic variant data from langtags.json + +* `metadata.version()`: Returns version data from langtags.json + +* `langtags`: Raw access to the langtags.json array of objects + +## Background + +Key documents for further reading (from langtags repository): + +* [Overview of language tags and how this project works with them](https://github.com/silnrsi/langtags/blob/master/doc/tagging.md) + +* [How to use langtags.json](https://github.com/silnrsi/langtags/blob/master/doc/langtags.md) + +## License + +* Copyright ©️ 2025 SIL Global. +* MIT License ([License text](https://github.com/keymanapp/keyman/tree/master/LICENSE.md)) diff --git a/common/web/langtags/build.sh b/common/web/langtags/build.sh new file mode 100755 index 0000000000..f125448d4a --- /dev/null +++ b/common/web/langtags/build.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +## START STANDARD BUILD SCRIPT INCLUDE +# adjust relative paths as necessary +THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" +. "${THIS_SCRIPT%/*}/../../../resources/build/builder.inc.sh" +## END STANDARD BUILD SCRIPT INCLUDE + +. "$KEYMAN_ROOT/resources/shellHelperFunctions.sh" +. "$KEYMAN_ROOT/resources/build/build-utils-ci.inc.sh" + +builder_describe "Build Keyman langtags.js common module" \ + "clean" \ + "configure" \ + "build" \ + "test" \ + "publish publish to npm" \ + "--npm-publish+ For publish, do a npm publish, not npm pack (only for CI)" \ + "--dry-run,-n don't actually publish, just dry run" + +builder_describe_outputs \ + configure /common/web/langtags/src/imports/langtags.js \ + build /common/web/langtags/build/src/main.js + +builder_parse "$@" + +#------------------------------------------------------------------------------------------------------------------- + +function compile_langtags() { + mkdir -p src/imports + echo 'export default ' > src/imports/langtags.js + cat "$KEYMAN_ROOT/resources/standards-data/langtags/langtags.json" >> src/imports/langtags.js +} + +function do_configure() { + verify_npm_setup + compile_langtags +} + +#------------------------------------------------------------------------------------------------------------------- + +builder_run_action clean rm -rf ./build ./src/imports ./node_modules +builder_run_action configure do_configure +builder_run_action build tsc --build +builder_run_action test echo 'no tests for langtags' +builder_run_action publish builder_publish_npm diff --git a/common/web/langtags/package.json b/common/web/langtags/package.json new file mode 100644 index 0000000000..f2848f587f --- /dev/null +++ b/common/web/langtags/package.json @@ -0,0 +1,39 @@ +{ + "name": "@keymanapp/langtags", + "description": "Keyman bundling of language tags from langtags.json", + "keywords": [ + "keyboard", + "keyman", + "ldml", + "unicode" + ], + "type": "module", + "types": "./build/src/main.d.ts", + "exports": { + ".": { + "es6-bundling": "./src/main.ts", + "types": "./build/src/main.d.ts", + "default": "./build/src/main.js" + } + }, + "files": [ + "/build/src/" + ], + "scripts": { + "build": "tsc -b", + "test": "echo no tests for langtags" + }, + "author": "Marc Durdin (https://github.com/mcdurdin)", + "license": "MIT", + "bugs": { + "url": "https://github.com/keymanapp/keyman/issues" + }, + "devDependencies": { + "typescript": "^5.4.5" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/keymanapp/keyman.git" + }, + "sideEffects": false +} diff --git a/common/web/langtags/src/main.ts b/common/web/langtags/src/main.ts new file mode 100644 index 0000000000..410bae313a --- /dev/null +++ b/common/web/langtags/src/main.ts @@ -0,0 +1,122 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Wraps langtags.json as a Javascript object + */ + +import langtags from './imports/langtags.js'; + +// These interfaces and types correspond to https://github.com/silnrsi/langtags/blob/master/source/langtags_schema.json + +export type LangTagBcp47 = string; // BCP 47 +export type LangTagBcp47Variant = string; // BCP 47 variant +export type LangTagIso639_3 = string; // ISO639-3 +export type LangTagIso3166_1 = string; // region +export type LangTagIso15924 = string; // script + + +export interface LangTag { + full: string; // should be LangTagBcp47? + iana?: string[]; + iso639_3?: LangTagIso639_3; + latnnames?: string[]; + localname?: string; + localnames?: string; + macrolang?: LangTagBcp47; + name?: string; + names?: string[]; + nophonvars?: boolean; + obsolete?: boolean; + region?: LangTagIso3166_1; + regionname?: string; + regions?: LangTagIso3166_1[]; + rod?: string; + script?: LangTagIso15924; + sldr?: boolean; + suppress?: boolean; + tag?: LangTagBcp47; + tags?: LangTagBcp47[]; + unwritten?: boolean; + variants?: LangTagBcp47Variant[]; + windows?: LangTagBcp47; +}; + +export interface LangTagConformance { + regions: LangTagIso3166_1[]; + scripts: LangTagIso15924[]; + tag: '_conformance'; +}; + +export interface LangTagGlobalVar { + variants: LangTagBcp47Variant[]; + tag: '_globalvar'; +}; + +export interface LangTagPhonVar { + variants: LangTagBcp47Variant[]; + tag: '_phonvar'; +}; + +export interface LangTagVersion { + api: string; + date: string; + tag: '_version'; +}; + +/** + * A map of all language tags, lower cased + */ +const langtagsByTag: Map = new Map(); + +/* metadata variables */ +let conformance: LangTagConformance = undefined; +let globalvar: LangTagGlobalVar = undefined; +let phonvar: LangTagPhonVar = undefined; +let version: LangTagVersion = undefined; + +const metadata = { + conformance: () => ({ regions: [...conformance.regions], scripts: [...conformance.scripts] }), + globalvar: () => ({ variants: [...globalvar.variants] }), + phonvar: () => ({ variants: [...phonvar.variants] }), + version: () => ({ api: version.api, date: version.date }) +}; + +/** + * Build a dictionary of language tags from langtags.json. This takes under 10ms + * on a reasonable laptop. + */ +function preinit(): void { + for(const tag of langtags) { + if(tag.tag == '_conformance') { + conformance = tag; + } else if(tag.tag == '_globalvar') { + globalvar = tag; + } else if(tag.tag == '_phonvar') { + phonvar = tag; + } else if(tag.tag == '_version') { + version = tag; + } else if(tag.full && tag.tag) { + langtagsByTag.set(tag.tag.toLowerCase(), tag); + langtagsByTag.set(tag.full.toLowerCase(), tag); + if(tag.tags) { + for(const t of tag.tags) { + langtagsByTag.set(t.toLowerCase(), tag); + } + } + } + } +} + +/** + * Find a LangTag from a given tag; matches on tag, tags, full properties, + * with a case-insensitive search. + * @param tag + * @returns LangTag or undefined if not found + */ +function getLangtagByTag(tag: string): LangTag { + return langtagsByTag.get(tag?.toLowerCase()); +} + +preinit(); + +export { langtags, getLangtagByTag, metadata }; diff --git a/common/web/langtags/tsconfig.json b/common/web/langtags/tsconfig.json new file mode 100644 index 0000000000..c09293aeb0 --- /dev/null +++ b/common/web/langtags/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../tsconfig.base.json", + + "compilerOptions": { + "outDir": "build/src/", + "rootDir": "src/", + "baseUrl": ".", + /* + following two settings allow us to import langtags.js. It cannot be a .ts + because the type is too complex for TypeScript to infer. + */ + "allowJs": true, + "noImplicitAny": false, + }, + "include": [ + "src/**/*.ts", + "src/imports/*.js", // Import langtags.json + ], +} diff --git a/common/web/types/build.sh b/common/web/types/build.sh index 597962aeee..a392ec022c 100755 --- a/common/web/types/build.sh +++ b/common/web/types/build.sh @@ -30,8 +30,8 @@ builder_parse "$@" function compile_schemas() { # We need the schema files at runtime and bundled, so always copy it for all actions except `clean` local schemas=( - "$KEYMAN_ROOT/resources/standards-data/ldml-keyboards/45/ldml-keyboard3.schema.json" - "$KEYMAN_ROOT/resources/standards-data/ldml-keyboards/45/ldml-keyboardtest3.schema.json" + "$KEYMAN_ROOT/resources/standards-data/ldml-keyboards/46/ldml-keyboard3.schema.json" + "$KEYMAN_ROOT/resources/standards-data/ldml-keyboards/46/ldml-keyboardtest3.schema.json" "$KEYMAN_ROOT/common/schemas/kvks/kvks.schema.json" "$KEYMAN_ROOT/common/schemas/kpj/kpj.schema.json" "$KEYMAN_ROOT/common/schemas/kpj-9.0/kpj-9.0.schema.json" diff --git a/common/web/types/tests/kmx/kmx-plus/kmx-plus.tests.ts b/common/web/types/tests/kmx/kmx-plus/kmx-plus.tests.ts new file mode 100644 index 0000000000..e1ce5b456c --- /dev/null +++ b/common/web/types/tests/kmx/kmx-plus/kmx-plus.tests.ts @@ -0,0 +1,55 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by Dr Mark C. Sinclair on 2025-02-03 + * + * Test code for kmx-plus.ts + */ + +import 'mocha'; +import { assert } from 'chai'; +import { Strs, StrsItem, StrsOptions, DependencySections } from '../../../src/kmx/kmx-plus/kmx-plus.js'; + +const GOTHIC_A = new StrsItem("𐌰", 0x10330); + +describe('Test of KMX Plus file', () => { + describe('Test of Strs', () => { + describe('Test of implicit constructor', () => { + it('can construct a Strs', () => { + const strs = new Strs(); + assert.isNotNull(strs); + assert.deepEqual(strs.strings, [new StrsItem('')]); + assert.deepEqual(strs.allProcessedStrings, new Set()); + }); + }); + describe('Test of allocString()', () => { + it('can allocate a one-character string', () => { + const strs = new Strs(); + strs['processString'] = stubStrsProcessString; + const csi = strs.allocString("𐌰", { singleOk: true }); + assert.deepEqual(csi, GOTHIC_A); + }); + it('can allocate an unknown string', () => { + const strs = new Strs(); + strs['processString'] = stubStrsProcessString; + const si = strs.allocString("𐌰𐌱𐌲"); + assert.deepEqual(si, new StrsItem("𐌰𐌱𐌲")); + assert.deepEqual(strs.strings, [new StrsItem(''), new StrsItem("𐌰𐌱𐌲")]); + }); + it('can allocate a known string', () => { + const strs = new Strs(); + strs['processString'] = stubStrsProcessString; + const one = strs.allocString("𐌰𐌱𐌲"); + assert.deepEqual(one, new StrsItem("𐌰𐌱𐌲")); + assert.deepEqual(strs.strings, [new StrsItem(''), new StrsItem("𐌰𐌱𐌲")]); + const two = strs.allocString("𐌰𐌱𐌲"); + assert.isTrue(two === one); + assert.deepEqual(strs.strings, [new StrsItem(''), new StrsItem("𐌰𐌱𐌲")]); + }); + }); + }); +}); + +function stubStrsProcessString(s: string, opts: StrsOptions, sections: DependencySections) { + return s; +} diff --git a/common/windows/delphi/general/RegistryKeys.pas b/common/windows/delphi/general/RegistryKeys.pas index bacca938c6..b40c309ddd 100644 --- a/common/windows/delphi/general/RegistryKeys.pas +++ b/common/windows/delphi/general/RegistryKeys.pas @@ -314,7 +314,6 @@ const SRegValue_ActiveProject_Filename = 'project filename'; SRegValue_ActiveProject_SourcePath = 'source path'; - SRegValue_AutomaticUpdates = 'automatic updates'; //CU SRegValue_CheckForUpdates = 'check for updates'; // CU SRegValue_LastUpdateCheckTime = 'last update check time'; // CU SRegValue_ApplyNow = 'apply now'; // CU Start the install now even though it will require an restart diff --git a/common/windows/delphi/general/Upload_Settings.pas b/common/windows/delphi/general/Upload_Settings.pas index d5347f4fb9..e1e8ce4294 100644 --- a/common/windows/delphi/general/Upload_Settings.pas +++ b/common/windows/delphi/general/Upload_Settings.pas @@ -58,6 +58,8 @@ const URLPath_KeymanDeveloper_KeymanForAndroidDownload = '/go/developer/'+SKeymanVersion+'/android-app'; URLPath_KeymanDeveloper_KeymanForIosDownload = '/go/developer/'+SKeymanVersion+'/ios-app'; + URLPath_KeymanDeveloper_KeyboardSearchForCloneKeymanCloud = '/go/developer/'+SKeymanVersion+'/clone-keyboard'; + URLPath_Support = '/go/'+SKeymanVersion+'/support'; URLPath_Privacy = '/go/'+SKeymanVersion+'/privacy'; URLPath_Privacy_Presentation = '/privacy'; @@ -71,6 +73,10 @@ const UrlPath_RegEx_MatchKeyboardsGo = '^http(?:s)?://keyman(?:-staging)?\.com(?:\.local)?/go/windows/[^/]+/download-keyboards'; // e.g. https://keyman-staging.com/go/windows/14.0/download-keyboards?version=14.0.146.0 + // Cloning keyboards - Keyman Developer + URLSubPath_KeymanDeveloper_Clone_Keyboards = '/keyboards/'; + URLSubPath_KeymanDeveloper_Clone_Keyboards_Custom = '/keyboards/h/'; + function URLPath_PackageDownload(const PackageID, BCP47: string; IsUpdate: Boolean): string; function API_Protocol: string; // = 'https'; @@ -86,6 +92,8 @@ function MakeAPIURL(path: string): string; function MakeKeymanURL(const path: string): string; +function URL_KmcMessage(const id: string): string; + implementation uses @@ -102,6 +110,7 @@ const S_UserAgent_Developer = 'Keyman Developer'; S_UserAgent_Diagnostics = 'Keyman for Windows Diagnostics'; + S_Host_KmnSh = 'https://kmn.sh'; S_KeymanCom = 'https://keyman.com'; S_APIProtocol = 'https'; S_APIServer = 'api.keyman.com'; @@ -114,6 +123,7 @@ const const URLPath_PackageDownload_Format = '/go/package/download/%0:s?platform=windows&tier=%1:s&bcp47=%2:s&update=%3:d'; + URL_KeymanDeveloper_HelpKmcMessage_Format = S_Host_KmnSh+'/%0:s'; function API_UserAgent: string; begin @@ -130,7 +140,6 @@ begin Result := S_UserAgent_Diagnostics + '/' + GetVersionString; end; - function MakeKeymanURL(const path: string): string; begin Result := KeymanCom_Protocol_Server + path; @@ -173,4 +182,9 @@ begin Result := Format(URLPath_PackageDownload_Format, [URLEncode(PackageID), URLEncode(CKeymanVersionInfo.Tier), URLEncode(BCP47), IsUpdateInt]); end; +function URL_KmcMessage(const id: string): string; +begin + Result := Format(URL_KeymanDeveloper_HelpKmcMessage_Format, [id.ToLower]); +end; + end. diff --git a/common/windows/delphi/general/VersionInfo.pas b/common/windows/delphi/general/VersionInfo.pas index 7567279196..3e399f2f0c 100644 --- a/common/windows/delphi/general/VersionInfo.pas +++ b/common/windows/delphi/general/VersionInfo.pas @@ -27,6 +27,7 @@ uses Winapi.Windows; function GetVersionString: WideString; +function GetMajorMinorVersionString: string; procedure GetVersionBuild(var vMajor, vMinor: Integer); function GetVersionCopyright: WideString; function GetFileVersionString(const FileName: WideString): WideString; @@ -54,6 +55,12 @@ begin IntToStr(LoWord(VersionMinor)); end; +function GetMajorMinorVersionString: string; +begin + Result := IntToStr(HiWord(VersionMajor)) + '.' + // I3310 + IntToStr(LoWord(VersionMajor)); +end; + procedure GetVersionBuild(var vMajor, vMinor: Integer); begin vMajor := VersionMajor; diff --git a/common/windows/delphi/general/utilfiletypes.pas b/common/windows/delphi/general/utilfiletypes.pas index 104c10f411..f34be0933d 100644 --- a/common/windows/delphi/general/utilfiletypes.pas +++ b/common/windows/delphi/general/utilfiletypes.pas @@ -1,18 +1,18 @@ (* Name: utilfiletypes Copyright: Copyright (C) SIL International. - Documentation: - Description: + Documentation: + Description: Create Date: 20 Jun 2006 Modified Date: 4 Dec 2006 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 - Refactor util functions into multiple units 04 Dec 2006 - mcdurdin - Add XML and HTML filetypes @@ -100,7 +100,15 @@ type end; type - TKeymanProjectType = (kptUnknown, kptBasic, kptLDMLKeyboard, kptImportWindowsKeyboard, kptWordlistLexicalModel); + TKeymanProjectType = ( + kptUnknown, + kptBasic, + kptLDMLKeyboard, + kptImportWindowsKeyboard, + kptWordlistLexicalModel, + kptCloneLocalProject, + kptCloneKeymanCloudProject, + kptCloneGitHubProject); implementation diff --git a/core/include/ldml/keyman_core_ldml.h b/core/include/ldml/keyman_core_ldml.h index 1d8aaa2575..189b37fc96 100644 --- a/core/include/ldml/keyman_core_ldml.h +++ b/core/include/ldml/keyman_core_ldml.h @@ -16,11 +16,11 @@ #pragma once #define LDML_BKSP_FLAGS_ERROR 0x1 -#define LDML_CLDR_IMPLIED_FORMS_IMPORT "45/scanCodes-implied.xml" -#define LDML_CLDR_IMPLIED_KEYS_IMPORT "45/keys-Latn-implied.xml" +#define LDML_CLDR_IMPLIED_FORMS_IMPORT "46/scanCodes-implied.xml" +#define LDML_CLDR_IMPLIED_KEYS_IMPORT "46/keys-Latn-implied.xml" #define LDML_CLDR_IMPORT_BASE "cldr" #define LDML_CLDR_TEST_VERSION_LATEST "techpreview" -#define LDML_CLDR_VERSION_LATEST "45" +#define LDML_CLDR_VERSION_LATEST "46" #define LDML_ELEM_FLAGS_ORDER_BITSHIFT 0x10 #define LDML_ELEM_FLAGS_ORDER_MASK 0xFF0000 #define LDML_ELEM_FLAGS_PREBASE 0x8 diff --git a/core/include/ldml/keyman_core_ldml.ts b/core/include/ldml/keyman_core_ldml.ts index b649d9424e..093ad839c0 100644 --- a/core/include/ldml/keyman_core_ldml.ts +++ b/core/include/ldml/keyman_core_ldml.ts @@ -59,7 +59,7 @@ class Constants { /** * The current CLDR version */ - readonly cldr_version_latest = '45'; + readonly cldr_version_latest = '46'; /** * The version for testdata files */ @@ -564,8 +564,17 @@ class Constants { /** maximum count of markers (not including 'any') */ readonly marker_max_count = this.marker_max_index - this.marker_min_index + 1; + /** + * List of versions to treat as if they were the 'latest' version. + */ + treatAsLatest(version: string): boolean { + return cldrTreatAsLatest.has(version); + } }; +/** There's no data or DTD change in 45, 46, 46.1, 47 so map them all to 46 at present. */ +const cldrTreatAsLatest: Set = new Set(['45', '46.1', '47']); + export const constants = new Constants(); // } diff --git a/core/src/ldml/C7532_ldml_updating.md b/core/src/ldml/C7532_ldml_updating.md index da6bbcb80c..10d7708ada 100644 --- a/core/src/ldml/C7532_ldml_updating.md +++ b/core/src/ldml/C7532_ldml_updating.md @@ -22,7 +22,7 @@ working on ‘layr’, using ‘disp’ as a model from https://github.com/keyma ## XML changes -- `resources/standards-data/ldml-keyboards/45/` : update / reimport / fix fixup script if needed +- `resources/standards-data/ldml-keyboards/46/` : update / reimport / fix fixup script if needed - E/C: `common/web/types/src/ldml-keyboard/ldml-keyboard-xml.ts` - add to `LKKeyboard` and subproperties as needed to support the structure on the XML side - Now would be a good time to stop and make sure everything compiles. It didn’t, there was an unrelated issue with snprintf! diff --git a/core/tests/unit/ldml/keyboards/k_001_tiny-test.xml b/core/tests/unit/ldml/keyboards/k_001_tiny-test.xml index 827dd54dfa..5494e8f41e 100644 --- a/core/tests/unit/ldml/keyboards/k_001_tiny-test.xml +++ b/core/tests/unit/ldml/keyboards/k_001_tiny-test.xml @@ -1,5 +1,5 @@ - + diff --git a/core/tests/unit/ldml/keyboards/k_006_backspace-test.xml b/core/tests/unit/ldml/keyboards/k_006_backspace-test.xml index c0157f8828..56aa8d8d01 100644 --- a/core/tests/unit/ldml/keyboards/k_006_backspace-test.xml +++ b/core/tests/unit/ldml/keyboards/k_006_backspace-test.xml @@ -1,5 +1,5 @@ - + diff --git a/core/tests/unit/ldml/keyboards/k_007_transform_rgx-test.xml b/core/tests/unit/ldml/keyboards/k_007_transform_rgx-test.xml index f4024275e8..efef244c8b 100644 --- a/core/tests/unit/ldml/keyboards/k_007_transform_rgx-test.xml +++ b/core/tests/unit/ldml/keyboards/k_007_transform_rgx-test.xml @@ -1,5 +1,5 @@ - + diff --git a/core/tests/unit/ldml/keyboards/k_008_transform_norm-test.xml b/core/tests/unit/ldml/keyboards/k_008_transform_norm-test.xml index 52323ea071..e146f5afae 100644 --- a/core/tests/unit/ldml/keyboards/k_008_transform_norm-test.xml +++ b/core/tests/unit/ldml/keyboards/k_008_transform_norm-test.xml @@ -1,5 +1,5 @@ - + diff --git a/core/tests/unit/ldml/keyboards/k_009_transform_nfc-test.xml b/core/tests/unit/ldml/keyboards/k_009_transform_nfc-test.xml index 8d63e86e02..0006f14e27 100644 --- a/core/tests/unit/ldml/keyboards/k_009_transform_nfc-test.xml +++ b/core/tests/unit/ldml/keyboards/k_009_transform_nfc-test.xml @@ -1,5 +1,5 @@ - + diff --git a/core/tests/unit/ldml/keyboards/k_020_fr-test.xml b/core/tests/unit/ldml/keyboards/k_020_fr-test.xml index 22fce78538..662f3ca665 100644 --- a/core/tests/unit/ldml/keyboards/k_020_fr-test.xml +++ b/core/tests/unit/ldml/keyboards/k_020_fr-test.xml @@ -1,6 +1,6 @@ - + diff --git a/core/tests/unit/ldml/keyboards/k_200_reorder_nod_Lana-test.xml b/core/tests/unit/ldml/keyboards/k_200_reorder_nod_Lana-test.xml index d23e596bbb..b1eea391af 100644 --- a/core/tests/unit/ldml/keyboards/k_200_reorder_nod_Lana-test.xml +++ b/core/tests/unit/ldml/keyboards/k_200_reorder_nod_Lana-test.xml @@ -1,5 +1,5 @@ - + diff --git a/core/tests/unit/ldml/keyboards/k_201_reorder_esk-test.xml b/core/tests/unit/ldml/keyboards/k_201_reorder_esk-test.xml index fbb3427878..3268aa733e 100644 --- a/core/tests/unit/ldml/keyboards/k_201_reorder_esk-test.xml +++ b/core/tests/unit/ldml/keyboards/k_201_reorder_esk-test.xml @@ -1,5 +1,5 @@ - + diff --git a/core/tests/unit/ldml/keyboards/k_210_marker-test.xml b/core/tests/unit/ldml/keyboards/k_210_marker-test.xml index 22865b12c5..57769f1c18 100644 --- a/core/tests/unit/ldml/keyboards/k_210_marker-test.xml +++ b/core/tests/unit/ldml/keyboards/k_210_marker-test.xml @@ -1,5 +1,5 @@ - + diff --git a/core/tests/unit/ldml/keyboards/k_211_marker_escape-test.xml b/core/tests/unit/ldml/keyboards/k_211_marker_escape-test.xml index 5fb09e7fc5..f1786fb974 100644 --- a/core/tests/unit/ldml/keyboards/k_211_marker_escape-test.xml +++ b/core/tests/unit/ldml/keyboards/k_211_marker_escape-test.xml @@ -1,5 +1,5 @@ - + diff --git a/core/tests/unit/ldml/keyboards/k_212_marker_11057-test.xml b/core/tests/unit/ldml/keyboards/k_212_marker_11057-test.xml index 92e2abbf3f..69749bc516 100644 --- a/core/tests/unit/ldml/keyboards/k_212_marker_11057-test.xml +++ b/core/tests/unit/ldml/keyboards/k_212_marker_11057-test.xml @@ -1,5 +1,5 @@ - + diff --git a/core/tests/unit/ldml/keyboards/ldml_test-test.xml b/core/tests/unit/ldml/keyboards/ldml_test-test.xml index 2c08741232..0e569ff8c5 100644 --- a/core/tests/unit/ldml/keyboards/ldml_test-test.xml +++ b/core/tests/unit/ldml/keyboards/ldml_test-test.xml @@ -1,5 +1,5 @@ - + diff --git a/core/tests/unit/ldml/keyboards/meson.build b/core/tests/unit/ldml/keyboards/meson.build index 394b8a6811..04a288710c 100644 --- a/core/tests/unit/ldml/keyboards/meson.build +++ b/core/tests/unit/ldml/keyboards/meson.build @@ -5,8 +5,8 @@ # -# keyboards in resources/standards-data/ldml-keyboards/45/3.0/ -# tests in resources/standards-data/ldml-keyboards/45/test/ +# keyboards in resources/standards-data/ldml-keyboards/46/3.0/ +# tests in resources/standards-data/ldml-keyboards/46/test/ tests_from_cldr = [ 'ja-Latn', 'pt-t-k0-abnt2', @@ -57,7 +57,7 @@ tests += tests_from_cldr # Setup kmc kmc_root = meson.global_source_root() / '../developer/src/kmc' -ldml_root = meson.global_source_root() / '../resources/standards-data/ldml-keyboards/45' +ldml_root = meson.global_source_root() / '../resources/standards-data/ldml-keyboards/46' ldml_data = join_paths(ldml_root, '3.0') ldml_testdata = join_paths(ldml_root, 'test') kmc_cmd = [node, '--enable-source-maps', kmc_root] diff --git a/developer/docs/help/context/clone-github-project-parameters.md b/developer/docs/help/context/clone-github-project-parameters.md new file mode 100644 index 0000000000..d2ec5c7aba --- /dev/null +++ b/developer/docs/help/context/clone-github-project-parameters.md @@ -0,0 +1,34 @@ +--- +title: Clone GitHub Project Parameters Dialog +--- + +![Clone GitHub Project Parameters dialog](../images/ui/frmCloneGitHubProjectParameters.png) + +Allows you to clone a project from a GitHub repository, giving the project a +new id, and organizing project files following the +[file layout](/developer/keyboards/) used in the +[Keyman keyboards repository](https://github.com/keymanapp/keyboards). + +Keyboard and lexical model projects can be cloned. + +Projects can also be cloned from the command line with [`kmc copy`](kmc). + +### Parameters + +GitHub URL: +: The full URL to a source project .kpj file to copy, on github.com. + +Destination path +: Specifies the base path where the project folder will be created. + The project folder name will be the keyboard ID. If the folder + already exists, then you will be prompted before Keyman Developer + overwrites files inside it. + +New project ID +: The base filename of the keyboard/model, project and package. This must + conform to the Keyman keyboard or lexical model identifier rules. + +Relocate external files into new project folder +: When selected, if the project references files that are not inside the same + folder as the .kpj file, move these files into an `external` subfolder + diff --git a/developer/docs/help/context/clone-keyman-cloud-project-parameters.md b/developer/docs/help/context/clone-keyman-cloud-project-parameters.md new file mode 100644 index 0000000000..71d1cf8a3a --- /dev/null +++ b/developer/docs/help/context/clone-keyman-cloud-project-parameters.md @@ -0,0 +1,37 @@ +--- +title: Clone Keyman Cloud Project Parameters Dialog +--- + +![Clone Keyman Cloud Project Parameters dialog](../images/ui/frmCloneKeymanCloudProjectParameters.png) + +Allows you to clone a project from Keyman Cloud, giving the project a +new id, and organizing project files following the +[file layout](/developer/keyboards/) used in the +[Keyman keyboards repository](https://github.com/keymanapp/keyboards). + +Projects can also be cloned from the command line with [`kmc copy`](kmc). + +Only keyboard projects can be cloned from Keyman Cloud at this time via this +dialog. Lexical model projects can be cloned from Keyman Cloud via the command +line. + +Search for a keyboard using the standard keyman.com keyboard search in the upper +pane. Once you select a keyboard, and fill in the New Project Details, the OK +button will be enabled to allow you to finish the cloning process. + +### Parameters + +Destination path +: Specifies the base path where the project folder will be created. + The project folder name will be the keyboard ID. If the folder + already exists, then you will be prompted before Keyman Developer + overwrites files inside it. + +New project ID +: The base filename of the keyboard, project and package. This must + conform to the Keyman keyboard identifier rules. + +Relocate external files into new project folder +: When selected, if the project references files that are not inside the same + folder as the .kpj file, move these files into an `external` subfolder + diff --git a/developer/docs/help/context/clone-local-project-parameters.md b/developer/docs/help/context/clone-local-project-parameters.md new file mode 100644 index 0000000000..3b1b5bc2c9 --- /dev/null +++ b/developer/docs/help/context/clone-local-project-parameters.md @@ -0,0 +1,34 @@ +--- +title: Clone Local Project Parameters Dialog +--- + +![Clone Local Project Parameters dialog](../images/ui/frmCloneLocalProjectParameters.png) + +Allows you to clone a project from your local filesystem, giving the project a +new id, and organizing project files following the +[file layout](/developer/keyboards/) used in the +[Keyman keyboards repository](https://github.com/keymanapp/keyboards). + +Keyboard and lexical model projects can be cloned. + +Projects can also be cloned from the command line with [`kmc copy`](kmc). + +### Parameters + +Source project filename +: The full path of the source project .kpj file to copy. + +Destination path +: Specifies the base path where the project folder will be created. + The project folder name will be the keyboard ID. If the folder + already exists, then you will be prompted before Keyman Developer + overwrites files inside it. + +New project ID +: The base filename of the keyboard/model, project and package. This must + conform to the Keyman keyboard or lexical model identifier rules. + +Relocate external files into new project folder +: When selected, if the project references files that are not inside the same + folder as the .kpj file, move these files into an `external` subfolder + diff --git a/developer/docs/help/context/new-project.md b/developer/docs/help/context/new-project.md index 1fd83fcd6f..8af883788e 100644 --- a/developer/docs/help/context/new-project.md +++ b/developer/docs/help/context/new-project.md @@ -21,4 +21,28 @@ Creates a keyboard project that matches the folder structure used in the Keyman Creates a lexical model project that matches the folder structure used in the Keyman [lexical models repository](https://github.com/keymanapp/lexical-models) and includes all basic files needed for a lexical model project. Clicking OK will open the [New Lexical Model Project Parameters dialog](new-lm-project-parameters). -The **Basic** and **Import Windows Keyboard** modes can also be driven by command line with [KMConvert](kmconvert). \ No newline at end of file +## Clone local project + +Copies a Keyman keyboard or lexical model project on your local filesystem into +a new folder, giving the project and included files a new name. Clicking OK will +open the [Clone Local Project Parameters dialog](clone-local-project-parameters). + +## Clone project from Keyman Cloud + +Copies a Keyman keyboard or lexical model project from keyman.com into a new +folder, giving the project and included files a new name. Clicking OK will +open the [Clone Keyman Cloud Project Parameters dialog](clone-keyman-cloud-project-parameters). + +## Clone project from GitHub + +Copies a Keyman keyboard or lexical model project from a GitHub repository into +a new folder, giving the project and included files a new name. Clicking OK will +open the [Clone GitHub Project Parameters dialog](clone-github-project-parameters). + +# Command line access + +[`kmc generate`](kmc) can be used to generate new Keyman keyboards, LDML +keyboards, and Wordlist lexical models from the command line. + +[KMConvert](kmconvert) can also be used to import Windows keyboards from the +command line. diff --git a/developer/docs/help/images/ui/frmCloneGitHubProjectParameters.png b/developer/docs/help/images/ui/frmCloneGitHubProjectParameters.png new file mode 100644 index 0000000000..ce7d1c99c3 Binary files /dev/null and b/developer/docs/help/images/ui/frmCloneGitHubProjectParameters.png differ diff --git a/developer/docs/help/images/ui/frmCloneKeymanCloudProjectParameters.png b/developer/docs/help/images/ui/frmCloneKeymanCloudProjectParameters.png new file mode 100644 index 0000000000..a71fd7ed98 Binary files /dev/null and b/developer/docs/help/images/ui/frmCloneKeymanCloudProjectParameters.png differ diff --git a/developer/docs/help/images/ui/frmCloneLocalProjectParameters.png b/developer/docs/help/images/ui/frmCloneLocalProjectParameters.png new file mode 100644 index 0000000000..705ada692e Binary files /dev/null and b/developer/docs/help/images/ui/frmCloneLocalProjectParameters.png differ diff --git a/developer/docs/help/images/ui/frmNewProject.png b/developer/docs/help/images/ui/frmNewProject.png index 8fc13cd763..1c41529b1d 100644 Binary files a/developer/docs/help/images/ui/frmNewProject.png and b/developer/docs/help/images/ui/frmNewProject.png differ diff --git a/developer/docs/help/index.md b/developer/docs/help/index.md index 9e9c250abb..d7fbc4098d 100644 --- a/developer/docs/help/index.md +++ b/developer/docs/help/index.md @@ -1,5 +1,5 @@ --- -title: Keyman Developer 18.0 User Guide +title: Keyman Developer 19.0 User Guide --- Need help using Keyman Developer to create your keyboard layouts? You'll @@ -8,7 +8,7 @@ and tutorials, and full reference information. ## Guides and Tutorials - [What is Keyman Developer?](guides/intro) -- [What's new in 18.0](whats-new) +- [What's new in 19.0](whats-new) - [Developing Keyman keyboard layouts](guides/develop) - [Testing Keyman keyboards](guides/test) - [Distributing Keyman keyboards](guides/distribute) diff --git a/developer/docs/help/reference/kmc/cli/get-started.md b/developer/docs/help/reference/kmc/cli/get-started.md index 9147eebda2..cb3e9073e2 100644 --- a/developer/docs/help/reference/kmc/cli/get-started.md +++ b/developer/docs/help/reference/kmc/cli/get-started.md @@ -26,7 +26,7 @@ configuration or installation is required. kmc is also available as an npm package, [@keymanapp/kmc](https://npmjs.com/package/@keymanapp/kmc). -You'll need [node.js](https://nodejs.org/), version 18.0 or later. +You'll need [node.js](https://nodejs.org/), version 20.0 or later. ```shell npm install -g @keymanapp/kmc diff --git a/developer/docs/help/reference/kmc/cli/reference.md b/developer/docs/help/reference/kmc/cli/reference.md index fdaed58faa..1d5ed9fc20 100644 --- a/developer/docs/help/reference/kmc/cli/reference.md +++ b/developer/docs/help/reference/kmc/cli/reference.md @@ -90,14 +90,33 @@ The following parameters are available: : Prints the version number of kmc -`--no-error-reporting`, `--error-reporting` +`--color`, `--no-color` -: Enable or disable error reporting to keyman.com, overriding [user - settings](../../user-settings). Error reporting is for fatal errors in the - compiler, and not errors in compiled files. No user data is sent in error +: Controls colorization for log messages, using ANSI color controls. If both of + these settings are omitted, kmc will attempt to detect from console, and will + use colorization for interactive terminals, and no colorization when + redirection is being used. + +`--error-reporting`, `--no-error-reporting` + +: Enable or disable error reporting to keyman.com, overriding + [user settings](../../user-settings). Error reporting is for fatal errors in + the compiler, and not errors in compiled files. No user data is sent in error reports, although some filenames and paths may be present in the diagnostic data attached to the report. +`--log-format ` + +: Output log format. The available options are: + * `formatted` (default): emits log messages in a human-readable format + * `tsv`: emits log messages in UTF-8 tab-separated format. This format will be + stable across versions of kmc. The format has the following fields: + * filename + * line number + * severity + * code + * message + `-l `, `--log-level ` : Controls the level of logging to console for messages relating to the @@ -108,9 +127,12 @@ The following parameters are available: * `hint`: emits compilation errors, warnings, and hints * `info` (default): emits compilation errors, warnings, hints, and informational messages + * `verbose`: emits all compilation messages, including verbose + informational messages * `debug`: emits all compilation messages, plus internal debug messages - Logging of specific [messages](messages) can be controlled with `--message`, `-m`. + Logging of specific [messages](messages) can be controlled with `--message`, + `-m` (`build` command only). Note that when warnings are treated as errors (`--compiler-warnings-as-errors`, `-w`), they will still be logged as @@ -118,13 +140,6 @@ The following parameters are available: ## `kmc build` options -`--color`, `--no-color` - -: Controls colorization for log messages, using ANSI color controls. If both of - these settings are omitted, kmc will attempt to detect from console, and will - use colorization for interactive terminals, and no colorization when - redirection is being used. - `-d`, `--debug` : Include debug information in output files. Debug information is used for @@ -167,17 +182,6 @@ The following parameters are available: : Turns off warnings (CWARN_HeaderStatementIsDeprecated, CWARN_LanguageHeadersDeprecatedInKeyman10) for deprecated code styles -`--log-format ` - -: Output log format. The available options are: - * `formatted` (default): emits log messages in a human-readable format - * `tsv`: emits log messages in UTF-8 tab-separated format. This format will be - stable across versions of kmc. The format has the following fields: - * filename - * line number - * severity - * code - * message `-o `, `--out-file ` @@ -509,6 +513,12 @@ following sources: : Show what would happen, without making changes +`-r, --relocate-external` + +: Copy referenced files that are outside the project folder into an 'external' + folder within the new project, rather than just referencing them in their + current location. + ### File copying, renaming, and structure rules The **origin** project folder is the one that contains the .kpj file. When a diff --git a/developer/docs/help/whats-new.md b/developer/docs/help/whats-new.md index d1dce092c3..1005476c3d 100644 --- a/developer/docs/help/whats-new.md +++ b/developer/docs/help/whats-new.md @@ -1,43 +1,5 @@ --- -title: What's new in Keyman Developer 18.0 +title: What's new in Keyman Developer 19.0 --- -Keyman Developer 17 has a number of significant changes: - -* `kmcomp` has been removed and replaced with `kmc`. Learn more in our [kmcomp migration guide](reference/kmc/cli/kmcomp-migration). - -* Windows installer packages should now be built with - `kmc build windows-package-installer` and can no longer be built within - the Keyman Developer IDE. More details on how to use this are in the - [`kmc` reference documentation](reference/kmc/cli/reference#toc-kmc-build-windows-package-installer-additional-options). - -* `.keyboard_info` files are now generated entirely from the package and - keyboard files. Extra fields are available in packages for license file, - welcome file, typing examples, related packages (including deprecated - packages), and additional font files. Keyboards in the Keyman Cloud - keyboard repository have already been updated; if you are an author of - one of these keyboards, you should pull the changes from the repository - before submitting future updates. - -* Keyboard project files (.kpj) now have a new format which does not list - individual files, but instead includes all files in the same folder and - subfolders relative to the project file. This prevents projects from - including unrelated files, and reduces the number of files that change - when keyboards are updated. - -* Keyman Developer will now open each project in a new window, allowing - the user to rapidly switch between multiple projects at the same time. - -* The Keyman Keyboard and Lexical Model Cloud repositories have new build - scripts which run `kmc`. These run much, much faster than previously! - -* The [`&displayMap`](/developer/language/reference/displaymap) store allows - keyboard developers to specify a font mapping to PUA for the On Screen - Keyboard and Touch Layout to resolve diacritic rendering issues. - -* Keyman Developer has been updated to support Unicode 15.1. - -* Additional non-printing characters have been added to the Touch Layout Editor. - -* Virtual keys in output of rules have never worked properly or been officially - supported; Keyman Developer will now warn if you attempt to use this pattern. +Keyman Developer 19 has the following significant changes: diff --git a/developer/src/build.sh b/developer/src/build.sh index dd4f5264e5..d01c6caeb8 100755 --- a/developer/src/build.sh +++ b/developer/src/build.sh @@ -156,6 +156,7 @@ function do_publish() { ./common/web/utils/build.sh publish $DRY_RUN $NPM_PUBLISH ../../common/web/keyman-version/build.sh publish $DRY_RUN $NPM_PUBLISH + ../../common/web/langtags/build.sh publish $DRY_RUN $NPM_PUBLISH ../../common/web/types/build.sh publish $DRY_RUN $NPM_PUBLISH ../../core/include/ldml/build.sh publish $DRY_RUN $NPM_PUBLISH # end TODO diff --git a/developer/src/common/delphi/lexicalmodels/Keyman.System.LexicalModelUtils.pas b/developer/src/common/delphi/lexicalmodels/Keyman.System.LexicalModelUtils.pas index c367331eee..44120f9cf9 100644 --- a/developer/src/common/delphi/lexicalmodels/Keyman.System.LexicalModelUtils.pas +++ b/developer/src/common/delphi/lexicalmodels/Keyman.System.LexicalModelUtils.pas @@ -4,7 +4,6 @@ interface type TLexicalModelUtils = class - private public const SPackageNameDoesNotFollowLexicalModelConventions_Message: string = 'The package file %0:s does not follow the recommended model filename conventions. The name should be all lower case, '+ @@ -31,6 +30,8 @@ type class function CleanLexicalModelIDComponent(const Name: string): string; static; class function IsCleanLexicalModelIDComponent(const Name: string): Boolean; static; + class function IsValidLexicalModelID(ID: string; EnforceCaseAndWhitespace: Boolean): Boolean; static; + class function ExtractFileExt(filename: string): string; static; class function RemoveFileExt(filename: string): string; static; class function ChangeFileExt(filename, ext: string): string; static; @@ -51,11 +52,20 @@ const // The model ID SHOULD adhere to this pattern (see also developer/src/kmc-model): // author .bcp47 .uniq + MODEL_ID_PATTERN = '^[a-z_][a-z0-9_]*\.[a-z_][a-z0-9_-]*\.[a-z_][a-z0-9_]*$'; MODEL_ID_PATTERN_JS = '^[a-z_][a-z0-9_]*\.[a-z_][a-z0-9_-]*\.[a-z_][a-z0-9_]*\.model\.js$'; MODEL_ID_PATTERN_TS = '^[a-z_][a-z0-9_]*\.[a-z_][a-z0-9_-]*\.[a-z_][a-z0-9_]*\.model\.ts$'; MODEL_ID_PATTERN_PACKAGE = '^[a-z_][a-z0-9_]*\.[a-z_][a-z0-9_-]*\.[a-z_][a-z0-9_]*\.model\.(kps|kmp)$'; MODEL_ID_PATTERN_PROJECT = '^[a-z_][a-z0-9_]*\.[a-z_][a-z0-9_-]*\.[a-z_][a-z0-9_]*\.model\.kpj$'; +class function TLexicalModelUtils.IsValidLexicalModelID(ID: string; + EnforceCaseAndWhitespace: Boolean): Boolean; +begin + if not EnforceCaseAndWhitespace then + ID := LowerCase(Trim(ID)); + Result := TRegEx.IsMatch(ID, MODEL_ID_PATTERN); +end; + class function TLexicalModelUtils.DoesJSFilenameFollowLexicalModelConventions( const Name: string): Boolean; begin diff --git a/developer/src/common/web/test-helpers/verifyCompilerMessagesObject.ts b/developer/src/common/web/test-helpers/verifyCompilerMessagesObject.ts index 8a16de6bf1..6c004d101c 100644 --- a/developer/src/common/web/test-helpers/verifyCompilerMessagesObject.ts +++ b/developer/src/common/web/test-helpers/verifyCompilerMessagesObject.ts @@ -1,4 +1,4 @@ -import { CompilerError, CompilerErrorMask } from '@keymanapp/developer-utils'; +import { CompilerError, CompilerErrorMask, CompilerEvent, dedentCompilerMessageDetail } from '@keymanapp/developer-utils'; import {assert, expect} from 'chai'; // @@ -37,8 +37,24 @@ export function verifyCompilerMessagesObject(source: Record, namespa const c = o[1].toUpperCase() + '_' + o[2]; expect(m[c]).to.be.a('number', `Expected constant name ${c} to exist`); - const v = m[key]('','','','','','','','','','','','' /* ignore arguments*/); + const v: CompilerEvent = m[key]('','','','','','','','','','','','' /* ignore arguments*/); expect(v.code).to.equal(m[c], `Function ${key} returns the wrong code`); + + assert.isNotNull(v.message, `Function ${key} returns an invalid message: '${v.message}'`); + assert.isNotEmpty(v.message, `Function ${key} returns an empty message: '${v.message}'`); + + // Verify layout of compiler detail lines + + if(v.detail) { + const detail = dedentCompilerMessageDetail(v).split('\n'); + for(const line of detail) { + // Prevent lines over 80 characters -- we don't rewrap at this stage + // Don't check lines with URLs because they may be forced to go long + if(!line.includes('http')) { + assert.isAtMost(line.length, 80, `Function ${key} returns a message detail with at least one line longer than 80 characters: '${line}'`); + } + } + } } else if(typeof m[key] == 'number') { const o = /^(DEBUG|VERBOSE|INFO|HINT|WARN|ERROR|FATAL)_([A-Za-z0-9_]+)$/.exec(key); diff --git a/developer/src/common/web/utils/src/compiler-interfaces.ts b/developer/src/common/web/utils/src/compiler-interfaces.ts index 4cb9ae3c22..869e0165da 100644 --- a/developer/src/common/web/utils/src/compiler-interfaces.ts +++ b/developer/src/common/web/utils/src/compiler-interfaces.ts @@ -291,10 +291,8 @@ export interface CompilerMessageOverride { level: CompilerErrorSeverityOverride; }; -export interface CompilerCallbackOptions { - logLevel?: CompilerLogLevel; - logFormat?: CompilerLogFormat; - color?: boolean; // null or undefined == use console default +export interface CompilerCallbackOptions extends CompilerBaseOptions { + // TODO: these overlap with CompilerOptions, should refactor compilerWarningsAsErrors?: boolean; messageOverrides?: CompilerMessageOverrideMap; }; @@ -401,6 +399,19 @@ export const CompilerMessageSpec = (code: number, message: string, detail?: stri detail, }); +/** + * Remove initial whitespace from compiler detail messages, to enable + * indented formatting of message detail strings inside the message + * definitions + * @param event + * @returns dedented event detail + */ +export function dedentCompilerMessageDetail(event: CompilerEvent) { + // TODO(lowpri): dedent may be too naive -- should use least + // non-zero whitespace line as amount to dedent + return (event.detail ?? '').replace(/^[ ]+/gm, ''); +} + export const CompilerMessageDef = (param: any) => String(param ?? ``); export const CompilerMessageSpecWithException = (code: number, message: string, exceptionVar: any, detail?: string) : CompilerEvent => ({ diff --git a/developer/src/common/web/utils/src/index.ts b/developer/src/common/web/utils/src/index.ts index f8ee6246da..f3e23e79f2 100644 --- a/developer/src/common/web/utils/src/index.ts +++ b/developer/src/common/web/utils/src/index.ts @@ -52,7 +52,7 @@ export { defaultCompilerOptions, CompilerBaseOptions, CompilerOptions, CompilerE ALL_COMPILER_LOG_FORMATS, CompilerLogFormat, CompilerMessageOverride, CompilerMessageOverrideMap, - + dedentCompilerMessageDetail, KeymanCompilerArtifact, KeymanCompilerArtifactOptional, KeymanCompilerArtifacts, diff --git a/developer/src/common/web/utils/src/types/ldml-keyboard/ldml-keyboard-xml-reader.ts b/developer/src/common/web/utils/src/types/ldml-keyboard/ldml-keyboard-xml-reader.ts index 928bb94bef..0a5abf6355 100644 --- a/developer/src/common/web/utils/src/types/ldml-keyboard/ldml-keyboard-xml-reader.ts +++ b/developer/src/common/web/utils/src/types/ldml-keyboard/ldml-keyboard-xml-reader.ts @@ -231,6 +231,10 @@ export class LDMLKeyboardXMLSourceFileReader { this.callbacks.reportMessage(CommonTypesMessages.Error_ImportInvalidPath({base, path, subtag})); return false; } + if (constants.treatAsLatest(paths[0])) { + /** There's no data or DTD change in 45, 46, 46.1, 47 so map them all to 46 at present. */ + paths[0] = constants.cldr_version_latest; + } importData = this.readImportFile(paths[0], paths[1]); } else { // local import diff --git a/developer/src/common/web/utils/src/utils/KeymanSentry.ts b/developer/src/common/web/utils/src/utils/KeymanSentry.ts index 94409eafc8..dfe62bfa3d 100644 --- a/developer/src/common/web/utils/src/utils/KeymanSentry.ts +++ b/developer/src/common/web/utils/src/utils/KeymanSentry.ts @@ -64,11 +64,17 @@ export class KeymanSentry { } } - static async captureException(e: any): Promise { + /** + * capture an exception for Sentry; note that in local environments, this will normally + * throw the exception rather than sending to Sentry + * @param e + * @param force if true, reports to Sentry even in local environments + */ + static async captureException(e: any, force?: boolean): Promise { if(isInit) { // For local development, we don't want to bury the trace; we need the cast to avoid // TS2367 (comparison appears to be unintentional) - if((KEYMAN_VERSION.VERSION_ENVIRONMENT as string) == 'local') { + if(!force && (KEYMAN_VERSION.VERSION_ENVIRONMENT as string) == 'local') { throw e; } diff --git a/developer/src/common/web/utils/test/fixtures/ldml-keyboard/import-local.xml b/developer/src/common/web/utils/test/fixtures/ldml-keyboard/import-local.xml index b3cc7cabdf..01bd9f9a43 100644 --- a/developer/src/common/web/utils/test/fixtures/ldml-keyboard/import-local.xml +++ b/developer/src/common/web/utils/test/fixtures/ldml-keyboard/import-local.xml @@ -2,6 +2,6 @@ - + diff --git a/developer/src/common/web/utils/test/fixtures/ldml-keyboard/invalid-import-local.xml b/developer/src/common/web/utils/test/fixtures/ldml-keyboard/invalid-import-local.xml index e8ac9dc964..bb3927a873 100644 --- a/developer/src/common/web/utils/test/fixtures/ldml-keyboard/invalid-import-local.xml +++ b/developer/src/common/web/utils/test/fixtures/ldml-keyboard/invalid-import-local.xml @@ -2,6 +2,6 @@ - + diff --git a/developer/src/common/web/utils/test/fixtures/ldml-keyboard/test-fr.xml b/developer/src/common/web/utils/test/fixtures/ldml-keyboard/test-fr.xml index b92ba41360..fc0224fcb8 100644 --- a/developer/src/common/web/utils/test/fixtures/ldml-keyboard/test-fr.xml +++ b/developer/src/common/web/utils/test/fixtures/ldml-keyboard/test-fr.xml @@ -1,5 +1,5 @@ - + - + diff --git a/developer/src/common/web/utils/test/kmx/ldml-keyboard-xml-reader.tests.ts b/developer/src/common/web/utils/test/kmx/ldml-keyboard-xml-reader.tests.ts index e3e6fc1e9f..fcb3628144 100644 --- a/developer/src/common/web/utils/test/kmx/ldml-keyboard-xml-reader.tests.ts +++ b/developer/src/common/web/utils/test/kmx/ldml-keyboard-xml-reader.tests.ts @@ -32,7 +32,7 @@ describe('ldml keyboard xml reader tests', function () { instancePath: '/keyboard3/conformsTo', keyword: 'enum', message: `must be equal to one of the allowed values`, - params: 'allowedValues="45"', + params: 'allowedValues="45,46"', // this has to be kept in sync with the DTD })], }, { diff --git a/developer/src/inst/kmdev.wxs b/developer/src/inst/kmdev.wxs index 832d8c55de..1602f624ff 100644 --- a/developer/src/inst/kmdev.wxs +++ b/developer/src/inst/kmdev.wxs @@ -59,7 +59,7 @@ - + @@ -198,7 +198,7 @@ - + diff --git a/developer/src/inst/node/dist/Keyman_README.md b/developer/src/inst/node/dist/Keyman_README.md index faf95051f5..6d4ce63744 100644 --- a/developer/src/inst/node/dist/Keyman_README.md +++ b/developer/src/inst/node/dist/Keyman_README.md @@ -1,6 +1,6 @@ # Bundled node.js -We are bundling node 18.14.1 here as of Keyman 17.0.52. +We are bundling node 20.16.1 here as of Keyman 18.0.156-alpha Note that if we change the node version, we will need to rebuild the binary modules in /developer/src/server/src/win32/ diff --git a/developer/src/kmc-copy/src/KeymanProjectCopier.ts b/developer/src/kmc-copy/src/KeymanProjectCopier.ts index 9e8d175991..a751e46731 100644 --- a/developer/src/kmc-copy/src/KeymanProjectCopier.ts +++ b/developer/src/kmc-copy/src/KeymanProjectCopier.ts @@ -4,7 +4,7 @@ * Copy a keyboard or lexical model project */ -import { CloudUrls, GitHubUrls, CompilerCallbacks, CompilerLogLevel, KeymanCompiler, KeymanCompilerArtifact, KeymanCompilerArtifacts, KeymanCompilerResult, KeymanDeveloperProject, KeymanDeveloperProjectOptions, KPJFileReader, KPJFileWriter, KpsFileReader, KpsFileWriter, ValidIds } from "@keymanapp/developer-utils"; +import { CloudUrls, GitHubUrls, CompilerCallbacks, KeymanCompiler, KeymanCompilerArtifact, KeymanCompilerArtifacts, KeymanCompilerResult, KeymanDeveloperProject, KeymanDeveloperProjectOptions, KPJFileReader, KPJFileWriter, KpsFileReader, KpsFileWriter, ValidIds, CompilerBaseOptions } from "@keymanapp/developer-utils"; import { KeymanFileTypes } from "@keymanapp/common-types"; import { CopierMessages } from "./copier-messages.js"; @@ -19,12 +19,7 @@ type CopierFunction = ( * @public * Options for the Keyman Developer project copier */ -export interface CopierOptions /* not inheriting from CompilerBaseOptions */ { - /** - * Reporting level to console, used by NodeCompilerCallbacks (not used in - * compiler modules; all messages are still reported to the internal log) - */ - logLevel?: CompilerLogLevel; +export interface CopierOptions extends CompilerBaseOptions { /** * output path where project folder will be created */ @@ -33,6 +28,12 @@ export interface CopierOptions /* not inheriting from CompilerBaseOptions */ { * dryRun: show what would happen */ dryRun: boolean; + /** + * Copy referenced files that are outside the project folder into an + * 'external' folder within the new project, rather than just referencing them + * in their current location + */ + relocateExternalFiles?: boolean; }; /** @@ -78,7 +79,6 @@ export class KeymanProjectCopier implements KeymanCompiler { isLocalOrigin(): boolean { return this.githubRef == undefined; } - relocateExternalFiles: boolean = false; // TODO-COPY: support public async init(callbacks: CompilerCallbacks, options: CopierOptions): Promise { if(!callbacks || !options) { @@ -357,6 +357,7 @@ export class KeymanProjectCopier implements KeymanCompiler { const subFilename = this.normalizePath(this.resolveFilename(project.projectFilename, normalizedFilePath)); const copier = this.copiers[ file.fileType] ?? this.copyGenericFile.bind(this); // Ignore errors because we will continue to do a best effort + this.callbacks.reportMessage(CopierMessages.Verbose_CopyingFile({from: normalizedFilePath, to: subFilename})); await copier(project, subFilename, subOutputPath, source, result); } @@ -559,20 +560,25 @@ export class KeymanProjectCopier implements KeymanCompiler { let subOutputPath: string; if(this.callbacks.path.isAbsolute(subFilenameRelative) || subFilenameRelative.startsWith('..')) { - if(this.isLocalOrigin() && !this.relocateExternalFiles) { + if(this.isLocalOrigin() && !this.options.relocateExternalFiles) { // Reference outside the project structure, do not attempt to normalize or copy, // but we do need to update references for the new output path - return this.normalizePath(this.callbacks.path.relative(result.artifacts[parentFilename].filename, subFilenameAbsolute)); + const destinationFilename = + this.normalizePath(this.callbacks.path.relative(result.artifacts[parentFilename].filename, subFilenameAbsolute)); + this.callbacks.reportMessage(CopierMessages.Verbose_CopyingFile({from: originalSubfilename, to: destinationFilename})); + return destinationFilename; } else { // Reference outside the project structure, we will copy into a subfolder of the project; // should be relative to root of remote repo, e.g. release/k/khmer_angkor references release/shared/fonts/..., // so should go external/release/shared/fonts/... - subOutputPath = this.normalizePath(this.callbacks.path.join(this.options.outPath, 'external', this.callbacks.path.dirname(subFilenameAbsolute))); + const relativePath = this.callbacks.path.dirname(subFilenameRelative).replace(/\.\.[/\\]/g, ''); + subOutputPath = this.normalizePath(this.callbacks.path.join(this.options.outPath, 'external', relativePath)); } } else { subOutputPath = this.normalizePath(this.callbacks.path.join(outputPath, this.callbacks.path.dirname(originalSubfilename))); } + this.callbacks.reportMessage(CopierMessages.Verbose_CopyingFile({from: originalSubfilename, to: subFilename})); await this.copyGenericFile(project, subFilename, subOutputPath, source, result); // Even if the subfile is missing, we'll still continue the overall copy diff --git a/developer/src/kmc-copy/src/copier-messages.ts b/developer/src/kmc-copy/src/copier-messages.ts index 8afff79149..d4751474b9 100644 --- a/developer/src/kmc-copy/src/copier-messages.ts +++ b/developer/src/kmc-copy/src/copier-messages.ts @@ -19,9 +19,10 @@ const SevFatal = CompilerErrorSeverity.Fatal | Namespace; */ export class CopierMessages { static FATAL_UnexpectedException = SevFatal | 0x0001; - static Fatal_UnexpectedException = (o:{e: any}) => m( + static Fatal_UnexpectedException = (o:{e: any}) => CompilerMessageSpecWithException( this.FATAL_UnexpectedException, - null, o.e ?? 'unknown error' + null, + o.e ?? 'unknown error' ); static INFO_CopyingProject = SevInfo | 0x0002; @@ -123,81 +124,90 @@ export class CopierMessages { static ERROR_CannotDownloadFolderFromGitHub = SevError | 0x0012; static Error_CannotDownloadFolderFromGitHub = (o:{ref: string, message?: string, cause?: string}) => m( this.ERROR_CannotDownloadFolderFromGitHub, - `The folder '${def(o.ref)}' could not be downloaded: ${def(o.message)} ${def(o.cause)}`, - `An error was encountered attempting to download a folder from GitHub API. Check the - provided error details for the cause.` - ); + `The folder '${def(o.ref)}' could not be downloaded: ${def(o.message)} ${def(o.cause)}`, ` + An error was encountered attempting to download a folder from GitHub API. Check + the provided error details for the cause. + `); static ERROR_FolderDownloadedFromGitHubIsNotAValidFolder = SevError | 0x0013; static Error_FolderDownloadedFromGitHubIsNotAValidFolder = (o:{ref: string}) => m( this.ERROR_FolderDownloadedFromGitHubIsNotAValidFolder, - `The path '${def(o.ref)}' does not appear to be a folder on GitHub`, - `The provided path may be a file or may not exist. Check the reference - before trying again.` - ); + `The path '${def(o.ref)}' does not appear to be a folder on GitHub`, ` + The provided path may be a file or may not exist. Check the reference + before trying again. + `); static WARN_CannotDownloadFileFromGitHub = SevWarn | 0x0014; static Warn_CannotDownloadFileFromGitHub = (o:{ref: string, message?: string, cause?: string}) => m( this.WARN_CannotDownloadFileFromGitHub, - `The file '${def(o.ref)}' could not be downloaded: ${def(o.message)} ${def(o.cause)}`, - `An error was encountered attempting to download a file from GitHub. Check the - provided error details for the cause.` - ); + `The file '${def(o.ref)}' could not be downloaded: ${def(o.message)} ${def(o.cause)}`, ` + An error was encountered attempting to download a file from GitHub. Check the + provided error details for the cause. + `); static ERROR_InvalidCloudKeyboardId = SevError | 0x0015; static Error_InvalidCloudKeyboardId = (o:{id: string}) => m( this.ERROR_InvalidCloudKeyboardId, - `The keyboard identifier '${def(o.id)}' is not a valid keyboard identifier`, - `Keyboard identifiers on Keyman Cloud can only use the characters a-z, 0-9, and _.` - ); + `The keyboard identifier '${def(o.id)}' is not a valid keyboard identifier`, ` + Keyboard identifiers on Keyman Cloud can only use the characters + \`a\`-\`z\`, \`0\`-\`9\`, and \`_\`. + `); static ERROR_CouldNotRetrieveFromCloud = SevError | 0x0016; static Error_CouldNotRetrieveFromCloud = (o:{id: string, message?: string, cause?: string}) => m( this.ERROR_CouldNotRetrieveFromCloud, - `Details for keyboard or model identified by '${def(o.id)}' could not be downloaded: ${def(o.message)} ${def(o.cause)}`, - `An error was encountered attempting to retrieve keyboard or model details from Keyman Cloud API. Check the - provided error details for the cause.` - ); + `Details for keyboard or model identified by '${def(o.id)}' could not be `+ + `downloaded: ${def(o.message)} ${def(o.cause)}`, ` + An error was encountered attempting to retrieve keyboard or model details from + Keyman Cloud API. Check the provided error details for the cause. + `); static ERROR_KeymanCloudReturnedInvalidData = SevError | 0x0017; static Error_KeymanCloudReturnedInvalidData = (o:{id: string}) => m( this.ERROR_KeymanCloudReturnedInvalidData, - `Keyman Cloud API returned invalid data for keyboard or model identified by '${def(o.id)}'`, - `There may be a network error or a server error. Retry your request later or contact - Keyman Support for assistance.` - ); + `Keyman Cloud API returned invalid data for keyboard or model identified by '${def(o.id)}'`, ` + There may be a network error or a server error. Retry your request later or + contact Keyman Support for assistance. + `); static ERROR_CloudDoesNotHaveSource = SevError | 0x0018; static Error_CloudDoesNotHaveSource = (o:{id: string}) => m( this.ERROR_CloudDoesNotHaveSource, - `The keyboard or model identified by '${def(o.id)}' does not have source available`, - `Legacy keyboards in Keyman Cloud do not have source available. Check the Keyman keyboard catalog - at keyman.com for further details. Some new keyboards or models may be available as binary-only.` - ); + `The keyboard or model identified by '${def(o.id)}' does not have source available`, ` + Legacy keyboards in Keyman Cloud do not have source available. Check the Keyman + keyboard catalog at https://keyman.com/keyboards for further details. Some new + keyboards or models may be available as binary-only. + `); static ERROR_CannotDownloadRepoFromGitHub = SevError | 0x0019 ; static Error_CannotDownloadRepoFromGitHub = (o:{ref: string, message?: string, cause?: string}) => m( this.ERROR_CannotDownloadRepoFromGitHub, - `The repository at '${def(o.ref)}' could not be accessed: ${def(o.message)} ${def(o.cause)}`, - `An error was encountered attempting to download details about a repository from GitHub API. - Check the provided error details for the cause.` - ); + `The repository at '${def(o.ref)}' could not be accessed: ${def(o.message)} ${def(o.cause)}`, ` + An error was encountered attempting to download details about a repository + from GitHub API. Check the provided error details for the cause. + `); + + //------------------------------------------------------------------------------| + // max length of detail message lines (checked by verifyCompilerMessagesObject) | + //------------------------------------------------------------------------------| static ERROR_CouldNotFindDefaultBranchOnGitHub = SevError | 0x001A; static Error_CouldNotFindDefaultBranchOnGitHub = (o:{ref: string}) => m( this.ERROR_CouldNotFindDefaultBranchOnGitHub, - `The default branch could not be found for the GitHub repository '${def(o.ref)}'`, - `The repository may be private, or you may have a typo in the owner or repository name.` - ); + `The default branch could not be found for the GitHub repository '${def(o.ref)}'`, ` + The repository may be private, or you may have a typo in the owner or + repository name. + `); static INFO_CannotDownloadBinaryFileFromGitHub = SevInfo | 0x001B; static Info_CannotDownloadBinaryFileFromGitHub = (o:{ref: string, message?: string, cause?: string}) => m( this.INFO_CannotDownloadBinaryFileFromGitHub, - `The Keyman binary file '${def(o.ref)}' could not be downloaded: ${def(o.message)} ${def(o.cause)}. This is not normally a problem`, - `In most repositories, Keyman binary files such as .kmx, .kmp, .js are not included. - This is not normally a problem, as the files can be built from the source. Check - the provided error details for more details.` - ); + `The Keyman binary file '${def(o.ref)}' could not be downloaded: ${def(o.message)} `+ + `${def(o.cause)}. This is not normally a problem`, ` + In most repositories, Keyman binary files such as .kmx, .kmp, .js are not + included. This is not normally a problem, as the files can be built from the + source. Check the provided error details for more details. + `); static VERBOSE_DownloadingFile = SevVerbose | 0x001C; static Verbose_DownloadingFile = (o:{filename: string, url: string}) => m( @@ -231,4 +241,10 @@ export class CopierMessages { `The output file '${def(o.filename)}' has two different possible source files.`, ); + static VERBOSE_CopyingFile = SevVerbose | 0x0021; + static Verbose_CopyingFile = (o:{from: string, to: string}) => m( + this.VERBOSE_CopyingFile, + `Copying file '${def(o.from)}' to '${def(o.to)}'`, + ); + }; diff --git a/developer/src/kmc-copy/test/copier.tests.ts b/developer/src/kmc-copy/test/copier.tests.ts index 6a2a8cbb1e..697e58e0b5 100644 --- a/developer/src/kmc-copy/test/copier.tests.ts +++ b/developer/src/kmc-copy/test/copier.tests.ts @@ -13,6 +13,7 @@ import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; import { KeymanProjectCopier } from '../src/KeymanProjectCopier.js'; import { makePathToFixture } from './helpers/index.js'; import { GitHubRef } from './cloud.js'; +import { KpsFileReader } from '@keymanapp/developer-utils'; const { TEST_SAVE_ARTIFACTS, TEST_SAVE_FIXTURES } = env; let outputRoot: string = '/an/imaginary/root/'; @@ -269,8 +270,8 @@ describe('KeymanProjectCopier', function() { "/release/j/jawa/source/jawa2.bmp": "source/jawa2.bmp", "/release/j/jawa/source/readme.htm": "source/readme.htm", "/release/j/jawa/source/welcome/welcome.htm": "source/welcome/welcome.htm", - "/release/shared/fonts/noto/Java/NotoSansJavanese-Bold.ttf": "external/release/shared/fonts/noto/Java/NotoSansJavanese-Bold.ttf", - "/release/shared/fonts/noto/Java/NotoSansJavanese-Regular.ttf": "external/release/shared/fonts/noto/Java/NotoSansJavanese-Regular.ttf", + "/release/shared/fonts/noto/Java/NotoSansJavanese-Bold.ttf": "external/shared/fonts/noto/Java/NotoSansJavanese-Bold.ttf", + "/release/shared/fonts/noto/Java/NotoSansJavanese-Regular.ttf": "external/shared/fonts/noto/Java/NotoSansJavanese-Regular.ttf", // NOTE: the following files are not included because this is a 1.0 // project and they are not referenced by any files: // * HISTORY.md @@ -523,6 +524,103 @@ describe('KeymanProjectCopier', function() { ]); }); + it('should copy external files into an "external" folder when relocateExternalFiles is true', async function() { + const copier = new KeymanProjectCopier(); + assert.isTrue(await copier.init(callbacks, { + dryRun: false, + outPath: 'organized', + relocateExternalFiles: true, + })); + + const result = await copier.run(makePathToFixture('projects','external','alephwithbeth','alephwithbeth.kpj')); + assert.sameMembers(Object.keys(result.artifacts).map(k => result.artifacts[k].filename), [ + 'organized', // kmc-copy:outputPath + 'organized/source/organized.keyman-touch-layout', + 'organized/source/organized.kmn', + 'organized/source/organized.ico', + 'organized/source/organized.kvks', + 'organized/source/organized.kps', + 'organized/source/readme.htm', + // files in 'another-folder/' get moved to 'external/' + 'organized/external/another-folder/welcome/welcome.htm', + 'organized/external/another-folder/welcome/KeyboardLayout.png', + 'organized/external/another-folder/welcome/KeyboardLayoutAlt.png', + 'organized/external/another-folder/welcome/KeyboardLayoutShift.png', + // build artifact files get moved to build/ + 'organized/build/organized.js', + 'organized/build/organized.kvk', + 'organized/build/organized.kmx', + // 'organized/build/organized.kmp', // TODO-COPY: do we need to copy this file as well? + 'organized/LICENSE.md', + 'organized/organized.kpj', + 'organized/HISTORY.md', + 'organized/README.md', + 'organized/source/help/organized.php', + 'organized/source/help/KeyboardLayout.png', + 'organized/source/help/KeyboardLayoutAlt.png', + 'organized/source/help/KeyboardLayoutShift.png', + ]); + + // Verify that .kps has the appropriate paths + const reader = new KpsFileReader(callbacks); + const kpsFilename = Object.keys(result.artifacts).find(k => result.artifacts[k].filename == 'organized/source/organized.kps')!; + const kps = reader.read(result.artifacts[kpsFilename].data); + assert.isDefined(kps.Package.Files?.File.find(file => file.Name == '../external/another-folder/welcome/welcome.htm')); + assert.isDefined(kps.Package.Files?.File.find(file => file.Name == '../external/another-folder/welcome/KeyboardLayout.png')); + assert.isDefined(kps.Package.Files?.File.find(file => file.Name == '../external/another-folder/welcome/KeyboardLayoutAlt.png')); + assert.isDefined(kps.Package.Files?.File.find(file => file.Name == '../external/another-folder/welcome/KeyboardLayoutShift.png')); + }); + + it('should leave external files in their original location when relocateExternalFiles is false', async function() { + const copier = new KeymanProjectCopier(); + assert.isTrue(await copier.init(callbacks, { + dryRun: false, + outPath: 'organized', + relocateExternalFiles: false, + })); + + const result = await copier.run(makePathToFixture('projects','external','alephwithbeth','alephwithbeth.kpj')); + assert.sameMembers(Object.keys(result.artifacts).map(k => result.artifacts[k].filename), [ + 'organized', // kmc-copy:outputPath + 'organized/source/organized.keyman-touch-layout', + 'organized/source/organized.kmn', + 'organized/source/organized.ico', + 'organized/source/organized.kvks', + 'organized/source/organized.kps', + 'organized/source/readme.htm', + + // files in 'another-folder/' stay there, so they are not referenced + // 'organized/external/welcome.htm', + // 'organized/external/KeyboardLayout.png', + // 'organized/external/KeyboardLayoutAlt.png', + // 'organized/external/KeyboardLayoutShift.png', + + // build artifact files get moved to build/ + 'organized/build/organized.js', + 'organized/build/organized.kvk', + 'organized/build/organized.kmx', + // 'organized/build/organized.kmp', // TODO-COPY: do we need to copy this file as well? + 'organized/LICENSE.md', + 'organized/organized.kpj', + 'organized/HISTORY.md', + 'organized/README.md', + 'organized/source/help/organized.php', + 'organized/source/help/KeyboardLayout.png', + 'organized/source/help/KeyboardLayoutAlt.png', + 'organized/source/help/KeyboardLayoutShift.png', + ]); + + // we'll check the .kps for its references, these are relative to the test/organized/source? folder + const reader = new KpsFileReader(callbacks); + const kpsFilename = Object.keys(result.artifacts).find(k => result.artifacts[k].filename == 'organized/source/organized.kps')!; + const kps = reader.read(result.artifacts[kpsFilename].data); + + assert.isDefined(kps.Package.Files?.File.find(file => file.Name == '../../../test/fixtures/projects/external/another-folder/welcome/welcome.htm')); + assert.isDefined(kps.Package.Files?.File.find(file => file.Name == '../../../test/fixtures/projects/external/another-folder/welcome/KeyboardLayout.png')); + assert.isDefined(kps.Package.Files?.File.find(file => file.Name == '../../../test/fixtures/projects/external/another-folder/welcome/KeyboardLayoutAlt.png')); + assert.isDefined(kps.Package.Files?.File.find(file => file.Name == '../../../test/fixtures/projects/external/another-folder/welcome/KeyboardLayoutShift.png')); + }); + // TODO-COPY: additional tests it.skip('should copy a standalone .kmn into a new project', async function() {}); it.skip('should copy a standalone .kmn and .kps into a new project', async function() {}); diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/HISTORY.md b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/HISTORY.md new file mode 100644 index 0000000000..65dd7e13c7 --- /dev/null +++ b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/HISTORY.md @@ -0,0 +1,35 @@ +alephwithbeth Change History +==================== + +1.0.6 (2024-06-11) +------------------ +* Remove source files from package + +1.0.5 (2023-02-28) +------------------ +* replaced hyphen with maqef +* added a touch-optimized keyboard layout for phone + +1.0.4 (2023-02-13) +------------------ +* Removed unnecessary version numbers. +* added description + +1.0.3 (2023-02-11) +------------------ +* Removed the faulty auto correct feature. +* Captialized all words of the title. +* Added title to .kmn + +1.0.2 (2023-02-09) +------------------ +* Renamed keyboard to "Aleph with Beth" + +1.0.1 (2023-02-08) +---------------- +* Updated author and website (in kps) +* Added copyrights where needed. + +1.0 (2023-01-28) +---------------- +* Created by Cameron Briggs diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/LICENSE.md b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/LICENSE.md new file mode 100644 index 0000000000..7f84a306c0 --- /dev/null +++ b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright © 2023-2024 Cameron Briggs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/README.md b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/README.md new file mode 100644 index 0000000000..16edd8db20 --- /dev/null +++ b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/README.md @@ -0,0 +1,25 @@ +alephwithbeth keyboard +============== + +Description +----------- +A Hebrew keyboard designed to intuitively match the standard QWERTY layout. + +Copyright +--------- +See [LICENSE.md](LICENSE.md) + +Supported Platforms +------------------- + * Windows + * macOS + * Linux + * Web + * iPhone + * iPad + * Android phone + * Android tablet + * Mobile devices + * Desktop devices + * Tablet devices + diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/alephwithbeth.kpj b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/alephwithbeth.kpj new file mode 100644 index 0000000000..e3309add20 --- /dev/null +++ b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/alephwithbeth.kpj @@ -0,0 +1,9 @@ + + + + 2.0 + $PROJECTPATH\build + True + True + + diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/build/alephwithbeth.js b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/build/alephwithbeth.js new file mode 100644 index 0000000000..a17168b28d --- /dev/null +++ b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/build/alephwithbeth.js @@ -0,0 +1 @@ +KeymanWeb.KR(new Keyboard_alephwithbeth());function Keyboard_alephwithbeth(){this._v=(typeof keyman!="undefined"&&typeof keyman.version=="string")?parseInt(keyman.version,10):9;this.KI="Keyboard_alephwithbeth";this.KN="Aleph With Beth";this.KMINVER="14.0";this.KV={F:' 1em "Arial"',K102:0};this.KV.KLS={"shift": ["","!","֘","֨","֜","֞","֔","֬","֝","(",")","_","+","","","","קּ","וּ","ֵ","רּ","תּ","יּ","ֻ","","וֹ","פּ","{","}","|","","","","ָ","סּ","דּ","טּ","גּ","הּ","וַיּ","כּ","לּ","״","\"","","","","","","","זּ","צּ","שּׂ","שּׁ","בּ","נּ","מּ","","ּ","?","","","","","",""],"default": ["֑","1","2","3","4","5","6","7","8","9","0","־","=","","","","ק","ו","ֶ","ר","ת","י","ע","ִ","ֹ","פ","[","]","\\","","","","ַ","ס","ד","ט","ג","ה","ח","כ","ל","ְ","'","","","","","","","ז","צ","שׂ","שׁ","ב","נ","מ","א",".","/","","","","","",""],"alt": ["","֗","֮","֙","֠","֩","֟","֡","֕","֓","֯","ֿ","◌","","","","","","ֱ","","","","","ֽ","ֳ","ף","֔","֒","֫","","","","ֲ","","","","•","̊","","ך","","׃","ׄ","","","","","","","","ץ","","","","ן","ם","֫","̈","״","","","","","",""]};this.KV.BK=(function(x){var e=Array.apply(null,Array(65)).map(String.prototype.valueOf,""),r=[],v,i,m=['default','shift','ctrl','shift-ctrl','alt','shift-alt','ctrl-alt','shift-ctrl-alt'];for(i=m.length-1;i>=0;i--)if((v=x[m[i]])||r.length)r=(v?v:e).slice().concat(r);return r})(this.KV.KLS);this.KDU=0;this.KH="Aleph With Beth";this.KM=0;this.KBVER="1.0.6";this.KMBM=0x70;this.KRTL=1;this.KVKL={"tablet":{"displayUnderlying":false,"layer":[{"id":"default","row":[{"id":"1","key":[{"id":"K_1","text":"1"},{"id":"K_2","text":"2"},{"id":"K_3","text":"3"},{"id":"K_4","text":"4"},{"id":"K_5","text":"5"},{"id":"K_6","text":"6"},{"id":"K_7","text":"7"},{"id":"K_8","text":"8"},{"id":"K_9","text":"9"},{"id":"K_0","text":"0"},{"id":"K_HYPHEN","text":"־"},{"id":"K_EQUAL","text":"="},{"id":"K_BKSP","text":"*BkSp*","width":"100","sp":"1"}]},{"id":"2","key":[{"id":"K_Q","text":"ק","pad":"75"},{"id":"K_W","text":"ו"},{"id":"K_E","text":"ֶ"},{"id":"K_R","text":"ר"},{"id":"K_T","text":"ת"},{"id":"K_Y","text":"י"},{"id":"K_U","text":"ע"},{"id":"K_I","text":"ִ"},{"id":"K_O","text":"ֹ"},{"id":"K_P","text":"פ"},{"id":"K_LBRKT","text":"["},{"id":"K_RBRKT","text":"]"},{"id":"T_new_138","width":"10","sp":"10"}]},{"id":"3","key":[{"id":"K_BKQUOTE","text":"֑"},{"id":"K_A","text":"ַ"},{"id":"K_S","text":"ס"},{"id":"K_D","text":"ד"},{"id":"K_F","text":"ט"},{"id":"K_G","text":"ג"},{"id":"K_H","text":"ה"},{"id":"K_J","text":"ח"},{"id":"K_K","text":"כ"},{"id":"K_L","text":"ל"},{"id":"K_COLON","text":"ְ"},{"id":"K_QUOTE","text":"'"},{"id":"K_BKSLASH","text":"\\"}]},{"id":"4","key":[{"id":"K_SHIFT","text":"*Shift*","width":"160","sp":"1","nextlayer":"shift"},{"id":"K_oE2"},{"id":"K_Z","text":"ז"},{"id":"K_X","text":"צ"},{"id":"K_C","text":"שׂ"},{"id":"K_V","text":"שׁ"},{"id":"K_B","text":"ב"},{"id":"K_N","text":"נ"},{"id":"K_M","text":"מ"},{"id":"K_COMMA","text":"א"},{"id":"K_PERIOD","text":"."},{"id":"K_SLASH","text":"/"},{"id":"T_new_164","width":"10","sp":"10"}]},{"id":"5","key":[{"id":"K_LCONTROL","text":"alt","width":"130","sp":"1","nextlayer":"alt"},{"id":"K_LOPT","text":"*Menu*","width":"140","sp":"1"},{"id":"K_SPACE","width":"930"},{"id":"K_ENTER","text":"*Enter*","width":"145","sp":"1"}]}]},{"id":"shift","row":[{"id":"1","key":[{"id":"K_1","text":"!"},{"id":"K_2","text":"֘"},{"id":"K_3","text":"֨"},{"id":"K_4","text":"֜"},{"id":"K_5","text":"֞"},{"id":"K_6","text":"֔"},{"id":"K_7","text":"֬"},{"id":"K_8","text":"֝"},{"id":"K_9","text":"("},{"id":"K_0","text":")"},{"id":"K_HYPHEN","text":"_"},{"id":"K_EQUAL","text":"+"},{"id":"K_BKSP","text":"*BkSp*","width":"100","sp":"1"}]},{"id":"2","key":[{"id":"K_Q","text":"קּ","pad":"75"},{"id":"K_W","text":"וּ"},{"id":"K_E","text":"ֵ"},{"id":"K_R","text":"רּ"},{"id":"K_T","text":"תּ"},{"id":"K_Y","text":"יּ"},{"id":"K_U","text":"ֻ"},{"id":"K_I"},{"id":"K_O","text":"וֹ"},{"id":"K_P","text":"פּ"},{"id":"K_LBRKT","text":"{"},{"id":"K_RBRKT","text":"}"},{"id":"T_new_362","width":"10","sp":"10"}]},{"id":"3","key":[{"id":"K_BKQUOTE"},{"id":"K_A","text":"ָ"},{"id":"K_S","text":"סּ"},{"id":"K_D","text":"דּ"},{"id":"K_F","text":"טּ"},{"id":"K_G","text":"גּ"},{"id":"K_H","text":"הּ"},{"id":"K_J","text":"וַיּ"},{"id":"K_K","text":"כּ"},{"id":"K_L","text":"לּ"},{"id":"K_COLON","text":"״"},{"id":"K_QUOTE","text":"\""},{"id":"K_BKSLASH","text":"|"}]},{"id":"4","key":[{"id":"K_SHIFT","text":"*Shift*","width":"160","sp":"1","nextlayer":"default"},{"id":"K_oE2"},{"id":"K_Z","text":"זּ"},{"id":"K_X","text":"צּ"},{"id":"K_C","text":"שּׂ"},{"id":"K_V","text":"שּׁ"},{"id":"K_B","text":"בּ"},{"id":"K_N","text":"נּ"},{"id":"K_M","text":"מּ"},{"id":"K_COMMA"},{"id":"K_PERIOD","text":"ּ"},{"id":"K_SLASH","text":"?"},{"id":"T_new_388","width":"10","sp":"10"}]},{"id":"5","key":[{"id":"K_LCONTROL","text":"alt","width":"130","sp":"1","nextlayer":"alt"},{"id":"K_LOPT","text":"*Menu*","width":"140","sp":"1"},{"id":"K_SPACE","width":"930"},{"id":"K_ENTER","text":"*Enter*","width":"145","sp":"1"}]}]},{"id":"alt","row":[{"id":"1","key":[{"id":"K_1","text":"֗"},{"id":"K_2","text":"֮"},{"id":"K_3","text":"֙"},{"id":"K_4","text":"֠"},{"id":"K_5","text":"֩"},{"id":"K_6","text":"֟"},{"id":"K_7","text":"֡"},{"id":"K_8","text":"֕"},{"id":"K_9","text":"֓"},{"id":"K_0","text":"֯"},{"id":"K_HYPHEN","text":"ֿ"},{"id":"K_EQUAL","text":"◌"},{"id":"K_BKSP","text":"*BkSp*","width":"100","sp":"1"}]},{"id":"2","key":[{"id":"K_Q","pad":"75"},{"id":"K_W"},{"id":"K_E","text":"ֱ"},{"id":"K_R"},{"id":"K_T"},{"id":"K_Y"},{"id":"K_U"},{"id":"K_I","text":"ֽ"},{"id":"K_O","text":"ֳ"},{"id":"K_P","text":"ף"},{"id":"K_LBRKT","text":"֔"},{"id":"K_RBRKT","text":"֒"},{"id":"T_new_138","width":"10","sp":"10"}]},{"id":"3","key":[{"id":"K_BKQUOTE"},{"id":"K_A","text":"ֲ"},{"id":"K_S"},{"id":"K_D"},{"id":"K_F"},{"id":"K_G","text":"•"},{"id":"K_H","text":"̊"},{"id":"K_J"},{"id":"K_K","text":"ך"},{"id":"K_L"},{"id":"K_COLON","text":"׃"},{"id":"K_QUOTE","text":"ׄ"},{"id":"K_BKSLASH","text":"֫"}]},{"id":"4","key":[{"id":"K_SHIFT","text":"*Shift*","width":"160","sp":"1","nextlayer":"shift","layer":"shift"},{"id":"K_oE2"},{"id":"K_Z"},{"id":"K_X","text":"ץ"},{"id":"K_C"},{"id":"K_V"},{"id":"K_B"},{"id":"K_N","text":"ן"},{"id":"K_M","text":"ם"},{"id":"K_COMMA","text":"֫"},{"id":"K_PERIOD","text":"̈"},{"id":"K_SLASH","text":"״"},{"id":"T_new_164","width":"10","sp":"10"}]},{"id":"5","key":[{"id":"K_LCONTROL","text":"default","width":"130","sp":"1","nextlayer":"default"},{"id":"K_LOPT","text":"*Menu*","width":"140","sp":"1"},{"id":"K_SPACE","width":"930"},{"id":"K_ENTER","text":"*Enter*","width":"145","sp":"1"}]}]}]},"phone":{"displayUnderlying":false,"layer":[{"id":"default","row":[{"id":"1","key":[{"id":"K_1","text":"1"},{"id":"K_2","text":"2"},{"id":"K_3","text":"3"},{"id":"K_4","text":"4"},{"id":"K_5","text":"5"},{"id":"K_6","text":"6"},{"id":"K_7","text":"7"},{"id":"K_8","text":"8"},{"id":"K_9","text":"9"},{"id":"K_0","text":"0"},{"id":"K_HYPHEN","text":"־"},{"id":"K_EQUAL","text":"="},{"id":"K_BKSP","text":"*BkSp*","width":"100","sp":"1"}]},{"id":"2","key":[{"id":"K_Q","text":"ק","pad":"75"},{"id":"K_W","text":"ו"},{"id":"K_E","text":"ֶ"},{"id":"K_R","text":"ר"},{"id":"K_T","text":"ת"},{"id":"K_Y","text":"י"},{"id":"K_U","text":"ע"},{"id":"K_I","text":"ִ"},{"id":"K_O","text":"ֹ"},{"id":"K_P","text":"פ"},{"id":"K_LBRKT","text":"["},{"id":"K_RBRKT","text":"]"},{"id":"T_new_138","width":"10","sp":"10"}]},{"id":"3","key":[{"id":"K_BKQUOTE","text":"֑"},{"id":"K_A","text":"ַ"},{"id":"K_S","text":"ס"},{"id":"K_D","text":"ד"},{"id":"K_F","text":"ט"},{"id":"K_G","text":"ג"},{"id":"K_H","text":"ה"},{"id":"K_J","text":"ח"},{"id":"K_K","text":"כ"},{"id":"K_L","text":"ל"},{"id":"K_COLON","text":"ְ"},{"id":"K_QUOTE","text":"'"},{"id":"K_BKSLASH","text":"\\"}]},{"id":"4","key":[{"id":"K_SHIFT","text":"*Shift*","width":"160","sp":"1","nextlayer":"shift"},{"id":"K_oE2"},{"id":"K_Z","text":"ז"},{"id":"K_X","text":"צ"},{"id":"K_C","text":"שׂ"},{"id":"K_V","text":"שׁ"},{"id":"K_B","text":"ב"},{"id":"K_N","text":"נ"},{"id":"K_M","text":"מ"},{"id":"K_COMMA","text":"א"},{"id":"K_PERIOD","text":"."},{"id":"K_SLASH","text":"/"},{"id":"T_new_164","width":"10","sp":"10"}]},{"id":"5","key":[{"id":"K_LCONTROL","text":"alt","width":"130","sp":"1","nextlayer":"alt"},{"id":"K_LOPT","text":"*Menu*","width":"140","sp":"1"},{"id":"K_SPACE","width":"930"},{"id":"K_ENTER","text":"*Enter*","width":"145","sp":"1"}]}]},{"id":"shift","row":[{"id":"1","key":[{"id":"K_1","text":"!"},{"id":"K_2","text":"֘"},{"id":"K_3","text":"֨"},{"id":"K_4","text":"֜"},{"id":"K_5","text":"֞"},{"id":"K_6","text":"֔"},{"id":"K_7","text":"֬"},{"id":"K_8","text":"֝"},{"id":"K_9","text":"("},{"id":"K_0","text":")"},{"id":"K_HYPHEN","text":"_"},{"id":"K_EQUAL","text":"+"},{"id":"K_BKSP","text":"*BkSp*","width":"100","sp":"1"}]},{"id":"2","key":[{"id":"K_Q","text":"קּ","pad":"75"},{"id":"K_W","text":"וּ"},{"id":"K_E","text":"ֵ"},{"id":"K_R","text":"רּ"},{"id":"K_T","text":"תּ"},{"id":"K_Y","text":"יּ"},{"id":"K_U","text":"ֻ"},{"id":"K_I"},{"id":"K_O","text":"וֹ"},{"id":"K_P","text":"פּ"},{"id":"K_LBRKT","text":"{"},{"id":"K_RBRKT","text":"}"},{"id":"T_new_362","width":"10","sp":"10"}]},{"id":"3","key":[{"id":"K_BKQUOTE"},{"id":"K_A","text":"ָ"},{"id":"K_S","text":"סּ"},{"id":"K_D","text":"דּ"},{"id":"K_F","text":"טּ"},{"id":"K_G","text":"גּ"},{"id":"K_H","text":"הּ"},{"id":"K_J","text":"וַיּ"},{"id":"K_K","text":"כּ"},{"id":"K_L","text":"לּ"},{"id":"K_COLON","text":"״"},{"id":"K_QUOTE","text":"\""},{"id":"K_BKSLASH","text":"|"}]},{"id":"4","key":[{"id":"K_SHIFT","text":"*Shift*","width":"160","sp":"1","nextlayer":"default"},{"id":"K_oE2"},{"id":"K_Z","text":"זּ"},{"id":"K_X","text":"צּ"},{"id":"K_C","text":"שּׂ"},{"id":"K_V","text":"שּׁ"},{"id":"K_B","text":"בּ"},{"id":"K_N","text":"נּ"},{"id":"K_M","text":"מּ"},{"id":"K_COMMA"},{"id":"K_PERIOD","text":"ּ"},{"id":"K_SLASH","text":"?"},{"id":"T_new_388","width":"10","sp":"10"}]},{"id":"5","key":[{"id":"K_LCONTROL","text":"alt","width":"130","sp":"1","nextlayer":"alt"},{"id":"K_LOPT","text":"*Menu*","width":"140","sp":"1"},{"id":"K_SPACE","width":"930"},{"id":"K_ENTER","text":"*Enter*","width":"145","sp":"1"}]}]},{"id":"alt","row":[{"id":"1","key":[{"id":"K_1","text":"֗"},{"id":"K_2","text":"֮"},{"id":"K_3","text":"֙"},{"id":"K_4","text":"֠"},{"id":"K_5","text":"֩"},{"id":"K_6","text":"֟"},{"id":"K_7","text":"֡"},{"id":"K_8","text":"֕"},{"id":"K_9","text":"֓"},{"id":"K_0","text":"֯"},{"id":"K_HYPHEN","text":"ֿ"},{"id":"K_EQUAL","text":"◌"},{"id":"K_BKSP","text":"*BkSp*","width":"100","sp":"1"}]},{"id":"2","key":[{"id":"K_Q","pad":"75"},{"id":"K_W"},{"id":"K_E","text":"ֱ"},{"id":"K_R"},{"id":"K_T"},{"id":"K_Y"},{"id":"K_U"},{"id":"K_I","text":"ֽ"},{"id":"K_O","text":"ֳ"},{"id":"K_P","text":"ף"},{"id":"K_LBRKT","text":"֔"},{"id":"K_RBRKT","text":"֒"},{"id":"T_new_138","width":"10","sp":"10"}]},{"id":"3","key":[{"id":"K_BKQUOTE"},{"id":"K_A","text":"ֲ"},{"id":"K_S"},{"id":"K_D"},{"id":"K_F"},{"id":"K_G","text":"•"},{"id":"K_H","text":"̊"},{"id":"K_J"},{"id":"K_K","text":"ך"},{"id":"K_L"},{"id":"K_COLON","text":"׃"},{"id":"K_QUOTE","text":"ׄ"},{"id":"K_BKSLASH","text":"֫"}]},{"id":"4","key":[{"id":"K_SHIFT","text":"*Shift*","width":"160","sp":"1","nextlayer":"shift","layer":"shift"},{"id":"K_oE2"},{"id":"K_Z"},{"id":"K_X","text":"ץ"},{"id":"K_C"},{"id":"K_V"},{"id":"K_B"},{"id":"K_N","text":"ן"},{"id":"K_M","text":"ם"},{"id":"K_COMMA","text":"֫"},{"id":"K_PERIOD","text":"̈"},{"id":"K_SLASH","text":"״"},{"id":"T_new_164","width":"10","sp":"10"}]},{"id":"5","key":[{"id":"K_LCONTROL","text":"default","width":"130","sp":"1","nextlayer":"default"},{"id":"K_LOPT","text":"*Menu*","width":"140","sp":"1"},{"id":"K_SPACE","width":"930"},{"id":"K_ENTER","text":"*Enter*","width":"145","sp":"1"}]}]}]}};this.s7="כמנפצ";this.s8="ךםןףץ";this.s9="כּפּ";this.s10="ךּףּ";this.s11="ּ";this.s12="מּנּצּ";this.s13="םןץ";this.s14="ֽ֑֖֛֢֣֤֥֦֧֪֚֭֮֒֓֔֕֗֘֙֜֝֞֟֠֡֨֩֫֬֯־ֿ׀ׄ";this.s15="...";this.KVER="18.0.175.0";this.KVS=[];this.gs=function(t,e) {return this.g0(t,e);};this.gs=function(t,e) {return this.g0(t,e);};this.g0=function(t,e) {var k=KeymanWeb,r=0,m=0;if(k.KKM(e,16384,9)) {if(k.KA(0,k.KC(3,1,t),this.s7)&&k.KA(1,k.KC(2,1,t),this.s14)&&k.KCM(1,t,"ּ",1)){r=m=1;k.KIO(3,this.s8,1,t);k.KIO(-1,this.s14,2,t);k.KO(-1,t,"ּ");}else if(k.KA(0,k.KC(3,1,t),this.s7)&&k.KCM(2,t,"ּ",1)&&k.KA(2,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(3,this.s8,1,t);k.KO(-1,t,"ּ");k.KIO(-1,this.s14,3,t);}else if(k.KA(0,k.KC(2,1,t),this.s7)&&k.KCM(1,t,"ּ",1)){r=m=1;k.KIO(2,this.s8,1,t);k.KO(-1,t,"ּ");}else if(k.KA(0,k.KC(2,1,t),this.s7)&&k.KA(1,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(2,this.s8,1,t);k.KIO(-1,this.s14,2,t);}else if(k.KA(0,k.KC(2,1,t),this.s9)&&k.KA(1,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(2,this.s10,1,t);k.KIO(-1,this.s14,2,t);}else if(k.KA(0,k.KC(2,1,t),this.s12)&&k.KA(1,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(2,this.s13,1,t);k.KO(-1,t,"ּ");k.KIO(-1,this.s14,2,t);}else if(k.KA(0,k.KC(1,1,t),this.s7)){r=m=1;k.KIO(1,this.s8,1,t);}else if(k.KA(0,k.KC(1,1,t),this.s9)){r=m=1;k.KIO(1,this.s10,1,t);}else if(k.KA(0,k.KC(1,1,t),this.s12)){r=m=1;k.KIO(1,this.s13,1,t);k.KO(-1,t,"ּ");}}else if(k.KKM(e,16384,13)) {if(k.KA(0,k.KC(3,1,t),this.s7)&&k.KA(1,k.KC(2,1,t),this.s14)&&k.KCM(1,t,"ּ",1)){r=m=1;k.KIO(3,this.s8,1,t);k.KIO(-1,this.s14,2,t);k.KO(-1,t,"ּ");}else if(k.KA(0,k.KC(3,1,t),this.s7)&&k.KCM(2,t,"ּ",1)&&k.KA(2,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(3,this.s8,1,t);k.KO(-1,t,"ּ");k.KIO(-1,this.s14,3,t);}else if(k.KA(0,k.KC(2,1,t),this.s7)&&k.KCM(1,t,"ּ",1)){r=m=1;k.KIO(2,this.s8,1,t);k.KO(-1,t,"ּ");}else if(k.KA(0,k.KC(2,1,t),this.s7)&&k.KA(1,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(2,this.s8,1,t);k.KIO(-1,this.s14,2,t);}else if(k.KA(0,k.KC(2,1,t),this.s9)&&k.KA(1,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(2,this.s10,1,t);k.KIO(-1,this.s14,2,t);}else if(k.KA(0,k.KC(2,1,t),this.s12)&&k.KA(1,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(2,this.s13,1,t);k.KO(-1,t,"ּ");k.KIO(-1,this.s14,2,t);}else if(k.KA(0,k.KC(1,1,t),this.s7)){r=m=1;k.KIO(1,this.s8,1,t);}else if(k.KA(0,k.KC(1,1,t),this.s9)){r=m=1;k.KIO(1,this.s10,1,t);}else if(k.KA(0,k.KC(1,1,t),this.s12)){r=m=1;k.KIO(1,this.s13,1,t);k.KO(-1,t,"ּ");}}else if(k.KKM(e,16384,32)) {if(k.KA(0,k.KC(3,1,t),this.s7)&&k.KA(1,k.KC(2,1,t),this.s14)&&k.KCM(1,t,"ּ",1)){r=m=1;k.KIO(3,this.s8,1,t);k.KIO(-1,this.s14,2,t);k.KO(-1,t,"ּ");}else if(k.KA(0,k.KC(3,1,t),this.s7)&&k.KCM(2,t,"ּ",1)&&k.KA(2,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(3,this.s8,1,t);k.KO(-1,t,"ּ");k.KIO(-1,this.s14,3,t);}else if(k.KA(0,k.KC(2,1,t),this.s7)&&k.KCM(1,t,"ּ",1)){r=m=1;k.KIO(2,this.s8,1,t);k.KO(-1,t,"ּ");}else if(k.KA(0,k.KC(2,1,t),this.s7)&&k.KA(1,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(2,this.s8,1,t);k.KIO(-1,this.s14,2,t);}else if(k.KA(0,k.KC(2,1,t),this.s9)&&k.KA(1,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(2,this.s10,1,t);k.KIO(-1,this.s14,2,t);}else if(k.KA(0,k.KC(2,1,t),this.s12)&&k.KA(1,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(2,this.s13,1,t);k.KO(-1,t,"ּ");k.KIO(-1,this.s14,2,t);}else if(k.KA(0,k.KC(1,1,t),this.s7)){r=m=1;k.KIO(1,this.s8,1,t);}else if(k.KA(0,k.KC(1,1,t),this.s9)){r=m=1;k.KIO(1,this.s10,1,t);}else if(k.KA(0,k.KC(1,1,t),this.s12)){r=m=1;k.KIO(1,this.s13,1,t);k.KO(-1,t,"ּ");}else if(1){r=m=1;k.KO(0,t," ");}}else if(k.KKM(e,16432,32)) {if(1){r=m=1;k.KO(0,t," ");}}else if(k.KKM(e,16400,32)) {if(1){r=m=1;k.KO(0,t," ");}}else if(k.KKM(e,16448,32)) {if(1){r=m=1;k.KO(0,t," ");}}else if(k.KKM(e,16400,49)) {if(1){r=m=1;k.KO(0,t,"!");}}else if(k.KKM(e,16400,222)) {if(1){r=m=1;k.KO(0,t,"\"");}}else if(k.KKM(e,16400,51)) {if(1){r=m=1;k.KO(0,t,"֨");}}else if(k.KKM(e,16400,52)) {if(1){r=m=1;k.KO(0,t,"֜");}}else if(k.KKM(e,16400,53)) {if(1){r=m=1;k.KO(0,t,"֞");}}else if(k.KKM(e,16400,55)) {if(1){r=m=1;k.KO(0,t,"֬");}}else if(k.KKM(e,16384,222)) {if(1){r=m=1;k.KO(0,t,"'");}}else if(k.KKM(e,16448,222)) {if(1){r=m=1;k.KO(0,t,"ׄ");}}else if(k.KKM(e,16400,57)) {if(1){r=m=1;k.KO(0,t,"(");}}else if(k.KKM(e,16400,48)) {if(1){r=m=1;k.KO(0,t,")");}}else if(k.KKM(e,16400,56)) {if(1){r=m=1;k.KO(0,t,"֝");}}else if(k.KKM(e,16400,187)) {if(1){r=m=1;k.KO(0,t,"+");}}else if(k.KKM(e,16448,188)) {if(1){r=m=1;k.KO(0,t,"֫");}}else if(k.KKM(e,16384,188)) {if(1){r=m=1;k.KO(0,t,"א");}}else if(k.KKM(e,16384,189)) {if(1){r=m=1;k.KO(0,t,"־");}}else if(k.KKM(e,16448,189)) {if(1){r=m=1;k.KO(0,t,"ֿ");}}else if(k.KKM(e,16384,190)) {if(1){r=m=1;k.KO(0,t,".");}}else if(k.KKM(e,16448,190)) {if(1){r=m=1;k.KO(0,t,"̈");}}else if(k.KKM(e,16384,191)) {if(1){r=m=1;k.KO(0,t,"/");}}else if(k.KKM(e,16448,191)) {if(1){r=m=1;k.KO(0,t,"״");}}else if(k.KKM(e,16384,48)) {if(1){r=m=1;k.KO(0,t,"0");}}else if(k.KKM(e,16448,48)) {if(1){r=m=1;k.KO(0,t,"֯");}}else if(k.KKM(e,16384,49)) {if(1){r=m=1;k.KO(0,t,"1");}}else if(k.KKM(e,16448,49)) {if(1){r=m=1;k.KO(0,t,"֗");}}else if(k.KKM(e,16384,50)) {if(1){r=m=1;k.KO(0,t,"2");}}else if(k.KKM(e,16448,50)) {if(1){r=m=1;k.KO(0,t,"֮");}}else if(k.KKM(e,16384,51)) {if(1){r=m=1;k.KO(0,t,"3");}}else if(k.KKM(e,16448,51)) {if(1){r=m=1;k.KO(0,t,"֙");}}else if(k.KKM(e,16384,52)) {if(1){r=m=1;k.KO(0,t,"4");}}else if(k.KKM(e,16448,52)) {if(1){r=m=1;k.KO(0,t,"֠");}}else if(k.KKM(e,16384,53)) {if(1){r=m=1;k.KO(0,t,"5");}}else if(k.KKM(e,16448,53)) {if(1){r=m=1;k.KO(0,t,"֩");}}else if(k.KKM(e,16384,54)) {if(1){r=m=1;k.KO(0,t,"6");}}else if(k.KKM(e,16448,54)) {if(1){r=m=1;k.KO(0,t,"֟");}}else if(k.KKM(e,16384,55)) {if(1){r=m=1;k.KO(0,t,"7");}}else if(k.KKM(e,16448,55)) {if(1){r=m=1;k.KO(0,t,"֡");}}else if(k.KKM(e,16384,56)) {if(1){r=m=1;k.KO(0,t,"8");}}else if(k.KKM(e,16448,56)) {if(1){r=m=1;k.KO(0,t,"֕");}}else if(k.KKM(e,16384,57)) {if(1){r=m=1;k.KO(0,t,"9");}}else if(k.KKM(e,16448,57)) {if(1){r=m=1;k.KO(0,t,"֓");}}else if(k.KKM(e,16400,186)) {if(1){r=m=1;k.KO(0,t,"״");}}else if(k.KKM(e,16448,186)) {if(1){r=m=1;k.KO(0,t,"׃");}}else if(k.KKM(e,16384,186)) {if(1){r=m=1;k.KO(0,t,"ְ");}}else if(k.KKM(e,16400,188)) {if(1){r=m=1;}}else if(k.KKM(e,16384,187)) {if(1){r=m=1;k.KO(0,t,"=");}}else if(k.KKM(e,16448,187)) {if(1){r=m=1;k.KO(0,t,"◌");}}else if(k.KKM(e,16400,190)) {if(1){r=m=1;k.KO(0,t,"ּ");}}else if(k.KKM(e,16400,191)) {if(1){r=m=1;k.KO(0,t,"?");}}else if(k.KKM(e,16400,50)) {if(1){r=m=1;k.KO(0,t,"֘");}}else if(k.KKM(e,16400,65)) {if(1){r=m=1;k.KO(0,t,"ָ");}}else if(k.KKM(e,16400,66)) {if(1){r=m=1;k.KO(0,t,"בּ");}}else if(k.KKM(e,16400,67)) {if(1){r=m=1;k.KO(0,t,"שּׂ");}}else if(k.KKM(e,16400,68)) {if(1){r=m=1;k.KO(0,t,"דּ");}}else if(k.KKM(e,16400,69)) {if(1){r=m=1;k.KO(0,t,"ֵ");}}else if(k.KKM(e,16400,70)) {if(1){r=m=1;k.KO(0,t,"טּ");}}else if(k.KKM(e,16400,71)) {if(1){r=m=1;k.KO(0,t,"גּ");}}else if(k.KKM(e,16400,72)) {if(1){r=m=1;k.KO(0,t,"הּ");}}else if(k.KKM(e,16400,73)) {if(1){r=m=1;}}else if(k.KKM(e,16400,74)) {if(1){r=m=1;k.KO(0,t,"וַיּ");}}else if(k.KKM(e,16400,75)) {if(1){r=m=1;k.KO(0,t,"כּ");}}else if(k.KKM(e,16400,76)) {if(1){r=m=1;k.KO(0,t,"לּ");}}else if(k.KKM(e,16400,77)) {if(1){r=m=1;k.KO(0,t,"מּ");}}else if(k.KKM(e,16400,78)) {if(1){r=m=1;k.KO(0,t,"נּ");}}else if(k.KKM(e,16400,79)) {if(1){r=m=1;k.KO(0,t,"וֹ");}}else if(k.KKM(e,16400,80)) {if(1){r=m=1;k.KO(0,t,"פּ");}}else if(k.KKM(e,16400,81)) {if(1){r=m=1;k.KO(0,t,"קּ");}}else if(k.KKM(e,16400,82)) {if(1){r=m=1;k.KO(0,t,"רּ");}}else if(k.KKM(e,16400,83)) {if(1){r=m=1;k.KO(0,t,"סּ");}}else if(k.KKM(e,16400,84)) {if(1){r=m=1;k.KO(0,t,"תּ");}}else if(k.KKM(e,16400,85)) {if(1){r=m=1;k.KO(0,t,"ֻ");}}else if(k.KKM(e,16400,86)) {if(1){r=m=1;k.KO(0,t,"שּׁ");}}else if(k.KKM(e,16400,87)) {if(1){r=m=1;k.KO(0,t,"וּ");}}else if(k.KKM(e,16400,88)) {if(1){r=m=1;k.KO(0,t,"צּ");}}else if(k.KKM(e,16400,89)) {if(1){r=m=1;k.KO(0,t,"יּ");}}else if(k.KKM(e,16400,90)) {if(1){r=m=1;k.KO(0,t,"זּ");}}else if(k.KKM(e,16384,219)) {if(1){r=m=1;k.KO(0,t,"[");}}else if(k.KKM(e,16448,219)) {if(1){r=m=1;k.KO(0,t,"֔");}}else if(k.KKM(e,16384,220)) {if(1){r=m=1;k.KO(0,t,"\\");}}else if(k.KKM(e,16448,220)) {if(1){r=m=1;k.KO(0,t,"֫");}}else if(k.KKM(e,16384,221)) {if(1){r=m=1;k.KO(0,t,"]");}}else if(k.KKM(e,16448,221)) {if(1){r=m=1;k.KO(0,t,"֒");}}else if(k.KKM(e,16400,54)) {if(1){r=m=1;k.KO(0,t,"֔");}}else if(k.KKM(e,16400,189)) {if(1){r=m=1;k.KO(0,t,"_");}}else if(k.KKM(e,16448,192)) {if(1){r=m=1;}}else if(k.KKM(e,16384,192)) {if(1){r=m=1;k.KO(0,t,"֑");}}else if(k.KKM(e,16448,65)) {if(1){r=m=1;k.KO(0,t,"ֲ");}}else if(k.KKM(e,16384,65)) {if(1){r=m=1;k.KO(0,t,"ַ");}}else if(k.KKM(e,16448,66)) {if(1){r=m=1;}}else if(k.KKM(e,16384,66)) {if(1){r=m=1;k.KO(0,t,"ב");}}else if(k.KKM(e,16448,67)) {if(1){r=m=1;}}else if(k.KKM(e,16384,67)) {if(1){r=m=1;k.KO(0,t,"שׂ");}}else if(k.KKM(e,16448,68)) {if(1){r=m=1;}}else if(k.KKM(e,16384,68)) {if(1){r=m=1;k.KO(0,t,"ד");}}else if(k.KKM(e,16448,69)) {if(1){r=m=1;k.KO(0,t,"ֱ");}}if(m) {}else if(k.KKM(e,16384,69)) {if(1){r=m=1;k.KO(0,t,"ֶ");}}else if(k.KKM(e,16448,70)) {if(1){r=m=1;}}else if(k.KKM(e,16384,70)) {if(1){r=m=1;k.KO(0,t,"ט");}}else if(k.KKM(e,16448,71)) {if(1){r=m=1;k.KO(0,t,"•");}}else if(k.KKM(e,16384,71)) {if(1){r=m=1;k.KO(0,t,"ג");}}else if(k.KKM(e,16448,72)) {if(1){r=m=1;k.KO(0,t,"̊");}}else if(k.KKM(e,16384,72)) {if(1){r=m=1;k.KO(0,t,"ה");}}else if(k.KKM(e,16448,73)) {if(1){r=m=1;k.KO(0,t,"ֽ");}}else if(k.KKM(e,16384,73)) {if(1){r=m=1;k.KO(0,t,"ִ");}}else if(k.KKM(e,16448,74)) {if(1){r=m=1;}}else if(k.KKM(e,16384,74)) {if(1){r=m=1;k.KO(0,t,"ח");}}else if(k.KKM(e,16448,75)) {if(1){r=m=1;k.KO(0,t,"ך");}}else if(k.KKM(e,16384,75)) {if(1){r=m=1;k.KO(0,t,"כ");}}else if(k.KKM(e,16448,76)) {if(1){r=m=1;}}else if(k.KKM(e,16384,76)) {if(1){r=m=1;k.KO(0,t,"ל");}}else if(k.KKM(e,16448,77)) {if(1){r=m=1;k.KO(0,t,"ם");}}else if(k.KKM(e,16384,77)) {if(1){r=m=1;k.KO(0,t,"מ");}}else if(k.KKM(e,16448,78)) {if(1){r=m=1;k.KO(0,t,"ן");}}else if(k.KKM(e,16384,78)) {if(1){r=m=1;k.KO(0,t,"נ");}}else if(k.KKM(e,16448,79)) {if(1){r=m=1;k.KO(0,t,"ֳ");}}else if(k.KKM(e,16384,79)) {if(1){r=m=1;k.KO(0,t,"ֹ");}}else if(k.KKM(e,16448,80)) {if(1){r=m=1;k.KO(0,t,"ף");}}else if(k.KKM(e,16384,80)) {if(1){r=m=1;k.KO(0,t,"פ");}}else if(k.KKM(e,16448,81)) {if(1){r=m=1;}}else if(k.KKM(e,16384,81)) {if(1){r=m=1;k.KO(0,t,"ק");}}else if(k.KKM(e,16448,82)) {if(1){r=m=1;}}else if(k.KKM(e,16384,82)) {if(1){r=m=1;k.KO(0,t,"ר");}}else if(k.KKM(e,16448,83)) {if(1){r=m=1;}}else if(k.KKM(e,16384,83)) {if(1){r=m=1;k.KO(0,t,"ס");}}else if(k.KKM(e,16448,84)) {if(1){r=m=1;}}else if(k.KKM(e,16384,84)) {if(1){r=m=1;k.KO(0,t,"ת");}}else if(k.KKM(e,16448,85)) {if(1){r=m=1;}}else if(k.KKM(e,16384,85)) {if(1){r=m=1;k.KO(0,t,"ע");}}else if(k.KKM(e,16448,86)) {if(1){r=m=1;}}else if(k.KKM(e,16384,86)) {if(1){r=m=1;k.KO(0,t,"שׁ");}}else if(k.KKM(e,16448,87)) {if(1){r=m=1;}}else if(k.KKM(e,16384,87)) {if(1){r=m=1;k.KO(0,t,"ו");}}else if(k.KKM(e,16448,88)) {if(1){r=m=1;k.KO(0,t,"ץ");}}else if(k.KKM(e,16384,88)) {if(1){r=m=1;k.KO(0,t,"צ");}}else if(k.KKM(e,16448,89)) {if(1){r=m=1;}}else if(k.KKM(e,16384,89)) {if(1){r=m=1;k.KO(0,t,"י");}}else if(k.KKM(e,16448,90)) {if(1){r=m=1;}}else if(k.KKM(e,16384,90)) {if(1){r=m=1;k.KO(0,t,"ז");}}else if(k.KKM(e,16400,219)) {if(1){r=m=1;k.KO(0,t,"{");}}else if(k.KKM(e,16400,220)) {if(1){r=m=1;k.KO(0,t,"|");}}else if(k.KKM(e,16400,221)) {if(1){r=m=1;k.KO(0,t,"}");}}else if(k.KKM(e,16400,192)) {if(1){r=m=1;}}return r;};} \ No newline at end of file diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/build/alephwithbeth.keyboard_info b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/build/alephwithbeth.keyboard_info new file mode 100644 index 0000000000..e96344dbbb --- /dev/null +++ b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/build/alephwithbeth.keyboard_info @@ -0,0 +1,40 @@ +{ + "id": "alephwithbeth", + "name": "Aleph With Beth", + "license": "mit", + "isRTL": true, + "authorName": "Cameron Briggs", + "description": "

alephwithbeth keyboard

", + "languages": { + "he": { + "examples": [], + "languageName": "Hebrew", + "displayName": "Hebrew" + } + }, + "lastModifiedDate": "2025-01-27T23:04:56.765Z", + "packageFilename": "alephwithbeth.kmp", + "packageFileSize": 244476, + "jsFilename": "alephwithbeth.js", + "jsFileSize": 23534, + "packageIncludes": [ + "documentation", + "welcome", + "visualKeyboard" + ], + "version": "1.0.6", + "encodings": [ + "unicode" + ], + "platformSupport": { + "windows": "full", + "macos": "full", + "linux": "full", + "desktopWeb": "full", + "ios": "full", + "android": "full", + "mobileWeb": "full" + }, + "minKeymanVersion": "14.0", + "helpLink": "https://help.keyman.com/keyboard/alephwithbeth" +} \ No newline at end of file diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/build/alephwithbeth.kmp b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/build/alephwithbeth.kmp new file mode 100644 index 0000000000..540f4ca3ec Binary files /dev/null and b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/build/alephwithbeth.kmp differ diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/build/alephwithbeth.kmx b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/build/alephwithbeth.kmx new file mode 100644 index 0000000000..27579d2002 Binary files /dev/null and b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/build/alephwithbeth.kmx differ diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/build/alephwithbeth.kvk b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/build/alephwithbeth.kvk new file mode 100644 index 0000000000..7772d84d9e Binary files /dev/null and b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/build/alephwithbeth.kvk differ diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/alephwithbeth.ico b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/alephwithbeth.ico new file mode 100644 index 0000000000..84b57c5aeb Binary files /dev/null and b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/alephwithbeth.ico differ diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/alephwithbeth.keyman-touch-layout b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/alephwithbeth.keyman-touch-layout new file mode 100644 index 0000000000..05db57c7f9 --- /dev/null +++ b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/alephwithbeth.keyman-touch-layout @@ -0,0 +1,1646 @@ +{ + "tablet": { + "displayUnderlying": false, + "layer": [ + { + "id": "default", + "row": [ + { + "id": 1, + "key": [ + { + "id": "K_1", + "text": "1" + }, + { + "id": "K_2", + "text": "2" + }, + { + "id": "K_3", + "text": "3" + }, + { + "id": "K_4", + "text": "4" + }, + { + "id": "K_5", + "text": "5" + }, + { + "id": "K_6", + "text": "6" + }, + { + "id": "K_7", + "text": "7" + }, + { + "id": "K_8", + "text": "8" + }, + { + "id": "K_9", + "text": "9" + }, + { + "id": "K_0", + "text": "0" + }, + { + "id": "K_HYPHEN", + "text": "־" + }, + { + "id": "K_EQUAL", + "text": "=" + }, + { + "id": "K_BKSP", + "text": "*BkSp*", + "width": "100", + "sp": "1" + } + ] + }, + { + "id": 2, + "key": [ + { + "id": "K_Q", + "text": "ק", + "pad": "75" + }, + { + "id": "K_W", + "text": "ו" + }, + { + "id": "K_E", + "text": "ֶ" + }, + { + "id": "K_R", + "text": "ר" + }, + { + "id": "K_T", + "text": "ת" + }, + { + "id": "K_Y", + "text": "י" + }, + { + "id": "K_U", + "text": "ע" + }, + { + "id": "K_I", + "text": "ִ" + }, + { + "id": "K_O", + "text": "ֹ" + }, + { + "id": "K_P", + "text": "פ" + }, + { + "id": "K_LBRKT", + "text": "[" + }, + { + "id": "K_RBRKT", + "text": "]" + }, + { + "id": "T_new_138", + "text": "", + "width": "10", + "sp": "10" + } + ] + }, + { + "id": 3, + "key": [ + { + "id": "K_BKQUOTE", + "text": "֑" + }, + { + "id": "K_A", + "text": "ַ" + }, + { + "id": "K_S", + "text": "ס" + }, + { + "id": "K_D", + "text": "ד" + }, + { + "id": "K_F", + "text": "ט" + }, + { + "id": "K_G", + "text": "ג" + }, + { + "id": "K_H", + "text": "ה" + }, + { + "id": "K_J", + "text": "ח" + }, + { + "id": "K_K", + "text": "כ" + }, + { + "id": "K_L", + "text": "ל" + }, + { + "id": "K_COLON", + "text": "ְ" + }, + { + "id": "K_QUOTE", + "text": "'" + }, + { + "id": "K_BKSLASH", + "text": "\\" + } + ] + }, + { + "id": 4, + "key": [ + { + "id": "K_SHIFT", + "text": "*Shift*", + "width": "160", + "sp": "1", + "nextlayer": "shift" + }, + { + "id": "K_oE2", + "text": "" + }, + { + "id": "K_Z", + "text": "ז" + }, + { + "id": "K_X", + "text": "צ" + }, + { + "id": "K_C", + "text": "שׂ" + }, + { + "id": "K_V", + "text": "שׁ" + }, + { + "id": "K_B", + "text": "ב" + }, + { + "id": "K_N", + "text": "נ" + }, + { + "id": "K_M", + "text": "מ" + }, + { + "id": "K_COMMA", + "text": "א" + }, + { + "id": "K_PERIOD", + "text": "." + }, + { + "id": "K_SLASH", + "text": "/" + }, + { + "id": "T_new_164", + "text": "", + "width": "10", + "sp": "10" + } + ] + }, + { + "id": 5, + "key": [ + { + "id": "K_LCONTROL", + "text": "alt", + "width": "130", + "sp": "1", + "nextlayer": "alt" + }, + { + "id": "K_LOPT", + "text": "*Menu*", + "width": "140", + "sp": "1" + }, + { + "id": "K_SPACE", + "text": "", + "width": "930" + }, + { + "id": "K_ENTER", + "text": "*Enter*", + "width": "145", + "sp": "1" + } + ] + } + ] + }, + { + "id": "shift", + "row": [ + { + "id": 1, + "key": [ + { + "id": "K_1", + "text": "!" + }, + { + "id": "K_2", + "text": "֘" + }, + { + "id": "K_3", + "text": "֨" + }, + { + "id": "K_4", + "text": "֜" + }, + { + "id": "K_5", + "text": "֞" + }, + { + "id": "K_6", + "text": "֔" + }, + { + "id": "K_7", + "text": "֬" + }, + { + "id": "K_8", + "text": "֝" + }, + { + "id": "K_9", + "text": "(" + }, + { + "id": "K_0", + "text": ")" + }, + { + "id": "K_HYPHEN", + "text": "_" + }, + { + "id": "K_EQUAL", + "text": "+" + }, + { + "id": "K_BKSP", + "text": "*BkSp*", + "width": "100", + "sp": "1" + } + ] + }, + { + "id": 2, + "key": [ + { + "id": "K_Q", + "text": "קּ", + "pad": "75" + }, + { + "id": "K_W", + "text": "וּ" + }, + { + "id": "K_E", + "text": "ֵ" + }, + { + "id": "K_R", + "text": "רּ" + }, + { + "id": "K_T", + "text": "תּ" + }, + { + "id": "K_Y", + "text": "יּ" + }, + { + "id": "K_U", + "text": "ֻ" + }, + { + "id": "K_I", + "text": "" + }, + { + "id": "K_O", + "text": "וֹ" + }, + { + "id": "K_P", + "text": "פּ" + }, + { + "id": "K_LBRKT", + "text": "{" + }, + { + "id": "K_RBRKT", + "text": "}" + }, + { + "id": "T_new_362", + "text": "", + "width": "10", + "sp": "10" + } + ] + }, + { + "id": 3, + "key": [ + { + "id": "K_BKQUOTE", + "text": "" + }, + { + "id": "K_A", + "text": "ָ" + }, + { + "id": "K_S", + "text": "סּ" + }, + { + "id": "K_D", + "text": "דּ" + }, + { + "id": "K_F", + "text": "טּ" + }, + { + "id": "K_G", + "text": "גּ" + }, + { + "id": "K_H", + "text": "הּ" + }, + { + "id": "K_J", + "text": "וַיּ" + }, + { + "id": "K_K", + "text": "כּ" + }, + { + "id": "K_L", + "text": "לּ" + }, + { + "id": "K_COLON", + "text": "״" + }, + { + "id": "K_QUOTE", + "text": "\"" + }, + { + "id": "K_BKSLASH", + "text": "|" + } + ] + }, + { + "id": 4, + "key": [ + { + "id": "K_SHIFT", + "text": "*Shift*", + "width": "160", + "sp": "1", + "nextlayer": "default" + }, + { + "id": "K_oE2", + "text": "" + }, + { + "id": "K_Z", + "text": "זּ" + }, + { + "id": "K_X", + "text": "צּ" + }, + { + "id": "K_C", + "text": "שּׂ" + }, + { + "id": "K_V", + "text": "שּׁ" + }, + { + "id": "K_B", + "text": "בּ" + }, + { + "id": "K_N", + "text": "נּ" + }, + { + "id": "K_M", + "text": "מּ" + }, + { + "id": "K_COMMA", + "text": "" + }, + { + "id": "K_PERIOD", + "text": "ּ" + }, + { + "id": "K_SLASH", + "text": "?" + }, + { + "id": "T_new_388", + "text": "", + "width": "10", + "sp": "10" + } + ] + }, + { + "id": 5, + "key": [ + { + "id": "K_LCONTROL", + "text": "alt", + "width": "130", + "sp": "1", + "nextlayer": "alt" + }, + { + "id": "K_LOPT", + "text": "*Menu*", + "width": "140", + "sp": "1" + }, + { + "id": "K_SPACE", + "text": "", + "width": "930" + }, + { + "id": "K_ENTER", + "text": "*Enter*", + "width": "145", + "sp": "1" + } + ] + } + ] + }, + { + "id": "alt", + "row": [ + { + "id": 1, + "key": [ + { + "id": "K_1", + "text": "֗" + }, + { + "id": "K_2", + "text": "֮" + }, + { + "id": "K_3", + "text": "֙" + }, + { + "id": "K_4", + "text": "֠" + }, + { + "id": "K_5", + "text": "֩" + }, + { + "id": "K_6", + "text": "֟" + }, + { + "id": "K_7", + "text": "֡" + }, + { + "id": "K_8", + "text": "֕" + }, + { + "id": "K_9", + "text": "֓" + }, + { + "id": "K_0", + "text": "֯" + }, + { + "id": "K_HYPHEN", + "text": "ֿ" + }, + { + "id": "K_EQUAL", + "text": "◌" + }, + { + "id": "K_BKSP", + "text": "*BkSp*", + "width": "100", + "sp": "1" + } + ] + }, + { + "id": 2, + "key": [ + { + "id": "K_Q", + "text": "", + "pad": "75" + }, + { + "id": "K_W", + "text": "" + }, + { + "id": "K_E", + "text": "ֱ" + }, + { + "id": "K_R", + "text": "" + }, + { + "id": "K_T", + "text": "" + }, + { + "id": "K_Y", + "text": "" + }, + { + "id": "K_U", + "text": "" + }, + { + "id": "K_I", + "text": "ֽ" + }, + { + "id": "K_O", + "text": "ֳ" + }, + { + "id": "K_P", + "text": "ף" + }, + { + "id": "K_LBRKT", + "text": "֔" + }, + { + "id": "K_RBRKT", + "text": "֒" + }, + { + "id": "T_new_138", + "text": "", + "width": "10", + "sp": "10" + } + ] + }, + { + "id": 3, + "key": [ + { + "id": "K_BKQUOTE", + "text": "" + }, + { + "id": "K_A", + "text": "ֲ" + }, + { + "id": "K_S", + "text": "" + }, + { + "id": "K_D", + "text": "" + }, + { + "id": "K_F", + "text": "" + }, + { + "id": "K_G", + "text": "•" + }, + { + "id": "K_H", + "text": "̊" + }, + { + "id": "K_J", + "text": "" + }, + { + "id": "K_K", + "text": "ך" + }, + { + "id": "K_L", + "text": "" + }, + { + "id": "K_COLON", + "text": "׃" + }, + { + "id": "K_QUOTE", + "text": "ׄ" + }, + { + "id": "K_BKSLASH", + "text": "֫" + } + ] + }, + { + "id": 4, + "key": [ + { + "id": "K_SHIFT", + "text": "*Shift*", + "width": "160", + "sp": "1", + "nextlayer": "shift", + "layer": "shift" + }, + { + "id": "K_oE2", + "text": "" + }, + { + "id": "K_Z", + "text": "" + }, + { + "id": "K_X", + "text": "ץ" + }, + { + "id": "K_C", + "text": "" + }, + { + "id": "K_V", + "text": "" + }, + { + "id": "K_B", + "text": "" + }, + { + "id": "K_N", + "text": "ן" + }, + { + "id": "K_M", + "text": "ם" + }, + { + "id": "K_COMMA", + "text": "֫" + }, + { + "id": "K_PERIOD", + "text": "̈" + }, + { + "id": "K_SLASH", + "text": "״" + }, + { + "id": "T_new_164", + "text": "", + "width": "10", + "sp": "10" + } + ] + }, + { + "id": 5, + "key": [ + { + "id": "K_LCONTROL", + "text": "default", + "width": "130", + "sp": "1", + "nextlayer": "default" + }, + { + "id": "K_LOPT", + "text": "*Menu*", + "width": "140", + "sp": "1" + }, + { + "id": "K_SPACE", + "text": "", + "width": "930" + }, + { + "id": "K_ENTER", + "text": "*Enter*", + "width": "145", + "sp": "1" + } + ] + } + ] + } + ] + }, + "phone": { + "displayUnderlying": false, + "layer": [ + { + "id": "default", + "row": [ + { + "id": 1, + "key": [ + { + "id": "K_1", + "text": "1" + }, + { + "id": "K_2", + "text": "2" + }, + { + "id": "K_3", + "text": "3" + }, + { + "id": "K_4", + "text": "4" + }, + { + "id": "K_5", + "text": "5" + }, + { + "id": "K_6", + "text": "6" + }, + { + "id": "K_7", + "text": "7" + }, + { + "id": "K_8", + "text": "8" + }, + { + "id": "K_9", + "text": "9" + }, + { + "id": "K_0", + "text": "0" + }, + { + "id": "K_HYPHEN", + "text": "־" + }, + { + "id": "K_EQUAL", + "text": "=" + }, + { + "id": "K_BKSP", + "text": "*BkSp*", + "width": "100", + "sp": "1" + } + ] + }, + { + "id": 2, + "key": [ + { + "id": "K_Q", + "text": "ק", + "pad": "75" + }, + { + "id": "K_W", + "text": "ו" + }, + { + "id": "K_E", + "text": "ֶ" + }, + { + "id": "K_R", + "text": "ר" + }, + { + "id": "K_T", + "text": "ת" + }, + { + "id": "K_Y", + "text": "י" + }, + { + "id": "K_U", + "text": "ע" + }, + { + "id": "K_I", + "text": "ִ" + }, + { + "id": "K_O", + "text": "ֹ" + }, + { + "id": "K_P", + "text": "פ" + }, + { + "id": "K_LBRKT", + "text": "[" + }, + { + "id": "K_RBRKT", + "text": "]" + }, + { + "id": "T_new_138", + "text": "", + "width": "10", + "sp": "10" + } + ] + }, + { + "id": 3, + "key": [ + { + "id": "K_BKQUOTE", + "text": "֑" + }, + { + "id": "K_A", + "text": "ַ" + }, + { + "id": "K_S", + "text": "ס" + }, + { + "id": "K_D", + "text": "ד" + }, + { + "id": "K_F", + "text": "ט" + }, + { + "id": "K_G", + "text": "ג" + }, + { + "id": "K_H", + "text": "ה" + }, + { + "id": "K_J", + "text": "ח" + }, + { + "id": "K_K", + "text": "כ" + }, + { + "id": "K_L", + "text": "ל" + }, + { + "id": "K_COLON", + "text": "ְ" + }, + { + "id": "K_QUOTE", + "text": "'" + }, + { + "id": "K_BKSLASH", + "text": "\\" + } + ] + }, + { + "id": 4, + "key": [ + { + "id": "K_SHIFT", + "text": "*Shift*", + "width": "160", + "sp": "1", + "nextlayer": "shift" + }, + { + "id": "K_oE2", + "text": "" + }, + { + "id": "K_Z", + "text": "ז" + }, + { + "id": "K_X", + "text": "צ" + }, + { + "id": "K_C", + "text": "שׂ" + }, + { + "id": "K_V", + "text": "שׁ" + }, + { + "id": "K_B", + "text": "ב" + }, + { + "id": "K_N", + "text": "נ" + }, + { + "id": "K_M", + "text": "מ" + }, + { + "id": "K_COMMA", + "text": "א" + }, + { + "id": "K_PERIOD", + "text": "." + }, + { + "id": "K_SLASH", + "text": "/" + }, + { + "id": "T_new_164", + "text": "", + "width": "10", + "sp": "10" + } + ] + }, + { + "id": 5, + "key": [ + { + "id": "K_LCONTROL", + "text": "alt", + "width": "130", + "sp": "1", + "nextlayer": "alt" + }, + { + "id": "K_LOPT", + "text": "*Menu*", + "width": "140", + "sp": "1" + }, + { + "id": "K_SPACE", + "text": "", + "width": "930" + }, + { + "id": "K_ENTER", + "text": "*Enter*", + "width": "145", + "sp": "1" + } + ] + } + ] + }, + { + "id": "shift", + "row": [ + { + "id": 1, + "key": [ + { + "id": "K_1", + "text": "!" + }, + { + "id": "K_2", + "text": "֘" + }, + { + "id": "K_3", + "text": "֨" + }, + { + "id": "K_4", + "text": "֜" + }, + { + "id": "K_5", + "text": "֞" + }, + { + "id": "K_6", + "text": "֔" + }, + { + "id": "K_7", + "text": "֬" + }, + { + "id": "K_8", + "text": "֝" + }, + { + "id": "K_9", + "text": "(" + }, + { + "id": "K_0", + "text": ")" + }, + { + "id": "K_HYPHEN", + "text": "_" + }, + { + "id": "K_EQUAL", + "text": "+" + }, + { + "id": "K_BKSP", + "text": "*BkSp*", + "width": "100", + "sp": "1" + } + ] + }, + { + "id": 2, + "key": [ + { + "id": "K_Q", + "text": "קּ", + "pad": "75" + }, + { + "id": "K_W", + "text": "וּ" + }, + { + "id": "K_E", + "text": "ֵ" + }, + { + "id": "K_R", + "text": "רּ" + }, + { + "id": "K_T", + "text": "תּ" + }, + { + "id": "K_Y", + "text": "יּ" + }, + { + "id": "K_U", + "text": "ֻ" + }, + { + "id": "K_I", + "text": "" + }, + { + "id": "K_O", + "text": "וֹ" + }, + { + "id": "K_P", + "text": "פּ" + }, + { + "id": "K_LBRKT", + "text": "{" + }, + { + "id": "K_RBRKT", + "text": "}" + }, + { + "id": "T_new_362", + "text": "", + "width": "10", + "sp": "10" + } + ] + }, + { + "id": 3, + "key": [ + { + "id": "K_BKQUOTE", + "text": "" + }, + { + "id": "K_A", + "text": "ָ" + }, + { + "id": "K_S", + "text": "סּ" + }, + { + "id": "K_D", + "text": "דּ" + }, + { + "id": "K_F", + "text": "טּ" + }, + { + "id": "K_G", + "text": "גּ" + }, + { + "id": "K_H", + "text": "הּ" + }, + { + "id": "K_J", + "text": "וַיּ" + }, + { + "id": "K_K", + "text": "כּ" + }, + { + "id": "K_L", + "text": "לּ" + }, + { + "id": "K_COLON", + "text": "״" + }, + { + "id": "K_QUOTE", + "text": "\"" + }, + { + "id": "K_BKSLASH", + "text": "|" + } + ] + }, + { + "id": 4, + "key": [ + { + "id": "K_SHIFT", + "text": "*Shift*", + "width": "160", + "sp": "1", + "nextlayer": "default" + }, + { + "id": "K_oE2", + "text": "" + }, + { + "id": "K_Z", + "text": "זּ" + }, + { + "id": "K_X", + "text": "צּ" + }, + { + "id": "K_C", + "text": "שּׂ" + }, + { + "id": "K_V", + "text": "שּׁ" + }, + { + "id": "K_B", + "text": "בּ" + }, + { + "id": "K_N", + "text": "נּ" + }, + { + "id": "K_M", + "text": "מּ" + }, + { + "id": "K_COMMA", + "text": "" + }, + { + "id": "K_PERIOD", + "text": "ּ" + }, + { + "id": "K_SLASH", + "text": "?" + }, + { + "id": "T_new_388", + "text": "", + "width": "10", + "sp": "10" + } + ] + }, + { + "id": 5, + "key": [ + { + "id": "K_LCONTROL", + "text": "alt", + "width": "130", + "sp": "1", + "nextlayer": "alt" + }, + { + "id": "K_LOPT", + "text": "*Menu*", + "width": "140", + "sp": "1" + }, + { + "id": "K_SPACE", + "text": "", + "width": "930" + }, + { + "id": "K_ENTER", + "text": "*Enter*", + "width": "145", + "sp": "1" + } + ] + } + ] + }, + { + "id": "alt", + "row": [ + { + "id": 1, + "key": [ + { + "id": "K_1", + "text": "֗" + }, + { + "id": "K_2", + "text": "֮" + }, + { + "id": "K_3", + "text": "֙" + }, + { + "id": "K_4", + "text": "֠" + }, + { + "id": "K_5", + "text": "֩" + }, + { + "id": "K_6", + "text": "֟" + }, + { + "id": "K_7", + "text": "֡" + }, + { + "id": "K_8", + "text": "֕" + }, + { + "id": "K_9", + "text": "֓" + }, + { + "id": "K_0", + "text": "֯" + }, + { + "id": "K_HYPHEN", + "text": "ֿ" + }, + { + "id": "K_EQUAL", + "text": "◌" + }, + { + "id": "K_BKSP", + "text": "*BkSp*", + "width": "100", + "sp": "1" + } + ] + }, + { + "id": 2, + "key": [ + { + "id": "K_Q", + "text": "", + "pad": "75" + }, + { + "id": "K_W", + "text": "" + }, + { + "id": "K_E", + "text": "ֱ" + }, + { + "id": "K_R", + "text": "" + }, + { + "id": "K_T", + "text": "" + }, + { + "id": "K_Y", + "text": "" + }, + { + "id": "K_U", + "text": "" + }, + { + "id": "K_I", + "text": "ֽ" + }, + { + "id": "K_O", + "text": "ֳ" + }, + { + "id": "K_P", + "text": "ף" + }, + { + "id": "K_LBRKT", + "text": "֔" + }, + { + "id": "K_RBRKT", + "text": "֒" + }, + { + "id": "T_new_138", + "text": "", + "width": "10", + "sp": "10" + } + ] + }, + { + "id": 3, + "key": [ + { + "id": "K_BKQUOTE", + "text": "" + }, + { + "id": "K_A", + "text": "ֲ" + }, + { + "id": "K_S", + "text": "" + }, + { + "id": "K_D", + "text": "" + }, + { + "id": "K_F", + "text": "" + }, + { + "id": "K_G", + "text": "•" + }, + { + "id": "K_H", + "text": "̊" + }, + { + "id": "K_J", + "text": "" + }, + { + "id": "K_K", + "text": "ך" + }, + { + "id": "K_L", + "text": "" + }, + { + "id": "K_COLON", + "text": "׃" + }, + { + "id": "K_QUOTE", + "text": "ׄ" + }, + { + "id": "K_BKSLASH", + "text": "֫" + } + ] + }, + { + "id": 4, + "key": [ + { + "id": "K_SHIFT", + "text": "*Shift*", + "width": "160", + "sp": "1", + "nextlayer": "shift", + "layer": "shift" + }, + { + "id": "K_oE2", + "text": "" + }, + { + "id": "K_Z", + "text": "" + }, + { + "id": "K_X", + "text": "ץ" + }, + { + "id": "K_C", + "text": "" + }, + { + "id": "K_V", + "text": "" + }, + { + "id": "K_B", + "text": "" + }, + { + "id": "K_N", + "text": "ן" + }, + { + "id": "K_M", + "text": "ם" + }, + { + "id": "K_COMMA", + "text": "֫" + }, + { + "id": "K_PERIOD", + "text": "̈" + }, + { + "id": "K_SLASH", + "text": "״" + }, + { + "id": "T_new_164", + "text": "", + "width": "10", + "sp": "10" + } + ] + }, + { + "id": 5, + "key": [ + { + "id": "K_LCONTROL", + "text": "default", + "width": "130", + "sp": "1", + "nextlayer": "default" + }, + { + "id": "K_LOPT", + "text": "*Menu*", + "width": "140", + "sp": "1" + }, + { + "id": "K_SPACE", + "text": "", + "width": "930" + }, + { + "id": "K_ENTER", + "text": "*Enter*", + "width": "145", + "sp": "1" + } + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/alephwithbeth.kmn b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/alephwithbeth.kmn new file mode 100644 index 0000000000..d4a2b9a993 --- /dev/null +++ b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/alephwithbeth.kmn @@ -0,0 +1,215 @@ +store(&TARGETS) 'any' +store(&NAME) 'Aleph With Beth' +store(&BITMAP) 'alephwithbeth.ico' +store(&VISUALKEYBOARD) 'alephwithbeth.kvks' + +store(letters-with-final-forms) "כמנפצ" +store(sofit-forms-of-letters) "ךםןףץ" +store(presentation-dageshed-letters-with-final-forms) "כּפּ" +store(presentation-dageshed-sofit-forms-of-letters) "ךּףּ" +store(dagesh) 'ּ' +c store(vowels) "ְֱֲֳִֵֶַׇׇָֹֻׅ" + +store(normal-dageshed-letters-with-final-forms) "מּנּצּ" +store(normal-undageshed-sofit-forms-of-letters) "םןץ" + +store(punctuation) U+0591 .. U+05AF U+05BD .. U+05BF U+05C0 U+05C4 +store(whitespace) [K_SPACE] [K_ENTER] [K_TAB] + +store(&KMW_RTL) '1' +store(&LAYOUTFILE) 'alephwithbeth.keyman-touch-layout' +store(©RIGHT) '© Cameron Briggs' +store(&KEYBOARDVERSION) '1.0.6' +store(&KMW_HELPTEXT) 'Aleph With Beth' + +begin Unicode > use(main) + +group(main) using keys ++ [SHIFT K_I] > nul ++ [ALT K_BKQUOTE] > nul ++ [ALT K_B] > nul ++ [ALT K_V] > nul ++ [ALT K_C] > nul ++ [ALT K_Z] > nul ++ [ALT K_L] > nul ++ [ALT K_J] > nul ++ [ALT K_F] > nul ++ [ALT K_D] > nul ++ [ALT K_S] > nul ++ [ALT K_U] > nul ++ [ALT K_Y] > nul ++ [ALT K_T] > nul ++ [ALT K_R] > nul ++ [ALT K_W] > nul ++ [ALT K_Q] > nul ++ [SHIFT K_BKSLASH] > '|' ++ [SHIFT K_EQUAL] > '+' ++ [SHIFT K_HYPHEN] > '_' ++ [SHIFT K_0] > ')' ++ [SHIFT K_9] > '(' ++ [SHIFT K_SLASH] > '?' ++ [SHIFT K_QUOTE] > '"' ++ [SHIFT K_BKQUOTE] > nul ++ [K_EQUAL] > '=' ++ [K_HYPHEN] > '־' ++ [K_0] > '0' ++ [K_9] > '9' ++ [K_8] > '8' ++ [K_7] > '7' ++ [K_6] > '6' ++ [K_5] > '5' ++ [K_4] > '4' ++ [K_3] > '3' ++ [K_2] > '2' ++ [K_1] > '1' ++ [K_PERIOD] > '.' ++ [K_SLASH] > '/' ++ [K_QUOTE] > U+0027 ++ [SHIFT K_COLON] > '״' ++ [ALT K_A] > 'ֲ' ++ [SHIFT K_RBRKT] > '}' ++ [SHIFT K_LBRKT] > '{' ++ [K_BKSLASH] > '\' ++ [K_RBRKT] > ']' ++ [K_LBRKT] > '[' ++ [SHIFT K_R] > 'רּ' ++ [SHIFT CTRL K_SPACE] > ' ' ++ [SHIFT K_SPACE] > ' ' ++ [K_SPACE] > ' ' ++ [ALT K_SPACE] > ' ' ++ [SHIFT K_8] > '֝' ++ [SHIFT K_7] > '֬' ++ [SHIFT K_6] > '֔' ++ [SHIFT K_5] > '֞' ++ [SHIFT K_4] > '֜' ++ [SHIFT K_3] > '֨' ++ [SHIFT K_2] > '֘' ++ [SHIFT K_1] > '!' ++ [ALT K_SLASH] > '״' ++ [ALT K_PERIOD] > '̈' ++ [ALT K_COMMA] > '֫' ++ [ALT K_QUOTE] > 'ׄ' ++ [ALT K_COLON] > '׃' ++ [ALT K_H] > '̊' ++ [ALT K_G] > '•' ++ [ALT K_BKSLASH] > '֫' ++ [ALT K_RBRKT] > '֒' ++ [ALT K_LBRKT] > '֔' ++ [ALT K_I] > 'ֽ' ++ [ALT K_O] > 'ֳ' ++ [ALT K_E] > 'ֱ' ++ [ALT K_EQUAL] > '◌' ++ [ALT K_HYPHEN] > 'ֿ' ++ [ALT K_0] > '֯' ++ [ALT K_9] > '֓' ++ [ALT K_8] > '֕' ++ [ALT K_7] > '֡' ++ [ALT K_6] > '֟' ++ [ALT K_5] > '֩' ++ [ALT K_4] > '֠' ++ [ALT K_3] > '֙' ++ [ALT K_2] > '֮' ++ [ALT K_1] > '֗' ++ [ALT K_N] > 'ן' ++ [ALT K_P] > 'ף' ++ [ALT K_K] > 'ך' ++ [ALT K_M] > 'ם' ++ [ALT K_X] > 'ץ' ++ [SHIFT K_PERIOD] > 'ּ' ++ [SHIFT K_COMMA] > nul ++ [SHIFT K_M] > 'מּ' ++ [SHIFT K_N] > 'נּ' ++ [SHIFT K_B] > 'בּ' ++ [SHIFT K_V] > 'שּׁ' ++ [SHIFT K_C] > 'שּׂ' ++ [SHIFT K_X] > 'צּ' ++ [SHIFT K_Z] > 'זּ' ++ [SHIFT K_L] > 'לּ' ++ [SHIFT K_K] > 'כּ' ++ [SHIFT K_J] > 'וַיּ' ++ [SHIFT K_H] > 'הּ' ++ [SHIFT K_G] > 'גּ' ++ [SHIFT K_F] > 'טּ' ++ [SHIFT K_D] > 'דּ' ++ [SHIFT K_S] > 'סּ' ++ [SHIFT K_A] > 'ָ' ++ [SHIFT K_P] > 'פּ' ++ [SHIFT K_O] > 'וֹ' ++ [SHIFT K_U] > 'ֻ' ++ [SHIFT K_Y] > 'יּ' ++ [SHIFT K_T] > 'תּ' ++ [SHIFT K_E] > 'ֵ' ++ [SHIFT K_W] > 'וּ' ++ [SHIFT K_Q] > 'קּ' ++ [K_BKQUOTE] > '֑' ++ [K_COMMA] > 'א' ++ [K_M] > 'מ' ++ [K_N] > 'נ' ++ [K_B] > 'ב' ++ [K_V] > 'שׁ' ++ [K_C] > 'שׂ' ++ [K_X] > 'צ' ++ [K_Z] > 'ז' ++ [K_COLON] > 'ְ' ++ [K_L] > 'ל' ++ [K_K] > 'כ' ++ [K_J] > 'ח' ++ [K_H] > 'ה' ++ [K_G] > 'ג' ++ [K_F] > 'ט' ++ [K_D] > 'ד' ++ [K_S] > 'ס' ++ [K_A] > 'ַ' ++ [K_P] > 'פ' ++ [K_O] > 'ֹ' ++ [K_I] > 'ִ' ++ [K_U] > 'ע' ++ [K_Y] > 'י' ++ [K_T] > 'ת' ++ [K_R] > 'ר' ++ [K_E] > 'ֶ' ++ [K_W] > 'ו' ++ [K_Q] > 'ק' +c Transform final letters +any(letters-with-final-forms) + any(whitespace) > index(sofit-forms-of-letters,1) + +c Transform final letters with a dagesh +any(letters-with-final-forms) $dagesh + any(whitespace) > index(sofit-forms-of-letters,1) $dagesh +any(presentation-dageshed-letters-with-final-forms) + any(whitespace) > index(presentation-dageshed-sofit-forms-of-letters,1) +any(normal-dageshed-letters-with-final-forms) + any(whitespace) > index(normal-undageshed-sofit-forms-of-letters,1) $dagesh + +c Transform final letters with a vowel +c any(letters-with-final-forms) any(vowels) + any(whitespace) > index(sofit-forms-of-letters,1) index(vowels,2) + +c Transform final letters with a vowel and a dagesh +c any(letters-with-final-forms) any(vowels) $dagesh + any(whitespace) > index(sofit-forms-of-letters,1) index(vowels,2) $dagesh +c any(letters-with-final-forms) $dagesh any(vowels) + any(whitespace) > index(sofit-forms-of-letters,1) $dagesh index(vowels,3) +c any(presentation-dageshed-letters-with-final-forms) any(vowels) + any(whitespace) > index(presentation-dageshed-sofit-forms-of-letters,1) index(vowels,2) +c any(normal-dageshed-letters-with-final-forms) any(vowels) + any(whitespace) > index(normal-undageshed-sofit-forms-of-letters,1) $dagesh index(vowels,2) + +c Transform final letters with punctuation +any(letters-with-final-forms) any(punctuation) + any(whitespace) > index(sofit-forms-of-letters,1) index(punctuation,2) + +c Transform final letters with punctuation and a dagesh +any(letters-with-final-forms) any(punctuation) $dagesh + any(whitespace) > index(sofit-forms-of-letters,1) index(punctuation,2) $dagesh +any(letters-with-final-forms) $dagesh any(punctuation) + any(whitespace) > index(sofit-forms-of-letters,1) $dagesh index(punctuation,3) +any(presentation-dageshed-letters-with-final-forms) any(punctuation) + any(whitespace) > index(presentation-dageshed-sofit-forms-of-letters,1) index(punctuation,2) +any(normal-dageshed-letters-with-final-forms) any(punctuation) + any(whitespace) > index(normal-undageshed-sofit-forms-of-letters,1) $dagesh index(punctuation,2) + +c Transfrom final letters with a punctuation and a vowel +c any(letters-with-final-forms) any(punctuation) any(vowels) + any(whitespace) > index(sofit-forms-of-letters,1) index(punctuation,2) index(vowels,3) +c any(letters-with-final-forms) any(vowels) any(punctuation) + any(whitespace) > index(sofit-forms-of-letters,1) index(vowels,2) index(punctuation,3) + +c Transform final letters with punctuation, a vowel, and a dagesh (phew!) +c any(letters-with-final-forms) any(punctuation) any(vowels) $dagesh + any(whitespace) > index(sofit-forms-of-letters,1) index(punctuation,2) index(vowels,3) $dagesh +c any(letters-with-final-forms) any(punctuation) $dagesh any(vowels) + any(whitespace) > index(sofit-forms-of-letters,1) index(punctuation,2) $dagesh index(vowels,4) +c any(letters-with-final-forms) any(vowels) any(punctuation) $dagesh + any(whitespace) > index(sofit-forms-of-letters,1) index(vowels,2) index(punctuation,3) $dagesh +c any(letters-with-final-forms) any(vowels) $dagesh any(punctuation) + any(whitespace) > index(sofit-forms-of-letters,1) index(vowels,2) $dagesh index(punctuation,4) +c any(letters-with-final-forms) $dagesh any(punctuation) any(vowels) + any(whitespace) > index(sofit-forms-of-letters,1) $dagesh index(punctuation,3) index(vowels,4) +c any(letters-with-final-forms) $dagesh any(vowels) any(punctuation) + any(whitespace) > index(sofit-forms-of-letters,1) $dagesh index(vowels,3) index(punctuation,4) + + +c any(presentation-dageshed-letters-with-final-forms) any(punctuation) any(vowels) + any(whitespace) > index(presentation-dageshed-sofit-forms-of-letters,1) index(punctuation,2) index(vowels,3) +c any(presentation-dageshed-letters-with-final-forms) any(vowels) any(punctuation) + any(whitespace) > index(presentation-dageshed-sofit-forms-of-letters,1) index(vowels,2) index(punctuation,3) +c any(normal-dageshed-letters-with-final-forms) any(punctuation) any(vowels) + any(whitespace) > index(normal-undageshed-sofit-forms-of-letters,1) $dagesh index(punctuation,2) index(vowels,3) +c any(normal-dageshed-letters-with-final-forms) any(vowels) any(punctuation) + any(whitespace) > index(normal-undageshed-sofit-forms-of-letters,1) $dagesh index(vowels,2) index(punctuation,3) diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/alephwithbeth.kps b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/alephwithbeth.kps new file mode 100644 index 0000000000..9c2b0a1251 --- /dev/null +++ b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/alephwithbeth.kps @@ -0,0 +1,95 @@ + + + + 17.0.325.0 + 7.0 + + + + readme.htm + ..\LICENSE.md + ..\..\another-folder\welcome\welcome.htm + + + + + + + + + + + Aleph With Beth + © Cameron Briggs + Cameron Briggs + https://freehebrew.online + alephwithbeth keyboard + + + + readme.htm + File readme.htm + 0 + .htm + + + ..\..\another-folder\welcome\welcome.htm + File welcome.htm + 0 + .htm + + + ..\build\alephwithbeth.js + File alephwithbeth.js + 0 + .js + + + ..\build\alephwithbeth.kvk + File alephwithbeth.kvk + 0 + .kvk + + + ..\build\alephwithbeth.kmx + Keyboard alephwithbeth + 0 + .kmx + + + ..\..\another-folder\welcome\KeyboardLayout.png + File KeyboardLayout.png + 0 + .png + + + ..\..\another-folder\welcome\KeyboardLayoutAlt.png + File KeyboardLayoutAlt.png + 0 + .png + + + ..\..\another-folder\welcome\KeyboardLayoutShift.png + File KeyboardLayoutShift.png + 0 + .png + + + ..\LICENSE.md + + 0 + .md + + + + + Aleph With Beth + alephwithbeth + 1.0.4 + + Hebrew + + + + + diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/alephwithbeth.kvks b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/alephwithbeth.kvks new file mode 100644 index 0000000000..6b3cf3eeb9 --- /dev/null +++ b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/alephwithbeth.kvks @@ -0,0 +1,138 @@ + + +
+ 10.0 + alephwithbeth + +
+ + + ! + " + ֨ + ֜ + ֞ + ֬ + ( + ) + ֝ + + + ״ + ּ + ? + ֘ + ָ + בּ + שּׂ + דּ + ֵ + טּ + גּ + הּ + וַיּ + כּ + לּ + מּ + נּ + וֹ + פּ + קּ + רּ + סּ + תּ + ֻ + שּׁ + וּ + צּ + יּ + זּ + ֔ + _ + { + | + } + + + ' + א + . + / + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + ְ + = + [ + \ + ] + ֑ + ַ + ב + שׂ + ד + ֶ + ט + ג + ה + ִ + ח + כ + ל + מ + נ + ֹ + פ + ק + ר + ס + ת + ע + שׁ + ו + צ + י + ז + ־ + 0 + + + ׄ + ֫ + ֿ + ̈ + ״ + ֯ + ֗ + ֮ + ֙ + ֠ + ֩ + ֟ + ֡ + ֕ + ֓ + ׃ + ◌ + ֔ + ֫ + ֒ + ֲ + ֱ + • + ̊ + ֽ + ך + ם + ן + ֳ + ף + ץ + + +
diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/help/KeyboardLayout.png b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/help/KeyboardLayout.png new file mode 100644 index 0000000000..d0998e288b Binary files /dev/null and b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/help/KeyboardLayout.png differ diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/help/KeyboardLayoutAlt.png b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/help/KeyboardLayoutAlt.png new file mode 100644 index 0000000000..f740eec1e3 Binary files /dev/null and b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/help/KeyboardLayoutAlt.png differ diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/help/KeyboardLayoutShift.png b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/help/KeyboardLayoutShift.png new file mode 100644 index 0000000000..9b243d4dd2 Binary files /dev/null and b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/help/KeyboardLayoutShift.png differ diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/help/alephwithbeth.php b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/help/alephwithbeth.php new file mode 100644 index 0000000000..48a2c2e7c8 --- /dev/null +++ b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/help/alephwithbeth.php @@ -0,0 +1,22 @@ + + + +

Start Using Aleph with Beth

+ +

Desktop Layout

+ +

Base layout

+Keyboard layout +

Layout when shift is pressed

+Keyboard layout (when shift key is pressed) +

Layout when alt is pressed

+Keyboard layout (when alt key is pressed) + diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/readme.htm b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/readme.htm new file mode 100644 index 0000000000..f187388c1e --- /dev/null +++ b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/readme.htm @@ -0,0 +1,22 @@ + + + + + + alephwithbeth + + + + +

Aleph With Beth

+ +

A Hebrew keyboard designed to intuitively match the standard QWERTY layout.

+ +

© Cameron Briggs

+ + + diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/another-folder/welcome/KeyboardLayout.png b/developer/src/kmc-copy/test/fixtures/projects/external/another-folder/welcome/KeyboardLayout.png new file mode 100644 index 0000000000..d0998e288b Binary files /dev/null and b/developer/src/kmc-copy/test/fixtures/projects/external/another-folder/welcome/KeyboardLayout.png differ diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/another-folder/welcome/KeyboardLayoutAlt.png b/developer/src/kmc-copy/test/fixtures/projects/external/another-folder/welcome/KeyboardLayoutAlt.png new file mode 100644 index 0000000000..f740eec1e3 Binary files /dev/null and b/developer/src/kmc-copy/test/fixtures/projects/external/another-folder/welcome/KeyboardLayoutAlt.png differ diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/another-folder/welcome/KeyboardLayoutShift.png b/developer/src/kmc-copy/test/fixtures/projects/external/another-folder/welcome/KeyboardLayoutShift.png new file mode 100644 index 0000000000..9b243d4dd2 Binary files /dev/null and b/developer/src/kmc-copy/test/fixtures/projects/external/another-folder/welcome/KeyboardLayoutShift.png differ diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/another-folder/welcome/welcome.htm b/developer/src/kmc-copy/test/fixtures/projects/external/another-folder/welcome/welcome.htm new file mode 100644 index 0000000000..717e7bedeb --- /dev/null +++ b/developer/src/kmc-copy/test/fixtures/projects/external/another-folder/welcome/welcome.htm @@ -0,0 +1,28 @@ + + + + + + Start Using alephwithbeth + + + + +

Start Using Aleph With Beth

+ +

Keyboard Layouts

+ +

Base layout

+Keyboard layout +

Layout when shift is pressed

+Keyboard layout (when shift key is pressed) +

Layout when alt is pressed

+Keyboard layout (when alt key is pressed) + + + diff --git a/developer/src/kmc-generate/build.sh b/developer/src/kmc-generate/build.sh index e99e8288c2..7db46acd56 100755 --- a/developer/src/kmc-generate/build.sh +++ b/developer/src/kmc-generate/build.sh @@ -13,6 +13,7 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" builder_describe "Build Keyman kmc-generate module" \ "@/common/web/keyman-version" \ + "@/common/web/langtags" \ "@/common/web/types" \ "@/developer/src/common/web/test-helpers" \ "@/developer/src/common/web/utils" \ diff --git a/developer/src/kmc-generate/package.json b/developer/src/kmc-generate/package.json index ba488ff575..daf4f92922 100644 --- a/developer/src/kmc-generate/package.json +++ b/developer/src/kmc-generate/package.json @@ -27,7 +27,8 @@ "dependencies": { "@keymanapp/common-types": "*", "@keymanapp/developer-utils": "*", - "@keymanapp/keyman-version": "*" + "@keymanapp/keyman-version": "*", + "@keymanapp/langtags": "*" }, "devDependencies": { "@keymanapp/developer-test-helpers": "*", diff --git a/developer/src/kmc-generate/src/abstract-generator.ts b/developer/src/kmc-generate/src/abstract-generator.ts index 5a6ed46cc0..3c56c00e07 100644 --- a/developer/src/kmc-generate/src/abstract-generator.ts +++ b/developer/src/kmc-generate/src/abstract-generator.ts @@ -4,7 +4,7 @@ * Base interfaces and classes for generating Keyman Developer source files */ -import { CompilerCallbacks, CompilerLogLevel, KeymanCompilerArtifact, KeymanCompilerArtifacts, KeymanCompilerResult } from "@keymanapp/developer-utils"; +import { CompilerBaseOptions, CompilerCallbacks, KeymanCompilerArtifact, KeymanCompilerArtifacts, KeymanCompilerResult } from "@keymanapp/developer-utils"; import { GeneratorMessages } from './generator-messages.js'; import { KeymanTargets } from "@keymanapp/common-types"; @@ -13,12 +13,7 @@ import { KeymanTargets } from "@keymanapp/common-types"; * @public * Options for the Keyman Developer project generator */ -export interface GeneratorOptions /* not inheriting from CompilerBaseOptions */ { - /** - * Reporting level to console, used by NodeCompilerCallbacks (not used in - * compiler modules; all messages are still reported to the internal log) - */ - logLevel?: CompilerLogLevel; +export interface GeneratorOptions extends CompilerBaseOptions { /** * identifier (basename) of the keyboard or model */ diff --git a/developer/src/kmc-generate/src/basic-generator.ts b/developer/src/kmc-generate/src/basic-generator.ts index fe5a5cd304..9975615523 100644 --- a/developer/src/kmc-generate/src/basic-generator.ts +++ b/developer/src/kmc-generate/src/basic-generator.ts @@ -6,6 +6,7 @@ import { KeymanTargets } from "@keymanapp/common-types"; import KEYMAN_VERSION from "@keymanapp/keyman-version"; +import { getLangtagByTag } from "@keymanapp/langtags"; import { AbstractGenerator, GeneratorArtifacts } from "./abstract-generator.js"; import { GeneratorMessages } from "./generator-messages.js"; @@ -26,13 +27,13 @@ export class BasicGenerator extends AbstractGenerator { protected preGenerate() { const dt = new Date(); - this.languageTags = this.options.languageTags.length + this.languageTags = this.options.languageTags?.length ? this.options.languageTags.map(tag => new Intl.Locale(tag).minimize().toString()) : [this.DEFAULT_LOCALE]; // Verify that targets passed in are valid let failed = false; - for(const target of this.options.targets) { + for(const target of this.options.targets ?? []) { if(!KeymanTargets.AllKeymanTargets.includes(target)) { this.callbacks.reportMessage(GeneratorMessages.Error_InvalidTarget({target})); failed = true; @@ -42,7 +43,7 @@ export class BasicGenerator extends AbstractGenerator { return false; } - if(this.options.targets.includes(KeymanTargets.KeymanTarget.any) || this.options.targets.length == 0) { + if(this.options.targets?.includes(KeymanTargets.KeymanTarget.any) || !this.options.targets?.length) { this.resolvedTargets = KeymanTargets.AllKeymanTargets.filter(target => target != KeymanTargets.KeymanTarget.any); } else { this.resolvedTargets = [].concat(this.options.targets); @@ -72,8 +73,12 @@ export class BasicGenerator extends AbstractGenerator { } private getLanguageName(tag: string) { - // TODO-GENERATE: probably need to use langtags.json - return new Intl.Locale(tag).language; + const langtag = getLangtagByTag(tag); + if(!langtag) { + // Fallback to Intl.Locale, probably will be undefined + return new Intl.Locale(tag).language; + } + return langtag.name; } private generateLanguageListForPackage(): string { diff --git a/developer/src/kmc-generate/src/generator-messages.ts b/developer/src/kmc-generate/src/generator-messages.ts index 6c9ccfa7d8..185f68cbe6 100644 --- a/developer/src/kmc-generate/src/generator-messages.ts +++ b/developer/src/kmc-generate/src/generator-messages.ts @@ -18,8 +18,9 @@ const SevFatal = CompilerErrorSeverity.Fatal | Namespace; */ export class GeneratorMessages { static FATAL_UnexpectedException = SevFatal | 0x0001; - static Fatal_UnexpectedException = (o:{e: any}) => m( - this.FATAL_UnexpectedException, null, o.e ?? 'unknown error' + static Fatal_UnexpectedException = (o:{e: any}) => CompilerMessageSpecWithException( + this.FATAL_UnexpectedException, null, + o.e ?? 'unknown error', ); static INFO_GeneratingProject = SevInfo | 0x0002; diff --git a/developer/src/kmc-generate/test/fixtures/keyman-keyboard/sample/source/sample.kps b/developer/src/kmc-generate/test/fixtures/keyman-keyboard/sample/source/sample.kps index 62931d3bcf..1e05660d2a 100644 --- a/developer/src/kmc-generate/test/fixtures/keyman-keyboard/sample/source/sample.kps +++ b/developer/src/kmc-generate/test/fixtures/keyman-keyboard/sample/source/sample.kps @@ -39,7 +39,7 @@ sample 1.0 - en + English diff --git a/developer/src/kmc-generate/test/fixtures/ldml-keyboard/sample/source/sample.kps b/developer/src/kmc-generate/test/fixtures/ldml-keyboard/sample/source/sample.kps index 62931d3bcf..1e05660d2a 100644 --- a/developer/src/kmc-generate/test/fixtures/ldml-keyboard/sample/source/sample.kps +++ b/developer/src/kmc-generate/test/fixtures/ldml-keyboard/sample/source/sample.kps @@ -39,7 +39,7 @@ sample 1.0 - en + English diff --git a/developer/src/kmc-generate/test/fixtures/lexical-model/sample.en.sample/source/sample.en.sample.model.kps b/developer/src/kmc-generate/test/fixtures/lexical-model/sample.en.sample/source/sample.en.sample.model.kps index 5020fd3eb0..76e9069e72 100644 --- a/developer/src/kmc-generate/test/fixtures/lexical-model/sample.en.sample/source/sample.en.sample.model.kps +++ b/developer/src/kmc-generate/test/fixtures/lexical-model/sample.en.sample/source/sample.en.sample.model.kps @@ -36,7 +36,7 @@ Sample Project sample.en.sample - en + English diff --git a/developer/src/kmc-generate/tsconfig.json b/developer/src/kmc-generate/tsconfig.json index 1fc0a249be..bf711ca14a 100644 --- a/developer/src/kmc-generate/tsconfig.json +++ b/developer/src/kmc-generate/tsconfig.json @@ -8,6 +8,7 @@ "paths": { "@keymanapp/keyman-version": ["../../../common/web/keyman-version/keyman-version.mts"], + "@keymanapp/langtags": ["../../../common/web/langtags/src/main"], "@keymanapp/common-types": ["../../../common/web/types/src/main"], }, @@ -20,6 +21,7 @@ ], "references": [ { "path": "../../../common/web/keyman-version" }, + { "path": "../../../common/web/langtags" }, { "path": "../../../common/web/types/" }, ] } diff --git a/developer/src/kmc-keyboard-info/.gitignore b/developer/src/kmc-keyboard-info/.gitignore index f118f231af..abbfb195cb 100644 --- a/developer/src/kmc-keyboard-info/.gitignore +++ b/developer/src/kmc-keyboard-info/.gitignore @@ -1,2 +1 @@ -src/imports/ test/fixtures/khmer_angkor/build/actual.keyboard_info \ No newline at end of file diff --git a/developer/src/kmc-keyboard-info/build.sh b/developer/src/kmc-keyboard-info/build.sh index acd6bd65de..87902b6876 100755 --- a/developer/src/kmc-keyboard-info/build.sh +++ b/developer/src/kmc-keyboard-info/build.sh @@ -8,6 +8,7 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" . "$KEYMAN_ROOT/resources/shellHelperFunctions.sh" builder_describe "Build Keyman kmc keyboard-info Compiler module" \ + "@/common/web/langtags" \ "@/common/web/types" \ "@/developer/src/common/web/utils" \ "@/developer/src/kmc-package" \ @@ -21,23 +22,16 @@ builder_describe "Build Keyman kmc keyboard-info Compiler module" \ "--dry-run,-n don't actually publish, just dry run" builder_describe_outputs \ - configure /developer/src/kmc-keyboard-info/src/imports/langtags.js \ + configure /node_modules \ build /developer/src/kmc-keyboard-info/build/src/index.js \ api /developer/build/api/kmc-keyboard-info.api.json builder_parse "$@" -function do_configure() { - verify_npm_setup - mkdir -p src/imports - echo 'export default ' > src/imports/langtags.js - cat "$KEYMAN_ROOT/resources/standards-data/langtags/langtags.json" >> src/imports/langtags.js -} - #------------------------------------------------------------------------------------------------------------------- builder_run_action clean rm -rf ./build/ ./tsconfig.tsbuildinfo -builder_run_action configure do_configure +builder_run_action configure verify_npm_setup builder_run_action build tsc --build builder_run_action api api-extractor run --local --verbose builder_run_action test builder_do_typescript_tests diff --git a/developer/src/kmc-keyboard-info/package.json b/developer/src/kmc-keyboard-info/package.json index 45e1001501..b97cdcc22a 100644 --- a/developer/src/kmc-keyboard-info/package.json +++ b/developer/src/kmc-keyboard-info/package.json @@ -25,7 +25,8 @@ "dependencies": { "@keymanapp/common-types": "*", "@keymanapp/developer-utils": "*", - "@keymanapp/kmc-package": "*" + "@keymanapp/kmc-package": "*", + "@keymanapp/langtags": "*" }, "devDependencies": { "@types/mocha": "^5.2.7", diff --git a/developer/src/kmc-keyboard-info/src/keyboard-info-compiler.ts b/developer/src/kmc-keyboard-info/src/keyboard-info-compiler.ts index 3e1db45aed..49da3d6684 100644 --- a/developer/src/kmc-keyboard-info/src/keyboard-info-compiler.ts +++ b/developer/src/kmc-keyboard-info/src/keyboard-info-compiler.ts @@ -7,7 +7,7 @@ import { minKeymanVersion } from "./min-keyman-version.js"; import { KeyboardInfoFile, KeyboardInfoFileIncludes, KeyboardInfoFileLanguageFont, KeyboardInfoFilePlatform } from "./keyboard-info-file.js"; import { KeymanFileTypes, KmpJsonFile, KmxFileReader, KMX, KeymanTargets } from "@keymanapp/common-types"; import { KeyboardInfoCompilerMessages } from "./keyboard-info-compiler-messages.js"; -import langtags from "./imports/langtags.js"; +import { getLangtagByTag } from "@keymanapp/langtags"; import { CompilerCallbacks, KeymanCompiler, CompilerOptions, KeymanCompilerResult, KeymanCompilerArtifacts, KeymanCompilerArtifact, KeymanUrls, isValidEmail, validateMITLicense } from "@keymanapp/developer-utils"; import { KmpCompiler } from "@keymanapp/kmc-package"; @@ -16,29 +16,6 @@ import { getFontFamily } from "@keymanapp/developer-utils"; const regionNames = new Intl.DisplayNames(['en'], { type: "region" }); const scriptNames = new Intl.DisplayNames(['en'], { type: "script" }); -const langtagsByTag = {}; - -/** - * Build a dictionary of language tags from langtags.json - */ - -function preinit(): void { - if(langtagsByTag['en']) { - // Already initialized, we can reasonably assume that 'en' will always be in - // langtags.json. - return; - } - - for(const tag of langtags) { - langtagsByTag[tag.tag] = tag; - langtagsByTag[tag.full] = tag; - if(tag.tags) { - for(const t of tag.tags) { - langtagsByTag[t] = tag; - } - } - } -} /** * @public @@ -108,10 +85,6 @@ export class KeyboardInfoCompiler implements KeymanCompiler { private callbacks: CompilerCallbacks; private options: KeyboardInfoCompilerOptions; - constructor() { - preinit(); - } - /** * Initialize the compiler. * Copies options. @@ -571,7 +544,7 @@ export class KeyboardInfoCompiler implements KeymanCompiler { } } } - const tag = langtagsByTag[bcp47] ?? langtagsByTag[locale.language]; + const tag = getLangtagByTag(bcp47) ?? getLangtagByTag(locale.language); language.languageName = tag ? tag.name : bcp47; language.regionName = mapName(locale.region, regionNames); language.scriptName = mapName(locale.script, scriptNames); @@ -586,7 +559,7 @@ export class KeyboardInfoCompiler implements KeymanCompiler { '' ); - const resolvedScript = locale.script ?? langtagsByTag[bcp47]?.script ?? langtagsByTag[locale.language]?.script ?? undefined; + const resolvedScript = locale.script ?? getLangtagByTag(bcp47)?.script ?? getLangtagByTag(locale.language)?.script ?? undefined; if(commonScript === null) { commonScript = resolvedScript; } else { @@ -648,10 +621,3 @@ export class KeyboardInfoCompiler implements KeymanCompiler { } } - -/** - * these are exported only for unit tests, do not use - */ -export const unitTestEndpoints = { - langtagsByTag, -}; \ No newline at end of file diff --git a/developer/src/kmc-keyboard-info/test/keyboard-info-compiler.tests.ts b/developer/src/kmc-keyboard-info/test/keyboard-info-compiler.tests.ts index 99c244be2d..7b62b012ed 100644 --- a/developer/src/kmc-keyboard-info/test/keyboard-info-compiler.tests.ts +++ b/developer/src/kmc-keyboard-info/test/keyboard-info-compiler.tests.ts @@ -3,8 +3,8 @@ import { assert } from 'chai'; import 'mocha'; import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; import { makePathToFixture } from './helpers/index.js'; -import { KeyboardInfoCompiler, KeyboardInfoCompilerResult, unitTestEndpoints } from '../src/keyboard-info-compiler.js'; -import langtags from "../src/imports/langtags.js"; +import { KeyboardInfoCompiler, KeyboardInfoCompilerResult } from '../src/keyboard-info-compiler.js'; +import { getLangtagByTag, langtags } from "@keymanapp/langtags"; import { KmpCompiler, KmpCompilerOptions } from '@keymanapp/kmc-package'; import { KMX, KeymanFileTypes, KeymanTargets, KmpJsonFile } from '@keymanapp/common-types'; import { CompilerCallbacks } from '@keymanapp/developer-utils'; @@ -114,10 +114,10 @@ describe('keyboard-info-compiler', function () { const compiler = new KeyboardInfoCompiler(); // indirectly call preinit() assert.isNotNull(compiler); const en_langtag = langtags.find(({ tag }) => tag === 'en'); - assert.deepEqual((unitTestEndpoints.langtagsByTag)['en'], en_langtag); - assert.deepEqual((unitTestEndpoints.langtagsByTag)['en-Latn-US'], en_langtag); - assert.deepEqual((unitTestEndpoints.langtagsByTag)['en-Latn'], en_langtag); - assert.deepEqual((unitTestEndpoints.langtagsByTag)['en-US'], en_langtag); + assert.deepEqual(getLangtagByTag('en'), en_langtag); + assert.deepEqual(getLangtagByTag('en-latn-us'), en_langtag); + assert.deepEqual(getLangtagByTag('en-latn'), en_langtag); + assert.deepEqual(getLangtagByTag('en-us'), en_langtag); }); it('check init initialises KeyboardInfoCompiler correctly', async function() { diff --git a/developer/src/kmc-keyboard-info/tsconfig.json b/developer/src/kmc-keyboard-info/tsconfig.json index 124c812ecf..2791b7542b 100644 --- a/developer/src/kmc-keyboard-info/tsconfig.json +++ b/developer/src/kmc-keyboard-info/tsconfig.json @@ -4,18 +4,12 @@ "compilerOptions": { "outDir": "build/src/", "rootDir": "src/", - /* - following two settings allow us to import langtags.js. It cannot be a .ts - because the type is too complex for TypeScript to infer. - */ - "allowJs": true, - "noImplicitAny": false, }, "include": [ "src/**/*.ts", - "src/imports/langtags.js", ], "references": [ + { "path": "../../../common/web/langtags" }, { "path": "../../../common/web/types" }, { "path": "../kmc-package/" }, { "path": "../common/web/utils/" }, diff --git a/developer/src/kmc-kmn/src/kmw-compiler/kmw-compiler-messages.ts b/developer/src/kmc-kmn/src/kmw-compiler/kmw-compiler-messages.ts index 8d8b8ca6ed..e58b5fb9bf 100644 --- a/developer/src/kmc-kmn/src/kmw-compiler/kmw-compiler-messages.ts +++ b/developer/src/kmc-kmn/src/kmw-compiler/kmw-compiler-messages.ts @@ -1,6 +1,5 @@ import { keyAddress, KmnCompilerMessages } from "../compiler/kmn-compiler-messages.js"; -import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerEvent, CompilerMessageDef as def, CompilerMessageSpec } from "@keymanapp/developer-utils"; -import { kmnfile } from "./compiler-globals.js"; +import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageDef as def, CompilerMessageSpec as m } from "@keymanapp/developer-utils"; import { KeyAddress } from "./validate-layout-file.js"; const Namespace = CompilerErrorNamespace.KmwCompiler; @@ -10,12 +9,6 @@ const SevHint = CompilerErrorSeverity.Hint | Namespace; const SevError = CompilerErrorSeverity.Error | Namespace; // const SevFatal = CompilerErrorSeverity.Fatal | Namespace; -const m = (code: number, message: string, o?: {filename?: string, line?: number}) : CompilerEvent => ({ - ...CompilerMessageSpec(code, message), - filename: o?.filename ?? kmnfile, - line: o?.line, -}); - /** * @internal * Error messages reported by the KeymanWeb .kmn compiler. @@ -34,25 +27,62 @@ export class KmwCompilerMessages extends KmnCompilerMessages { // 0x0001 - Reserved for removed identifier ERROR_NotAnyRequiresVersion14 = // SevError | 0x0001, added in 17.0, removed in 18.0 + //------------------------------------------------------------------------------| + // max length of detail message lines (checked by verifyCompilerMessagesObject) | + //------------------------------------------------------------------------------| + static ERROR_TouchLayoutIdentifierRequires15 = SevError | 0x0002; - static Error_TouchLayoutIdentifierRequires15 = (o:{keyId:string, platformName:string, layerId:string, address:KeyAddress}) => m(this.ERROR_TouchLayoutIdentifierRequires15, - `Key "${def(o.keyId)}" on "${def(o.platformName)}", layer "${def(o.layerId)}" (${keyAddress(o.address)}) has a multi-part identifier which requires version 15.0 or newer.`); + static Error_TouchLayoutIdentifierRequires15 = (o:{keyId:string, platformName:string, layerId:string, address:KeyAddress}) => m( + this.ERROR_TouchLayoutIdentifierRequires15, + `Key "${def(o.keyId)}" on "${def(o.platformName)}", layer "${def(o.layerId)}" (${keyAddress(o.address)}) has a `+ + `multi-part identifier which requires version 15.0 or newer`, ` + The Unicode key format \`U_xxxx_yyyy\` is supported in Keyman 15.0 and later + versions. + + For example, \`U_0041_0300\` means the key will by default emit + \`U+0041 U+0300\` (À); earlier versions allow only a single Unicode value in + the identifier, e.g. \`U_0300\`. + `); static ERROR_InvalidTouchLayoutFileFormat = SevError | 0x0003; - static Error_InvalidTouchLayoutFileFormat = (o:{msg: string}) => m(this.ERROR_InvalidTouchLayoutFileFormat, - `Invalid touch layout file: ${def(o.msg)}`); + static Error_InvalidTouchLayoutFileFormat = (o:{msg: string}) => m( + this.ERROR_InvalidTouchLayoutFileFormat, + `Invalid touch layout file: ${def(o.msg)}`, ` + The referenced .keyman-touch-layout file contained invalid JSON content. The + touch layout file format is documented at + https://help.keyman.com/developer/current-version/reference/file-types/keyman-touch-layout. + `); static ERROR_TouchLayoutFileDoesNotExist = SevError | 0x0004; - static Error_TouchLayoutFileDoesNotExist = (o:{filename:string}) => m(this.ERROR_TouchLayoutFileDoesNotExist, - `Touch layout file ${def(o.filename)} does not exist`); + static Error_TouchLayoutFileDoesNotExist = (o:{filename:string}) => m( + this.ERROR_TouchLayoutFileDoesNotExist, + `Touch layout file ${def(o.filename)} does not exist`, ` + The compiler was unable to load the referenced .keyman-touch-layout file. + Verify that the referenced file does exist and is accessible to the compiler. + `); static HINT_TouchLayoutUsesUnsupportedGesturesDownlevel = SevHint | 0x0005; - static Hint_TouchLayoutUsesUnsupportedGesturesDownlevel = (o:{keyId:string}) => m(this.HINT_TouchLayoutUsesUnsupportedGesturesDownlevel, - `The touch layout uses a flick or multi-tap gesture on key ${def(o.keyId)}, which is only available on version 17.0+ of Keyman`); + static Hint_TouchLayoutUsesUnsupportedGesturesDownlevel = (o:{keyId:string}) => m( + this.HINT_TouchLayoutUsesUnsupportedGesturesDownlevel, + `The touch layout uses a flick or multi-tap gesture on key ${def(o.keyId)}, which `+ + `is only available on version 17.0+ of Keyman`, ` + Flick and multi-tap gesture support was added to Keyman mobile platforms in + version 17.0. Keyboards which include these gestures can still work in earlier + versions of Keyman, but any flick and multi-tap gestures will not be available, + which may make some characters inaccesssible. + `); static INFO_MinimumWebEngineVersion = SevInfo | 0x0006; static Info_MinimumWebEngineVersion = (o:{version:string}) => m( this.INFO_MinimumWebEngineVersion, - `The compiler has assigned a minimum web engine version of ${o.version} based on features used in this keyboard` - ); + `The compiler has assigned a minimum web engine version of ${o.version} based on `+ + `features used in this keyboard`, ` + If the [\`&version\` store](https://help.keyman.com/developer/language/reference/version) + is not present in the keyboard source, the compiler attempts to assign the + lowest possible Keyman version that can support the features found in the + keyboard. Details on the history of language features and supported versions + can be found at https://help.keyman.com/developer/language/guide/history. + `); + + //------------------------------------------------------------------------------| }; diff --git a/developer/src/kmc-ldml/src/compiler/ldml-compiler-messages.ts b/developer/src/kmc-ldml/src/compiler/ldml-compiler-messages.ts index 5df0da4b73..3577a2e110 100644 --- a/developer/src/kmc-ldml/src/compiler/ldml-compiler-messages.ts +++ b/developer/src/kmc-ldml/src/compiler/ldml-compiler-messages.ts @@ -217,33 +217,45 @@ export class LdmlCompilerMessages { static Error_UnparseableTransformFrom = (o: { from: string, message: string }) => m(this.ERROR_UnparseableTransformFrom, `Invalid transform from="${def(o.from)}": "${def(o.message)}"`); + //------------------------------------------------------------------------------| + // max length of detail message lines (checked by verifyCompilerMessagesObject) | + //------------------------------------------------------------------------------| + static ERROR_IllegalTransformDollarsign = SevErrorTransform | 0x01; static Error_IllegalTransformDollarsign = (o: { from: string }) => m( this.ERROR_IllegalTransformDollarsign, - `Invalid transform from="${def(o.from)}": Unescaped dollar-sign ($) is not valid transform syntax.`, - `**Hint**: Use \`\\$\` to match a literal dollar-sign. If this precedes a variable name, `+ - `the variable name may not be valid (A-Z, a-z, 0-9, _, 32 character maximum).` - ); + `Invalid transform from="${def(o.from)}": Unescaped dollar-sign ($) is not valid transform syntax.`, ` + **Hint**: Use \`\\$\` to match a literal dollar-sign. If this precedes a + variable name, the variable name may not be valid (A-Z, a-z, 0-9, _, 32 + character maximum). + `); static ERROR_TransformFromMatchesNothing = SevErrorTransform | 0x02; - static Error_TransformFromMatchesNothing = (o: { from: string }) => - m(this.ERROR_TransformFromMatchesNothing, `Invalid transfom from="${def(o.from)}": Matches an empty string.`); + static Error_TransformFromMatchesNothing = (o: { from: string }) => m( + this.ERROR_TransformFromMatchesNothing, + `Invalid transfom from="${def(o.from)}": Matches an empty string.` + ); static ERROR_IllegalTransformPlus = SevErrorTransform | 0x03; - static Error_IllegalTransformPlus = (o: { from: string }) => - m(this.ERROR_IllegalTransformPlus, `Invalid transform from="${def(o.from)}": Unescaped plus (+) is not valid transform syntax.`, - '**Hint**: Use `\\+` to match a literal plus.'); + static Error_IllegalTransformPlus = (o: { from: string }) => m( + this.ERROR_IllegalTransformPlus, + `Invalid transform from="${def(o.from)}": Unescaped plus (+) is not valid transform syntax.`, ` + **Hint**: Use \`\\+\` to match a literal plus. + `); static ERROR_IllegalTransformAsterisk = SevErrorTransform | 0x04; - static Error_IllegalTransformAsterisk = (o: { from: string }) => - m(this.ERROR_IllegalTransformAsterisk, `Invalid transform from="${def(o.from)}": Unescaped asterisk (*) is not valid transform syntax.`, - '**Hint**: Use `\\*` to match a literal asterisk.'); + static Error_IllegalTransformAsterisk = (o: { from: string }) =>m( + this.ERROR_IllegalTransformAsterisk, + `Invalid transform from="${def(o.from)}": Unescaped asterisk (*) is not valid transform syntax.`, ` + **Hint**: Use \`\\*\` to match a literal asterisk. + `); static ERROR_IllegalTransformToUset = SevErrorTransform | 0x05; static Error_IllegalTransformToUset = (o: { to: string }) => m( this.ERROR_IllegalTransformToUset, - `Invalid transform to="${def(o.to)}": Set variable (\\$[…]) cannot be used in 'to=' unless part of a map.`, - '**Hint**: If a map was meant, must use the form ``.' - ); + `Invalid transform to="${def(o.to)}": Set variable (\\$[…]) cannot be used in 'to=' unless part of a map.`, ` + **Hint**: If a map was meant, must use the form + \`\`. + `); } diff --git a/developer/src/kmc-ldml/test/fixtures/sections/keys/import-local.xml b/developer/src/kmc-ldml/test/fixtures/sections/keys/import-local.xml index d97d711301..c3ef2b3ef6 100644 --- a/developer/src/kmc-ldml/test/fixtures/sections/keys/import-local.xml +++ b/developer/src/kmc-ldml/test/fixtures/sections/keys/import-local.xml @@ -4,7 +4,7 @@ - + diff --git a/developer/src/kmc-ldml/test/fixtures/test-fr.xml b/developer/src/kmc-ldml/test/fixtures/test-fr.xml index a150915901..401a86bdbf 100644 --- a/developer/src/kmc-ldml/test/fixtures/test-fr.xml +++ b/developer/src/kmc-ldml/test/fixtures/test-fr.xml @@ -1,5 +1,5 @@ - + diff --git a/developer/src/kmc-package/src/compiler/kmp-compiler.ts b/developer/src/kmc-package/src/compiler/kmp-compiler.ts index cd6cf6df36..04eae4afd8 100644 --- a/developer/src/kmc-package/src/compiler/kmp-compiler.ts +++ b/developer/src/kmc-package/src/compiler/kmp-compiler.ts @@ -105,12 +105,14 @@ export class KmpCompiler implements KeymanCompiler { return null; } + outputFilename = outputFilename ?? inputFilename.replace(/\.kps$/, '.kmp'); + // // Validate the package file // const validation = new PackageValidation(this.callbacks, this.options); - if(!validation.validate(inputFilename, kmpJsonData)) { + if(!validation.validate(inputFilename, outputFilename, kmpJsonData)) { return null; } @@ -127,7 +129,7 @@ export class KmpCompiler implements KeymanCompiler { artifacts: { kmp: { data, - filename: outputFilename ?? inputFilename.replace(/\.kps$/, '.kmp') + filename: outputFilename } } } diff --git a/developer/src/kmc-package/src/compiler/package-compiler-messages.ts b/developer/src/kmc-package/src/compiler/package-compiler-messages.ts index 6545b7d963..5a088be128 100644 --- a/developer/src/kmc-package/src/compiler/package-compiler-messages.ts +++ b/developer/src/kmc-package/src/compiler/package-compiler-messages.ts @@ -184,5 +184,31 @@ export class PackageCompilerMessages { static Warn_VisualKeyboardFileIsInvalid = (o:{filename: string}) => m( this.WARN_VisualKeyboardFileIsInvalid, `Visual keyboard file '${def(o.filename)}' is invalid.` ); + + static WARN_PackageVersionIsUnrecognizedFormat = SevWarn | 0x0027; + static Warn_PackageVersionIsUnrecognizedFormat = (o:{version: string}) => m( + this.WARN_PackageVersionIsUnrecognizedFormat, `Package version '${def(o.version)}' has an unrecognized format.`, + `The format for version numbers should be number[.number[.number]]. Each + number component should be an integer, without leading zeroes.` + ); + + static ERROR_PackageMustNotContainItself = SevError | 0x0028; + static Error_PackageMustNotContainItself = (o:{outputFilename: string}) => m( + this.ERROR_PackageMustNotContainItself, `The package may not include a .kmp file of the same name '${def(o.outputFilename)}'.`, ` + While it is possible for a package file to contain other .kmp package files, + it is an error for a package file to contain a package with the same name as + itself. + + **Note:** it is not recommended to include other package files within a + package, as the user experience for installation and uninstallation is + complex, and not consistent across all platforms. + + **Note**: Nested packages are not checked for validity or whether or not they + may violate this rule transitively. + `); + + //------------------------------------------------------------------------------| + // max length of detail message lines (checked by verifyCompilerMessagesObject) | + //------------------------------------------------------------------------------| } diff --git a/developer/src/kmc-package/src/compiler/package-validation.ts b/developer/src/kmc-package/src/compiler/package-validation.ts index 3f98f7d830..ae5fd331f5 100644 --- a/developer/src/kmc-package/src/compiler/package-validation.ts +++ b/developer/src/kmc-package/src/compiler/package-validation.ts @@ -12,7 +12,7 @@ export class PackageValidation { constructor(private callbacks: CompilerCallbacks, private options: CompilerOptions) { } - public validate(filename: string, kmpJson: KmpJsonFile.KmpJsonFile) { + public validate(filename: string, outputFilename: string, kmpJson: KmpJsonFile.KmpJsonFile) { if(!this.checkForModelsAndKeyboardsInSamePackage(kmpJson)) { return false; } @@ -22,7 +22,7 @@ export class PackageValidation { if(!this.checkLexicalModels(filename, kmpJson)) { return false; } - if(!this.checkContentFiles(kmpJson)) { + if(!this.checkContentFiles(kmpJson, outputFilename)) { return false; } if(!this.checkPackageInfo(kmpJson)) { @@ -130,9 +130,9 @@ export class PackageValidation { return true; } - private checkContentFiles(kmpJson: KmpJsonFile.KmpJsonFile): boolean { + private checkContentFiles(kmpJson: KmpJsonFile.KmpJsonFile, outputFilename: string): boolean { for(let file of kmpJson.files) { - if(!this.checkContentFile(file)) { + if(!this.checkContentFile(file, outputFilename)) { return false; } } @@ -140,7 +140,7 @@ export class PackageValidation { return true; } - private checkContentFile(file: KmpJsonFile.KmpJsonFileContentFile): boolean { + private checkContentFile(file: KmpJsonFile.KmpJsonFileContentFile, outputFilename: string): boolean { const filename = this.callbacks.path.basename(file.name); const ext = this.callbacks.path.extname(filename); const base = filename.substring(0, filename.length-ext.length); @@ -155,9 +155,20 @@ export class PackageValidation { return false; } + if(this.doesContentFileEqualOutputFilename(file, outputFilename)) { + this.callbacks.reportMessage(PackageCompilerMessages.Error_PackageMustNotContainItself({outputFilename})); + return false; + } + return true; } + private doesContentFileEqualOutputFilename(file: KmpJsonFile.KmpJsonFileContentFile, outputFilename: string): boolean { + // case-insensitive comparison because Windows has case-insensitive filenames + return this.callbacks.path.basename(file.name).toLowerCase() == + this.callbacks.path.basename(outputFilename).toLowerCase(); + } + private checkIfContentFileIsDangerous(file: KmpJsonFile.KmpJsonFileContentFile): boolean { const filename = this.callbacks.path.basename(file.name).toLowerCase(); diff --git a/developer/src/kmc-package/src/compiler/package-version-validator.ts b/developer/src/kmc-package/src/compiler/package-version-validator.ts index b20e086aec..7250b6bef4 100644 --- a/developer/src/kmc-package/src/compiler/package-version-validator.ts +++ b/developer/src/kmc-package/src/compiler/package-version-validator.ts @@ -91,6 +91,11 @@ export class PackageVersionValidator { kmp.info.version = {description: kmp.keyboards[0].version}; } + if(result && !isValidVersionNumber(kmp.info.version.description)) { + this.callbacks.reportMessage(PackageCompilerMessages.Warn_PackageVersionIsUnrecognizedFormat({version: kmp.info.version.description})); + return false; + } + return result; } @@ -109,3 +114,11 @@ export class PackageVersionValidator { return true; } } + +function isValidVersionNumber(version: string) { + return /^(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*)){0,2}$/.test(version); +} + +export const unitTestEndpoints = { + isValidVersionNumber, +}; diff --git a/developer/src/kmc-package/test/fixtures/invalid/error_package_must_not_contain_itself.kps b/developer/src/kmc-package/test/fixtures/invalid/error_package_must_not_contain_itself.kps new file mode 100644 index 0000000000..430ccd3b71 --- /dev/null +++ b/developer/src/kmc-package/test/fixtures/invalid/error_package_must_not_contain_itself.kps @@ -0,0 +1,37 @@ + + + + 15.0.266.0 + 7.0 + + + error_package_must_not_contain_itself + © 2019 National Research Council Canada + Eddie Antonio Santos + 1.0 + + + + error_package_must_not_contain_itself.kmp + error_package_must_not_contain_itself + 0 + .kmp + + + basic.kmx + Keyboard Basic + 0 + .kmx + + + + + Basic + basic + 1.0 + + Central Khmer (Khmer, Cambodia) + + + + diff --git a/developer/src/kmc-package/test/fixtures/invalid/warn_package_version_is_unrecognized_format.kps b/developer/src/kmc-package/test/fixtures/invalid/warn_package_version_is_unrecognized_format.kps new file mode 100644 index 0000000000..30e310bc6e --- /dev/null +++ b/developer/src/kmc-package/test/fixtures/invalid/warn_package_version_is_unrecognized_format.kps @@ -0,0 +1,32 @@ + + + + 15.0.266.0 + 7.0 + + + + warn_package_version_is_unrecognized_format + © 2019 National Research Council Canada + Eddie Antonio Santos + 1.0-invalid + + + + basic.kmx + Keyboard Basic + 0 + .kmx + + + + + Basic + basic + 1.0 + + Central Khmer (Khmer, Cambodia) + + + + diff --git a/developer/src/kmc-package/test/messages.tests.ts b/developer/src/kmc-package/test/messages.tests.ts index ba5ec56b08..21f1e0e4a2 100644 --- a/developer/src/kmc-package/test/messages.tests.ts +++ b/developer/src/kmc-package/test/messages.tests.ts @@ -241,4 +241,11 @@ describe('PackageCompilerMessages', function () { await testForMessage(this, ['invalid', 'error_package_file_has_empty_version.kps'], PackageCompilerMessages.ERROR_PackageFileHasEmptyVersion); }); + + it('should generate Warn_PackageVersionIsUnrecognizedFormat if package version field is not a valid format', async function() { + await testForMessage(this, ['invalid', 'warn_package_version_is_unrecognized_format.kps'], + PackageCompilerMessages.WARN_PackageVersionIsUnrecognizedFormat); + }); + + // ERROR_PackageMustNotContainItself test in package-compiler.tests.ts }); diff --git a/developer/src/kmc-package/test/package-compiler.tests.ts b/developer/src/kmc-package/test/package-compiler.tests.ts index 16c523bda9..a9777a2a08 100644 --- a/developer/src/kmc-package/test/package-compiler.tests.ts +++ b/developer/src/kmc-package/test/package-compiler.tests.ts @@ -294,18 +294,30 @@ describe('KmpCompiler', function () { it(`should handle a range of valid BCP47 tags`, function () { const inputFilename = makePathToFixture('bcp47', 'valid_bcp47.kps'); + const outputFilename = makePathToFixture('bcp47', 'valid_bcp47.kmp'); const kmpJson = kmpCompiler.transformKpsToKmpObject(inputFilename); assert.isNotNull(kmpJson); const validation = new PackageValidation(callbacks, {}); - assert.isTrue(validation.validate(inputFilename, kmpJson)); + assert.isTrue(validation.validate(inputFilename, outputFilename, kmpJson)); }); it(`should reject an invalid BCP47 tag`, function () { const inputFilename = makePathToFixture('bcp47', 'invalid_bcp47_1.kps'); + const outputFilename = makePathToFixture('bcp47', 'invalid_bcp47_1.kmp'); const kmpJson = kmpCompiler.transformKpsToKmpObject(inputFilename); assert.isNotNull(kmpJson); const validation = new PackageValidation(callbacks, {}); - assert.isFalse(validation.validate(inputFilename, kmpJson)); + assert.isFalse(validation.validate(inputFilename, outputFilename, kmpJson)); + }); + + it(`should reject an package that contains itself`, function () { + const inputFilename = makePathToFixture('invalid', 'error_package_must_not_contain_itself.kps'); + const outputFilename = makePathToFixture('invalid', 'error_package_must_not_contain_itself.kmp'); + const kmpJson = kmpCompiler.transformKpsToKmpObject(inputFilename); + assert.isNotNull(kmpJson); + const validation = new PackageValidation(callbacks, {}); + assert.isFalse(validation.validate(inputFilename, outputFilename, kmpJson)); + assert.equal(callbacks.messages[0].code, PackageCompilerMessages.ERROR_PackageMustNotContainItself); }); }); diff --git a/developer/src/kmc-package/test/versioning.tests.ts b/developer/src/kmc-package/test/versioning.tests.ts index 98d00b853c..adcba30019 100644 --- a/developer/src/kmc-package/test/versioning.tests.ts +++ b/developer/src/kmc-package/test/versioning.tests.ts @@ -1,9 +1,10 @@ import 'mocha'; import { assert } from 'chai'; import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; -import { makePathToFixture } from './helpers/index.js'; -import { KmpCompiler } from '../src/compiler/kmp-compiler.js'; import { KmpJsonFile } from '@keymanapp/common-types'; +import { KmpCompiler } from '../src/compiler/kmp-compiler.js'; +import { unitTestEndpoints } from '../src/compiler/package-version-validator.js'; +import { makePathToFixture } from './helpers/index.js'; // This unit test was translated from a Delphi test // Keyman.Test.System.CompilePackageVersioningTest, but note the difference in @@ -41,4 +42,34 @@ describe('package versioning', function () { assert.isTrue(kmpJson !== null); }); } + + it('should validate recognized version numbers', function() { + const goodVersions = [ + '1.0', + '1', + '1.0.2', + '2.2.3', + '9', + '0', + ], badVersions = [ + '0001', + '0.1m', + '2.3.4.5', + '1.03.4', + 'test', + '1.0-beta', + '', + null, + undefined, + 'xxx', + ]; + + for(const v of goodVersions) { + assert.isTrue(unitTestEndpoints.isValidVersionNumber(v), `'${v}' should be recognized as a valid version number`); + } + + for(const v of badVersions) { + assert.isFalse(unitTestEndpoints.isValidVersionNumber(v), `'${v}' should be recognized as an invalid version number`); + } + }); }); diff --git a/developer/src/kmc/README.md b/developer/src/kmc/README.md index 5e5183bff5..cd63fbb7c5 100644 --- a/developer/src/kmc/README.md +++ b/developer/src/kmc/README.md @@ -25,7 +25,7 @@ configuration or installation is required. kmc is also available as an npm package, [@keymanapp/kmc](https://npmjs.com/package/@keymanapp/kmc). -You'll need [node.js](https://nodejs.org/), version 18.0 or later. +You'll need [node.js](https://nodejs.org/), version 20.0 or later. ```shell npm install -g @keymanapp/kmc diff --git a/developer/src/kmc/src/commands/analyze.ts b/developer/src/kmc/src/commands/analyze.ts index 65119c30d9..b0699977c3 100644 --- a/developer/src/kmc/src/commands/analyze.ts +++ b/developer/src/kmc/src/commands/analyze.ts @@ -3,18 +3,14 @@ import * as path from 'path'; import { Command, Option } from 'commander'; import { NodeCompilerCallbacks } from '../util/NodeCompilerCallbacks.js'; import { InfrastructureMessages } from '../messages/infrastructureMessages.js'; -import { CompilerCallbacks, CompilerLogLevel } from '@keymanapp/developer-utils'; +import { CompilerBaseOptions, CompilerCallbacks } from '@keymanapp/developer-utils'; import { AnalyzeOskCharacterUse, AnalyzeOskRewritePua } from '@keymanapp/kmc-analyze'; import { BaseOptions } from '../util/baseOptions.js'; import { runOnFiles } from '../util/projectRunner.js'; import { exitProcess } from '../util/sysexits.js'; +import { commanderOptionsToBaseOptions } from '../util/extendedCompilerOptions.js'; -interface AnalysisActivityOptions /* not inheriting from CompilerBaseOptions */ { - /** - * Reporting level to console, used by NodeCompilerCallbacks (not used in compiler modules; - * all messages are still reported to the internal log) - */ - logLevel?: CompilerLogLevel; +interface AnalysisActivityOptions extends CompilerBaseOptions { base?: string; stripDottedCircle?: boolean; includeCounts?: boolean; @@ -30,7 +26,7 @@ export function declareAnalyze(program: Command) { function declareOskCharUse(command: Command) { let subCommand = command.command('osk-char-use [infile...]'); - BaseOptions.addLogLevel(subCommand); + BaseOptions.addAll(subCommand); subCommand .description('Analyze On Screen Keyboards for character usage') .option('-b, --base', 'First PUA codepoint to use, in hexadecimal (default F100)', 'F100') @@ -75,9 +71,24 @@ function declareOskRewrite(command: Command) { }); } +function commanderOptionsToAnalyzeOptions(options: any): AnalysisActivityOptions { + // TODO: split options per sub-command + const result: AnalysisActivityOptions = { + ...commanderOptionsToBaseOptions(options), + // AnalysisActivityOptions + base: options.base, + includeCounts: options.includeCounts ?? false, + inputMappingFile: options.inputMappingFile, + mappingFile: options.mappingFile, + stripDottedCircle: options.stripDottedCircle ?? false, + }; + return result; +} + async function analyze(action: (callbacks: CompilerCallbacks, filenames: string[], options: AnalysisActivityOptions)=>Promise, - filenames: string[], options: AnalysisActivityOptions): Promise { - let callbacks = new NodeCompilerCallbacks({logLevel: options.logLevel}); + filenames: string[], commanderOptions: any): Promise { + const options = commanderOptionsToAnalyzeOptions(commanderOptions); + let callbacks = new NodeCompilerCallbacks(options); try { return await action(callbacks, filenames, options); } catch(e) { diff --git a/developer/src/kmc/src/commands/build.ts b/developer/src/kmc/src/commands/build.ts index 996e767a77..39e0e20578 100644 --- a/developer/src/kmc/src/commands/build.ts +++ b/developer/src/kmc/src/commands/build.ts @@ -7,20 +7,18 @@ import { NodeCompilerCallbacks } from '../util/NodeCompilerCallbacks.js'; import { InfrastructureMessages } from '../messages/infrastructureMessages.js'; import { KeymanFileTypes } from '@keymanapp/common-types'; import { CompilerOptions, CompilerFileCallbacks } from '@keymanapp/developer-utils'; -import { BaseOptions } from '../util/baseOptions.js'; +import { BuildBaseOptions } from '../util/baseOptions.js'; import { expandFileLists } from '../util/fileLists.js'; import { isProject } from '../util/projectLoader.js'; import { buildTestData } from './buildTestData/index.js'; import { buildWindowsPackageInstaller } from './buildWindowsPackageInstaller/index.js'; -import { commandOptionsToCompilerOptions } from '../util/extendedCompilerOptions.js'; +import { commanderOptionsToCompilerOptions } from '../util/extendedCompilerOptions.js'; import { exitProcess } from '../util/sysexits.js'; export function declareBuild(program: Command) { // TODO: localization? const buildCommand = program .command('build') - .option('--color', 'Force colorization for log messages') - .option('--no-color', 'No colorization for log messages; if both omitted, detects from console') // These options are only used with build file but are included here so that // they are visible in `kmc build --help` @@ -32,7 +30,7 @@ export function declareBuild(program: Command) { .option('--no-compiler-version', 'Exclude compiler version metadata from output') .option('--no-warn-deprecated-code', 'Turn off warnings for deprecated code styles'); - BaseOptions.addAll(buildCommand); + BuildBaseOptions.addAll(buildCommand); buildCommand.command('file [infile...]', {isDefault: true}) .description(`Compile one or more source files or projects ('file' subcommand is default).`) @@ -74,7 +72,7 @@ function initialize(commanderOptions: any) { // We use a default callback instance when validating command line, but throw // it away once we have completed initialization const initializationCallbacks = new NodeCompilerCallbacks({}); - const options = commandOptionsToCompilerOptions(commanderOptions, initializationCallbacks); + const options = commanderOptionsToCompilerOptions(commanderOptions, initializationCallbacks); return options; } diff --git a/developer/src/kmc/src/commands/buildTestData/index.ts b/developer/src/kmc/src/commands/buildTestData/index.ts index 5343c81702..220bc4b26f 100644 --- a/developer/src/kmc/src/commands/buildTestData/index.ts +++ b/developer/src/kmc/src/commands/buildTestData/index.ts @@ -4,13 +4,13 @@ import * as kmcLdml from '@keymanapp/kmc-ldml'; import { CompilerCallbacks, defaultCompilerOptions, LDMLKeyboardTestDataXMLSourceFile, LDMLKeyboardXMLSourceFileReader } from '@keymanapp/developer-utils'; import { NodeCompilerCallbacks } from '../../util/NodeCompilerCallbacks.js'; import { fileURLToPath } from 'url'; -import { CommandLineBaseOptions } from 'src/util/baseOptions.js'; +import { CommandLineBuildBaseOptions } from 'src/util/baseOptions.js'; import { exitProcess } from '../../util/sysexits.js'; import { InfrastructureMessages } from '../../messages/infrastructureMessages.js'; import { dirname } from 'node:path'; export async function buildTestData(infile: string, _options: any, commander: any): Promise { - const options: CommandLineBaseOptions = commander.optsWithGlobals(); + const options: CommandLineBuildBaseOptions = commander.optsWithGlobals(); let compilerOptions: kmcLdml.LdmlCompilerOptions = { ...defaultCompilerOptions, diff --git a/developer/src/kmc/src/commands/buildWindowsPackageInstaller/index.ts b/developer/src/kmc/src/commands/buildWindowsPackageInstaller/index.ts index 06076db929..4dddfac1f2 100644 --- a/developer/src/kmc/src/commands/buildWindowsPackageInstaller/index.ts +++ b/developer/src/kmc/src/commands/buildWindowsPackageInstaller/index.ts @@ -3,10 +3,10 @@ import * as path from 'path'; import { CompilerCallbacks, defaultCompilerOptions } from '@keymanapp/developer-utils'; import { NodeCompilerCallbacks } from '../../util/NodeCompilerCallbacks.js'; import { WindowsPackageInstallerCompiler, WindowsPackageInstallerSources } from '@keymanapp/kmc-package'; -import { CommandLineBaseOptions } from 'src/util/baseOptions.js'; +import { CommandLineBuildBaseOptions } from 'src/util/baseOptions.js'; import { exitProcess } from '../../util/sysexits.js'; -interface WindowsPackageInstallerCommandLineOptions extends CommandLineBaseOptions { +interface WindowsPackageInstallerCommandLineOptions extends CommandLineBuildBaseOptions { msi: string; exe: string; license: string; diff --git a/developer/src/kmc/src/commands/copy.ts b/developer/src/kmc/src/commands/copy.ts index 1d4ab94dcf..b7d0aef12f 100644 --- a/developer/src/kmc/src/commands/copy.ts +++ b/developer/src/kmc/src/commands/copy.ts @@ -10,16 +10,18 @@ import { InfrastructureMessages } from '../messages/infrastructureMessages.js'; import { BaseOptions } from '../util/baseOptions.js'; import { exitProcess } from '../util/sysexits.js'; import { CopierOptions, KeymanProjectCopier } from '@keymanapp/kmc-copy'; +import { commanderOptionsToBaseOptions } from '../util/extendedCompilerOptions.js'; /* c8 ignore start */ export function declareCopy(program: Command) { const command = program.command('copy '); - BaseOptions.addLogLevel(command); + BaseOptions.addAll(command); command .description('Copy a Keyman keyboard or lexical model project') .option('-o, --out-path ', 'New name and path for project') .option('-n, --dry-run', 'Show what would happen, without making changes') + .option('-r, --relocate-external', 'Copy external files into "external" folder') .action(copyProject) .addHelpText('before', ` can be: @@ -34,8 +36,11 @@ export function declareCopy(program: Command) { function commanderOptionsToCopierOptions(options: any): CopierOptions { const result: CopierOptions = { + ...commanderOptionsToBaseOptions(options), + // CopierOptions outPath: options.outPath, dryRun: options.dryRun ?? false, + relocateExternalFiles: options.relocateExternal ?? false, }; return result; } @@ -44,15 +49,16 @@ const MaxDryRunCopyLogMessages = 10000; async function copyProject(ids: string | string[], _options: any, commander: any): Promise { const commanderOptions = commander.optsWithGlobals(); - const callbacks = new NodeCompilerCallbacks({logLevel: commanderOptions.logLevel ?? 'info'}); - if(!await doCopy(callbacks, ids, commanderOptions)) { + const options = commanderOptionsToCopierOptions(commanderOptions); + const callbacks = new NodeCompilerCallbacks(options); + if(!await doCopy(callbacks, ids, options)) { return await exitProcess(1); } } /* c8 ignore stop */ -async function doCopy(callbacks: NodeCompilerCallbacks, sources: string | string[], commanderOptions: any): Promise { +async function doCopy(callbacks: NodeCompilerCallbacks, sources: string | string[], options: CopierOptions): Promise { const source = sources; if(!source || typeof source != 'string') { @@ -62,8 +68,6 @@ async function doCopy(callbacks: NodeCompilerCallbacks, sources: string | string return false; } - const options = commanderOptionsToCopierOptions(commanderOptions); - if(options.dryRun) { // For dry run, we may need a lot of log messages, to show // where all the files are copied to diff --git a/developer/src/kmc/src/commands/generate.ts b/developer/src/kmc/src/commands/generate.ts index d5abcec989..b362d01125 100644 --- a/developer/src/kmc/src/commands/generate.ts +++ b/developer/src/kmc/src/commands/generate.ts @@ -6,6 +6,7 @@ import { BaseOptions } from '../util/baseOptions.js'; import { exitProcess } from '../util/sysexits.js'; import { GeneratorOptions, KeymanKeyboardGenerator, LdmlKeyboardGenerator, LexicalModelGenerator } from '@keymanapp/kmc-generate'; import { CompilerCallbacks, KeymanCompiler } from '@keymanapp/developer-utils'; +import { commanderOptionsToBaseOptions } from '../util/extendedCompilerOptions.js'; /* c8 ignore start */ @@ -18,7 +19,7 @@ export function declareGenerate(program: Command) { function declareGenerateKmnKeyboard(command: Command) { const subCommand = command.command('keyman-keyboard '); - BaseOptions.addLogLevel(subCommand); + BaseOptions.addAll(subCommand); subCommand .description('Generate a .kmn keyboard project') .option('-t, --target ', 'Target platforms', @@ -38,7 +39,7 @@ function declareGenerateKmnKeyboard(command: Command) { function declareGenerateLdmlKeyboard(command: Command) { const ldmlSubCommand = command.command('ldml-keyboard '); - BaseOptions.addLogLevel(ldmlSubCommand); + BaseOptions.addAll(ldmlSubCommand); ldmlSubCommand .description('Generate an LDML .xml keyboard project') .option('-t, --target ', 'Target platforms', @@ -56,7 +57,7 @@ function declareGenerateLdmlKeyboard(command: Command) { function declareGenerateLexicalModel(command: Command) { const modelSubCommand = command.command('lexical-model '); - BaseOptions.addLogLevel(modelSubCommand); + BaseOptions.addAll(modelSubCommand); modelSubCommand .description('Generate a wordlist lexical model project') .option('-o, --out-path ', 'Output path (may exist)') @@ -74,6 +75,7 @@ function declareGenerateLexicalModel(command: Command) { function commanderOptionsToGeneratorOptions(id: string, options: any): GeneratorOptions { const result: GeneratorOptions = { + ...commanderOptionsToBaseOptions(options), // TODO-GENERATE: icon support // icon: options.icon ?? true, id, @@ -84,7 +86,6 @@ function commanderOptionsToGeneratorOptions(id: string, options: any): Generator version: options.version ?? '1.0', author: options.author ?? id, copyright: options.copyright ?? options.author ?? id, - logLevel: options.logLevel ?? 'info', description: options.description ?? '', }; return result; @@ -99,32 +100,35 @@ const generateLexicalModel = async (ids: string | string[], _options: any, comma async function generate(generator: KeymanCompiler, ids: string | string[], commander: any): Promise { const commanderOptions = commander.optsWithGlobals(); - const callbacks = new NodeCompilerCallbacks({logLevel: commanderOptions.logLevel ?? 'info'}); - if(!await doGenerate(callbacks, generator, ids, commanderOptions)) { + + const id = ids; + + if(!id || typeof id != 'string') { + // Note that commander can pass an array for the ids parameter, so we + // constrain here; as this is a parameter error before we have parsed + // other options, we use a 'default' compiler callback to generate + // the error message + (new NodeCompilerCallbacks({})).reportMessage(InfrastructureMessages.Error_GenerateRequiresId()); + return await exitProcess(1); + } + + const options = commanderOptionsToGeneratorOptions(id, commanderOptions); + + const callbacks = new NodeCompilerCallbacks(options); + if(!await doGenerate(callbacks, generator, options)) { return await exitProcess(1); } } /* c8 ignore stop */ -async function doGenerate(callbacks: CompilerCallbacks, generator: KeymanCompiler, ids: string | string[], commanderOptions: any): Promise { - const id = ids; - - if(!id || typeof id != 'string') { - // Note that commander can pass an array for the ids parameter, so we - // constrain here - callbacks.reportMessage(InfrastructureMessages.Error_GenerateRequiresId()); - return false; - } - - const options = commanderOptionsToGeneratorOptions(id, commanderOptions); - +async function doGenerate(callbacks: CompilerCallbacks, generator: KeymanCompiler, options: GeneratorOptions): Promise { try { if(!await generator.init(callbacks, options)) { // errors will have been reported by the generator return false; } - const result = await generator.run(id); // note: id is currently ignored here + const result = await generator.run(options.id); // note: id is currently ignored here if(!result) { // errors will have been reported by the generator return false; diff --git a/developer/src/kmc/src/commands/messageCommand.ts b/developer/src/kmc/src/commands/messageCommand.ts index 257eacc913..d0feac380e 100644 --- a/developer/src/kmc/src/commands/messageCommand.ts +++ b/developer/src/kmc/src/commands/messageCommand.ts @@ -2,7 +2,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { Command, Option } from 'commander'; -import { escapeMarkdownChar, KeymanUrls, CompilerBaseOptions, CompilerCallbacks, CompilerError, CompilerErrorNamespace, CompilerEvent } from '@keymanapp/developer-utils'; +import { escapeMarkdownChar, KeymanUrls, CompilerBaseOptions, CompilerCallbacks, CompilerError, CompilerErrorNamespace, CompilerEvent, dedentCompilerMessageDetail } from '@keymanapp/developer-utils'; import { CompilerMessageSource, messageNamespaceKeys, messageSources } from '../messages/messageNamespaces.js'; import { NodeCompilerCallbacks } from '../util/NodeCompilerCallbacks.js'; import { exitProcess } from '../util/sysexits.js'; @@ -189,7 +189,7 @@ function getMessageDetail(cls: any, id: string, escapeMarkdown: boolean): Compil throw new Error(`Call to ${cls.name}.${f} returned null`); } - event.detail = (event.detail ?? '').replace(/^[ ]+/gm, ''); // TODO(lowpri): dedent may be too naive? + event.detail = dedentCompilerMessageDetail(event); event.message = event.message ?? ''; event.message = event?.exceptionVar ? 'This is an internal error; the message will vary' diff --git a/developer/src/kmc/src/kmlmp.ts b/developer/src/kmc/src/kmlmp.ts index bb0cda5e53..a1346ac0d3 100644 --- a/developer/src/kmc/src/kmlmp.ts +++ b/developer/src/kmc/src/kmlmp.ts @@ -3,7 +3,7 @@ * kmlmp - Keyman Lexical Model Package Compiler */ -// Note: this is a deprecated package and will be removed in Keyman 18.0 +// Note: this is a deprecated package and will be removed in Keyman 19.0 import { Command } from 'commander'; import { KmpCompiler } from '@keymanapp/kmc-package'; diff --git a/developer/src/kmc/src/util/TestKeymanSentry.ts b/developer/src/kmc/src/util/TestKeymanSentry.ts index 430ab73d68..b8572516bd 100644 --- a/developer/src/kmc/src/util/TestKeymanSentry.ts +++ b/developer/src/kmc/src/util/TestKeymanSentry.ts @@ -43,7 +43,7 @@ export class TestKeymanSentry { try { throw new Error('Test error from -sentry-client-test-exception event'); } catch(e: any) { - await KeymanSentry.captureException(e); + await KeymanSentry.captureException(e, true); } } } diff --git a/developer/src/kmc/src/util/baseOptions.ts b/developer/src/kmc/src/util/baseOptions.ts index ab07921dbd..f3223238ec 100644 --- a/developer/src/kmc/src/util/baseOptions.ts +++ b/developer/src/kmc/src/util/baseOptions.ts @@ -10,9 +10,10 @@ export interface CommandLineBaseOptions { logLevel?: CompilerLogLevel; logFormat?: CompilerLogFormat; color?: boolean; +}; - // This option is not in CompilerBaseOptions - outFile?:string; +export interface CommandLineBuildBaseOptions extends CommandLineBaseOptions { + outFile?: string; } /** @@ -27,6 +28,22 @@ export class BaseOptions { return program.addOption(new Option('--log-format ', 'Log format').choices(ALL_COMPILER_LOG_FORMATS).default('formatted')); } + public static addColor(program: Command) { + return program + .option('--color', 'Force colorization for log messages') + .option('--no-color', 'No colorization for log messages; if both omitted, detects from console'); + } + + public static addAll(program: Command) { + return [ + this.addLogLevel, + this.addLogFormat, + this.addColor, + ].reduce((p,f) => f(p), program); + } +} + +export class BuildBaseOptions extends BaseOptions { public static addOutFile(program: Command) { return program.option('-o, --out-file ', 'Override the default path and filename for the output file') } @@ -35,6 +52,7 @@ export class BaseOptions { return [ this.addLogLevel, this.addLogFormat, + this.addColor, this.addOutFile, ].reduce((p,f) => f(p), program); } diff --git a/developer/src/kmc/src/util/extendedCompilerOptions.ts b/developer/src/kmc/src/util/extendedCompilerOptions.ts index 0d72b5b550..9a2e482f68 100644 --- a/developer/src/kmc/src/util/extendedCompilerOptions.ts +++ b/developer/src/kmc/src/util/extendedCompilerOptions.ts @@ -1,4 +1,4 @@ -import { CompilerCallbacks, CompilerError, CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageOverride, CompilerMessageOverrideMap, CompilerOptions } from '@keymanapp/developer-utils'; +import { CompilerBaseOptions, CompilerCallbacks, CompilerError, CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageOverride, CompilerMessageOverrideMap, CompilerOptions } from '@keymanapp/developer-utils'; import { InfrastructureMessages } from '../messages/infrastructureMessages.js'; import { CompilerMessageSource, messageNamespaceKeys, messageSources } from '../messages/messageNamespaces.js'; @@ -27,7 +27,7 @@ export interface ExtendedCompilerOptions extends CompilerOptions { * @returns CompilerMessageOverride map with the new severity level for the * message; or `null` if parameter is invalid. */ -function commandOptionsMessageToCompilerOptionsMessage(message: string, callbacks: CompilerCallbacks): CompilerMessageOverride { +function commanderOptionsMessageToCompilerOptionsMessage(message: string, callbacks: CompilerCallbacks): CompilerMessageOverride { const pattern = /^(KM)?([0-9a-f]+)(:(D|Disable|I|Info|H|Hint|W|Warn|E|Error))?$/i; const result = message.match(pattern); if(!result) { @@ -50,14 +50,14 @@ function commandOptionsMessageToCompilerOptionsMessage(message: string, callback return override; } -function commandOptionsMessagesToCompilerOptionsMessages(messages: any, callbacks: CompilerCallbacks): CompilerMessageOverrideMap { +function commanderOptionsMessagesToCompilerOptionsMessages(messages: any, callbacks: CompilerCallbacks): CompilerMessageOverrideMap { if(!messages || !Array.isArray(messages)) { return {}; } const result: CompilerMessageOverrideMap = {}; for(let message of messages) { - const override = commandOptionsMessageToCompilerOptionsMessage(message, callbacks); + const override = commanderOptionsMessageToCompilerOptionsMessage(message, callbacks); if(!override) { return null; } @@ -165,14 +165,23 @@ function checkMessageOverride(override: CompilerMessageOverride, callbacks: Comp return true; } +export function commanderOptionsToBaseOptions(options: any): CompilerBaseOptions { + return { + // CompilerBaseOptions + logLevel: options.logLevel, + logFormat: options.logFormat, + color: options.color, + }; +} + /** * Maps command line compiler options to the compiler API options. * @param options * @param callbacks * @returns */ -export function commandOptionsToCompilerOptions(options: any, callbacks: CompilerCallbacks): ExtendedCompilerOptions { - const overrides = commandOptionsMessagesToCompilerOptionsMessages(options.message, callbacks); +export function commanderOptionsToCompilerOptions(options: any, callbacks: CompilerCallbacks): ExtendedCompilerOptions { + const overrides = commanderOptionsMessagesToCompilerOptionsMessages(options.message, callbacks); if(!overrides) { return null; } @@ -181,10 +190,7 @@ export function commandOptionsToCompilerOptions(options: any, callbacks: Compile // properties that we have in CompilerOptions, but nor do we want to rename // CompilerOptions properties... return { - // CompilerBaseOptions - logLevel: options.logLevel, - logFormat: options.logFormat, - color: options.color, + ...commanderOptionsToBaseOptions(options), // CompilerOptions shouldAddCompilerVersion: options.compilerVersion, saveDebug: options.debug, @@ -202,5 +208,5 @@ export function commandOptionsToCompilerOptions(options: any, callbacks: Compile */ export const unitTestEndpoints = { checkMessageOverride, - commandOptionsMessageToCompilerOptionsMessage + commanderOptionsMessageToCompilerOptionsMessage }; diff --git a/developer/src/kmc/src/util/kmcSentryOptions.ts b/developer/src/kmc/src/util/kmcSentryOptions.ts index bbb5c09a87..c6c47f732f 100644 --- a/developer/src/kmc/src/util/kmcSentryOptions.ts +++ b/developer/src/kmc/src/util/kmcSentryOptions.ts @@ -1,5 +1,19 @@ +import Sentry from "@sentry/node"; +import * as process from "node:process"; import { SentryNodeOptions } from "@keymanapp/developer-utils"; +Sentry.setContext('Command Line', { + // obfusate parameters with longer paths e.g. from 'c:/users/name/a/b' to + // '…/a/b' to minimize PII risk without losing all command line data. + // Also normalizes backslashes to slashes for simplicity. + argv: process.argv.map(param => { + const p = param.replaceAll('\\', '/').split('/'); + return (p.length < 3) + ? param + : ("…/" + p.slice(-2).join('/')); + }).join(' ') +}); + /** * Rewrites sourcemap paths for the esbuild distribution of kmc (as used in * Keyman Developer itself) //kmc.mjs to /dist/kmc.mjs, so that diff --git a/developer/src/kmc/test/extendedCompilerOptions.tests.ts b/developer/src/kmc/test/extendedCompilerOptions.tests.ts index adda29dfdb..b2f04c81b7 100644 --- a/developer/src/kmc/test/extendedCompilerOptions.tests.ts +++ b/developer/src/kmc/test/extendedCompilerOptions.tests.ts @@ -42,7 +42,7 @@ describe('commandOptionsMessageToCompilerOptionsMessage', function () { ]; for(const test of valid) { it(`should recognize valid message definition ${test.input}`, () => { - const result = unitTestEndpoints.commandOptionsMessageToCompilerOptionsMessage(test.input, callbacks); + const result = unitTestEndpoints.commanderOptionsMessageToCompilerOptionsMessage(test.input, callbacks); callbacks.printMessages(); assert.deepEqual(result, { code: CompilerError.error(test.result.code), level: test.result.level }); assert.isEmpty(callbacks.messages); @@ -58,7 +58,7 @@ describe('commandOptionsMessageToCompilerOptionsMessage', function () { ]; for(const test of invalid) { it(`should reject invalid message definition ${test.input}`, () => { - assert.isNull(unitTestEndpoints.commandOptionsMessageToCompilerOptionsMessage(test.input, callbacks)); + assert.isNull(unitTestEndpoints.commanderOptionsMessageToCompilerOptionsMessage(test.input, callbacks)); assert.isTrue(callbacks.hasMessage(test.code)); }); } diff --git a/developer/src/kmc/test/generateKmnKeyboard.tests.ts b/developer/src/kmc/test/generateKmnKeyboard.tests.ts index 5fd29f5310..811adc49b3 100644 --- a/developer/src/kmc/test/generateKmnKeyboard.tests.ts +++ b/developer/src/kmc/test/generateKmnKeyboard.tests.ts @@ -6,20 +6,21 @@ import 'mocha'; import { assert } from 'chai'; import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; -import { KeymanKeyboardGenerator } from '@keymanapp/kmc-generate'; +import { GeneratorOptions, KeymanKeyboardGenerator } from '@keymanapp/kmc-generate'; import { unitTestEndpoints } from '../src/commands/generate.js'; -import { InfrastructureMessages } from '../src/messages/infrastructureMessages.js'; +import { KeymanTargets } from '@keymanapp/common-types'; describe('generateKeymanKeyboard', function() { - const keyboardOptions = { - icon: true, + const keyboardOptions: GeneratorOptions = { + // icon: true, // id is written by generate functions + id: '', languageTags: ['en'], name: 'Sample Keyboard', outPath: '.', - targets: 'any', + targets: [ KeymanTargets.KeymanTarget.any ], version: '1.0', author: 'Keyman', copyright: 'Keyman', @@ -45,32 +46,11 @@ describe('generateKeymanKeyboard', function() { outPath = null; }); - it('should only allow a single id', async function() { - assert.isFalse(await unitTestEndpoints.doGenerate( - callbacks, - new KeymanKeyboardGenerator(), - ['sample1','sample2'], - {...keyboardOptions} - )); - assert.isTrue(callbacks.hasMessage(InfrastructureMessages.ERROR_GenerateRequiresId)); - }); - - it('should require an id parameter', async function() { - assert.isFalse(await unitTestEndpoints.doGenerate( - callbacks, - new KeymanKeyboardGenerator(), - null, - {...keyboardOptions} - )); - assert.isTrue(callbacks.hasMessage(InfrastructureMessages.ERROR_GenerateRequiresId)); - }); - it('should generate a Keyman keyboard project', async function() { assert.isTrue(await unitTestEndpoints.doGenerate( callbacks, new KeymanKeyboardGenerator(), - 'sample', - {...keyboardOptions, outPath} + {...keyboardOptions, id: 'sample', outPath} )); // Note: we won't test contents of files as this is tested in kmc-generate directly diff --git a/developer/src/kmc/test/generateLdmlKeyboard.tests.ts b/developer/src/kmc/test/generateLdmlKeyboard.tests.ts index ad885c3dab..f28d1fec8f 100644 --- a/developer/src/kmc/test/generateLdmlKeyboard.tests.ts +++ b/developer/src/kmc/test/generateLdmlKeyboard.tests.ts @@ -6,20 +6,21 @@ import 'mocha'; import { assert } from 'chai'; import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; -import { LdmlKeyboardGenerator } from '@keymanapp/kmc-generate'; +import { GeneratorOptions, LdmlKeyboardGenerator } from '@keymanapp/kmc-generate'; import { unitTestEndpoints } from '../src/commands/generate.js'; -import { InfrastructureMessages } from '../src/messages/infrastructureMessages.js'; +import { KeymanTargets } from '@keymanapp/common-types'; describe('generateLdmlKeyboard', function() { - const keyboardOptions = { - icon: true, + const keyboardOptions: GeneratorOptions = { + // icon: true, // id is written by generate functions + id: '', languageTags: ['en'], name: 'Sample Keyboard', outPath: '.', - targets: 'any', + targets: [KeymanTargets.KeymanTarget.any], version: '1.0', author: 'Keyman', copyright: 'Keyman', @@ -45,32 +46,11 @@ describe('generateLdmlKeyboard', function() { outPath = null; }); - it('should only allow a single id', async function() { - assert.isFalse(await unitTestEndpoints.doGenerate( - callbacks, - new LdmlKeyboardGenerator(), - ['sample1','sample2'], - { ...keyboardOptions } - )); - assert.isTrue(callbacks.hasMessage(InfrastructureMessages.ERROR_GenerateRequiresId)); - }); - - it('should require an id parameter', async function() { - assert.isFalse(await unitTestEndpoints.doGenerate( - callbacks, - new LdmlKeyboardGenerator(), - null, - { ...keyboardOptions } - )); - assert.isTrue(callbacks.hasMessage(InfrastructureMessages.ERROR_GenerateRequiresId)); - }); - it('should generate a LDML keyboard project', async function() { assert.isTrue(await unitTestEndpoints.doGenerate( callbacks, new LdmlKeyboardGenerator(), - 'sample', - { ...keyboardOptions, outPath } + { ...keyboardOptions, id: 'sample', outPath } )); // Note: we won't test contents of files as this is tested in kmc-generate directly diff --git a/developer/src/kmc/test/generateLexicalModel.tests.ts b/developer/src/kmc/test/generateLexicalModel.tests.ts index 00b01b489a..47a48098b8 100644 --- a/developer/src/kmc/test/generateLexicalModel.tests.ts +++ b/developer/src/kmc/test/generateLexicalModel.tests.ts @@ -5,20 +5,21 @@ import * as path from 'node:path'; import 'mocha'; import { assert } from 'chai'; -import { LexicalModelGenerator } from '@keymanapp/kmc-generate'; +import { GeneratorOptions, LexicalModelGenerator } from '@keymanapp/kmc-generate'; import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; import { unitTestEndpoints } from '../src/commands/generate.js'; -import { InfrastructureMessages } from '../src/messages/infrastructureMessages.js'; +import { KeymanTargets } from '@keymanapp/common-types'; describe('generateLexicalModel', function() { - const modelOptions = { - icon: true, + const modelOptions: GeneratorOptions = { + // icon: true, // id is written by generate functions + id: '', languageTags: ['en'], name: 'Sample Model', outPath: '.', - targets: 'any', + targets: [KeymanTargets.KeymanTarget.any], version: '1.0', author: 'Keyman', copyright: 'Keyman', @@ -44,32 +45,11 @@ describe('generateLexicalModel', function() { outPath = null; }); - it('should only allow a single id', async function() { - assert.isFalse(await unitTestEndpoints.doGenerate( - callbacks, - new LexicalModelGenerator(), - ['sample1.en.sample','sample2.en.sample'], - { ...modelOptions } - )); - assert.isTrue(callbacks.hasMessage(InfrastructureMessages.ERROR_GenerateRequiresId)); - }); - - it('should require an id parameter', async function() { - assert.isFalse(await unitTestEndpoints.doGenerate( - callbacks, - new LexicalModelGenerator(), - null, - { ...modelOptions } - )); - assert.isTrue(callbacks.hasMessage(InfrastructureMessages.ERROR_GenerateRequiresId)); - }); - it('should generate a lexical model project', async function() { assert.isTrue(await unitTestEndpoints.doGenerate( callbacks, new LexicalModelGenerator(), - 'sample.en.sample', - { ...modelOptions, outPath } + { ...modelOptions, id: 'sample.en.sample', outPath } )); // Note: we won't test contents of files as this is tested in kmc-generate directly diff --git a/developer/src/kmconvert/Keyman.Developer.System.KeyboardProjectTemplate.pas b/developer/src/kmconvert/Keyman.Developer.System.KeyboardProjectTemplate.pas index ce4add7717..1ba224d50b 100644 --- a/developer/src/kmconvert/Keyman.Developer.System.KeyboardProjectTemplate.pas +++ b/developer/src/kmconvert/Keyman.Developer.System.KeyboardProjectTemplate.pas @@ -258,6 +258,7 @@ begin f := TPackageContentFile.Create(kps); f.FileName := BasePath + ID + '\' + SFolder_Source + '\' + SFile_WelcomeHTM; kps.Files.Add(f); + kps.Options.WelcomeFile := f; // Add readme f := TPackageContentFile.Create(kps); diff --git a/developer/src/kmconvert/Keyman.Developer.System.LdmlKeyboardProjectTemplate.pas b/developer/src/kmconvert/Keyman.Developer.System.LdmlKeyboardProjectTemplate.pas index d8cfa9cd8f..77d93d8df9 100644 --- a/developer/src/kmconvert/Keyman.Developer.System.LdmlKeyboardProjectTemplate.pas +++ b/developer/src/kmconvert/Keyman.Developer.System.LdmlKeyboardProjectTemplate.pas @@ -217,6 +217,7 @@ begin f := TPackageContentFile.Create(kps); f.FileName := BasePath + ID + '\' + SFolder_Source + '\' + SFile_WelcomeHTM; kps.Files.Add(f); + kps.Options.WelcomeFile := f; // Add readme f := TPackageContentFile.Create(kps); diff --git a/developer/src/kmconvert/Keyman.Developer.System.ModelProjectTemplate.pas b/developer/src/kmconvert/Keyman.Developer.System.ModelProjectTemplate.pas index b5b64f6b86..6edd42dfa2 100644 --- a/developer/src/kmconvert/Keyman.Developer.System.ModelProjectTemplate.pas +++ b/developer/src/kmconvert/Keyman.Developer.System.ModelProjectTemplate.pas @@ -19,13 +19,11 @@ type procedure WriteWordlist; procedure WriteKPS; procedure WriteKPJ; - procedure WriteModelInfo; function GetModelFilename: string; function GetWordlistFilename: string; protected const - SFileTemplate_ModelInfo = '%s.model_info'; // in root SFileTemplate_Package = '%s.model.kps'; SFilename_Model = 'model.ts'; // Only for source SFilename_Wordlist = 'wordlist.tsv'; @@ -82,8 +80,6 @@ begin WriteKPJ; WriteRepositoryMetadata; - - WriteModelInfo; end; function TModelProjectTemplate.GetModelFilename: string; @@ -107,15 +103,6 @@ begin Format(SFolder_Source + '\' + SFileTemplate_Model, [ID])); end; -procedure TModelProjectTemplate.WriteModelInfo; -begin - // Write modelid.model_info - Transform( - Format(SFileTemplate_ModelInfo, ['model']), - Format(SFileTemplate_ModelInfo, [ID]) - ); -end; - procedure TModelProjectTemplate.WriteWordlist; begin Transform(SFolder_Source + '\' + SFilename_Wordlist); @@ -127,10 +114,13 @@ var begin kpj := TProject.Create(ptLexicalModel, GetProjectFilename, False); try + kpj.Options.Version := pv20; kpj.Options.BuildPath := '$PROJECTPATH\' + SFolder_Build; + kpj.Options.SourcePath := '$PROJECTPATH\' + SFolder_Source; kpj.Options.WarnDeprecatedCode := True; kpj.Options.CompilerWarningsAsErrors := True; kpj.Options.CheckFilenameConventions := True; + kpj.Options.SkipMetadataFiles := False; // Add model and package to project kpj.Files.Add(TmodelTsProjectFile.Create(kpj, GetModelFilename, nil)); @@ -140,7 +130,6 @@ begin kpj.Files.Add(TOpenableProjectFile.Create(kpj, BasePath + ID + '\' + SFile_HistoryMD, nil)); kpj.Files.Add(TOpenableProjectFile.Create(kpj, BasePath + ID + '\' + SFile_LicenseMD, nil)); kpj.Files.Add(TOpenableProjectFile.Create(kpj, BasePath + ID + '\' + SFile_ReadmeMD, nil)); - kpj.Files.Add(TOpenableProjectFile.Create(kpj, BasePath + ID + '\' + Format(SFileTemplate_ModelInfo, [ID]), nil)); kpj.Save; finally @@ -173,6 +162,7 @@ begin f := TPackageContentFile.Create(kps); f.FileName := BasePath + ID + '\' + SFolder_Source + '\' + SFile_WelcomeHTM; kps.Files.Add(f); + kps.Options.WelcomeFile := f; // Add readme f := TPackageContentFile.Create(kps); diff --git a/developer/src/packages.inc.sh b/developer/src/packages.inc.sh index 61b77e6438..62993c5ab9 100644 --- a/developer/src/packages.inc.sh +++ b/developer/src/packages.inc.sh @@ -5,6 +5,7 @@ readonly PACKAGES=( common/web/keyman-version + common/web/langtags common/web/types core/include/ldml developer/src/common/web/utils diff --git a/developer/src/tike/child/Keyman.Developer.UI.Debug.UfrmLdmlKeyboardDebug.pas b/developer/src/tike/child/Keyman.Developer.UI.Debug.UfrmLdmlKeyboardDebug.pas index 1712523026..1e22070ad7 100644 --- a/developer/src/tike/child/Keyman.Developer.UI.Debug.UfrmLdmlKeyboardDebug.pas +++ b/developer/src/tike/child/Keyman.Developer.UI.Debug.UfrmLdmlKeyboardDebug.pas @@ -39,7 +39,7 @@ uses UframeTextEditor, UfrmMDIEditor, UfrmTike, - UserMessages; + UserMessages, Keyman.Developer.UI.RichEdit41; type TLdmlDebugUIStatus = ( @@ -340,7 +340,7 @@ begin if not SetKeyEventContext then Exit(False); - if km_core_process_event(FDebugCore.State, Message.WParam, modifier, 1, KM_CORE_EVENT_FLAG_DEFAULT) = KM_CORE_STATUS_OK then + if km_core_process_event(FDebugCore.State, VKToScanCodeToVK(Message.WParam, GetKeyboardLayout(0)), modifier, 1, KM_CORE_EVENT_FLAG_DEFAULT) = KM_CORE_STATUS_OK then begin // Process keystroke -- true = swallow keystroke Result := True; diff --git a/developer/src/tike/child/UfrmDebug.pas b/developer/src/tike/child/UfrmDebug.pas index d322d406a7..36ef51d291 100644 --- a/developer/src/tike/child/UfrmDebug.pas +++ b/developer/src/tike/child/UfrmDebug.pas @@ -41,7 +41,7 @@ uses UfrmDebugStatus, UfrmMDIEditor, UfrmTike, - UserMessages; + UserMessages, Keyman.Developer.UI.RichEdit41; type TDebugLineEvent = procedure(Sender: TObject; ALine: Integer) of object; @@ -465,7 +465,7 @@ begin if not SetKeyEventContext then Exit(False); - if km_core_process_event(FDebugCore.State, Message.WParam, modifier, 1, KM_CORE_EVENT_FLAG_DEFAULT) = KM_CORE_STATUS_OK then + if km_core_process_event(FDebugCore.State, VKToScanCodeToVK(Message.WParam, GetKeyboardLayout(0)), modifier, 1, KM_CORE_EVENT_FLAG_DEFAULT) = KM_CORE_STATUS_OK then begin // Process keystroke -- true = swallow keystroke Result := True; diff --git a/developer/src/tike/child/UfrmPackageEditor.pas b/developer/src/tike/child/UfrmPackageEditor.pas index 5aa02b50b3..4f3a096bd9 100644 --- a/developer/src/tike/child/UfrmPackageEditor.pas +++ b/developer/src/tike/child/UfrmPackageEditor.pas @@ -319,7 +319,7 @@ type procedure UpdateStartMenuPrograms; procedure FillFileList(combo: TComboBox; obj: TObject; FileType: TKMFileType = ftOther); procedure UpdateImagePreviews; - procedure AddFile(FileName: WideString); + procedure AddFile(FileName: string); procedure SourceChanged(Sender: TObject); procedure MovePackageToSource; function MoveSourceToPackage: Boolean; @@ -381,6 +381,7 @@ implementation uses System.Math, + System.StrUtils, Vcl.Clipbrd, Vcl.Imaging.GifImg, @@ -708,7 +709,7 @@ begin mtConfirmation, mbOkCancel, 0) = mrOk; end; -procedure TfrmPackageEditor.AddFile(FileName: WideString); +procedure TfrmPackageEditor.AddFile(FileName: string); var f: TPackageContentFile; ki: TKeyboardInfo; @@ -771,7 +772,7 @@ begin end; pack.Files.Add(f); - lbFiles.ItemIndex := lbFiles.Items.AddObject(ExtractFileName(FileName), f); + lbFiles.ItemIndex := lbFiles.Items.AddObject(ExtractFileName(FileName.Replace('/','\',[rfReplaceAll])), f); lbFilesClick(lbFiles); UpdateWelcomeFile; UpdateReadme; @@ -1315,7 +1316,7 @@ begin lbFiles.Clear; for i := 0 to pack.Files.Count - 1 do - lbFiles.Items.AddObject(ExtractFileName(pack.Files[i].FileName), pack.Files[i]); + lbFiles.Items.AddObject(ExtractFileName(ReplaceStr(pack.Files[i].FileName,'/','\')), pack.Files[i]); UpdateOutPath; UpdateWelcomeFile; diff --git a/developer/src/tike/compile/Keyman.Developer.System.KmcWrapper.pas b/developer/src/tike/compile/Keyman.Developer.System.KmcWrapper.pas index 24c070a5e8..168ea8c77d 100644 --- a/developer/src/tike/compile/Keyman.Developer.System.KmcWrapper.pas +++ b/developer/src/tike/compile/Keyman.Developer.System.KmcWrapper.pas @@ -1,20 +1,30 @@ +(* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Wrapper functions for the kmc command line tool + *) unit Keyman.Developer.System.KmcWrapper; interface uses - Keyman.Developer.System.Project.ProjectFile; + Keyman.Developer.System.Project.ProjectFile, + utilexecute; type TKmcWrapper = class + private + function Run(const parameters: TArray; const path, logFilename: string; Callback: TUtilExecuteCallbackEvent): Boolean; public - function Compile(ProjectFile: TProjectFile; const infile, outfile: string; debug: Boolean): Boolean; + function Compile(ProjectFile: TProjectFile; const infile, outfile: string; debug: Boolean; Callback: TUtilExecuteCallbackEvent = nil): Boolean; + function Copy(const source, dest, cwd: string; relocateExternal: Boolean; Callback: TUtilExecuteCallbackEvent = nil): Boolean; end; const // TODO: these are still used for messages raised from the IDE, perhaps need to be factored out CERR_ERROR = $00004000; CERR_WARNING = $00002000; + CERR_INFO = $00000000; implementation @@ -24,8 +34,8 @@ uses Keyman.Developer.System.Project.Project, Keyman.Developer.System.Project.ProjectLog, - Keyman.Developer.System.KeymanDeveloperPaths, - utilexecute; + Keyman.Developer.UI.Project.ProjectFileUI, + Keyman.Developer.System.KeymanDeveloperPaths; { TKmcWrapper } @@ -33,9 +43,71 @@ function TKmcWrapper.Compile( ProjectFile: TProjectFile; const infile: string; const outfile: string; - debug: Boolean + debug: Boolean; + Callback: TUtilExecuteCallbackEvent ): Boolean; var + cmdline: TArray; +begin + cmdline := [ + 'build', + '--log-format', 'tsv', + '--log-level', 'info', + infile + ]; + + if outfile <> '' then + cmdline := cmdline + ['--out-file', outfile]; + + if Assigned(FGlobalProject) and (FGlobalProject.Options.CompilerWarningsAsErrors) then + cmdline := cmdline + ['--compiler-warnings-as-errors'] + else + cmdline := cmdline + ['--no-compiler-warnings-as-errors']; + + if not FGlobalProject.Options.WarnDeprecatedCode then + cmdline := cmdline + ['--no-warn-deprecated-code']; + + if debug then + cmdline := cmdline + ['--debug']; + + Result := Run(cmdline, ExtractFileDir(infile), infile, Callback); +end; + +function TKmcWrapper.Copy(const source, dest, cwd: string; + relocateExternal: Boolean; + Callback: TUtilExecuteCallbackEvent): Boolean; +var + destFilename, destPath: string; + cmdline: TArray; +begin + if not dest.EndsWith('.kpj') then + raise Exception.Create('Invalid destination "'+dest+'" for TKmcWrapper.copy: must be a filename ending in .kpj'); + destFilename := ChangeFileExt(ExtractFileName(dest), ''); + destPath := ExtractFileDir(dest); + if ExtractFileName(destPath) <> destFilename then + raise Exception.Create('Invalid destination "'+dest+'" for TKmcWrapper.copy: filename must match target folder name'); + + cmdline := [ + 'copy', + '--out-path', destPath, + '--log-format', 'tsv', + '--log-level', 'info', + source + ]; + + if relocateExternal then + cmdline := cmdline + ['--relocate-external']; + + Result := Run(cmdline, cwd, dest, Callback); +end; + +function TKmcWrapper.Run( + const parameters: TArray; + const path, logFilename: string; + Callback: TUtilExecuteCallbackEvent +): Boolean; +var + param: string; s: TStringList; logtext, cmdline: string; ec: Integer; @@ -53,26 +125,23 @@ begin // TODO: log-level as opt? // TODO: embed in server for better performance - cmdline := Format('"%s" build --log-format tsv --log-level info "%s"', [TKeymanDeveloperPaths.KmcPath, infile]); - if outfile <> '' then - cmdline := cmdline + Format(' --out-file "%s"', [outfile]); - if FGlobalProject.Options.CompilerWarningsAsErrors then - cmdline := cmdline + ' --compiler-warnings-as-errors' - else - cmdline := cmdline + ' --no-compiler-warnings-as-errors'; - if FGlobalProject.Options.WarnDeprecatedCode then - cmdline := cmdline + ' --no-warn-deprecated-code'; - if debug then - cmdline := cmdline + ' --debug'; + cmdline := '"'+TKeymanDeveloperPaths.KmcPath+'"'; + for param in parameters do + begin + cmdline := cmdline + ' "'+param+'"'; + end; - Result := TUtilExecute.Console(cmdline, ExtractFileDir(infile), logtext, ec); + // Log the precise command line used + LogMessage(plsInfo, logFilename, cmdline, CERR_INFO, 0); + + Result := TUtilExecute.Console(cmdline, path, logtext, ec, Callback); logtext := UTF8ToString(AnsiString(logtext)); if not Result then begin - FGlobalProject.Log(plsError, infile, - Format('Compiler failed to start with error %d: %s', [GetLastError, SysErrorMessage(GetLastError)]), CERR_ERROR, 0); + LogMessage(plsError, logFilename, + Format('kmc failed to start with error %d: %s', [GetLastError, SysErrorMessage(GetLastError)]), CERR_ERROR, 0); end; Result := Result and (ec = 0); @@ -101,7 +170,7 @@ begin else if msgType = 'warn' then begin state := plsWarning; - if FGlobalProject.Options.CompilerWarningsAsErrors then + if Assigned(FGlobalProject) and FGlobalProject.Options.CompilerWarningsAsErrors then Result := False; end else if msgType = 'error' then @@ -116,10 +185,10 @@ begin end else // assume msgType = 'info' (fine also for 'verbose', 'debug') state := plsInfo; - FGlobalProject.Log(state, msgFilename, msgText, msgCode, msgLine); + LogMessage(state, msgFilename, msgText, msgCode, msgLine); end else - FGlobalProject.Log(plsInfo, infile, line, 0, 0); + LogMessage(plsInfo, logFilename, line, 0, 0); end; finally s.Free; diff --git a/developer/src/tike/help/Keyman.Developer.System.HelpTopics.pas b/developer/src/tike/help/Keyman.Developer.System.HelpTopics.pas index 3e150aa42c..3b9301768e 100644 --- a/developer/src/tike/help/Keyman.Developer.System.HelpTopics.pas +++ b/developer/src/tike/help/Keyman.Developer.System.HelpTopics.pas @@ -7,6 +7,9 @@ const SHelpTopic_Context_BitmapEditorText = 'context/keyboard-editor#toc-icon-tab'; SHelpTopic_Context_CharacterMap = 'context/character-map'; SHelpTopic_Context_CharacterIdentifier = 'context/character-identifier'; + SHelpTopic_Context_CloneGitHubProjectParameters = 'context/clone-github-project-parameters'; + SHelpTopic_Context_CloneKeymanCloudProjectParameters = 'context/clone-keyman-cloud-project-parameters'; + SHelpTopic_Context_CloneLocalProjectParameters = 'context/clone-local-project-parameters'; SHelpTopic_Context_Debug = 'context/debug'; SHelpTopic_Context_LdmlDebug = 'context/ldml-debug'; SHelpTopic_Context_LdmlEditor = 'context/ldml-editor'; diff --git a/developer/src/tike/main/UfrmMessages.dfm b/developer/src/tike/main/UfrmMessages.dfm index bed2632463..aed03fb40c 100644 --- a/developer/src/tike/main/UfrmMessages.dfm +++ b/developer/src/tike/main/UfrmMessages.dfm @@ -26,7 +26,6 @@ inherited frmMessages: TfrmMessages Font.Name = 'Courier New' Font.Style = [] ParentFont = False - PopupMenu = mnuPopup ReadOnly = True ScrollBars = ssBoth TabOrder = 0 @@ -35,6 +34,8 @@ inherited frmMessages: TfrmMessages OnClick = memoMessageClick OnDblClick = memoMessageDblClick OnKeyDown = memoMessageKeyDown + OnMouseDown = memoMessageMouseDown + OnMouseMove = memoMessageMouseMove end object dlgSave: TSaveDialog DefaultExt = 'txt' @@ -51,6 +52,10 @@ inherited frmMessages: TfrmMessages Default = True OnClick = mnuViewItemClick end + object mnuOpenDocumentation: TMenuItem + Caption = 'Open &documentation on selected message' + OnClick = mnuOpenDocumentationClick + end object mnuSeparator1: TMenuItem Caption = '-' end diff --git a/developer/src/tike/main/UfrmMessages.pas b/developer/src/tike/main/UfrmMessages.pas index f57e097fc4..9a76254c96 100644 --- a/developer/src/tike/main/UfrmMessages.pas +++ b/developer/src/tike/main/UfrmMessages.pas @@ -33,7 +33,7 @@ interface uses System.Classes, - System.Contnrs, + System.Generics.Collections, System.SysUtils, Vcl.Controls, Vcl.Dialogs, @@ -54,12 +54,24 @@ uses UfrmTike, Vcl.ComCtrls; type + TMessageItemSegment = record + text: string; + color: TColor; + underline: Boolean; + index: Integer; + end; + TMessageItem = class FileName: string; Msg: string; MsgCode, Line: Integer; + + Segments: TArray; + LineLength: Integer; end; + TMessageItemList = class(TObjectList); + TfrmMessages = class(TTikeDockForm) dlgSave: TSaveDialog; memoMessage: TRichEdit; @@ -71,6 +83,7 @@ type cmdmNextMessage: TMenuItem; mnuViewItem: TMenuItem; mnuSeparator1: TMenuItem; + mnuOpenDocumentation: TMenuItem; procedure memoMessageDblClick(Sender: TObject); procedure cmdmSaveToFileClick(Sender: TObject); procedure cmdmClearClick(Sender: TObject); @@ -83,8 +96,13 @@ type procedure mnuPopupPopup(Sender: TObject); procedure mnuViewItemClick(Sender: TObject); procedure FormDestroy(Sender: TObject); + procedure memoMessageMouseMove(Sender: TObject; Shift: TShiftState; X, + Y: Integer); + procedure mnuOpenDocumentationClick(Sender: TObject); + procedure memoMessageMouseDown(Sender: TObject; Button: TMouseButton; + Shift: TShiftState; X, Y: Integer); private - FMessageItems: TObjectList; + FMessageItems: TMessageItemList; function GetSelLine: Integer; procedure SetSelLine(Value: Integer); protected @@ -107,6 +125,8 @@ var implementation uses + System.RegularExpressions, + System.Types, Winapi.RichEdit, UfrmMain, @@ -116,7 +136,9 @@ uses Keyman.Developer.System.Project.Project, Keyman.Developer.System.Project.ProjectFile, Keyman.Developer.UI.Project.ProjectFileUI, - Keyman.Developer.UI.Project.ProjectUI; + Keyman.Developer.UI.Project.ProjectUI, + Upload_Settings, + utilexecute; {$R *.DFM} @@ -149,24 +171,29 @@ const // ABGR - text on white bg, Colors similar to Light+ VSCode color theme INFO_ProjectBuiltSuccessfully = NAMESPACE_Infrastructure or $000B; INFO_ProjectNotBuiltSuccessfully = NAMESPACE_Infrastructure or $000C; + Segment_Filename = 0; + Segment_Filename_Separator = 1; + Segment_Line = 2; + Segment_Line_Separator = 3; + Segment_State = 4; + Segment_Code = 5; + Segment_Code_Separator = 6; + Segment_Message = 7; procedure TfrmMessages.Add(state: TProjectLogState; filename, msg: WideString; MsgCode, line: Integer); -type - TSegment = record - text: string; - color: TColor; - end; var mi: TMessageItem; FColor: TColor; FTextColor: TColor; - Segments: array of TSegment; + Segments: TArray; + i, x: Integer; - procedure AddText(const text: string; color: TColor); + procedure AddText(const text: string; color: TColor; underline: Boolean = false); var - s: TSegment; + s: TMessageItemSegment; begin s.text := text; + s.underline := underline; s.color := color; SetLength(Segments, Length(Segments)+1); Segments[High(Segments)] := s; @@ -175,7 +202,7 @@ var procedure AddLine; var t: string; - s: TSegment; + s: TMessageItemSegment; cr: TCharRange; cf: TCharFormat; gtle: TGetTextLengthEx; @@ -184,7 +211,7 @@ var // use direct messages to the RichEdit control to emit formatted text FillChar(cf, sizeof(TCharFormat), 0); cf.cbSize := sizeof(TCharFormat); - cf.dwMask := CFM_COLOR; + cf.dwMask := CFM_COLOR or CFM_UNDERLINE; gtle.flags := GTL_PRECISE or GTL_NUMCHARS; gtle.codepage := 1200; @@ -198,10 +225,13 @@ var for s in Segments do begin + if s.Text.Length = 0 then + Continue; cr.cpMin := cr.cpMax; cr.cpMax := cr.cpMax + s.text.Length; SendMessage(memoMessage.Handle, EM_EXSETSEL, 0, NativeUint(@cr)); cf.crTextColor := ColorToRGB(s.color); + if s.underline then cf.dwEffects := CFE_UNDERLINE else cf.dwEffects := 0; SendMessage(memoMessage.Handle, EM_SETCHARFORMAT, SCF_SELECTION, NativeUint(@cf)); end; cr.cpMin := cr.cpMax; @@ -251,15 +281,45 @@ begin begin AddText(':', Color_Text); AddText(IntToStr(Line), Color_Line); + end + else + begin + // Keep same number of segments + AddText('', Color_Text); + AddText('', Color_Text); end; AddText(' - ', Color_Text); - AddText(ProjectLogStateTitle[state], FColor); - AddText(' KM'+IntToHex(MsgCode, 5), Color_Code); + + if MsgCode = 0 then + begin + AddText(ProjectLogStateTitle[state], FColor); + AddText('', Color_Text); + end + else + begin + AddText(ProjectLogStateTitle[state]+' ', FColor); + AddText('KM'+IntToHex(MsgCode, 5), Color_Code, True); + end; + AddText(': ', Color_Text); AddText(StringReplace(msg, #13#10, ' ', [rfReplaceAll]), FTextColor); AddText(#13#10, clBlack); + + // Calculate segment lengths for later cursor + link calculations + + x := 0; + for i := 0 to High(Segments) - 1 do + begin + Segments[i].index := x; Inc(x, Segments[i].text.Length); + end; + mi.Segments := Segments; + mi.LineLength := x; + AddLine; + + // Strip #$D#$A from segments for further calculations + SetLength(mi.Segments, Length(mi.Segments) - 1); finally SendMessage(memoMessage.Handle, WM_SETREDRAW, 1, 0); SendMessage(memoMessage.Handle, EM_SETEVENTMASK, 0, eventMask); @@ -293,7 +353,7 @@ begin SelLine := line; - mi := FMessageItems[line] as TMessageItem; + mi := FMessageItems[line]; FFilename := mi.FileName; if FFileName = FGlobalProject.FileName then @@ -333,6 +393,74 @@ begin Key := 0; end; +procedure TfrmMessages.memoMessageMouseDown(Sender: TObject; + Button: TMouseButton; Shift: TShiftState; X, Y: Integer); +var + pt: TPointL; + ch: Integer; + px: TPoint; +begin + if Button = mbRight then + begin + pt.x := X; + pt.y := Y; + ch := SendMessage(memoMessage.Handle, EM_CHARFROMPOS, 0, NativeInt(@pt)); + memoMessage.SelStart := ch; + memoMessage.SelLength := 0; + px := memoMessage.ClientToScreen(Point(X, Y)); + mnuPopup.Popup(px.X, px.Y); + end; +end; + +procedure TfrmMessages.memoMessageMouseMove(Sender: TObject; Shift: TShiftState; + X, Y: Integer); +var + pt: TPOINTL; + ch: Integer; + Row, Col, i: Integer; + mi: TMessageItem; + seg: TMessageItemSegment; +begin + pt.x := X; + pt.y := Y; + ch := SendMessage(memoMessage.Handle, EM_CHARFROMPOS, 0, NativeInt(@pt)); + + x := 0; + Row := -1; + for I := 0 to FMessageItems.Count - 1 do + begin + if x + FMessageItems[i].LineLength >= ch then + begin + Row := i; + Break; + end; + Inc(x, FMessageItems[i].LineLength + 1); + end; + + if Row < 0 then + begin + // Mouse is outside bounds of text + memoMessage.Cursor := crDefault; + Exit; + end; + + Col := ch - x; + mi := FMessageItems[Row]; + + if Length(mi.Segments) <= Segment_Code then + begin + // Safety check + memoMessage.Cursor := crDefault; + Exit; + end; + + seg := mi.Segments[Segment_Code]; + + if (Col >= seg.index) and (Col < seg.index + seg.text.Length) + then memoMessage.Cursor := crHandPoint + else memoMessage.Cursor := crDefault; +end; + procedure TfrmMessages.cmdmSaveToFileClick(Sender: TObject); begin if dlgSave.Execute then @@ -433,16 +561,37 @@ begin memoMessage.SelLength := Length(memoMessage.Lines[Value]); end; +procedure TfrmMessages.mnuOpenDocumentationClick(Sender: TObject); +var + line: Integer; + mi: TMessageItem; +begin + line := SelLine; + if line < 0 then Exit; + mi := FMessageItems[line]; + if Length(mi.Segments) <= Segment_Code then + Exit; + + TUtilExecute.URL(URL_KmcMessage(mi.Segments[Segment_Code].text)); +end; + procedure TfrmMessages.mnuPopupPopup(Sender: TObject); var e: Boolean; + line: Integer; begin e := memoMessage.Lines.Count > 0; - cmdmNextMessage.Enabled := e and (SelLine < memoMessage.Lines.Count - 1); - cmdmPreviousMessage.Enabled := e and (SelLine > 0); + if e + then line := SelLine + else line := -1; + cmdmNextMessage.Enabled := e and (line < memoMessage.Lines.Count - 1); + cmdmPreviousMessage.Enabled := e and (line > 0); + e := e and (line >= 0) and (line < FMessageItems.Count); + mnuViewItem.Enabled := e; - cmdmClear.Enabled := e; - cmdmSaveToFile.Enabled := e; + mnuOpenDocumentation.Enabled := e; + cmdmClear.Enabled := FMessageItems.Count > 0; + cmdmSaveToFile.Enabled := FMessageItems.Count > 0; end; procedure TfrmMessages.mnuViewItemClick(Sender: TObject); @@ -451,14 +600,30 @@ begin end; procedure TfrmMessages.memoMessageClick(Sender: TObject); +var + p: TPoint; + mi: TMessageItem; + seg: TMessageItemSegment; begin - ; + p := memoMessage.CaretPos; + if (p.Y < 0) or (p.Y >= FMessageItems.Count) then + Exit; + + mi := FMessageItems[p.Y]; + if Length(mi.Segments) <= Segment_Code then + Exit; + + seg := mi.Segments[Segment_Code]; + if (p.X >= seg.index) and (p.X < seg.index + seg.text.Length) then + begin + TUtilExecute.URL(URL_KmcMessage(seg.text)); + end; end; procedure TfrmMessages.FormCreate(Sender: TObject); begin inherited; - FMessageItems := TObjectList.Create; + FMessageItems := TMessageItemList.Create; end; procedure TfrmMessages.FormDestroy(Sender: TObject); diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.ProjectFileUI.pas b/developer/src/tike/project/Keyman.Developer.UI.Project.ProjectFileUI.pas index ee5c4cb126..dccd28a650 100644 --- a/developer/src/tike/project/Keyman.Developer.UI.Project.ProjectFileUI.pas +++ b/developer/src/tike/project/Keyman.Developer.UI.Project.ProjectFileUI.pas @@ -83,6 +83,8 @@ type TProjectFileUIClass = class of TProjectFileUI; +procedure LogMessage(AState: TProjectLogState; Filename, Msg: string; MsgCode, line: Integer); + implementation uses @@ -181,6 +183,11 @@ begin frmMessages.Add(AState, Filename, Msg, MsgCode, line); end; +procedure LogMessage(AState: TProjectLogState; Filename, Msg: string; MsgCode, line: Integer); +begin + frmMessages.Add(AState, Filename, Msg, MsgCode, line); +end; + procedure TProjectUI.Refresh; begin if State = psReady then diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneGitHubProjectParameters.dfm b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneGitHubProjectParameters.dfm new file mode 100644 index 0000000000..006aa4b07f --- /dev/null +++ b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneGitHubProjectParameters.dfm @@ -0,0 +1,175 @@ +inherited frmCloneGitHubProjectParameters: TfrmCloneGitHubProjectParameters + BorderIcons = [biSystemMenu] + BorderStyle = bsDialog + Caption = 'Clone Project from GitHub' + ClientHeight = 297 + ClientWidth = 738 + Position = poScreenCenter + ExplicitWidth = 744 + ExplicitHeight = 326 + PixelsPerInch = 96 + TextHeight = 13 + object cmdOK: TButton + Left = 578 + Top = 264 + Width = 73 + Height = 25 + Anchors = [akRight, akBottom] + Caption = 'OK' + Default = True + TabOrder = 2 + OnClick = cmdOKClick + ExplicitLeft = 441 + ExplicitTop = 236 + end + object cmdCancel: TButton + Left = 657 + Top = 264 + Width = 73 + Height = 25 + Anchors = [akRight, akBottom] + Cancel = True + Caption = 'Cancel' + ModalResult = 2 + TabOrder = 3 + ExplicitLeft = 520 + ExplicitTop = 236 + end + object gbNewProjectDetails: TGroupBox + Left = 9 + Top = 103 + Width = 720 + Height = 154 + Caption = 'New Project Details' + TabOrder = 1 + object lblFileName: TLabel + Left = 16 + Top = 24 + Width = 76 + Height = 13 + Caption = '&New project ID:' + FocusControl = editProjectID + end + object lblProjectFilename: TLabel + Left = 15 + Top = 126 + Width = 101 + Height = 13 + Caption = 'New project &filename' + FocusControl = editProjectFilename + end + object lblPath: TLabel + Left = 15 + Top = 99 + Width = 83 + Height = 13 + Caption = 'Destination &path:' + FocusControl = editPath + end + object lblSourceProjectLexicalModel: TLabel + Left = 15 + Top = 48 + Width = 531 + Height = 13 + Caption = + 'The source project appears to be a lexical model. The new projec' + + 't ID must use the author.bcp47.uniq pattern.' + end + object lblSourceProjectKeyboard: TLabel + Left = 16 + Top = 48 + Width = 682 + Height = 13 + Caption = + 'The source project appears to be a keyboard project. The new pro' + + 'ject ID must be a valid keyboard id pattern (alphanumeric and un' + + 'derscore).' + end + object editProjectID: TEdit + Left = 148 + Top = 21 + Width = 205 + Height = 21 + TabOrder = 0 + OnChange = editProjectIDChange + end + object editProjectFilename: TEdit + Left = 147 + Top = 123 + Width = 564 + Height = 21 + TabStop = False + ParentColor = True + ReadOnly = True + TabOrder = 4 + OnChange = editProjectIDChange + end + object cmdBrowse: TButton + Left = 644 + Top = 96 + Width = 67 + Height = 21 + Caption = '&Browse...' + TabOrder = 3 + OnClick = cmdBrowseClick + end + object editPath: TEdit + Left = 147 + Top = 96 + Width = 491 + Height = 21 + TabOrder = 2 + OnChange = editGitHubURLChange + end + object chkRelocateExternal: TCheckBox + Left = 147 + Top = 72 + Width = 257 + Height = 17 + Caption = 'Relocate &external files into new project folder' + TabOrder = 1 + end + end + object gbSourceProjectDetails: TGroupBox + Left = 8 + Top = 8 + Width = 721 + Height = 89 + Caption = 'Source Project' + TabOrder = 0 + object Label1: TLabel + Left = 17 + Top = 19 + Width = 58 + Height = 13 + Caption = 'GitHub URL:' + FocusControl = editGitHubURL + end + object Label2: TLabel + Left = 17 + Top = 43 + Width = 512 + Height = 13 + Caption = + 'A URL to a GitHub repository (or subfolder within a repository) ' + + 'that matches the Keyman project file layout' + end + object Label3: TLabel + Left = 17 + Top = 62 + Width = 420 + Height = 13 + Caption = + 'e.g. https://github.com/keyman-keyboards/khmer_angkor/tree/main/' + + 'khmer_angkor.kpj' + end + object editGitHubURL: TEdit + Left = 149 + Top = 16 + Width = 563 + Height = 21 + TabOrder = 0 + OnChange = editGitHubURLChange + end + end +end diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneGitHubProjectParameters.pas b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneGitHubProjectParameters.pas new file mode 100644 index 0000000000..ad042fc872 --- /dev/null +++ b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneGitHubProjectParameters.pas @@ -0,0 +1,370 @@ +(* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Parameters dialog for cloning a project from a GitHub repo + *) +unit Keyman.Developer.UI.Project.UfrmCloneGitHubProjectParameters; + +interface + +uses + System.Classes, + System.SysUtils, + System.UITypes, + System.Variants, + Winapi.Messages, + Winapi.Windows, + Vcl.CheckLst, + Vcl.Controls, + Vcl.Dialogs, + Vcl.ExtCtrls, + Vcl.Forms, + Vcl.Graphics, + Vcl.Grids, + Vcl.StdCtrls, + + kpsfile, + PackageInfo, + UfrmTike, + UKeymanTargets, + UfrmDownloadProgress, + utilfiletypes, + Browse4Folder; + +type + TfrmCloneGitHubProjectParameters = class(TTikeForm) + cmdOK: TButton; + cmdCancel: TButton; + gbNewProjectDetails: TGroupBox; + lblFileName: TLabel; + lblProjectFilename: TLabel; + editProjectID: TEdit; + editProjectFilename: TEdit; + lblPath: TLabel; + cmdBrowse: TButton; + editPath: TEdit; + chkRelocateExternal: TCheckBox; + gbSourceProjectDetails: TGroupBox; + Label1: TLabel; + editGitHubURL: TEdit; + Label2: TLabel; + Label3: TLabel; + lblSourceProjectLexicalModel: TLabel; + lblSourceProjectKeyboard: TLabel; + procedure cmdOKClick(Sender: TObject); + procedure editSourceProjectFilenameChange(Sender: TObject); + procedure FormCreate(Sender: TObject); + procedure editVersionChange(Sender: TObject); + procedure editGitHubURLChange(Sender: TObject); + procedure editProjectIDChange(Sender: TObject); + procedure cmdBrowseClick(Sender: TObject); + private + dlgBrowse: TBrowse4Folder; + frmDownloadProgress: TfrmDownloadProgress; + function GetBasePath: string; + function GetProjectID: string; + function Validate: Boolean; + procedure EnableControls; + procedure UpdateProjectFilename; + function GetProjectFilename: string; + function GetSourceProjectFilename: string; + function GetRelocateExternal: Boolean; + procedure DownloadCallback(Owner: TfrmDownloadProgress; var Result: Boolean); + procedure DownloadWrapperCallback(var Cancelled: Boolean); + function IsSourceProjectALexicalModelProject: Boolean; + procedure UpdateSourceProjectNote; + protected + function GetHelpTopic: string; override; + public + property BasePath: string read GetBasePath; + property ProjectID: string read GetProjectID; + property ProjectFilename: string read GetProjectFilename; + property SourceProjectFilename: string read GetSourceProjectFilename; + property RelocateExternal: Boolean read GetRelocateExternal; + end; + +function ShowCloneGitHubProjectParameters(Owner: TComponent): Boolean; + +implementation + +uses + System.Net.UrlClient, + System.RegularExpressions, + Vcl.ComCtrls, + + KeymanDeveloperOptions, + Keyman.Developer.System.KmcWrapper, + Keyman.Developer.System.HelpTopics, + Keyman.Developer.System.Project.Project, + Keyman.Developer.System.Project.ProjectFile, + Keyman.System.KeyboardUtils, + Keyman.System.LexicalModelUtils, + UfrmMain; + +{$R *.dfm} + +function ShowCloneGitHubProjectParameters(Owner: TComponent): Boolean; +var + f: TfrmCloneGitHubProjectParameters; + path: string; + p: TfrmDownloadProgress; +begin + f := TfrmCloneGitHubProjectParameters.Create(Owner); + try + Result := f.ShowModal = mrOk; + if not Result then + Exit; + + path := ExtractFileDir(f.BasePath); + if not ForceDirectories(path) then + begin + ShowMessage('Unable to create path "'+path+'": '+SysErrorMessage(GetLastError)); + Exit; + end; + + p := TfrmDownloadProgress.Create(Owner); + try + p.Callback := f.DownloadCallback; + p.progress.Style := pbstMarquee; + Result := p.ShowModal = mrOk; + finally + p.Free; + end; + finally + f.Free; + end; +end; + +{ TfrmCloneGitHubProjectParameters } + +procedure TfrmCloneGitHubProjectParameters.DownloadWrapperCallback(var Cancelled: Boolean); +begin + Application.ProcessMessages; + Cancelled := frmDownloadProgress.Cancel; +end; + +procedure TfrmCloneGitHubProjectParameters.DownloadCallback( + Owner: TfrmDownloadProgress; var Result: Boolean); +var + wrapper: TKmcWrapper; + path: string; +begin + Result := False; + path := ExtractFileDir(Self.BasePath); + + Update; + frmDownloadProgress := Owner; + wrapper := TKmcWrapper.Create; + try + if wrapper.Copy(Self.SourceProjectFilename, Self.ProjectFilename, path, Self.RelocateExternal, DownloadWrapperCallback) then + begin + frmKeymanDeveloper.OpenProject(Self.ProjectFilename); + Result := True; + end; + finally + wrapper.Free; + end; + frmDownloadProgress := nil; +end; + +procedure TfrmCloneGitHubProjectParameters.FormCreate(Sender: TObject); +begin + inherited; + editPath.Text := FKeymanDeveloperOptions.DefaultProjectPath; + + dlgBrowse := TBrowse4Folder.Create(Self); + dlgBrowse.InitialDir := editPath.Text; + dlgBrowse.Options := [OnlySelectFileSysDir, ShowEditBox, UseNewDialogStyle]; + dlgBrowse.Root := Desktop; + dlgBrowse.Title := 'Select folder to save project to'; + + UpdateSourceProjectNote; + EnableControls; +end; + +procedure TfrmCloneGitHubProjectParameters.cmdBrowseClick(Sender: TObject); +begin + dlgBrowse.InitialDir := editPath.Text; + + if dlgBrowse.Execute and (dlgBrowse.FileName <> '') then + editPath.Text := ExcludeTrailingPathDelimiter(dlgBrowse.FileName); +end; + +procedure TfrmCloneGitHubProjectParameters.cmdOKClick(Sender: TObject); +begin + if Validate then + ModalResult := mrOk; +end; + +procedure TfrmCloneGitHubProjectParameters.UpdateSourceProjectNote; +begin + lblSourceProjectLexicalModel.Visible := IsSourceProjectALexicalModelProject; + lblSourceProjectKeyboard.Visible := (editGitHubURL.Text <> '') and not IsSourceProjectALexicalModelProject; +end; + +procedure TfrmCloneGitHubProjectParameters.editProjectIDChange(Sender: TObject); +begin + UpdateProjectFilename; + EnableControls; +end; + +procedure TfrmCloneGitHubProjectParameters.editSourceProjectFilenameChange(Sender: TObject); +begin + EnableControls; +end; + +procedure TfrmCloneGitHubProjectParameters.editGitHubURLChange(Sender: TObject); +begin + UpdateProjectFilename; + UpdateSourceProjectNote; + EnableControls; +end; + +procedure TfrmCloneGitHubProjectParameters.editVersionChange(Sender: TObject); +begin + EnableControls; +end; + +function IsValidGitHubProjectURL(const uri: string): Boolean; +const + // From github-urls.ts (minor RE tweaks to match required format for PCRE + + {** + * Matches only a GitHub permanent raw URI with a commit hash, without any other + * components; note hash is called branch to match other URI formats + *} + GITHUB_STABLE_SOURCE = '^https:\/\/github\.com\/(?[a-zA-Z0-9-]+)\/(?[\w\.-]+)\/raw\/(?[a-f0-9]{40})\/(?.+)$'; + + {** + * Matches any GitHub git resource raw 'user content' URI which can be + * translated to a permanent URI with a commit hash + *} + GITHUB_RAW_URI = '^https:\/\/raw\.githubusercontent\.com\/(?[a-zA-Z0-9-]+)\/(?[\w\.-]+)\/(?:refs\/(?:heads|tags)\/)?(?[^\/]+)\/(?.+)$'; + + {** + * Matches any GitHub git resource raw URI which can be translated to a + * permanent URI with a commit hash + *} + GITHUB_URI = '^https:\/\/github\.com\/(?[a-zA-Z0-9-]+)\/(?[\w\.-]+)\/(?:raw|blob|tree)\/(?:refs\/(?:heads|tags)\/)?(?[^\/]+)\/(?.+)$'; + + {** + * Matches any GitHub git resource raw URI which can be translated to a + * permanent URI with a commit hash, with the http[s] protocol optional, for + * matching user-supplied URLs. groups are: `owner`, `repo`, `branch`, and + * `path`. + *} + GITHUB_URI_OPTIONAL_PROTOCOL = '^(?:http(?:s)?:\/\/)?github\.com\/(?[a-zA-Z0-9-]+)\/(?[\w\.-]+)(?:\/(?:(?:raw|blob|tree)\/(?:refs\/(?:heads|tags)\/)?(?[^\/]+)\/(?.*))?)?$'; + +var + opt: TRegEx; + raw: TRegEx; +begin + opt := TRegEx.Create(GITHUB_URI_OPTIONAL_PROTOCOL); + raw := TRegEx.Create(GITHUB_RAW_URI); + + Result := opt.IsMatch(uri) or raw.IsMatch(uri); +end; + +procedure TfrmCloneGitHubProjectParameters.EnableControls; +var + e: Boolean; + id: string; +begin + e := + IsValidGitHubProjectURL(editGitHubURL.Text) and + (Trim(editPath.Text) <> '') and + (Trim(editProjectID.Text) <> ''); + + if e then + begin + id := Trim(editProjectID.Text); + if IsSourceProjectALexicalModelProject + then e := TLexicalModelUtils.IsValidLexicalModelID(id, True) + else e := TKeyboardUtils.IsValidKeyboardID(id, True); + end; + + cmdOK.Enabled := e; +end; + +function TfrmCloneGitHubProjectParameters.GetBasePath: string; +begin + Result := Trim(editPath.Text); +end; + +function TfrmCloneGitHubProjectParameters.GetHelpTopic: string; +begin + Result := SHelpTopic_Context_CloneGitHubProjectParameters; +end; + +function TfrmCloneGitHubProjectParameters.GetProjectID: string; +begin + Result := Trim(LowerCase(editProjectID.Text)); +end; + +function TfrmCloneGitHubProjectParameters.GetProjectFilename: string; +begin + Result := editProjectFilename.Text; +end; + +function TfrmCloneGitHubProjectParameters.GetRelocateExternal: Boolean; +begin + Result := chkRelocateExternal.Checked; +end; + +function TfrmCloneGitHubProjectParameters.GetSourceProjectFilename: string; +begin + Result := editGitHubURL.Text; +end; + +function TfrmCloneGitHubProjectParameters.IsSourceProjectALexicalModelProject: Boolean; +var + components: TArray; + sourceId: string; +begin + sourceId := editGitHubURL.Text; + components := sourceId.Split(['\','/']); + if Length(components) = 0 then + Exit(False); + sourceId := components[Length(components) - 1]; + sourceId := ChangeFileExt(sourceId, '').ToLower; + Result := TLexicalModelUtils.IsValidLexicalModelID(sourceId, False); +end; + +function TfrmCloneGitHubProjectParameters.Validate: Boolean; +var + ProjectFolder: string; +begin + if IsSourceProjectALexicalModelProject + then Result := TLexicalModelUtils.IsValidLexicalModelID(Trim(editProjectID.Text), True) + else Result := TKeyboardUtils.IsValidKeyboardID(Trim(editProjectID.Text), True); + + if Result then + begin + if not DirectoryExists(editPath.Text) then + begin + if MessageDlg('The target folder '+editPath.Text+' does not exist. Create it now?', mtConfirmation, mbOkCancel, 0) = mrCancel then + Exit(False); + end; + + ProjectFolder := IncludeTrailingPathDelimiter(editPath.Text) + editProjectID.Text; + if DirectoryExists(ProjectFolder) then + begin + if MessageDlg('The project folder '+ProjectFolder+' already exists. Are you sure you want to overwrite it?', mtWarning, + mbOkCancel, 0) = mrCancel then + Exit(False); + end; + end; +end; + +procedure TfrmCloneGitHubProjectParameters.UpdateProjectFilename; +begin + editProjectFilename.Text := + IncludeTrailingPathDelimiter(BasePath) + + ProjectID + PathDelim + + ProjectID + Ext_ProjectSource; + // Scroll to the end of the control to show the filename + editProjectFilename.Perform(EM_SETSEL, Length(editProjectFilename.Text), Length(editProjectFilename.Text)); + editProjectFilename.Perform(EM_SCROLLCARET, 0, 0); +end; + +end. diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneKeymanCloudProjectParameters.dfm b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneKeymanCloudProjectParameters.dfm new file mode 100644 index 0000000000..fb286bccc3 --- /dev/null +++ b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneKeymanCloudProjectParameters.dfm @@ -0,0 +1,119 @@ +inherited frmCloneKeymanCloudProjectParameters: TfrmCloneKeymanCloudProjectParameters + BorderIcons = [biSystemMenu, biMaximize] + Caption = 'Clone Project from Keyman Cloud' + ClientHeight = 506 + ClientWidth = 840 + Position = poScreenCenter + ExplicitWidth = 856 + ExplicitHeight = 545 + PixelsPerInch = 96 + TextHeight = 13 + object cmdOK: TButton + Left = 680 + Top = 473 + Width = 73 + Height = 25 + Anchors = [akRight, akBottom] + Caption = 'OK' + Default = True + TabOrder = 2 + OnClick = cmdOKClick + end + object cmdCancel: TButton + Left = 759 + Top = 473 + Width = 73 + Height = 25 + Anchors = [akRight, akBottom] + Cancel = True + Caption = 'Cancel' + ModalResult = 2 + TabOrder = 3 + end + object panWebHost: TPanel + Left = 0 + Top = 0 + Width = 842 + Height = 334 + Anchors = [akLeft, akTop, akRight, akBottom] + BevelOuter = bvNone + TabOrder = 0 + end + object gbNewProjectDetails: TGroupBox + Left = 8 + Top = 340 + Width = 823 + Height = 129 + Anchors = [akLeft, akRight, akBottom] + Caption = 'New Project Details' + TabOrder = 1 + object lblFileName: TLabel + Left = 16 + Top = 24 + Width = 76 + Height = 13 + Caption = '&New project ID:' + FocusControl = editKeyboardID + end + object lblProjectFilename: TLabel + Left = 16 + Top = 102 + Width = 101 + Height = 13 + Caption = 'New project &filename' + FocusControl = editProjectFilename + end + object lblPath: TLabel + Left = 16 + Top = 75 + Width = 83 + Height = 13 + Caption = 'Destination &path:' + FocusControl = editPath + end + object editKeyboardID: TEdit + Left = 148 + Top = 21 + Width = 205 + Height = 21 + TabOrder = 0 + OnChange = editKeyboardIDChange + end + object editProjectFilename: TEdit + Left = 148 + Top = 99 + Width = 669 + Height = 21 + TabStop = False + ParentColor = True + ReadOnly = True + TabOrder = 4 + OnChange = editKeyboardIDChange + end + object cmdBrowse: TButton + Left = 744 + Top = 72 + Width = 73 + Height = 21 + Caption = '&Browse...' + TabOrder = 3 + OnClick = cmdBrowseClick + end + object editPath: TEdit + Left = 148 + Top = 72 + Width = 590 + Height = 21 + TabOrder = 2 + OnChange = editPathChange + end + object chkRelocateExternal: TCheckBox + Left = 148 + Top = 48 + Width = 257 + Height = 17 + Caption = 'Relocate &external files into new project folder' + TabOrder = 1 + end + end +end diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneKeymanCloudProjectParameters.pas b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneKeymanCloudProjectParameters.pas new file mode 100644 index 0000000000..557fd24ec2 --- /dev/null +++ b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneKeymanCloudProjectParameters.pas @@ -0,0 +1,326 @@ +(* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Parameters dialog for cloning a project from Keyman Cloud + *) +unit Keyman.Developer.UI.Project.UfrmCloneKeymanCloudProjectParameters; + +interface + +uses + System.Classes, + System.SysUtils, + System.UITypes, + System.Variants, + Winapi.Messages, + Winapi.Windows, + Vcl.CheckLst, + Vcl.Controls, + Vcl.Dialogs, + Vcl.ExtCtrls, + Vcl.Forms, + Vcl.Graphics, + Vcl.Grids, + Vcl.StdCtrls, + + kpsfile, + PackageInfo, + UfrmTike, + UKeymanTargets, + UfrmDownloadProgress, + utilfiletypes, + Browse4Folder, + Keyman.UI.UframeCEFHost; + +type + TfrmCloneKeymanCloudProjectParameters = class(TTikeForm) + cmdOK: TButton; + cmdCancel: TButton; + panWebHost: TPanel; + gbNewProjectDetails: TGroupBox; + lblFileName: TLabel; + lblProjectFilename: TLabel; + editKeyboardID: TEdit; + editProjectFilename: TEdit; + lblPath: TLabel; + cmdBrowse: TButton; + editPath: TEdit; + chkRelocateExternal: TCheckBox; + procedure cmdOKClick(Sender: TObject); + procedure editSourceProjectFilenameChange(Sender: TObject); + procedure FormCreate(Sender: TObject); + procedure editVersionChange(Sender: TObject); + procedure editPathChange(Sender: TObject); + procedure editKeyboardIDChange(Sender: TObject); + procedure cmdBrowseClick(Sender: TObject); + private + cef: TframeCEFHost; + dlgBrowse: TBrowse4Folder; + FKeymanID: string; + frmDownloadProgress: TfrmDownloadProgress; + function GetBasePath: string; + function GetKeyboardID: string; + function Validate: Boolean; + procedure EnableControls; + procedure SetKeyboardID(const Value: string); + procedure UpdateProjectFilename; + function GetProjectFilename: string; + function GetSourceProjectFilename: string; + function GetRelocateExternal: Boolean; + procedure RefreshHTML; + procedure cefLoadEnd(Sender: TObject); + procedure DownloadCallback(Owner: TfrmDownloadProgress; var Result: Boolean); + procedure DownloadWrapperCallback(var Cancelled: Boolean); + protected + function GetHelpTopic: string; override; + public + property BasePath: string read GetBasePath; + property KeyboardID: string read GetKeyboardID write SetKeyboardID; + property ProjectFilename: string read GetProjectFilename; + property SourceProjectFilename: string read GetSourceProjectFilename; + property RelocateExternal: Boolean read GetRelocateExternal; + end; + +function ShowCloneKeymanCloudProjectParameters(Owner: TComponent): Boolean; + +implementation + +uses + System.Net.UrlClient, + Vcl.ComCtrls, + + KeymanDeveloperOptions, + Keyman.Developer.System.KmcWrapper, + Keyman.Developer.System.HelpTopics, + Keyman.Developer.System.Project.Project, + Keyman.Developer.System.Project.ProjectFile, + Keyman.System.KeyboardUtils, + UfrmMain, + Upload_Settings; + +{$R *.dfm} + +function ShowCloneKeymanCloudProjectParameters(Owner: TComponent): Boolean; +var + f: TfrmCloneKeymanCloudProjectParameters; + path: string; + p: TfrmDownloadProgress; +begin + f := TfrmCloneKeymanCloudProjectParameters.Create(Owner); + try + Result := f.ShowModal = mrOk; + if not Result then + Exit; + + path := ExtractFileDir(f.BasePath); + if not ForceDirectories(path) then + begin + ShowMessage('Unable to create path "'+path+'": '+SysErrorMessage(GetLastError)); + Exit; + end; + + p := TfrmDownloadProgress.Create(Owner); + try + p.Callback := f.DownloadCallback; + p.progress.Style := pbstMarquee; + Result := p.ShowModal = mrOk; + finally + p.Free; + end; + finally + f.Free; + end; +end; + +{ TfrmCloneKeymanCloudProjectParameters } + +procedure TfrmCloneKeymanCloudProjectParameters.DownloadWrapperCallback(var Cancelled: Boolean); +begin + Application.ProcessMessages; + Cancelled := frmDownloadProgress.Cancel; +end; + +procedure TfrmCloneKeymanCloudProjectParameters.DownloadCallback( + Owner: TfrmDownloadProgress; var Result: Boolean); +var + wrapper: TKmcWrapper; + path: string; +begin + Result := False; + path := ExtractFileDir(Self.BasePath); + + Update; + frmDownloadProgress := Owner; + wrapper := TKmcWrapper.Create; + try + if wrapper.Copy(Self.SourceProjectFilename, Self.ProjectFilename, path, Self.RelocateExternal, DownloadWrapperCallback) then + begin + frmKeymanDeveloper.OpenProject(Self.ProjectFilename); + Result := True; + end; + finally + wrapper.Free; + end; + frmDownloadProgress := nil; +end; + +procedure TfrmCloneKeymanCloudProjectParameters.FormCreate(Sender: TObject); +begin + inherited; + cef := TframeCEFHost.Create(Self); + cef.Parent := panWebHost; + cef.Visible := True; + cef.OnLoadEnd := cefLoadEnd; + RefreshHTML; + + editPath.Text := FKeymanDeveloperOptions.DefaultProjectPath; + + dlgBrowse := TBrowse4Folder.Create(Self); + dlgBrowse.InitialDir := editPath.Text; + dlgBrowse.Options := [OnlySelectFileSysDir, ShowEditBox, UseNewDialogStyle]; + dlgBrowse.Root := Desktop; + dlgBrowse.Title := 'Select folder to save project to'; + + EnableControls; +end; + +procedure TfrmCloneKeymanCloudProjectParameters.RefreshHTML; +begin + cef.Navigate(MakeKeymanURL(URLPath_KeymanDeveloper_KeyboardSearchForCloneKeymanCloud)); +end; + +procedure TfrmCloneKeymanCloudProjectParameters.cefLoadEnd(Sender: TObject); +var + u: TUri; +begin + u := TUri.Create(cef.cef.DocumentURL); + // We want to match on keyman.com/keyboards/ + // but we need to avoid keyman.com/keyboards/h/ + if u.Path.StartsWith(URLSubPath_KeymanDeveloper_Clone_Keyboards) and + not u.Path.StartsWith(URLSubPath_KeymanDeveloper_Clone_Keyboards_Custom) + then FKeymanID := u.Path.Substring(URLSubPath_KeymanDeveloper_Clone_Keyboards.Length) + else FKeymanID := ''; + EnableControls; +end; + +procedure TfrmCloneKeymanCloudProjectParameters.cmdBrowseClick(Sender: TObject); +begin + dlgBrowse.InitialDir := editPath.Text; + + if dlgBrowse.Execute and (dlgBrowse.FileName <> '') then + editPath.Text := ExcludeTrailingPathDelimiter(dlgBrowse.FileName); +end; + +procedure TfrmCloneKeymanCloudProjectParameters.cmdOKClick(Sender: TObject); +begin + if Validate then + ModalResult := mrOk; +end; + +procedure TfrmCloneKeymanCloudProjectParameters.editKeyboardIDChange(Sender: TObject); +begin + UpdateProjectFilename; + EnableControls; +end; + +procedure TfrmCloneKeymanCloudProjectParameters.editSourceProjectFilenameChange(Sender: TObject); +begin + EnableControls; +end; + +procedure TfrmCloneKeymanCloudProjectParameters.editPathChange(Sender: TObject); +begin + UpdateProjectFilename; + EnableControls; +end; + +procedure TfrmCloneKeymanCloudProjectParameters.editVersionChange(Sender: TObject); +begin + EnableControls; +end; + +procedure TfrmCloneKeymanCloudProjectParameters.EnableControls; +var + e: Boolean; +begin + e := + (FKeymanID <> '') and + (Trim(editPath.Text) <> '') and + (Trim(editKeyboardID.Text) <> '') and + TKeyboardUtils.IsValidKeyboardID(Trim(editKeyboardID.Text), True); + + cmdOK.Enabled := e; +end; + +function TfrmCloneKeymanCloudProjectParameters.GetBasePath: string; +begin + Result := Trim(editPath.Text); +end; + +function TfrmCloneKeymanCloudProjectParameters.GetHelpTopic: string; +begin + Result := SHelpTopic_Context_CloneKeymanCloudProjectParameters; +end; + +function TfrmCloneKeymanCloudProjectParameters.GetKeyboardID: string; +begin + Result := Trim(LowerCase(editKeyboardID.Text)); +end; + +function TfrmCloneKeymanCloudProjectParameters.GetProjectFilename: string; +begin + Result := editProjectFilename.Text; +end; + +function TfrmCloneKeymanCloudProjectParameters.GetRelocateExternal: Boolean; +begin + Result := chkRelocateExternal.Checked; +end; + +function TfrmCloneKeymanCloudProjectParameters.GetSourceProjectFilename: string; +begin + Result := 'cloud:'+FKeymanID; +end; + +function TfrmCloneKeymanCloudProjectParameters.Validate: Boolean; +var + ProjectFolder: string; +begin + Result := TKeyboardUtils.IsValidKeyboardID(Trim(editKeyboardID.Text), True); + + if Result then + begin + if not DirectoryExists(editPath.Text) then + begin + if MessageDlg('The target folder '+editPath.Text+' does not exist. Create it now?', mtConfirmation, mbOkCancel, 0) = mrCancel then + Exit(False); + end; + + ProjectFolder := IncludeTrailingPathDelimiter(editPath.Text) + editKeyboardID.Text; + if DirectoryExists(ProjectFolder) then + begin + if MessageDlg('The project folder '+ProjectFolder+' already exists. Are you sure you want to overwrite it?', mtWarning, + mbOkCancel, 0) = mrCancel then + Exit(False); + end; + end; +end; + +procedure TfrmCloneKeymanCloudProjectParameters.SetKeyboardID(const Value: string); +begin + editKeyboardID.Text := Value; + EnableControls; +end; + +procedure TfrmCloneKeymanCloudProjectParameters.UpdateProjectFilename; +begin + editProjectFilename.Text := + IncludeTrailingPathDelimiter(BasePath) + + KeyboardID + PathDelim + + KeyboardID + Ext_ProjectSource; + // Scroll to the end of the control to show the filename + editProjectFilename.Perform(EM_SETSEL, Length(editProjectFilename.Text), Length(editProjectFilename.Text)); + editProjectFilename.Perform(EM_SCROLLCARET, 0, 0); +end; + +end. diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneLocalProjectParameters.dfm b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneLocalProjectParameters.dfm new file mode 100644 index 0000000000..c60a3bcab6 --- /dev/null +++ b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneLocalProjectParameters.dfm @@ -0,0 +1,174 @@ +inherited frmCloneLocalProjectParameters: TfrmCloneLocalProjectParameters + BorderIcons = [biSystemMenu] + BorderStyle = bsDialog + Caption = 'Clone Project' + ClientHeight = 257 + ClientWidth = 721 + Position = poScreenCenter + ExplicitWidth = 727 + ExplicitHeight = 286 + PixelsPerInch = 96 + TextHeight = 13 + object cmdOK: TButton + Left = 561 + Top = 222 + Width = 73 + Height = 25 + Caption = 'OK' + Default = True + TabOrder = 2 + OnClick = cmdOKClick + end + object cmdCancel: TButton + Left = 640 + Top = 222 + Width = 73 + Height = 25 + Cancel = True + Caption = 'Cancel' + ModalResult = 2 + TabOrder = 3 + end + object gbNewProjectDetails: TGroupBox + Left = 8 + Top = 63 + Width = 705 + Height = 153 + Caption = 'New Project Details' + TabOrder = 1 + object lblFileName: TLabel + Left = 12 + Top = 22 + Width = 76 + Height = 13 + Caption = '&New project ID:' + FocusControl = editProjectID + end + object lblProjectFilename: TLabel + Left = 12 + Top = 124 + Width = 101 + Height = 13 + Caption = 'New project &filename' + FocusControl = editProjectFilename + end + object lblPath: TLabel + Left = 12 + Top = 97 + Width = 83 + Height = 13 + Caption = 'Destination &path:' + FocusControl = editPath + end + object lblSourceProjectLexicalModel: TLabel + Left = 12 + Top = 46 + Width = 531 + Height = 13 + Caption = + 'The source project appears to be a lexical model. The new projec' + + 't ID must use the author.bcp47.uniq pattern.' + end + object lblSourceProjectKeyboard: TLabel + Left = 12 + Top = 46 + Width = 682 + Height = 13 + Caption = + 'The source project appears to be a keyboard project. The new pro' + + 'ject ID must be a valid keyboard id pattern (alphanumeric and un' + + 'derscore).' + end + object editProjectID: TEdit + Left = 144 + Top = 19 + Width = 205 + Height = 21 + TabOrder = 0 + OnChange = editProjectIDChange + end + object editProjectFilename: TEdit + Left = 144 + Top = 121 + Width = 553 + Height = 21 + TabStop = False + ParentColor = True + ReadOnly = True + TabOrder = 4 + OnChange = editProjectIDChange + end + object cmdBrowse: TButton + Left = 624 + Top = 94 + Width = 73 + Height = 21 + Caption = '&Browse...' + TabOrder = 3 + OnClick = cmdBrowseClick + end + object editPath: TEdit + Left = 144 + Top = 94 + Width = 474 + Height = 21 + TabOrder = 2 + OnChange = editPathChange + end + object chkRelocateExternal: TCheckBox + Left = 144 + Top = 70 + Width = 257 + Height = 17 + Caption = 'Relocate &external files into new project folder' + TabOrder = 1 + end + end + object gbSourceProjectDetails: TGroupBox + Left = 8 + Top = 8 + Width = 705 + Height = 49 + Caption = 'Source Project' + TabOrder = 0 + object lblKeyboardName: TLabel + Left = 12 + Top = 19 + Width = 117 + Height = 13 + Caption = '&Source project filename:' + FocusControl = editSourceProjectFilename + end + object editSourceProjectFilename: TEdit + Left = 144 + Top = 16 + Width = 474 + Height = 21 + TabOrder = 0 + OnChange = editSourceProjectFilenameChange + end + object cmdBrowseSourceProject: TButton + Left = 624 + Top = 16 + Width = 73 + Height = 21 + Caption = 'B&rowse...' + TabOrder = 1 + OnClick = cmdBrowseSourceProjectClick + end + end + object dlgBrowseSourceProject: TFileOpenDialog + DefaultExtension = '.kpj' + FavoriteLinks = <> + FileTypes = < + item + DisplayName = 'Keyman Project Files (*.kpj)' + FileMask = '*.kpj' + end> + OkButtonLabel = 'Select' + Options = [fdoPathMustExist, fdoFileMustExist, fdoNoTestFileCreate] + Title = 'Select source project' + Left = 584 + Top = 8 + end +end diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneLocalProjectParameters.pas b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneLocalProjectParameters.pas new file mode 100644 index 0000000000..cfc24801b3 --- /dev/null +++ b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneLocalProjectParameters.pas @@ -0,0 +1,287 @@ +(* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Parameters dialog for cloning a project from local filesystem + *) +unit Keyman.Developer.UI.Project.UfrmCloneLocalProjectParameters; + +interface + +uses + System.Classes, + System.SysUtils, + System.UITypes, + System.Variants, + Winapi.Messages, + Winapi.Windows, + Vcl.CheckLst, + Vcl.Controls, + Vcl.Dialogs, + Vcl.Forms, + Vcl.Graphics, + Vcl.Grids, + Vcl.StdCtrls, + + kpsfile, + PackageInfo, + UfrmTike, + UKeymanTargets, + utilfiletypes, + Browse4Folder; + +type + TfrmCloneLocalProjectParameters = class(TTikeForm) + cmdOK: TButton; + cmdCancel: TButton; + dlgBrowseSourceProject: TFileOpenDialog; + gbNewProjectDetails: TGroupBox; + lblFileName: TLabel; + lblProjectFilename: TLabel; + lblPath: TLabel; + editProjectID: TEdit; + editProjectFilename: TEdit; + cmdBrowse: TButton; + editPath: TEdit; + chkRelocateExternal: TCheckBox; + gbSourceProjectDetails: TGroupBox; + lblKeyboardName: TLabel; + editSourceProjectFilename: TEdit; + cmdBrowseSourceProject: TButton; + lblSourceProjectLexicalModel: TLabel; + lblSourceProjectKeyboard: TLabel; + procedure cmdOKClick(Sender: TObject); + procedure editSourceProjectFilenameChange(Sender: TObject); + procedure FormCreate(Sender: TObject); + procedure editVersionChange(Sender: TObject); + procedure editPathChange(Sender: TObject); + procedure editProjectIDChange(Sender: TObject); + procedure cmdBrowseClick(Sender: TObject); + procedure cmdBrowseSourceProjectClick(Sender: TObject); + private + dlgBrowse: TBrowse4Folder; + function GetBasePath: string; + function GetProjectID: string; + function Validate: Boolean; + procedure EnableControls; + procedure UpdateProjectFilename; + function GetProjectFilename: string; + function GetSourceProjectFilename: string; + function GetRelocateExternal: Boolean; + function IsSourceProjectALexicalModelProject: Boolean; + procedure UpdateSourceProjectNote; + protected + function GetHelpTopic: string; override; + public + property BasePath: string read GetBasePath; + property ProjectID: string read GetProjectID; + property ProjectFilename: string read GetProjectFilename; + property SourceProjectFilename: string read GetSourceProjectFilename; + property RelocateExternal: Boolean read GetRelocateExternal; + end; + +function ShowCloneLocalProjectParameters(Owner: TComponent): Boolean; + +implementation + +uses + KeymanDeveloperOptions, + Keyman.Developer.System.KmcWrapper, + Keyman.Developer.System.HelpTopics, + Keyman.Developer.System.Project.Project, + Keyman.Developer.System.Project.ProjectFile, + Keyman.System.KeyboardUtils, + Keyman.System.LexicalModelUtils, + UfrmMain; + +{$R *.dfm} + +function ShowCloneLocalProjectParameters(Owner: TComponent): Boolean; +var + f: TfrmCloneLocalProjectParameters; + wrapper: TKmcWrapper; +begin + f := TfrmCloneLocalProjectParameters.Create(Owner); + try + Result := f.ShowModal = mrOk; + if not Result then + Exit; + + wrapper := TKmcWrapper.Create; + if wrapper.Copy(f.SourceProjectFilename, f.ProjectFilename, ExtractFileDir(f.SourceProjectFilename), f.RelocateExternal) then + begin + frmKeymanDeveloper.OpenProject(f.ProjectFilename); + Result := True; + end; + finally + f.Free; + end; +end; + +{ TfrmCloneLocalProjectParameters } + +procedure TfrmCloneLocalProjectParameters.FormCreate(Sender: TObject); +begin + inherited; + editPath.Text := FKeymanDeveloperOptions.DefaultProjectPath; + + dlgBrowse := TBrowse4Folder.Create(Self); + dlgBrowse.InitialDir := editPath.Text; + dlgBrowse.Options := [OnlySelectFileSysDir, ShowEditBox, UseNewDialogStyle]; + dlgBrowse.Root := Desktop; + dlgBrowse.Title := 'Select folder to save project to'; + + UpdateSourceProjectNote; + EnableControls; +end; + +procedure TfrmCloneLocalProjectParameters.cmdBrowseClick(Sender: TObject); +begin + dlgBrowse.InitialDir := editPath.Text; + + if dlgBrowse.Execute and (dlgBrowse.FileName <> '') then + editPath.Text := ExcludeTrailingPathDelimiter(dlgBrowse.FileName); +end; + +procedure TfrmCloneLocalProjectParameters.cmdBrowseSourceProjectClick( + Sender: TObject); +begin + if dlgBrowseSourceProject.Execute then + begin + editSourceProjectFilename.Text := dlgBrowseSourceProject.Filename; + UpdateSourceProjectNote; + EnableControls; + end; +end; + +procedure TfrmCloneLocalProjectParameters.UpdateSourceProjectNote; +begin + lblSourceProjectLexicalModel.Visible := IsSourceProjectALexicalModelProject; + lblSourceProjectKeyboard.Visible := (editSourceProjectFilename.Text <> '') and not IsSourceProjectALexicalModelProject; +end; + +procedure TfrmCloneLocalProjectParameters.cmdOKClick(Sender: TObject); +begin + if Validate then + ModalResult := mrOk; +end; + +procedure TfrmCloneLocalProjectParameters.editProjectIDChange(Sender: TObject); +begin + UpdateProjectFilename; + EnableControls; +end; + +procedure TfrmCloneLocalProjectParameters.editSourceProjectFilenameChange(Sender: TObject); +begin + EnableControls; + UpdateSourceProjectNote; +end; + +procedure TfrmCloneLocalProjectParameters.editPathChange(Sender: TObject); +begin + UpdateProjectFilename; + EnableControls; +end; + +procedure TfrmCloneLocalProjectParameters.editVersionChange(Sender: TObject); +begin + EnableControls; +end; + +function TfrmCloneLocalProjectParameters.IsSourceProjectALexicalModelProject: Boolean; +var + sourceId: string; +begin + sourceId := ChangeFileExt(ExtractFileName(editSourceProjectFilename.Text), '').ToLower; + Result := TLexicalModelUtils.IsValidLexicalModelID(sourceId, False); +end; + +procedure TfrmCloneLocalProjectParameters.EnableControls; +var + e: Boolean; + id: string; +begin + e := (Trim(editSourceProjectFilename.Text) <> '') and + FileExists(Trim(editSourceProjectFilename.Text)) and + (Trim(editPath.Text) <> '') and + (Trim(editProjectID.Text) <> ''); + + if e then + begin + id := Trim(editProjectID.Text); + if IsSourceProjectALexicalModelProject + then e := TLexicalModelUtils.IsValidLexicalModelID(id, True) + else e := TKeyboardUtils.IsValidKeyboardID(id, True); + end; + + cmdOK.Enabled := e; +end; + +function TfrmCloneLocalProjectParameters.GetBasePath: string; +begin + Result := Trim(editPath.Text); +end; + +function TfrmCloneLocalProjectParameters.GetHelpTopic: string; +begin + Result := SHelpTopic_Context_CloneLocalProjectParameters; +end; + +function TfrmCloneLocalProjectParameters.GetProjectID: string; +begin + Result := Trim(LowerCase(editProjectID.Text)); +end; + +function TfrmCloneLocalProjectParameters.GetProjectFilename: string; +begin + Result := editProjectFilename.Text; +end; + +function TfrmCloneLocalProjectParameters.GetRelocateExternal: Boolean; +begin + Result := chkRelocateExternal.Checked; +end; + +function TfrmCloneLocalProjectParameters.GetSourceProjectFilename: string; +begin + Result := editSourceProjectFilename.Text; +end; + +function TfrmCloneLocalProjectParameters.Validate: Boolean; +var + ProjectFolder: string; +begin + if IsSourceProjectALexicalModelProject + then Result := TLexicalModelUtils.IsValidLexicalModelID(Trim(editProjectID.Text), True) + else Result := TKeyboardUtils.IsValidKeyboardID(Trim(editProjectID.Text), True); + + if Result then + begin + if not DirectoryExists(editPath.Text) then + begin + if MessageDlg('The target folder '+editPath.Text+' does not exist. Create it now?', mtConfirmation, mbOkCancel, 0) = mrCancel then + Exit(False); + end; + + ProjectFolder := IncludeTrailingPathDelimiter(editPath.Text) + editProjectID.Text; + if DirectoryExists(ProjectFolder) then + begin + if MessageDlg('The project folder '+ProjectFolder+' already exists. Are you sure you want to overwrite it?', mtWarning, + mbOkCancel, 0) = mrCancel then + Exit(False); + end; + end; +end; + +procedure TfrmCloneLocalProjectParameters.UpdateProjectFilename; +begin + editProjectFilename.Text := + IncludeTrailingPathDelimiter(BasePath) + + ProjectID + PathDelim + + ProjectID + Ext_ProjectSource; + // Scroll to the end of the control to show the filename + editProjectFilename.Perform(EM_SETSEL, Length(editProjectFilename.Text), Length(editProjectFilename.Text)); + editProjectFilename.Perform(EM_SCROLLCARET, 0, 0); +end; + +end. diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewLDMLKeyboardProjectParameters.dfm b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewLDMLKeyboardProjectParameters.dfm index 3942364fd8..6ef44a8f7a 100644 --- a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewLDMLKeyboardProjectParameters.dfm +++ b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewLDMLKeyboardProjectParameters.dfm @@ -94,7 +94,7 @@ inherited frmNewLDMLKeyboardProjectParameters: TfrmNewLDMLKeyboardProjectParamet Top = 260 Width = 102 Height = 13 - Caption = 'Supported &languages' + Caption = '&Supported languages' FocusControl = memoLanguages end object Label4: TLabel diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewProject.dfm b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewProject.dfm index 5208255bbc..1761e2f1c2 100644 --- a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewProject.dfm +++ b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewProject.dfm @@ -42,18 +42,26 @@ inherited frmNewProject: TfrmNewProject FullDrag = True HideSelection = False Items.ItemData = { - 05FA0000000400000001000000FFFFFFFFFFFFFFFF00000000FFFFFFFF000000 - 000F4B00650079006D0061006E0020004B006500790062006F00610072006400 + 05DE0100000700000001000000FFFFFFFFFFFFFFFF00000000FFFFFFFF000000 + 000F4B00650079006D0061006E0020006B006500790062006F00610072006400 01000000FFFFFFFFFFFFFFFF00000000FFFFFFFF000000000D4C0044004D004C - 0020004B006500790062006F0061007200640001000000FFFFFFFFFFFFFFFF00 + 0020006B006500790062006F0061007200640001000000FFFFFFFFFFFFFFFF00 000000FFFFFFFF000000001749006D0070006F00720074002000570069006E00 - 64006F007700730020004B006500790062006F0061007200640002000000FFFF + 64006F007700730020006B006500790062006F0061007200640002000000FFFF FFFFFFFFFFFF00000000FFFFFFFF000000001657006F00720064006C00690073 - 00740020004C00650078006900630061006C0020004D006F00640065006C00} + 00740020006C00650078006900630061006C0020006D006F00640065006C0001 + 000000FFFFFFFFFFFFFFFF00000000FFFFFFFF000000001343006C006F006E00 + 650020006C006F00630061006C002000700072006F006A006500630074000100 + 0000FFFFFFFFFFFFFFFF00000000FFFFFFFF000000001F43006C006F006E0065 + 002000700072006F006A006500630074002000660072006F006D0020004B0065 + 0079006D0061006E00200043006C006F007500640001000000FFFFFFFFFFFFFF + FF00000000FFFFFFFF000000001943006C006F006E0065002000700072006F00 + 6A006500630074002000660072006F006D002000470069007400480075006200} LargeImages = ilLarge ReadOnly = True SmallImages = ilSmall TabOrder = 0 + ViewStyle = vsList OnDblClick = lvItemsDblClick OnSelectItem = lvItemsSelectItem end diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewProject.pas b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewProject.pas index 535a584ad6..36f65bd103 100644 --- a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewProject.pas +++ b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewProject.pas @@ -81,6 +81,9 @@ uses Keyman.Developer.System.Project.Project, Keyman.Developer.System.Project.ProjectFile, Keyman.Developer.UI.Project.ProjectUI, + Keyman.Developer.UI.Project.UfrmCloneGitHubProjectParameters, + Keyman.Developer.UI.Project.UfrmCloneLocalProjectParameters, + Keyman.Developer.UI.Project.UfrmCloneKeymanCloudProjectParameters, Keyman.Developer.UI.Project.UfrmNewLDMLKeyboardProjectParameters, Keyman.Developer.UI.Project.UfrmNewProjectParameters, Keyman.Developer.UI.Project.UfrmNewModelProjectParameters, @@ -115,6 +118,12 @@ begin Result := ShowNewModelProjectParameters(Owner); kptImportWindowsKeyboard: Result := ShowImportWindowsKeyboard(Owner); + kptCloneLocalProject: + Result := ShowCloneLocalProjectParameters(Owner); + kptCloneKeymanCloudProject: + Result := ShowCloneKeymanCloudProjectParameters(Owner); + kptCloneGitHubProject: + Result := ShowCloneGitHubProjectParameters(Owner); end; end; @@ -143,14 +152,20 @@ begin // Overly wordy but clarifies that we may have other types later if not Assigned(lvItems.Selected) then Result := kptUnknown - else if lvItems.Selected.Caption = 'Keyman Keyboard' then + else if lvItems.Selected.Caption = 'Keyman keyboard' then Result := kptBasic - else if lvItems.Selected.Caption = 'LDML Keyboard' then + else if lvItems.Selected.Caption = 'LDML keyboard' then Result := kptLDMLKeyboard - else if lvItems.Selected.Caption = 'Import Windows Keyboard' then + else if lvItems.Selected.Caption = 'Import Windows keyboard' then Result := kptImportWindowsKeyboard - else if lvItems.Selected.Caption = 'Wordlist Lexical Model' then + else if lvItems.Selected.Caption = 'Wordlist lexical model' then Result := kptWordlistLexicalModel + else if lvItems.Selected.Caption = 'Clone local project' then + Result := kptCloneLocalProject + else if lvItems.Selected.Caption = 'Clone project from Keyman Cloud' then + Result := kptCloneKeymanCloudProject + else if lvItems.Selected.Caption = 'Clone project from GitHub' then + Result := kptCloneGitHubProject else Result := kptUnknown; end; @@ -192,6 +207,16 @@ begin 'Creates a new lexical model project that matches the folder structure used in '+ 'the Keyman lexical-models repository and includes all basic files '+ 'needed for a wordlist lexical model.'; + kptCloneLocalProject: lblDescription.Caption := + 'Copies a Keyman keyboard or lexical model project on your local filesystem '+ + 'into a new folder, giving the project and included files a new name.'; + kptCloneKeymanCloudProject: lblDescription.Caption := + 'Copies a Keyman keyboard or lexical model project from keyman.com '+ + 'into a new folder, giving the project and included files a new name.'; + kptCloneGitHubProject: lblDescription.Caption := + 'Copies a Keyman keyboard or lexical model project from a GitHub '+ + 'repository into a new folder, giving the project and included files a '+ + 'new name.'; end; end; diff --git a/developer/src/tike/tike.dpr b/developer/src/tike/tike.dpr index e686949fc0..6724e254f4 100644 --- a/developer/src/tike/tike.dpr +++ b/developer/src/tike/tike.dpr @@ -228,7 +228,7 @@ uses Keyman.Developer.System.VisualKeyboardToTouchLayoutConverter in '..\kmconvert\Keyman.Developer.System.VisualKeyboardToTouchLayoutConverter.pas', Keyman.Developer.System.Project.kmnProjectFileAction in 'project\Keyman.Developer.System.Project.kmnProjectFileAction.pas', Keyman.Developer.System.Project.kpsProjectFileAction in 'project\Keyman.Developer.System.Project.kpsProjectFileAction.pas', - Keyman.Developer.UI.Project.UfrmNewProjectParameters in 'project\Keyman.Developer.UI.Project.UfrmNewProjectParameters.pas' {frmNewProjectParameters}, + Keyman.Developer.UI.Project.UfrmCloneGitHubProjectParameters in 'project\Keyman.Developer.UI.Project.UfrmCloneGitHubProjectParameters.pas' {frmCloneGitHubProjectParameters}, Keyman.Developer.UI.Project.UfrmNewLDMLKeyboardProjectParameters in 'project\Keyman.Developer.UI.Project.UfrmNewLDMLKeyboardProjectParameters.pas' {frmNewLDMLKeyboardProjectParameters}, Keyman.Developer.UI.Project.UfrmNewProject in 'project\Keyman.Developer.UI.Project.UfrmNewProject.pas' {frmNewProject}, Keyman.Developer.System.KeyboardProjectTemplate in '..\kmconvert\Keyman.Developer.System.KeyboardProjectTemplate.pas', @@ -300,7 +300,10 @@ uses Keyman.Developer.System.Main in 'main\Keyman.Developer.System.Main.pas', Keyman.Developer.System.LaunchProjects in 'main\Keyman.Developer.System.LaunchProjects.pas', Keyman.System.Debug.DebugUtils in 'debug\Keyman.System.Debug.DebugUtils.pas', - Keyman.Developer.UI.RichEdit41 in '..\common\delphi\components\Keyman.Developer.UI.RichEdit41.pas'; + Keyman.Developer.UI.RichEdit41 in '..\common\delphi\components\Keyman.Developer.UI.RichEdit41.pas', + Keyman.Developer.UI.Project.UfrmNewProjectParameters in 'project\Keyman.Developer.UI.Project.UfrmNewProjectParameters.pas' {frmNewProjectParameters}, + Keyman.Developer.UI.Project.UfrmCloneLocalProjectParameters in 'project\Keyman.Developer.UI.Project.UfrmCloneLocalProjectParameters.pas' {frmCloneLocalProjectParameters}, + Keyman.Developer.UI.Project.UfrmCloneKeymanCloudProjectParameters in 'project\Keyman.Developer.UI.Project.UfrmCloneKeymanCloudProjectParameters.pas' {frmCloneKeymanCloudProjectParameters}; {$R *.RES} {$R ICONS.RES} diff --git a/developer/src/tike/tike.dproj b/developer/src/tike/tike.dproj index 2a626a30ac..9ee2d83579 100644 --- a/developer/src/tike/tike.dproj +++ b/developer/src/tike/tike.dproj @@ -471,8 +471,8 @@ - -
frmNewProjectParameters
+ +
frmCloneGitHubProjectParameters
dfm
@@ -585,6 +585,18 @@ + +
frmNewProjectParameters
+ dfm +
+ +
frmCloneLocalProjectParameters
+ dfm +
+ +
frmCloneKeymanCloudProjectParameters
+ dfm +
Cfg_2 diff --git a/docs/build/linux-ubuntu.md b/docs/build/linux-ubuntu.md index 0f531acb59..98e15185bf 100644 --- a/docs/build/linux-ubuntu.md +++ b/docs/build/linux-ubuntu.md @@ -23,7 +23,7 @@ The following projects **cannot** be built on Linux: ### System Requirements -- Minimum Ubuntu version: Ubuntu 20.04 +- Minimum Ubuntu version: Ubuntu 22.04 Other Linux distributions will also work if appropriate dependencies are installed. @@ -113,7 +113,7 @@ All dependencies are already installed if you followed the instructions under Keyman Core can be built with the `core/build.sh` script. -- [Building Keyman Core](../../core/doc/BUILDING.md) +- [Building Keyman Core](../../core/docs/BUILDING.md) ## Keyman for Linux diff --git a/docs/linux/README.md b/docs/linux/README.md index 80c1a78434..64ef6c967f 100644 --- a/docs/linux/README.md +++ b/docs/linux/README.md @@ -147,7 +147,7 @@ Run `ibus restart` after installing any of them. Keyman tries to activate the keyboard automatically. If you want to activate it for a different language, you can do so by following the steps below. -#### GNOME3 (focal and bionic default, also newer Ubuntu versions) +#### GNOME3 (Ubuntu default) - Click the connection/sound/shutdown section in the top right. Then the tools icon for Settings. diff --git a/docs/minimum-versions.md b/docs/minimum-versions.md index 174f4b518e..4f67d5a626 100644 --- a/docs/minimum-versions.md +++ b/docs/minimum-versions.md @@ -4,7 +4,7 @@ title: Keyman Minimum Versions Changes in [minimum-versions.inc.sh](minimum-versions.inc.sh) should manually be propagated to the linked help files below: -## Keyman 18.0 +## Keyman 19.0 Target Operating System and Platform Versions @@ -53,7 +53,7 @@ https://help.keyman.com/developer/engine/android/latest-version/ | KEYMAN_MIN_TARGET_VERSION_ANDROID_CHROME | 53.0 | | KEYMAN_MIN_TARGET_VERSION_IOS | 12.2 | | KEYMAN_MIN_TARGET_VERSION_MAC | 10.13 | -| KEYMAN_MIN_TARGET_VERSION_UBUNTU | 20.04 | +| KEYMAN_MIN_TARGET_VERSION_UBUNTU | 22.04 | | KEYMAN_MIN_TARGET_VERSION_WEB_CHROME | 95.0 | | KEYMAN_MIN_TARGET_VERSION_WEB_FIREFOX | 79.0 | | KEYMAN_MIN_TARGET_VERSION_WEB_OPERA | 47.0 | diff --git a/docs/minimum-versions.md.in b/docs/minimum-versions.md.in index ffe508e12f..41a664d454 100644 --- a/docs/minimum-versions.md.in +++ b/docs/minimum-versions.md.in @@ -4,7 +4,7 @@ title: Keyman Minimum Versions Changes in [minimum-versions.inc.sh](minimum-versions.inc.sh) should manually be propagated to the linked help files below: -## Keyman 18.0 +## Keyman 19.0 Target Operating System and Platform Versions diff --git a/docs/npm-packages.md b/docs/npm-packages.md index 494bec0044..8306130d36 100644 --- a/docs/npm-packages.md +++ b/docs/npm-packages.md @@ -19,6 +19,7 @@ Packages are **never** published from a developer's machine. * `@keymanapp/kmc-model` -- located at `developer/src/kmc-model` * `@keymanapp/kmc-model-info` -- located at `developer/src/kmc-model-info` * `@keymanapp/kmc-package` -- located at `developer/src/kmc-package` +* `@keymanapp/langtags` -- located at `common/web/langtags` ### Deprecated npm packages diff --git a/ios/Cartfile.resolved b/ios/Cartfile.resolved index 4e1a058a51..ca05da1ae9 100644 --- a/ios/Cartfile.resolved +++ b/ios/Cartfile.resolved @@ -1,4 +1,4 @@ github "ashleymills/Reachability.swift" "v5.2.4" github "devicekit/DeviceKit" "5.5.0" -github "getsentry/sentry-cocoa" "8.38.0" +github "getsentry/sentry-cocoa" "8.44.0" github "weichsel/ZIPFoundation" "0.9.19" diff --git a/ios/docs/engine/guides/in-app/index.md b/ios/docs/engine/guides/in-app/index.md index 14ba8ba4c5..3dfebbf82d 100644 --- a/ios/docs/engine/guides/in-app/index.md +++ b/ios/docs/engine/guides/in-app/index.md @@ -1,5 +1,5 @@ --- -title: Guide: build an in-app keyboard for iPhone and iPad with Keyman Engine +title: Guide: build an in-app keyboard --- Keyman Engine for iPhone and iPad allows you to use any Keyman touch diff --git a/ios/docs/engine/guides/system-keyboard/index.md b/ios/docs/engine/guides/system-keyboard/index.md index 4a533f08d8..ea652360b2 100644 --- a/ios/docs/engine/guides/system-keyboard/index.md +++ b/ios/docs/engine/guides/system-keyboard/index.md @@ -1,5 +1,5 @@ --- -title: Guide: build a system keyboard app on iPhone and iPad with Keyman Engine +title: Guide: build a system keyboard app --- In [part 1](../in-app/) of this series, we looked at the steps involved diff --git a/ios/docs/engine/index.md b/ios/docs/engine/index.md index b87af4e260..bdba88b741 100644 --- a/ios/docs/engine/index.md +++ b/ios/docs/engine/index.md @@ -4,7 +4,7 @@ title: Keyman for iPhone and iPad Developer Support ## Overview -The Keyman Engine for iPhone and iPad 18.0 SDK is designed to provide +The Keyman Engine for iPhone and iPad 19.0 SDK is designed to provide advanced international keyboard support to iOS apps. As a developer, you simply need to use (or subclass) TextView or diff --git a/ios/docs/help/about/index.md b/ios/docs/help/about/index.md index d37b9ed978..71ead99252 100644 --- a/ios/docs/help/about/index.md +++ b/ios/docs/help/about/index.md @@ -1,5 +1,5 @@ --- -title: About Keyman for iPhone and iPad +title: About Keyman --- * [Welcome to Keyman](welcome) diff --git a/ios/docs/help/about/whatsnew.md b/ios/docs/help/about/whatsnew.md index aa4cf6b333..b123b1f23c 100644 --- a/ios/docs/help/about/whatsnew.md +++ b/ios/docs/help/about/whatsnew.md @@ -1,8 +1,5 @@ --- -title: What's New in Keyman 18.0 for iPhone and iPad +title: What's New in Keyman 19.0 for iPhone and iPad --- -Here are some of the new features we have added to Keyman for iPhone and iPad 18.0: -Additional changes: - -* Performance improvements +Here are some of the new features we have added to Keyman for iPhone and iPad 19.0: diff --git a/ios/docs/help/basic/index.md b/ios/docs/help/basic/index.md index e93f77f3aa..b67688e03c 100644 --- a/ios/docs/help/basic/index.md +++ b/ios/docs/help/basic/index.md @@ -1,5 +1,5 @@ --- -title: Keyman for iPhone and iPad Basic Help +title: Basic Help --- * [Using the Banner on the Keyboard](using-the-banner) diff --git a/ios/docs/help/context/gestures.md b/ios/docs/help/context/gestures.md index ab15c3203d..251de0f1ce 100644 --- a/ios/docs/help/context/gestures.md +++ b/ios/docs/help/context/gestures.md @@ -1,5 +1,5 @@ --- -title: Gesture Hints and Tips - Keyman for iPhone and iPad +title: Gesture Hints and Tips --- ## Using Gestures on the Keyboard Keys diff --git a/ios/docs/help/index.md b/ios/docs/help/index.md index 994bbbda9e..cd0f81300e 100644 --- a/ios/docs/help/index.md +++ b/ios/docs/help/index.md @@ -1,5 +1,5 @@ --- -title: Keyman for iPhone and iPad 18.0 Help +title: Keyman for iPhone and iPad 19.0 Help --- ## [About Keyman](about/) diff --git a/ios/docs/help/start/index.md b/ios/docs/help/start/index.md index d7bb7ad72e..f48fc3f185 100644 --- a/ios/docs/help/start/index.md +++ b/ios/docs/help/start/index.md @@ -1,5 +1,5 @@ --- -title: Getting Started with Keyman for iPhone and iPad +title: Getting Started --- * [How To - Adding a Keyboard](searching-for-keyboards) diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Keyboard/KeymanWebViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/Keyboard/KeymanWebViewController.swift index c101ebd832..a0c8c6d5e4 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Keyboard/KeymanWebViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Keyboard/KeymanWebViewController.swift @@ -137,7 +137,7 @@ class KeymanWebViewController: UIViewController { webView!.backgroundColor = UIColor.clear webView!.navigationDelegate = self webView!.scrollView.isScrollEnabled = false - + if #available(iOSApplicationExtension 16.4, *) { if(Version.current.tier != .stable) { webView!.isInspectable = true @@ -164,6 +164,9 @@ class KeymanWebViewController: UIViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) + if #available(iOSApplicationExtension 14.0, *) { + self.userContentController.removeAllScriptMessageHandlers() + } self.userContentController.add(self, name: keymanWebViewName) } @@ -600,6 +603,10 @@ extension KeymanWebViewController: KeymanWebDelegate { os_log("%{public}s", log: KeymanEngineLogger.engine, type: .info, message) SentryManager.breadcrumb(message, sentryLevel: .debug) + // Overwrites the baseline "embeddingApp" text available before Web engine init with the + // active bundle identifier. + webView!.evaluateJavaScript("setHostAppName(\"\(Bundle.main.infoDictionary!["CFBundleIdentifier"]!)\")") + self.setSentryState() resizeKeyboard() @@ -696,7 +703,7 @@ extension KeymanWebViewController: KeymanWebDelegate { // MARK: - Manage views extension KeymanWebViewController { // MARK: - Sizing - + @objc func menuKeyHeld(_ keymanWeb: KeymanWebViewController) { self.delegate?.menuKeyHeld(self) } @@ -713,10 +720,10 @@ extension KeymanWebViewController { let width: CGFloat width = UIScreen.main.bounds.width var height: CGFloat - + // get orientation differently if system or in-app keyboard let portrait = Util.isSystemKeyboard ? InputViewController.isPortrait : UIDevice.current.orientation.isPortrait - + /** * If keyboard height is saved in UserDefaults, then use it */ @@ -728,17 +735,17 @@ extension KeymanWebViewController { */ height = self.determineDefaultKeyboardHeight(isPortrait: portrait) self.writeKeyboardHeightIfDoesNotExist(isPortrait: portrait, height: height) - + /** * If we need to write out the keyboard height for one orientation, then we - * expect that the other must be written also. + * expect that the other must be written also. * Write it out now, but only if a value for keyboard height does not already exist. */ height = self.determineDefaultKeyboardHeight(isPortrait: !portrait) self.writeKeyboardHeightIfDoesNotExist(isPortrait: !portrait, height: height) } - - /** + + /** * no need to check for Util.isSystemKeyboard because this is shared storage * the UserDefaults are readable and writeable by both system and in-app */ @@ -768,7 +775,7 @@ extension KeymanWebViewController { return height; } - + /** * Write out the keyboard height to the UserDefaults but only if it does not exist there yet. * If it exists, then we assume it was configured by the user and do not want to @@ -789,7 +796,7 @@ extension KeymanWebViewController { } } } - + var keyboardSize: CGSize { get { if kbSize.equalTo(CGSize.zero) { diff --git a/ios/engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/ios-host.js b/ios/engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/ios-host.js index bd323b970b..b390b438de 100644 --- a/ios/engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/ios-host.js +++ b/ios/engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/ios-host.js @@ -131,7 +131,7 @@ function getOskWidth() { function getOskHeight() { var height = oskHeight; - if(keyman.osk.banner._activeType != 'blank') { + if(keyman.osk && keyman.osk.banner && keyman.osk.banner._activeType != 'blank') { height = height - keyman.osk.banner.height; } return height; @@ -149,6 +149,11 @@ function setKeymanLanguage(stub) { }); } +function setHostAppName(name) { + // Sentry will capture this in a breadcrumb for Web errors. + keyman.config.embeddingApp = name; +} + var fragmentToggle = 0; /** * Inserts the selected string s diff --git a/ios/keyman/Keyman/Keyman.xcodeproj/project.pbxproj b/ios/keyman/Keyman/Keyman.xcodeproj/project.pbxproj index 6211f26573..a8b4f8624b 100644 --- a/ios/keyman/Keyman/Keyman.xcodeproj/project.pbxproj +++ b/ios/keyman/Keyman/Keyman.xcodeproj/project.pbxproj @@ -1004,7 +1004,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = "/usr/bin/env bash"; - shellScript = "\"$KEYMAN_ROOT/resources/build/xcode-wrap.sh\" \"$KEYMAN_ROOT/resources/build/set-bundle-versions-and-settings-tagged.sh\"\n"; + shellScript = "\"$KEYMAN_ROOT/resources/build/xcode-wrap.sh\" \"$KEYMAN_ROOT/resources/build/set-bundle-versions-and-settings-tagged.sh\"\necho \"script - set version complete\"\n"; showEnvVarsInLog = 0; }; CED3CFDA240E49DF001540A1 /* ShellScript */ = { diff --git a/linux/debian/changelog b/linux/debian/changelog index 99ce27f238..a15c962527 100644 --- a/linux/debian/changelog +++ b/linux/debian/changelog @@ -1,3 +1,9 @@ +keyman (17.0.335-1) unstable; urgency=medium + + * New upstream release. + + -- Eberhard Beilharz Thu, 06 Feb 2025 10:33:00 +0100 + keyman (17.0.334-1) unstable; urgency=medium * New upstream release. diff --git a/linux/debian/control b/linux/debian/control index fbe7066f5c..e12da18aa3 100644 --- a/linux/debian/control +++ b/linux/debian/control @@ -10,7 +10,7 @@ Build-Depends: debhelper-compat (= 12), dh-python, gawk, - gir1.2-webkit2-4.1 | gir1.2-webkit2-4.0, + gir1.2-webkit2-4.1, ibus, libevdev-dev, libgtk-3-dev, @@ -88,7 +88,7 @@ Architecture: all Depends: dbus-x11, dconf-cli, - gir1.2-webkit2-4.1 | gir1.2-webkit2-4.0, + gir1.2-webkit2-4.1, keyman-engine, python3-bs4, python3-fonttools, diff --git a/linux/debian/rules b/linux/debian/rules index 8da37f40e4..5138b58330 100755 --- a/linux/debian/rules +++ b/linux/debian/rules @@ -66,10 +66,7 @@ override_dh_auto_install: rm $(CURDIR)/debian/keyman/usr/share/locale/*.po* # Don't call `build.sh install` - dh_auto_install does some extra smarts dh_auto_install --sourcedir=linux/keyman-config --buildsystem=pybuild $@ - # Unfortunately bash-completion 2.10 (focal) doesn't yet provide dh-sequence-bash-completion, - # which we could add as build-dependency, so we'll have to explicitly call - # dh_bash_completion here instead of using `dh $@ --with-python3 --with bash-completion` - dh_bash-completion -O--buildsystem=pybuild + dh $@ --with-python3 --with bash-completion dh_python3 -O--buildsystem=pybuild override_dh_missing: diff --git a/linux/docs/help/about/welcome.md b/linux/docs/help/about/welcome.md index eacc8c449c..dfd410b46e 100644 --- a/linux/docs/help/about/welcome.md +++ b/linux/docs/help/about/welcome.md @@ -6,6 +6,6 @@ Thank you for installing Keyman for Linux. Whether you're a new or returning use ## What is Keyman for Linux? -Keyman for Linux makes it possible to type in over 1,000 languages in any Linux application. +Keyman for Linux makes it possible to type in over 2,000 languages in any Linux application. [Click here](../start/) to get started. diff --git a/linux/docs/help/about/whatsnew.md b/linux/docs/help/about/whatsnew.md index 4a4d5a30e1..5521261b96 100644 --- a/linux/docs/help/about/whatsnew.md +++ b/linux/docs/help/about/whatsnew.md @@ -1,5 +1,5 @@ --- -title: What's New in Keyman 18.0 for Linux +title: What's New in Keyman 19.0 for Linux --- -Here are some of the new features we have added to Keyman for Linux 18.0: +Here are some of the new features we have added to Keyman for Linux 19.0: diff --git a/linux/docs/help/common/index.md b/linux/docs/help/common/index.md index b2d606b38d..a7f9e786bf 100644 --- a/linux/docs/help/common/index.md +++ b/linux/docs/help/common/index.md @@ -59,8 +59,8 @@ no on-screen keyboard for Wayland that works with Keyman. **A.** Keyman runs on Debian, Ubuntu, Wasta Linux and can be compiled to run from source in most distributions. -**Note:** Ubuntu versions before Ubuntu 20.04 LTS are no longer supported with -Keyman 17. If you are still running an older version and require Keyman you'll +**Note:** Ubuntu versions before Ubuntu 22.04 LTS are no longer supported with +Keyman 18. If you are still running an older version and require Keyman you'll have to install an older Keyman version. ## Q. Will my existing Windows Keyman keyboard work with Keyman for Linux? diff --git a/linux/docs/help/index.md b/linux/docs/help/index.md index 58d245063a..329c66f69b 100644 --- a/linux/docs/help/index.md +++ b/linux/docs/help/index.md @@ -1,5 +1,5 @@ --- -title: Keyman for Linux 18.0 Help +title: Keyman for Linux 19.0 Help --- Need help using Keyman for Linux? In time, this product documentation will grow and explain frequently asked questions. diff --git a/linux/ibus-keyman/src/KeymanSystemServiceClient.cpp b/linux/ibus-keyman/src/KeymanSystemServiceClient.cpp index 50dbccba8d..b411e808f7 100644 --- a/linux/ibus-keyman/src/KeymanSystemServiceClient.cpp +++ b/linux/ibus-keyman/src/KeymanSystemServiceClient.cpp @@ -15,7 +15,6 @@ extern gboolean testing; class KeymanSystemServiceClient { private: - sd_bus_error *error = NULL; sd_bus_message *msg = NULL; sd_bus *bus = NULL; @@ -25,6 +24,7 @@ public: void SetCapsLockIndicator(guint32 capsLock); gint32 GetCapsLockIndicator(); + void CallOrderedOutputSentinel(); }; KeymanSystemServiceClient::KeymanSystemServiceClient() { @@ -40,18 +40,13 @@ KeymanSystemServiceClient::KeymanSystemServiceClient() { } KeymanSystemServiceClient::~KeymanSystemServiceClient() { - if (error) { sd_bus_error_free(error); } if (msg) { sd_bus_message_unref(msg); } if (bus) { sd_bus_unref(bus); } } void KeymanSystemServiceClient::SetCapsLockIndicator(guint32 capsLock) { - // If Set/GetCapsLockIndicator is called more than once and previously - // failed, we will leak `error`. - assert(error == NULL); - if (!bus) { - // we already reported the error, so just return + // we already reported the error in the c'tor, so just return return; } @@ -60,30 +55,30 @@ void KeymanSystemServiceClient::SetCapsLockIndicator(guint32 capsLock) { return; } + sd_bus_error *error = NULL; int result = sd_bus_call_method(bus, KEYMAN_BUS_NAME, KEYMAN_OBJECT_PATH, KEYMAN_INTERFACE_NAME, "SetCapsLockIndicator", error, &msg, "b", capsLock); if (result < 0) { g_error("%s: Failed to call method SetCapsLockIndicator: %s. %s. %s.", __FUNCTION__, strerror(-result), error ? error->name : "-", error ? error->message : "-"); + sd_bus_error_free(error); return; } } gint32 KeymanSystemServiceClient::GetCapsLockIndicator() { - // If Set/GetCapsLockIndicator is called more than once and previously - // failed, we will leak `error`. - assert(error == NULL); - if (!bus) { - // we already reported the error, so just return + // we already reported the error in the c'tor, so just return return -1; } + sd_bus_error *error = NULL; int result = sd_bus_call_method(bus, KEYMAN_BUS_NAME, KEYMAN_OBJECT_PATH, KEYMAN_INTERFACE_NAME, "GetCapsLockIndicator", error, &msg, ""); if (result < 0) { g_error("%s: Failed to call method GetCapsLockIndicator: %s. %s. %s.", __FUNCTION__, strerror(-result), error ? error->name : "-", error ? error->message : "-"); + sd_bus_error_free(error); return -1; } @@ -98,9 +93,26 @@ gint32 KeymanSystemServiceClient::GetCapsLockIndicator() { return capsLock; } -void set_capslock_indicator( - guint32 capsLock -) { +void +KeymanSystemServiceClient::CallOrderedOutputSentinel() { + if (!bus) { + // we already reported the error in the c'tor, so just return + return; + } + + sd_bus_error *error = NULL; + int result = sd_bus_call_method(bus, KEYMAN_BUS_NAME, KEYMAN_OBJECT_PATH, + KEYMAN_INTERFACE_NAME, "CallOrderedOutputSentinel", error, &msg, ""); + if (result < 0) { + g_error("%s: Failed to call method CallOrderedOutputSentinel: %s. %s. %s.", + __FUNCTION__, strerror(-result), error ? error->name : "-", error ? error->message : "-"); + sd_bus_error_free(error); + return; + } +} + +void +set_capslock_indicator(guint32 capsLock) { KeymanSystemServiceClient client; client.SetCapsLockIndicator(capsLock); } @@ -109,3 +121,10 @@ gint32 get_capslock_indicator() { KeymanSystemServiceClient client; return client.GetCapsLockIndicator(); } + +void +call_ordered_output_sentinel() { + g_message("%s: Calling order output sentinel on keyman-system-service", __FUNCTION__); + KeymanSystemServiceClient client; + client.CallOrderedOutputSentinel(); +} diff --git a/linux/ibus-keyman/src/KeymanSystemServiceClient.h b/linux/ibus-keyman/src/KeymanSystemServiceClient.h index 746974df51..bf5766434f 100644 --- a/linux/ibus-keyman/src/KeymanSystemServiceClient.h +++ b/linux/ibus-keyman/src/KeymanSystemServiceClient.h @@ -9,6 +9,7 @@ extern "C" { void set_capslock_indicator(guint32 capsLockState); gint32 get_capslock_indicator(); +void call_ordered_output_sentinel(); #ifdef __cplusplus } diff --git a/linux/ibus-keyman/src/engine.c b/linux/ibus-keyman/src/engine.c index 296467e863..29d1be00d2 100644 --- a/linux/ibus-keyman/src/engine.c +++ b/linux/ibus-keyman/src/engine.c @@ -39,29 +39,18 @@ #include "engine.h" #include "keycodes.h" -// Fallback for older ibus versions that don't define IBUS_PREFILTER_MASK -#ifndef IBUS_HAS_PREFILTER -#ifdef KEYMAN_PKG_BUILD -// When building packages on Ubuntu and Debian servers we probably don't have -// a patched ibus available and additionally treat warnings as errors, but -// still want to build packages. -#pragma message "Compiling against ibus version that does not include prefilter mask patch\n(https://github.com/ibus/ibus/pull/2440). Output ordering guarantees will be disabled." -#else -#warning Compiling against ibus version that does not include prefilter mask patch (https://github.com/ibus/ibus/pull/2440). Output ordering guarantees will be disabled. -#endif - -#define IBUS_PREFILTER_MASK (1 << 23) -#endif - #define MAXCONTEXT_ITEMS 128 + +// Values from /usr/include/linux/input-event-codes.h #define KEYMAN_BACKSPACE 14 #define KEYMAN_BACKSPACE_KEYSYM IBUS_KEY_BackSpace -#define KEYMAN_LCTRL 29 // 0x1D -#define KEYMAN_LALT 56 // 0x38 -#define KEYMAN_RCTRL 97 // 0x61 -#define KEYMAN_RALT 100 // 0x64 -#define KEYMAN_F24_KEYCODE_OUTPUT_SENTINEL 202 -#define KEYMAN_NOCHAR_KEYSYM (0xfdd0 | 0x1000000) // Unicode NOCHAR +#define KEYMAN_LCTRL 29 // 0x1D +#define KEYMAN_LSHIFT 42 // 0x2A +#define KEYMAN_RSHIFT 54 // 0x36 +#define KEYMAN_LALT 56 // 0x38 +#define KEYMAN_RCTRL 97 // 0x61 +#define KEYMAN_RALT 100 // 0x64 +#define KEYMAN_F24_KEYCODE_OUTPUT_SENTINEL 194 // 0xC2 typedef struct _IBusKeymanEngine IBusKeymanEngine; typedef struct _IBusKeymanEngineClass IBusKeymanEngineClass; @@ -69,11 +58,12 @@ typedef struct _IBusKeymanEngineClass IBusKeymanEngineClass; #define MAX_QUEUE_SIZE 100 typedef struct _commit_queue_item { - // char_buffer and emitting_keystroke as well as more than one queue - // item are only used if ibus supports prefilter but the client - // doesn't support surrounding text (non-compliant app) + // char_buffer, emitting_keystroke and code_points_to_delete as well as + // more than one queue item are only used if the client doesn't + // support surrounding text (non-compliant app) gchar *char_buffer; gboolean emitting_keystroke; + guint code_points_to_delete; guint keyval; guint keycode; @@ -253,17 +243,6 @@ debug_utf8_with_codepoints(const gchar *utf8) { #endif } -static gboolean -client_supports_prefilter(IBusEngine *engine) -{ - g_assert(engine != NULL); -#ifdef IBUS_HAS_PREFILTER - return (engine->client_capabilities & IBUS_CAP_PREFILTER) != 0; -#else - return FALSE; -#endif -} - static gboolean client_supports_surrounding_text(IBusEngine *engine) { g_assert(engine != NULL); @@ -660,14 +639,14 @@ process_output_action(IBusEngine *engine, const km_core_usv* output_utf32) { IBusKeymanEngine *keyman = (IBusKeymanEngine *)engine; gchar *output_utf8 = g_ucs4_to_utf8(output_utf32, -1, NULL, NULL, NULL); g_autofree gchar *debug = NULL; - if (client_supports_prefilter(engine) && !client_supports_surrounding_text(engine)) { - // non-compliant app with patched ibus + if (!client_supports_surrounding_text(engine)) { + // non-compliant app g_message("%s: Adding to commit queue: %s", __FUNCTION__, debug = debug_utf8_with_codepoints(output_utf8)); g_assert(keyman->commit_item->char_buffer == NULL); keyman->commit_item->char_buffer = output_utf8; // don't free output_utf8 - assigned to char_buffer! } else { - // compliant app or unpatched ibus + // compliant app g_message("%s: Outputing %s", __FUNCTION__, debug = debug_utf8_with_codepoints(output_utf8)); commit_string(keyman, output_utf8); g_free(output_utf8); @@ -695,11 +674,9 @@ process_backspace_action(IBusEngine *engine, unsigned int code_points_to_delete) g_message("%s: compliant app: deleting surrounding text %d codepoints", __FUNCTION__, code_points_to_delete); ibus_engine_delete_surrounding_text(engine, -code_points_to_delete, code_points_to_delete); } else { - g_message("%s: non-compliant app: forwarding %d backspaces", __FUNCTION__, code_points_to_delete); - while (code_points_to_delete > 0) { - ibus_engine_forward_key_event(engine, KEYMAN_BACKSPACE_KEYSYM, KEYMAN_BACKSPACE, 0); - code_points_to_delete--; - } + g_message("%s: non-compliant app: queueing %d backspaces", __FUNCTION__, code_points_to_delete); + IBusKeymanEngine *keyman = (IBusKeymanEngine *)engine; + keyman->commit_item->code_points_to_delete = code_points_to_delete; } } @@ -723,8 +700,8 @@ process_emit_keystroke_action(IBusEngine *engine, km_core_bool emit_keystroke) { return; } IBusKeymanEngine *keyman = (IBusKeymanEngine *)engine; - if (!client_supports_prefilter(engine) || client_supports_surrounding_text(engine)) { - // compliant app or unpatched ibus version + if (client_supports_surrounding_text(engine)) { + // compliant app ibus_engine_forward_key_event(engine, keyman->commit_item->keyval, keyman->commit_item->keycode, keyman->commit_item->state); return; @@ -745,21 +722,42 @@ process_capslock_action(km_core_caps_state caps_state) { static void commit_current_queue_item(IBusKeymanEngine *keyman) { - // only called for non-compliant apps with patched ibus + // only called for non-compliant apps g_assert(keyman != NULL); - g_assert(client_supports_prefilter((IBusEngine *)keyman)); - g_assert(!client_supports_surrounding_text((IBusEngine *)keyman)); + IBusEngine* engine = (IBusEngine *)keyman; + g_assert(!client_supports_surrounding_text(engine)); - if (keyman->commit_item <= keyman->commit_queue) + if (keyman->commit_item <= keyman->commit_queue){ + g_message("%s: queue is empty", __FUNCTION__); return; + } commit_queue_item *current_item = &keyman->commit_queue[0]; + if (current_item->code_points_to_delete > 0) { + g_message("%s: Forwarding %d backspaces from commit queue", __FUNCTION__, current_item->code_points_to_delete); + while (current_item->code_points_to_delete > 0) { + ibus_engine_forward_key_event(engine, KEYMAN_BACKSPACE_KEYSYM, KEYMAN_BACKSPACE, 0); + current_item->code_points_to_delete--; + } + // don't remove the item from the queue yet - we need to process it + // again for the output and keystrokes. Instead emit the sentinel key + // again. + g_message("%s: Forcing ordered output", __FUNCTION__); + call_ordered_output_sentinel(); + return; + } if (current_item->char_buffer != NULL) { + g_autofree gchar *debug = NULL; + g_message("%s: Committing from commit queue: %s", __FUNCTION__, + debug = debug_utf8_with_codepoints(current_item->char_buffer)); commit_string(keyman, current_item->char_buffer); g_free(current_item->char_buffer); + current_item->char_buffer = NULL; } if (current_item->emitting_keystroke) { - ibus_engine_forward_key_event((IBusEngine*)keyman, current_item->keyval, current_item->keycode, current_item->state); + g_message("%s: Forwarding key from commit queue: keyval=0x%02x, keycode=0x%02x, state=0x%02x", + __FUNCTION__, current_item->keyval, current_item->keycode, current_item->state); + ibus_engine_forward_key_event(engine, current_item->keyval, current_item->keycode, current_item->state); } keyman->commit_item--; memmove(keyman->commit_queue, &keyman->commit_queue[1], sizeof(commit_queue_item) * MAX_QUEUE_SIZE - 1); @@ -770,30 +768,41 @@ static void finish_process_actions(IBusEngine *engine) { g_assert(engine != NULL); IBusKeymanEngine *keyman = (IBusKeymanEngine *)engine; - if (!client_supports_prefilter(engine) || client_supports_surrounding_text(engine)) { - // compliant app or unpatched ibus + if (client_supports_surrounding_text(engine)) { + // compliant app return; } - // non-compliant app with patched ibus - guint state = keyman->commit_item->state; - keyman->commit_item++; - if (keyman->commit_item > &keyman->commit_queue[MAX_QUEUE_SIZE-1]) { - g_error("Overflow of keyman commit_queue!"); - // TODO: log to Sentry - keyman->commit_item--; - } + // non-compliant app + guint keycode = keyman->commit_item->keycode; - // Forward a fake key event to get the correct order of events so that any backspace key we - // generated will be processed before the character we're adding. We need to send a - // valid keyval/keycode combination so that it doesn't get swallowed by GTK but which - // isn't very likely used in real keyboards. F24 seems to work for that. - ibus_engine_forward_key_event(engine, - KEYMAN_NOCHAR_KEYSYM, - KEYMAN_F24_KEYCODE_OUTPUT_SENTINEL, - (state & IBUS_RELEASE_MASK) - ? IBUS_PREFILTER_MASK | IBUS_RELEASE_MASK - : IBUS_PREFILTER_MASK); + switch (keycode) { + case KEYMAN_LSHIFT: + case KEYMAN_RSHIFT: + case KEYMAN_LCTRL: + case KEYMAN_RCTRL: + case KEYMAN_LALT: + case KEYMAN_RALT: + // we don't forward modifier keys that the user holds while pressing another + // key. + g_message("%s: Ignoring modifier key", __FUNCTION__); + break; + default: + keyman->commit_item++; + if (keyman->commit_item > &keyman->commit_queue[MAX_QUEUE_SIZE - 1]) { + g_error("Overflow of keyman commit_queue!"); + // TODO: log to Sentry + keyman->commit_item--; + } + + // Forward a fake key event to get the correct order of events so that any backspace key we + // generated will be processed before the character we're adding. We need to send a + // valid keycode so that it doesn't get swallowed by GTK but which isn't very likely used + // in real keyboards. F24 seems to work for that. + g_message("%s: Forcing ordered output", __FUNCTION__); + call_ordered_output_sentinel(); + break; + } } static void @@ -826,13 +835,25 @@ ibus_keyman_engine_process_key_event( g_message("-----------------------------------------------------------------------------------------------------------------"); g_message( - "DAR: %s - keyval=0x%02x keycode=0x%02x, state=0x%02x, isKeyDown=%d, supports_prefilter=%d, compliant=%d", __FUNCTION__, keyval, keycode, - state, isKeyDown, client_supports_prefilter(engine), client_supports_surrounding_text(engine)); + "DAR: %s - keyval=0x%02x keycode=0x%02x, state=0x%02x, isKeyDown=%d, compliant=%d", __FUNCTION__, keyval, keycode, + state, isKeyDown, client_supports_surrounding_text(engine)); // This keycode is a fake keycode that we send when it's time to commit the text, ensuring the // correct output order of backspace and text. - if (client_supports_prefilter(engine) && !client_supports_surrounding_text(engine) && - keycode == KEYMAN_F24_KEYCODE_OUTPUT_SENTINEL && (state & IBUS_PREFILTER_MASK)) { + if (!client_supports_surrounding_text(engine) && keycode == KEYMAN_F24_KEYCODE_OUTPUT_SENTINEL) { + if (!isKeyDown) { + g_message("%s: got F24 Sentinel, ignore keyup", __FUNCTION__); + return TRUE; + } + g_message("%s: got F24 Sentinel, queue content:", __FUNCTION__); + for (int i = 0; i < MAX_QUEUE_SIZE && &keyman->commit_queue[i] != keyman->commit_item; i++) { + commit_queue_item *item = &keyman->commit_queue[i]; + g_message( + " queue item %d: keyval=0x%02x keycode=0x%02x, state=0x%02x, char_buffer=%s, " + "emitting_keystroke=%d, code_points_to_delete=%d", + i, item->keyval, item->keycode, item->state, item->char_buffer, + item->emitting_keystroke, item->code_points_to_delete); + } commit_current_queue_item(keyman); return TRUE; } @@ -869,7 +890,7 @@ ibus_keyman_engine_process_key_event( // #10476: Core currently doesn't handle IBUS_MOD{2-4}_MASK modifiers. // On Ubuntu 23.10/24.04 we get IBUS_MOD4_MASK set on keycode 0x39 (space) when // the user tries to switch keyboards. Since this is not a regular keypress - // and Core doesn't handle it, we need to just return and let the Gnome deal + // and Core doesn't handle it, we need to just return and let GTK deal // with it. We could consider to add it to Core and let Core ignore it. // As for IBUS_MOD3_MASK it's unclear when/how that gets set, so we // just not deal with that for now until we notice problems. @@ -883,7 +904,7 @@ ibus_keyman_engine_process_key_event( // it. At the moment however we let Core process the keypress and since // it doesn't have rules for the numeric keypad keys we eventually // forward the key to ibus (in process_emit_keystroke_action) and let - // Gnome deal with it. + // GTK deal with it. // keyman modifiers are different from X11/ibus uint16_t km_mod_state = 0; @@ -929,15 +950,14 @@ ibus_keyman_engine_process_key_event( process_actions(engine, core_actions); - // If we have a new ibus version that supports prefilter and a non-compliant - // client, i.e. a client that doesn't support surrounding text (e.g. - // Chromium as of v104) we forwarded the key event with IBUS_PREFILTER_MASK - // set and now stop further processing by returning TRUE. - // With an old ibus version without prefilter support as well as with - // a compliant client (i.e. it does support surrounding text), we return + // If we have a non-compliant client, i.e. a client that doesn't support + // surrounding text (e.g. Chromium as of v104) we sent the key event + // to the system service and now stop further processing by returning TRUE. + // With a compliant client (i.e. it does support surrounding text), we return // TRUE because we completely processed the event and no further // processing should happen. g_message("%s: after processing all actions: %s", __FUNCTION__, debug_context2 = get_context_debug(engine)); + return TRUE; } diff --git a/linux/ibus-keyman/src/keymanutil.c b/linux/ibus-keyman/src/keymanutil.c index 9f48f0ffce..4bb0ac4078 100644 --- a/linux/ibus-keyman/src/keymanutil.c +++ b/linux/ibus-keyman/src/keymanutil.c @@ -649,9 +649,8 @@ _ptr_array_new_from_array( } // `g_ptr_array_new_from_null_terminated_array` is only available in GLib 2.76, but we're still -// stuck to 2.64 (Ubuntu 20.04 Focal) and 2.72 (Ubuntu 22.04 Jammy). Therefore we -// copy the implementation here (slightly simplified). Once we're past 2.76 we can use the GLib method -// directly. +// stuck to 2.72 (Ubuntu 22.04 Jammy). Therefore we copy the implementation here (slightly +// simplified). Once we're past 2.76 we can use the GLib method directly. GPtrArray * _g_ptr_array_new_from_null_terminated_array( gpointer *data, diff --git a/linux/ibus-keyman/src/test/keymanutil.tests.c b/linux/ibus-keyman/src/test/keymanutil.tests.c index 13636857d8..56965a41ad 100644 --- a/linux/ibus-keyman/src/test/keymanutil.tests.c +++ b/linux/ibus-keyman/src/test/keymanutil.tests.c @@ -123,22 +123,6 @@ _free_tst_kb_data(add_keyboard_data* kb_data) { G_DEFINE_AUTOPTR_CLEANUP_FUNC(add_keyboard_data, _free_tst_kb_data) -// Newer glib versions have g_assert_cmpstrv which would allow to do -// g_assert_cmpstrv(result, keyboards); -// but unfortunately Ubuntu 20.04 Focal doesn't have that, so we roll -// our own -void -_kmn_assert_cmpstrv(gchar** result, gchar** expected) { - g_assert_nonnull(result); - g_assert_nonnull(expected); - int i = 0; - for (; result[i] && expected[i]; i++) { - g_assert_cmpstr(result[i], ==, expected[i]); - } - g_assert_null(result[i]); - g_assert_null(expected[i]); -} - //---------------------------------------------------------------------------------------------- void test_keyman_put_keyboard_options_todconf__invalid() { @@ -526,7 +510,7 @@ test_keyman_set_custom_keyboards__new_key() { // Verify g_auto(GStrv) result = _get_tst_kbds_key(); g_assert_nonnull(result); - _kmn_assert_cmpstrv(result, keyboards); + g_assert_cmpstrv(result, keyboards); // Cleanup _delete_tst_kbds_key(); @@ -546,7 +530,7 @@ test_keyman_set_custom_keyboards__overwrite_key() { // Verify g_auto(GStrv) result = _get_tst_kbds_key(); g_assert_nonnull(result); - _kmn_assert_cmpstrv(result, keyboards); + g_assert_cmpstrv(result, keyboards); // Cleanup _delete_tst_kbds_key(); @@ -565,7 +549,7 @@ test_keyman_set_custom_keyboards__delete_key_NULL() { g_auto(GStrv) result = _get_tst_kbds_key(); gchar* expected[] = {NULL}; g_assert_nonnull(result); - _kmn_assert_cmpstrv(result, expected); + g_assert_cmpstrv(result, expected); // Cleanup _delete_tst_kbds_key(); @@ -585,7 +569,7 @@ test_keyman_set_custom_keyboards__delete_key_empty_array() { // Verify g_auto(GStrv) result = _get_tst_kbds_key(); g_assert_nonnull(result); - _kmn_assert_cmpstrv(result, keyboards); + g_assert_cmpstrv(result, keyboards); // Cleanup _delete_tst_kbds_key(); @@ -604,7 +588,7 @@ test_keyman_set_custom_keyboards__invalid_values() { g_auto(GStrv) result = _get_tst_kbds_key(); g_assert_nonnull(result); gchar* expected[] = {"fr:/tmp/test/test.kmx", NULL}; - _kmn_assert_cmpstrv(result, expected); + g_assert_cmpstrv(result, expected); // Cleanup _delete_tst_kbds_key(); @@ -622,7 +606,7 @@ test_keyman_get_custom_keyboards__value() { // Verify g_assert_nonnull(result); - _kmn_assert_cmpstrv(result, keyboards); + g_assert_cmpstrv(result, keyboards); // Cleanup _delete_tst_kbds_key(); @@ -640,7 +624,7 @@ test_keyman_get_custom_keyboards__invalid_values() { // Verify gchar* expected[] = {"fr:/tmp/test/test.kmx", NULL}; g_assert_nonnull(result); - _kmn_assert_cmpstrv(result, expected); + g_assert_cmpstrv(result, expected); // Cleanup _delete_tst_kbds_key(); diff --git a/linux/ibus-keyman/tests/ibusimcontext.c b/linux/ibus-keyman/tests/ibusimcontext.c index c6d7af1e98..d3181727ce 100644 --- a/linux/ibus-keyman/tests/ibusimcontext.c +++ b/linux/ibus-keyman/tests/ibusimcontext.c @@ -23,26 +23,13 @@ */ // This file is based on https://github.com/ibus/ibus/blob/master/client/gtk2/ibusimcontext.c -// commit 506ac9993d5166196b7c4e9bfa9fb0f9d3792ffa plus our two prefilter commits. +// commit 506ac9993d5166196b7c4e9bfa9fb0f9d3792ffa. // It simulates the GTK2 client, leaving out code for GTK3 and GTK4 and // simplyfying the code a bit by replacing async calls with direct synchronous // method calls. #include -#ifndef IBUS_HAS_PREFILTER -#ifdef KEYMAN_PKG_BUILD -// When building packages on Ubuntu and Debian servers we probably don't have -// a patched ibus available and additionally treat warnings as errors, but -// still want to build packages. -#pragma message "Compiling against ibus version that does not include prefilter mask patch\n(https://github.com/ibus/ibus/pull/2440). Output ordering guarantees will be disabled." -#else -#warning Compiling against ibus version that does not include prefilter mask patch (https://github.com/ibus/ibus/pull/2440). Output ordering guarantees will be disabled. -#endif - -#define IBUS_PREFILTER_MASK (1 << 23) -#endif - #include "ibusimcontext.h" #include #include @@ -249,7 +236,6 @@ _process_key_event_done(GObject *object, GAsyncResult *res, gpointer user_data) if (retval == FALSE) { ((GdkEventKey *)event)->state |= IBUS_IGNORED_MASK; - ((GdkEventKey *)event)->state &= ~IBUS_PREFILTER_MASK; gdk_event_put(event); } gdk_event_free(event); @@ -401,10 +387,6 @@ ibus_im_context_init(GObject *obj) { ibusimcontext->caps |= IBUS_CAP_SURROUNDING_TEXT; } -#ifdef IBUS_HAS_PREFILTER - ibusimcontext->caps |= IBUS_CAP_PREFILTER; -#endif - ibusimcontext->events_queue = g_queue_new(); if (ibus_bus_is_connected(_bus)) { @@ -463,7 +445,7 @@ ibus_im_context_filter_keypress(GtkIMContext *context, GdkEventKey *event) { /* Do not call gtk_im_context_filter_keypress() because * gtk_im_context_simple_filter_keypress() binds Ctrl-Shift-u */ - if (event->state & IBUS_IGNORED_MASK && !(event->state & IBUS_PREFILTER_MASK)) + if (event->state & IBUS_IGNORED_MASK) return ibus_im_context_commit_event(ibusimcontext, event); /* XXX it is a workaround for some applications do not set client @@ -956,10 +938,6 @@ _ibus_context_forward_key_event_cb( /* _create_gdk_event() will add 8 to keycode. */ if (keycode != 0) keycode -= 8; - } else if (state & IBUS_PREFILTER_MASK) { - // _create_gdk_event() will add 8 to keycode - if (keycode != 0) - keycode -= 8; } GdkEventKey *event = _create_gdk_event(ibusimcontext, keyval, keycode, state); @@ -983,8 +961,6 @@ _ibus_context_forward_key_event_cb( } } while (index >= 0); g_string_erase(ibusimcontext->text, index, len); - } else if (state & IBUS_PREFILTER_MASK) { - gtk_im_context_filter_keypress((GtkIMContext *)ibusimcontext, event); } else { gdk_event_put((GdkEvent *)event); } diff --git a/linux/ibus-keyman/tests/meson.build b/linux/ibus-keyman/tests/meson.build index 5ae10ae211..6e3c4355a4 100644 --- a/linux/ibus-keyman/tests/meson.build +++ b/linux/ibus-keyman/tests/meson.build @@ -66,103 +66,104 @@ find_tests = find_program('find-tests.sh', dirs: [meson.current_source_dir() / ' mutter = find_program('mutter', required: false, version: '>=40') can_build_wayland = mutter.found() -test( - 'setup-x11', - setup_tests, - args: ['--x11', env_file, cleanup_file, pid_file], - env: test_env, - priority: -10, - is_parallel: false, - protocol: 'exitcode' -) +# TODO: re-enable and fix test setup +# test( +# 'setup-x11', +# setup_tests, +# args: ['--x11', env_file, cleanup_file, pid_file], +# env: test_env, +# priority: -10, +# is_parallel: false, +# protocol: 'exitcode' +# ) -test( - 'teardown-x11', - teardown_tests, - args: [cleanup_file], - priority: -19, - is_parallel: false, - protocol: 'exitcode' -) +# test( +# 'teardown-x11', +# teardown_tests, +# args: [cleanup_file], +# priority: -19, +# is_parallel: false, +# protocol: 'exitcode' +# ) -if can_build_wayland - test( - 'setup-wayland', - setup_tests, - args: ['--wayland', env_file, cleanup_file, pid_file], - env: test_env, - priority: -20, - is_parallel: false, - protocol: 'exitcode' - ) +# if can_build_wayland +# test( +# 'setup-wayland', +# setup_tests, +# args: ['--wayland', env_file, cleanup_file, pid_file], +# env: test_env, +# priority: -20, +# is_parallel: false, +# protocol: 'exitcode' +# ) - test( - 'teardown-wayland', - teardown_tests, - args: [cleanup_file], - priority: -29, - is_parallel: false, - protocol: 'exitcode' - ) -endif +# test( +# 'teardown-wayland', +# teardown_tests, +# args: [cleanup_file], +# priority: -29, +# is_parallel: false, +# protocol: 'exitcode' +# ) +# endif -kmxtest_files = run_command( - find_tests, - [ common_dir / 'test/keyboards/baseline' ], - check: true, -).stdout().split('\n') +# kmxtest_files = run_command( +# find_tests, +# [ common_dir / 'test/keyboards/baseline' ], +# check: true, +# ).stdout().split('\n') -foreach kmx: kmxtest_files - filename = kmx.split('\t') - if filename[0] == '' - continue - endif - testname = filename[1].split('.kmx')[0] - test_args = [ '--tap', '-k', '--env', env_file, '--cleanup', cleanup_file, '--check', pid_file, '--', filename] - test( - 'X11-' + testname + '__surrounding-text', - run_test, - args: [ '--testname', testname, '--x11', '--surrounding-text', test_args], - env: test_env, - depends: [test_exe], - priority: -11, - is_parallel: false, - timeout: 120, - protocol: 'tap', - ) - test( - 'X11-' + testname + '__no-surrounding-text', - run_test, - args: [ '--testname', testname, '--x11', '--no-surrounding-text', test_args], - env: test_env, - depends: [test_exe], - priority: -12, - is_parallel: false, - timeout: 120, - protocol: 'tap', - ) - if can_build_wayland - test( - 'Wayland-' + testname + '__surrounding-text', - run_test, - args: [ '--testname', testname, '--wayland', '--surrounding-text', test_args], - env: test_env, - depends: [test_exe], - priority: -21, - is_parallel: false, - timeout: 120, - protocol: 'tap', - ) - test( - 'Wayland-' + testname + '__no-surrounding-text', - run_test, - args: [ '--testname', testname, '--wayland', '--no-surrounding-text', test_args], - env: test_env, - depends: [test_exe], - priority: -22, - is_parallel: false, - timeout: 120, - protocol: 'tap', - ) - endif -endforeach +# foreach kmx: kmxtest_files +# filename = kmx.split('\t') +# if filename[0] == '' +# continue +# endif +# testname = filename[1].split('.kmx')[0] +# test_args = [ '--tap', '-k', '--env', env_file, '--cleanup', cleanup_file, '--check', pid_file, '--', filename] +# test( +# 'X11-' + testname + '__surrounding-text', +# run_test, +# args: [ '--testname', testname, '--x11', '--surrounding-text', test_args], +# env: test_env, +# depends: [test_exe], +# priority: -11, +# is_parallel: false, +# timeout: 120, +# protocol: 'tap', +# ) +# test( +# 'X11-' + testname + '__no-surrounding-text', +# run_test, +# args: [ '--testname', testname, '--x11', '--no-surrounding-text', test_args], +# env: test_env, +# depends: [test_exe], +# priority: -12, +# is_parallel: false, +# timeout: 120, +# protocol: 'tap', +# ) +# if can_build_wayland +# test( +# 'Wayland-' + testname + '__surrounding-text', +# run_test, +# args: [ '--testname', testname, '--wayland', '--surrounding-text', test_args], +# env: test_env, +# depends: [test_exe], +# priority: -21, +# is_parallel: false, +# timeout: 120, +# protocol: 'tap', +# ) +# test( +# 'Wayland-' + testname + '__no-surrounding-text', +# run_test, +# args: [ '--testname', testname, '--wayland', '--no-surrounding-text', test_args], +# env: test_env, +# depends: [test_exe], +# priority: -22, +# is_parallel: false, +# timeout: 120, +# protocol: 'tap', +# ) +# endif +# endforeach diff --git a/linux/ibus-keyman/tests/scripts/run-tests.sh b/linux/ibus-keyman/tests/scripts/run-tests.sh index 012dee996f..4b28327259 100755 --- a/linux/ibus-keyman/tests/scripts/run-tests.sh +++ b/linux/ibus-keyman/tests/scripts/run-tests.sh @@ -65,7 +65,11 @@ function run_tests() { setup "$DISPLAY_SERVER" "$ENV_FILE" "$CLEANUP_FILE" "$PID_FILE" --standalone - echo "# NOTE: When the tests fail check /tmp/ibus-engine-keyman.log and /tmp/ibus-daemon.log!" + if [[ "${DOCKER_RUNNING:-false}" == "true" ]]; then + echo "# NOTE: When the tests fail check ibus-engine-keyman.log, ibus-daemon.log and km-test-server.log in build/docker-linux/tmp/!" + else + echo "# NOTE: When the tests fail check /tmp/ibus-engine-keyman.log, /tmp/ibus-daemon.log and /tmp/km-test-server.log!" + fi echo "" if [ $# -gt 0 ]; then diff --git a/linux/ibus-keyman/tests/scripts/test-helper.inc.sh b/linux/ibus-keyman/tests/scripts/test-helper.inc.sh index 940c5ad310..3287acd05c 100755 --- a/linux/ibus-keyman/tests/scripts/test-helper.inc.sh +++ b/linux/ibus-keyman/tests/scripts/test-helper.inc.sh @@ -140,7 +140,7 @@ function _setup_test_dbus_server() { CLEANUP_FILE=$2 # Start test dbus server. This will create `/tmp/km-test-server.env`. - "${TOP_BINDIR}/tests/km-dbus-test-server" &> /dev/null & + "${TOP_BINDIR}/tests/km-dbus-test-server" &> /tmp/km-test-server.log & sleep 1 cat /tmp/km-test-server.env >> "$ENV_FILE" @@ -251,7 +251,7 @@ function _setup_ibus() { #shellcheck disable=SC2086 ibus-daemon ${ARG_VERBOSE-} --daemonize --panel=disable --address=unix:abstract="${TEMP_DATA_DIR}/test-ibus" ${IBUS_CONFIG-} &> /tmp/ibus-daemon.log PID=$(pgrep -f "${TEMP_DATA_DIR}/test-ibus") - if [[ "${STANDALONE}" == "--standalone" ]] && [[ ! "${DOCKER_RUNNING:-false}" ]]; then + if [[ "${STANDALONE}" == "--standalone" ]] && [[ "${DOCKER_RUNNING:-false}" != "true" ]]; then # manual test run echo "if kill -9 ${PID}; then ibus restart || ibus start; fi # ibus-daemon" >> "${CLEANUP_FILE}" else @@ -321,6 +321,15 @@ function cleanup() { rm "$CLEANUP_FILE" echo "# Finished shutdown of processes." fi + + if [[ "${DOCKER_RUNNING:-false}" == "true" ]]; then + # Note: build/tmp is build/docker-linux/tmp on the host! + BUILD_TMP="$(dirname "$0")/../../../build/tmp" + mkdir -p "${BUILD_TMP}" + [[ -f /tmp/ibus-engine-keyman.log ]] && mv /tmp/ibus-engine-keyman.log "${BUILD_TMP}/ibus-engine-keyman.log" + [[ -f /tmp/ibus-daemon.log ]] && mv /tmp/ibus-daemon.log "${BUILD_TMP}/ibus-daemon.log" + [[ -f /tmp/km-test-server.log ]] && mv /tmp/km-test-server.log "${BUILD_TMP}/km-test-server.log" + fi } function exit_on_package_build() { diff --git a/linux/keyman-config/keyman_config/downloadkeyboard.py b/linux/keyman-config/keyman_config/downloadkeyboard.py index 42ff74308d..ca3b091f92 100755 --- a/linux/keyman-config/keyman_config/downloadkeyboard.py +++ b/linux/keyman-config/keyman_config/downloadkeyboard.py @@ -7,12 +7,7 @@ import urllib.parse import gi gi.require_version('Gtk', '3.0') -try: - gi.require_version('WebKit2', '4.1') -except ValueError: - # TODO: Remove once we drop support for Ubuntu 20.04 Focal - gi.require_version('WebKit2', '4.0') - +gi.require_version('WebKit2', '4.1') from gi.repository import Gtk, WebKit2 from keyman_config import KeymanComUrl, _, __releaseversion__, __tier__ diff --git a/linux/keyman-config/keyman_config/install_window.py b/linux/keyman-config/keyman_config/install_window.py index dda242c7e1..1342dd44f6 100755 --- a/linux/keyman-config/keyman_config/install_window.py +++ b/linux/keyman-config/keyman_config/install_window.py @@ -15,12 +15,7 @@ import packaging.version import gi gi.require_version('Gtk', '3.0') -try: - gi.require_version('WebKit2', '4.1') -except ValueError: - # TODO: Remove once we drop support for Ubuntu 20.04 Focal - gi.require_version('WebKit2', '4.0') - +gi.require_version('WebKit2', '4.1') from gi.repository import Gtk, WebKit2 from keyman_config import _, secure_lookup diff --git a/linux/keyman-config/keyman_config/keyboard_options_view.py b/linux/keyman-config/keyman_config/keyboard_options_view.py index 795386d7bf..38499a558a 100644 --- a/linux/keyman-config/keyman_config/keyboard_options_view.py +++ b/linux/keyman-config/keyman_config/keyboard_options_view.py @@ -8,12 +8,7 @@ from urllib.parse import parse_qsl, urlencode import gi gi.require_version('Gtk', '3.0') -try: - gi.require_version('WebKit2', '4.1') -except ValueError: - # TODO: Remove once we drop support for Ubuntu 20.04 Focal - gi.require_version('WebKit2', '4.0') - +gi.require_version('WebKit2', '4.1') from gi.repository import Gtk, WebKit2 from keyman_config import _ diff --git a/linux/keyman-config/keyman_config/welcome.py b/linux/keyman-config/keyman_config/welcome.py index af001ea464..a14d747c6a 100644 --- a/linux/keyman-config/keyman_config/welcome.py +++ b/linux/keyman-config/keyman_config/welcome.py @@ -7,11 +7,7 @@ import webbrowser import gi gi.require_version('Gtk', '3.0') -try: - gi.require_version('WebKit2', '4.1') -except ValueError: - # TODO: Remove once we drop support for Ubuntu 20.04 Focal - gi.require_version('WebKit2', '4.0') +gi.require_version('WebKit2', '4.1') from gi.repository import Gtk, WebKit2 from keyman_config import _ diff --git a/linux/keyman-system-service/src/KeyboardDevice.cpp b/linux/keyman-system-service/src/KeyboardDevice.cpp index a744a96b82..783902acaf 100644 --- a/linux/keyman-system-service/src/KeyboardDevice.cpp +++ b/linux/keyman-system-service/src/KeyboardDevice.cpp @@ -10,19 +10,26 @@ using namespace std; KeyboardDevice::KeyboardDevice() { - dev = nullptr; - fd = -1; + dev = nullptr; + fd = -1; hasCapsLockLed = -1; debug = false; } KeyboardDevice::~KeyboardDevice() +{ + Close(); +} + +void KeyboardDevice::Close() { if (dev) { libevdev_free(dev); + dev = nullptr; } if (fd != -1) { close(fd); + fd = -1; } } @@ -34,14 +41,14 @@ bool KeyboardDevice::Initialize(const char* name) fd = open(path.c_str(), O_RDWR); if (fd < 0) { std::cerr << "Failed to open device " << path << ": " << strerror(errno) << std::endl; + Close(); return false; } int rc = libevdev_new_from_fd(fd, &dev); if (rc < 0) { std::cerr << "Failed to init libevdev for " << path << ": " << strerror(-rc) << std::endl; - close(fd); - fd = -1; + Close(); return false; } diff --git a/linux/keyman-system-service/src/KeyboardDevice.h b/linux/keyman-system-service/src/KeyboardDevice.h index af9ce119f3..87310db15e 100644 --- a/linux/keyman-system-service/src/KeyboardDevice.h +++ b/linux/keyman-system-service/src/KeyboardDevice.h @@ -3,6 +3,7 @@ #include +// Any keyboard device available on the system class KeyboardDevice { public: @@ -20,6 +21,8 @@ class KeyboardDevice int fd; int hasCapsLockLed; bool debug; + + void Close(); }; #endif // __KEYBOARDDEVICE_H__ diff --git a/linux/keyman-system-service/src/KeymanSystemService.cpp b/linux/keyman-system-service/src/KeymanSystemService.cpp index 03d7add533..5e5b4f6e3e 100644 --- a/linux/keyman-system-service/src/KeymanSystemService.cpp +++ b/linux/keyman-system-service/src/KeymanSystemService.cpp @@ -63,10 +63,24 @@ on_get_caps_lock_indicator( return sd_bus_reply_method_return(msg, "b", state); } +static int32_t +on_call_ordered_output_sentinel( + sd_bus_message *msg, + void *user_data, + sd_bus_error *ret_error +) { + *ret_error = SD_BUS_ERROR_NULL; + + KeymanSystemService *service = static_cast(user_data); + service->CallOrderedOutputSentinel(); + return sd_bus_reply_method_return(msg, ""); +} + static const sd_bus_vtable system_service_vtable[] = { SD_BUS_VTABLE_START(0), SD_BUS_METHOD("SetCapsLockIndicator", "b", "", on_set_caps_lock_indicator, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_METHOD("GetCapsLockIndicator", "", "b", on_get_caps_lock_indicator, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD("CallOrderedOutputSentinel", "", "", on_call_ordered_output_sentinel, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_VTABLE_END }; @@ -75,6 +89,7 @@ KeymanSystemService::KeymanSystemService() int ret; GetKbdDevices(); + CreateOrderedOutputDevice(); #ifdef KEYMAN_TESTING ret = sd_bus_open_user(&bus); @@ -120,6 +135,12 @@ KeymanSystemService::~KeymanSystemService() delete device; } delete kbd_devices; + kbd_devices = nullptr; + } + + if (kbd_ordered_output) { + delete kbd_ordered_output; + kbd_ordered_output = nullptr; } } @@ -171,6 +192,16 @@ void KeymanSystemService::GetKbdDevices() { } } +void KeymanSystemService::CreateOrderedOutputDevice() { + if (!kbd_ordered_output) { + kbd_ordered_output = new OrderedOutputDevice(); + if (!kbd_ordered_output->Initialize()) { + delete kbd_ordered_output; + kbd_ordered_output = nullptr; + } + } +} + // Set the CapsLock indicator on all keyboard devices. void KeymanSystemService::SetCapsLockIndicatorOnDevices(uint32_t state) { @@ -198,3 +229,13 @@ KeymanSystemService::GetCapsLockIndicatorOnDevices() { } return state; } + +// Emit a ordered output sentinel key event +void +KeymanSystemService::CallOrderedOutputSentinel() { + if (!kbd_ordered_output) { + syslog(LOG_USER | LOG_ERR, "%s: No keyboard initialized", __FUNCTION__); + return; + } + kbd_ordered_output->PressSentinelKey(); +} diff --git a/linux/keyman-system-service/src/KeymanSystemService.h b/linux/keyman-system-service/src/KeymanSystemService.h index 86637f27a9..19c7efe6cc 100644 --- a/linux/keyman-system-service/src/KeymanSystemService.h +++ b/linux/keyman-system-service/src/KeymanSystemService.h @@ -8,18 +8,21 @@ #else #include #endif +#include "OrderedOutputDevice.h" #include "KeyboardDevice.h" using namespace std; class KeymanSystemService { private: - std::list* kbd_devices = NULL; - sd_bus_slot *slot = NULL; - sd_bus *bus = NULL; - bool failed = false; + std::list* kbd_devices = nullptr; + OrderedOutputDevice *kbd_ordered_output = nullptr; + sd_bus_slot *slot = nullptr; + sd_bus *bus = nullptr; + bool failed = false; void GetKbdDevices(); + void CreateOrderedOutputDevice(); public: KeymanSystemService(); @@ -33,6 +36,7 @@ public: int Loop(); void SetCapsLockIndicatorOnDevices(uint32_t state); uint32_t GetCapsLockIndicatorOnDevices(); + void CallOrderedOutputSentinel(); }; #endif // __KEYMANSYSTEMSERVICE_H__ diff --git a/linux/keyman-system-service/src/OrderedOutputDevice.cpp b/linux/keyman-system-service/src/OrderedOutputDevice.cpp new file mode 100644 index 0000000000..cf90ffa2f1 --- /dev/null +++ b/linux/keyman-system-service/src/OrderedOutputDevice.cpp @@ -0,0 +1,79 @@ +#include +#include +#include +#include +#include +#include +#include +#include "OrderedOutputDevice.h" + +using namespace std; + +#define KEYMAN_F24_KEYCODE_OUTPUT_SENTINEL 194 // 0xC2 + +OrderedOutputDevice::OrderedOutputDevice() { + uinput_dev = nullptr; +} + +OrderedOutputDevice::~OrderedOutputDevice() { + Close(); +} + +void +OrderedOutputDevice::Close() { + if (uinput_dev) { + libevdev_uinput_destroy(uinput_dev); + uinput_dev = nullptr; + } +} + +bool +OrderedOutputDevice::Initialize() { + struct libevdev* dev; + + syslog(LOG_USER | LOG_ALERT, "%s: creating fake device", __FUNCTION__); + + dev = libevdev_new(); + libevdev_set_name(dev, "Ordered Output Keyman Keyboard Device"); + + libevdev_enable_event_type(dev, EV_KEY); + + // F24 is the only key we support. + libevdev_enable_event_code(dev, EV_KEY, KEYMAN_F24_KEYCODE_OUTPUT_SENTINEL, NULL); + + int rc = libevdev_uinput_create_from_device(dev, LIBEVDEV_UINPUT_OPEN_MANAGED, &uinput_dev); + if (rc < 0) { + syslog(LOG_USER | LOG_ERR, "%s: Failed to create Ordered Output keyman keyboard device: %s", + __FUNCTION__, strerror(-rc)); + libevdev_free(dev); + Close(); + return false; + } + + return true; +} + +bool +OrderedOutputDevice::PressSentinelKey() { + int error = libevdev_uinput_write_event(uinput_dev, EV_KEY, KEYMAN_F24_KEYCODE_OUTPUT_SENTINEL, 1); + if (error < 0) { + syslog(LOG_USER | LOG_ERR, + "%s: Error writing send key event for sentinel key (down): %s", + __FUNCTION__, strerror(-error)); + return false; + } + error = libevdev_uinput_write_event(uinput_dev, EV_KEY, KEYMAN_F24_KEYCODE_OUTPUT_SENTINEL, 0); + if (error < 0) { + syslog(LOG_USER | LOG_ERR, "%s: Error writing send key event for sentinel key (up): %s", __FUNCTION__, strerror(-error)); + return false; + } + // process the key event immediately + error = libevdev_uinput_write_event(uinput_dev, EV_SYN, SYN_REPORT, 0); + if (error < 0) { + syslog(LOG_USER | LOG_ERR, + "%s: Error writing syn event for sentinel key: %s", __FUNCTION__, + strerror(-error)); + return false; + } + return true; +} diff --git a/linux/keyman-system-service/src/OrderedOutputDevice.h b/linux/keyman-system-service/src/OrderedOutputDevice.h new file mode 100644 index 0000000000..0fb2d15a3f --- /dev/null +++ b/linux/keyman-system-service/src/OrderedOutputDevice.h @@ -0,0 +1,24 @@ +#ifndef __ORDEREDOUTPUTDEVICE_H__ +#define __ORDEROUTPUTDEVICE_H__ + +#include + +// The fake keyboard we use to force the serializing of the output (#10799/#7079) +// Generate a fake key event to get the correct order of events so that any backspace key we +// generate will be processed before the character we're adding. We need to send a +// valid keycode so that it doesn't get swallowed by GTK but which isn't very likely used +// in real keyboards. F24 seems to work for that. +class OrderedOutputDevice { +public: + OrderedOutputDevice(); + virtual ~OrderedOutputDevice(); + + bool Initialize(); + bool PressSentinelKey(); + +private: + void Close(); + struct libevdev_uinput *uinput_dev; +}; + +#endif // __ORDEROUTPUTDEVICE_H__ diff --git a/linux/keyman-system-service/src/com.keyman.SystemService1.System.xml b/linux/keyman-system-service/src/com.keyman.SystemService1.System.xml index b75a1f2390..b93bcffa3a 100644 --- a/linux/keyman-system-service/src/com.keyman.SystemService1.System.xml +++ b/linux/keyman-system-service/src/com.keyman.SystemService1.System.xml @@ -25,5 +25,14 @@ + + + + + diff --git a/linux/keyman-system-service/src/meson.build b/linux/keyman-system-service/src/meson.build index 3c3dd27fa6..a99c977efb 100644 --- a/linux/keyman-system-service/src/meson.build +++ b/linux/keyman-system-service/src/meson.build @@ -1,6 +1,7 @@ service_files = files( 'KeyboardDevice.cpp', 'KeymanSystemService.cpp', + 'OrderedOutputDevice.cpp', 'main.cpp', ) diff --git a/linux/keyman-system-service/tests/KeyboardDeviceMock.cpp b/linux/keyman-system-service/tests/KeyboardDeviceMock.cpp index dbaa6aafab..c909e13025 100644 --- a/linux/keyman-system-service/tests/KeyboardDeviceMock.cpp +++ b/linux/keyman-system-service/tests/KeyboardDeviceMock.cpp @@ -24,6 +24,9 @@ KeyboardDeviceMock::KeyboardDeviceMock() { KeyboardDeviceMock::~KeyboardDeviceMock() { } +void KeyboardDeviceMock::Close() { +} + bool KeyboardDeviceMock::Initialize(const char* name) { return true; diff --git a/linux/keyman-system-service/tests/meson.build b/linux/keyman-system-service/tests/meson.build index 749e098591..917f0b15e6 100644 --- a/linux/keyman-system-service/tests/meson.build +++ b/linux/keyman-system-service/tests/meson.build @@ -1,6 +1,7 @@ test_service_files = files( '../src/main.cpp', '../src/KeymanSystemService.cpp', + '../src/OrderedOutputDevice.cpp', 'KeyboardDeviceMock.cpp', ) diff --git a/linux/scripts/launchpad.sh b/linux/scripts/launchpad.sh index 98ab9238f0..4b89980994 100755 --- a/linux/scripts/launchpad.sh +++ b/linux/scripts/launchpad.sh @@ -33,7 +33,7 @@ else fi echo "ppa: ${ppa}" -distributions="${DIST:-focal jammy noble oracular plucky}" +distributions="${DIST:-jammy noble oracular plucky}" packageversion="${PACKAGEVERSION:-1~sil1}" BASEDIR=$(pwd) diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/Base.lproj/preferences.xib b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/Base.lproj/preferences.xib index e6cc0173aa..a257c34419 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/Base.lproj/preferences.xib +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/Base.lproj/preferences.xib @@ -1,9 +1,9 @@ - + - - + + @@ -203,12 +203,6 @@ - - - - - - diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.h index 684db6fb38..c1264cb210 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.h @@ -14,5 +14,6 @@ - (void)menuAction:(id)sender; - (void)handleBackspace:(NSEvent *)event; - +- (void)changeClients:(NSString *)clientAppId; +- (void)deactivateClient; @end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m index dd53b49622..c628311b9f 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m @@ -25,22 +25,13 @@ KMInputMethodEventHandler* _eventHandler; - (id)initWithServer:(IMKServer *)server delegate:(id)delegate client:(id)inputClient { - os_log_debug([KMLogs lifecycleLog], "initWithServer, active app: '%{public}@'", [KMInputMethodLifecycle getRunningApplicationId]); + os_log_debug([KMLogs lifecycleLog], "+++KMInputController initWithServer, active app: '%{public}@'", [KMInputMethodLifecycle getRunningApplicationId]); self = [super initWithServer:server delegate:delegate client:inputClient]; if (self) { - self.appDelegate.inputController = self; + os_log_debug([KMLogs lifecycleLog], " +++initWithServer, self: %p", self); } - /** - * Register to receive the Deactivated and ChangedClient notification generated from KMInputMethodLifecycle so - * that the eventHandler can be changed. There is no need to receive the Activated notification because - * the InputController does it all it needs to when it receives the ChangedClient. - */ - - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inputMethodDeactivated:) name:kInputMethodDeactivatedNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inputMethodChangedClient:) name:kInputMethodClientChangeNotification object:nil]; - return self; } @@ -67,35 +58,81 @@ KMInputMethodEventHandler* _eventHandler; } /** - * The Keyman input method is deactivating because the user chose a different input method: notification from KMInputMethodLifecycle + * Called by the app delegate when KMInputMethodLifecycle determines the user has switched clients */ -- (void)inputMethodDeactivated:(NSNotification *)notification { - os_log_debug([KMLogs lifecycleLog], "***KMInputController inputMethodDeactivated, deactivating eventHandler"); - if (_eventHandler != nil) { - [_eventHandler deactivate]; - } +- (void)changeClients:(NSString *)clientAppId { + os_log_debug([KMLogs lifecycleLog], "***KMInputController changeClients, deactivate old eventHandler and activate new one"); + + [self deactivateEventHandler]; + [self activateEventHandler:clientAppId]; } /** - * The user has switched to a different text input client: notification from KMInputMethodLifecycle + * Called by the app delegate when KMInputMethodLifecycle determines the user has deactivated Keyman */ -- (void)inputMethodChangedClient:(NSNotification *)notification { - os_log_debug([KMLogs lifecycleLog], "***KMInputController inputMethodChangedClient, deactivating old eventHandler and activating new one"); - if (_eventHandler != nil) { - [_eventHandler deactivate]; - } - _eventHandler = [[KMInputMethodEventHandler alloc] initWithClient:[KMInputMethodLifecycle getRunningApplicationId] client:self.client]; - +- (void)deactivateClient { + os_log_debug([KMLogs lifecycleLog], "***KMInputController deactivateClient, deactivate old eventHandler"); + + [self deactivateEventHandler]; } +- (void)deactivateEventHandler { + if (_eventHandler != nil) { + os_log_debug([KMLogs lifecycleLog], " * KMInputController deactivate old eventHandler"); + [_eventHandler deactivate]; + } +} + +- (void)activateEventHandler:(NSString *)clientAppId { + os_log_debug([KMLogs lifecycleLog], " * KMInputController activate new eventHandler"); + _eventHandler = [[KMInputMethodEventHandler alloc] initWithClient:clientAppId client:self.client]; +} + +/** + * Called by the OS to activate the input method, but some calls are not useful and are followed milliseconds later + * by a deactivateServer. These short-lived activations may be generated by clicking and releasing menu items without changing applications. + * Rather than treat every message received as a true activation, we send a message to KMInputMethodLifeCycle to evaluate. + */ - (void)activateServer:(id)sender { + os_log_info([KMLogs lifecycleLog], " +++KMInputController, activateServer, self=%p", self); [sender overrideKeyboardWithKeyboardNamed:@"com.apple.keylayout.US"]; + + /* + When this KMInputController becomes the active server for the input method, + then immediately update the AppDelegate. The duration that this controller is the + active server may be extremely short, but, if so, we will receive another + call to activateServer moments later and can update the AppDelegate again. + */ + [self attachToAppDelegate]; + + /* + Call the shared lifecycle object so it can evaluate the current state + and determine whether this is + 1) a real activation of the input method or + 2) a change in clients or + 3) a false alarm + */ [KMInputMethodLifecycle.shared activateClient:sender]; } +- (void)attachToAppDelegate { + self.appDelegate.inputController = self; +} + +/** + * Called by the OS to deactivate the input method + */ - (void)deactivateServer:(id)sender { + os_log_info([KMLogs lifecycleLog], " +++KMInputController, deactivateServer, self=%p", self); + + /* + Call the shared lifecycle object so it can evaluate the current state + and determine whether this is + 1) a real deactivation of the input method or + 2) a change in clients or + 3) a false alarm + */ [KMInputMethodLifecycle.shared deactivateClient:sender]; - [[NSNotificationCenter defaultCenter] removeObserver:self]; } - (NSMenu *)menu { diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m index 0c4199959e..5e88d2b59b 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m @@ -123,15 +123,39 @@ id _lastServerWithOSKShowing = nil; // register to receive notifications generated from KMInputMethodLifecycle [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inputMethodActivated:) name:kInputMethodActivatedNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inputMethodDeactivated:) name:kInputMethodDeactivatedNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inputMethodChangedClient:) name:kInputMethodClientChangeNotification object:nil]; // start Input Method lifecycle [KMInputMethodLifecycle.shared startLifecycle]; } /** - * When the input method is deactivated, hide the OSK and disable the low-level event tap + * When the input method is activated -- notification from KMInputMethodLifecycle + */ +- (void)inputMethodActivated:(NSNotification *)notification { + + // enable event tap + if (self.lowLevelEventTap && !CGEventTapIsEnabled(self.lowLevelEventTap)) { + os_log_debug([KMLogs lifecycleLog], "***KMInputMethodAppDelegate inputMethodActivated, re-enabling event tap..."); + CGEventTapEnable(self.lowLevelEventTap, YES); + } + + // show OSK if necessary + os_log_debug([KMLogs lifecycleLog], "--- inputMethodActivated, kvk is non-nil: %{public}@ showOskOnActivate: %{public}@", (_kvk!=nil)?@"true":@"false", [KMInputMethodLifecycle.shared shouldShowOskOnActivate]?@"true":@"false"); + + if ([KMInputMethodLifecycle.shared shouldShowOskOnActivate]) { + os_log_debug([KMLogs oskLog], "***KMInputMethodAppDelegate inputMethodActivated, showing OSK"); + [KMSentryHelper addDebugBreadCrumb:@"lifecycle" message:@"opening OSK on input method activation"]; + [self showOSK]; + } +} + +/** + * When the input method is deactivated -- notification from KMInputMethodLifecycle */ - (void)inputMethodDeactivated:(NSNotification *)notification { + + // if the OSK is visible, hide it if ([self.oskWindow.window isVisible]) { os_log_debug([KMLogs oskLog], "***KMInputMethodAppDelegate inputMethodDeactivated, hiding OSK"); [KMSentryHelper addInfoBreadCrumb:@"lifecycle" message:@"hiding OSK on input method deactivation"]; @@ -141,28 +165,22 @@ id _lastServerWithOSKShowing = nil; } [KMSentryHelper addOskVisibleTag:[self.oskWindow.window isVisible]]; + // disable the event tap if (self.lowLevelEventTap) { os_log_debug([KMLogs lifecycleLog], "***inputMethodDeactivated, disabling event tap"); CGEventTapEnable(self.lowLevelEventTap, NO); } + + // deactive the text input client + [self.inputController deactivateClient]; } /** - * When the input method is activated, enable the low-level event tap and show the OSK + * When the user switches to a different text input client -- notification from KMInputMethodLifecycle */ -- (void)inputMethodActivated:(NSNotification *)notification { - if (self.lowLevelEventTap && !CGEventTapIsEnabled(self.lowLevelEventTap)) { - os_log_debug([KMLogs lifecycleLog], "***KMInputMethodAppDelegate inputMethodActivated, re-enabling event tap..."); - CGEventTapEnable(self.lowLevelEventTap, YES); - } - - os_log_debug([KMLogs lifecycleLog], "--- inputMethodActivated, kvk is non-nil: %{public}@ showOskOnActivate: %{public}@", (_kvk!=nil)?@"true":@"false", [KMInputMethodLifecycle.shared shouldShowOskOnActivate]?@"true":@"false"); - - if ([KMInputMethodLifecycle.shared shouldShowOskOnActivate]) { - os_log_debug([KMLogs oskLog], "***KMInputMethodAppDelegate inputMethodActivated, showing OSK"); - [KMSentryHelper addInfoBreadCrumb:@"lifecycle" message:@"opening OSK on input method activation"]; - [self showOSK]; - } +- (void)inputMethodChangedClient:(NSNotification *)notification { + os_log_debug([KMLogs lifecycleLog], "***KMInputMethodAppDelegate inputMethodChangedClient"); + [self.inputController changeClients:[KMInputMethodLifecycle getRunningApplicationId]]; } - (KeymanVersionInfo)versionInfo { diff --git a/mac/build.sh b/mac/build.sh index 8f17ba868d..00e577a45d 100755 --- a/mac/build.sh +++ b/mac/build.sh @@ -109,7 +109,7 @@ execBuildCommand() { set +e local ret_code=0 eval "$cmnd" || ret_code=$? - set -ex + set -e printXCodeBuildScriptLogs diff --git a/mac/docs/help/about/whatsnew.md b/mac/docs/help/about/whatsnew.md index 73ccb4d49b..8cc29bdadb 100644 --- a/mac/docs/help/about/whatsnew.md +++ b/mac/docs/help/about/whatsnew.md @@ -1,5 +1,5 @@ --- -title: What's New in Keyman 18.0 for macOS +title: What's New in Keyman 19.0 for macOS --- -Here are some of the new features we have added to Keyman 18.0 for macOS: +Here are some of the new features we have added to Keyman 19.0 for macOS: diff --git a/mac/docs/help/index.md b/mac/docs/help/index.md index aa507a531e..12397a903d 100644 --- a/mac/docs/help/index.md +++ b/mac/docs/help/index.md @@ -1,5 +1,5 @@ --- -title: Keyman 18.0 for macOS Help +title: Keyman 19.0 for macOS Help --- Need help using Keyman for macOS? You'll find everything you need here, including product documentation, diff --git a/oem/firstvoices/ios/Cartfile b/oem/firstvoices/ios/Cartfile index 0dae92f36b..9ffc07d330 100644 --- a/oem/firstvoices/ios/Cartfile +++ b/oem/firstvoices/ios/Cartfile @@ -1,4 +1,4 @@ github "weichsel/ZIPFoundation" ~> 0.9 github "devicekit/DeviceKit" ~> 5.0 github "ashleymills/Reachability.swift" -github "getsentry/sentry-cocoa" ~> 8.7.0 +github "getsentry/sentry-cocoa" ~> 8.38.0 diff --git a/oem/firstvoices/ios/Cartfile.resolved b/oem/firstvoices/ios/Cartfile.resolved index 95e608b932..ca05da1ae9 100644 --- a/oem/firstvoices/ios/Cartfile.resolved +++ b/oem/firstvoices/ios/Cartfile.resolved @@ -1,4 +1,4 @@ -github "ashleymills/Reachability.swift" "v5.1.0" -github "devicekit/DeviceKit" "5.1.0" -github "getsentry/sentry-cocoa" "8.15.2" -github "weichsel/ZIPFoundation" "0.9.17" +github "ashleymills/Reachability.swift" "v5.2.4" +github "devicekit/DeviceKit" "5.5.0" +github "getsentry/sentry-cocoa" "8.44.0" +github "weichsel/ZIPFoundation" "0.9.19" diff --git a/oem/firstvoices/windows/src/xml/strings.xml b/oem/firstvoices/windows/src/xml/strings.xml index 692a582dd7..51b19c060d 100644 --- a/oem/firstvoices/windows/src/xml/strings.xml +++ b/oem/firstvoices/windows/src/xml/strings.xml @@ -390,13 +390,8 @@ - - Automatically download updates in the background, for installation later - - - - - Automatically check keyman.com weekly for updates + + Automatically check for updates and download diff --git a/package-lock.json b/package-lock.json index 22eca5d230..ea094f457f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,8 +25,10 @@ "developer/src/kmc", "developer/src/server", "common/test/resources", - "common/tools/*", - "common/web/*", + "common/web/keyman-version", + "common/web/langtags", + "common/web/types", + "common/tools/sourcemap-path-remapper", "common/tools/hextobin", "web", "web/src/engine/osk/gesture-processor", @@ -38,6 +40,7 @@ "@keymanapp/developer-utils": "file:developer/src/common/web/utils", "@keymanapp/hextobin": "file:common/tools/hextobin", "@keymanapp/keyman-version": "file:common/web/keyman-version", + "@keymanapp/langtags": "file:common/web/langtags", "@keymanapp/ldml-keyboard-constants": "file:core/include/ldml" }, "devDependencies": { @@ -113,6 +116,8 @@ }, "common/tools/sourcemap-path-remapper/node_modules/convert-source-map": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "license": "MIT" }, "common/web/es6-shim": { @@ -131,6 +136,13 @@ "typescript": "^5.4.5" } }, + "common/web/langtags": { + "name": "@keymanapp/langtags", + "license": "MIT", + "devDependencies": { + "typescript": "^5.4.5" + } + }, "common/web/types": { "name": "@keymanapp/common-types", "license": "MIT", @@ -773,7 +785,8 @@ "dependencies": { "@keymanapp/common-types": "*", "@keymanapp/developer-utils": "*", - "@keymanapp/keyman-version": "*" + "@keymanapp/keyman-version": "*", + "@keymanapp/langtags": "*" }, "devDependencies": { "@keymanapp/developer-test-helpers": "*", @@ -888,7 +901,8 @@ "dependencies": { "@keymanapp/common-types": "*", "@keymanapp/developer-utils": "*", - "@keymanapp/kmc-package": "*" + "@keymanapp/kmc-package": "*", + "@keymanapp/langtags": "*" }, "devDependencies": { "@types/mocha": "^5.2.7", @@ -3011,6 +3025,10 @@ "resolved": "developer/src/kmc-package", "link": true }, + "node_modules/@keymanapp/langtags": { + "resolved": "common/web/langtags", + "link": true + }, "node_modules/@keymanapp/ldml-keyboard-constants": { "resolved": "core/include/ldml", "link": true diff --git a/package.json b/package.json index 135b0fdd3e..f0bc72f6b2 100644 --- a/package.json +++ b/package.json @@ -50,8 +50,10 @@ "developer/src/kmc", "developer/src/server", "common/test/resources", - "common/tools/*", - "common/web/*", + "common/web/keyman-version", + "common/web/langtags", + "common/web/types", + "common/tools/sourcemap-path-remapper", "common/tools/hextobin", "web", "web/src/engine/osk/gesture-processor", @@ -59,6 +61,7 @@ ], "dependencies": { "@keymanapp/common-types": "file:common/web/types", + "@keymanapp/langtags": "file:common/web/langtags", "@keymanapp/developer-test-helpers": "file:developer/src/common/web/test-helpers", "@keymanapp/developer-utils": "file:developer/src/common/web/utils", "@keymanapp/hextobin": "file:common/tools/hextobin", diff --git a/resources/build/increment-version.sh b/resources/build/increment-version.sh index 3be11dba1f..9e9ac699b8 100755 --- a/resources/build/increment-version.sh +++ b/resources/build/increment-version.sh @@ -192,7 +192,7 @@ if [ "$action" == "commit" ]; then git commit -m "$message (history cherry-pick to master)" # TODO: once we are sure this is stable, add `-l auto` to get the "auto:" # label - hub pull-request -f --no-edit -b master + hub pull-request -fp --no-edit fi # Return to our best working branch diff --git a/resources/build/minimum-versions.inc.sh b/resources/build/minimum-versions.inc.sh index 41a7c3690f..2e1a2914b1 100644 --- a/resources/build/minimum-versions.inc.sh +++ b/resources/build/minimum-versions.inc.sh @@ -1,4 +1,4 @@ -# Required minimum versions as of Keyman 18.0 +# Required minimum versions as of Keyman 19.0 # # This is a list of minimum, maximum, and specific versions of any external # components or dependencies found in Keyman. @@ -14,7 +14,7 @@ KEYMAN_MIN_TARGET_VERSION_ANDROID=5.0 # Lollipop KEYMAN_MIN_TARGET_VERSION_IOS=12.2 # iOS 12.2 KEYMAN_MIN_TARGET_VERSION_WINDOWS=10 # Windows 10 KEYMAN_MIN_TARGET_VERSION_MAC=10.13 # MacOS 10.13 (High Sierra) -KEYMAN_MIN_TARGET_VERSION_UBUNTU=20.04 # Ubuntu 20.04 Focal +KEYMAN_MIN_TARGET_VERSION_UBUNTU=22.04 # Ubuntu 22.04 Jammy KEYMAN_MIN_TARGET_VERSION_ANDROID_CHROME=53.0 # min version of Chrome for Keyman for Android # Target web browsers for KeymanWeb -- we do not have polyfills for diff --git a/resources/standards-data/ldml-keyboards/45/cldr_info.json b/resources/standards-data/ldml-keyboards/45/cldr_info.json deleted file mode 100644 index c7dd469c87..0000000000 --- a/resources/standards-data/ldml-keyboards/45/cldr_info.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "sha": "dd5776f650183c9172abe2a57313ed41619d41fb", - "description": "release-45", - "date": "Fri, 19 Apr 2024 22:23:26 +0000" -} diff --git a/resources/standards-data/ldml-keyboards/45/readme.md b/resources/standards-data/ldml-keyboards/45/readme.md deleted file mode 100644 index b2265826b7..0000000000 --- a/resources/standards-data/ldml-keyboards/45/readme.md +++ /dev/null @@ -1,3 +0,0 @@ -# ldml-keyboards/techpreview - -See [../readme.md](../readme.md) diff --git a/resources/standards-data/ldml-keyboards/45/3.0/bn.xml b/resources/standards-data/ldml-keyboards/46/3.0/bn.xml similarity index 98% rename from resources/standards-data/ldml-keyboards/45/3.0/bn.xml rename to resources/standards-data/ldml-keyboards/46/3.0/bn.xml index b579e39f38..800f71d7a9 100644 --- a/resources/standards-data/ldml-keyboards/45/3.0/bn.xml +++ b/resources/standards-data/ldml-keyboards/46/3.0/bn.xml @@ -1,4 +1,8 @@ + diff --git a/resources/standards-data/ldml-keyboards/45/3.0/ja-Hira-t-k0-flicks.xml b/resources/standards-data/ldml-keyboards/46/3.0/ja-Hira-t-k0-flicks.xml similarity index 97% rename from resources/standards-data/ldml-keyboards/45/3.0/ja-Hira-t-k0-flicks.xml rename to resources/standards-data/ldml-keyboards/46/3.0/ja-Hira-t-k0-flicks.xml index c10342320a..6448542a26 100644 --- a/resources/standards-data/ldml-keyboards/45/3.0/ja-Hira-t-k0-flicks.xml +++ b/resources/standards-data/ldml-keyboards/46/3.0/ja-Hira-t-k0-flicks.xml @@ -1,4 +1,8 @@ + diff --git a/resources/standards-data/ldml-keyboards/45/3.0/ja-Latn.xml b/resources/standards-data/ldml-keyboards/46/3.0/ja-Latn.xml similarity index 88% rename from resources/standards-data/ldml-keyboards/45/3.0/ja-Latn.xml rename to resources/standards-data/ldml-keyboards/46/3.0/ja-Latn.xml index a2ffda522a..2758d077e8 100644 --- a/resources/standards-data/ldml-keyboards/45/3.0/ja-Latn.xml +++ b/resources/standards-data/ldml-keyboards/46/3.0/ja-Latn.xml @@ -1,4 +1,8 @@ + diff --git a/resources/standards-data/ldml-keyboards/45/3.0/mt-t-k0-47key.xml b/resources/standards-data/ldml-keyboards/46/3.0/mt-t-k0-47key.xml similarity index 95% rename from resources/standards-data/ldml-keyboards/45/3.0/mt-t-k0-47key.xml rename to resources/standards-data/ldml-keyboards/46/3.0/mt-t-k0-47key.xml index 9c7ed0fccc..6a875b37f2 100644 --- a/resources/standards-data/ldml-keyboards/45/3.0/mt-t-k0-47key.xml +++ b/resources/standards-data/ldml-keyboards/46/3.0/mt-t-k0-47key.xml @@ -1,4 +1,8 @@ + diff --git a/resources/standards-data/ldml-keyboards/45/3.0/mt.xml b/resources/standards-data/ldml-keyboards/46/3.0/mt.xml similarity index 95% rename from resources/standards-data/ldml-keyboards/45/3.0/mt.xml rename to resources/standards-data/ldml-keyboards/46/3.0/mt.xml index d552888f7e..b76b08cd9a 100644 --- a/resources/standards-data/ldml-keyboards/45/3.0/mt.xml +++ b/resources/standards-data/ldml-keyboards/46/3.0/mt.xml @@ -4,6 +4,10 @@ Copyright © 1991-2024 Unicode, Inc. SPDX-License-Identifier: Unicode-3.0 CLDR data files are interpreted according to the LDML specification (http://unicode.org/reports/tr35/) --> + diff --git a/resources/standards-data/ldml-keyboards/45/3.0/pcm.xml b/resources/standards-data/ldml-keyboards/46/3.0/pcm.xml similarity index 92% rename from resources/standards-data/ldml-keyboards/45/3.0/pcm.xml rename to resources/standards-data/ldml-keyboards/46/3.0/pcm.xml index 8447330284..cf8002f94d 100644 --- a/resources/standards-data/ldml-keyboards/45/3.0/pcm.xml +++ b/resources/standards-data/ldml-keyboards/46/3.0/pcm.xml @@ -1,4 +1,8 @@ + diff --git a/resources/standards-data/ldml-keyboards/45/3.0/pt-t-k0-abnt2.xml b/resources/standards-data/ldml-keyboards/46/3.0/pt-t-k0-abnt2.xml similarity index 94% rename from resources/standards-data/ldml-keyboards/45/3.0/pt-t-k0-abnt2.xml rename to resources/standards-data/ldml-keyboards/46/3.0/pt-t-k0-abnt2.xml index d612141166..003d1a6af2 100644 --- a/resources/standards-data/ldml-keyboards/45/3.0/pt-t-k0-abnt2.xml +++ b/resources/standards-data/ldml-keyboards/46/3.0/pt-t-k0-abnt2.xml @@ -1,4 +1,8 @@ + diff --git a/resources/standards-data/ldml-keyboards/46/cldr_info.json b/resources/standards-data/ldml-keyboards/46/cldr_info.json new file mode 100644 index 0000000000..8895bf132c --- /dev/null +++ b/resources/standards-data/ldml-keyboards/46/cldr_info.json @@ -0,0 +1,5 @@ +{ + "sha": "e1d37acce5dae468c414172be53b666d58d45be8", + "description": "release-46", + "date": "Thu, 30 Jan 2025 21:17:01 +0000" +} diff --git a/resources/standards-data/ldml-keyboards/45/dtd/ldmlKeyboard3.dtd b/resources/standards-data/ldml-keyboards/46/dtd/ldmlKeyboard3.dtd similarity index 98% rename from resources/standards-data/ldml-keyboards/45/dtd/ldmlKeyboard3.dtd rename to resources/standards-data/ldml-keyboards/46/dtd/ldmlKeyboard3.dtd index 618cf1cafd..58f294a37c 100644 --- a/resources/standards-data/ldml-keyboards/45/dtd/ldmlKeyboard3.dtd +++ b/resources/standards-data/ldml-keyboards/46/dtd/ldmlKeyboard3.dtd @@ -10,7 +10,7 @@ CLDR data files are interpreted according to the LDML specification (http://unic - + @@ -32,7 +32,7 @@ CLDR data files are interpreted according to the LDML specification (http://unic - + diff --git a/resources/standards-data/ldml-keyboards/45/dtd/ldmlKeyboard3.xsd b/resources/standards-data/ldml-keyboards/46/dtd/ldmlKeyboard3.xsd similarity index 98% rename from resources/standards-data/ldml-keyboards/45/dtd/ldmlKeyboard3.xsd rename to resources/standards-data/ldml-keyboards/46/dtd/ldmlKeyboard3.xsd index 6cad096c41..84dc19e492 100644 --- a/resources/standards-data/ldml-keyboards/45/dtd/ldmlKeyboard3.xsd +++ b/resources/standards-data/ldml-keyboards/46/dtd/ldmlKeyboard3.xsd @@ -35,6 +35,7 @@ Note: DTD @-annotations are not currently converted to .xsd. For full CLDR file + @@ -74,10 +75,10 @@ Note: DTD @-annotations are not currently converted to .xsd. For full CLDR file - + - + diff --git a/resources/standards-data/ldml-keyboards/45/dtd/ldmlKeyboardTest3.dtd b/resources/standards-data/ldml-keyboards/46/dtd/ldmlKeyboardTest3.dtd similarity index 100% rename from resources/standards-data/ldml-keyboards/45/dtd/ldmlKeyboardTest3.dtd rename to resources/standards-data/ldml-keyboards/46/dtd/ldmlKeyboardTest3.dtd diff --git a/resources/standards-data/ldml-keyboards/45/dtd/ldmlKeyboardTest3.xsd b/resources/standards-data/ldml-keyboards/46/dtd/ldmlKeyboardTest3.xsd similarity index 100% rename from resources/standards-data/ldml-keyboards/45/dtd/ldmlKeyboardTest3.xsd rename to resources/standards-data/ldml-keyboards/46/dtd/ldmlKeyboardTest3.xsd diff --git a/resources/standards-data/ldml-keyboards/45/import/README.md b/resources/standards-data/ldml-keyboards/46/import/README.md similarity index 100% rename from resources/standards-data/ldml-keyboards/45/import/README.md rename to resources/standards-data/ldml-keyboards/46/import/README.md diff --git a/resources/standards-data/ldml-keyboards/45/import/keys-Latn-implied.xml b/resources/standards-data/ldml-keyboards/46/import/keys-Latn-implied.xml similarity index 100% rename from resources/standards-data/ldml-keyboards/45/import/keys-Latn-implied.xml rename to resources/standards-data/ldml-keyboards/46/import/keys-Latn-implied.xml diff --git a/resources/standards-data/ldml-keyboards/45/import/keys-Zyyy-currency.xml b/resources/standards-data/ldml-keyboards/46/import/keys-Zyyy-currency.xml similarity index 100% rename from resources/standards-data/ldml-keyboards/45/import/keys-Zyyy-currency.xml rename to resources/standards-data/ldml-keyboards/46/import/keys-Zyyy-currency.xml diff --git a/resources/standards-data/ldml-keyboards/45/import/keys-Zyyy-punctuation.xml b/resources/standards-data/ldml-keyboards/46/import/keys-Zyyy-punctuation.xml similarity index 100% rename from resources/standards-data/ldml-keyboards/45/import/keys-Zyyy-punctuation.xml rename to resources/standards-data/ldml-keyboards/46/import/keys-Zyyy-punctuation.xml diff --git a/resources/standards-data/ldml-keyboards/45/import/scanCodes-implied.xml b/resources/standards-data/ldml-keyboards/46/import/scanCodes-implied.xml similarity index 100% rename from resources/standards-data/ldml-keyboards/45/import/scanCodes-implied.xml rename to resources/standards-data/ldml-keyboards/46/import/scanCodes-implied.xml diff --git a/resources/standards-data/ldml-keyboards/45/ldml-keyboard3.schema.json b/resources/standards-data/ldml-keyboards/46/ldml-keyboard3.schema.json similarity index 99% rename from resources/standards-data/ldml-keyboards/45/ldml-keyboard3.schema.json rename to resources/standards-data/ldml-keyboards/46/ldml-keyboard3.schema.json index fe6496c17e..e6660661fd 100644 --- a/resources/standards-data/ldml-keyboards/45/ldml-keyboard3.schema.json +++ b/resources/standards-data/ldml-keyboards/46/ldml-keyboard3.schema.json @@ -178,9 +178,6 @@ "additionalProperties": false, "properties": { "base": { - "enum": [ - "cldr" - ], "type": "string" }, "path": { @@ -601,7 +598,7 @@ "properties": { "cldrVersion": { "enum": [ - "45" + "46" ], "type": "string" }, @@ -618,7 +615,8 @@ "properties": { "conformsTo": { "enum": [ - "45" + "45", + "46" ], "type": "string" }, @@ -691,6 +689,6 @@ "required": [ "keyboard3" ], - "title": "45/dtd/ldmlKeyboard3.xsd", + "title": "46/dtd/ldmlKeyboard3.xsd", "type": "object" } diff --git a/resources/standards-data/ldml-keyboards/45/ldml-keyboardtest3.schema.json b/resources/standards-data/ldml-keyboards/46/ldml-keyboardtest3.schema.json similarity index 99% rename from resources/standards-data/ldml-keyboards/45/ldml-keyboardtest3.schema.json rename to resources/standards-data/ldml-keyboards/46/ldml-keyboardtest3.schema.json index b386de8882..f3c624a2ce 100644 --- a/resources/standards-data/ldml-keyboards/45/ldml-keyboardtest3.schema.json +++ b/resources/standards-data/ldml-keyboards/46/ldml-keyboardtest3.schema.json @@ -220,6 +220,6 @@ "required": [ "keyboardTest3" ], - "title": "45/dtd/ldmlKeyboardTest3.xsd", + "title": "46/dtd/ldmlKeyboardTest3.xsd", "type": "object" } diff --git a/resources/standards-data/ldml-keyboards/45/test/README.md b/resources/standards-data/ldml-keyboards/46/test/README.md similarity index 100% rename from resources/standards-data/ldml-keyboards/45/test/README.md rename to resources/standards-data/ldml-keyboards/46/test/README.md diff --git a/resources/standards-data/ldml-keyboards/45/test/bn-test.xml b/resources/standards-data/ldml-keyboards/46/test/bn-test.xml similarity index 100% rename from resources/standards-data/ldml-keyboards/45/test/bn-test.xml rename to resources/standards-data/ldml-keyboards/46/test/bn-test.xml diff --git a/resources/standards-data/ldml-keyboards/45/test/fr-t-k0-test-test.xml b/resources/standards-data/ldml-keyboards/46/test/fr-t-k0-test-test.xml similarity index 100% rename from resources/standards-data/ldml-keyboards/45/test/fr-t-k0-test-test.xml rename to resources/standards-data/ldml-keyboards/46/test/fr-t-k0-test-test.xml diff --git a/resources/standards-data/ldml-keyboards/45/test/ja-Latn-test.xml b/resources/standards-data/ldml-keyboards/46/test/ja-Latn-test.xml similarity index 100% rename from resources/standards-data/ldml-keyboards/45/test/ja-Latn-test.xml rename to resources/standards-data/ldml-keyboards/46/test/ja-Latn-test.xml diff --git a/resources/standards-data/ldml-keyboards/45/test/pcm-test.xml b/resources/standards-data/ldml-keyboards/46/test/pcm-test.xml similarity index 100% rename from resources/standards-data/ldml-keyboards/45/test/pcm-test.xml rename to resources/standards-data/ldml-keyboards/46/test/pcm-test.xml diff --git a/resources/standards-data/ldml-keyboards/45/test/pt-t-k0-abnt2-test.xml b/resources/standards-data/ldml-keyboards/46/test/pt-t-k0-abnt2-test.xml similarity index 100% rename from resources/standards-data/ldml-keyboards/45/test/pt-t-k0-abnt2-test.xml rename to resources/standards-data/ldml-keyboards/46/test/pt-t-k0-abnt2-test.xml diff --git a/resources/standards-data/ldml-keyboards/fixup-schema.js b/resources/standards-data/ldml-keyboards/fixup-schema.js index 4760d24db5..67fdf25753 100644 --- a/resources/standards-data/ldml-keyboards/fixup-schema.js +++ b/resources/standards-data/ldml-keyboards/fixup-schema.js @@ -85,6 +85,8 @@ if (data.title.endsWith('ldmlKeyboard3.xsd')) { delete item.items.type; item.items['$ref'] = `#/definitions/${newName}`; } + // Workaround CLDR-18138 by not making 'cldr' the default for the base + delete data.definitions?.import?.properties?.base?.enum; } if (data.title.endsWith('ldmlKeyboardTest3.xsd')) { diff --git a/tsconfig.base.json b/tsconfig.base.json index 9369051820..6880709c17 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -26,6 +26,7 @@ "paths": { "@keymanapp/common-types": ["./common/web/types/src/main"], + "@keymanapp/langtags": ["./common/web/langtags/src/main"], "@keymanapp/keyman": ["./web" ], "@keymanapp/models-templates": ["./web/src/engine/predictive-text/templates/"], "@keymanapp/models-wordbreakers": ["./web/src/engine/predictive-text/wordbreakers"], diff --git a/tsconfig.json b/tsconfig.json index 855c39f765..ac2a37037b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,8 @@ { "path": "./common/tools/hextobin/" }, { "path": "./common/web/keyman-version" }, - { "path": "./common/web/types/" }, + { "path": "./common/web/langtags" }, + { "path": "./common/web/types" }, { "path": "./developer/src/common/web/test-helpers/tsconfig.json" }, { "path": "./developer/src/common/web/utils/tsconfig.json" }, @@ -44,9 +45,9 @@ { "path": "./web/src/engine/predictive-text/worker-thread" }, { "path": "./web/src/engine/osk/gesture-processor/tsconfig.json" }, { "path": "./web/src/engine/osk/gesture-processor/src/tools/unit-test-resources/tsconfig.json" }, - { "path": "./web/src/engine/sentry-manager" }, + { "path": "./web/src/engine/sentry-manager/src" }, - { "path": "./web/src/tools/recorder-recorder" }, + { "path": "./web/src/tools/testing/recorder" }, // { "path": "./web/tools/recorder/tsconfig.json" }, // { "path": "./web/tools/sourcemap-root/tsconfig.json" }, ] diff --git a/web/docs/engine/guide/examples/__auto-control.html b/web/docs/engine/guide/examples/__auto-control.html index e7fc2768c6..050e03a9f0 100644 --- a/web/docs/engine/guide/examples/__auto-control.html +++ b/web/docs/engine/guide/examples/__auto-control.html @@ -26,6 +26,6 @@

-Back to Document +Back to Document \ No newline at end of file diff --git a/web/docs/engine/guide/examples/__control-by-control.html b/web/docs/engine/guide/examples/__control-by-control.html index 440a7862d0..93a481a75f 100644 --- a/web/docs/engine/guide/examples/__control-by-control.html +++ b/web/docs/engine/guide/examples/__control-by-control.html @@ -44,6 +44,6 @@ -Back to Document +Back to Document \ No newline at end of file diff --git a/web/docs/engine/guide/examples/__first-example.html b/web/docs/engine/guide/examples/__first-example.html index 1657db9440..feb96f4daa 100644 --- a/web/docs/engine/guide/examples/__first-example.html +++ b/web/docs/engine/guide/examples/__first-example.html @@ -21,6 +21,6 @@

First Example

Click me and type:

-Back to Document +Back to Document \ No newline at end of file diff --git a/web/docs/engine/guide/examples/__full-manual-control.html b/web/docs/engine/guide/examples/__full-manual-control.html index 24b0d004a7..5a70c7183f 100644 --- a/web/docs/engine/guide/examples/__full-manual-control.html +++ b/web/docs/engine/guide/examples/__full-manual-control.html @@ -51,6 +51,6 @@ window.addEventListener( 'load' , SetupDocument );

-Back to Document +Back to Document diff --git a/web/docs/engine/guide/examples/__manual-control.html b/web/docs/engine/guide/examples/__manual-control.html index e9f5f219ca..9ccc74824c 100644 --- a/web/docs/engine/guide/examples/__manual-control.html +++ b/web/docs/engine/guide/examples/__manual-control.html @@ -40,7 +40,7 @@

-Back to Document +Back to Document \ No newline at end of file diff --git a/web/docs/engine/guide/index.md b/web/docs/engine/guide/index.md index 26e2e42b47..4f59a4ebf0 100644 --- a/web/docs/engine/guide/index.md +++ b/web/docs/engine/guide/index.md @@ -1,8 +1,8 @@ --- -title: KeymanWeb 18.0 - The Guide +title: KeymanWeb - The Guide --- -Welcome to the guide for using KeymanWeb 18.0. +Welcome to the guide for using KeymanWeb. - [Getting Started with Keyman Web](get-started) - [User Interface Design](user-interface-design) @@ -10,4 +10,4 @@ Welcome to the guide for using KeymanWeb 18.0. - [Installing Keyboards](adding-keyboards) - [Additional Examples](examples/) -[Return to the main KeymanWeb 18.0 page](../) \ No newline at end of file +[Return to the main KeymanWeb page](../) \ No newline at end of file diff --git a/web/docs/engine/index.md b/web/docs/engine/index.md index 63d14e2fd1..5515112f88 100644 --- a/web/docs/engine/index.md +++ b/web/docs/engine/index.md @@ -1,27 +1,27 @@ --- -title: Keyman Engine for Web 18.0 Developer Help +title: Keyman Engine for Web 19.0 Developer Help --- -Keyman Engine for Web 18.0 is the current version of KeymanWeb and +Keyman Engine for Web 19.0 is the current version of KeymanWeb and supports touch devices with custom touch-layouts as well as desktop computer browsers. [Download](https://keyman.com/developer/keymanweb/) -: Downloading Keyman Engine for Web 18.0 +: Downloading Keyman Engine for Web [Guide](guide/) -: Keyman Engine for Web 18.0 Guide +: Keyman Engine for Web Guide [Reference](reference) -: Keyman Engine for Web 18.0 Developer Reference +: Keyman Engine for Web Developer Reference [What's New](whats-new) -: What's new in Keyman Engine for Web 18.0 +: What's new in Keyman Engine for Web 19.0 diff --git a/web/docs/engine/reference/core/version.md b/web/docs/engine/reference/core/version.md index 7df0978ac8..40b9db4529 100644 --- a/web/docs/engine/reference/core/version.md +++ b/web/docs/engine/reference/core/version.md @@ -22,7 +22,7 @@ Read only ### Return Value -`'18.0'` (for KeymanWeb 18.0) +`'19.0'` (for KeymanWeb 19.0) ## Description diff --git a/web/docs/engine/reference/index.md b/web/docs/engine/reference/index.md index 74d83c3931..f6e5a3a8de 100644 --- a/web/docs/engine/reference/index.md +++ b/web/docs/engine/reference/index.md @@ -1,5 +1,5 @@ --- -title: KeymanWeb 18.0 Reference +title: KeymanWeb Reference --- [KeymanWeb Overview](overview) diff --git a/web/docs/engine/whats-new.md b/web/docs/engine/whats-new.md index 49fa337fe0..e220fb9c13 100644 --- a/web/docs/engine/whats-new.md +++ b/web/docs/engine/whats-new.md @@ -1,3 +1,3 @@ --- -title: What's New in KeymanWeb 18.0 +title: What's New in KeymanWeb 19.0 --- diff --git a/web/src/engine/osk/gesture-processor/docs/host-page-viz.png b/web/docs/internal/gestures/host-page-viz.png similarity index 100% rename from web/src/engine/osk/gesture-processor/docs/host-page-viz.png rename to web/docs/internal/gestures/host-page-viz.png diff --git a/web/src/engine/osk/gesture-processor/docs/recognizer-configuration.md b/web/docs/internal/gestures/recognizer-configuration.md similarity index 100% rename from web/src/engine/osk/gesture-processor/docs/recognizer-configuration.md rename to web/docs/internal/gestures/recognizer-configuration.md diff --git a/web/src/app/webview/src/configuration.ts b/web/src/app/webview/src/configuration.ts index d5b7079e6d..507442e6bb 100644 --- a/web/src/app/webview/src/configuration.ts +++ b/web/src/app/webview/src/configuration.ts @@ -47,6 +47,10 @@ export class WebviewConfiguration extends EngineConfiguration { return this._embeddingApp; } + set embeddingApp(name: string) { + this._embeddingApp = name; + } + get oninserttext() { return this._oninserttext; } diff --git a/web/src/engine/predictive-text/worker-thread/src/main/correction/classical-calculation.ts b/web/src/engine/predictive-text/worker-thread/src/main/correction/classical-calculation.ts index 296476b098..33b8a64783 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/correction/classical-calculation.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/correction/classical-calculation.ts @@ -1,6 +1,6 @@ import { SENTINEL_CODE_UNIT } from '@keymanapp/models-templates'; -type EditOperation = 'insert' | 'delete' | 'match' | 'substitute' | 'transpose-start' | 'transpose-end' | 'transpose-insert' | 'transpose-delete'; +export type EditOperation = 'insert' | 'delete' | 'match' | 'substitute' | 'transpose-start' | 'transpose-end' | 'transpose-insert' | 'transpose-delete'; /** * Represents the lowest-level unit for comparison during edit-distance calculations. @@ -206,12 +206,15 @@ export class ClassicalDistanceCalculation= 2 && editPath[editLength - 2] == 'substitute' && editPath[editLength - 1] == 'match') { + return editPath.lastIndexOf('match', editLength - 2); + } else { + return editPath.lastIndexOf('match'); + } +} + export class TrackedContextSuggestion { suggestion: Suggestion; tokenWidth: number; @@ -42,9 +54,20 @@ export class TrackedContextToken { isWhitespace?: boolean; transformDistributions: Distribution[] = []; - replacements: TrackedContextSuggestion[]; + replacements: TrackedContextSuggestion[] = []; activeReplacementId: number = -1; + constructor(); + constructor(instance: TrackedContextToken); + constructor(instance?: TrackedContextToken) { + if(instance) { + Object.assign(this, instance); + // We don't alter the values in replacements, but we do wish to prevent aliasing + // of the array containing them. + this.replacements = instance.replacements.slice(); + } + } + get currentText(): string { if(this.replacementText === undefined || this.replacementText === null) { return this.raw; @@ -60,8 +83,9 @@ export class TrackedContextToken { }); } - revert() { - delete this.activeReplacementId; + clearReplacements() { + this.activeReplacementId = -1; + this.replacements = [] } /** @@ -85,7 +109,8 @@ export class TrackedContextToken { }); this.raw = tokenText; - this.transformDistributions = backspacedTokenContext; + this.transformDistributions = backspacedTokenContext; + this.clearReplacements(); } update(transformDistribution: Distribution, tokenText?: USVString) { @@ -98,6 +123,7 @@ export class TrackedContextToken { // Replace old token's raw-text with new token's raw-text. this.raw = tokenText; + this.clearReplacements(); } } @@ -127,15 +153,9 @@ export class TrackedContextState { // Be sure to deep-copy the tokens! Pointer-aliasing is bad here. this.tokens = source.tokens.map(function(token) { let copy = new TrackedContextToken(); - copy.raw = token.raw; - copy.replacements = [].concat(token.replacements); - copy.activeReplacementId = token.activeReplacementId; - copy.transformDistributions = [].concat(token.transformDistributions); - - if(token.replacementText) { - copy.replacementText = token.replacementText; - } - + Object.assign(copy, token); + copy.replacements = copy.replacements.slice(); + copy.transformDistributions = copy.transformDistributions.slice(); return copy; }); @@ -320,6 +340,9 @@ interface ContextMatchResult { * (Refer to #12494 for an example case.) */ preservationTransform?: Transform; + + headTokensRemoved: number; + tailTokensAdded: number; } export class ContextTracker extends CircularArray { @@ -342,25 +365,8 @@ export class ContextTracker extends CircularArray { ); let editPath = mapping.editPath(); - - // When the context has but two tokens, the path algorithm tends to invert - // 'insert' and 'substitute' from our preferred ordering for them. - // Logically, either order makes sense... but logic for other cases is - // far simpler if we have 'substitute' before 'insert'. - if(editPath.length == 2 && editPath[0] == 'insert' && editPath[1] == 'substitute') { - editPath[0] = 'substitute'; - editPath[1] = 'insert'; - } - const firstMatch = editPath.indexOf('match'); - let lastMatch = editPath.lastIndexOf('match'); - - // Special handling: appending whitespace to whitespace with the default wordbreaker. - // The default wordbreaker currently adds an empty token after whitespace; this would - // show up with 'substitute', 'match' at the end of the edit path. - if(editPath.length >= 2 && editPath[editPath.length - 2] == 'substitute' && editPath[editPath.length - 1] == 'match') { - lastMatch = editPath.lastIndexOf('match', editPath.length - 2); - } + const lastMatch = getEditPathLastMatch(editPath); // Assertion: for a long context, the bulk of the edit path should be a // continuous block of 'match' entries. If there's anything else in @@ -376,7 +382,7 @@ export class ContextTracker extends CircularArray { // If we have a perfect match with a pre-existing context, no mutations have // happened; just re-use the old context state. if(firstMatch == 0 && lastMatch == editPath.length - 1) { - return { state: matchState, baseState: matchState }; + return { state: matchState, baseState: matchState, headTokensRemoved: 0, tailTokensAdded: 0 }; } // If mutations HAVE happened, we have work to do. @@ -418,6 +424,7 @@ export class ContextTracker extends CircularArray { // does not land as part of the final token in the resulting context. This // component should be preserved by any suggestions that get applied. let preservationTransform: Transform; + let pushedTokenCount = 0; // Now to update the end of the context window. for(let i = lastMatch+1; i < editPath.length; i++) { @@ -449,15 +456,14 @@ export class ContextTracker extends CircularArray { // transform as part of the input when doing correction-search. let primaryInput = hasDistribution ? tokenDistribution[0]?.sample : null; - // If the incoming token has text but we have no transform to match it with, - // abort the matching attempt. We can't match this case well yet. - // These cases are generally infrequent enough to be low-ROI, not worth the - // investment to optimize further. (Doing so isn't the simplest.) - // - // This may happen if tokenization for pre-existing text changes due to incoming - // input - refer to #12494 for an example case. - if(!primaryInput && incomingToken.text != '') { - return null; + // If the incoming token has text but we have no transform (or 'insert') to match + // it with, abort the matching attempt. We can't match this case well yet. + if(editPath[i] != 'delete') { + if(!incomingToken) { + return null; + } else if(!(primaryInput || editPath[i] == 'insert' ) && incomingToken?.text != '') { + return null; + } } if(primaryInput && primaryInput.insert == "" && primaryInput.deleteLeft == 0 && !primaryInput.deleteRight) { @@ -471,7 +477,7 @@ export class ContextTracker extends CircularArray { // Note: will need a either a different approach or more specialized // handling if/when supporting phrase-level (multi-token) suggestions. if(!isLastToken) { - preservationTransform = preservationTransform ? buildMergedTransform(preservationTransform, primaryInput) : primaryInput; + preservationTransform = preservationTransform && primaryInput ? buildMergedTransform(preservationTransform, primaryInput) : (primaryInput ?? preservationTransform); } const isBackspace = primaryInput && TransformUtils.isBackspace(primaryInput); @@ -481,8 +487,9 @@ export class ContextTracker extends CircularArray { state = new TrackedContextState(state); } + const sourceToken = matchState.tokens[i]; + state.tokens[i - poppedTokenCount] = sourceToken; const token = state.tokens[i - poppedTokenCount]; - const matchToken = matchState.tokens[i]; // TODO: I'm beginning to believe that searchSpace should (eventually) be tracked // on the tokens, rather than on the overall 'state'. @@ -520,7 +527,7 @@ export class ContextTracker extends CircularArray { // transforms. That said, it's also very low impact. Best as I can // see, this is only really used for debugging info? if(state != matchState && !isLastToken) { - matchToken.replacementText = incomingToken.text; + token.replacementText = incomingToken.text; } break; @@ -558,11 +565,17 @@ export class ContextTracker extends CircularArray { // we know to properly relocate the `f` and `l` transforms? if(primaryInput) { pushedToken.transformDistributions = tokenDistribution ? [tokenDistribution] : []; + } else if(incomingToken.text) { + // We have no transform data to match against an inserted token with text; abort! + // Refer to #12494 for an example case; we currently can't map previously-committed + // input transforms to a newly split-off token. + return null; } pushedToken.isWhitespace = incomingToken.isWhitespace; // Auto-replaces the search space to correspond with the new token. state.pushTail(pushedToken); + pushedTokenCount++; break; case 'match': // The default (Unicode) wordbreaker returns an empty token after whitespace blocks. @@ -573,21 +586,47 @@ export class ContextTracker extends CircularArray { continue; } // else 'fallthrough' / return null - default: - // No 'delete' should exist on the trailing edge of context when the - // context window slides. While it can happen due to keystrokes with - // `deleteLeft`, we keep a cache of recent contexts - an older one will - // likely match sufficiently. - // - may see 'delete' followed by 'substitute' in such cases. + case 'delete': + // While we do keep a cache of recent contexts, logic constraints for handling + // multitaps makes it tricky to reliably use in all situations. + // It's best to handle `delete` cases directly for this reason. + for(let j = i + 1; j < editPath.length; j++) { + // If something _other_ than delete follows a 'delete' on the edit path, + // we probably have a context mismatch. + // + // It's possible to construct cases where this isn't true, but it's likely not + // worth trying to handle such rare cases. + if(editPath[j] != 'delete') { + return null; + } + } + + // If ALL that remains are deletes, we're good to go. // - // No 'transform' edits should exist within this section, either. + // This may not be the token at the index, but since all that remains are deletes, + // we'll have deleted the correct total number from the end once all iterations + // are done. + if(state == matchState) { + state = new TrackedContextState(state); + } + + state.tokens.pop(); + break; + default: + // No 'transform' edits should exist within this section. return null; } priorEdit = editPath[i]; } - return { state, baseState: matchState, preservationTransform }; + return { + state, + baseState: matchState, + preservationTransform, + headTokensRemoved: poppedTokenCount, + tailTokensAdded: pushedTokenCount + }; } private static modelContextState( @@ -642,11 +681,13 @@ export class ContextTracker extends CircularArray { * @param model * @param context * @param transformDistribution + * @param preserveMatchState Set to `true` to avoid any edits to the matched context state when they might normally occur. */ analyzeState( model: LexicalModel, context: Context, - transformDistribution?: Distribution + transformDistribution?: Distribution, + preserveMatchState?: boolean ): ContextMatchResult { if(!model.traverseFromRoot) { // Assumption: LexicalModel provides a valid traverseFromRoot function. (Is technically optional) @@ -727,7 +768,7 @@ export class ContextTracker extends CircularArray { let state = ContextTracker.modelContextState(tokenizedContext.left, model); state.taggedContext = context; this.enqueue(state); - return { state, baseState: null }; + return { state, baseState: null, headTokensRemoved: 0, tailTokensAdded: 0 }; } clearCache() { diff --git a/web/src/engine/predictive-text/worker-thread/src/main/correction/index.ts b/web/src/engine/predictive-text/worker-thread/src/main/correction/index.ts index 33d9f70f54..6da63b7ced 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/correction/index.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/correction/index.ts @@ -1,4 +1,5 @@ export * from './classical-calculation.js'; export * from './context-tracker.js'; export * from './distance-modeler.js'; -export * from './execution-timer.js'; \ No newline at end of file +export * from './execution-timer.js'; +export * from './transform-tokenization.js'; \ No newline at end of file diff --git a/web/src/engine/predictive-text/worker-thread/src/main/model-compositor.ts b/web/src/engine/predictive-text/worker-thread/src/main/model-compositor.ts index f8d94eef22..d0b4658b13 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/model-compositor.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/model-compositor.ts @@ -318,7 +318,7 @@ export class ModelCompositor { this.contextTracker.popNewest(); } - this.contextTracker.newest.tail.revert(); + this.contextTracker.newest.tail.activeReplacementId = -1; // Will need to be modified a bit if/when phrase-level suggestions are implemented. // Those will be tracked on the first token of the phrase, which won't be the tail diff --git a/web/src/engine/predictive-text/worker-thread/src/tests/mocha/cases/edit-distance/classical-calculation.js b/web/src/engine/predictive-text/worker-thread/src/tests/mocha/cases/edit-distance/classical-calculation.js index 1833e9f4d4..07a86b2bbb 100644 --- a/web/src/engine/predictive-text/worker-thread/src/tests/mocha/cases/edit-distance/classical-calculation.js +++ b/web/src/engine/predictive-text/worker-thread/src/tests/mocha/cases/edit-distance/classical-calculation.js @@ -513,6 +513,16 @@ describe('Classical Damerau-Levenshtein edit-distance calculation', function() { assert.deepEqual(buffer.editPath(), editSequence); }); + it("'then' -> 'their'", function() { + let buffer = compute("then", "their"); + + let editSequence = [ + op.m, op.m, op.m, op.s, op.i + ]; + + assert.deepEqual(buffer.editPath(), editSequence); + }); + // Two transpositions: abc -> ca, ig <- ghi. Also, one deletion: 'd'. it("'abczdefig' -> 'cazefghi'", function() { let buffer = compute("abczdefig", "cazefghi", "InputThenMatch", 2); diff --git a/web/src/engine/predictive-text/worker-thread/src/tests/mocha/cases/edit-distance/context-tracker.js b/web/src/engine/predictive-text/worker-thread/src/tests/mocha/cases/edit-distance/context-tracker.js index c663072f6d..65298a2dd3 100644 --- a/web/src/engine/predictive-text/worker-thread/src/tests/mocha/cases/edit-distance/context-tracker.js +++ b/web/src/engine/predictive-text/worker-thread/src/tests/mocha/cases/edit-distance/context-tracker.js @@ -1,14 +1,18 @@ import { assert } from 'chai'; import { ContextTracker } from '#./correction/context-tracker.js'; +import { tokenizeTransformDistribution } from '#./correction/transform-tokenization.js'; import ModelCompositor from '#./model-compositor.js'; import * as models from '#./models/index.js'; +import { determineModelTokenizer } from '#./model-helpers.js'; import { default as defaultBreaker } from '@keymanapp/models-wordbreakers'; import { deepCopy } from '@keymanapp/web-utils'; import { jsonFixture } from '@keymanapp/common-test-resources/model-helpers.mjs'; +const tokenizer = determineModelTokenizer(new models.DummyModel({wordbreaker: defaultBreaker})); + describe('ContextTracker', function() { function toWrapperDistribution(transforms) { transforms = Array.isArray(transforms) ? transforms : [transforms]; @@ -35,6 +39,8 @@ describe('ContextTracker', function() { let newContextMatch = ContextTracker.attemptMatchContext(newContext.left, baseContextMatch, toWrapperDistribution(transform)); assert.isNotNull(newContextMatch?.state); assert.deepEqual(newContextMatch.state.tokens.map(token => token.raw), rawTokens); + assert.equal(newContextMatch.headTokensRemoved, 1); + assert.equal(newContextMatch.tailTokensAdded, 0); }); it("properly matches and aligns when lead token + following whitespace are removed", function() { @@ -53,6 +59,8 @@ describe('ContextTracker', function() { let newContextMatch = ContextTracker.attemptMatchContext(newContext.left, baseContextMatch, toWrapperDistribution(transform)); assert.isNotNull(newContextMatch?.state); assert.deepEqual(newContextMatch.state.tokens.map(token => token.raw), rawTokens); + assert.equal(newContextMatch.headTokensRemoved, 2); + assert.equal(newContextMatch.tailTokensAdded, 0); }); it("properly matches and aligns when final token is edited", function() { @@ -72,6 +80,8 @@ describe('ContextTracker', function() { let newContextMatch = ContextTracker.attemptMatchContext(newContext.left, baseContextMatch, toWrapperDistribution(transform)); assert.isNotNull(newContextMatch?.state); assert.deepEqual(newContextMatch.state.tokens.map(token => token.raw), rawTokens); + assert.equal(newContextMatch.headTokensRemoved, 0); + assert.equal(newContextMatch.tailTokensAdded, 0); }); // Needs improved context-state management (due to 2x tokens) @@ -99,6 +109,31 @@ describe('ContextTracker', function() { let state = newContextMatch?.state; assert.isNotEmpty(state.tokens[state.tokens.length - 2].transformDistributions); assert.isEmpty(state.tokens[state.tokens.length - 1].transformDistributions); + assert.equal(newContextMatch.headTokensRemoved, 0); + assert.equal(newContextMatch.tailTokensAdded, 2); + }); + + it("properly matches and aligns when a 'wordbreak' is removed via backspace", function() { + let existingContext = models.tokenize(defaultBreaker, { + left: "an apple a day keeps the doctor " + }); + let transform = { + insert: '', + deleteLeft: 1 + } + let newContext = models.tokenize(defaultBreaker, { + left: "an apple a day keeps the doctor" + }); + let rawTokens = ["an", " ", "apple", " ", "a", " ", "day", " ", "keeps", " ", "the", " ", "doctor"]; + + let baseContextMatch = ContextTracker.modelContextState(existingContext.left); + let newContextMatch = ContextTracker.attemptMatchContext(newContext.left, baseContextMatch, toWrapperDistribution(transform)); + assert.isOk(newContextMatch?.state); + assert.deepEqual(newContextMatch?.state.tokens.map(token => token.raw), rawTokens); + + // The 'wordbreak' transform + assert.equal(newContextMatch.headTokensRemoved, 0); + assert.equal(newContextMatch.tailTokensAdded, 0); }); it("properly matches and aligns when an implied 'wordbreak' occurs (as when following \"'\")", function() { @@ -124,6 +159,9 @@ describe('ContextTracker', function() { let state = newContextMatch.state; assert.isNotEmpty(state.tokens[state.tokens.length - 2].transformDistributions); assert.isNotEmpty(state.tokens[state.tokens.length - 1].transformDistributions); + + assert.equal(newContextMatch.headTokensRemoved, 0); + assert.equal(newContextMatch.tailTokensAdded, 1); }) // Needs improved context-state management (due to 2x tokens) @@ -151,6 +189,75 @@ describe('ContextTracker', function() { let state = newContextMatch.state; assert.isNotEmpty(state.tokens[state.tokens.length - 2].transformDistributions); assert.isEmpty(state.tokens[state.tokens.length - 1].transformDistributions); + + assert.equal(newContextMatch.headTokensRemoved, 2); + assert.equal(newContextMatch.tailTokensAdded, 2); + }); + + it("properly matches and aligns when initial token is modified AND a 'wordbreak' is added'", function() { + let existingContext = models.tokenize(defaultBreaker, { + left: "an" + }); + let transform = { + insert: 'd ', + deleteLeft: 0 + } + let newContext = models.tokenize(defaultBreaker, { + left: "and " + }); + let rawTokens = ["and", " ", ""]; + + let baseContextMatch = ContextTracker.modelContextState(existingContext.left); + let newContextMatch = ContextTracker.attemptMatchContext( + newContext.left, + baseContextMatch, + tokenizeTransformDistribution(tokenizer, {left: "an"}, [{sample: transform, p: 1}]) + ); + assert.isNotNull(newContextMatch?.state); + assert.deepEqual(newContextMatch.state.tokens.map(token => token.raw), rawTokens); + // We want to preserve all text preceding the new token when applying a suggestion. + assert.deepEqual(newContextMatch.preservationTransform, { insert: 'd ', deleteLeft: 0, deleteRight: 0}); + + // The 'wordbreak' transform + let state = newContextMatch.state; + assert.isNotEmpty(state.tokens[state.tokens.length - 2].transformDistributions); + assert.isEmpty(state.tokens[state.tokens.length - 1].transformDistributions); + + assert.equal(newContextMatch.headTokensRemoved, 0); + assert.equal(newContextMatch.tailTokensAdded, 2); + }); + + it("properly matches and aligns when tail token is modified AND a 'wordbreak' is added'", function() { + let existingContext = models.tokenize(defaultBreaker, { + left: "apple a day keeps the doc" + }); + let transform = { + insert: 'tor ', + deleteLeft: 0 + } + let newContext = models.tokenize(defaultBreaker, { + left: "apple a day keeps the doctor " + }); + let rawTokens = ["apple", " ", "a", " ", "day", " ", "keeps", " ", "the", " ", "doctor", " ", ""]; + + let baseContextMatch = ContextTracker.modelContextState(existingContext.left); + let newContextMatch = ContextTracker.attemptMatchContext( + newContext.left, + baseContextMatch, + tokenizeTransformDistribution(tokenizer, {left: "apple a day keeps the doc"}, [{sample: transform, p: 1}]) + ); + assert.isNotNull(newContextMatch?.state); + assert.deepEqual(newContextMatch.state.tokens.map(token => token.raw), rawTokens); + // We want to preserve all text preceding the new token when applying a suggestion. + assert.deepEqual(newContextMatch.preservationTransform, { insert: 'tor ', deleteLeft: 0, deleteRight: 0 }); + + // The 'wordbreak' transform + let state = newContextMatch.state; + assert.isNotEmpty(state.tokens[state.tokens.length - 2].transformDistributions); + assert.isEmpty(state.tokens[state.tokens.length - 1].transformDistributions); + + assert.equal(newContextMatch.headTokensRemoved, 0); + assert.equal(newContextMatch.tailTokensAdded, 2); }); it('rejects hard-to-handle case: tail token is split into three rather than two', function() { @@ -172,7 +279,11 @@ describe('ContextTracker', function() { insert: '\"', deleteLeft: 0 } - let problemContextMatch = ContextTracker.attemptMatchContext(newContext.left, baseContextMatch, toWrapperDistribution(transform)); + let problemContextMatch = ContextTracker.attemptMatchContext( + newContext.left, + baseContextMatch, + tokenizeTransformDistribution(tokenizer, {left: "text'"}, [{sample: transform, p: 1}]) + ); assert.isNull(problemContextMatch); }); }); diff --git a/windows/docs/engine/api/index.md b/windows/docs/engine/api/index.md index 9ff2784f57..f95332c45a 100644 --- a/windows/docs/engine/api/index.md +++ b/windows/docs/engine/api/index.md @@ -1,14 +1,14 @@ --- -title: Keyman Engine for Windows 14.0 API +title: Keyman Engine for Windows API --- ## Introduction -The Keyman Engine for Windows 18.0 API is implemented in COM. It can be +The Keyman Engine for Windows API is implemented in COM. It can be instantiated with `CreateObject("keymanapi.Keyman")`. -**Note:** This documentation applies to Keyman Engine for Windows -versions 14.0 and up. +> [!Note] +> This documentation applies to Keyman Engine for Windows versions 14.0 and up. ## Interface Hierarchy diff --git a/windows/docs/engine/index.md b/windows/docs/engine/index.md index acdd77d877..0687433412 100644 --- a/windows/docs/engine/index.md +++ b/windows/docs/engine/index.md @@ -1,8 +1,8 @@ --- -title: Keyman Engine for Windows 18.0 +title: Keyman Engine for Windows --- -Keyman Engine for Windows 18.0 gives you the tools to build a customised +Keyman Engine for Windows gives you the tools to build a customised desktop keyboarding product for Windows. **Note:** This documentation applies to Keyman Engine for Windows diff --git a/windows/docs/help/about/credits.md b/windows/docs/help/about/credits.md index d0f16aca23..bd7698ecde 100644 --- a/windows/docs/help/about/credits.md +++ b/windows/docs/help/about/credits.md @@ -3,7 +3,7 @@ title: Credits --- These help files were written by Marc Durdin, Peirce Baehr and Jeremy -Kirkham; updated by Makara Sok, Joshua Horton, Darcy Wong, +Kirkham; updated by MengHeng Hav, Makara Sok, Joshua Horton, Darcy Wong, Eberhard Beilharz. Keyman is a registered trademark of Summer Institute of Linguistics Inc in diff --git a/windows/docs/help/about/index.md b/windows/docs/help/about/index.md index 4c0748acb2..856f4fde97 100644 --- a/windows/docs/help/about/index.md +++ b/windows/docs/help/about/index.md @@ -1,5 +1,5 @@ --- -title: About Keyman +title: About Keyman for Windows --- * [Welcome to Keyman](welcome) diff --git a/windows/docs/help/about/license.md b/windows/docs/help/about/license.md index b3d2e88414..2e3b74c13a 100644 --- a/windows/docs/help/about/license.md +++ b/windows/docs/help/about/license.md @@ -4,7 +4,7 @@ title: End User Licence and Services Agreement The MIT License -Copyright (c) 2017-2022 SIL International +Copyright (c) 2017-2025 SIL Global Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/windows/docs/help/about/whatsnew.md b/windows/docs/help/about/whatsnew.md index 4f11c82ec2..ee917f3870 100644 --- a/windows/docs/help/about/whatsnew.md +++ b/windows/docs/help/about/whatsnew.md @@ -1,8 +1,8 @@ --- -title: What's New in Keyman 18.0 for Windows +title: What's New in Keyman 19.0 for Windows --- -Here are some of the new features we have added to Keyman 18.0 for Windows: +Here are some of the new features we have added to Keyman 19.0 for Windows: ## Related Topics diff --git a/windows/docs/help/advanced/index.md b/windows/docs/help/advanced/index.md index 371f85521d..5be5c488fb 100644 --- a/windows/docs/help/advanced/index.md +++ b/windows/docs/help/advanced/index.md @@ -1,5 +1,5 @@ --- -title: Advanced Help +title: Keyman for Windows Advanced Help --- * [proxy_config](proxy_config) diff --git a/windows/docs/help/basic/index.md b/windows/docs/help/basic/index.md index 0267f1ccc3..42bda8ba6f 100644 --- a/windows/docs/help/basic/index.md +++ b/windows/docs/help/basic/index.md @@ -1,5 +1,5 @@ --- -title: Basic Help +title: Keyman for Windows Basic Help --- * [New Ways to Use Keyman](new-ways-to-use-keyman) diff --git a/windows/docs/help/basic/uninstall-keyboard.md b/windows/docs/help/basic/uninstall-keyboard.md index b36ff14240..b06abc1802 100644 --- a/windows/docs/help/basic/uninstall-keyboard.md +++ b/windows/docs/help/basic/uninstall-keyboard.md @@ -1,5 +1,5 @@ --- -title: Keyboard Task - Uninstall a Keyboard +title: Keyboard Task - Uninstall a Keyman Keyboard on Windows --- Uninstalling a Keyman keyboard removes it from Keyman. If you want to diff --git a/windows/docs/help/basic/uninstall.md b/windows/docs/help/basic/uninstall.md index 6d98645613..cdd963d21c 100644 --- a/windows/docs/help/basic/uninstall.md +++ b/windows/docs/help/basic/uninstall.md @@ -1,5 +1,5 @@ --- -title: Software Task - Uninstall Keyman +title: Software Task - Uninstall Keyman for Windows --- ## Uninstall Keyman from Windows 10 diff --git a/windows/docs/help/basic/update.md b/windows/docs/help/basic/update.md index 37e4e3237f..03955bd518 100644 --- a/windows/docs/help/basic/update.md +++ b/windows/docs/help/basic/update.md @@ -1,5 +1,5 @@ --- -title: Software Task - Update Keyman +title: Software Task - Update Keyman for Windows --- ## Check for Updates Automatically diff --git a/windows/docs/help/index.md b/windows/docs/help/index.md index c7cccc7695..231ed5fc7d 100644 --- a/windows/docs/help/index.md +++ b/windows/docs/help/index.md @@ -1,5 +1,5 @@ --- -title: Keyman for Windows 18.0 Help +title: Keyman for Windows 19.0 Help --- Need help using Keyman for Windows? You'll find everything you need here, including product documentation, diff --git a/windows/docs/help/start/index.md b/windows/docs/help/start/index.md index e3927980d1..4ac74eb998 100644 --- a/windows/docs/help/start/index.md +++ b/windows/docs/help/start/index.md @@ -1,5 +1,5 @@ --- -title: Getting Started +title: Getting Started on Windows --- * [Getting Started with Keyman](tutorial) diff --git a/windows/docs/help/start/tutorial.md b/windows/docs/help/start/tutorial.md index 7eaf4bd00a..a01678bd02 100644 --- a/windows/docs/help/start/tutorial.md +++ b/windows/docs/help/start/tutorial.md @@ -1,5 +1,5 @@ --- -title: Getting Started with Keyman +title: Getting Started with Keyman for Windows --- Follow these steps for a basic introduction to Keyman: diff --git a/windows/src/desktop/kmshell/locale/am-ET/strings.xml b/windows/src/desktop/kmshell/locale/am-ET/strings.xml index 20e21edee6..cb11dbc397 100644 --- a/windows/src/desktop/kmshell/locale/am-ET/strings.xml +++ b/windows/src/desktop/kmshell/locale/am-ET/strings.xml @@ -321,10 +321,7 @@ የእንኳን ደህና መጡ ምስልን አሳይ - - - - የተሻሻሉ መረጃዎችን ለማግኘት በየሳምንቱ keyman.com ይመልከቱ + diff --git a/windows/src/desktop/kmshell/locale/az-AZ/strings.xml b/windows/src/desktop/kmshell/locale/az-AZ/strings.xml index 26898fe496..68a6aa00b9 100644 --- a/windows/src/desktop/kmshell/locale/az-AZ/strings.xml +++ b/windows/src/desktop/kmshell/locale/az-AZ/strings.xml @@ -303,10 +303,6 @@ Xoş gəldiniz ekranını göstərin - - Yeniləmələr üçün həftəlik keyman.com-u avtomatik olaraq yoxlamaq - - Keyman başlayanda ziddiyyətli tətbiqetmələr üçün test et diff --git a/windows/src/desktop/kmshell/locale/bwr-NG/strings.xml b/windows/src/desktop/kmshell/locale/bwr-NG/strings.xml index 5b45800b3b..76aaaac053 100644 --- a/windows/src/desktop/kmshell/locale/bwr-NG/strings.xml +++ b/windows/src/desktop/kmshell/locale/bwr-NG/strings.xml @@ -303,10 +303,6 @@ Cangtǝ lale ata screen ni - - Pakǝ ka kǝrni akwa keyman.com kǝlara sati kamnyar wutǝ labar na bǝlin - - Ngkǝrha kamnyar sǝndǝr applicationyeri aɗi kǝɓa wa sakatǝ Keyman baɗita diff --git a/windows/src/desktop/kmshell/locale/ckl-NG/strings.xml b/windows/src/desktop/kmshell/locale/ckl-NG/strings.xml index c854c08137..454936a6ee 100644 --- a/windows/src/desktop/kmshell/locale/ckl-NG/strings.xml +++ b/windows/src/desktop/kmshell/locale/ckl-NG/strings.xml @@ -323,10 +323,6 @@ Tsanti welcome screen - - Chakǝnta keyman.com kǝlara sati akuya ka gǝ wulba updatyere - - Nkǝrti ka ngaba suyere kura kal kal akwa applicationyere ma Keyman badicini kǝthlǝr diff --git a/windows/src/desktop/kmshell/locale/cs-CZ/strings.xml b/windows/src/desktop/kmshell/locale/cs-CZ/strings.xml index ef99e2a77f..e1967680c1 100644 --- a/windows/src/desktop/kmshell/locale/cs-CZ/strings.xml +++ b/windows/src/desktop/kmshell/locale/cs-CZ/strings.xml @@ -327,10 +327,6 @@ Zobrazit úvodní obrazovku - - Automaticky kontrolovat aktualizace na keyman.com každý týden - - Test pro protichůdné aplikace při spuštění klíče diff --git a/windows/src/desktop/kmshell/locale/de/strings.xml b/windows/src/desktop/kmshell/locale/de/strings.xml index cf5713f09a..aa4f02dd81 100644 --- a/windows/src/desktop/kmshell/locale/de/strings.xml +++ b/windows/src/desktop/kmshell/locale/de/strings.xml @@ -323,10 +323,6 @@ Willkommensbildschirm anzeigen - - keyman.com wöchentlich auf Updates überprüfen - - Beim Start von Keyman nach inkompatiblen Anwendungen suchen diff --git a/windows/src/desktop/kmshell/locale/el-polyton/strings.xml b/windows/src/desktop/kmshell/locale/el-polyton/strings.xml index 04263b7013..a11a89da39 100644 --- a/windows/src/desktop/kmshell/locale/el-polyton/strings.xml +++ b/windows/src/desktop/kmshell/locale/el-polyton/strings.xml @@ -323,10 +323,6 @@ Προβάλετε τὴν ὀθόνης καλωσορίσματος - - Νὰ ἐλέγχετε σὲ ἑβδομαδιαία βάση τὸ keyman.com γιὰ ἐνημερώσεις - - Κατὰ τὴν ἔναρξη τοῦ Keyman, νὰ ἐξετάζετε ἂν ὑπάρχουν μὴ συμβατὲς ἐφαρμογές diff --git a/windows/src/desktop/kmshell/locale/es-419/strings.xml b/windows/src/desktop/kmshell/locale/es-419/strings.xml index 24ef2431e7..116683e5bf 100644 --- a/windows/src/desktop/kmshell/locale/es-419/strings.xml +++ b/windows/src/desktop/kmshell/locale/es-419/strings.xml @@ -327,12 +327,8 @@ Mostrar pantalla de bienvenida - - Buscar actualizaciones y descargar automáticamente - - - - Buscar actualizaciones automáticamente en keyman.com cada semana + + Buscar actualizaciones y descargar automáticamente diff --git a/windows/src/desktop/kmshell/locale/es-ES/strings.xml b/windows/src/desktop/kmshell/locale/es-ES/strings.xml index af321bec5c..ef40f75150 100644 --- a/windows/src/desktop/kmshell/locale/es-ES/strings.xml +++ b/windows/src/desktop/kmshell/locale/es-ES/strings.xml @@ -327,10 +327,6 @@ Mostrar pantalla de bienvenida - - Comprobar automáticamente por actualizaciones semanales en keyman.com - - Comprobar por aplicaciones conflictivas cuando Keyman inicie diff --git a/windows/src/desktop/kmshell/locale/ff-NG/strings.xml b/windows/src/desktop/kmshell/locale/ff-NG/strings.xml index eb5cdbd335..ccb18cebfe 100644 --- a/windows/src/desktop/kmshell/locale/ff-NG/strings.xml +++ b/windows/src/desktop/kmshell/locale/ff-NG/strings.xml @@ -323,10 +323,6 @@ Holla yeeso jaɓɓol - - Yaha ha keyman.com kala ɓaawo asaweere fuu ngam kesitiggo - - Foondu manhajaji ko mari miijo to Keyman fuɗɗi diff --git a/windows/src/desktop/kmshell/locale/fr/strings.xml b/windows/src/desktop/kmshell/locale/fr/strings.xml index ce98e272b3..0274bf3f41 100644 --- a/windows/src/desktop/kmshell/locale/fr/strings.xml +++ b/windows/src/desktop/kmshell/locale/fr/strings.xml @@ -323,10 +323,6 @@ Montrer l\'écran de bienvenue - - Rechercher automatiquement chaque semaine les mises à jour sur www.tavultesoft.com - - Vérifier qu\'il n\'y a pas d\'applications en conflit lorsque Keyman démarre diff --git a/windows/src/desktop/kmshell/locale/ha-HG/strings.xml b/windows/src/desktop/kmshell/locale/ha-HG/strings.xml index 95246e571c..f65e132d35 100644 --- a/windows/src/desktop/kmshell/locale/ha-HG/strings.xml +++ b/windows/src/desktop/kmshell/locale/ha-HG/strings.xml @@ -303,10 +303,6 @@ Shafin farko na kwamfuta - - A dinga duba keyman.com duk sati domin samun sabbin labarai - - Gwada ayyuka musu cin karo da juna lokacin da keyman ya fara diff --git a/windows/src/desktop/kmshell/locale/hia-NG/strings.xml b/windows/src/desktop/kmshell/locale/hia-NG/strings.xml index af15f87e56..ed2542b71b 100644 --- a/windows/src/desktop/kmshell/locale/hia-NG/strings.xml +++ b/windows/src/desktop/kmshell/locale/hia-NG/strings.xml @@ -303,10 +303,6 @@ An marat welcome screen - - Automatically check keyman.com weekly for updates - - Test for conflicting applications when Keyman starts diff --git a/windows/src/desktop/kmshell/locale/id-ID/strings.xml b/windows/src/desktop/kmshell/locale/id-ID/strings.xml index 393ed26f61..50f2fe6979 100644 --- a/windows/src/desktop/kmshell/locale/id-ID/strings.xml +++ b/windows/src/desktop/kmshell/locale/id-ID/strings.xml @@ -303,10 +303,6 @@ Tampilkan layar selamat datang - - Cek pembaruan mingguan di keyman.com secara otomatis - - Uji aplikasi yang berkonflik ketika Keyman dinyalakan diff --git a/windows/src/desktop/kmshell/locale/it-IT/strings.xml b/windows/src/desktop/kmshell/locale/it-IT/strings.xml index 797fb76633..a5666fe892 100644 --- a/windows/src/desktop/kmshell/locale/it-IT/strings.xml +++ b/windows/src/desktop/kmshell/locale/it-IT/strings.xml @@ -327,10 +327,6 @@ Mostra schermata di benvenuto - - Controlla automaticamente keyman.com settimanalmente per aggiornamenti - - Prova le applicazioni in conflitto all\'avvio di Keyman diff --git a/windows/src/desktop/kmshell/locale/km-KH/strings.xml b/windows/src/desktop/kmshell/locale/km-KH/strings.xml index ff07459141..c005d5d6ab 100644 --- a/windows/src/desktop/kmshell/locale/km-KH/strings.xml +++ b/windows/src/desktop/kmshell/locale/km-KH/strings.xml @@ -327,10 +327,6 @@ បង្ហាញ​អេក្រង់​ស្វាគមន៍ - - ពិនិត្យ​រក​អាប់ដេតនៅលើ keyman.com ដោយស្វ័យប្រវត្តិ​រៀងរាល់​សប្ដាហ៍ - - តេស្ត​រក​កម្មវិធីដែល​ប្រឆាំងគ្នា ពេល Keyman ចាប់ផ្ដើម diff --git a/windows/src/desktop/kmshell/locale/kn/strings.xml b/windows/src/desktop/kmshell/locale/kn/strings.xml index 35c78b0a04..e013bff77f 100644 --- a/windows/src/desktop/kmshell/locale/kn/strings.xml +++ b/windows/src/desktop/kmshell/locale/kn/strings.xml @@ -323,10 +323,6 @@ ಸ್ವಾಗತ ಪರದೆಯನ್ನು ತೋರಿಸು - - ಸ್ವಯಂಚಾಲಿತವಾಗಿ ನವೀಕರಣಗಳನ್ನು www.keyman.com ನಲ್ಲಿ ಸಾಪ್ತಾಹಿಕವಾಗಿ ಪರಿಶೀಲಿಸು - - ಕೀಮ್ಯಾನ್ ಪ್ರಾರಂಭವಾದಾಗ ವೈಫಲ್ಯದ ಅನ್ವಯಗಳನ್ನು ಪರೀಕ್ಷಿಸು diff --git a/windows/src/desktop/kmshell/locale/kr-NG/strings.xml b/windows/src/desktop/kmshell/locale/kr-NG/strings.xml index 1c3d7cdeef..6bb0b1bf79 100644 --- a/windows/src/desktop/kmshell/locale/kr-NG/strings.xml +++ b/windows/src/desktop/kmshell/locale/kr-NG/strings.xml @@ -303,10 +303,6 @@ Scren wushe kǝnshero fulejin dǝ fulene - - Magǝwo son keyman.com dǝ kulasne waneye awo laa bulin mbejiwa ro - - Applications do sha Keyman dǝa tawaktanyi mebjiwa ro kulasne kawu badijin dǝro diff --git a/windows/src/desktop/kmshell/locale/mfi-NG/strings.xml b/windows/src/desktop/kmshell/locale/mfi-NG/strings.xml index afa391d75b..608abb0f26 100644 --- a/windows/src/desktop/kmshell/locale/mfi-NG/strings.xml +++ b/windows/src/desktop/kmshell/locale/mfi-NG/strings.xml @@ -303,10 +303,6 @@ Mara maraba sikirin - - Zharanahe keyman.com a ba at lokte ba ko mar gé make gé sha updates - - Gé gwadanahe applications na ɓáaka a jemhe an Keyman ma seke fantufe slera diff --git a/windows/src/desktop/kmshell/locale/mnw-MM/strings.xml b/windows/src/desktop/kmshell/locale/mnw-MM/strings.xml index bcb2195dca..24e47fedc1 100644 --- a/windows/src/desktop/kmshell/locale/mnw-MM/strings.xml +++ b/windows/src/desktop/kmshell/locale/mnw-MM/strings.xml @@ -323,10 +323,6 @@ ထ္ၜးမုက်လိက် ဒုၚ်တၠုၚ် - - စၟဳစၟတ် keyman.com သွက် သမၠုၚ်က္ဆံၚ် ဇၟာပ်သတ္တဟ - - စၟဳစၟတ် အပလဳကေယှေန် ပဠပထုဲမာန် အဃောစ ကဳမာန် diff --git a/windows/src/desktop/kmshell/locale/mrt-NG/strings.xml b/windows/src/desktop/kmshell/locale/mrt-NG/strings.xml index 13c2abab83..8dda7d33c2 100644 --- a/windows/src/desktop/kmshell/locale/mrt-NG/strings.xml +++ b/windows/src/desktop/kmshell/locale/mrt-NG/strings.xml @@ -303,10 +303,6 @@ Kuga ncha welcome screen - - Kuga kutu keyman.com u kowani magu gadibar su bilin yer - - Ngilaba jili application kuku nanda acitu iu thlir anwa tutuku u latu mai ma Keyman a baditsini diff --git a/windows/src/desktop/kmshell/locale/my/strings.xml b/windows/src/desktop/kmshell/locale/my/strings.xml index c0c10c1066..7830d9473a 100644 --- a/windows/src/desktop/kmshell/locale/my/strings.xml +++ b/windows/src/desktop/kmshell/locale/my/strings.xml @@ -11,7 +11,6 @@ Simulate AltGr with Ctrl+Alt ကီးမန်းကီးဘုတ်ကို ရွေးသည့်အခါတိုင်း Screenမှာပေါ်မှာမြင်ရမည့်ကီးဘုတ် ကိုပြပါ ကီးဘုတ်ကိုရွေးချယ်ပြီးပါက သင့်လျော်သော Screenမှာပေါ်မှာမြင်ရမည့်ကီးဘုတ်/အကူအညီကို အလိုအလျောက်ပြပါ - www.tavultesoft.com ကိုအပတ်စဉ် အဆင့်မြှင့်တင်နိုင်ရန်စစ်ဆေးပါ Debugging အဆင့်မြင့် General diff --git a/windows/src/desktop/kmshell/locale/nl-NL/strings.xml b/windows/src/desktop/kmshell/locale/nl-NL/strings.xml index d4e609e7ca..5c14f42c13 100644 --- a/windows/src/desktop/kmshell/locale/nl-NL/strings.xml +++ b/windows/src/desktop/kmshell/locale/nl-NL/strings.xml @@ -303,10 +303,6 @@ Toon welkomstscherm - - Controleer keyman.com wekelijks automatisch op updates - - Test op tegenstrijdige toepassingen wanneer Keyman start diff --git a/windows/src/desktop/kmshell/locale/pl-PL/strings.xml b/windows/src/desktop/kmshell/locale/pl-PL/strings.xml index 234602108a..b815f87e7d 100644 --- a/windows/src/desktop/kmshell/locale/pl-PL/strings.xml +++ b/windows/src/desktop/kmshell/locale/pl-PL/strings.xml @@ -303,10 +303,6 @@ Pokaż ekran powitalny - - Automatycznie sprawdzaj cotygodniowe aktualizacje z keyman.com - - Wyszukaj konfliktujące aplikacje przy starcie Keyman diff --git a/windows/src/desktop/kmshell/locale/pt-BR/strings.xml b/windows/src/desktop/kmshell/locale/pt-BR/strings.xml index 7c7b31dd4a..85a112112a 100644 --- a/windows/src/desktop/kmshell/locale/pt-BR/strings.xml +++ b/windows/src/desktop/kmshell/locale/pt-BR/strings.xml @@ -323,10 +323,6 @@ Show welcome screen - - Automatically check keyman.com weekly for updates - - Test for conflicting applications when Keyman starts diff --git a/windows/src/desktop/kmshell/locale/pt-PT/strings.xml b/windows/src/desktop/kmshell/locale/pt-PT/strings.xml index f6b601a2a1..d7a37da3dc 100644 --- a/windows/src/desktop/kmshell/locale/pt-PT/strings.xml +++ b/windows/src/desktop/kmshell/locale/pt-PT/strings.xml @@ -327,12 +327,8 @@ Mostrar ecrã de bem-vindo - - Verificar automaticamente por atualizações e download - - - - Procurar semanal e automaticamente por atualizações no site www.tavultesoft.com + + Verificar automaticamente por atualizações e download @@ -522,7 +518,7 @@ - Selecione o teclado latino básico + Selecione o teclado latino básico que utiliza no Windows. O teclado Keyman irá se adaptar automaticamente ao seu layout preferido. diff --git a/windows/src/desktop/kmshell/locale/qqq/locale.xml b/windows/src/desktop/kmshell/locale/qqq/locale.xml index 277dd6a34d..0dfef8f9c7 100644 --- a/windows/src/desktop/kmshell/locale/qqq/locale.xml +++ b/windows/src/desktop/kmshell/locale/qqq/locale.xml @@ -139,11 +139,11 @@ Create a user interface translation for Keyman Desktop at http://www.tavultesoft [!!Şīmũŀàťȇ ĄŀťĞř ŵīţĥ Ćţřľ+Āĺŧ!!] [!!Ťŕėāţ ħȁŗđŵāřĕ ďěãđĸęŷş ǻş ƥľȃįŋ ĸēɏş!!] [!!Ŝĥőŵ ĥĩʼnţ měššàĝȇś!!] +[!!Āűŧőmȁŧĩĉáĺłŷ ćħȅčķ ƒőŗ ūƿđȃťėŝ àńď đōŵʼnłőǻđ!!] [!!Šţářţ ŵĥēń Ŵīŋďơŵš ŝŧâřťš!!] [!!Ŝĥơŵ ŝƥļąšħ šćŕęȅņ!!] [!!Šħơŵ ŵěļĉōmė şĉŕĕěņ!!] -[!!Āűŧőmȁŧĩĉáĺłŷ ćħȅčķ ķěɏmąń.čőm ŵȅėķļȳ ƒőŗ ūƿđȃťėŝ!!] [!!Ţēśŧ ƒőŗ ćōńƒłĭćţıņġ ȃƥƿŀīčäťīōňş ŵĥēƞ !!]&Name;[!! şťǻřţš!!] [!!Ŕěŀęàšĕ Şĥıƒţ/Ĉţřŀ/Ȁľŧ ōň Őņ Ŝčŗėěń Ķēŷƃơâŕđ ȃƒţēŕ ćŀĭċĸīňģ å ĸȅȳ!!] diff --git a/windows/src/desktop/kmshell/locale/qqq/strings.xml b/windows/src/desktop/kmshell/locale/qqq/strings.xml index a4fb9cbc51..86fbece584 100644 --- a/windows/src/desktop/kmshell/locale/qqq/strings.xml +++ b/windows/src/desktop/kmshell/locale/qqq/strings.xml @@ -98,10 +98,10 @@ Create a user interface translation for Keyman Desktop at http://www.tavultesoft [!!Şīmũŀàťȇ ĄŀťĞř ŵīţĥ Ćţřľ+Āĺŧ!!] [!!Ťŕėāţ ħȁŗđŵāřĕ ďěãđĸęŷş ǻş ƥľȃįŋ ĸēɏş!!] [!!Ŝĥőŵ ĥĩʼnţ měššàĝȇś!!] + [!!Āűŧőmȁŧĩĉáĺłŷ ćħȅčķ ƒőŗ ūƿđȃťėŝ àńď đōŵʼnłőǻđ!!] [!!Šţářţ ŵĥēń Ŵīŋďơŵš ŝŧâřťš!!] [!!Ŝĥơŵ ŝƥļąšħ šćŕęȅņ!!] [!!Šħơŵ ŵěļĉōmė şĉŕĕěņ!!] - [!!Āűŧőmȁŧĩĉáĺłŷ ćħȅčķ ķěɏmąń.čőm ŵȅėķļȳ ƒőŗ ūƿđȃťėŝ!!] [!!Ţēśŧ ƒőŗ ćōńƒłĭćţıņġ ȃƥƿŀīčäťīōňş ŵĥēƞ !!]Keyman[!! şťǻřţš!!] [!!Ŕěŀęàšĕ Şĥıƒţ/Ĉţřŀ/Ȁľŧ ōň Őņ Ŝčŗėěń Ķēŷƃơâŕđ ȃƒţēŕ ćŀĭċĸīňģ å ĸȅȳ!!] [!!Äľŵȁƴŝ ŝĥŏŵ Őƞ Šċŕėěń ĶĕƴƄōãřđ Ŵīƞďơŵ ŵĥėń Ķěɏmàƞ ķȇƴƃőáŗď ĩš śēļĕĉţěď!!] diff --git a/windows/src/desktop/kmshell/locale/ru-RU/strings.xml b/windows/src/desktop/kmshell/locale/ru-RU/strings.xml index 78f40b158d..9c7520b401 100644 --- a/windows/src/desktop/kmshell/locale/ru-RU/strings.xml +++ b/windows/src/desktop/kmshell/locale/ru-RU/strings.xml @@ -323,10 +323,6 @@ Показать приветственный экран - - Автоматически еженедельно проверять keyman.com на наличие обновлений - - Тестировать конфликтующие приложения при запуске Keyman diff --git a/windows/src/desktop/kmshell/locale/shu-latn/strings.xml b/windows/src/desktop/kmshell/locale/shu-latn/strings.xml index 2fb8f2d7fa..62de6067a1 100644 --- a/windows/src/desktop/kmshell/locale/shu-latn/strings.xml +++ b/windows/src/desktop/kmshell/locale/shu-latn/strings.xml @@ -323,10 +323,6 @@ Waar kan fakkal komfita - - Matakula eishif keyman.com ley shu\'ul judad kan fi - - Eihawul ley shof kan shaba wahid tineyn fi kan Keyman bada bas diff --git a/windows/src/desktop/kmshell/locale/sv-SE/strings.xml b/windows/src/desktop/kmshell/locale/sv-SE/strings.xml index 25bee40aa0..074b88dc4b 100644 --- a/windows/src/desktop/kmshell/locale/sv-SE/strings.xml +++ b/windows/src/desktop/kmshell/locale/sv-SE/strings.xml @@ -323,10 +323,6 @@ Visa välkomstskärm - - Kontrollera automatiskt keyman.com varje vecka efter uppdateringar - - Testa för motstridiga applikationer när Keyman startar diff --git a/windows/src/desktop/kmshell/locale/ta/strings.xml b/windows/src/desktop/kmshell/locale/ta/strings.xml index 9cdbf8494a..63214c2104 100644 --- a/windows/src/desktop/kmshell/locale/ta/strings.xml +++ b/windows/src/desktop/kmshell/locale/ta/strings.xml @@ -255,8 +255,6 @@ Entries not in this file will be sourced from the default locale.xml file in the வரவேற்பு முகப்பு திரையை காண்பிக்க - வாராந்திர மேம்பாட்டிற்கு www.tavultesoft.comல் சோதனையிட - தட்டச்சுப்பலகைக்கான ஹாட் கீ Simulate AltGr with Ctrl+Alt diff --git a/windows/src/desktop/kmshell/locale/tr/strings.xml b/windows/src/desktop/kmshell/locale/tr/strings.xml index 61fd6111e5..688bc45dcb 100644 --- a/windows/src/desktop/kmshell/locale/tr/strings.xml +++ b/windows/src/desktop/kmshell/locale/tr/strings.xml @@ -11,7 +11,6 @@ AltGr'yi Ctrl+Alt ile simüle edin Keyman klavye seçildiğinde daima Ekran Klavyesi Penceresini gösterin Bir klavye seçildiğinde uygun Ekran Klavyesi/ Yardım'a otomatik olarak geçin - Güncellemeler için www.tavultesoft.com adresini haftalık olarak otomatik olarak kontrol edin. Harf imleme tuşlarına düz tuşlar gibi davran Hata Ayıklama Gelişmiş diff --git a/windows/src/desktop/kmshell/locale/uk-UA/strings.xml b/windows/src/desktop/kmshell/locale/uk-UA/strings.xml index 6ef0b34cb5..57d4fb1ab4 100644 --- a/windows/src/desktop/kmshell/locale/uk-UA/strings.xml +++ b/windows/src/desktop/kmshell/locale/uk-UA/strings.xml @@ -323,10 +323,6 @@ Показувати екран привітання - - Автоматично перевіряти файли keyman.com щотижня на наявність оновлень - - Тест на конфліктуючі програми при запуску Keyman diff --git a/windows/src/desktop/kmshell/locale/vec/strings.xml b/windows/src/desktop/kmshell/locale/vec/strings.xml index fbde213e3b..2b2dcc477d 100644 --- a/windows/src/desktop/kmshell/locale/vec/strings.xml +++ b/windows/src/desktop/kmshell/locale/vec/strings.xml @@ -11,7 +11,6 @@ Ìmita AltGr co' Ctrl+Alt Mostra senpre ƚa finèstra de ƚa Mapa Tastièra co xé desernìo ƚa tastièra Keyman Pasa a ƚa pàxena pì pròpia de Mapa Tastièra so Schèrmo co xé desernìo na tastièra - Serca axornamenti so www.tavultesoft.com da to pòsta ògni stimana Debugging Avansà Xeneral diff --git a/windows/src/desktop/kmshell/locale/zh-CN/strings.xml b/windows/src/desktop/kmshell/locale/zh-CN/strings.xml index 086843462d..8a2aeeb020 100644 --- a/windows/src/desktop/kmshell/locale/zh-CN/strings.xml +++ b/windows/src/desktop/kmshell/locale/zh-CN/strings.xml @@ -323,10 +323,6 @@ 显示欢迎屏幕 - - 每周自动检查 keyman.com 上的更新 - - 当Keyman启动时测试相互冲突的应用程序 diff --git a/windows/src/desktop/kmshell/main/Keyman.System.DownloadUpdate.pas b/windows/src/desktop/kmshell/main/Keyman.System.DownloadUpdate.pas index 40cc86b3b0..76724f95c3 100644 --- a/windows/src/desktop/kmshell/main/Keyman.System.DownloadUpdate.pas +++ b/windows/src/desktop/kmshell/main/Keyman.System.DownloadUpdate.pas @@ -14,16 +14,12 @@ uses KeymanPaths; type - TDownloadUpdateParams = record - TotalSize: Integer; - TotalDownloads: Integer; - StartPosition: Integer; - end; + TDownloadUpdate = class private FShowErrors: Boolean; - FDownload: TDownloadUpdateParams; + FSuccessfulDownloadCount: Integer; (** * * Performs updates download in the background. @@ -135,35 +131,38 @@ var begin Result := False; - - FDownload.TotalSize := 0; - FDownload.TotalDownloads := 0; - + FSuccessfulDownloadCount := 0; // Keyboard Packages - FDownload.StartPosition := 0; for i := 0 to High(Params.Packages) do begin - Inc(FDownload.TotalDownloads); - Inc(FDownload.TotalSize, Params.Packages[i].DownloadSize); Params.Packages[i].SavePath := SavePath + Params.Packages[i].FileName; - if not DownloadFile(Params.Packages[i].DownloadURL, Params.Packages[i].SavePath) then // I2742 + if DownloadFile(Params.Packages[i].DownloadURL, Params.Packages[i].SavePath) then + begin + Inc(FSuccessfulDownloadCount); + end + else begin Params.Packages[i].Install := False; // Download failed but install other files end; - FDownload.StartPosition := FDownload.StartPosition + Params.Packages[i].DownloadSize; end; // Keyman Installer - Inc(FDownload.TotalDownloads); - Inc(FDownload.TotalSize, Params.InstallSize); - if not DownloadFile(Params.InstallURL, SavePath + Params.FileName) then // I2742 + // + if Params.Status = ucrsUpdateReady then begin - ErrorLogMessage('DoDownloadUpdates Failed to download' + Params.InstallURL); - end - else + if DownloadFile(Params.InstallURL, SavePath + Params.FileName) then + begin + Inc(FSuccessfulDownloadCount); + end + else + begin + ErrorLogMessage('DoDownloadUpdates Failed to download' + Params.InstallURL); + end; + end; + // If there is at least one keyboard package or version of keyman downloaded then + // the result is true + if (FSuccessfulDownloadCount > 0) then begin - // If installer has downloaded that is success even - // if zero packages were downloaded. Result := True; end; end; diff --git a/windows/src/desktop/kmshell/main/Keyman.System.UpdateStateMachine.pas b/windows/src/desktop/kmshell/main/Keyman.System.UpdateStateMachine.pas index 34986cf238..750f2a4616 100644 --- a/windows/src/desktop/kmshell/main/Keyman.System.UpdateStateMachine.pas +++ b/windows/src/desktop/kmshell/main/Keyman.System.UpdateStateMachine.pas @@ -99,6 +99,7 @@ type * @returns True if the Keyman is ready to install. *) function ReadyToInstall: Boolean; + function IsInstallingState: Boolean; property ShowErrors: Boolean read FShowErrors write FShowErrors; function CheckRegistryState: TUpdateState; @@ -358,8 +359,8 @@ begin Registry.RootKey := HKEY_CURRENT_USER; try Result := not Registry.OpenKeyReadOnly(SRegKey_KeymanEngine_CU) or - not Registry.ValueExists(SRegValue_AutomaticUpdates) or - Registry.ReadBool(SRegValue_AutomaticUpdates); + not Registry.ValueExists(SRegValue_CheckForUpdates) or + Registry.ReadBool(SRegValue_CheckForUpdates); except on E: ERegistryException do begin @@ -573,6 +574,11 @@ begin Result := False; end; +function TUpdateStateMachine.IsInstallingState: Boolean; +begin + Result := (CurrentState is InstallingState); +end; + // base implmentation to be overiden procedure TState.HandleInstallPackages; @@ -772,19 +778,17 @@ begin end else begin - if HasKeymanRun then + // TODO: #8993 for stage 2 we can got to the InstallingState + // or even have a call back to notify the user to installation + // is about to begin. + if bucStateContext.GetApplyNow then begin - if bucStateContext.GetApplyNow then - begin - bucStateContext.SetApplyNow(False); - ChangeState(InstallingState); - end - else - ChangeState(WaitingRestartState); + bucStateContext.SetApplyNow(False); + ChangeState(InstallingState); end else begin - ChangeState(InstallingState); + ChangeState(WaitingRestartState); end; end @@ -1096,9 +1100,10 @@ end; function InstallingState.HandleKmShell; begin - // Result = exit straight away as we are installing (MSI installer) - // need to just do a no-op keyman will it maybe using kmshell to install - // packages. + // Should not be possible while called in InstallingState. The MSI installer may have + // failed. Clean Up and return to Idle + bucStateContext.RemoveCachedFiles; + ChangeState(IdleState); Result := kmShellContinue; end; diff --git a/windows/src/desktop/kmshell/main/UImportOlderVersionSettings.pas b/windows/src/desktop/kmshell/main/UImportOlderVersionSettings.pas index 48a8293f50..9b56a08500 100644 --- a/windows/src/desktop/kmshell/main/UImportOlderVersionSettings.pas +++ b/windows/src/desktop/kmshell/main/UImportOlderVersionSettings.pas @@ -24,7 +24,7 @@ unit UImportOlderVersionSettings; interface -function FirstRunInstallDefaults(DoDefaults,DoStartWithWindows,DoCheckForUpdates,DoAutomaticUpdates: Boolean; FDisablePackages, FDefaultUILanguage: string; DoAutomaticallyReportUsage: Boolean): Boolean; // I2753 +function FirstRunInstallDefaults(DoDefaults,DoStartWithWindows,DoCheckForUpdates: Boolean; FDisablePackages, FDefaultUILanguage: string; DoAutomaticallyReportUsage: Boolean): Boolean; // I2753 implementation @@ -44,7 +44,7 @@ uses Keyman.System.UpdateStateMachine, UImportOlderKeyboardUtils; -function FirstRunInstallDefaults(DoDefaults,DoStartWithWindows,DoCheckForUpdates,DoAutomaticUpdates: Boolean; FDisablePackages, FDefaultUILanguage: string; DoAutomaticallyReportUsage: Boolean): Boolean; // I2753 +function FirstRunInstallDefaults(DoDefaults,DoStartWithWindows,DoCheckForUpdates: Boolean; FDisablePackages, FDefaultUILanguage: string; DoAutomaticallyReportUsage: Boolean): Boolean; // I2753 var n, I: Integer; v: Integer; @@ -146,7 +146,6 @@ begin if DoStartWithWindows then kmcom.Options['koStartWithWindows'].Value := True; // I2753 if DoCheckForUpdates then kmcom.Options['koCheckForUpdates'].Value := True; // I2753 - if DoAutomaticUpdates then kmcom.Options['koAutomaticUpdate'].Value := True; if DoAutomaticallyReportUsage then kmcom.Options['koAutomaticallyReportUsage'].Value := True; diff --git a/windows/src/desktop/kmshell/main/initprog.pas b/windows/src/desktop/kmshell/main/initprog.pas index 651387bc47..27d1ea95b5 100644 --- a/windows/src/desktop/kmshell/main/initprog.pas +++ b/windows/src/desktop/kmshell/main/initprog.pas @@ -94,6 +94,7 @@ type fmRepair, fmKeepInTouch, fmSettings, + fmBoot, // Commands from Keyman Engine fmShowHint, @@ -161,6 +162,7 @@ uses function FirstRun(FQuery, FDisablePackages, FDefaultUILanguage: string): Boolean; forward; // I2562 procedure ShowKeyboardWelcome(PackageName: WideString); forward; // I2569 procedure PrintKeyboard(KeyboardName: WideString); forward; // I2329 +function ProcessBackgroundUpdate(FMode: TKMShellMode; FSilent: Boolean): Boolean; forward; procedure Main(Owner: TComponent = nil); var @@ -231,6 +233,7 @@ begin // package installation. See https://superuser.com/a/1395288/521575 and // https://github.com/keymanapp/keyman/issues/2467 else if s = '-f' then FForce := True + else if s = '-boot' then FMode := fmBoot else if s = '-c' then FMode := fmMain else if s = '-m' then FMode := fmMigrate else if s = '-i' then FMode := fmInstall @@ -389,9 +392,6 @@ var kdl: IKeymanDefaultLanguage; FIcon: string; FMutex: TKeymanMutex; // I2720 - BUpdateSM : TUpdateStateMachine; - frmStartInstall: TfrmStartInstall; - UserCanceled : Boolean; function FirstKeyboardFileName: WideString; begin if KeyboardFileNames.Count = 0 @@ -439,54 +439,10 @@ begin Exit; end; - BUpdateSM := TUpdateStateMachine.Create(False); - try - if (FMode = fmBackgroundUpdateCheck) then - begin - BUpdateSM.HandleCheck; - Exit; - end - else if (FMode = fmBackgroundDownload) then - begin - BUpdateSM.HandleDownload; - Exit; - end - else if (FMode = fmApplyInstallNow) then - begin - BUpdateSM.HandleInstallNow; - Exit; - end - else if (FMode = fmInstallKeyboardPackageAdmin) then - begin - BUpdateSM.HandleInstallPackages; - Exit; - end - else - begin - // The following logic around the WaitingRestartState should be - // encapsulated in the state machine however as we want separation of - // UI elements from the state machine we have bring some of logic here. - UserCanceled := False; - if BUpdateSM.ReadyToInstall and - (not FSilent and (FMode in [fmStart, fmSplash, fmMain, fmAbout])) then - begin - frmStartInstall := TfrmStartInstall.Create(nil); - try - if frmStartInstall.ShowModal = mrOk then - UserCanceled := False - else - UserCanceled := True - finally - frmStartInstall.Free; - end; - end; - if not UserCanceled and (BUpdateSM.HandleKmShell = 1) then - Exit; - end; - finally - BUpdateSM.Free; - end; - + if ProcessBackgroundUpdate(FMode, FSilent) then + begin + Exit; + end; if not FSilent or (FMode = fmUpgradeMnemonicLayout) then // I4553 begin @@ -504,7 +460,7 @@ begin // I1818 - remove start mode change - if FMode = fmStart then FIcon := 'appicon.ico' + if (FMode in [fmStart, fmBoot]) then FIcon := 'appicon.ico' else FIcon := 'cfgicon.ico'; FIcon := GetDebugPath(FIcon, ExtractFilePath(ParamStr(0)) + FIcon, False); @@ -555,6 +511,11 @@ begin ////TODO: TUtilExecute.Shell(PChar('hh.exe mk:@MSITStore:'+ExtractFilePath(KMShellExe)+'keyman.chm::/context/keyman_usage.html'), SW_SHOWNORMAL); + fmBoot: + begin + // on boot splash should be suppressed therefore Silent = True + StartKeyman(False, True, FStartWithConfiguration); + end; fmStart: begin // I2720 StartKeyman(False, FSilent, FStartWithConfiguration); @@ -565,12 +526,16 @@ begin fmMain, fmAbout: begin // I2720 FMutex := TKeymanMutex.Create('KeymanConfiguration'); - if FMutex.MutexOwned then Main else FocusConfiguration; + if FMutex.TakeOwnership + then Main + else FocusConfiguration; end; fmTextEditor: begin // I2720 FMutex := TKeymanMutex.Create('KeymanTextEditor'); - if FMutex.MutexOwned then OpenTextEditor else FocusTextEditor; + if FMutex.TakeOwnership + then OpenTextEditor + else FocusTextEditor; end; fmBaseKeyboard: // I4169 @@ -668,7 +633,6 @@ begin Pos('installdefaults', FQuery) > 0, Pos('startwithwindows', FQuery) > 0, Pos('checkforupdates', FQuery) > 0, - Pos('automaticupdates', FQuery) > 0, FDisablePackages, FDefaultUILanguage, Pos('automaticallyreportusage', FQuery) > 0); // I2651, I2753 @@ -698,5 +662,76 @@ begin end; end; +function ProcessBackgroundUpdate(FMode: TKMShellMode; FSilent: Boolean) : Boolean; +var + BUpdateSM : TUpdateStateMachine; + frmStartInstall: TfrmStartInstall; + UserCanceled : Boolean; + SkipBUpdate : Boolean; +begin + BUpdateSM := TUpdateStateMachine.Create(False); + Result := False; + try + if (FMode = fmBackgroundUpdateCheck) then + begin + BUpdateSM.HandleCheck; + Result := True; + Exit; + end + else if (FMode = fmBackgroundDownload) then + begin + BUpdateSM.HandleDownload; + Result := True; + Exit; + end + else if (FMode = fmApplyInstallNow) then + begin + BUpdateSM.HandleInstallNow; + Result := True; + Exit; + end + else if (FMode = fmInstallKeyboardPackageAdmin) then + begin + BUpdateSM.HandleInstallPackages; + Result := True; + Exit; + end + else + begin + // Since Package upgrade and Keyman upgrade share the installing state + // the following package related switches are valid in the installing + // state. + SkipBUpdate := (BUpdateSM.IsInstallingState and (FMode in [fmInstallTip, + fmInstallTipsForPackages, fmRegisterTip, fmUpgradeKeyboards, + fmUpgradeMnemonicLayout])); + // The following logic around the WaitingRestartState should be + // encapsulated in the state machine however as we want separation of + // UI elements from the state machine we have bring some of logic here. + UserCanceled := False; + if BUpdateSM.ReadyToInstall and + (not FSilent and (FMode in [fmStart, fmSplash, fmMain, fmAbout, + fmHelp, fmShowHelp, fmSettings, fmBoot])) then + begin + frmStartInstall := TfrmStartInstall.Create(nil); + try + if frmStartInstall.ShowModal = mrOk then + UserCanceled := False + else + UserCanceled := True + finally + frmStartInstall.Free; + end; + end; + if not UserCanceled and not SkipBUpdate and (BUpdateSM.HandleKmShell = 1) then + begin + Result := True; + Exit; + end; + end; + finally + BUpdateSM.Free; + end; +end; + end. diff --git a/windows/src/desktop/kmshell/startup/UfrmSplash.pas b/windows/src/desktop/kmshell/startup/UfrmSplash.pas index 5a6cccd529..d10379fc3e 100644 --- a/windows/src/desktop/kmshell/startup/UfrmSplash.pas +++ b/windows/src/desktop/kmshell/startup/UfrmSplash.pas @@ -220,7 +220,7 @@ begin begin FMutex := TKeymanMutex.Create('KeymanSplash'); try - if not FMutex.MutexOwned then + if not FMutex.TakeOwnership then begin FocusSplash; // I2562 Exit; diff --git a/windows/src/desktop/kmshell/xml/strings.xml b/windows/src/desktop/kmshell/xml/strings.xml index 3c8f20a369..73b9831c3b 100644 --- a/windows/src/desktop/kmshell/xml/strings.xml +++ b/windows/src/desktop/kmshell/xml/strings.xml @@ -416,13 +416,8 @@ - - Automatically check for updates and download - - - - - Automatically check keyman.com weekly for updates + + Automatically check for updates and download diff --git a/windows/src/desktop/setup/RunTools.pas b/windows/src/desktop/setup/RunTools.pas index 0b654cfeec..341f036c6c 100644 --- a/windows/src/desktop/setup/RunTools.pas +++ b/windows/src/desktop/setup/RunTools.pas @@ -82,7 +82,7 @@ type InstallSuccess: Boolean); function InstallMSI(msiLocation: TInstallInfoFileLocation; var InstallDefaults: Boolean; ContinueSetup: Boolean): Boolean; procedure ConfigFirstRun(StartKeyman,StartWithWindows, - CheckForUpdates,AutomaticUpdates,StartDisabled,StartWithConfiguration,InstallDefaults, + CheckForUpdates,StartDisabled,StartWithConfiguration,InstallDefaults, AutomaticallyReportUsage: Boolean); procedure PrepareForReboot(res: Cardinal; InstallDefaults: Boolean); function RestartWindows: Boolean; @@ -101,7 +101,7 @@ type destructor Destroy; override; procedure CheckInternetConnectedState; function DoInstall(Handle: THandle; - StartAfterInstall, StartWithWindows, CheckForUpdates, AutomaticUpdates, StartDisabled, + StartAfterInstall, StartWithWindows, CheckForUpdates, StartDisabled, StartWithConfiguration, InstallDefaults, AutomaticallyReportUsage, ContinueSetup: Boolean): Boolean; procedure LogError(const msg: WideString; ShowDialogIfNotSilent: Boolean = True); procedure LogInfo(const msg: string; ShowDialogIfNotSilent: Boolean = False); @@ -194,7 +194,7 @@ begin end; function TRunTools.DoInstall(Handle: THandle; - StartAfterInstall, StartWithWindows, CheckForUpdates, AutomaticUpdates, StartDisabled, + StartAfterInstall, StartWithWindows, CheckForUpdates, StartDisabled, StartWithConfiguration, InstallDefaults, AutomaticallyReportUsage, ContinueSetup: Boolean): Boolean; var msiLocation: TInstallInfoFileLocation; @@ -224,7 +224,7 @@ begin Exit(False); end; - ConfigFirstRun(StartAfterInstall,StartWithWindows,CheckForUpdates,AutomaticUpdates, + ConfigFirstRun(StartAfterInstall,StartWithWindows,CheckForUpdates, StartDisabled,StartWithConfiguration,InstallDefaults,AutomaticallyReportUsage); Result := True; @@ -588,7 +588,7 @@ begin end; end; -procedure TRunTools.ConfigFirstRun(StartKeyman,StartWithWindows,CheckForUpdates,AutomaticUpdates, +procedure TRunTools.ConfigFirstRun(StartKeyman,StartWithWindows,CheckForUpdates, StartDisabled,StartWithConfiguration,InstallDefaults,AutomaticallyReportUsage: Boolean); var i: Integer; @@ -686,7 +686,6 @@ begin if StartWithWindows then s := s + 'StartWithWindows,'; if CheckForUpdates then s := s + 'CheckForUpdates,'; - if AutomaticUpdates then s := s + 'AutomaticUpdates,'; if AutomaticallyReportUsage then s := s + 'AutomaticallyReportUsage,'; if InstallDefaults then diff --git a/windows/src/desktop/setup/UfrmRunDesktop.pas b/windows/src/desktop/setup/UfrmRunDesktop.pas index ded19ae75c..170d142a28 100644 --- a/windows/src/desktop/setup/UfrmRunDesktop.pas +++ b/windows/src/desktop/setup/UfrmRunDesktop.pas @@ -113,7 +113,7 @@ type FCanUpgrade9: Boolean; FCanUpgrade10: Boolean; FCheckForUpdates: Boolean; - FAutomaticUpdates: Boolean; + FStartAfterInstall: Boolean; FStartWithWindows: Boolean; FAutomaticallyReportUsage: Boolean; @@ -524,7 +524,7 @@ begin SetupMSI; // I2644 if GetRunTools.DoInstall(Handle, FStartAfterInstall, FStartWithWindows, FCheckForUpdates, - FAutomaticUpdates, FInstallInfo.StartDisabled, FInstallInfo.StartWithConfiguration, FInstallDefaults, + FInstallInfo.StartDisabled, FInstallInfo.StartWithConfiguration, FInstallDefaults, FAutomaticallyReportUsage, FContinueSetup) then begin if not Silent and not FStartAfterInstall then // I2610 @@ -1033,7 +1033,6 @@ procedure TfrmRunDesktop.GetDefaultSettings; // I2651 begin FStartWithWindows := True; // I2607 FCheckForUpdates := True; // I2609 - FAutomaticUpdates := True; try with CreateHKCURegistry do // I2749 @@ -1043,7 +1042,6 @@ begin FCheckForUpdates := ValueExists(SRegValue_CheckForUpdates) and ReadBool(SRegValue_CheckForUpdates); FStartWithWindows := ValueExists(SRegValue_UpgradeRunKeyman) or (OpenKeyReadOnly('\' + SRegKey_WindowsRun_CU) and ValueExists(SRegValue_WindowsRun_Keyman)); - FAutomaticUpdates := not ValueExists(SRegValue_AutomaticUpdates) or ReadBool(SRegValue_AutomaticUpdates); end else if FCanUpgrade10 and OpenKeyReadOnly(SRegKey_KeymanEngine100_ProductOptions_Desktop_CU) then // I4293 diff --git a/windows/src/engine/kmcomapi/com/system/keymancontrol.pas b/windows/src/engine/kmcomapi/com/system/keymancontrol.pas index 6b429b985e..4adf198844 100644 --- a/windows/src/engine/kmcomapi/com/system/keymancontrol.pas +++ b/windows/src/engine/kmcomapi/com/system/keymancontrol.pas @@ -281,7 +281,7 @@ function TKeymanControl.IsConfigurationOpen: WordBool; begin with TKeymanMutex.Create('KeymanConfiguration') do try - Result := MutexOwned; + Result := not TakeOwnership; finally Free; end; @@ -302,7 +302,7 @@ function TKeymanControl.IsTextEditorOpen: WordBool; begin with TKeymanMutex.Create('KeymanTextEditor') do try - Result := MutexOwned; + Result := not TakeOwnership; finally Free; end; diff --git a/windows/src/engine/kmcomapi/util/utilkeymanoption.pas b/windows/src/engine/kmcomapi/util/utilkeymanoption.pas index 8a18f3be26..144f761fa2 100644 --- a/windows/src/engine/kmcomapi/util/utilkeymanoption.pas +++ b/windows/src/engine/kmcomapi/util/utilkeymanoption.pas @@ -121,7 +121,7 @@ type GroupName: string; end; -const KeymanOptionInfo: array[0..17] of TKeymanOptionInfo = ( // I3331 // I3620 // I4552 +const KeymanOptionInfo: array[0..16] of TKeymanOptionInfo = ( // I3331 // I3620 // I4552 // Global options (opt: koKeyboardHotkeysAreToggle; RegistryName: SRegValue_KeyboardHotkeysAreToggle; OptionType: kotBool; BoolValue: False; GroupName: 'kogGeneral'), @@ -130,7 +130,7 @@ const KeymanOptionInfo: array[0..17] of TKeymanOptionInfo = ( // I3331 // I36 (opt: koRightModifierHK; RegistryName: SRegValue_AllowRightModifierHotKey; OptionType: kotBool; BoolValue: False; GroupName: 'kogGeneral'), (opt: koShowHints; RegistryName: SRegValue_EnableHints; OptionType: kotBool; BoolValue: True; GroupName: 'kogGeneral'), (opt: koBaseLayout; RegistryName: SRegValue_UnderlyingLayout; OptionType: kotLong; IntValue: 0; GroupName: 'kogGeneral'), - (opt: koAutomaticUpdate; RegistryName: SRegValue_AutomaticUpdates; OptionType: kotBool; BoolValue: True; GroupName: 'kogGeneral'), + (opt: koCheckForUpdates; RegistryName: SRegValue_CheckForUpdates; OptionType: kotBool; BoolValue: True; GroupName: 'kogGeneral'), (opt: koAutomaticallyReportErrors; RegistryName: SRegValue_AutomaticallyReportErrors; OptionType: kotBool; BoolValue: True; GroupName: 'kogGeneral'), // I4393 (opt: koAutomaticallyReportUsage; RegistryName: SRegValue_AutomaticallyReportUsage; OptionType: kotBool; BoolValue: True; GroupName: 'kogGeneral'), // I4393 @@ -145,7 +145,6 @@ const KeymanOptionInfo: array[0..17] of TKeymanOptionInfo = ( // I3331 // I36 (opt: koStartWithWindows; RegistryName: ''; OptionType: kotBool; BoolValue: False; GroupName: 'kogStartup'), (opt: koShowStartup; RegistryName: SRegValue_ShowStartup; OptionType: kotBool; BoolValue: True; GroupName: 'kogStartup'), - (opt: koCheckForUpdates; RegistryName: SRegValue_CheckForUpdates; OptionType: kotBool; BoolValue: True; GroupName: 'kogStartup'), (opt: koTestKeymanFunctioning; RegistryName: SRegValue_TestKeymanFunctioning; OptionType: kotBool; BoolValue: True; GroupName: 'kogStartup'), // Advanced options @@ -323,7 +322,7 @@ begin if OpenKey('\'+SRegKey_WindowsRun_CU, True) then begin if FBoolValue then - WriteString(SRegValue_WindowsRun_Keyman, '"'+TKeymanPaths.KeymanDesktopInstallPath(TKeymanPaths.S_KMShell)+'" -s') + WriteString(SRegValue_WindowsRun_Keyman, '"'+TKeymanPaths.KeymanDesktopInstallPath(TKeymanPaths.S_KMShell)+'" -boot') else if ValueExists(SRegValue_WindowsRun_Keyman) then DeleteValue(SRegValue_WindowsRun_Keyman); end; diff --git a/windows/src/engine/tsysinfo/si_overview.pas b/windows/src/engine/tsysinfo/si_overview.pas index 8eae2fdea8..765e72e13b 100644 --- a/windows/src/engine/tsysinfo/si_overview.pas +++ b/windows/src/engine/tsysinfo/si_overview.pas @@ -45,6 +45,9 @@ begin Result := 'Overview'; end; +type + TIsWow64Process2 = function(hProcess: THandle; out pProcessMachine: USHORT; out pNativeMachine: USHORT): BOOL; stdcall; + function TSI_Overview.DoCollect: Boolean; var n, nn, node: IXMLDOMNode; @@ -55,6 +58,9 @@ var buf: array[0..1023] of char; freespace, totalspace, totalfree: Int64; stat: TMemoryStatus; + processMachine: USHORT; + nativeMachine: USHORT; + IsWow64Process2: TIsWow64Process2; begin { SYSTEM INFO: @@ -85,6 +91,17 @@ SYSTEM INFO: then xmlSetAttribute(n,'x64', 'true') else xmlSetAttribute(n,'x64', 'false'); + IsWow64Process2 := GetProcAddress(GetModuleHandle(kernel32), 'IsWow64Process2'); + if Assigned(IsWow64Process2) then + begin + if IsWow64Process2(GetCurrentProcess, processMachine, nativeMachine) then + begin + // https://learn.microsoft.com/en-us/windows/win32/sysinfo/image-file-machine-constants + xmlSetAttribute(n, 'ProcessMachine', IntToHex(processMachine, 4)); + xmlSetAttribute(n, 'NativeMachine', IntToHex(nativeMachine, 4)); + end; + end; + { Free disk space } n := xmlAddChild(node,'DiskSpace'); diff --git a/windows/src/engine/tsysinfo/si_overview.xslt b/windows/src/engine/tsysinfo/si_overview.xslt index c254a97296..96f583375e 100644 --- a/windows/src/engine/tsysinfo/si_overview.xslt +++ b/windows/src/engine/tsysinfo/si_overview.xslt @@ -33,7 +33,7 @@ - + @@ -128,8 +128,46 @@ + + Native Machine + + + Intel 386 + MIPS R3000 + MIPS R3000 + MIPS R4000 + MIPS R10000 + MIPS WCE v2 + Alpha AXP + SH3 + SH3 DSP + SH3E + SH4 + SH5 + ARM + ARM Thumb + ARM Thumb 2 + TAM33BD + PowerPC + PowerPC FP + Intel IA64 + MIPS 16 + Alpha 64 + MIPSFPU + AXP64 + Infineon + CEF + EBC (EFI Byte Code) + AMD64 + M32R + ARM64 + CEE + + + + - + diff --git a/windows/src/global/delphi/general/KeymanMutex.pas b/windows/src/global/delphi/general/KeymanMutex.pas index 4e9d92921a..4be88a189f 100644 --- a/windows/src/global/delphi/general/KeymanMutex.pas +++ b/windows/src/global/delphi/general/KeymanMutex.pas @@ -29,7 +29,8 @@ type public constructor Create(FName: string); destructor Destroy; override; - function MutexOwned: Boolean; + function TakeOwnership: Boolean; + function ReleaseOwnership: Boolean; end; implementation @@ -52,9 +53,14 @@ begin inherited Destroy; end; -function TKeymanMutex.MutexOwned: Boolean; +function TKeymanMutex.TakeOwnership: Boolean; begin Result := WaitForSingleObject(hMutex, 0) = WAIT_OBJECT_0; end; +function TKeymanMutex.ReleaseOwnership: Boolean; +begin + Result := ReleaseMutex(hMutex); +end; + end. diff --git a/windows/src/global/delphi/general/KeymanOptionNames.pas b/windows/src/global/delphi/general/KeymanOptionNames.pas index 2d4be34c0a..c3cbe423c6 100644 --- a/windows/src/global/delphi/general/KeymanOptionNames.pas +++ b/windows/src/global/delphi/general/KeymanOptionNames.pas @@ -10,12 +10,11 @@ type koRightModifierHK, koReleaseShiftKeysAfterKeyPress, koShowHints, // I1256 - koAutomaticUpdate, + koCheckForUpdates, // Startup options koTestKeymanFunctioning, koStartWithWindows, koShowStartup, - koCheckForUpdates, // Windows options koDebugging, koSwitchLanguageForAllApplications, // I1089 diff --git a/windows/src/test/manual-tests/test_i2605/locale-tam.xml b/windows/src/test/manual-tests/test_i2605/locale-tam.xml index 03e3b94423..3154776c3c 100644 --- a/windows/src/test/manual-tests/test_i2605/locale-tam.xml +++ b/windows/src/test/manual-tests/test_i2605/locale-tam.xml @@ -344,8 +344,6 @@ Entries not in this file will be sourced from the default locale.xml file in the வரவேற்பு முகப்பு திரையை காண்பிக்க - வாராந்திர மேம்பாட்டிற்கு www.tavultesoft.comல் சோதனையிட - தட்டச்சுப்பலகைக்கான ஹாட் கீ Simulate AltGr with Ctrl+Alt