diff --git a/common/web/types/src/keyman-touch-layout/keyman-touch-layout-file.ts b/common/web/types/src/keyman-touch-layout/keyman-touch-layout-file.ts index e24fd295c84..2a009bc420f 100644 --- a/common/web/types/src/keyman-touch-layout/keyman-touch-layout-file.ts +++ b/common/web/types/src/keyman-touch-layout/keyman-touch-layout-file.ts @@ -1,23 +1,26 @@ -// -// .keyman-touch-layout JSON format -// -// Follows /common/schemas/keyman-touch-layout/keyman-touch-layout.spec.json for -// reading and -// /common/schemas/keyman-touch-layout/keyman-touch-layout.clean.spec.json for -// writing -// +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * .keyman-touch-layout JSON format definitions + * + * Follows scheams in /common/schemas/keyman-touch-layout/, using + * keyman-touch-layout.spec.json for reading and + * keyman-touch-layout.clean.spec.json for writing + */ /** * On screen keyboard description consisting of specific layouts for tablet, phone, * and desktop. Despite its name, this format is used for both touch layouts and * hardware-style layouts. */ -export interface TouchLayoutFile { +export type TouchLayoutFile = { tablet?: TouchLayoutPlatform; phone?: TouchLayoutPlatform; desktop?: TouchLayoutPlatform; }; +export type TouchLayoutPlatformName = keyof TouchLayoutFile; + export type TouchLayoutFont = string; export type TouchLayoutFontSize = string; export type TouchLayoutDefaultHint = "none"|"dot"|"longpress"|"multitap"|"flick"|"flick-n"|"flick-ne"|"flick-e"|"flick-se"|"flick-s"|"flick-sw"|"flick-w"|"flick-nw"; diff --git a/developer/src/kmc-kmn/src/compiler/embed-osk/embed-osk-touch-layout.ts b/developer/src/kmc-kmn/src/compiler/embed-osk/embed-osk-touch-layout.ts new file mode 100644 index 00000000000..0cfa8db7f77 --- /dev/null +++ b/developer/src/kmc-kmn/src/compiler/embed-osk/embed-osk-touch-layout.ts @@ -0,0 +1,261 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Convert .keyman-touch-layout data into KMXPlus data for embedding into .kmx + */ +import { TouchLayout, KMXPlus } from "@keymanapp/common-types"; +import { CompilerCallbacks, TouchLayoutFileReader, oskFontMagicToken } from "@keymanapp/developer-utils"; +import { KmnCompilerMessages } from "../kmn-compiler-messages.js"; + +export class EmbedOskTouchLayoutInKmx { + + constructor(private callbacks: CompilerCallbacks) { + } + + public loadTouchLayoutFile(filename: string): TouchLayout.TouchLayoutFile { + const data = this.callbacks.loadFile(filename); + if(!data) { + this.callbacks.reportMessage(KmnCompilerMessages.Error_FileNotFound({filename})); + return null; + } + + const reader = new TouchLayoutFileReader(); + try { + const touchLayout = reader.read(data); + reader.validate(touchLayout); + return touchLayout; + } catch(e) { + // TODO-EMBED-OSK-IN-KMX report on e + this.callbacks.reportMessage(KmnCompilerMessages.Error_InvalidTouchLayoutFile({filename})); + return null; + } + } + + public transformTouchLayoutToKmxPlus(kmx: KMXPlus.KMXPlusFile, tl: TouchLayout.TouchLayoutFile): void { + // empty the keys into layer bags + // build the layers + // don't forget all the gestures + // what about the VKDictionary? + + // transformTouchLayoutPlatform(kmx, tl, 'desktop', tl.desktop); // probably not needed + + this.addPlatformFromTouchLayoutPlatform(kmx.kmxplus, 'tablet', tl.tablet, 200); // anything larger than 200mm width + this.addPlatformFromTouchLayoutPlatform(kmx.kmxplus, 'phone', tl.phone, 1); // anything larger than 1mm width + } + + private addPlatformFromTouchLayoutPlatform(kmxplus: KMXPlus.KMXPlusData, platformName: TouchLayout.TouchLayoutPlatformName, platform: TouchLayout.TouchLayoutPlatform, deviceWidth: number): void { + if(!platform) { + // The platform may not be used in the touch layout; this is fine + return; + } + + const form = new KMXPlus.LayrForm(); + form.baseLayout = kmxplus.strs.allocString('en-us'); // TODO-EMBED-OSK-IN-KMX: should this be null for touch? + form.hardware = kmxplus.strs.allocString(KMXPlus.LayrFormHardware.touch); + form.fontFaceName = kmxplus.strs.allocString(oskFontMagicToken); + form.fontSizePct = 100; + form.flags = platform.displayUnderlying ? KMXPlus.LayrFormFlags.showBaseLayout : 0; + form.minDeviceWidth = deviceWidth; + form.layers = []; + + // platform.defaultHint; TODO-EMBED-OSK-IN-KMX + // platform.font; [ignore] + // platform.fontsize; [ignore] + + for(const layer of platform.layer) { + this.addLayerFromTouchLayoutLayer(kmxplus, form, platformName, layer); + } + + kmxplus.layr.forms.push(form); + } + + private addLayerFromTouchLayoutLayer(kmxplus: KMXPlus.KMXPlusData, form: KMXPlus.LayrForm, platformName: TouchLayout.TouchLayoutPlatformName, layer: TouchLayout.TouchLayoutLayer): void { + const entry = new KMXPlus.LayrEntry(); + entry.id = kmxplus.strs.allocString(layer.id); // TODO-EMBED-OSK-IN-KMX: is this correct? + entry.mod = 0; + for(const row of layer.row) { + this.addRowFromTouchLayoutRow(kmxplus, entry, this.keyIdPrefix(platformName, layer.id), row); + } + form.layers.push(entry); + } + + private keyIdPrefix(platformName: TouchLayout.TouchLayoutPlatformName, layerId: string) { + return platformName + '-' + layerId + '-'; + } + + private addRowFromTouchLayoutRow(kmxplus: KMXPlus.KMXPlusData, entry: KMXPlus.LayrEntry, idPrefix: string, row: TouchLayout.TouchLayoutRow): void { + const er = new KMXPlus.LayrRow(); + + for(const key of row.key) { + this.addKeyFromTouchLayoutKey(kmxplus, er, idPrefix, key); + } + + entry.rows.push(er); + } + + private addKeyFromTouchLayoutKey(kmxplus: KMXPlus.DependencySections, er: KMXPlus.LayrRow, idPrefix: string, key: TouchLayout.TouchLayoutKey): void { + const lk = new KMXPlus.KeysKeys(); + + lk.id = kmxplus.strs.allocString(idPrefix + key.id); + lk.to = this.getKeyCap(kmxplus, key.id, key.text); // kmxplus.strs.allocString(subKey.text); + + lk.flags = 0; // TODO-EMBED-OSK-IN-KMX: extend,gap // KMXPlus.KeysKeysFlags. + + if(key.flick) { + const flicks = new KMXPlus.KeysFlicks(lk.id); + for(const direction of Object.keys(key.flick) as (keyof TouchLayout.TouchLayoutFlick)[]) { + this.addFlickFromTouchLayoutFlick(kmxplus, flicks, direction, key.flick[direction]); + } + kmxplus.keys.flicks.push(flicks); + lk.flicks = flicks.id.value; + } else { + lk.flicks = null; + } + + if(key.sk && key.sk.length) { + const { listItem, defaultId } = this.addKeysFromSubKeys(kmxplus, key.sk); + lk.longPress = listItem; + lk.longPressDefault = defaultId; + } else { + lk.longPress = null; + lk.longPressDefault = kmxplus.strs.allocString(''); + } + + if(key.multitap && key.multitap.length) { + const { listItem } = this.addKeysFromSubKeys(kmxplus, key.multitap); + lk.multiTap = listItem; + } else { + lk.multiTap = null; + } + + lk.switch = kmxplus.strs.allocString(key.nextlayer || ''); + + + lk.width = key.width ?? 100; + + if(key.hint) { + const hintDisp: KMXPlus.DispItem = { + to: null, // not used in v19 + id: null, // not used in v19 + display: kmxplus.strs.allocString(key.hint), + toId: lk.id, + flags: + KMXPlus.DispItemFlags.hintNE | + KMXPlus.DispItemFlags.isId, // TODO-EMBED-OSK-IN-KMX: Do we need to support special key caps here? + }; + kmxplus.disp.disps.push(hintDisp); + } + + // TODO-EMBED-OSK-IN-KMX key.pad + // TODO-EMBED-OSK-IN-KMX key.sp + + kmxplus.keys.keys.push(lk); + er.keys.push(lk.id); + } + + private addKeysFromSubKeys(kmxplus: KMXPlus.DependencySections, sk: TouchLayout.TouchLayoutSubKey[]) { + let defaultId: KMXPlus.StrsItem = null; + const ids: string[] = []; + for(const subKey of sk) { + const lk = this.keyFromSubKey(kmxplus, subKey); + kmxplus.keys.keys.push(lk); + if(subKey.default) { + defaultId = lk.id; + } + ids.push(lk.id.value); + } + + if(defaultId === null) { + defaultId = kmxplus.strs.allocString(ids.length ? ids[0] : ''); + } + + const listItem = kmxplus.list.allocList(ids, {}, kmxplus); + return { listItem, defaultId }; + } + + private addFlickFromTouchLayoutFlick(kmxplus: KMXPlus.DependencySections, flicks: KMXPlus.KeysFlicks, direction: string, subKey: TouchLayout.TouchLayoutSubKey): void { + const flick = new KMXPlus.KeysFlick(); + flick.directions = kmxplus.list.allocList([direction], {}, kmxplus); + flick.keyId = kmxplus.strs.allocString(subKey.id); + + const lk = this.keyFromSubKey(kmxplus, subKey); + kmxplus.keys.keys.push(lk); + flicks.flicks.push(flick); + } + + private keyFromSubKey(kmxplus: KMXPlus.DependencySections, subKey: TouchLayout.TouchLayoutSubKey) { + const lk = new KMXPlus.KeysKeys(); + lk.id = kmxplus.strs.allocString(subKey.id); + lk.flags = 0; + lk.flicks = null; + lk.longPress = null; + lk.longPressDefault = kmxplus.strs.allocString(''); + lk.multiTap = null; + lk.switch = kmxplus.strs.allocString(subKey.nextlayer || ''); + // TODO: Disp + lk.to = this.getKeyCap(kmxplus, subKey.id, subKey.text); // kmxplus.strs.allocString(subKey.text); + lk.width = 100; + return lk; + } + + private getKeyCap(kmxplus: KMXPlus.DependencySections, id: string, text: string) { + text = text ?? ''; + if(text.match(/^\*(.+)\*$/)) { + const disp: KMXPlus.DispItem = { + to: null, // not used in v19 + id: null, // not used in v19 + display: kmxplus.strs.allocString(''), + toId: kmxplus.strs.allocString(id), + flags: + KMXPlus.DispItemFlags.hintPrimary | + KMXPlus.DispItemFlags.isId | + KMXPlus.DispItemFlags.keyCap123, //TODO-EMBED-OSK-IN-KMX + } + kmxplus.disp.disps.push(disp); + return kmxplus.strs.allocString(''); + } else if(text.trim() === '' && id.startsWith('U_')) { + // if key cap == U_xxxx[_yyyy], then we generate key cap from that + return kmxplus.strs.allocString(this.unicodeKeyIdToString(id)); + } else { + return kmxplus.strs.allocString(text); + } + } + + private unicodeKeyIdToString(id: string) { + // Test for fall back to U_xxxxxx key id + // For this first test, we ignore the keyCode and use the keyName + if(!id || id.substring(0, 2) != 'U_') { + return null; + } + + let result = ''; + const codePoints = id.substring(2).split('_'); + for(const codePoint of codePoints) { + const codePointValue = parseInt(codePoint, 16); + if (isNaN(codePointValue) || (0x0 <= codePointValue && codePointValue <= 0x1F) || (0x80 <= codePointValue && codePointValue <= 0x9F)) { + // Code points [U_0000 - U_001F] and [U_0080 - U_009F] refer to Unicode C0 and C1 control codes. + // Check the codePoint number and do not allow output of these codes via U_xxxxxx shortcuts. + // Also handles invalid identifiers (e.g. `U_ghij`) for which parseInt returns NaN + + // We'll still attempt to add valid chars + continue; + } else { + result += String.fromCodePoint(codePointValue); + } + } + return result ? result : null; + } + + public unitTestEndpoints = { + // public loadTouchLayoutFile: this.loadTouchLayoutFile.bind(this), + // public transformTouchLayoutToKmxPlus: this.transformTouchLayoutToKmxPlus.bind(this), + addPlatformFromTouchLayoutPlatform: this.addPlatformFromTouchLayoutPlatform.bind(this), + addLayerFromTouchLayoutLayer: this.addLayerFromTouchLayoutLayer.bind(this), + addRowFromTouchLayoutRow: this.addRowFromTouchLayoutRow.bind(this), + addKeyFromTouchLayoutKey: this.addKeyFromTouchLayoutKey.bind(this), + addKeysFromSubKeys: this.addKeysFromSubKeys.bind(this), + addFlickFromTouchLayoutFlick: this.addFlickFromTouchLayoutFlick.bind(this), + keyFromSubKey: this.keyFromSubKey.bind(this), + } + +} \ No newline at end of file diff --git a/developer/src/kmc-kmn/src/compiler/embed-osk/embed-osk.ts b/developer/src/kmc-kmn/src/compiler/embed-osk/embed-osk.ts index 1d00e8b1fdb..fe32131f717 100644 --- a/developer/src/kmc-kmn/src/compiler/embed-osk/embed-osk.ts +++ b/developer/src/kmc-kmn/src/compiler/embed-osk/embed-osk.ts @@ -12,8 +12,7 @@ import { KMXPlusVersion } from "@keymanapp/ldml-keyboard-constants"; import { KmnCompilerOptions } from "../compiler.js"; import { PuaMap, loadKvkFile } from "../osk.js"; import { EmbedOskKvkInKmx } from "./embed-osk-kvk.js"; - -// import { EmbedOskTouchLayoutInKmx } from "./embed-osk-touch-layout.js"; +import { EmbedOskTouchLayoutInKmx } from "./embed-osk-touch-layout.js"; export class EmbedOskInKmx { constructor( @@ -31,7 +30,7 @@ export class EmbedOskInKmx { kmx.kmxplus.elem = new KMXPlus.Elem(kmx.kmxplus); kmx.kmxplus.disp = new KMXPlus.Disp(); kmx.kmxplus.keys = new KMXPlus.Keys(strs); - // list? + kmx.kmxplus.list = new KMXPlus.List(strs); kmx.kmxplus.loca = new KMXPlus.Loca(); kmx.kmxplus.meta = new KMXPlus.Meta(); kmx.kmxplus.meta.author = strs.allocString(); @@ -68,6 +67,15 @@ export class EmbedOskInKmx { embedKvk.transformVisualKeyboardToKmxPlus(kmxPlus, vk); } + if(touchLayoutFilename) { + const embedTouchLayout = new EmbedOskTouchLayoutInKmx(this.callbacks); + const tl = embedTouchLayout.loadTouchLayoutFile(touchLayoutFilename); + if(!tl) { + // error will have been reported by loadTouchLayoutFile + return null; + } + embedTouchLayout.transformTouchLayoutToKmxPlus(kmxPlus, tl); + } // TODO-EMBED-OSK-IN-KMX: touch layout to ldml // TODO-EMBED-OSK-IN-KMX: display map remapping diff --git a/developer/src/kmc-kmn/test/embed-osk-touch-layout.tests.ts b/developer/src/kmc-kmn/test/embed-osk-touch-layout.tests.ts new file mode 100644 index 00000000000..c78fe698ec9 --- /dev/null +++ b/developer/src/kmc-kmn/test/embed-osk-touch-layout.tests.ts @@ -0,0 +1,328 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ +import 'mocha'; +import { assert } from 'chai'; +import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; +import { KMXPlus, LdmlKeyboardTypes, TouchLayout } from '@keymanapp/common-types'; +import { EmbedOskInKmx } from '../src/compiler/embed-osk/embed-osk.js'; +import { EmbedOskTouchLayoutInKmx } from '../src/compiler/embed-osk/embed-osk-touch-layout.js'; +import { oskFontMagicToken } from '@keymanapp/developer-utils'; + +const Q_KEY: TouchLayout.TouchLayoutKey = { + "id": "K_Q", + "text": "q", + "hint": "1", + "width": 80, + "pad": 20, + "nextlayer": "shift", + "multitap": [ + { + "id": "U_1235" + } + ], + "sk": [ + { + "id": "K_ENTER", + "text": "*Enter*" + } + ], + "flick": { + "s": { + "text": "!", // not '1', to avoid false matches + "id": "K_1" + } + } +}; + +// TODO-EMBED-OSK-IN-KMX: +// const SP_KEYS: TouchLayout.TouchLayoutKey[] = [ +// { "id": "K_A", "text": "special", "sp": TouchLayout.TouchLayoutKeySp.special, "width": 100, }, +// { "id": "K_B", "text": "specialActive", "sp": TouchLayout.TouchLayoutKeySp.specialActive, "width": 100, }, +// { "id": "K_C", "text": "customSpecial", "sp": TouchLayout.TouchLayoutKeySp.customSpecial, "width": 100, }, +// { "id": "K_D", "text": "customSpecialActive", "sp": TouchLayout.TouchLayoutKeySp.customSpecialActive, "width": 100, }, +// { "id": "K_E", "text": "deadkey", "sp": TouchLayout.TouchLayoutKeySp.deadkey, "width": 100, }, +// { "id": "K_F", "text": "blank", "sp": TouchLayout.TouchLayoutKeySp.blank, "width": 100, }, +// { "id": "K_G", "text": "spacer", "sp": TouchLayout.TouchLayoutKeySp.spacer, "width": 100, }, +// ]; + +describe('Compiler OSK Embedding', function() { + let kmxplus: KMXPlus.KMXPlusData = null; + let kmxfile: KMXPlus.KMXPlusFile = null; + const callbacks = new TestCompilerCallbacks(); + + this.beforeEach(function() { + callbacks.clear(); + + kmxfile = new EmbedOskInKmx(callbacks,{}).unitTestEndpoints.createEmptyKmxPlusFile(); + assert.isNotNull(kmxfile); + kmxplus = kmxfile.kmxplus; + }); + + this.afterEach(function() { + if(this.currentTest.isFailed()) { + callbacks.printMessages(); + } + kmxfile = null; + kmxplus = null; + }); + + describe('EmbedOskTouchLayoutInKmx', function() { + const embedder = new EmbedOskTouchLayoutInKmx(callbacks); + + describe('EmbedOskTouchLayoutInKmx.keyFromTouchLayoutKey', function() { + it('should convert a touch layout key into a KMX+ key', async function() { + const er = new KMXPlus.LayrRow(); + embedder.unitTestEndpoints.addKeyFromTouchLayoutKey(kmxplus, er, 'phone-default-', Q_KEY); + assert.lengthOf(callbacks.messages, 0); + + // --- kmxplus --- + + assert.hasAllKeys(kmxplus, + ['elem', 'disp', 'keys', 'layr', 'list', 'loca', 'meta', 'strs'] + ); + + // these should all be defined by addKeyFromTouchLayoutKey or initialization + assert.isObject(kmxplus.elem); + assert.isObject(kmxplus.disp); + assert.isObject(kmxplus.keys); + assert.isObject(kmxplus.layr); + assert.isObject(kmxplus.list); + assert.isObject(kmxplus.loca); + assert.isObject(kmxplus.meta); + assert.isObject(kmxplus.strs); + + // --- kmxplus.strs --- + + assert.lengthOf(kmxplus.strs.strings, 11); + assert.equal(kmxplus.strs.strings[0].value, ''); // null string, always defined + assert.equal(kmxplus.strs.strings[1].value, 'phone-default-K_Q'); // id of Q key + assert.equal(kmxplus.strs.strings[2].value, 'q'); // text of Q key + assert.equal(kmxplus.strs.strings[3].value, 's'); // south-flick direction + assert.equal(kmxplus.strs.strings[4].value, 'K_1'); // flick id + assert.equal(kmxplus.strs.strings[5].value, '!'); // flick text + assert.equal(kmxplus.strs.strings[6].value, 'K_ENTER'); // longpress id + assert.equal(kmxplus.strs.strings[7].value, 'U_1235'); // multitap id + assert.equal(kmxplus.strs.strings[8].value, 'ስ'); // multitap output (inferred from id) + assert.equal(kmxplus.strs.strings[9].value, 'shift'); // nextlayer on Q key + assert.equal(kmxplus.strs.strings[10].value, '1'); // hint on Q key + + // --- kmxplus.keys --- + + // There should be 4 keys: K_Q, U_1235, K_ENTER, and K_1 + // TODO-EMBED-OSK-IN-KMX: we need a gap key for pad=20 + assert.sameDeepMembers(kmxplus.keys.keys, [ + { // K_1 + flags: 0, + flicks: null, + id: kmxplus.strs.strings[4], + longPress: null, + longPressDefault: kmxplus.strs.strings[0], + multiTap: null, + switch: kmxplus.strs.strings[0], + to: kmxplus.strs.strings[5], + width: 100 + }, + { // K_ENTER + flags: 0, + flicks: null, + id: kmxplus.strs.strings[6], + longPress: null, + longPressDefault: kmxplus.strs.strings[0], + multiTap: null, + switch: kmxplus.strs.strings[0], + to: kmxplus.strs.strings[0], + width: 100 + }, + { // U_1235 + flags: 0, + flicks: null, + id: kmxplus.strs.strings[7], + longPress: null, + longPressDefault: kmxplus.strs.strings[0], + multiTap: null, + switch: kmxplus.strs.strings[0], + to: kmxplus.strs.strings[8], + width: 100 + }, + { // K_Q + flags: 0, + flicks: "phone-default-K_Q", + id: kmxplus.strs.strings[1], + longPress: kmxplus.list.lists[2], + longPressDefault: kmxplus.strs.strings[6], + multiTap: kmxplus.list.lists[3], + switch: kmxplus.strs.strings[9], + to: kmxplus.strs.strings[2], + width: 80 + }, + ]); + + assert.lengthOf(kmxplus.keys.flicks, 2); + + // null flick + assert.lengthOf(kmxplus.keys.flicks[0].flicks, 0); + assert.equal(kmxplus.keys.flicks[0].id, kmxplus.strs.strings[0]); + + // south-flick K_1 (phone-default-K_Q) + assert.lengthOf(kmxplus.keys.flicks[1].flicks, 1); + assert.equal(kmxplus.keys.flicks[1].id, kmxplus.strs.strings[1]); + assert.equal(kmxplus.keys.flicks[1].flicks[0].directions, kmxplus.list.lists[1]); + assert.equal(kmxplus.keys.flicks[1].flicks[0].keyId, kmxplus.strs.strings[4]); + + assert.isArray(kmxplus.keys.kmap); + assert.isEmpty(kmxplus.keys.kmap); + + // --- kmxplus.disp --- + + assert.sameDeepMembers(kmxplus.disp.disps, [ + { // K_ENTER key cap + display: kmxplus.strs.strings[0], + flags: KMXPlus.DispItemFlags.hintPrimary | KMXPlus.DispItemFlags.isId | KMXPlus.DispItemFlags.keyCap123, + id: null, + to: null, + toId: kmxplus.strs.strings[6], + }, + { // phone-default-K_Q hint + display: kmxplus.strs.strings[10], + flags: KMXPlus.DispItemFlags.hintNE | KMXPlus.DispItemFlags.isId, + id: null, + to: null, + toId: kmxplus.strs.strings[1], + } + ]); + + // --- kmxplus.elem --- + + assert.sameDeepMembers(kmxplus.elem.strings, [[]]); + + // --- kmxplus.layr --- + + assert.sameDeepMembers(kmxplus.layr.forms, []); + + // --- kmxplus.list --- + + assert.sameDeepMembers(kmxplus.list.lists, [ + [], + [{value: kmxplus.strs.strings[3]}], + [{value: kmxplus.strs.strings[6]}], + [{value: kmxplus.strs.strings[7]}], + ]); + + // --- kmxplus.loca --- + + assert.sameDeepMembers(kmxplus.loca.locales, []); + + // --- kmxplus.meta --- + + assert.equal(kmxplus.meta.author, kmxplus.strs.strings[0]); + assert.equal(kmxplus.meta.conform, kmxplus.strs.strings[0]); + assert.equal(kmxplus.meta.layout, kmxplus.strs.strings[0]); + assert.equal(kmxplus.meta.name, kmxplus.strs.strings[0]); + assert.equal(kmxplus.meta.indicator, kmxplus.strs.strings[0]); + assert.equal(kmxplus.meta.version, kmxplus.strs.strings[0]); + assert.equal(kmxplus.meta.settings, 0); + + // --- er: the returned row --- + + assert.isArray(er.keys); + assert.lengthOf(er.keys, 1); + assert.equal(er.keys[0].value, kmxplus.keys.keys[3].id.value); + }); + + // TODO-EMBED-OSK-IN-KMX: it should do various flags + }); + + describe('EmbedOskTouchLayoutInKmx.addRowFromTouchLayoutRow', function() { + it('should build a row of keys', async function() { + const entry = new KMXPlus.LayrEntry(); + const row = { id: 1, key: [ Q_KEY ] }; + embedder.unitTestEndpoints.addRowFromTouchLayoutRow(kmxplus, entry, 'row', row); + + assert.lengthOf(callbacks.messages, 0); + assert.lengthOf(entry.rows, 1); + assert.lengthOf(entry.rows[0].keys, 1); + }); + }); + + describe('EmbedOskTouchLayoutInKmx.addLayerFromTouchLayoutLayer', function() { + it('should transform a touch layout layer into a KMX+ structure', async function() { + const layer: TouchLayout.TouchLayoutLayer = { + id: 'default', row: [ { id: 1, key: [ Q_KEY ] } ] + }; + + const form = new KMXPlus.LayrForm(); + + embedder.unitTestEndpoints.addLayerFromTouchLayoutLayer(kmxplus, form, 'phone', layer); + + assert.lengthOf(callbacks.messages, 0); + assert.lengthOf(form.layers, 1); + assert.equal(form.layers[0].id.value, 'default'); + assert.equal(form.layers[0].mod, 0); + assert.lengthOf(form.layers[0].rows, 1); + }); + }); + + describe('EmbedOskTouchLayoutInKmx.platformFromTouchLayoutPlatform', function() { + it('should transform a whole platform into a KMX+ structure', async function() { + const platform: TouchLayout.TouchLayoutPlatform = { + defaultHint: 'dot', + layer: [ + { id: 'default', row: [ { id: 1, key: [ Q_KEY ] } ] } + ], + displayUnderlying: false, + }; + + embedder.unitTestEndpoints.addPlatformFromTouchLayoutPlatform(kmxplus, 'phone', platform, 200); + + assert.lengthOf(callbacks.messages, 0); + assert.lengthOf(kmxplus.layr.forms, 1); + assert.equal(kmxplus.layr.forms[0].baseLayout, kmxplus.strs.strings[1]); + assert.equal(kmxplus.layr.forms[0].flags, 0); + assert.equal(kmxplus.layr.forms[0].fontFaceName, kmxplus.strs.strings[3]); + assert.equal(kmxplus.layr.forms[0].fontSizePct, 100); + assert.equal(kmxplus.layr.forms[0].hardware, kmxplus.strs.strings[2]); + assert.equal(kmxplus.layr.forms[0].minDeviceWidth, 200); + assert.lengthOf(kmxplus.layr.forms[0].layers, 1); + assert.equal(kmxplus.layr.forms[0].layers[0].id, kmxplus.strs.strings[4]); + assert.equal(kmxplus.layr.forms[0].layers[0].mod, 0); + + assert.equal(kmxplus.strs.strings[1].value, 'en-us'); + assert.equal(kmxplus.strs.strings[2].value, 'touch'); + assert.equal(kmxplus.strs.strings[3].value, oskFontMagicToken); // *OSK-FONT-MAGIC-TOKEN-OSK-FONT* + }); + }); + + describe('EmbedOskTouchLayoutInKmx.transformTouchLayoutToKmxPlus', function() { + it('should transform a whole touch layout file into a KMX+ structure', async function() { + const tl: TouchLayout.TouchLayoutFile = { + phone: { + defaultHint: 'dot', + layer: [ + { id: 'default', row: [ { id: 1, key: [ Q_KEY ] } ] } + ], + displayUnderlying: false, + } + }; + + embedder.transformTouchLayoutToKmxPlus(kmxfile, tl); + assert.lengthOf(callbacks.messages, 0); + assert.lengthOf(kmxplus.layr.forms, 1); + + assert.equal(kmxplus.layr.forms[0].baseLayout, kmxplus.strs.strings[1]); + assert.equal(kmxplus.layr.forms[0].flags, 0); + assert.equal(kmxplus.layr.forms[0].fontFaceName, kmxplus.strs.strings[3]); + assert.equal(kmxplus.layr.forms[0].fontSizePct, 100); + assert.equal(kmxplus.layr.forms[0].hardware, kmxplus.strs.strings[2]); + assert.equal(kmxplus.layr.forms[0].minDeviceWidth, 1); + assert.lengthOf(kmxplus.layr.forms[0].layers, 1); + assert.equal(kmxplus.layr.forms[0].layers[0].id, kmxplus.strs.strings[4]); + assert.equal(kmxplus.layr.forms[0].layers[0].mod, 0); + + assert.equal(kmxplus.strs.strings[1].value, 'en-us'); + assert.equal(kmxplus.strs.strings[2].value, 'touch'); + assert.equal(kmxplus.strs.strings[3].value, oskFontMagicToken); // *OSK-FONT-MAGIC-TOKEN-OSK-FONT* + }); + }); + }); +});