diff --git a/.github/workflows/deb-packaging.yml b/.github/workflows/deb-packaging.yml index c5d1829ebbd..36fa538d057 100644 --- a/.github/workflows/deb-packaging.yml +++ b/.github/workflows/deb-packaging.yml @@ -242,8 +242,13 @@ jobs: - name: Verify API run: | cd linux + if [ -f debian/libkeymancore.symbols ]; then + PKG_NAME=libkeymancore + else + PKG_NAME=libkmnkbp0-0 + fi SRC_PKG="${GITHUB_WORKSPACE}/artifacts/keyman-srcpkg/keyman_${{ needs.sourcepackage.outputs.VERSION }}-1.debian.tar.xz" \ - BIN_PKG="${GITHUB_WORKSPACE}/artifacts/keyman-binarypkgs/libkmnkbp0-0_${{ needs.sourcepackage.outputs.VERSION }}-1${{ needs.sourcepackage.outputs.PRERELEASE_TAG }}+jammy1_amd64.deb" \ + BIN_PKG="${GITHUB_WORKSPACE}/artifacts/keyman-binarypkgs/${PKG_NAME}_${{ needs.sourcepackage.outputs.VERSION }}-1${{ needs.sourcepackage.outputs.PRERELEASE_TAG }}+jammy1_amd64.deb" \ PKG_VERSION="${{ needs.sourcepackage.outputs.VERSION }}" \ ./scripts/deb-packaging.sh --gha verify 2>> $GITHUB_STEP_SUMMARY @@ -254,6 +259,13 @@ jobs: path: linux/debian/libkmnkbp0-0.symbols if: always() + - name: Archive .symbols file + uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 + with: + name: libkeymancore.symbols + path: linux/debian/libkeymancore.symbols + if: always() + set_status: name: Set result status on PR builds needs: [sourcepackage, binary_packages, api_verification] diff --git a/HISTORY.md b/HISTORY.md index e1b0f257c92..05831dc9625 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,21 @@ # Keyman Version History +## 17.0.195 alpha 2023-10-19 + +* chore(linux): Allow to collect coverage on TC (#9790) +* fix(common): don't use URL in common/web/types (#9798) +* chore: update kmp.schema.json and docs for kps schema (#9800) +* docs(windows): update text and images for windows 11 (#9689) + +## 17.0.194 alpha 2023-10-18 + +* chore(linux): Re-enable building for Ubuntu 23.10 Mantic (#9780) +* chore(linux): Add missing tests (#9783) +* fix(web): fixes touch form-factor default kbd on cookieless keymanweb.com page load (#9786) +* fix(developer): three kmc .keyboard_info generation bugs (#9784) +* fix(developer): handle invalid project folders cleanly (#9785) +* chore(linux): Fix build scripts (#9781) + ## 17.0.193 alpha 2023-10-17 * fix(developer): kmc crash on start (#9771) diff --git a/VERSION.md b/VERSION.md index 2c5f2b0026a..9bd4bcef436 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -17.0.194 \ No newline at end of file +17.0.196 \ No newline at end of file diff --git a/common/schemas/kmp/README.md b/common/schemas/kmp/README.md new file mode 100644 index 00000000000..6c8372b7790 --- /dev/null +++ b/common/schemas/kmp/README.md @@ -0,0 +1,24 @@ +# kmp.schema.json + +* kmp.json file format, metadata included in Keyman .kmp package files + +Documentation at https://help.keyman.com/developer/current-version/reference/file-types/metadata + +# kmp.schema.json version history + +## 2023-10-19 2.0 +* Add relatedPackages, options.licenseFile, options.welcomeFile, + keyboard.examples, keyboard.webOskFonts, keyboard.webDisplayFonts, + info.description (all of these formerly were stored in .keyboard_info) + +## 2019-01-31 1.1.0 +* Add lexicalModels properties (note: `version` is optional and currently unused) + +## 2018-02-13 1.0.2 +* Add rtl property for keyboard layouts + +## 2018-01-22 1.0.1 +* Remove id field as it is derived from the filename anyway + +## 2017-11-30 1.0 beta +* Initial version diff --git a/common/schemas/kmp/kmp.schema.json b/common/schemas/kmp/kmp.schema.json new file mode 100644 index 00000000000..a28aa43195a --- /dev/null +++ b/common/schemas/kmp/kmp.schema.json @@ -0,0 +1,353 @@ +{ + "$schema": "http://json-schema.org/schema#", + "$ref": "#/definitions/package", + "definitions": { + "package": { + "type": "object", + "properties": { + "system": { + "$ref": "#/definitions/system" + }, + "options": { + "$ref": "#/definitions/options" + }, + "startMenu": { + "$ref": "#/definitions/startMenu" + }, + "strings": { + "$ref": "#/definitions/strings" + }, + "files": { + "$ref": "#/definitions/files" + }, + "keyboards": { + "$ref": "#/definitions/keyboards" + }, + "lexicalModels": { + "$ref": "#/definitions/lexicalModels" + }, + "info": { + "$ref": "#/definitions/info" + }, + "relatedPackages": { + "type": "array", + "items": { + "$ref": "#/definitions/relatedPackage" + } + } + }, + "additionalProperties": false, + "required": [ + "options", + "system" + ] + }, + "system": { + "type": "object", + "properties": { + "keymanDeveloperVersion": { + "type": "string" + }, + "fileVersion": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "fileVersion" + ] + }, + "options": { + "type": "object", + "properties": { + "readmeFile": { + "type": "string" + }, + "graphicFile": { + "type": "string" + }, + "licenseFile": { + "type": "string" + }, + "welcomeFile": { + "type": "string" + }, + "executeProgram": { + "type": "string" + }, + "msiFilename": { + "type": "string" + }, + "msiOptions": { + "type": "string" + } + }, + "additionalProperties": false + }, + "startMenu": { + "type": "object", + "properties": { + "folder": { + "type": "string" + }, + "addUninstallEntry": { + "type": "boolean" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/startMenuItem" + } + } + }, + "additionalProperties": false + }, + "startMenuItem": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "filename": { + "type": "string" + }, + "arguments": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "location": { + "type": "number" + } + }, + "additionalProperties": false, + "required": [ + "name", + "filename" + ] + }, + "strings": { + "type": "object", + "patternProperties": { + ".": { + "type": "string" + } + }, + "additionalProperties": false + }, + "files": { + "type": "array", + "items": { + "$ref": "#/definitions/file" + } + }, + "file": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "copyLocation": { + "type": "number" + } + }, + "additionalProperties": false, + "required": [ + "name", + "description" + ] + }, + "keyboards": { + "type": "array", + "items": { + "$ref": "#/definitions/keyboard" + } + }, + "keyboard": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "id": { + "type": "string" + }, + "version": { + "type": "string" + }, + "oskFont": { + "type": "string" + }, + "displayFont": { + "type": "string" + }, + "rtl": { + "type": "boolean" + }, + "languages": { + "type": "array", + "items": { + "$ref": "#/definitions/keyboardLanguage" + } + }, + "examples": { + "type": "array", + "items": { + "$ref": "#/definitions/keyboardExample" + } + }, + "webOskFonts": { + "type": "array", + "items": { + "type": "string" + } + }, + "webDisplayFonts": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false, + "required": [ + "name", + "id", + "version" + ] + }, + "keyboardLanguage": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "id": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "id", + "name" + ] + }, + "info": { + "type": "object", + "properties": { + "website": { + "$ref": "#/definitions/infoItem" + }, + "version": { + "$ref": "#/definitions/infoItem" + }, + "name": { + "$ref": "#/definitions/infoItem" + }, + "copyright": { + "$ref": "#/definitions/infoItem" + }, + "author": { + "$ref": "#/definitions/infoItem" + }, + "description": { + "$ref": "#/definitions/infoItem" + } + }, + "additionalProperties": false + }, + "infoItem": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "description" + ] + }, + "lexicalModels": { + "type": "array", + "items": { + "$ref": "#/definitions/lexicalModel" + } + }, + "lexicalModel": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "id": { + "type": "string" + }, + "version": { + "type": "string" + }, + "rtl": { + "type": "boolean" + }, + "languages": { + "type": "array", + "items": { + "$ref": "#/definitions/keyboardLanguage" + } + } + }, + "additionalProperties": false, + "required": [ + "name", + "id", + "languages" + ] + }, + "keyboardExample": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "keys": { + "type": "string" + }, + "text": { + "type": "string" + }, + "note": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "id", + "keys" + ] + }, + "relatedPackage": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "relationship": { + "type": "string", + "enum": ["deprecates", "related"] + } + }, + "additionalProperties": false, + "required": [ + "id", + "relationship" + ] + } + } +} \ No newline at end of file diff --git a/common/schemas/kps/README.md b/common/schemas/kps/README.md index e4aacf97d8f..aea03f2a5b9 100644 --- a/common/schemas/kps/README.md +++ b/common/schemas/kps/README.md @@ -1,9 +1,22 @@ # kps.xsd -Master version: https://github.com/keymanapp/api.keyman.com/blob/master/schemas/kps/7.0/kps.xsd +Master version: https://github.com/keymanapp/api.keyman.com/blob/master/schemas/kps/17.0/kps.xsd -## 2021-07-19 7.0 -* Initial version 7.0 +## 2023-10-19 17.0 +* Version 17.0 adds: + - LicenseFile - a .md file, usually named LICENSE.md + - WelcomeFile - a .htm file, usually named welcome.htm (later versions will support .md) + - Info/Description - a short Markdown description of the content of the package, e.g. shown in search results on keyman.com + - RelatedPackages - a list of other packages which relate to this one, or are deprecated by it + - Keyboards/Keyboard/Examples - a list of typing examples for the keyboard + - Keyboarsd/Keyboard/WebOSKFonts - a list of font filenames (not necessarily in package) suitable for rendering the on screen keyboard + - Keyboarsd/Keyboard/WebDisplayFonts - a list of font filenames (not necessarily in package) suitable for use with the keyboard +* Version 17.0 removes: + - LexicalModels/LexicalModel/Version - version information is not stored in the models, but only in the package metadata (was unused) ## 2023-04-21 7.0.1 * Removes LexicalModel.Version, as it was never read or written + +## 2021-07-19 7.0 +* Initial version 7.0 + diff --git a/common/web/types/src/ldml-keyboard/ldml-keyboard-testdata-xml.ts b/common/web/types/src/ldml-keyboard/ldml-keyboard-testdata-xml.ts index 86a05e38934..acbcf0ead44 100644 --- a/common/web/types/src/ldml-keyboard/ldml-keyboard-testdata-xml.ts +++ b/common/web/types/src/ldml-keyboard/ldml-keyboard-testdata-xml.ts @@ -41,34 +41,41 @@ export interface LKTTests { export interface LKTTest { name?: string; startContext?: LKTStartContext; - actions?: LKTAction[]; // differs from XML, to represent order of actions + actions?: LKTAnyAction[]; // differs from XML, to represent order of actions }; export interface LKTStartContext { to?: string; }; -export interface LKTCheck { +/** + * Test Actions. + * The expectation is that each LKTAction object will have exactly one non-falsy field. + */ +export interface LKTAction { + type?: "check" | "emit" | "keystroke" | "backspace"; +}; + +export interface LKTCheck extends LKTAction { + type: "check"; result?: string; }; -export interface LKTEmit { +export interface LKTEmit extends LKTAction { + type: "emit"; to?: string; }; -export interface LKTKeystroke { +export interface LKTKeystroke extends LKTAction { + type: "keystroke"; key?: string; flick?: string; longPress?: string; tapCount?: string; }; -/** - * Test Actions. - * The expectation is that each LKTAction object will have exactly one non-falsy field. - */ -export interface LKTAction { - check?: LKTCheck; - emit?: LKTEmit; - keystroke?: LKTKeystroke; -}; +export interface LKTBackspace extends LKTAction { + type: "backspace"; +} + +export type LKTAnyAction = LKTCheck | LKTEmit | LKTKeystroke | LKTBackspace; diff --git a/common/web/types/src/ldml-keyboard/ldml-keyboard-xml-reader.ts b/common/web/types/src/ldml-keyboard/ldml-keyboard-xml-reader.ts index 5e7c1ab2d30..3d0b2c71314 100644 --- a/common/web/types/src/ldml-keyboard/ldml-keyboard-xml-reader.ts +++ b/common/web/types/src/ldml-keyboard/ldml-keyboard-xml-reader.ts @@ -21,8 +21,8 @@ export class LDMLKeyboardXMLSourceFileReader { constructor(private options: LDMLKeyboardXMLSourceFileReaderOptions, private callbacks : CompilerCallbacks) { } - static get defaultImportsURL() { - return new URL(`../import/`, import.meta.url); + static get defaultImportsURL(): [string,string] { + return ['../import/', import.meta.url]; } readImportFile(version: string, subpath: string): Uint8Array { @@ -393,15 +393,12 @@ export class LDMLKeyboardXMLSourceFileReader { r.stuffBoxes(test, $$, 'startContext'); // singleton // now the actions test.actions = $$.map(v => { - const subtag = v['#name']; - const subv = LDMLKeyboardXMLSourceFileReader.defaultMapper(v, r); - switch(subtag) { - case 'keystroke': return { keystroke: subv }; - case 'check': return { check: subv }; - case 'emit': return { emit: subv }; - case 'startContext': return null; // handled above - default: this.callbacks.reportMessage(CommonTypesMessages.Error_TestDataUnexpectedAction({ subtag })); return null; + const type = v['#name']; // element name + if (type === 'startContext') { + return null; // handled above } + const subv = LDMLKeyboardXMLSourceFileReader.defaultMapper(v, r); + return Object.assign({ type }, subv); }).filter(v => v !== null); return test; }); diff --git a/common/web/types/src/util/common-events.ts b/common/web/types/src/util/common-events.ts index b8b33005504..65743d686d8 100644 --- a/common/web/types/src/util/common-events.ts +++ b/common/web/types/src/util/common-events.ts @@ -43,8 +43,4 @@ export class CommonTypesMessages { m(this.ERROR_TestDataUnexpectedArray, `Problem reading test data: expected single ${o.subtag} element, found multiple`); static ERROR_TestDataUnexpectedArray = SevError | 0x0007; - static Error_TestDataUnexpectedAction = (o: {subtag: string}) => - m(this.ERROR_TestDataUnexpectedAction, - `Problem reading test data: unexpected action element ${o.subtag}`); - static ERROR_TestDataUnexpectedAction = SevError | 0x0008; }; diff --git a/common/web/types/test/fixtures/test-fr.xml b/common/web/types/test/fixtures/test-fr.xml index cfd63c4f758..c705605521b 100644 --- a/common/web/types/test/fixtures/test-fr.xml +++ b/common/web/types/test/fixtures/test-fr.xml @@ -1,6 +1,12 @@ + @@ -17,6 +23,8 @@ + + diff --git a/common/web/types/test/helpers/reader-callback-test.ts b/common/web/types/test/helpers/reader-callback-test.ts index 86758ad413f..907793cdf7e 100644 --- a/common/web/types/test/helpers/reader-callback-test.ts +++ b/common/web/types/test/helpers/reader-callback-test.ts @@ -9,7 +9,7 @@ import { TestCompilerCallbacks } from './TestCompilerCallbacks.js'; import { fileURLToPath } from 'url'; const readerOptions: LDMLKeyboardXMLSourceFileReaderOptions = { - importsPath: fileURLToPath(LDMLKeyboardXMLSourceFileReader.defaultImportsURL) + importsPath: fileURLToPath(new URL(...LDMLKeyboardXMLSourceFileReader.defaultImportsURL)) }; export interface CompilationCase { diff --git a/common/web/types/test/ldml-keyboard/test-ldml-keyboard-testdata-reader.ts b/common/web/types/test/ldml-keyboard/test-ldml-keyboard-testdata-reader.ts index 80a78a1187b..a0e1202f851 100644 --- a/common/web/types/test/ldml-keyboard/test-ldml-keyboard-testdata-reader.ts +++ b/common/web/types/test/ldml-keyboard/test-ldml-keyboard-testdata-reader.ts @@ -2,12 +2,14 @@ import { constants } from '@keymanapp/ldml-keyboard-constants'; import { assert } from 'chai'; import 'mocha'; import { testTestdataReaderCases } from '../helpers/reader-callback-test.js'; +import { LKTAnyAction } from './ldml-keyboard-testdata-xml.js'; describe('ldml keyboard xml reader tests', function () { this.slow(500); // 0.5 sec -- json schema validation takes a while testTestdataReaderCases([ { + // Note! There's another test case against similar data, in developer/src/kmc-ldml/test/test-testdata-e2e.ts using test-fr.json subpath: 'test-fr.xml', callback: (data, source) => { assert.ok(source); @@ -35,16 +37,19 @@ describe('ldml keyboard xml reader tests', function () { const test0 = source.keyboardTest3.tests[0].test[0]; assert.equal('key-test', test0.name); assert.equal('abc\\u0022...', test0.startContext?.to); - assert.sameDeepOrderedMembers([ - { keystroke: { key: 's' } }, - { check: { result: 'abc\\u0022...s' } }, - { keystroke: { key: 't' } }, - { check: { result: 'abc\\u0022...st' } }, - { keystroke: { key: 'u' } }, - { check: { result: 'abc\\u0022...stu' } }, - { emit: { to: 'v' } }, - { check: { result: 'abc\\u0022...stuv' } }, - ], test0.actions); + const expectedActions : LKTAnyAction[] = [ + { type: "keystroke", key: 's' }, + { type: "check", result: 'abc\\u0022...s' }, + { type: "keystroke", key: 't' }, + { type: "check", result: 'abc\\u0022...st' }, + { type: "keystroke", key: 'u' }, + { type: "check", result: 'abc\\u0022...stu' }, + { type: "emit", to: 'v' }, + { type: "check", result: 'abc\\u0022...stuv' }, + { type: "backspace" }, + { type: "check", result: 'abc\\u0022...stu' }, + ]; + assert.sameDeepOrderedMembers(expectedActions, test0.actions, 'Static data in .ts file should match parsed test-fr.xml'); }, } ]); diff --git a/core/src/debuglog.h b/core/src/debuglog.h index d8e3dc9473f..67e6ce601f9 100644 --- a/core/src/debuglog.h +++ b/core/src/debuglog.h @@ -1,5 +1,7 @@ /* Debugging */ +#pragma once + #include namespace km { @@ -18,10 +20,12 @@ extern const char *s_key_names[]; #ifdef _MSC_VER #define DebugLog(msg,...) (km::core::kmx::ShouldDebug() ? km::core::kmx::DebugLog_1(__FILE__, __LINE__, __FUNCTION__, (msg),__VA_ARGS__) : 0) +#define DebugLog2(file,line,function,msg,...) (km::core::kmx::ShouldDebug() ? km::core::kmx::DebugLog_1(file, line, function, (msg),__VA_ARGS__) : 0) #define console_error(msg,...) write_console(TRUE, (msg), __VA_ARGS__) #define console_log(msg,...) write_console(FALSE, (msg), __VA_ARGS__) #else #define DebugLog(msg,...) (km::core::kmx::ShouldDebug() ? km::core::kmx::DebugLog_1(__FILE__, __LINE__, __FUNCTION__, (msg), ##__VA_ARGS__) : 0) +#define DebugLog2(file,line,function,msg,...) (km::core::kmx::ShouldDebug() ? km::core::kmx::DebugLog_1(file, line, function, (msg), ##__VA_ARGS__) : 0) #define console_error(msg,...) write_console(TRUE, (msg), ##__VA_ARGS__) #define console_log(msg,...) write_console(FALSE, (msg), ##__VA_ARGS__) #endif diff --git a/core/src/ldml/ldml_processor.cpp b/core/src/ldml/ldml_processor.cpp index 7489ce98418..f4bb2648f1e 100644 --- a/core/src/ldml/ldml_processor.cpp +++ b/core/src/ldml/ldml_processor.cpp @@ -6,6 +6,7 @@ */ #include +#include #include "ldml/ldml_processor.hpp" #include "state.hpp" #include "kmx_file.h" @@ -236,88 +237,16 @@ ldml_processor::process_event( // all other VKs { // Look up the key - const std::u16string str = keys.lookup(vk, modifier_state); + const std::u16string key_str = keys.lookup(vk, modifier_state); - if (str.empty()) { + if (key_str.empty()) { // no key was found, so pass the keystroke on to the Engine state->actions().push_invalidate_context(); state->actions().push_emit_keystroke(); break; // ----- commit and exit } - // found a string - push it into the context and actions - // we convert it here instead of using the emit_text() overload - // so that we don't have to reconvert it inside the transform code. - const std::u32string str32 = kmx::u16string_to_u32string(str); - - if (!transforms) { - // No transforms: just emit the string. - emit_text(state, str32); - } else { - // Process transforms here - /** - * a copy of the current/changed context, for transform use. - * - */ - std::u32string ctxtstr; - (void)context_to_string(state, ctxtstr); - // add the newly added key output to ctxtstr - ctxtstr.append(str32); - - /** the output buffer for transforms */ - std::u32string outputString; - - // apply the transform, get how much matched (at the end) - const size_t matchedContext = transforms->apply(ctxtstr, outputString); - - if (matchedContext == 0) { - // No match, just emit the original string - emit_text(state, str32); - } else { - // We have a match. - - ctxtstr.resize(ctxtstr.length() - str32.length()); - /** how many chars of the context we need to clear */ - auto charsToDelete = matchedContext - str32.length(); /* we don't need to clear the output of the current key */ - - /** how many context items need to be removed */ - size_t contextRemoved = 0; - for (auto c = state->context().rbegin(); charsToDelete > 0 && c != state->context().rend(); c++, contextRemoved++) { - /** last char of context */ - km_core_usv lastCtx = ctxtstr.back(); - uint8_t type = c->type; - assert(type == KM_CORE_BT_CHAR || type == KM_CORE_BT_MARKER); - if (type == KM_CORE_BT_CHAR) { - // single char, drop it - charsToDelete--; - assert(c->character == lastCtx); - ctxtstr.pop_back(); - state->actions().push_backspace(KM_CORE_BT_CHAR, lastCtx); // Cause prior char to be removed - } else if (type == KM_CORE_BT_MARKER) { - // it's a marker, 'worth' 3 uchars - assert(charsToDelete >= 3); - assert(lastCtx == c->marker); // end of list - charsToDelete -= 3; - // pop off the three-part sentinel string - ctxtstr.pop_back(); - ctxtstr.pop_back(); - ctxtstr.pop_back(); - // push a special backspace to delete the marker - state->actions().push_backspace(KM_CORE_BT_MARKER, c->marker); - } - } - // now, pop the right number of context items - for (size_t i = 0; i < contextRemoved; i++) { - // we don't pop during the above loop because the iterator gets confused - state->context().pop_back(); - } - // Now, add in the updated text. This will convert UC_SENTINEL, etc back to marker actions. - emit_text(state, outputString); - // If we needed it further. we could update ctxtstr here: - // ctxtstr.append(outputString); - // ... but it is no longer needed at this point. - } // end of transform match - } // end of processing transforms + process_key_string(state, key_str); } // end of processing a 'normal' vk } // end of switch // end of normal processing: commit and exit @@ -330,6 +259,103 @@ ldml_processor::process_event( return KM_CORE_STATUS_OK; } +void +ldml_processor::process_key_string(km_core_state *state, const std::u16string &key_str) const { + // We know that key_str is not empty per the caller. + assert(!key_str.empty()); + + // we convert the keys str to UTF-32 here instead of using the emit_text() overload + // so that we don't have to reconvert it inside the transform code. + std::u32string key_str32 = kmx::u16string_to_u32string(key_str); + assert(ldml::normalize_nfd(key_str32)); // TODO-LDML: else fail? + + // extract context string, in NFC + std::u32string old_ctxtstr_nfc; + (void)context_to_string(state, old_ctxtstr_nfc, false); + assert(ldml::normalize_nfc(old_ctxtstr_nfc)); // TODO-LDML: else fail? + + // context string in NFD + std::u32string ctxtstr; + (void)context_to_string(state, ctxtstr, true); // with markers + // add the newly added key output to ctxtstr + ctxtstr.append(key_str32); + assert(ldml::normalize_nfd(ctxtstr)); // TODO-LDML: else fail? + + /** transform output string */ + std::u32string outputString; + /** how many chars of the ctxtstr to replace */ + size_t matchedContext = 0; // zero if no transforms + + // begin modifications to the string + + if(transforms) { + matchedContext = transforms->apply(ctxtstr, outputString); + } else { + // no transforms, no output + } + + // drop last 'matchedContext': + ctxtstr.resize(ctxtstr.length() - matchedContext); + ctxtstr.append(outputString); // TODO-LDML: should be able to do a normalization-safe append here. + assert(ldml::normalize_nfd(ctxtstr)); // TODO-LDML: else fail? + + // Ok. We've done all the happy manipulations. + + /** NFC and no markers */ + std::u32string ctxtstr_cleanedup = ctxtstr; + // TODO-LDML: remove markers! + assert(ldml::normalize_nfc(ctxtstr_cleanedup)); // TODO-LDML: else fail? + + // find common prefix + auto ctxt_prefix = mismatch(old_ctxtstr_nfc.begin(), old_ctxtstr_nfc.end(), ctxtstr_cleanedup.begin(), ctxtstr_cleanedup.end()); + /** the part of the old str that changed */ + std::u32string old_ctxtstr_changed(ctxt_prefix.first,old_ctxtstr_nfc.end()); + std::u32string new_ctxtstr_changed(ctxt_prefix.second,ctxtstr_cleanedup.end()); + + // drop the old suffix. Note: this mutates old_ctxtstr_changed. + remove_text(state, old_ctxtstr_changed, old_ctxtstr_changed.length()); + assert(old_ctxtstr_changed.length() == 0); + emit_text(state, new_ctxtstr_changed); +} + +void +ldml_processor::remove_text(km_core_state *state, std::u32string &str, size_t length) { + /** how many context items need to be removed */ + size_t contextRemoved = 0; + for (auto c = state->context().rbegin(); length > 0 && c != state->context().rend(); c++, contextRemoved++) { + /** last char of context */ + km_core_usv lastCtx = str.back(); + uint8_t type = c->type; + assert(type == KM_CORE_BT_CHAR || type == KM_CORE_BT_MARKER); + if (type == KM_CORE_BT_CHAR) { + // single char, drop it + length--; + assert(c->character == lastCtx); + str.pop_back(); + state->actions().push_backspace(KM_CORE_BT_CHAR, c->character); // Cause prior char to be removed + } else if (type == KM_CORE_BT_MARKER) { + // it's a marker, 'worth' 3 uchars + assert(length >= 3); + assert(lastCtx == c->marker); // end of list + length -= 3; + // pop off the three-part sentinel string (in reverse order of course) + assert(str.back() == c->marker); // marker # + str.pop_back(); + assert(str.back() == LDML_MARKER_CODE); + str.pop_back(); + assert(str.back() == LDML_UC_SENTINEL); + str.pop_back(); + // push a special backspace to delete the marker + state->actions().push_backspace(KM_CORE_BT_MARKER, c->marker); + } + } + // now, pop the right number of context items + for (size_t i = 0; i < contextRemoved; i++) { + // we don't pop during the above loop because the iterator gets confused + state->context().pop_back(); + } +} + km_core_attr const & ldml_processor::attributes() const { return engine_attrs; } @@ -395,10 +421,10 @@ ldml_processor::emit_marker(km_core_state *state, KMX_DWORD marker_no) { } size_t -ldml_processor::context_to_string(km_core_state *state, std::u32string &str) { +ldml_processor::context_to_string(km_core_state *state, std::u32string &str, bool include_markers) { str.clear(); auto &cp = state->context(); - size_t ctxlen = 0; // TODO-LDML: is this needed? + size_t ctxlen = 0; // TODO-LDML: not used by callers? uint8_t last_type = KM_CORE_BT_UNKNOWN; for (auto c = cp.rbegin(); c != cp.rend(); c++, ctxlen++) { last_type = c->type; @@ -406,7 +432,9 @@ ldml_processor::context_to_string(km_core_state *state, std::u32string &str) { str.insert(0, 1, c->character); } else if (last_type == KM_CORE_BT_MARKER) { assert(km::core::kmx::is_valid_marker(c->marker)); - prepend_marker(str, c->marker); + if (include_markers) { + prepend_marker(str, c->marker); + } } else { break; } @@ -414,6 +442,5 @@ ldml_processor::context_to_string(km_core_state *state, std::u32string &str) { return ctxlen; // consumed the entire context buffer. } - } // namespace core } // namespace km diff --git a/core/src/ldml/ldml_processor.hpp b/core/src/ldml/ldml_processor.hpp index 31fc48ff646..79c6ae4ac4b 100644 --- a/core/src/ldml/ldml_processor.hpp +++ b/core/src/ldml/ldml_processor.hpp @@ -93,6 +93,15 @@ namespace core { static void emit_text(km_core_state *state, km_core_usv ch); /** emit a marker */ static void emit_marker(km_core_state *state, KMX_DWORD marker); + /** + * Delete text from the state. + * @param str string with text to remove, from the end + * @param length number of chars from the end of str to drop + */ + static void remove_text(km_core_state *state, std::u32string &str, size_t length); + + /** process a typed key */ + void process_key_string(km_core_state *state, const std::u16string &key_str) const; /** * add the string+marker portion of the context to the beginning of str. @@ -100,7 +109,7 @@ namespace core { * Convert markers into the UC_SENTINEL format. * @return the number of context items consumed */ - static size_t context_to_string(km_core_state *state, std::u32string &str); + static size_t context_to_string(km_core_state *state, std::u32string &str, bool include_markers = true); /** prepend the marker string in UC_SENTINEL format to the str */ inline static void prepend_marker(std::u32string &str, KMX_DWORD marker); diff --git a/core/src/ldml/ldml_transforms.cpp b/core/src/ldml/ldml_transforms.cpp index 10ba6250e06..6ae8feafc87 100644 --- a/core/src/ldml/ldml_transforms.cpp +++ b/core/src/ldml/ldml_transforms.cpp @@ -12,7 +12,7 @@ #include "kmx/kmx_xstring.h" #ifndef assert -#define assert(x) // TODO-LDML +#define assert(x) ((void)0) #endif namespace km { @@ -403,25 +403,29 @@ transform_entry::transform_entry(const transform_entry &other) transform_entry::transform_entry(const std::u32string &from, const std::u32string &to) : fFrom(from), fTo(to), fFromPattern(nullptr), fMapFromStrId(), fMapToStrId(), fMapFromList(), fMapToList() { - assert(!fFrom.empty()); // TODO-LDML: should not happen? + assert(!fFrom.empty()); init(); } -// TODO-LDML: How do we return errors from here? transform_entry::transform_entry( const std::u32string &from, const std::u32string &to, KMX_DWORD mapFrom, KMX_DWORD mapTo, - const kmx::kmx_plus &kplus) + const kmx::kmx_plus &kplus, + bool &valid) : fFrom(from), fTo(to), fFromPattern(nullptr), fMapFromStrId(mapFrom), fMapToStrId(mapTo) { + if (!valid) + return; // exit early assert(!fFrom.empty()); // TODO-LDML: should not happen? assert((fMapFromStrId == 0) == (fMapToStrId == 0)); // we have both or we have neither. assert(kplus.strs != nullptr); assert(kplus.vars != nullptr); assert(kplus.elem != nullptr); - init(); + if(!init()) { + valid = false; + } // setup mapFrom if (fMapFromStrId != 0) { @@ -456,18 +460,23 @@ transform_entry::transform_entry( } } -void +bool transform_entry::init() { - if (!fFrom.empty()) { - // TODO-LDML: if we have mapFrom, may need to do other processing. - const std::u16string patstr = km::core::kmx::u32string_to_u16string(fFrom); - UErrorCode status = U_ZERO_ERROR; - /* const */ icu::UnicodeString patustr = icu::UnicodeString(patstr.data(), (int32_t)patstr.length()); - // add '$' to match to end - patustr.append(u'$'); - fFromPattern.reset(icu::RegexPattern::compile(patustr, 0, status)); - assert(U_SUCCESS(status)); // TODO-LDML: may be best to propagate status up ^^ + if (fFrom.empty()) { + return false; } + // TODO-LDML: if we have mapFrom, may need to do other processing. + const std::u16string patstr = km::core::kmx::u32string_to_u16string(fFrom); + UErrorCode status = U_ZERO_ERROR; + /* const */ icu::UnicodeString patustr_raw = icu::UnicodeString(patstr.data(), (int32_t)patstr.length()); + // add '$' to match to end + patustr_raw.append(u'$'); + icu::UnicodeString patustr; + const icu::Normalizer2 *nfd = icu::Normalizer2::getNFDInstance(status); + // NFD normalize on pattern creation + nfd->normalize(patustr_raw, patustr, status); + fFromPattern.reset(icu::RegexPattern::compile(patustr, 0, status)); + return (UASSERT_SUCCESS(status)); } size_t @@ -480,7 +489,7 @@ transform_entry::apply(const std::u32string &input, std::u32string &output) cons icu::UnicodeString matchustr = icu::UnicodeString(matchstr.data(), (int32_t)matchstr.length()); // TODO-LDML: create a new Matcher every time. These could be cached and reset. std::unique_ptr matcher(fFromPattern->matcher(matchustr, status)); - assert(U_SUCCESS(status)); + UASSERT_SUCCESS(status); if (!matcher->find(status)) { // i.e. matches somewhere, in this case at end of str return 0; // no match @@ -490,7 +499,7 @@ transform_entry::apply(const std::u32string &input, std::u32string &output) cons // TODO-LDML: if we had an underlying UText this would be simpler. int32_t matchStart = matcher->start(status); int32_t matchEnd = matcher->end(status); - assert(U_SUCCESS(status)); + UASSERT_SUCCESS(status); // extract.. const icu::UnicodeString substr = matchustr.tempSubStringBetween(matchStart, matchEnd); // preflight to UTF-32 to get length @@ -517,7 +526,7 @@ transform_entry::apply(const std::u32string &input, std::u32string &output) cons // we actually need the group(1) string here. // this is only the content in parenthesis () icu::UnicodeString group1 = matcher->group(1, status); - assert(U_SUCCESS(status)); // TODO-LDML: could be a malformed from pattern + UASSERT_SUCCESS(status); // TODO-LDML: could be a malformed from pattern // now, how long is group1 in UTF-32, hmm? UErrorCode preflightStatus = U_ZERO_ERROR; // throwaway status auto group1Len = group1.toUTF32(nullptr, 0, preflightStatus); @@ -525,7 +534,7 @@ transform_entry::apply(const std::u32string &input, std::u32string &output) cons assert(s != nullptr); // TODO-LDML: OOM // convert substr.toUTF32((UChar32 *)s, group1Len + 1, status); - assert(U_SUCCESS(status)); + UASSERT_SUCCESS(status); std::u32string match32(s, group1Len); // taken from just group1 // clean up buffer delete [] s; @@ -545,12 +554,21 @@ transform_entry::apply(const std::u32string &input, std::u32string &output) cons rustr = icu::UnicodeString(rstr.data(), (int32_t)rstr.length()); // and we return to the regular code flow. } + const icu::Normalizer2 *nfd = icu::Normalizer2::getNFDInstance(status); + icu::UnicodeString rustr2; + nfd->normalize(rustr, rustr2, status); + UASSERT_SUCCESS(status); // here we replace the match output. - icu::UnicodeString entireOutput = matcher->replaceFirst(rustr, status); - assert(U_SUCCESS(status)); // TODO-LDML: could fail here due to bad input (syntax err) + icu::UnicodeString entireOutput = matcher->replaceFirst(rustr2, status); + UASSERT_SUCCESS(status); // TODO-LDML: could fail here due to bad input (syntax err) // entireOutput includes all of 'input', but modified. Need to substring it. - icu::UnicodeString outu = entireOutput.tempSubString(matchStart); + icu::UnicodeString outu_raw = entireOutput.tempSubString(matchStart); + + // normalize the replaced string + icu::UnicodeString outu; + nfd->normalize(outu_raw, outu, status); + UASSERT_SUCCESS(status); // Special case if there's no output, save some allocs if (outu.length() == 0) { @@ -565,7 +583,7 @@ transform_entry::apply(const std::u32string &input, std::u32string &output) cons assert(s != nullptr); // convert outu.toUTF32((UChar32 *)s, out32len + 1, status); - assert(U_SUCCESS(status)); + UASSERT_SUCCESS(status); output.assign(s, out32len); // now, build a u32string std::u32string out32(s, out32len); @@ -754,35 +772,34 @@ transforms::load( const kmx::kmx_plus &kplus, const core::kmx::COMP_KMXPLUS_TRAN *tran, const core::kmx::COMP_KMXPLUS_TRAN_Helper &tranHelper) { + bool valid = true; if (tran == nullptr) { DebugLog("for tran: tran is null"); - assert(false); - return nullptr; - } - if (!tranHelper.valid()) { + valid = false; + } else if (!tranHelper.valid()) { DebugLog("for tran: tranHelper is invalid"); - assert(false); - return nullptr; - } - if (nullptr == kplus.elem) { + valid = false; + } else if (nullptr == kplus.elem) { DebugLog("for tran: kplus.elem == nullptr"); - assert(false); - return nullptr; - } - if (nullptr == kplus.strs) { + valid = false; + } else if (nullptr == kplus.strs) { DebugLog("for tran: kplus.strs == nullptr"); // need a string table to get strings - assert(false); - return nullptr; - } - if (nullptr == kplus.vars) { + valid = false; + } else if (nullptr == kplus.vars) { DebugLog("for tran: kplus.vars == nullptr"); // need a vars table to get maps - assert(false); + valid = false; + } + + assert(valid); + if (!valid) { return nullptr; } // with that out of the way, let's set it up - transforms *transforms = new ldml::transforms(); + std::unique_ptr transforms; + + transforms.reset(new ldml::transforms()); for (KMX_DWORD groupNumber = 0; groupNumber < tran->groupCount; groupNumber++) { const kmx::COMP_KMXPLUS_TRAN_GROUP *group = tranHelper.getGroup(groupNumber); @@ -798,7 +815,15 @@ transforms::load( const std::u32string toStr = kmx::u16string_to_u32string(kplus.strs->get(element->to)); KMX_DWORD mapFrom = element->mapFrom; // copy, because of alignment KMX_DWORD mapTo = element->mapTo; // copy, because of alignment - newGroup.emplace_back(fromStr, toStr, mapFrom, mapTo, kplus); // creating a transform_entry + assert(!fromStr.empty()); + if (fromStr.empty()) { + valid = false; + } + newGroup.emplace_back(fromStr, toStr, mapFrom, mapTo, kplus, valid); // creating a transform_entry + assert(valid); + if(!valid) { + return nullptr; + } } transforms->addGroup(newGroup); } else if (group->type == LDML_TRAN_GROUP_TYPE_REORDER) { @@ -828,7 +853,61 @@ transforms::load( return nullptr; } } - return transforms; + assert(valid); + if (!valid) { + return nullptr; + } else { + return transforms.release(); + } +} + +// string manipulation + +bool normalize_nfd(std::u32string &str) { + std::u16string rstr = km::core::kmx::u32string_to_u16string(str); + if(!normalize_nfd(rstr)) { + return false; + } else { + str = km::core::kmx::u16string_to_u32string(rstr); + return true; + } +} + +/** internal function to normalize with a specified mode */ +static bool normalize(const icu::Normalizer2 *n, std::u16string &str, UErrorCode &status) { + UASSERT_SUCCESS(status); + assert(n != nullptr); + icu::UnicodeString dest; + icu::UnicodeString src = icu::UnicodeString(str.data(), (int32_t)str.length()); + n->normalize(src, dest, status); + if (UASSERT_SUCCESS(status)) { + str.assign(dest.getBuffer(), dest.length()); + } + return U_SUCCESS(status); +} + +bool normalize_nfd(std::u16string &str) { + UErrorCode status = U_ZERO_ERROR; + const icu::Normalizer2 *nfd = icu::Normalizer2::getNFDInstance(status); + UASSERT_SUCCESS(status); + return normalize(nfd, str, status); +} + +bool normalize_nfc(std::u32string &str) { + std::u16string rstr = km::core::kmx::u32string_to_u16string(str); + if(!normalize_nfc(rstr)) { + return false; + } else { + str = km::core::kmx::u16string_to_u32string(rstr); + return true; + } +} + +bool normalize_nfc(std::u16string &str) { + UErrorCode status = U_ZERO_ERROR; + const icu::Normalizer2 *nfc = icu::Normalizer2::getNFCInstance(status); + UASSERT_SUCCESS(status); + return normalize(nfc, str, status); } } // namespace ldml diff --git a/core/src/ldml/ldml_transforms.hpp b/core/src/ldml/ldml_transforms.hpp index d260423c331..044520ade5e 100644 --- a/core/src/ldml/ldml_transforms.hpp +++ b/core/src/ldml/ldml_transforms.hpp @@ -13,6 +13,7 @@ #include #include #include +#include "debuglog.h" #if !defined(HAVE_ICU4C) #error icu4c is required for this code @@ -25,11 +26,24 @@ #include "unicode/unistr.h" #include "unicode/regex.h" #include "unicode/utext.h" +#include "unicode/normalizer2.h" namespace km { namespace core { namespace ldml { +/** @returns true on success */ +inline bool uassert_success(const char *file, int line, const char *function, UErrorCode status) { + if (U_FAILURE(status)) { + DebugLog2(file, line, function, "U_FAILURE(%s)", u_errorName(status)); + return false; + } else { + return true; + } +} + +#define UASSERT_SUCCESS(status) assert(U_SUCCESS(status)), uassert_success(__FILE__, __LINE__, __FUNCTION__, status) + using km::core::kmx::SimpleUSet; /** @@ -94,7 +108,8 @@ class transform_entry { const std::u32string &to, KMX_DWORD mapFrom, KMX_DWORD mapTo, - const kmx::kmx_plus &kplus); + const kmx::kmx_plus &kplus, + bool &valid); /** * If matching, apply the match to the output string @@ -113,8 +128,8 @@ class transform_entry { const KMX_DWORD fMapToStrId; std::deque fMapFromList; std::deque fMapToList; - /** Internal function to setup pattern string */ - void init(); + /** Internal function to setup pattern string @returns true on success */ + bool init(); /** @returns the index of the item in the fMapFromList list, or -1 */ int32_t findIndexFrom(const std::u32string &match) const; public: @@ -270,6 +285,17 @@ class transforms { const core::kmx::COMP_KMXPLUS_TRAN_Helper &tranHelper); }; +// string routines + +/** Normalize a u32string inplace to NFD. @return false on failure */ +bool normalize_nfd(std::u32string &str); +/** Normalize a u16string inplace to NFD. @return false on failure */ +bool normalize_nfd(std::u16string &str); +/** Normalize a u32string inplace to NFC. @return false on failure */ +bool normalize_nfc(std::u32string &str); +/** Normalize a u16string inplace to NFC. @return false on failure */ +bool normalize_nfc(std::u16string &str); + } // namespace ldml } // namespace core } // namespace km diff --git a/core/src/version.rc b/core/src/version.rc index d129fa52f16..af2a12eb587 100644 --- a/core/src/version.rc +++ b/core/src/version.rc @@ -23,7 +23,7 @@ VALUE "InternalName", "Keyman Core" VALUE "LegalCopyright", "© SIL International" VALUE "LegalTrademarks", "" - VALUE "OriginalFilename", "KEYMANCORE1-0.DLL" + VALUE "OriginalFilename", "KEYMANCORE1.DLL" VALUE "ProductName", "Keyman Core" VALUE "ProductVersion", KM_CORE_VERSION_STRING VALUE "Comments", "" diff --git a/core/subprojects/packagefiles/icu/meson.build b/core/subprojects/packagefiles/icu/meson.build index 128fc9fbf97..7b329e27df7 100644 --- a/core/subprojects/packagefiles/icu/meson.build +++ b/core/subprojects/packagefiles/icu/meson.build @@ -35,7 +35,7 @@ uconfig.set('U_ENABLE_DYLOAD', 0) # no DLL uconfig.set('U_CHECK_DYLOAD', 0) # no DLL uconfig.set('UCONFIG_NO_FILE_IO', 1) uconfig.set('UCONFIG_NO_LEGACY_CONVERSION', 1) # turn off file based codepage conversion -uconfig.set('UCONFIG_NO_NORMALIZATION', 1) # TODO-LDML: may want this +uconfig.set('UCONFIG_NO_NORMALIZATION', 0) uconfig.set('UCONFIG_NO_BREAK_ITERATION', 1) # TODO-LDML: may want this uconfig.set('UCONFIG_NO_IDNA', 1) uconfig.set('UCONFIG_NO_COLLATION', 1) diff --git a/core/tests/unit/ldml/keyboards/k_006_backspace-test.xml b/core/tests/unit/ldml/keyboards/k_006_backspace-test.xml new file mode 100644 index 00000000000..7ef89381aaa --- /dev/null +++ b/core/tests/unit/ldml/keyboards/k_006_backspace-test.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/core/tests/unit/ldml/keyboards/k_006_backspace.xml b/core/tests/unit/ldml/keyboards/k_006_backspace.xml index b3f126eeabe..940da405e15 100644 --- a/core/tests/unit/ldml/keyboards/k_006_backspace.xml +++ b/core/tests/unit/ldml/keyboards/k_006_backspace.xml @@ -1,12 +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 new file mode 100644 index 00000000000..025fe36a483 --- /dev/null +++ b/core/tests/unit/ldml/keyboards/k_008_transform_norm-test.xml @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/tests/unit/ldml/keyboards/k_008_transform_norm.xml b/core/tests/unit/ldml/keyboards/k_008_transform_norm.xml new file mode 100644 index 00000000000..b66ff1a4b6a --- /dev/null +++ b/core/tests/unit/ldml/keyboards/k_008_transform_norm.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/tests/unit/ldml/keyboards/meson.build b/core/tests/unit/ldml/keyboards/meson.build index 8c1ccff573b..89077fabaca 100644 --- a/core/tests/unit/ldml/keyboards/meson.build +++ b/core/tests/unit/ldml/keyboards/meson.build @@ -21,7 +21,6 @@ tests_without_testdata = [ 'k_003_transform', 'k_004_tinyshift', 'k_005_modbittest', - # 'k_006_backspace', ## not quite there yet. TODO-LDML 'k_010_mt', 'k_011_mt_iso', 'k_100_keytest', @@ -33,7 +32,9 @@ tests_without_testdata = [ # These tests have a k_001_tiny-test.xml file as well. tests_with_testdata = [ 'k_001_tiny', + 'k_006_backspace', 'k_007_transform_rgx', + 'k_008_transform_norm', 'k_020_fr', # TODO-LDML: move to cldr above (fix vkey) 'k_200_reorder_nod_Lana', 'k_210_marker', diff --git a/core/tests/unit/ldml/ldml.cpp b/core/tests/unit/ldml/ldml.cpp index 20dd4315765..5c2b53fb841 100644 --- a/core/tests/unit/ldml/ldml.cpp +++ b/core/tests/unit/ldml/ldml.cpp @@ -213,6 +213,7 @@ run_test(const km::core::path &source, const km::core::path &compiled, km::tests // Run through actions, applying output for each event for (test_source.next_action(action); action.type != km::tests::LDML_ACTION_DONE; test_source.next_action(action)) { + // handle backspace here if (action.type == km::tests::LDML_ACTION_KEY_EVENT) { auto &p = action.k; std::cout << "- key action: 0x" << std::hex << p.vk << "/modifier 0x" << p.modifier_state << std::dec << std::endl; @@ -294,9 +295,12 @@ run_test(const km::core::path &source, const km::core::path &compiled, km::tests /** * Run all tests for this keyboard */ -int run_all_tests(const km::core::path &source, const km::core::path &compiled) { - std::cout << "source file = " << source << std::endl - << "compiled file = " << compiled << std::endl; +int run_all_tests(const km::core::path &source, const km::core::path &compiled, const std::string &filter) { + std::wcout << console_color::fg(console_color::BLUE) << "source file = " << source << std::endl + << "compiled file = " << compiled << console_color::reset() << std::endl; + if(!filter.empty()) { + std::wcout << "Running only tests matching (substring search): " << filter.c_str() << std::endl; + } km::tests::LdmlEmbeddedTestSource embedded_test_source; @@ -304,9 +308,15 @@ int run_all_tests(const km::core::path &source, const km::core::path &compiled) int embedded_result = embedded_test_source.load_source(source); - if (embedded_result == 0) { + if (!filter.empty()) { + // Always skip the embedded test if there's a filter. + std::wcout << console_color::fg(console_color::YELLOW) << "SKIP: " << source.name() << " (embedded)" << console_color::reset() + << std::endl; + embedded_result = 0; // no error + } else if (embedded_result == 0) { // embedded loaded OK, try it - std::cout << "TEST: " << source.name() << " (embedded)" << std::endl; + std::wcout << console_color::fg(console_color::BLUE) << console_color::bold() << "TEST: " << source.name() << " (embedded)" + << console_color::reset() << std::endl; embedded_result = run_test(source, compiled, embedded_test_source); if (embedded_result != 0) { failures.push_back("in-XML (@@ comment) embedded test failed"); @@ -324,29 +334,55 @@ int run_all_tests(const km::core::path &source, const km::core::path &compiled) if (json_result != -1) { const km::tests::JsonTestMap& json_tests = json_factory.get_tests(); + size_t skip_count = 0; assert(json_tests.size() > 0); // Loop over all tests for (const auto& n : json_tests) { - std::cout << "TEST: " << json_path.stem() << "/" << n.first << std::endl; + const auto test_name = n.first; + auto qq = test_name.find(filter); + if (filter == "--list" || (qq == std::string::npos)) { + skip_count ++; + std::wcout << console_color::fg(console_color::YELLOW) << "SKIP: " << json_path.stem().c_str() << "/" << console_color::bold() << n.first.c_str() << console_color::reset() << std::endl; + continue; + } + std::wcout << console_color::fg(console_color::BLUE) << "TEST: " << json_path.stem().c_str() << "/" << console_color::bold() << n.first.c_str() << console_color::reset() << std::endl; int sub_test = run_test(source, compiled, *n.second); if (sub_test != 0) { - std::cout << " FAIL: " << json_path.stem() << "/" << n.first << std::endl; + std::wcout << console_color::fg(console_color::BRIGHT_RED) << "FAIL: " << json_path.stem() << "/" << console_color::bold() << n.first.c_str() + << console_color::reset() << std::endl; failures.push_back(json_path.stem() + "/" + n.first); json_result = sub_test; // set to last failure } else { - std::cout << " PASS: " << json_path.stem() << "/" << n.first << std::endl; + std::wcout << console_color::fg(console_color::GREEN) << "PASS: " << console_color::reset() << json_path.stem() + << "/" << console_color::bold() << n.first.c_str() << std::endl; } } - std::cout << " " << json_tests.size() << " JSON test(s) in " << json_path.stem() << std::endl; + auto all_count = json_tests.size(); + auto fail_count = failures.size(); + auto pass_count = all_count - fail_count - skip_count; + if (pass_count > 0) { + std::wcout << console_color::fg(console_color::GREEN) << " +" << pass_count; + } + if (fail_count > 0) { + std::wcout << console_color::fg(console_color::BRIGHT_RED) << + " -" << fail_count; + } + if (skip_count > 0) { + std::wcout << console_color::fg(console_color::YELLOW) << + " (skipped " << skip_count << ")"; + } + std::wcout << console_color::reset() << " of " << all_count << " JSON tests in " + << json_path.stem() << std::endl; } // OK. + std::wcout << console_color::fg(console_color::YELLOW) << "---- Summary of " << source.name() << " ----" << console_color::reset() << std::endl; if (embedded_result == -1) { - std::cout << "Note: No embedded test." << std::endl; + std::wcout << console_color::fg(console_color::YELLOW) << "Note: No embedded test." << console_color::reset() << std::endl; } if (json_result == -1) { - std::cout << "Note: No json test." << std::endl; + std::wcout << console_color::fg(console_color::YELLOW) << "Note: No json test." << console_color::reset() << std::endl; } // if both are missing, that's an error in itself. @@ -358,7 +394,7 @@ int run_all_tests(const km::core::path &source, const km::core::path &compiled) // recap the failures if (failures.size() > 0) { for (const auto& f : failures) { - std::cerr << "failure summary: " << f << std::endl; + std::wcerr << console_color::fg(console_color::RED) << "failed: " << f.c_str() << console_color::reset() << std::endl; } return -1; } else { @@ -371,10 +407,11 @@ int run_all_tests(const km::core::path &source, const km::core::path &compiled) constexpr const auto help_str = "\ -ldml [--color] \n\ +ldml [--color] [ | --list ]\n\ help:\n\ -\tKMN_FILE:\tThe ldml test file for the keyboard under test.\n\ -\tKMX_FILE:\tThe corresponding compiled kmx file.\n"; +\tLDML_FILE:\tThe .xml file for the keyboard under test.\n\ +\tKMX_FILE:\tThe corresponding compiled kmx file.\n\ +\tTEST_FILTER:\tIf present, only run json tests containing the filter substring. --list will list all tests\n"; } // namespace @@ -387,22 +424,30 @@ int error_args() { int main(int argc, char *argv[]) { int first_arg = 1; - if (argc < 3) { + if ((argc - first_arg) < 2) { // if < 2 remaining args return error_args(); } - auto arg_color = std::string(argv[1]) == "--color"; + auto arg_color = std::string(argv[first_arg]) == "--color"; if(arg_color) { first_arg++; - if(argc < 4) { - return error_args(); - } } console_color::enabled = console_color::isaterminal() || arg_color; - int rc = run_all_tests(argv[first_arg], argv[first_arg + 1]); + if ((argc - first_arg) < 2) { + return error_args(); + } + const km::kbp::path ldml_file = argv[first_arg++]; + const km::kbp::path kmx_file = argv[first_arg++]; + + std::string filter; // default to 'all tests' + if ((argc - first_arg) >= 1) { + filter = argv[first_arg++]; + } + + int rc = run_all_tests(ldml_file, kmx_file, filter); if (rc != EXIT_SUCCESS) { - std::cerr << "FAILED" << std::endl; + std::wcerr << console_color::fg(console_color::BRIGHT_RED) << "FAILED" << console_color::reset() << std::endl; rc = EXIT_FAILURE; } return rc; diff --git a/core/tests/unit/ldml/ldml_test_source.cpp b/core/tests/unit/ldml/ldml_test_source.cpp index 089aaae2ac4..81a91da0fa8 100644 --- a/core/tests/unit/ldml/ldml_test_source.cpp +++ b/core/tests/unit/ldml/ldml_test_source.cpp @@ -24,6 +24,7 @@ #include #include "ldml/keyman_core_ldml.h" #include "ldml/ldml_processor.hpp" +#include "ldml/ldml_transforms.hpp" #include "path.hpp" #include "state.hpp" @@ -461,35 +462,39 @@ LdmlJsonTestSource::next_action(ldml_action &fillin) { action_index++; auto action = data["/actions"_json_pointer].at(action_index); + // load up several common attributes + auto type = action["/type"_json_pointer]; + auto result = action["/result"_json_pointer]; + auto key = action["/key"_json_pointer]; + auto to = action["/to"_json_pointer]; // is it a check event? - auto as_check = action["/check/result"_json_pointer]; - if (as_check.is_string()) { + if (type == "check") { fillin.type = LDML_ACTION_CHECK_EXPECTED; - fillin.string = LdmlTestSource::parse_u8_source_string(as_check.get()); + fillin.string = LdmlTestSource::parse_u8_source_string(result.get()); + assert(km::kbp::ldml::normalize_nfc(fillin.string)); return; - } - - // is it a keystroke by id? - auto as_key = action["/keystroke/key"_json_pointer]; - if (as_key.is_string()) { + } else if (type == "keystroke") { fillin.type = LDML_ACTION_KEY_EVENT; - auto keyId = LdmlTestSource::parse_u8_source_string(as_key.get()); + auto keyId = LdmlTestSource::parse_u8_source_string(key.get()); // now, look up the key set_key_from_id(fillin.k, keyId); return; - } - // TODO-LDML: handle gesture, etc - - auto as_emit = action["/emit/to"_json_pointer]; - if (as_emit.is_string()) { + } else if (type == "emit") { fillin.type = LDML_ACTION_EMIT_STRING; - fillin.string = LdmlTestSource::parse_u8_source_string(as_emit.get()); + fillin.string = LdmlTestSource::parse_u8_source_string(to.get()); + assert(km::kbp::ldml::normalize_nfc(fillin.string)); + return; + } else if (type == "backspace") { + // backspace is handled as a key event + fillin.type = LDML_ACTION_KEY_EVENT; + fillin.k.modifier_state = 0; + fillin.k.vk = KM_CORE_VKEY_BKSP; return; } // TODO-LDML: error passthrough - std::cerr << "TODO-LDML: Error, unknown/unhandled action: " << action << std::endl; + std::cerr << "TODO-LDML: Error, unknown/unhandled action: " << type << std::endl; fillin.type = LDML_ACTION_DONE; } @@ -502,7 +507,7 @@ int LdmlJsonTestSource::load(const nlohmann::json &data) { this->data = data; // TODO-LDML auto startContext = data["/startContext/to"_json_pointer]; context = LdmlTestSource::parse_u8_source_string(startContext); - + assert(km::kbp::ldml::normalize_nfc(context)); return 0; } @@ -704,7 +709,9 @@ int LdmlJsonTestSourceFactory::load(const km::core::path &compiled, const km::co auto info_author = data["/keyboardTest3/info/author"_json_pointer].get(); auto info_name = data["/keyboardTest3/info/name"_json_pointer].get(); // TODO-LDML: store these elsewhere? - std::cout << "JSON: reading " << info_name << " test of " << info_keyboard << " by " << info_author << std::endl; + std::wcout << console_color::fg(console_color::BLUE) << "test file = " << path.name().c_str() << console_color::reset() << std::endl; + std::wcout << console_color::fg(console_color::YELLOW) << info_name.c_str() << "/ " << console_color::reset() + << " test: " << info_keyboard.c_str() << " author: " << info_author.c_str() << std::endl; auto all_tests = data["/keyboardTest3/tests"_json_pointer]; assert_or_return((!all_tests.empty()) && (all_tests.size() > 0)); // TODO-LDML: can be empty if repertoire only? @@ -715,7 +722,7 @@ int LdmlJsonTestSourceFactory::load(const km::core::path &compiled, const km::co auto test_name = test["/name"_json_pointer].get(); std::string test_path; test_path.append(info_name).append("/tests/").append(tests_name).append("/").append(test_name); - std::cout << "JSON: reading " << info_name << "/" << test_path << std::endl; + // std::cout << "JSON: reading " << info_name << "/" << test_path << std::endl; std::unique_ptr subtest(new LdmlJsonTestSource(test_path, kmxplus.get())); assert_or_return(subtest->load(test) == 0); diff --git a/developer/src/kmc-ldml/test/fixtures/test-fr.json b/developer/src/kmc-ldml/test/fixtures/test-fr.json index 895360dd85f..9710e3758a4 100644 --- a/developer/src/kmc-ldml/test/fixtures/test-fr.json +++ b/developer/src/kmc-ldml/test/fixtures/test-fr.json @@ -29,44 +29,43 @@ }, "actions": [ { - "keystroke": { - "key": "s" - } + "type": "keystroke", + "key": "s" }, { - "check": { - "result": "abc\\u0022...s" - } + "type": "check", + "result": "abc\\u0022...s" }, { - "keystroke": { - "key": "t" - } + "type": "keystroke", + "key": "t" }, { - "check": { - "result": "abc\\u0022...st" - } + "type": "check", + "result": "abc\\u0022...st" }, { - "keystroke": { - "key": "u" - } + "type": "keystroke", + "key": "u" }, { - "check": { - "result": "abc\\u0022...stu" - } + "type": "check", + "result": "abc\\u0022...stu" }, { - "emit": { - "to": "v" - } + "type": "emit", + "to": "v" }, { - "check": { - "result": "abc\\u0022...stuv" - } + "type": "check", + "result": "abc\\u0022...stuv" + }, + { + "type": "backspace" + }, + { + "type": "check", + "result": "abc\\u0022...stu" } ] } diff --git a/developer/src/kmc-ldml/test/fixtures/test-fr.xml b/developer/src/kmc-ldml/test/fixtures/test-fr.xml index cfd63c4f758..519140174fb 100644 --- a/developer/src/kmc-ldml/test/fixtures/test-fr.xml +++ b/developer/src/kmc-ldml/test/fixtures/test-fr.xml @@ -1,6 +1,7 @@ + @@ -17,6 +18,8 @@ + + diff --git a/developer/src/kmc-ldml/test/helpers/index.ts b/developer/src/kmc-ldml/test/helpers/index.ts index db0704bf75f..c919e729ac0 100644 --- a/developer/src/kmc-ldml/test/helpers/index.ts +++ b/developer/src/kmc-ldml/test/helpers/index.ts @@ -37,7 +37,7 @@ export const compilerTestCallbacks = new TestCompilerCallbacks(); export const compilerTestOptions: LdmlCompilerOptions = { readerOptions: { - importsPath: fileURLToPath(LDMLKeyboardXMLSourceFileReader.defaultImportsURL) + importsPath: fileURLToPath(new URL(...LDMLKeyboardXMLSourceFileReader.defaultImportsURL)) } }; diff --git a/developer/src/kmc-ldml/test/test-testdata-e2e.ts b/developer/src/kmc-ldml/test/test-testdata-e2e.ts index 3b706fd4cc3..eea3631759d 100644 --- a/developer/src/kmc-ldml/test/test-testdata-e2e.ts +++ b/developer/src/kmc-ldml/test/test-testdata-e2e.ts @@ -9,6 +9,7 @@ describe('testdata-tests', function() { it('should-build-testdata-fixtures', async function() { // Let's build test-fr.json // It should match test-fr.json (built from test-fr.xml) + // Note! There's another test case against similar data, in common/web/types/test/ldml-keyboard/test-ldml-keyboard-testdata-reader.ts const inputFilename = makePathToFixture('test-fr.xml'); const jsonFilename = makePathToFixture('test-fr.json'); @@ -19,6 +20,6 @@ describe('testdata-tests', function() { const jsonData = JSON.parse(readFileSync(jsonFilename, 'utf-8')); - assert.deepEqual(testData, jsonData); + assert.deepEqual(testData, jsonData, 'parsed +test-fr.xml should match -test-fr.json'); }); }); diff --git a/developer/src/kmc/src/commands/buildClasses/BuildLdmlKeyboard.ts b/developer/src/kmc/src/commands/buildClasses/BuildLdmlKeyboard.ts index 19db56db99a..1b0c2b22e5c 100644 --- a/developer/src/kmc/src/commands/buildClasses/BuildLdmlKeyboard.ts +++ b/developer/src/kmc/src/commands/buildClasses/BuildLdmlKeyboard.ts @@ -48,7 +48,7 @@ async function buildLdmlKeyboardToMemory(inputFilename: string, callbacks: Compi ...defaultCompilerOptions, ...options, readerOptions: { - importsPath: fileURLToPath(LDMLKeyboardXMLSourceFileReader.defaultImportsURL) + importsPath: fileURLToPath(new URL(...LDMLKeyboardXMLSourceFileReader.defaultImportsURL)) } }; diff --git a/developer/src/kmc/src/commands/buildTestData/index.ts b/developer/src/kmc/src/commands/buildTestData/index.ts index 97650d272ae..834112bc130 100644 --- a/developer/src/kmc/src/commands/buildTestData/index.ts +++ b/developer/src/kmc/src/commands/buildTestData/index.ts @@ -14,7 +14,7 @@ export function buildTestData(infile: string, _options: any, commander: any) { saveDebug: false, shouldAddCompilerVersion: false, readerOptions: { - importsPath: fileURLToPath(LDMLKeyboardXMLSourceFileReader.defaultImportsURL) + importsPath: fileURLToPath(new URL(...LDMLKeyboardXMLSourceFileReader.defaultImportsURL)) } }; diff --git a/docs/build/macos.md b/docs/build/macos.md index f5ff0409640..aa1eea34bfd 100644 --- a/docs/build/macos.md +++ b/docs/build/macos.md @@ -1,6 +1,8 @@ # Setup your Keyman build environment on macOS -This document describes prerequisites and tools required for building various Keyman projects on macOS. Each project will also have additional notes linked below. +This document describes prerequisites and tools required for building various +Keyman projects on macOS. Each project will also have additional notes linked +below. ## Target Projects @@ -10,6 +12,7 @@ On macOS, you can build the following projects: * Keyman for iOS ([additional details](../../ios/README.md)) * Keyman for macOS ([additional details](../../mac/README.md)) * KeymanWeb ([additional details](../../web/README.md)) +* Keyman Developer Command Line Tools (kmc) The following libraries can also be built: @@ -20,19 +23,24 @@ The following projects **cannot** be built on macOS: * Keyman for Linux * Keyman for Windows -* Keyman Developer +* Keyman Developer (IDE component) ## System Requirements * Minimum macOS version: macOS Catalina 10.15 or Big Sur 11.0 -**Note:** to make a fully M1-compatible release build of Keyman for macOS (for the setup Applescript), Big Sur 11.0 is required, as osacompile on earlier versions does not build for arm64 (M1). The build will still work on earlier versions, but the installer won't be able to run on M1 Macs that do not have Rosetta 2 installed. +**Note:** to make a fully M1-compatible release build of Keyman for macOS (for +the setup Applescript), Big Sur 11.0 is required, as osacompile on earlier +versions does not build for arm64 (M1). The build will still work on earlier +versions, but the installer won't be able to run on M1 Macs that do not have +Rosetta 2 installed. ## Prerequisites Many dependencies are only required for specific projects. -* XCode (iOS, macOS) 12.4 or later +* XCode (iOS, macOS) 12.4 or later is needed only for Keyman for macOS and Keyman + for iOS. * Install from App Store * Accept the Xcode license: `sudo xcodebuild -license accept` @@ -48,73 +56,83 @@ These dependencies are also listed below if you'd prefer to install manually. ## Shared Dependencies -* Shared: HomeBrew, Bash 5.0+, jq, Python 2.7, Python 3, Meson, Ninja, coreutils, Pandoc +* HomeBrew, Bash 5.0+, jq, Python 2.7, Python 3, Meson, Ninja, coreutils, Pandoc - ```shell - /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" - brew install bash jq python3 meson ninja coreutils pandoc pyenv - # Python 2.7 required for DeviceKit (among others?) at present - pyenv install 2.7.18 - pyenv global 2.7.18 - echo 'eval "$(pyenv init --path)"' >> ~/.bash_profile - ``` +```shell +/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" +brew install bash jq python3 meson ninja coreutils pandoc pyenv +# Python 2.7 required for DeviceKit (among others?) at present +pyenv install 2.7.18 +pyenv global 2.7.18 +echo 'eval "$(pyenv init --path)"' >> ~/.bash_profile +``` - On macOS, you will need to adjust your PATH so that coreutils’ `realpath` takes precedence over the BSD one: +On macOS, you will need to adjust your PATH so that coreutils’ `realpath` takes +precedence over the BSD one: - ```shell - # Credit: brew info coreutils - PATH="$HOMEBREW_PREFIX/opt/coreutils/libexec/gnubin:$PATH" - ``` +```shell +# Credit: brew info coreutils +PATH="$HOMEBREW_PREFIX/opt/coreutils/libexec/gnubin:$PATH" +``` -* Web: node.js 18+, emscripten, openjdk 8 +## KeymanWeb Dependencies - ```shell - brew install node emscripten openjdk@8 - ``` +* node.js 18+, emscripten, openjdk 8 - Note: if you install emscripten with brew on macOS, only emscripten binaries - are added to the path via symlinks. This makes it reasonably safe to have - emscripten on the path, unlike on other platforms where emscripten also ends - up adding its versions of node, python, and other binaries to the path. +```shell +brew install node emscripten openjdk@8 +``` -* iOS: swiftlint, carthage +Note: if you install emscripten with brew on macOS, only emscripten binaries are +added to the path via symlinks. This makes it reasonably safe to have emscripten +on the path, unlike on other platforms where emscripten also ends up adding its +versions of node, python, and other binaries to the path. - ```shell - brew install swiftlint carthage - ``` +## Keyman for iOS Dependencies -* macOS: carthage, cocoapods +* XCode, swiftlint, carthage - ```shell - brew install carthage cocoapods - ``` +```shell +brew install swiftlint carthage +``` -* Android: openjdk 8, Android SDK, Android Studio, Ant, Gradle, Maven +## Keyman for Mac Dependencies - ```shell - brew install openjdk@8 android-sdk android-studio ant gradle maven - # update path - source ../resources/devbox/macos/keyman.macos.env.sh - # optionally install sdk images - sdkmanager "system-images;android-30;google_apis;armeabi-v7a" - sdkmanager --update - sdkmanager --licenses - ``` +* XCode, carthage, cocoapods -* kmc (optional): node - - Required to build keyboards using kmc +```shell +brew install carthage cocoapods +``` - ```bash - brew tap homebrew/cask-versions - brew install --cask --no-quarantine wine-stable - ``` +## Keyman for Android Dependencies -* sentry-cli (optional) - - Uploading symbols for Sentry-based error reporting +* openjdk 11, Android SDK, Android Studio, Ant, Gradle, Maven - ``` +```shell +brew install openjdk@11 android-sdk android-studio ant gradle maven +# update path +source ../resources/devbox/macos/keyman.macos.env.sh +# optionally install sdk images +sdkmanager "system-images;android-30;google_apis;armeabi-v7a" +sdkmanager --update +sdkmanager --licenses +``` + +* Note: Run Android Studio once after installation to install additional +components such as emulator images and SDK updates. + +## Keyman Developer Command Line (kmc) + +* node.js, emscripten + +```shell +brew install node emscripten +``` + +## Optional Tools + +* sentry-cli: Uploading symbols for Sentry-based error reporting + + ```shell brew install getsentry/tools/sentry-cli ``` - -* Run Android Studio once after installation to install additional components - such as emulator images and SDK updates. diff --git a/docs/linux/README.md b/docs/linux/README.md index 8ee91fbe81e..fe29b0d623a 100644 --- a/docs/linux/README.md +++ b/docs/linux/README.md @@ -102,6 +102,9 @@ sudo apt install -y lcov libdatetime-perl gcovr pip3 install coverage ``` +**Note:** You want lcov > 1.16, so you might have to download and install +a newer version e.g. from . + #### Creating and displaying code coverage reports All three projects (ibus-keyman, keyman-config, and keyman-system-service) diff --git a/linux/keyman-config/build.sh b/linux/keyman-config/build.sh index 35608c69eb7..9c0588f796b 100755 --- a/linux/keyman-config/build.sh +++ b/linux/keyman-config/build.sh @@ -46,7 +46,7 @@ execute_with_temp_schema() { TEMP_DATA_DIR=$(mktemp -d) SCHEMA_DIR="${TEMP_DATA_DIR}/glib-2.0/schemas" export XDG_DATA_DIRS="${TEMP_DATA_DIR}":${XDG_DATA_DIRS-} - export GSETTINGS_SCHEMA_DIR="${SCHEMA_DIR}" + export GSETTINGS_SCHEMA_DIR="${SCHEMA_DIR}:/usr/share/glib-2.0/schemas/:${GSETTINGS_SCHEMA_DIR-}" mkdir -p "${SCHEMA_DIR}" cp resources/com.keyman.gschema.xml "${SCHEMA_DIR}"/ glib-compile-schemas "${SCHEMA_DIR}" diff --git a/linux/keyman-config/run-tests.sh b/linux/keyman-config/run-tests.sh index de81ec7f738..de99c55e8f2 100755 --- a/linux/keyman-config/run-tests.sh +++ b/linux/keyman-config/run-tests.sh @@ -16,10 +16,13 @@ if [ -n "$TEAMCITY_VERSION" ]; then if ! pip3 list --format=columns | grep -q teamcity-messages; then pip3 install teamcity-messages fi - python3 -m teamcity.unittestpy discover -s tests -p test_*.py + test_module=teamcity.unittestpy else - # shellcheck disable=SC2086 - python3 ${coverage:-} -m unittest discover -v -s tests -p test_*.py + test_module=unittest + extra_opts=-v fi +# shellcheck disable=SC2086 +python3 ${coverage:-} -m ${test_module:-} discover ${extra_opts:-} -s tests -p test_*.py + rm -rf "$XDG_CONFIG_HOME" diff --git a/linux/scripts/deb-packaging.sh b/linux/scripts/deb-packaging.sh index 4fb4c90c773..00eef3b1cc8 100755 --- a/linux/scripts/deb-packaging.sh +++ b/linux/scripts/deb-packaging.sh @@ -57,14 +57,21 @@ fi if builder_start_action verify; then tar xf "${SRC_PKG}" - if [ ! -f debian/libkmnkbp0-0.symbols ]; then - echo ":warning: Missing libkmnkbp0-0.symbols file" >&2 + if [ ! -f debian/libkmnkbp0-0.symbols ] && [ ! -f debian/libkeymancore.symbols ]; then + echo ":warning: Missing libkmnkbp0-0.symbols/libkeymancore.symbols file" >&2 else + if [ -f debian/libeymancore.symbols ]; then + PKG_NAME=libkeymancore + LIB_NAME=libkeymancore + else + PKG_NAME=libkmnkbp0-0 + LIB_NAME=libkmnkbp0 + fi tmpDir=$(mktemp -d) dpkg -x "${BIN_PKG}" "$tmpDir" cd debian - dpkg-gensymbols -v"${PKG_VERSION}" -plibkmnkbp0-0 -e"${tmpDir}"/usr/lib/x86_64-linux-gnu/libkeymancore.so* -Olibkmnkbp0-0.symbols -c4 - echo ":heavy_check_mark: libkmnkbp0-0 API didn't change" >&2 + dpkg-gensymbols -v"${PKG_VERSION}" -p${PKG_NAME} -e"${tmpDir}"/usr/lib/x86_64-linux-gnu/${LIB_NAME}.so* -O${PKG_NAME}.symbols -c4 + echo ":heavy_check_mark: ${LIB_NAME} API didn't change" >&2 fi builder_finish_action success verify exit 0 diff --git a/mac/Keyman4MacIM/Keyman4MacIM/Base.lproj/MainMenu.xib b/mac/Keyman4MacIM/Keyman4MacIM/Base.lproj/MainMenu.xib index 4062663c00f..0696c81eba2 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/Base.lproj/MainMenu.xib +++ b/mac/Keyman4MacIM/Keyman4MacIM/Base.lproj/MainMenu.xib @@ -1,8 +1,8 @@ - + - + @@ -19,19 +19,16 @@ - + + + - - - - - - + - + diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m index 14ee3c9c351..70937eecbf3 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m @@ -364,6 +364,8 @@ - (void)checkBoxAction:(id)sender { [self saveActiveKeyboards]; } else if (checkBox.state == NSOffState) { + if ([self.AppDelegate debugMode]) + NSLog(@"Disabling active keyboard: %@", kmxFilePath); [self.activeKeyboards removeObject:kmxFilePath]; [self saveActiveKeyboards]; } diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m index 1607e915e07..47d2bfceb58 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m @@ -183,65 +183,40 @@ - (KMXFile *)kmx { return self.AppDelegate.kmx; } - - (void)menuAction:(id)sender { NSMenuItem *mItem = [sender objectForKey:kIMKCommandMenuItemName]; NSInteger itag = mItem.tag; if ([self.AppDelegate debugMode]) NSLog(@"Keyman menu clicked - tag: %lu", itag); - if (itag == 2) { - // Using `showConfigurationWindow` instead of `showPreferences:` because `showPreferences:` is missing in - // High Sierra (10.13.1 - 10.13.3). See: https://bugreport.apple.com/web/?problemID=35422518 - // rrb: where Apple's API is broken (10.13.1-10.13.3) call our workaround, otherwise, call showPreferences - u_int16_t systemVersion = [KMOSVersion SystemVersion]; - if ([KMOSVersion Version_10_13_1] <= systemVersion && systemVersion <= [KMOSVersion Version_10_13_3]) // between 10.13.1 and 10.13.3 inclusive - { - NSLog(@"Input Menu: calling workaround instead of showPreferences (sys ver %x)", systemVersion); - [self.AppDelegate showConfigurationWindow]; // call our workaround - } - else - { - NSLog(@"Input Menu: calling Apple's showPreferences (sys ver %x)", systemVersion); - [self showPreferences:sender]; // call Apple API - } + if (itag == CONFIG_MENUITEM_TAG) { + [self showConfigurationWindow:sender]; } - else if (itag == 3) { + else if (itag == OSK_MENUITEM_TAG) { [self.AppDelegate showOSK]; } - else if (itag == 4) { + else if (itag == ABOUT_MENUITEM_TAG) { [self.AppDelegate showAboutWindow]; } - else if (itag >= 1000) { - NSMenuItem *keyboards = [self.AppDelegate.menu itemWithTag:1]; - for (NSMenuItem *item in keyboards.submenu.itemArray) { - if (item.tag == itag) - [item setState:NSOnState]; - else - [item setState:NSOffState]; - } - - NSString *path = [self.AppDelegate.activeKeyboards objectAtIndex:itag%1000]; - KMXFile *kmx = [[KMXFile alloc] initWithFilePath:path]; - [self.AppDelegate setKmx:kmx]; - KVKFile *kvk = nil; - NSDictionary *kmxInfo = [KMXFile keyboardInfoFromKmxFile:path]; - NSString *kvkFilename = [kmxInfo objectForKey:kKMVisualKeyboardKey]; - if (kvkFilename != nil) { - NSString *kvkFilePath = [self.AppDelegate kvkFilePathFromFilename:kvkFilename]; - if (kvkFilePath != nil) - kvk = [[KVKFile alloc] initWithFilePath:kvkFilePath]; - } - [self.AppDelegate setKvk:kvk]; - NSString *keyboardName = [kmxInfo objectForKey:kKMKeyboardNameKey]; - if ([self.AppDelegate debugMode]) - NSLog(@"Selected keyboard from menu: %@", keyboardName); - [self.AppDelegate setKeyboardName:keyboardName]; - [self.AppDelegate setKeyboardIcon:[kmxInfo objectForKey:kKMKeyboardIconKey]]; - [self.AppDelegate setContextBuffer:nil]; - [self.AppDelegate setSelectedKeyboard:path]; - [self.AppDelegate loadSavedStores]; - if (kvk != nil && self.AppDelegate.alwaysShowOSK) - [self.AppDelegate showOSK]; + else if (itag >= KEYMAN_FIRST_KEYBOARD_MENUITEM_TAG) { + [self.AppDelegate selectKeyboardFromMenu:itag]; } } + +- (void)showConfigurationWindow:(id)sender { + // Using `showConfigurationWindow` instead of `showPreferences:` because `showPreferences:` is missing in + // High Sierra (10.13.1 - 10.13.3). See: https://bugreport.apple.com/web/?problemID=35422518 + // rrb: where Apple's API is broken (10.13.1-10.13.3) call our workaround, otherwise, call showPreferences + u_int16_t systemVersion = [KMOSVersion SystemVersion]; + if ([KMOSVersion Version_10_13_1] <= systemVersion && systemVersion <= [KMOSVersion Version_10_13_3]) // between 10.13.1 and 10.13.3 inclusive + { + NSLog(@"Input Menu: calling workaround instead of showPreferences (sys ver %x)", systemVersion); + [self.AppDelegate showConfigurationWindow]; // call our workaround + } + else + { + NSLog(@"Input Menu: calling Apple's showPreferences (sys ver %x)", systemVersion); + [self showPreferences:sender]; // call Apple API + } +} + @end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h index 5214faf8212..e4e53e5972f 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h @@ -38,6 +38,21 @@ typedef struct { NSString *apiKeymanCom; } KeymanVersionInfo; +// tags for default menu items, displayed whether keyboards are active or not +static const int DIVIDER_MENUITEM_TAG = -4; +static const int CONFIG_MENUITEM_TAG = -3; +static const int OSK_MENUITEM_TAG = -2; +static const int ABOUT_MENUITEM_TAG = -1; + +// the number of menu items that do not represent active keyboards +static const int DEFAULT_KEYMAN_MENU_ITEM_COUNT = 4; + +// the tag for the first keyboard dynamically added to the menu +static const int KEYMAN_FIRST_KEYBOARD_MENUITEM_TAG = 0; + +// the menu index for the first keyboard dynamically added to the menu +static const int KEYMAN_FIRST_KEYBOARD_MENUITEM_INDEX = 0; + @interface KMInputMethodAppDelegate : NSObject #define USE_ALERT_SHOW_HELP_TO_FORCE_EASTER_EGG_CRASH_FROM_ENGINE 1 #ifdef USE_ALERT_SHOW_HELP_TO_FORCE_EASTER_EGG_CRASH_FROM_ENGINE @@ -55,6 +70,7 @@ typedef struct { @property (nonatomic, strong) NSMutableArray *kmxFileList; @property (nonatomic, strong) NSString *selectedKeyboard; @property (nonatomic, strong) NSMutableArray *activeKeyboards; +@property (assign) int numberOfKeyboardMenuItems; @property (nonatomic, strong) NSMutableString *contextBuffer; @property (nonatomic, assign) NSEventModifierFlags currentModifierFlags; @property (nonatomic, assign) CFMachPortRef lowLevelEventTap; @@ -88,6 +104,7 @@ typedef struct { - (void)showAboutWindow; - (void)showOSK; - (void)showConfigurationWindow; +- (void)selectKeyboardFromMenu:(NSInteger)tag; - (void)sleepFollowingDeactivationOfServer:(id)lastServer; - (void)wakeUpWith:(id)newServer; - (void)handleKeyEvent:(NSEvent *)event; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m index 44df2ef6fa2..b04236ed5b8 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m @@ -700,14 +700,14 @@ - (void)saveActiveKeyboards { [userData setObject:_activeKeyboards forKey:kKMActiveKeyboardsKey]; [userData synchronize]; [self resetActiveKeyboards]; - [self setKeyboardsSubMenu]; + [self updateKeyboardMenuItems]; } - (void)clearActiveKeyboards { NSUserDefaults *userData = [NSUserDefaults standardUserDefaults]; [userData setObject:nil forKey:kKMActiveKeyboardsKey]; [userData synchronize]; - [self setKeyboardsSubMenu]; + [self updateKeyboardMenuItems]; } - (void)resetActiveKeyboards { @@ -760,90 +760,174 @@ - (void)setContextBuffer:(NSMutableString *)contextBuffer { } - (void)awakeFromNib { - [self setKeyboardsSubMenu]; - - NSMenuItem *config = [self.menu itemWithTag:2]; - if (config) - [config setAction:@selector(menuAction:)]; - - NSMenuItem *osk = [self.menu itemWithTag:3]; - if (osk) - [osk setAction:@selector(menuAction:)]; - - NSMenuItem *about = [self.menu itemWithTag:4]; - if (about) - [about setAction:@selector(menuAction:)]; -} - -- (void)setKeyboardsSubMenu { - NSMenuItem *keyboards = [self.menu itemWithTag:1]; - if (keyboards) { - KVKFile *kvk = nil; - BOOL didSetKeyboard = NO; - NSInteger itag = 1000; - [keyboards.submenu removeAllItems]; - for (NSString *path in self.activeKeyboards) { - NSDictionary *infoDict = [KMXFile keyboardInfoFromKmxFile:path]; - if (!infoDict) - continue; - //NSString *str = [NSString stringWithFormat:@"%@ (%@)", [infoDict objectForKey:kKMKeyboardNameKey], [infoDict objectForKey:kKMKeyboardVersionKey]]; - NSString *str = [infoDict objectForKey:kKMKeyboardNameKey]; - NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:str action:@selector(menuAction:) keyEquivalent:@""]; - [item setTag:itag++]; - if ([path isEqualToString:self.selectedKeyboard]) { - [item setState:NSOnState]; - KMXFile *kmx = [[KMXFile alloc] initWithFilePath:path]; - [self setKmx:kmx]; - NSDictionary *kmxInfo = [KMXFile keyboardInfoFromKmxFile:path]; - NSString *kvkFilename = [kmxInfo objectForKey:kKMVisualKeyboardKey]; - if (kvkFilename != nil) { - NSString *kvkFilePath = [self kvkFilePathFromFilename:kvkFilename]; - if (kvkFilePath != nil) - kvk = [[KVKFile alloc] initWithFilePath:kvkFilePath]; - } - [self setKvk:kvk]; - [self setKeyboardName:[kmxInfo objectForKey:kKMKeyboardNameKey]]; - [self setKeyboardIcon:[kmxInfo objectForKey:kKMKeyboardIconKey]]; - [self loadSavedStores]; + [self setDefaultKeymanMenuItems]; + [self updateKeyboardMenuItems]; +} - didSetKeyboard = YES; - } - else - [item setState:NSOffState]; +- (void)setDefaultKeymanMenuItems { + NSMenuItem *config = [self.menu itemWithTag:CONFIG_MENUITEM_TAG]; + if (config) { + [config setAction:@selector(menuAction:)]; + } + + NSMenuItem *osk = [self.menu itemWithTag:OSK_MENUITEM_TAG]; + if (osk) { + [osk setAction:@selector(menuAction:)]; + } + + NSMenuItem *about = [self.menu itemWithTag:ABOUT_MENUITEM_TAG]; + if (about) { + [about setAction:@selector(menuAction:)]; + } +} - [keyboards.submenu addItem:item]; +- (void)updateKeyboardMenuItems { + self.numberOfKeyboardMenuItems = [self calculateNumberOfKeyboardMenuItems]; + [self removeDynamicKeyboardMenuItems]; + [self addDynamicKeyboardMenuItems]; +} + +- (int)calculateNumberOfKeyboardMenuItems { + if (self.activeKeyboards.count == 0) { + // if there are no active keyboards, then we will insert one placeholder menu item 'No Active Keyboards' + return 1; + } else { + return (int) self.activeKeyboards.count; + } +} + +- (void)removeDynamicKeyboardMenuItems { + int numberToRemove = (int) self.menu.numberOfItems - DEFAULT_KEYMAN_MENU_ITEM_COUNT; + + if (self.debugMode) { + NSLog(@"*** removeDynamicKeyboardMenuItems, self.menu.numberOfItems = %ld, number of items to remove = %d", (long)self.menu.numberOfItems, numberToRemove); + } + + if (numberToRemove > 0) { + for (int i = 0; i < numberToRemove; i++) { + [self.menu removeItemAtIndex:KEYMAN_FIRST_KEYBOARD_MENUITEM_INDEX]; + } + } +} + +- (void)addDynamicKeyboardMenuItems { + BOOL didSetSelectedKeyboard = NO; + NSInteger itag = KEYMAN_FIRST_KEYBOARD_MENUITEM_TAG; + NSString *keyboardMenuName = @""; + int menuItemIndex = KEYMAN_FIRST_KEYBOARD_MENUITEM_INDEX; + + if (self.debugMode) { + NSLog(@"*** populateKeyboardMenuItems, number of active keyboards=%lu", self.activeKeyboards.count); + } + + // loop through the active keyboards list and add them to the menu + for (NSString *path in self.activeKeyboards) { + NSDictionary *infoDict = [KMXFile keyboardInfoFromKmxFile:path]; + if (!infoDict) { + continue; } + keyboardMenuName = [infoDict objectForKey:kKMKeyboardNameKey]; + NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:keyboardMenuName action:@selector(menuAction:) keyEquivalent:@""]; + [item setTag:itag++]; + + // if this is the selected keyboard, then configure it as selected + if ([path isEqualToString:self.selectedKeyboard]) { + [self setSelectedKeyboard:path inMenuItem:item]; + didSetSelectedKeyboard = YES; + } + else { + [item setState:NSOffState]; + } + + [self.menu insertItem:item atIndex:menuItemIndex++]; + } + + if (self.activeKeyboards.count == 0) { + [self addKeyboardPlaceholderMenuItem]; + } else if (!didSetSelectedKeyboard) { + [self setDefaultSelectedKeyboard]; + } +} - if (keyboards.submenu.numberOfItems == 0) { - NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:@"(None)" action:NULL keyEquivalent:@""]; - [keyboards.submenu addItem:item]; - [self setKmx:nil]; - [self setKvk:nil]; - [self setKeyboardName:nil]; - [self setKeyboardIcon:nil]; - [self setContextBuffer:nil]; - [self setSelectedKeyboard:nil]; +- (void) setSelectedKeyboard:(NSString*)keyboardName inMenuItem:(NSMenuItem*) menuItem { + KVKFile *kvk = nil; + + [menuItem setState:NSOnState]; + KMXFile *kmx = [[KMXFile alloc] initWithFilePath:keyboardName]; + [self setKmx:kmx]; + NSDictionary *kmxInfo = [KMXFile keyboardInfoFromKmxFile:keyboardName]; + NSString *kvkFilename = [kmxInfo objectForKey:kKMVisualKeyboardKey]; + if (kvkFilename != nil) { + NSString *kvkFilePath = [self kvkFilePathFromFilename:kvkFilename]; + if (kvkFilePath != nil) { + kvk = [[KVKFile alloc] initWithFilePath:kvkFilePath]; } - else if (!didSetKeyboard) { - [keyboards.submenu itemAtIndex:0].state = NSOnState; - NSString *path = [self.activeKeyboards objectAtIndex:0]; - KMXFile *kmx = [[KMXFile alloc] initWithFilePath:path]; - [self setKmx:kmx]; - NSDictionary *kmxInfo = [KMXFile keyboardInfoFromKmxFile:path]; - NSString *kvkFilename = [kmxInfo objectForKey:kKMVisualKeyboardKey]; - if (kvkFilename != nil) { - NSString *kvkFilePath = [self kvkFilePathFromFilename:kvkFilename]; - if (kvkFilePath != nil) - kvk = [[KVKFile alloc] initWithFilePath:kvkFilePath]; + } + [self setKvk:kvk]; + [self setKeyboardName:[kmxInfo objectForKey:kKMKeyboardNameKey]]; + [self setKeyboardIcon:[kmxInfo objectForKey:kKMKeyboardIconKey]]; + [self loadSavedStores]; +} + +// defaults to the whatever keyboard happens to be first in the list +- (void) setDefaultSelectedKeyboard { + NSMenuItem* menuItem = [self.menu itemAtIndex:KEYMAN_FIRST_KEYBOARD_MENUITEM_INDEX]; + NSString *keyboardName = [self.activeKeyboards objectAtIndex:0]; + [self setSelectedKeyboard:keyboardName inMenuItem:menuItem]; + [self setSelectedKeyboard:keyboardName]; + [self setContextBuffer:nil]; +} + +- (void) addKeyboardPlaceholderMenuItem { + NSString* placeholder = NSLocalizedString(@"no-keyboard-configured-menu-placeholder", nil); + NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:placeholder action:NULL keyEquivalent:@""]; + [self.menu insertItem:item atIndex:KEYMAN_FIRST_KEYBOARD_MENUITEM_INDEX]; + [self setKmx:nil]; + [self setKvk:nil]; + [self setKeyboardName:nil]; + [self setKeyboardIcon:nil]; + [self setContextBuffer:nil]; + [self setSelectedKeyboard:nil]; +} + +- (void)selectKeyboardFromMenu:(NSInteger)tag { + NSMenuItem *menuItem = [self.menu itemWithTag:tag]; + NSString *title = menuItem.title; + NSLog(@"Input Menu, selected Keyboards menu, itag: %lu, title: %@", tag, title); + for (NSMenuItem *item in self.menu.itemArray) { + // set the state of the keyboard items in the Keyman menu based on the new selection + if (item.tag >= KEYMAN_FIRST_KEYBOARD_MENUITEM_TAG) { + if (item.tag == tag) { + [item setState:NSOnState]; + } + else { + [item setState:NSOffState]; } - [self setKvk:kvk]; - [self setKeyboardName:[kmxInfo objectForKey:kKMKeyboardNameKey]]; - [self setKeyboardIcon:[kmxInfo objectForKey:kKMKeyboardIconKey]]; - [self setContextBuffer:nil]; - [self loadSavedStores]; - [self setSelectedKeyboard:path]; } } + + NSString *path = [self.activeKeyboards objectAtIndex:tag-KEYMAN_FIRST_KEYBOARD_MENUITEM_TAG]; + KMXFile *kmx = [[KMXFile alloc] initWithFilePath:path]; + [self setKmx:kmx]; + KVKFile *kvk = nil; + NSDictionary *kmxInfo = [KMXFile keyboardInfoFromKmxFile:path]; + NSString *kvkFilename = [kmxInfo objectForKey:kKMVisualKeyboardKey]; + if (kvkFilename != nil) { + NSString *kvkFilePath = [self kvkFilePathFromFilename:kvkFilename]; + if (kvkFilePath != nil) + kvk = [[KVKFile alloc] initWithFilePath:kvkFilePath]; + } + [self setKvk:kvk]; + NSString *keyboardName = [kmxInfo objectForKey:kKMKeyboardNameKey]; + if ([self debugMode]) + NSLog(@"Selected keyboard from menu: %@", keyboardName); + [self setKeyboardName:keyboardName]; + [self setKeyboardIcon:[kmxInfo objectForKey:kKMKeyboardIconKey]]; + [self setContextBuffer:nil]; + [self setSelectedKeyboard:path]; + [self loadSavedStores]; + if (kvk != nil && self.alwaysShowOSK) + [self showOSK]; } - (NSArray *)KMXFiles { diff --git a/mac/Keyman4MacIM/Keyman4MacIM/en.lproj/Localizable.strings b/mac/Keyman4MacIM/Keyman4MacIM/en.lproj/Localizable.strings index f14a66b0b4a..6b815a7b176 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/en.lproj/Localizable.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/en.lproj/Localizable.strings @@ -71,4 +71,6 @@ /* message displayed to alert user to need grant accessibility permission */ "privacy-alert-text" = "To function properly, Keyman requires accessibility features:\n\nGrant access in System Preferences, Security & Privacy.\nRestart your system."; +/* Text of menu item in Input Menu when no Keyboards are configured -- include parentheses */ +"no-keyboard-configured-menu-placeholder" = "(No Keyboard Configured)"; diff --git a/web/src/app/browser/src/contextManager.ts b/web/src/app/browser/src/contextManager.ts index 492c627146d..42b79109ef7 100644 --- a/web/src/app/browser/src/contextManager.ts +++ b/web/src/app/browser/src/contextManager.ts @@ -184,7 +184,17 @@ export default class ContextManager extends ContextManagerBase, sendEvents: boolean) { diff --git a/windows/src/desktop/help/advanced/index.md b/windows/src/desktop/help/advanced/index.md index 46d35ac6642..371f85521d7 100644 --- a/windows/src/desktop/help/advanced/index.md +++ b/windows/src/desktop/help/advanced/index.md @@ -4,4 +4,3 @@ title: Advanced Help * [proxy_config](proxy_config) * [locale_edit](locale_edit) -* [text_services_framework](text_services_framework) diff --git a/windows/src/desktop/help/advanced/text_services_framework.md b/windows/src/desktop/help/advanced/text_services_framework.md deleted file mode 100644 index e7a977a062f..00000000000 --- a/windows/src/desktop/help/advanced/text_services_framework.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -title: Text Services Framework ---- - -## What is the Text Services Framework? - -The Text Services Framework, or TSF, is a set of components for modern -applications that support advanced input and text processing. It -supports features such as keyboard drivers, handwriting recognition, -speech recognition, as well as spell checking and other text processing -functions. - -With Keyman Desktop 9 and later versions, all keyboards are registered -through the Windows interfaces, and the key advantage is that Keyman now -automatically detects applications that have support for TSF and -upgrades the experience for those applications. - -For those applications that support TSF, the most important advantage is -that Keyman can read the current 'context' from the application. The -'context' is the characters on the screen around the insertion point. -Earlier versions of Keyman remembered the context while inputting text, -but as soon as an arrow key was pressed, or the mouse clicked, Keyman -would forget the context. This means that existing text can be edited in -a much more intuitive manner when using TSF. - -## What applications support TSF? - -TSF is supported by a wide range of software and Windows components. diff --git a/windows/src/desktop/help/basic/make-taskbar-icon-visible.md b/windows/src/desktop/help/basic/make-taskbar-icon-visible.md index 62b90be5d46..eebd699e0a6 100644 --- a/windows/src/desktop/help/basic/make-taskbar-icon-visible.md +++ b/windows/src/desktop/help/basic/make-taskbar-icon-visible.md @@ -37,3 +37,16 @@ Here's how to make the change with Windows Settings: 5. The Keyman icon will now always appear in the Windows Taskbar near the clock, if Keyman is on. 6. Continue on to [Step 5](../start/tutorial#step-5-) of this guide. + +- On Windows 11 + + 1. Right-click on the Windows Taskbar. + + ![](../desktop_images/win11-taskbar1.png) + 2. Select 'Taskbar settings'. + 3. Under 'Other system tray icons', expand the list, + click the toggle button beside 'Keyman Engine x86'to turn it on. + ![](../desktop_images/win11-taskbar2.png) + 4. The Keyman icon will now always appear in the Windows + Taskbar near the clock, if Keyman is on. + 5. Continue on to [Step 5](../start/tutorial#step-5-) of this guide. \ No newline at end of file diff --git a/windows/src/desktop/help/basic/uninstall.md b/windows/src/desktop/help/basic/uninstall.md index 7e7cb2274c7..6d986456138 100644 --- a/windows/src/desktop/help/basic/uninstall.md +++ b/windows/src/desktop/help/basic/uninstall.md @@ -15,3 +15,18 @@ title: Software Task - Uninstall Keyman 5. Click Uninstall. 6. Follow the prompts to complete the uninstall. + +## Uninstall Keyman from Windows 11 + +1. Exit Keyman. + +2. Open Windows Start Menu and type "Add or Remove Programs", and open that app. + +3. In the Apps > Installed apps search box, type "Keyman". + +4. Click the three horizontal dots to the right of Keyman in the list. +![](../desktop_images/win11-uninstall.png) + +5. Click Uninstall. + +6. Follow the prompts to complete the uninstall. diff --git a/windows/src/desktop/help/desktop_images/win11-taskbar1.png b/windows/src/desktop/help/desktop_images/win11-taskbar1.png new file mode 100644 index 00000000000..ec72256a713 Binary files /dev/null and b/windows/src/desktop/help/desktop_images/win11-taskbar1.png differ diff --git a/windows/src/desktop/help/desktop_images/win11-taskbar2.png b/windows/src/desktop/help/desktop_images/win11-taskbar2.png new file mode 100644 index 00000000000..a349d65aa9f Binary files /dev/null and b/windows/src/desktop/help/desktop_images/win11-taskbar2.png differ diff --git a/windows/src/desktop/help/desktop_images/win11-uninstall.png b/windows/src/desktop/help/desktop_images/win11-uninstall.png new file mode 100644 index 00000000000..a0611a8b394 Binary files /dev/null and b/windows/src/desktop/help/desktop_images/win11-uninstall.png differ diff --git a/windows/src/desktop/help/desktop_images/win11_km_install_anywhere_1.png b/windows/src/desktop/help/desktop_images/win11_km_install_anywhere_1.png new file mode 100644 index 00000000000..f991126de01 Binary files /dev/null and b/windows/src/desktop/help/desktop_images/win11_km_install_anywhere_1.png differ diff --git a/windows/src/desktop/help/index.md b/windows/src/desktop/help/index.md index 6dd83bca6fb..f5d5ddf3780 100644 --- a/windows/src/desktop/help/index.md +++ b/windows/src/desktop/help/index.md @@ -33,7 +33,6 @@ frequently asked questions, tutorials, and videos. ### [Advanced Topics](advanced/) * [Character Map](basic/toolbox/character-map) * [Hotkeys](start/hotkey_set) -* [Text Services Framework Addin](advanced/text_services_framework) * [Use Word macros to switch keyboard and font](https://help.keyman.com/kb/93) * [Application Programming Interface (API)](https://help.keyman.com/developer/engine/desktop/current-version/api/) * [Keyman for Windows version history](https://help.keyman.com/products/windows/version-history) diff --git a/windows/src/desktop/help/start/download-and-install-keyman.md b/windows/src/desktop/help/start/download-and-install-keyman.md index 27902001c76..00a85385087 100644 --- a/windows/src/desktop/help/start/download-and-install-keyman.md +++ b/windows/src/desktop/help/start/download-and-install-keyman.md @@ -17,7 +17,7 @@ After you download Keyman, to install the program: 2. Double-click the file to begin installation. -3. You may be warned to use a "Microsoft-verfied" App from the Microsoft Store. This is because Keyman is not available in the Microsoft Store. If "**Install Anyway**" is an option, select that. Otherwise follow the instructions found here [How To - Allow Windows 10 to Install Apps From Anywhere](../troubleshooting/install-app-from-anywhere) +3. You may be warned to use a "Microsoft-verfied" App from the Microsoft Store. This is because Keyman is not available in the Microsoft Store. If "**Install Anyway**" is an option, select that. Otherwise follow the instructions found here [How To - Allow Windows to Install Apps From Anywhere](../troubleshooting/install-app-from-anywhere) ![](../desktop_images/km_non_app_store_2.png) 4. You will be asked to allow the installer to access your system. diff --git a/windows/src/desktop/help/troubleshooting/index.md b/windows/src/desktop/help/troubleshooting/index.md index 54416ad9ba0..cafa5106479 100644 --- a/windows/src/desktop/help/troubleshooting/index.md +++ b/windows/src/desktop/help/troubleshooting/index.md @@ -10,4 +10,4 @@ title: Troubleshooting * [How To - Resolve Security Software Conflicts with keyman32.dll](securitysoftware) * [How To - Use the Keyman Setup Bootstrapper](bootstrapper) -* [How To - Allow Windows 10 to Install Apps From Anywhere](install-app-from-anywhere) +* [How To - Allow Windows to Install Apps From Anywhere](install-app-from-anywhere) diff --git a/windows/src/desktop/help/troubleshooting/install-app-from-anywhere.md b/windows/src/desktop/help/troubleshooting/install-app-from-anywhere.md index 160d7cf0238..23c4213b748 100644 --- a/windows/src/desktop/help/troubleshooting/install-app-from-anywhere.md +++ b/windows/src/desktop/help/troubleshooting/install-app-from-anywhere.md @@ -1,5 +1,5 @@ --- -title: How To - Allow Windows 10 to Install Apps From Anywhere +title: How To - Allow Windows to Install Apps From Anywhere --- ## Allow Windows 10 to Install Apps From Anywhere @@ -20,6 +20,22 @@ If "**Change my app recommendation settings**" is shown, select that and go to s 5. Now continue installing by double-clicking on the Keyman file identified in Step 1 of [How To - Download and Install a Keyman](../start/download-and-install-keyman). +## Allow Windows 11 to Install Apps From Anywhere + +1. You may be warned to use a "Microsoft-verified" App from the Microsoft Store. This is because Keyman is not available in the Microsoft Store. +If "**Change my app recommendation settings**" is shown, select that and go to step 4. +![](../desktop_images/km_non_app_store_1.png) + + +2. If that is not available, open the Windows 11 start menu (Windows logo) and select **Settings**. + +3. Click on the **Apps** in the **Windows Settings** menu + +4. Click on **Advanced app settings** from the option **choose where to get apps**, click on the dropdown menu below and select "Anywhere, but warn me before installing an app that's not from the Microsoft Store" or one of the other options that allows you to install from *Anywhere*. +![](../desktop_images/win11_km_install_anywhere_1.png) + +5. Now continue installing by double-clicking on the Keyman file identified in Step 1 of [How To - Download and Install a Keyman](../start/download-and-install-keyman). + ## Related Topics - [How To - Download and Install a Keyman](../start/download-and-install-keyman)