diff --git a/developer/src/kmc-kmn/build.sh b/developer/src/kmc-kmn/build.sh index a6163d9eaf..b4ca1dd43b 100755 --- a/developer/src/kmc-kmn/build.sh +++ b/developer/src/kmc-kmn/build.sh @@ -70,7 +70,7 @@ if builder_start_action test; then copy_deps tsc --build test/ npm run lint - readonly C8_THRESHOLD=70 + readonly C8_THRESHOLD=74 c8 --reporter=lcov --reporter=text --lines $C8_THRESHOLD --statements $C8_THRESHOLD --branches $C8_THRESHOLD --functions $C8_THRESHOLD mocha builder_echo warning "Coverage thresholds are currently $C8_THRESHOLD%, which is lower than ideal." builder_echo warning "Please increase threshold in build.sh as test coverage improves." diff --git a/developer/src/kmc-kmn/src/kmw-compiler/kmw-compiler-messages.ts b/developer/src/kmc-kmn/src/kmw-compiler/kmw-compiler-messages.ts index 4c4b41505c..7eec5cc07e 100644 --- a/developer/src/kmc-kmn/src/kmw-compiler/kmw-compiler-messages.ts +++ b/developer/src/kmc-kmn/src/kmw-compiler/kmw-compiler-messages.ts @@ -5,7 +5,7 @@ import { kmnfile } from "./compiler-globals.js"; const Namespace = CompilerErrorNamespace.KmwCompiler; // const SevInfo = CompilerErrorSeverity.Info | Namespace; // const SevHint = CompilerErrorSeverity.Hint | Namespace; -// const SevWarn = CompilerErrorSeverity.Warn | Namespace; +const SevWarn = CompilerErrorSeverity.Warn | Namespace; const SevError = CompilerErrorSeverity.Error | Namespace; // const SevFatal = CompilerErrorSeverity.Fatal | Namespace; @@ -106,4 +106,8 @@ export class KmwCompilerMessages extends KmnCompilerMessages { static Error_TouchLayoutFileDoesNotExist = (o:{filename:string}) => m(this.ERROR_TouchLayoutFileDoesNotExist, `Touch layout file ${o.filename} does not exist`); static ERROR_TouchLayoutFileDoesNotExist = SevError | 0x0004; + + static Warn_TouchLayoutUsesUnsupportedGesturesDownlevel = (o:{keyId:string}) => m(this.WARN_TouchLayoutUsesUnsupportedGesturesDownlevel, + `The touch layout uses a flick or multi-tap gesture on key ${o.keyId}, which is only available on version 17.0+ of Keyman`); + static WARN_TouchLayoutUsesUnsupportedGesturesDownlevel = SevWarn | 0x0005; }; diff --git a/developer/src/kmc-kmn/src/kmw-compiler/validate-layout-file.ts b/developer/src/kmc-kmn/src/kmw-compiler/validate-layout-file.ts index dfde10772a..48aaba1912 100644 --- a/developer/src/kmc-kmn/src/kmw-compiler/validate-layout-file.ts +++ b/developer/src/kmc-kmn/src/kmw-compiler/validate-layout-file.ts @@ -237,6 +237,14 @@ export function ValidateLayoutFile(fk: KMX.KEYBOARD, FDebug: boolean, sLayoutFil return null; } + let hasWarnedOfGestureUseDownlevel = false; + const warnGesturesIfNeeded = function(keyId: string) { + if(!hasWarnedOfGestureUseDownlevel && !IsKeyboardVersion17OrLater()) { + hasWarnedOfGestureUseDownlevel = true; + callbacks.reportMessage(KmwCompilerMessages.Warn_TouchLayoutUsesUnsupportedGesturesDownlevel({keyId})); + } + } + let result: boolean = true; let FTouchLayoutFont = ''; // I4872 let pid: keyof TouchLayout.TouchLayoutFile; @@ -267,6 +275,7 @@ export function ValidateLayoutFile(fk: KMX.KEYBOARD, FDebug: boolean, sLayoutFil let direction: keyof TouchLayout.TouchLayoutFlick; if(key.flick) { for(direction in key.flick) { + warnGesturesIfNeeded(key.id); result = CheckKey(pid, platform, layer, key.flick[direction].id, key.flick[direction].text, key.flick[direction].nextlayer, key.flick[direction].sp, FRequiredKeys, FDictionary) && result; } @@ -274,6 +283,7 @@ export function ValidateLayoutFile(fk: KMX.KEYBOARD, FDebug: boolean, sLayoutFil if(key.multitap) { for(let subkey of key.multitap) { + warnGesturesIfNeeded(key.id); result = CheckKey(pid, platform, layer, subkey.id, subkey.text, subkey.nextlayer, subkey.sp, FRequiredKeys, FDictionary) && result; } } diff --git a/developer/src/kmc-kmn/test/fixtures/kmw/validate_gesture.keyman-touch-layout b/developer/src/kmc-kmn/test/fixtures/kmw/validate_gesture.keyman-touch-layout new file mode 100644 index 0000000000..997d3c56e8 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/kmw/validate_gesture.keyman-touch-layout @@ -0,0 +1,670 @@ +{ + "tablet": { + "font": "Tahoma", + "layer": [ + { + "id": "default", + "row": [ + { + "id": 1, + "key": [ + { + "id": "K_Q", + "text": "q" + }, + { + "id": "K_W", + "text": "w" + }, + { + "id": "K_E", + "text": "e" + }, + { + "id": "K_R", + "text": "r" + }, + { + "id": "K_T", + "text": "t" + }, + { + "id": "K_Y", + "text": "y" + }, + { + "id": "K_U", + "text": "u" + }, + { + "id": "K_I", + "text": "i" + }, + { + "id": "K_O", + "text": "o" + }, + { + "id": "K_P", + "text": "p" + } + ] + }, + { + "id": 2, + "key": [ + { + "id": "K_A", + "text": "a", + "pad": 70, + "flick": { + "n": { + "text": "A", + "id": "U_0041" + } + } + }, + { + "id": "K_S", + "text": "s" + }, + { + "id": "K_D", + "text": "d" + }, + { + "id": "K_F", + "text": "f" + }, + { + "id": "K_G", + "text": "g" + }, + { + "id": "K_H", + "text": "h" + }, + { + "id": "K_J", + "text": "j" + }, + { + "id": "K_K", + "text": "k" + }, + { + "id": "K_L", + "text": "l" + }, + { + "id": "T_new_88", + "width": 10, + "sp": 10 + } + ] + }, + { + "id": 3, + "key": [ + { + "id": "K_SHIFT", + "text": "*Shift*", + "width": 110, + "sp": 1, + "nextlayer": "shift" + }, + { + "id": "K_Z", + "text": "z" + }, + { + "id": "K_X", + "text": "x" + }, + { + "id": "K_C", + "text": "c" + }, + { + "id": "K_V", + "text": "v" + }, + { + "id": "K_B", + "text": "b" + }, + { + "id": "K_N", + "text": "n" + }, + { + "id": "K_M", + "text": "m" + }, + { + "id": "K_PERIOD", + "text": ".", + "sk": [ + { + "text": ",", + "id": "K_COMMA" + }, + { + "text": "!", + "id": "K_1", + "layer": "shift" + }, + { + "text": "?", + "id": "K_SLASH", + "layer": "shift" + }, + { + "text": "'", + "id": "K_QUOTE" + }, + { + "text": "\"", + "id": "K_QUOTE", + "layer": "shift" + }, + { + "text": "\\", + "id": "K_BKSLASH" + }, + { + "text": ":", + "id": "K_COLON", + "layer": "shift" + }, + { + "text": ";", + "id": "K_COLON" + } + ] + }, + { + "id": "K_BKSP", + "text": "*BkSp*", + "width": 90, + "sp": 1 + } + ] + }, + { + "id": 4, + "key": [ + { + "id": "K_NUMLOCK", + "text": "*123*", + "width": 140, + "sp": 1, + "nextlayer": "numeric" + }, + { + "id": "K_LOPT", + "text": "*Menu*", + "width": 120, + "sp": 1 + }, + { + "id": "K_SPACE", + "text": "", + "width": 630, + "sp": 0 + }, + { + "id": "K_ENTER", + "text": "*Enter*", + "width": 140, + "sp": 1 + } + ] + } + ] + }, + { + "id": "shift", + "row": [ + { + "id": 1, + "key": [ + { + "id": "K_Q", + "text": "Q" + }, + { + "id": "K_W", + "text": "W" + }, + { + "id": "K_E", + "text": "E" + }, + { + "id": "K_R", + "text": "R" + }, + { + "id": "K_T", + "text": "T" + }, + { + "id": "K_Y", + "text": "Y" + }, + { + "id": "K_U", + "text": "U" + }, + { + "id": "K_I", + "text": "I" + }, + { + "id": "K_O", + "text": "O" + }, + { + "id": "K_P", + "text": "P" + } + ] + }, + { + "id": 2, + "key": [ + { + "id": "K_A", + "text": "A", + "pad": 70 + }, + { + "id": "K_S", + "text": "S" + }, + { + "id": "K_D", + "text": "D" + }, + { + "id": "K_F", + "text": "F" + }, + { + "id": "K_G", + "text": "G" + }, + { + "id": "K_H", + "text": "H" + }, + { + "id": "K_J", + "text": "J" + }, + { + "id": "K_K", + "text": "K" + }, + { + "id": "K_L", + "text": "L" + }, + { + "sp": 10, + "width": 10 + } + ] + }, + { + "id": 3, + "key": [ + { + "id": "K_SHIFT", + "text": "*Shift*", + "width": 110, + "sp": 2, + "nextlayer": "default" + }, + { + "id": "K_Z", + "text": "Z" + }, + { + "id": "K_X", + "text": "X" + }, + { + "id": "K_C", + "text": "C" + }, + { + "id": "K_V", + "text": "V" + }, + { + "id": "K_B", + "text": "B" + }, + { + "id": "K_N", + "text": "N" + }, + { + "id": "K_M", + "text": "M" + }, + { + "id": "K_PERIOD", + "text": ".", + "layer": "default", + "sk": [ + { + "text": ",", + "id": "K_COMMA", + "layer": "default" + }, + { + "text": "!", + "id": "K_1", + "layer": "shift" + }, + { + "text": "?", + "id": "K_SLASH", + "layer": "shift" + }, + { + "text": "'", + "id": "K_QUOTE", + "layer": "default" + }, + { + "text": "\"", + "id": "K_QUOTE", + "layer": "shift" + }, + { + "text": "\\", + "id": "K_BKSLASH", + "layer": "default" + }, + { + "text": ":", + "id": "K_COLON", + "layer": "shift" + }, + { + "text": ";", + "id": "K_COLON", + "layer": "default" + } + ] + }, + { + "id": "K_BKSP", + "text": "*BkSp*", + "width": 90, + "sp": 1 + } + ] + }, + { + "id": 4, + "key": [ + { + "id": "K_NUMLOCK", + "text": "*123*", + "width": 140, + "sp": 1, + "nextlayer": "numeric" + }, + { + "id": "K_LOPT", + "text": "*Menu*", + "width": 120, + "sp": 1 + }, + { + "id": "K_SPACE", + "text": "", + "width": 630, + "sp": 0 + }, + { + "id": "K_ENTER", + "text": "*Enter*", + "width": 140, + "sp": 1 + } + ] + } + ] + }, + { + "id": "numeric", + "row": [ + { + "id": 1, + "key": [ + { + "id": "K_1", + "text": "1" + }, + { + "id": "K_2", + "text": "2" + }, + { + "id": "K_3", + "text": "3" + }, + { + "id": "K_4", + "text": "4" + }, + { + "id": "K_5", + "text": "5" + }, + { + "id": "K_6", + "text": "6" + }, + { + "id": "K_7", + "text": "7" + }, + { + "id": "K_8", + "text": "8" + }, + { + "id": "K_9", + "text": "9" + }, + { + "id": "K_0", + "text": "0" + } + ] + }, + { + "id": 2, + "key": [ + { + "id": "K_4", + "text": "$", + "layer": "shift", + "pad": 70 + }, + { + "id": "K_2", + "text": "@", + "layer": "shift" + }, + { + "id": "K_3", + "text": "#", + "layer": "shift" + }, + { + "id": "K_5", + "text": "%", + "layer": "shift" + }, + { + "id": "K_7", + "text": "&", + "layer": "shift" + }, + { + "id": "K_HYPHEN", + "text": "_", + "layer": "shift" + }, + { + "id": "K_EQUAL", + "text": "=", + "layer": "default" + }, + { + "id": "K_BKSLASH", + "text": "|", + "layer": "shift" + }, + { + "id": "K_BKSLASH", + "text": "\\", + "layer": "default" + }, + { + "text": "", + "width": 10, + "sp": 10 + } + ] + }, + { + "id": 3, + "key": [ + { + "id": "K_SHIFT", + "text": "*Shift*", + "width": 110, + "sp": 1 + }, + { + "id": "K_LBRKT", + "text": "[", + "sk": [ + { + "id": "U_00AB", + "text": "«" + }, + { + "id": "K_COMMA", + "text": "<", + "layer": "shift" + }, + { + "id": "K_LBRKT", + "text": "{", + "layer": "shift" + } + ] + }, + { + "id": "K_9", + "text": "(", + "layer": "shift" + }, + { + "id": "K_0", + "text": ")", + "layer": "shift" + }, + { + "id": "K_RBRKT", + "text": "]", + "sk": [ + { + "id": "U_00BB", + "text": "»" + }, + { + "id": "K_PERIOD", + "text": ">", + "layer": "shift" + }, + { + "id": "K_RBRKT", + "text": "}", + "layer": "shift" + } + ] + }, + { + "id": "K_EQUAL", + "text": "+", + "layer": "shift" + }, + { + "id": "K_HYPHEN", + "text": "-", + "layer": "default" + }, + { + "id": "K_8", + "text": "*", + "layer": "shift" + }, + { + "id": "K_SLASH", + "text": "/", + "layer": "default" + }, + { + "id": "K_BKSP", + "text": "*BkSp*", + "width": 90, + "sp": 1 + } + ] + }, + { + "id": 4, + "key": [ + { + "id": "K_LOWER", + "text": "*abc*", + "width": 140, + "sp": 1, + "nextlayer": "default" + }, + { + "id": "K_LOPT", + "text": "*Menu*", + "width": 120, + "sp": 1 + }, + { + "id": "K_SPACE", + "text": "", + "width": 630, + "sp": 0 + }, + { + "id": "K_ENTER", + "text": "*Enter*", + "width": 140, + "sp": 1 + } + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/developer/src/kmc-kmn/test/fixtures/kmw/validate_gesture.kmn b/developer/src/kmc-kmn/test/fixtures/kmw/validate_gesture.kmn new file mode 100644 index 0000000000..c37cec616d --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/kmw/validate_gesture.kmn @@ -0,0 +1,12 @@ +c validate_gesture generated from template at 2024-01-11 15:15:16 +c with name "validate_gesture" +store(&VERSION) '10.0' +store(&NAME) 'validate_gesture' +store(©RIGHT) '©' +store(&KEYBOARDVERSION) '1.0' +store(&TARGETS) 'mobile' +store(&LAYOUTFILE) 'validate_gesture.keyman-touch-layout' + +begin Unicode > use(main) + +group(main) using keys diff --git a/developer/src/kmc-kmn/test/kmw/test-kmw-messages.ts b/developer/src/kmc-kmn/test/kmw/test-kmw-messages.ts new file mode 100644 index 0000000000..8c17126a3b --- /dev/null +++ b/developer/src/kmc-kmn/test/kmw/test-kmw-messages.ts @@ -0,0 +1,74 @@ +import 'mocha'; +import { assert } from 'chai'; +import { KmwCompilerMessages } from '../../src/kmw-compiler/kmw-compiler-messages.js'; +import { TestCompilerCallbacks, verifyCompilerMessagesObject } from '@keymanapp/developer-test-helpers'; +import { makePathToFixture } from '../helpers/index.js'; +import { KmnCompiler } from '../../src/main.js'; +import { CompilerErrorNamespace } from '@keymanapp/common-types'; + +describe('KmwCompilerMessages', function () { + const callbacks = new TestCompilerCallbacks(); + + it('should have a valid KmwCompilerMessages object', function() { + return verifyCompilerMessagesObject(KmwCompilerMessages, CompilerErrorNamespace.KmwCompiler); + }); + + // + // Message tests + // + + async function testForMessage(context: Mocha.Context, fixture: string[], messageId?: number) { + context.timeout(10000); + + callbacks.clear(); + + const compiler = new KmnCompiler(); + assert(await compiler.init(callbacks, {saveDebug: true, shouldAddCompilerVersion: false})); + assert(compiler.verifyInitialized()); + + const kmnPath = makePathToFixture(...fixture); + + // Note: throwing away compile results (just to memory) + await compiler.run(kmnPath, null); + + if(messageId) { + assert.isTrue(callbacks.hasMessage(messageId), `messageId ${messageId.toString(16)} not generated, instead got: `+JSON.stringify(callbacks.messages,null,2)); + assert.lengthOf(callbacks.messages, 1); + } else { + assert.lengthOf(callbacks.messages, 0, `messages should be empty, but instead got: `+JSON.stringify(callbacks.messages,null,2)); + } + } + + // WARN_TouchLayoutUsesUnsupportedGesturesDownlevel + + it('should generate WARN_TouchLayoutUsesUnsupportedGesturesDownlevel if the touch layout has gestures but the keyboard is an old version', async function() { + await testForMessage(this, ['kmw', 'validate_gesture.kmn'], KmwCompilerMessages.WARN_TouchLayoutUsesUnsupportedGesturesDownlevel); + }); + + // TODO: other messages + + // ERROR_NotAnyRequiresVersion14 + + // it('should generate ERROR_NotAnyRequiresVersion14 if ...', async function() { + // await testForMessage(this, ['kmw', '....kmn'], KmwCompilerMessages.ERROR_NotAnyRequiresVersion14); + // }); + + // ERROR_TouchLayoutIdentifierRequires15 + + // it('should generate ERROR_TouchLayoutIdentifierRequires15 if ...', async function() { + // await testForMessage(this, ['kmw', '....kmn'], KmwCompilerMessages.ERROR_TouchLayoutIdentifierRequires15); + // }); + + // ERROR_InvalidTouchLayoutFileFormat + + // it('should generate ERROR_InvalidTouchLayoutFileFormat if ...', async function() { + // await testForMessage(this, ['kmw', '....kmn'], KmwCompilerMessages.ERROR_InvalidTouchLayoutFileFormat); + // }); + + // ERROR_TouchLayoutFileDoesNotExist + + // it('should generate ERROR_TouchLayoutFileDoesNotExist if ...', async function() { + // await testForMessage(this, ['kmw', '....kmn'], KmwCompilerMessages.ERROR_TouchLayoutFileDoesNotExist); + // }); + +});