diff --git a/HISTORY.md b/HISTORY.md
index f45e1e85cda..b58c086466b 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -1,5 +1,32 @@
# Keyman Version History
+## 18.0.91 alpha 2024-08-16
+
+* refactor(web): move `predictive-text` → `worker-main` (#12146)
+* fix(web): restore flick functionality (#12187)
+* refactor(web): move `lm-message-types` → `predictive-text/types` (#12149)
+* fix(developer): enforce presence of kps Info.Description field in info compilers (#12204)
+* fix(developer): enforce presence of Version field when FollowKeyboardVersion is not set, in package compiler (#12205)
+
+## 18.0.90 alpha 2024-08-15
+
+* refactor(web): move parts of `keyboard-processor` → `js-processor` (#12111)
+* fix(web): allow `lm-worker` to build on Linux (#12181)
+* refactor(web): move remaining parts of `keyboard-processor` → `keyboard` (#12131)
+* docs: update .kmx documentation around bitmaps, modifier state (#12183)
+* refactor(web): rename `package-cache` → `keyboard-storage` (#12135)
+
+## 18.0.89 alpha 2024-08-14
+
+* feat(web): test skipped prediction round handling (#12169)
+* fix(web): support live configuration of longpress delay (#12175)
+* feat(web): add osk.gestureParams for better gesture-config persistence (#12176)
+* refactor(core): move utfcodec to common (#12171)
+* refactor: move kmx_u16 to common and rename to km_u16 (#12177)
+* refactor(developer): use npm or local source for server addons instead of github references (#12090)
+* fix(developer): fix crash with Windows Clipboard by ignoring zero scan code in debugger (#12166)
+* chore(developer): update SIL logo (#12168)
+
## 18.0.88 alpha 2024-08-13
* docs: add .kmx specification (#12163)
diff --git a/LICENSE.md b/LICENSE.md
index e031c6cc139..5b203762357 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1,6 +1,6 @@
# License
-Copyright (c) 2017-2022 SIL International. All rights reserved.
+Copyright (c) 2017-2024 SIL Global. All rights reserved.
Licensed under the MIT License.
@@ -10,7 +10,7 @@ Licensed under the MIT License.
The MIT License
-Copyright (c) 2017-2022 SIL International
+Copyright (c) 2017-2024 SIL Global
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index acd890cc452..caab39c2f9d 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
# License
-Copyright (c) SIL International.
+Copyright (c) SIL Global.
Keyman is an open source project distributed under the [MIT license](LICENSE.md).
diff --git a/VERSION.md b/VERSION.md
index 06ebe83646e..ce8cfb73ead 100644
--- a/VERSION.md
+++ b/VERSION.md
@@ -1 +1 @@
-18.0.89
\ No newline at end of file
+18.0.92
\ No newline at end of file
diff --git a/common/predictive-text/.build-builder b/common/predictive-text/.build-builder
deleted file mode 100644
index 4e15741a629..00000000000
--- a/common/predictive-text/.build-builder
+++ /dev/null
@@ -1 +0,0 @@
-The presence of this file tells CI to use the new builder_ style parameters for build.sh and unit_tests/test.sh.
\ No newline at end of file
diff --git a/common/web/build.sh b/common/web/build.sh
index 554b1e8ebb3..fbc015613fd 100755
--- a/common/web/build.sh
+++ b/common/web/build.sh
@@ -10,7 +10,6 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
#
# TODO: future modules may include
-# :lm-message-types \
# :sentry-manager \
#
diff --git a/common/web/keyboard-processor/build.sh b/common/web/keyboard-processor/build.sh
deleted file mode 100755
index 3921063bb93..00000000000
--- a/common/web/keyboard-processor/build.sh
+++ /dev/null
@@ -1,95 +0,0 @@
-#!/usr/bin/env bash
-#
-# Compile KeymanWeb's 'keyboard-processor' module, one of the components of Web's 'core' module.
-#
-## 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"
-
-BUNDLE_CMD="node ${KEYMAN_ROOT}/common/web/es-bundling/build/common-bundle.mjs"
-
-################################ Main script ################################
-
-builder_describe \
- "Compiles the web-oriented utility function module." \
- "@/web/src/tools/testing/recorder-core test" \
- "@/common/web/keyman-version" \
- "@/common/web/es-bundling" \
- "@/common/web/types" \
- "@/common/web/utils" \
- configure \
- clean \
- build \
- test \
- "--ci For use with action $(builder_term test) - emits CI-friendly test reports"
-
-builder_describe_outputs \
- configure /node_modules \
- build /common/web/keyboard-processor/build/lib/index.mjs
-
-builder_parse "$@"
-
-function do_configure() {
- verify_npm_setup
-
- # Configure Web browser-engine testing environments. As is, this should only
- # make changes when we update the dependency, even on our CI build agents.
- playwright install
-}
-
-function do_build() {
- tsc --build "${THIS_SCRIPT_PATH}/tsconfig.all.json"
-
- # Base product - the main keyboard processor
- builder_echo "Bundle base product - the main keyboard processor"
- ${BUNDLE_CMD} "${KEYMAN_ROOT}/common/web/keyboard-processor/build/obj/index.js" \
- --out "${KEYMAN_ROOT}/common/web/keyboard-processor/build/lib/index.mjs" \
- --format esm
-
- # The DOM-oriented keyboard loader
- builder_echo "Bundle the DOM-oriented keyboard loader"
- ${BUNDLE_CMD} "${KEYMAN_ROOT}/common/web/keyboard-processor/build/obj/keyboards/loaders/dom-keyboard-loader.js" \
- --out "${KEYMAN_ROOT}/common/web/keyboard-processor/build/lib/dom-keyboard-loader.mjs" \
- --format esm
-
- # The Node-oriented keyboard loader
- builder_echo "Bundle the Node-oriented keyboard loader"
- ${BUNDLE_CMD} "${KEYMAN_ROOT}/common/web/keyboard-processor/build/obj/keyboards/loaders/node-keyboard-loader.js" \
- --out "${KEYMAN_ROOT}/common/web/keyboard-processor/build/lib/node-keyboard-loader.mjs" \
- --format esm \
- --platform node
-
- # Tests
- builder_echo "Bundle tests"
- ${BUNDLE_CMD} "${KEYMAN_ROOT}/common/web/keyboard-processor/build/tests/dom/cases/domKeyboardLoader.spec.js" \
- --out "${KEYMAN_ROOT}/common/web/keyboard-processor/build/tests/dom/domKeyboardLoader.spec.mjs" \
- --format esm
-
- # Declaration bundling.
- builder_echo "Declaration bundling"
- tsc --emitDeclarationOnly --outFile ./build/lib/index.d.ts
- tsc --emitDeclarationOnly --outFile ./build/lib/dom-keyboard-loader.d.ts -p src/keyboards/loaders/tsconfig.dom.json
- tsc --emitDeclarationOnly --outFile ./build/lib/node-keyboard-loader.d.ts -p src/keyboards/loaders/tsconfig.node.json
-}
-
-function do_test() {
- local MOCHA_FLAGS=
- local WTR_CONFIG=
- if builder_has_option --ci; then
- echo "Replacing user-friendly test reports with CI-friendly versions."
- MOCHA_FLAGS="$MOCHA_FLAGS --reporter mocha-teamcity-reporter"
- WTR_CONFIG=.CI
- fi
-
- c8 mocha --recursive $MOCHA_FLAGS ./tests/node/
- web-test-runner --config tests/dom/web-test-runner${WTR_CONFIG}.config.mjs
-}
-
-builder_run_action configure do_configure
-builder_run_action clean rm -rf ./build
-builder_run_action build do_build
-builder_run_action test do_test
diff --git a/common/web/keyboard-processor/package.json b/common/web/keyboard-processor/package.json
deleted file mode 100644
index 3f0a5958970..00000000000
--- a/common/web/keyboard-processor/package.json
+++ /dev/null
@@ -1,69 +0,0 @@
-{
- "name": "@keymanapp/keyboard-processor",
- "description": "Core module for Keyman keyboard support in KeymanWeb.",
- "repository": {
- "type": "git",
- "url": "git+https://github.com/keymanapp/keyman.git"
- },
- "keywords": [
- "input",
- "languages",
- "keyboards"
- ],
- "author": "SIL International",
- "license": "MIT",
- "bugs": {
- "url": "https://github.com/keymanapp/keyman/issues"
- },
- "homepage": "https://github.com/keymanapp/keyman#readme",
- "devDependencies": {
- "@keymanapp/resources-gosh": "*",
- "c8": "^7.12.0",
- "mocha": "^10.0.0",
- "mocha-teamcity-reporter": "^4.0.0",
- "typescript": "^5.4.5"
- },
- "scripts": {
- "build": "gosh build.sh",
- "clean": "gosh build.sh clean",
- "test": "gosh build.sh test"
- },
- "dependencies": {
- "@keymanapp/common-types": "*",
- "@keymanapp/models-types": "*",
- "@keymanapp/keyman-version": "*",
- "@keymanapp/web-utils": "*"
- },
- "type": "module",
- "main": "./build/obj/index.js",
- "types": "./build/obj/index.d.ts",
- "exports": {
- ".": {
- "es6-bundling": "./src/index.ts",
- "default": "./build/obj/index.js"
- },
- "./node-keyboard-loader": {
- "es6-bundling": "./src/keyboards/loaders/node-keyboard-loader.ts",
- "types": "./build/obj/keyboards/loaders/node-keyboard-loader.d.ts",
- "import": "./build/obj/keyboards/loaders/node-keyboard-loader.js"
- },
- "./dom-keyboard-loader": {
- "es6-bundling": "./src/keyboards/loaders/dom-keyboard-loader.ts",
- "types": "./build/obj/keyboards/loaders/dom-keyboard-loader.d.ts",
- "import": "./build/obj/keyboards/loaders/dom-keyboard-loader.js"
- },
- "./lib": {
- "types": "./build/lib/index.d.ts",
- "import": "./build/lib/index.mjs"
- },
- "./lib/node-keyboard-loader": {
- "types": "./build/lib/node-keyboard-loader.d.ts",
- "import": "./build/lib/node-keyboard-loader.mjs"
- },
- "./lib/dom-keyboard-loader": {
- "types": "./build/lib/dom-keyboard-loader.d.ts",
- "import": "./build/lib/dom-keyboard-loader.mjs"
- },
- "./obj/*.js": "./build/obj/*.js"
- }
-}
diff --git a/common/web/keyboard-processor/src/keyboards/loaders/tsconfig.dom.json b/common/web/keyboard-processor/src/keyboards/loaders/tsconfig.dom.json
deleted file mode 100644
index 7ba2abe026a..00000000000
--- a/common/web/keyboard-processor/src/keyboards/loaders/tsconfig.dom.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "extends": "../../../../tsconfig.kmw-main-base.json",
- "compilerOptions": {
- "baseUrl": "../../../",
- "outDir": "../../../build/obj/keyboards/loaders/",
- "tsBuildInfoFile": "../../../build/obj/keyboards/loaders/tsconfig.dom.tsbuildinfo",
- "rootDir": "."
- },
- "references": [
- { "path": "../../../tsconfig.json" }
- ],
- "include": ["dom-keyboard-loader.ts", "domKeyboardLoader.ts"],
-}
diff --git a/common/web/keyboard-processor/src/keyboards/loaders/tsconfig.node.json b/common/web/keyboard-processor/src/keyboards/loaders/tsconfig.node.json
deleted file mode 100644
index 0be3f169ad3..00000000000
--- a/common/web/keyboard-processor/src/keyboards/loaders/tsconfig.node.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "extends": "../../../../tsconfig.kmw-main-base.json",
- "compilerOptions": {
- "types": [ "node" ],
- "baseUrl": "../../../",
- "outDir": "../../../build/obj/keyboards/loaders/",
- "tsBuildInfoFile": "../../../build/obj/keyboards/loaders/tsconfig.node.tsbuildinfo",
- "rootDir": "."
- },
- "references": [
- { "path": "../../../tsconfig.json" }
- ],
- "include": ["node-keyboard-loader.ts", "nodeKeyboardLoader.ts"],
-}
diff --git a/common/web/keyboard-processor/tests/dom/readme.md b/common/web/keyboard-processor/tests/dom/readme.md
deleted file mode 100644
index 153b7f4a1e8..00000000000
--- a/common/web/keyboard-processor/tests/dom/readme.md
+++ /dev/null
@@ -1,5 +0,0 @@
-Automated tests in this subfolder and its children are designed to facilitate simple, browser-independent
-unit tests that are DOM-reliant.
-
-Tests for anything that may reasonably vary depending upon the browser used to run the code should go under
-the "integrated" folder instead.
\ No newline at end of file
diff --git a/common/web/keyboard-processor/tests/dom/web-test-runner.CI.config.mjs b/common/web/keyboard-processor/tests/dom/web-test-runner.CI.config.mjs
deleted file mode 100644
index d18a8a9cb09..00000000000
--- a/common/web/keyboard-processor/tests/dom/web-test-runner.CI.config.mjs
+++ /dev/null
@@ -1,13 +0,0 @@
-// @ts-check
-import BASE_CONFIG from './web-test-runner.config.mjs';
-import teamcityReporter from '@keymanapp/common-test-resources/test-runner-TC-reporter.mjs';
-import { sessionStabilityReporter } from '@keymanapp/common-test-resources/test-runner-stability-reporter.mjs';
-
-/** @type {import('@web/test-runner').TestRunnerConfig} */
-export default {
- ...BASE_CONFIG,
- reporters: [
- teamcityReporter(), /* custom-written, for CI-friendly reports */
- sessionStabilityReporter({ciMode: true})
- ]
-}
\ No newline at end of file
diff --git a/common/web/keyboard-processor/tests/dom/web-test-runner.config.mjs b/common/web/keyboard-processor/tests/dom/web-test-runner.config.mjs
deleted file mode 100644
index 76651182acb..00000000000
--- a/common/web/keyboard-processor/tests/dom/web-test-runner.config.mjs
+++ /dev/null
@@ -1,62 +0,0 @@
-// @ts-check
-import { devices, playwrightLauncher } from '@web/test-runner-playwright';
-import { esbuildPlugin } from '@web/dev-server-esbuild';
-import { defaultReporter, summaryReporter } from '@web/test-runner';
-import { LauncherWrapper, sessionStabilityReporter } from '@keymanapp/common-test-resources/test-runner-stability-reporter.mjs';
-import { importMapsPlugin } from '@web/dev-server-import-maps';
-import { dirname, resolve } from 'path';
-import { fileURLToPath } from 'url';
-
-const dir = dirname(fileURLToPath(import.meta.url));
-const KEYMAN_ROOT = resolve(dir, '../../../../..');
-
-/** @type {import('@web/test-runner').TestRunnerConfig} */
-export default {
- // debug: true,
- browsers: [
- new LauncherWrapper(playwrightLauncher({ product: 'chromium' })),
- new LauncherWrapper(playwrightLauncher({ product: 'firefox' })),
- new LauncherWrapper(playwrightLauncher({ product: 'webkit', concurrency: 1 })),
- ],
- concurrency: 10,
- nodeResolve: true,
- files: [
- 'build/tests/dom/**/*.spec.mjs'
- ],
- middleware: [
- // Rewrites short-hand paths for test resources, making them fully relative to the repo root.
- function rewriteResourcePath(context, next) {
- if(context.url.startsWith('/resources/')) {
- context.url = '/common/test' + context.url;
- }
-
- return next();
- }
- ],
- plugins: [
- esbuildPlugin({ts: true, target: 'auto'}),
- importMapsPlugin({
- inject: {
- importMap: {
- // Redirects `eventemitter3` imports to the bundled ESM library. The standard import is an
- // ESM wrapper around the CommonJS implementation, and WTR fails when it hits the CommonJS.
- imports: {
- 'eventemitter3': '/node_modules/eventemitter3/dist/eventemitter3.esm.js'
- }
- }
- }
- })
- ],
- reporters: [
- summaryReporter({}), /* local-dev mocha-style */
- sessionStabilityReporter({}),
- defaultReporter({})
- ],
- /*
- Un-comment the next two lines for easy interactive debugging; it'll launch the
- test page in your preferred browser.
- */
- // open: true,
- // manual: true,
- rootDir: KEYMAN_ROOT
-}
diff --git a/common/web/keyboard-processor/tests/tsconfig.json b/common/web/keyboard-processor/tests/tsconfig.json
deleted file mode 100644
index 0e978545b81..00000000000
--- a/common/web/keyboard-processor/tests/tsconfig.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "extends": "../tsconfig.json",
- "compilerOptions": {
- "baseUrl": "../",
- "outDir": "../build/tests/",
- "tsBuildInfoFile": "../build/tests/tsconfig.tsbuildinfo",
- "rootDir": "./"
- },
- "include": [ "./dom/**/*.ts"],
- "exclude": []
-}
diff --git a/common/web/keyboard-processor/tsconfig.json b/common/web/keyboard-processor/tsconfig.json
deleted file mode 100644
index 728935b74f6..00000000000
--- a/common/web/keyboard-processor/tsconfig.json
+++ /dev/null
@@ -1,18 +0,0 @@
-// Is used by the keyboard-loader submodules.
-{
- "extends": "../tsconfig.kmw-main-base.json",
- "compilerOptions": {
- "baseUrl": "./",
- "outDir": "build/obj/",
- "tsBuildInfoFile": "build/obj/tsconfig.tsbuildinfo",
- "rootDir": "./src/"
- },
- "references": [
- { "path": "../types" },
- { "path": "../../models/types" },
- { "path": "../keyman-version/" },
- { "path": "../utils/" }
- ],
- "include": [ "./src/**/*.ts"],
- "exclude": ["./src/keyboards/loaders/**/*.ts"]
-}
diff --git a/common/web/lm-message-types/tsconfig.json b/common/web/lm-message-types/tsconfig.json
deleted file mode 100644
index 559b764980e..00000000000
--- a/common/web/lm-message-types/tsconfig.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "extends": "../../../tsconfig.base.json",
- "compilerOptions": {
- "declaration": true,
- "outDir": "build/",
- "sourceMap": true,
- "lib": ["es6"],
- "target": "es6"
- },
- "include": ["./*.ts"],
- "exclude": ["test.ts"]
-}
diff --git a/common/web/lm-worker/build.sh b/common/web/lm-worker/build.sh
index 22ffa9208c5..0343d151c8f 100755
--- a/common/web/lm-worker/build.sh
+++ b/common/web/lm-worker/build.sh
@@ -98,7 +98,7 @@ function do_test() {
if builder_has_option --ci; then
MOCHA_FLAGS="$MOCHA_FLAGS --reporter mocha-teamcity-reporter"
- WTR_CONFIG=.ci
+ WTR_CONFIG=.CI
fi
if builder_has_option --debug; then
diff --git a/common/web/lm-worker/tsconfig.json b/common/web/lm-worker/tsconfig.json
index f439e969409..95b403c768b 100644
--- a/common/web/lm-worker/tsconfig.json
+++ b/common/web/lm-worker/tsconfig.json
@@ -13,7 +13,7 @@
"references": [
// types
{ "path": "../../models/types" },
- { "path": "../lm-message-types" },
+ { "path": "../../../web/src/engine/predictive-text/types" },
// modules
{ "path": "../keyman-version" },
{ "path": "../utils" },
diff --git a/common/web/types/src/keyman-touch-layout/keyman-touch-layout-file.ts b/common/web/types/src/keyman-touch-layout/keyman-touch-layout-file.ts
index 757316a70a9..786d9213e2b 100644
--- a/common/web/types/src/keyman-touch-layout/keyman-touch-layout-file.ts
+++ b/common/web/types/src/keyman-touch-layout/keyman-touch-layout-file.ts
@@ -58,7 +58,7 @@ export const PRIVATE_USE_IDS = [
] as const;
/* A map of key field names with values matching the `typeof` the corresponding property
- * exists in common/web/keyboard-processor, keyboards/activeLayout.ts.
+ * exists in /web/src/engine/keyboard/src/keyboards/activeLayout.ts.
*
* Make sure that when one is updated, the other also is. TS types are compile-time only,
* so the run-time-accessible mapping in activeLayout.ts cannot be auto-generated by TS. */
diff --git a/developer/src/common/web/utils/package.json b/developer/src/common/web/utils/package.json
index 79b0d6a7f09..488051f8f0e 100644
--- a/developer/src/common/web/utils/package.json
+++ b/developer/src/common/web/utils/package.json
@@ -10,6 +10,8 @@
],
"dependencies": {
"@sentry/node": "^7.57.0",
+ "@keymanapp/common-types": "*",
+ "eventemitter3": "^5.0.0",
"restructure": "^3.0.1",
"semver": "^7.5.2",
"sax": ">=0.6.0",
diff --git a/developer/src/kmc-keyboard-info/src/keyboard-info-compiler-messages.ts b/developer/src/kmc-keyboard-info/src/keyboard-info-compiler-messages.ts
index 285e272da5b..bf5d989718b 100644
--- a/developer/src/kmc-keyboard-info/src/keyboard-info-compiler-messages.ts
+++ b/developer/src/kmc-keyboard-info/src/keyboard-info-compiler-messages.ts
@@ -52,8 +52,14 @@ export class KeyboardInfoCompilerMessages {
static Error_FontFileCannotBeRead = (o:{filename: string}) => m(this.ERROR_FontFileCannotBeRead,
`Font ${def(o.filename)} could not be parsed to extract a font family.`);
-static ERROR_FontFileMetaDataIsInvalid = SevError | 0x000F;
-static Error_FontFileMetaDataIsInvalid = (o:{filename: string,message:string}) => m(this.ERROR_FontFileMetaDataIsInvalid,
+ static ERROR_FontFileMetaDataIsInvalid = SevError | 0x000F;
+ static Error_FontFileMetaDataIsInvalid = (o:{filename: string,message:string}) => m(
+ this.ERROR_FontFileMetaDataIsInvalid,
`Font ${def(o.filename)} meta data invalid: ${def(o.message)}.`);
+
+ static ERROR_DescriptionIsMissing = SevError | 0x0010;
+ static Error_DescriptionIsMissing = (o:{filename:string}) => m(
+ this.ERROR_DescriptionIsMissing,
+ `The Info.Description field in the package ${def(o.filename)} is required, but is missing or empty.`);
}
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 74699556988..8e89bb81646 100644
--- a/developer/src/kmc-keyboard-info/src/keyboard-info-compiler.ts
+++ b/developer/src/kmc-keyboard-info/src/keyboard-info-compiler.ts
@@ -251,6 +251,9 @@ export class KeyboardInfoCompiler implements KeymanCompiler {
if(kmpJsonData.info.description?.description) {
keyboard_info.description = kmpJsonData.info.description.description.trim();
+ } else {
+ this.callbacks.reportMessage(KeyboardInfoCompilerMessages.Error_DescriptionIsMissing({filename:sources.kpsFilename}));
+ return null;
}
// extract the language identifiers from the language metadata arrays for
diff --git a/developer/src/kmc-keyboard-info/test/test-keyboard-info-compiler.ts b/developer/src/kmc-keyboard-info/test/test-keyboard-info-compiler.ts
index 9b61b8e93d9..99c244be2dd 100644
--- a/developer/src/kmc-keyboard-info/test/test-keyboard-info-compiler.ts
+++ b/developer/src/kmc-keyboard-info/test/test-keyboard-info-compiler.ts
@@ -835,17 +835,4 @@ describe('keyboard-info-compiler', function () {
const result = await compiler['fontSourceToKeyboardInfoFont'](KHMER_ANGKOR_KPS, kmpJsonData, fonts);
assert.deepEqual(result, KHMER_ANGKOR_DISPLAY_FONT_INFO);
});
-
- it('handles missing info.version in a package file', async function() {
- const sources = {
- ...KHMER_ANGKOR_SOURCES,
- kpsFilename: makePathToFixture('missing-info-version-in-kps-11856', 'khmer_angkor.kps')
- };
- const compiler = new KeyboardInfoCompiler();
- assert.isTrue(await compiler.init(callbacks, {sources}));
- const kpjFilename = KHMER_ANGKOR_KPJ;
- const result = await compiler.run(kpjFilename);
- const actual = JSON.parse(new TextDecoder().decode(result.artifacts.keyboard_info.data));
- assert.equal(actual.version, '1.0');
- });
});
diff --git a/developer/src/kmc-model-info/src/model-info-compiler-messages.ts b/developer/src/kmc-model-info/src/model-info-compiler-messages.ts
index e033d6c021c..cbf6b57eb90 100644
--- a/developer/src/kmc-model-info/src/model-info-compiler-messages.ts
+++ b/developer/src/kmc-model-info/src/model-info-compiler-messages.ts
@@ -44,5 +44,10 @@ export class ModelInfoCompilerMessages {
static ERROR_NoLicenseFound = SevError | 0x0009;
static Error_NoLicenseFound = () => m(this.ERROR_NoLicenseFound,
`No license for the model was found. MIT license is required for publication to Keyman lexical-models repository.`);
+
+ static ERROR_DescriptionIsMissing = SevError | 0x000A;
+ static Error_DescriptionIsMissing = (o:{filename:string}) => m(
+ this.ERROR_DescriptionIsMissing,
+ `The Info.Description field in the package ${def(o.filename)} is required, but is missing or empty.`);
}
diff --git a/developer/src/kmc-model-info/src/model-info-compiler.ts b/developer/src/kmc-model-info/src/model-info-compiler.ts
index d4f3d10d435..a5fb62cbcda 100644
--- a/developer/src/kmc-model-info/src/model-info-compiler.ts
+++ b/developer/src/kmc-model-info/src/model-info-compiler.ts
@@ -204,6 +204,9 @@ export class ModelInfoCompiler implements KeymanCompiler {
if(sources.kmpJsonData.info.description?.description) {
model_info.description = sources.kmpJsonData.info.description.description.trim();
+ } else {
+ this.callbacks.reportMessage(ModelInfoCompilerMessages.Error_DescriptionIsMissing({filename:sources.kpsFilename}));
+ return null;
}
// isRTL -- this is a little bit of a heuristic from a compiled .js
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 154cb2143e7..00d8a380b5e 100644
--- a/developer/src/kmc-package/src/compiler/package-compiler-messages.ts
+++ b/developer/src/kmc-package/src/compiler/package-compiler-messages.ts
@@ -138,5 +138,11 @@ export class PackageCompilerMessages {
static ERROR_InvalidAuthorEmail = SevError | 0x0020;
static Error_InvalidAuthorEmail = (o:{email:string}) => m(this.ERROR_InvalidAuthorEmail,
`Invalid author email: ${def(o.email)}`);
+
+ static ERROR_PackageFileHasEmptyVersion = SevError | 0x0021;
+ static Error_PackageFileHasEmptyVersion = () => m(
+ this.ERROR_PackageFileHasEmptyVersion,
+ `Package version is not following keyboard version, but the package version field is blank.`
+ );
}
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 1aad375d400..b20e086aec7 100644
--- a/developer/src/kmc-package/src/compiler/package-version-validator.ts
+++ b/developer/src/kmc-package/src/compiler/package-version-validator.ts
@@ -42,6 +42,11 @@ export class PackageVersionValidator {
if(!this.checkFollowKeyboardVersion(kmp)) {
return false;
}
+ } else {
+ if(!kmp.info.version) {
+ this.callbacks.reportMessage(PackageCompilerMessages.Error_PackageFileHasEmptyVersion());
+ return false;
+ }
}
if(!kmp.keyboards) {
diff --git a/developer/src/kmc-package/test/fixtures/absolute_path/source/absolute_path.kps b/developer/src/kmc-package/test/fixtures/absolute_path/source/absolute_path.kps
index e7c4a8b8512..b5a74c06ee1 100644
--- a/developer/src/kmc-package/test/fixtures/absolute_path/source/absolute_path.kps
+++ b/developer/src/kmc-package/test/fixtures/absolute_path/source/absolute_path.kps
@@ -17,6 +17,7 @@
Absolute Path
+ 1.0
diff --git a/developer/src/kmc-package/test/fixtures/invalid/error_package_file_has_empty_version.kps b/developer/src/kmc-package/test/fixtures/invalid/error_package_file_has_empty_version.kps
new file mode 100644
index 00000000000..386b4e2e03e
--- /dev/null
+++ b/developer/src/kmc-package/test/fixtures/invalid/error_package_file_has_empty_version.kps
@@ -0,0 +1,32 @@
+
+
+
+ 15.0.266.0
+ 7.0
+
+
+
+ Invalid Email Address
+ © 2019 National Research Council Canada
+ Eddie Antonio Santos
+
+
+
+
+ basic.kmx
+ Keyboard Basic
+ 0
+ .kmx
+
+
+
+
+ Basic
+ basic
+ 1.0
+
+ Central Khmer (Khmer, Cambodia)
+
+
+
+
diff --git a/developer/src/kmc-package/test/test-messages.ts b/developer/src/kmc-package/test/test-messages.ts
index e8fdfa842a5..2d8df44eb50 100644
--- a/developer/src/kmc-package/test/test-messages.ts
+++ b/developer/src/kmc-package/test/test-messages.ts
@@ -236,4 +236,8 @@ describe('PackageCompilerMessages', function () {
PackageCompilerMessages.ERROR_InvalidAuthorEmail);
});
+ it('should generate ERROR_PackageFileHasEmptyVersion if FollowKeyboardVersion is not present and Version is empty', async function() {
+ await testForMessage(this, ['invalid', 'error_package_file_has_empty_version.kps'],
+ PackageCompilerMessages.ERROR_PackageFileHasEmptyVersion);
+ });
});
diff --git a/developer/src/kmc-package/test/test-package-compiler.ts b/developer/src/kmc-package/test/test-package-compiler.ts
index 3f0e8d28fd2..7952e2145a2 100644
--- a/developer/src/kmc-package/test/test-package-compiler.ts
+++ b/developer/src/kmc-package/test/test-package-compiler.ts
@@ -209,6 +209,8 @@ describe('KmpCompiler', function () {
kmpJson = kmpCompiler.transformKpsToKmpObject(kpsPath);
});
+ assert.isNotNull(kmpJson);
+
await assert.isNull(kmpCompiler.buildKmpFile(kpsPath, kmpJson));
if(debug) callbacks.printMessages();
diff --git a/developer/src/server/build-addins.inc.sh b/developer/src/server/build-addins.inc.sh
index d48d44d1e74..052a24d77b9 100644
--- a/developer/src/server/build-addins.inc.sh
+++ b/developer/src/server/build-addins.inc.sh
@@ -41,9 +41,10 @@ do_build_addins() {
# Build node-windows-trayicon
#
- pushd "$KEYMAN_ROOT/node_modules/node-windows-trayicon"
+ pushd "$KEYMAN_ROOT/developer/src/server/src/win32/trayicon/addon-src"
rm -rf build
- npx node-gyp clean configure build --arch=$ARCH --silent
+ npm ci
+ "$KEYMAN_ROOT/node_modules/.bin/node-gyp" clean configure build --arch=$ARCH --silent
cp build/Release/addon.node "$TRAYICON_SRC_TARGET"
cp build/Release/addon.node "$TRAYICON_BIN_TARGET"
popd
@@ -52,9 +53,9 @@ do_build_addins() {
# Build hetrodo-node-hide-console-window-napi
#
- pushd "$KEYMAN_ROOT/node_modules/hetrodo-node-hide-console-window-napi"
+ pushd "$KEYMAN_ROOT/node_modules/node-hide-console-window"
rm -rf build
- npx node-gyp clean configure build --arch=$ARCH --silent
+ "$KEYMAN_ROOT/node_modules/.bin/node-gyp" clean configure build --arch=$ARCH --silent
cp build/Release/node-hide-console-window.node "$HIDECONSOLE_SRC_TARGET"
cp build/Release/node-hide-console-window.node "$HIDECONSOLE_BIN_TARGET"
popd
diff --git a/developer/src/server/build.sh b/developer/src/server/build.sh
index e053d72b96e..c73ed853f77 100755
--- a/developer/src/server/build.sh
+++ b/developer/src/server/build.sh
@@ -87,9 +87,16 @@ function build_server() {
# Post build
mkdir -p "$THIS_SCRIPT_PATH/build/src/site/"
- mkdir -p "$THIS_SCRIPT_PATH/build/src/win32/"
cp -r "$THIS_SCRIPT_PATH/src/site/"** "$THIS_SCRIPT_PATH/build/src/site/"
- cp -r "$THIS_SCRIPT_PATH/src/win32/"** "$THIS_SCRIPT_PATH/build/src/win32/"
+
+ mkdir -p "$THIS_SCRIPT_PATH/build/src/win32/"
+ mkdir -p "$THIS_SCRIPT_PATH/build/src/win32/console"
+ mkdir -p "$THIS_SCRIPT_PATH/build/src/win32/trayicon"
+ cp "$THIS_SCRIPT_PATH/src/win32/README.md" "$THIS_SCRIPT_PATH/build/src/win32/"
+ cp "$THIS_SCRIPT_PATH/src/win32/console/node-hide-console-window.node" "$THIS_SCRIPT_PATH/build/src/win32/console/"
+ cp "$THIS_SCRIPT_PATH/src/win32/console/node-hide-console-window.x64.node" "$THIS_SCRIPT_PATH/build/src/win32/console/"
+ cp "$THIS_SCRIPT_PATH/src/win32/trayicon/addon.node" "$THIS_SCRIPT_PATH/build/src/win32/trayicon/"
+ cp "$THIS_SCRIPT_PATH/src/win32/trayicon/addon.x64.node" "$THIS_SCRIPT_PATH/build/src/win32/trayicon/"
replaceVersionStrings "$THIS_SCRIPT_PATH/build/src/site/lib/sentry/init.js.in" "$THIS_SCRIPT_PATH/build/src/site/lib/sentry/init.js"
rm "$THIS_SCRIPT_PATH/build/src/site/lib/sentry/init.js.in"
@@ -114,10 +121,13 @@ function installer_server() {
rm -f node_modules/ngrok/bin/ngrok.exe
popd
- # @keymanapp/keyman-version is required in build/ now but we need to copy it in manually
+ # Dependencies are required in build/ but we need to copy them in manually
mkdir -p "$PRODBUILDTEMP/node_modules/@keymanapp/"
cp -R "$KEYMAN_ROOT/node_modules/@keymanapp/keyman-version/" "$PRODBUILDTEMP/node_modules/@keymanapp/"
cp -R "$KEYMAN_ROOT/node_modules/@keymanapp/developer-utils/" "$PRODBUILDTEMP/node_modules/@keymanapp/"
+ cp -R "$KEYMAN_ROOT/node_modules/@keymanapp/common-types/" "$PRODBUILDTEMP/node_modules/@keymanapp/"
+ cp -R "$KEYMAN_ROOT/node_modules/@keymanapp/ldml-keyboard-constants/" "$PRODBUILDTEMP/node_modules/@keymanapp/"
+ cp -R "$KEYMAN_ROOT/node_modules/eventemitter3/" "$PRODBUILDTEMP/node_modules/"
# We'll build in the $KEYMAN_ROOT/developer/bin/server/ folder
rm -rf "$KEYMAN_ROOT/developer/bin/server/"
@@ -144,8 +154,8 @@ function publish_server() {
builder_run_action clean:server clean_server
builder_run_action configure:server configure_server
-builder_run_action build:server build_server
builder_run_action build:addins build_addins
+builder_run_action build:server build_server
builder_run_action test:server test_server
# builder_run_action test:addins # no op
builder_run_action installer:server installer_server # TODO: rename to install-prep
diff --git a/developer/src/server/package.json b/developer/src/server/package.json
index c4c195ae1b7..b0e438ad15e 100644
--- a/developer/src/server/package.json
+++ b/developer/src/server/package.json
@@ -16,11 +16,14 @@
"multer": "^1.4.5-lts.1",
"ngrok": "^5.0.0-beta.2",
"open": "^8.4.0",
- "ws": "^8.17.1"
- },
+ "restructure": "^3.0.1",
+ "sax": ">=0.6.0",
+ "semver": "^7.5.2",
+ "ws": "^8.17.1",
+ "xmlbuilder": "~11.0.0"
+},
"optionalDependencies": {
- "hetrodo-node-hide-console-window-napi": "keymanapp/hetrodo-node-hide-console-window-napi#keyman-15.0",
- "node-windows-trayicon": "keymanapp/node-windows-trayicon#keyman-16.0"
+ "node-hide-console-window": "^2.2.0"
},
"devDependencies": {
"@keymanapp/keyman-version": "*",
@@ -32,6 +35,7 @@
"@types/ws": "^8.2.2",
"copyfiles": "^2.4.1",
"mocha": "^10.0.0",
+ "node-gyp": "^10.2.0",
"tsc-watch": "^4.5.0",
"typescript": "^5.4.5"
}
diff --git a/developer/src/server/src/win32/trayicon/README.md b/developer/src/server/src/win32/trayicon/README.md
index 2a6dfcff37c..9dbc4b19f5a 100644
--- a/developer/src/server/src/win32/trayicon/README.md
+++ b/developer/src/server/src/win32/trayicon/README.md
@@ -8,3 +8,8 @@ bundled libraries target for specific versions of node.
* Tree: https://github.com/mceSystems/node-windows-trayicon/tree/cbd7543ac186ca15ee8d7141aac43f26ceae1655
* License: MIT
* Built from: /developer/src/server/win32/node-windows-trayicon
+
+---
+
+The source for trayicon is currently in addon-src, as the original package is
+unmaintained.
\ No newline at end of file
diff --git a/developer/src/server/src/win32/trayicon/addon-src/.gitignore b/developer/src/server/src/win32/trayicon/addon-src/.gitignore
new file mode 100644
index 00000000000..c6bf5be1c71
--- /dev/null
+++ b/developer/src/server/src/win32/trayicon/addon-src/.gitignore
@@ -0,0 +1,62 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (http://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Typescript v1 declaration files
+typings/
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variables file
+.env
+
+.vscode
+build/
+test/icon.ico
\ No newline at end of file
diff --git a/developer/src/server/src/win32/trayicon/addon-src/LICENSE b/developer/src/server/src/win32/trayicon/addon-src/LICENSE
new file mode 100644
index 00000000000..454c5bc10ab
--- /dev/null
+++ b/developer/src/server/src/win32/trayicon/addon-src/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018 mceSystems
+
+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/server/src/win32/trayicon/addon-src/README.md b/developer/src/server/src/win32/trayicon/addon-src/README.md
new file mode 100644
index 00000000000..6c429beb599
--- /dev/null
+++ b/developer/src/server/src/win32/trayicon/addon-src/README.md
@@ -0,0 +1,57 @@
+# windows-trayicon
+Native addon to add a windows tray icon with menu, built on windows-native libraries (no .NET dependency)
+
+# Installation
+```
+npm install --save windows-trayicon
+```
+
+# Usage
+```
+const WindowsTrayicon = require("windows-trayicon");
+const path = require("path");
+const fs = require("fs");
+
+const myTrayApp = new WindowsTrayicon({
+ title: "Trayicon Test",
+ icon: path.resolve(__dirname, "icon.ico"),
+ menu: [
+ {
+ id: "item-1-id",
+ caption: "First Item"
+ },
+ {
+ id: "item-2-id",
+ caption: "Second Item"
+ },
+ {
+ id: "item-3-id-exit",
+ caption: "Exit"
+ }
+ ]
+});
+
+myTrayApp.item((id) => {
+ console.log(`Menu id selected=${id}`);
+ switch (id) {
+ case "item-1-id": {
+ console.log("First item selected...");
+ break;
+ }
+ case "item-2-id": {
+ myTrayApp.balloon("Hello There!", "This is my message to you").then(() => {
+ console.log("Balloon clicked");
+ })
+ break;
+ }
+ case "item-3-id-exit": {
+ myTrayApp.exit();
+ process.exit(0)
+ break;
+ }
+ }
+});
+
+process.stdin.resume()
+
+```
\ No newline at end of file
diff --git a/developer/src/server/src/win32/trayicon/addon-src/TrayIcon.cpp b/developer/src/server/src/win32/trayicon/addon-src/TrayIcon.cpp
new file mode 100644
index 00000000000..549ff935e39
--- /dev/null
+++ b/developer/src/server/src/win32/trayicon/addon-src/TrayIcon.cpp
@@ -0,0 +1,512 @@
+/*
+* Copyright (c) Istvan Pasztor
+* This source has been published on www.codeproject.com under the CPOL license.
+*/
+#include "stdafx.h"
+#include "TrayIcon.h"
+#include
+
+#define TRAY_WINDOW_MESSAGE (WM_USER + 100)
+
+namespace
+{
+// A map that never holds allocated memory when it is empty. This map will be created with placement new as a static variable,
+// and its destructor will be never called, and it shouldn't leak memory if it contains no items at program exit.
+// This dirty trick is useful when you create your trayicon object as static. In this case we can not control the
+// order of destruction of this map object and the static trayicon object. However this dirty trick ensures that
+// the map is freed exactly when the destructor of the last static trayicon is unregistering itself.
+class CIdToTrayIconMap
+{
+ public:
+ typedef UINT KeyType;
+ typedef CTrayIcon *ValueType;
+
+ // typedef didn't work with VC++6
+ struct StdMap : public std::map
+ {
+ };
+ typedef StdMap::iterator iterator;
+
+ CIdToTrayIconMap() : m_Empty(true) {}
+ ValueType &operator[](KeyType k)
+ {
+ return GetOrCreateStdMap()[k];
+ }
+ ValueType *find(KeyType k)
+ {
+ if (m_Empty)
+ return false;
+ StdMap::iterator it = GetStdMap().find(k);
+ if (it == GetStdMap().end())
+ return NULL;
+ return &it->second;
+ }
+ int erase(KeyType k)
+ {
+ if (m_Empty)
+ return 0;
+ StdMap &m = GetStdMap();
+ int res = (int)m.erase(k);
+ if (m.empty())
+ {
+ m.~StdMap();
+ m_Empty = true;
+ }
+ return res;
+ }
+ bool empty() const
+ {
+ return m_Empty;
+ }
+ // Call this only when the container is not empty!!!
+ iterator begin()
+ {
+ assert(!m_Empty); // Call this only when the container is not empty!!!
+ return m_Empty ? iterator() : GetStdMap().begin();
+ }
+ // Call this only when the container is not empty!!!
+ iterator end()
+ {
+ assert(!m_Empty); // Call this only when the container is not empty!!!
+ return m_Empty ? iterator() : GetStdMap().end();
+ }
+
+ private:
+ StdMap &GetStdMap()
+ {
+ assert(!m_Empty);
+ return (StdMap &)m_MapBuffer;
+ }
+ StdMap &GetOrCreateStdMap()
+ {
+ if (m_Empty)
+ {
+ new ((void *)&m_MapBuffer) StdMap();
+ m_Empty = false;
+ }
+ return (StdMap &)m_MapBuffer;
+ }
+
+ private:
+ bool m_Empty;
+ char m_MapBuffer[sizeof(StdMap)];
+};
+
+static CIdToTrayIconMap &GetIdToTrayIconMap()
+{
+ // This hack prevents running the destructor of our map, so it isn't problem if someone tries to reach this from a static destructor.
+ // Because of using MyMap this will not cause a memory leak if the user removes all items from the container before exiting.
+ static char id_to_tray_icon_buffer[sizeof(CIdToTrayIconMap)];
+ static bool initialized = false;
+ if (!initialized)
+ {
+ initialized = true;
+ new ((void *)id_to_tray_icon_buffer) CIdToTrayIconMap();
+ }
+ return (CIdToTrayIconMap &)id_to_tray_icon_buffer;
+}
+
+static UINT GetNextTrayIconId()
+{
+ static UINT next_id = 1;
+ return next_id++;
+}
+}
+
+void CallJsWithOptionalString(Napi::Env env, Function callback, Context *context, std::string* data) {
+ if (env != nullptr) {
+ if (callback != nullptr) {
+ if (data) {
+ callback.Call(context->Value(), { String::New(env, *data) });
+ } else {
+ callback.Call(context->Value(), {});
+ }
+
+ }
+ }
+ if (data != nullptr) {
+ delete data;
+ }
+}
+
+static const UINT g_WndMsgTaskbarCreated = RegisterWindowMessage(TEXT("TaskbarCreated"));
+LRESULT CALLBACK CTrayIcon::MessageProcessorWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
+{
+ if (uMsg == TRAY_WINDOW_MESSAGE)
+ {
+ if (CTrayIcon **ppIcon = GetIdToTrayIconMap().find((UINT)wParam))
+ (*ppIcon)->OnMessage((UINT)lParam);
+ return 0;
+ }
+ else if (uMsg == g_WndMsgTaskbarCreated)
+ {
+ CIdToTrayIconMap &id_to_tray = GetIdToTrayIconMap();
+ if (!id_to_tray.empty())
+ {
+ for (std::map::const_iterator it = id_to_tray.begin(), eit = id_to_tray.end(); it != eit; ++it)
+ {
+ CTrayIcon *pTrayIcon = it->second;
+ pTrayIcon->OnTaskbarCreated();
+ }
+ }
+ }
+ return DefWindowProc(hWnd, uMsg, wParam, lParam);
+}
+
+HWND CTrayIcon::GetMessageProcessorHWND()
+{
+ static HWND hWnd = NULL;
+ if (!hWnd)
+ {
+ static const TCHAR TRAY_ICON_MESSAGE_PROCESSOR_WND_CLASSNAME[] = TEXT("TRAY_ICON_MESSAGE_PROCESSOR_WND_CLASS");
+ HINSTANCE hInstance = (HINSTANCE)GetModuleHandle(NULL);
+
+ WNDCLASSEX wc;
+ wc.cbSize = sizeof(wc);
+ wc.cbClsExtra = 0;
+ wc.cbWndExtra = 0;
+ wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
+ wc.hCursor = LoadCursor(NULL, IDC_ARROW);
+ wc.hIcon = LoadIcon(NULL, IDI_WINLOGO);
+ wc.hIconSm = NULL;
+ wc.hInstance = hInstance;
+ wc.lpfnWndProc = MessageProcessorWndProc;
+ wc.lpszClassName = TRAY_ICON_MESSAGE_PROCESSOR_WND_CLASSNAME;
+ wc.lpszMenuName = NULL;
+ wc.style = 0;
+ if (!RegisterClassEx(&wc))
+ return NULL;
+
+ hWnd = CreateWindowEx(
+ 0,
+ TRAY_ICON_MESSAGE_PROCESSOR_WND_CLASSNAME,
+ TEXT("TRAY_ICON_MESSAGE_PROCESSOR_WND"),
+ WS_POPUP,
+ 0, 0, 0, 0,
+ NULL,
+ NULL,
+ hInstance,
+ NULL);
+ }
+ return hWnd;
+}
+
+CTrayIcon::CTrayIcon(const char *name, bool visible, HICON hIcon, bool destroy_icon_in_destructor)
+ : m_Id(GetNextTrayIconId()), m_Name(name), m_hIcon(hIcon), m_Visible(false), m_DestroyIconInDestructor(destroy_icon_in_destructor), m_pOnMessageFunc(NULL), m_pListener(NULL)
+{
+ GetIdToTrayIconMap()[m_Id] = this;
+ SetVisible(visible);
+}
+
+CTrayIcon::~CTrayIcon()
+{
+ SetVisible(false);
+ SetIcon(NULL, m_DestroyIconInDestructor);
+ GetIdToTrayIconMap().erase(m_Id);
+}
+
+HICON CTrayIcon::InternalGetIcon() const
+{
+ return m_hIcon ? m_hIcon : ::LoadIcon(NULL, IDI_APPLICATION);
+}
+
+bool CTrayIcon::AddIcon()
+{
+ NOTIFYICONDATAA data;
+ FillNotifyIconData(data);
+ data.uFlags |= NIF_MESSAGE | NIF_ICON | NIF_TIP;
+ data.uCallbackMessage = TRAY_WINDOW_MESSAGE;
+ data.hIcon = InternalGetIcon();
+
+ size_t tip_len = max(sizeof(data.szTip) - 1, strlen(m_Name.c_str()));
+ memcpy(data.szTip, m_Name.c_str(), tip_len);
+ data.szTip[tip_len] = 0;
+
+ return FALSE != Shell_NotifyIconA(NIM_ADD, &data);
+}
+
+bool CTrayIcon::RemoveIcon()
+{
+ NOTIFYICONDATAA data;
+ FillNotifyIconData(data);
+ return FALSE != Shell_NotifyIconA(NIM_DELETE, &data);
+}
+
+void CTrayIcon::OnTaskbarCreated()
+{
+ if (m_Visible)
+ AddIcon();
+}
+
+void CTrayIcon::SetName(const char *name)
+{
+ m_Name = name;
+ if (m_Visible)
+ {
+ NOTIFYICONDATAA data;
+ FillNotifyIconData(data);
+ data.uFlags |= NIF_TIP;
+
+ size_t tip_len = max(sizeof(data.szTip) - 1, strlen(name));
+ memcpy(data.szTip, name, tip_len);
+ data.szTip[tip_len] = 0;
+
+ Shell_NotifyIconA(NIM_MODIFY, &data);
+ }
+}
+
+bool CTrayIcon::SetVisible(bool visible)
+{
+ if (m_Visible == visible)
+ return true;
+ m_Visible = visible;
+ if (m_Visible)
+ return AddIcon();
+ return RemoveIcon();
+}
+
+void CTrayIcon::SetIcon(HICON hNewIcon, bool destroy_current_icon)
+{
+ if (m_hIcon == hNewIcon)
+ return;
+ if (destroy_current_icon && m_hIcon)
+ DestroyIcon(m_hIcon);
+ m_hIcon = hNewIcon;
+
+ if (m_Visible)
+ {
+ NOTIFYICONDATAA data;
+ FillNotifyIconData(data);
+ data.uFlags |= NIF_ICON;
+ data.hIcon = InternalGetIcon();
+ Shell_NotifyIconA(NIM_MODIFY, &data);
+ }
+}
+
+bool CTrayIcon::ShowBalloonTooltip(const char *title, const char *msg, ETooltipIcon icon)
+{
+#ifndef NOTIFYICONDATA_V2_SIZE
+ return false;
+#else
+ if (!m_Visible)
+ return false;
+
+ NOTIFYICONDATAA data;
+ FillNotifyIconData(data);
+ data.cbSize = NOTIFYICONDATAA_V2_SIZE; // win2k and later
+ data.uFlags |= NIF_INFO;
+ data.dwInfoFlags = icon;
+ data.uTimeout = 10000; // deprecated as of Windows Vista, it has a min(10000) and max(30000) value on previous Windows versions.
+
+ strcpy_s(data.szInfoTitle, title);
+ strcpy_s(data.szInfo, msg);
+
+ return FALSE != Shell_NotifyIconA(NIM_MODIFY, &data);
+#endif
+}
+
+void CTrayIcon::OnMessage(UINT uMsg)
+{
+ if (m_pOnMessageFunc)
+ m_pOnMessageFunc(this, uMsg);
+ if (m_pListener)
+ m_pListener->OnTrayIconMessage(this, uMsg);
+}
+
+void CTrayIcon::FillNotifyIconData(NOTIFYICONDATAA &data)
+{
+ memset(&data, 0, sizeof(data));
+ // the basic functions need only V1
+#ifdef NOTIFYICONDATA_V1_SIZE
+ data.cbSize = NOTIFYICONDATA_V1_SIZE;
+#else
+ data.cbSize = sizeof(data);
+#endif
+ data.hWnd = GetMessageProcessorHWND();
+ assert(data.hWnd);
+ data.uID = m_Id;
+}
+
+void TrayOnMessage(CTrayIcon *pTrayIcon, UINT uMsg)
+{
+ switch (uMsg)
+ {
+ case WM_LBUTTONUP:
+ case WM_RBUTTONUP:
+ ((CTrayIconContainer *)pTrayIcon->GetUserData())->PopupMenu();
+ break;
+
+ case NIN_BALLOONUSERCLICK:
+ ((CTrayIconContainer *)pTrayIcon->GetUserData())->BalloonClick();
+ break;
+ }
+}
+
+HICON GetIconHandle(std::string iconPath)
+{
+ return (HICON)LoadImage(NULL, iconPath.c_str(), IMAGE_ICON, 0, 0, LR_LOADFROMFILE | LR_DEFAULTSIZE | LR_SHARED);
+}
+
+
+CTrayIconContainer::CTrayIconContainer(const CallbackInfo& info) : ObjectWrap(info), m_OnBalloonClick(nullptr), m_OnMenuItem(nullptr) {}
+
+Object CTrayIconContainer::Init(Napi::Env env, Object exports){
+ Function func =
+ DefineClass(env,
+ "CTrayIconContainer",
+ {
+ InstanceMethod("Start", &CTrayIconContainer::Start),
+ InstanceMethod("SetIconPath", &CTrayIconContainer::SetIconPath),
+ InstanceMethod("SetTitle", &CTrayIconContainer::SetTitle),
+ InstanceMethod("Stop", &CTrayIconContainer::Stop),
+ InstanceMethod("AddMenuItem", &CTrayIconContainer::AddMenuItem),
+ InstanceMethod("OnMenuItem", &CTrayIconContainer::OnMenuItem),
+ InstanceMethod("ShowBalloon", &CTrayIconContainer::ShowBalloon),
+ }
+ );
+
+ FunctionReference* constructor = new FunctionReference();
+ *constructor = Napi::Persistent(func);
+ env.SetInstanceData(constructor);
+
+ exports.Set("CTrayIconContainer", func);
+ return exports;
+}
+
+void CTrayIconContainer::Stop(const CallbackInfo& info)
+{
+ if (m_OnMenuItem) {
+ m_OnMenuItem.Release();
+ m_OnMenuItem = nullptr;
+ }
+ if (m_OnBalloonClick) {
+ m_OnBalloonClick.Release();
+ m_OnBalloonClick = nullptr;
+ }
+ PostThreadMessage(GetThreadId(m_worker->native_handle()), WM_QUIT, NULL, NULL);
+ m_worker->join();
+ delete m_worker;
+ m_worker = nullptr;
+ m_tray.SetVisible(false);
+}
+
+LRESULT CALLBACK pWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
+{
+ return DefWindowProc(hWnd, uMsg, wParam, lParam);
+}
+
+void CTrayIconContainer::SetIconPath(const CallbackInfo& info)
+{
+ std::string iconPath = info[0].As();
+ m_tray.SetIcon(GetIconHandle(iconPath));
+}
+void CTrayIconContainer::SetTitle(const CallbackInfo& info)
+{
+ std::string title = info[0].As();
+ m_tray.SetName(title.c_str());
+}
+void CTrayIconContainer::Start(const CallbackInfo& info)
+{
+ HANDLE ready = CreateEvent(nullptr, true, false, nullptr);
+
+ m_worker = new std::thread([this, ready] {
+ m_tray.SetUserData(this);
+ m_tray.SetVisible(true);
+ m_tray.SetListener(TrayOnMessage);
+
+ SetEvent(ready);
+
+ static const TCHAR *class_name = TEXT("MCE_HWND_MESSAGE");
+ WNDCLASSEX wx = {};
+ wx.cbSize = sizeof(WNDCLASSEX);
+ wx.lpfnWndProc = pWndProc;
+ wx.hInstance = 0;
+ wx.lpszClassName = class_name;
+ RegisterClassEx(&wx);
+ m_hwnd = CreateWindowEx(0, class_name, TEXT("MCE_HWND_MESSAGE"), 0, 0, 0, 0, 0, HWND_MESSAGE, NULL, NULL, NULL);
+
+ MSG msg;
+ while (GetMessage(&msg, NULL, 0, 0))
+ {
+ TranslateMessage(&msg);
+ DispatchMessage(&msg);
+ }
+
+ DestroyWindow(m_hwnd);
+
+ return 0;
+ });
+
+ WaitForSingleObject(ready, INFINITE);
+ CloseHandle(ready);
+}
+
+void CTrayIconContainer::BalloonClick()
+{
+ m_OnBalloonClick.BlockingCall(nullptr);
+}
+
+void CTrayIconContainer::PopupMenu()
+{
+ POINT pt;
+ if (GetCursorPos(&pt))
+ {
+ HMENU menu = CreatePopupMenu();
+ int i = 1;
+ for (auto const &item : m_menuItems)
+ {
+ if(item.m_caption == "-")
+ {
+ AppendMenuA(menu, MF_SEPARATOR, -1, NULL);
+ }
+ else
+ {
+ AppendMenuA(menu, MF_STRING, i, item.m_caption.c_str());
+ }
+ i++;
+ }
+
+ HWND activeHwnd = GetForegroundWindow();
+ SetForegroundWindow(m_hwnd);
+ UINT cmd = TrackPopupMenu(menu, TPM_RETURNCMD | TPM_RIGHTBUTTON, pt.x, pt.y, 0, m_hwnd, NULL);
+ PostMessage(m_hwnd, WM_NULL, 0, 0);
+ SetForegroundWindow(activeHwnd);
+ if(cmd > 0) {
+ m_OnMenuItem.BlockingCall(new std::string(m_menuItems[cmd-1].m_id));
+ }
+ }
+}
+
+void CTrayIconContainer::AddMenuItem(const CallbackInfo& info)
+{
+ std::string id = info[0].As();
+ std::string caption = info[1].As();
+ m_menuItems.push_back(CTrayIconMenuItem(id, caption));
+}
+
+void CTrayIconContainer::OnMenuItem(const CallbackInfo& info)
+{
+ Function cb = info[0].As();
+ Context *context = new Reference(Persistent(info.This()));
+ if (m_OnMenuItem) {
+ m_OnMenuItem.Release();
+ }
+ m_OnMenuItem = TSFNOptString::New(info.Env(), cb, "wintrayicon_OnMenuItem", 0, 1, context);
+}
+
+void CTrayIconContainer::ShowBalloon(const CallbackInfo& info)
+{
+ std::string title = info[0].As();
+ std::string text = info[1].As();
+ int timeout = info[2].As();
+ Function cb = info[3].As();
+
+ Context *context = new Reference(Persistent(info.This()));
+
+ if (m_OnBalloonClick) {
+ m_OnBalloonClick.Release();
+ }
+
+ m_OnBalloonClick = TSFNOptString::New(info.Env(), cb, "wintrayicon_ShowBalloon", 0, 1, context);
+ m_tray.ShowBalloonTooltip(title.c_str(), text.c_str());
+}
diff --git a/developer/src/server/src/win32/trayicon/addon-src/TrayIcon.h b/developer/src/server/src/win32/trayicon/addon-src/TrayIcon.h
new file mode 100644
index 00000000000..ee0c85561ec
--- /dev/null
+++ b/developer/src/server/src/win32/trayicon/addon-src/TrayIcon.h
@@ -0,0 +1,193 @@
+/*
+* Copyright (c) Istvan Pasztor
+* This source has been published on www.codeproject.com under the CPOL license.
+*/
+#ifndef __TRAY_ICON_H__
+#define __TRAY_ICON_H__
+#pragma once
+
+// NOTE: include the following headers in your stdafx.h: