From 51a94a1d7dc11e506da006c07ccab3051efed8c7 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Sat, 9 Jul 2022 14:51:20 +1000 Subject: [PATCH] feat(developer): cleanup touch layout files on load and save Improves the .keyman-touch-layout clean schema and adds additional cleanup during load and save in the Touch Layout Editor. The compiler already does transforms for the relevant fields to the formats that KeymanWeb is expecting (in particular, width, pad to string). This is preparation for adding hint data to the format as well. --- common/schemas/keyman-touch-layout/README.md | 17 ++-- .../keyman-touch-layout.clean.spec.json | 30 +++++-- .../src/tike/xml/layoutbuilder/builder.js | 20 +---- .../src/tike/xml/layoutbuilder/builder.xsl | 1 + .../src/tike/xml/layoutbuilder/cleanup.js | 79 +++++++++++++++++++ 5 files changed, 114 insertions(+), 33 deletions(-) create mode 100644 developer/src/tike/xml/layoutbuilder/cleanup.js diff --git a/common/schemas/keyman-touch-layout/README.md b/common/schemas/keyman-touch-layout/README.md index 193dc6a3b8..70664b5769 100644 --- a/common/schemas/keyman-touch-layout/README.md +++ b/common/schemas/keyman-touch-layout/README.md @@ -42,7 +42,8 @@ The general structure of a .keyman-touch-layout file is: ## `root` object The `root` object may contain up to three properties, each being a [`platform` -object](#platform-object), named `"phone"`, `"tablet"` or `"desktop"`. +object](#platform-object), named `"phone"`, `"tablet"` or `"desktop"`. There +must be at least one property. ## `platform` object @@ -68,7 +69,7 @@ optionally contain three additional properties: `font`, `fontsize`, and `displayUnderlying`. * `layer` -: An array of [`layer` objects](#layer-object). Required. +: A non-empty array of [`layer` objects](#layer-object). Required. * `font` : An optional CSS-style font-family specification, being the name of the font to @@ -140,7 +141,7 @@ affect the modifiers in any key events generated by keys on the layer. * `row` -: An array of [`row` objects](#row-object). Required. +: A non-empty array of [`row` objects](#row-object). Required. ## `row` object @@ -150,19 +151,19 @@ properties: `id` and `key`. * `id` : An non-negative integer denoting where the row appears, with `0` being the top - row. + row. Required. Legacy: this value was sometimes an integer in a string. This string format is deprecated and will not be supported in future versions of Keyman Developer. * `key` -: An array of [`key` objects](#key-object). +: A non-empty array of [`key` objects](#key-object). Required. ## `key` object Describes a single key on a touch layout row. See also [`sk` -object](#sk-object). A `key` object has no required properties. +object](#sk-object). A `key` object must include either an `id` or `sp` property. * `id` @@ -314,7 +315,9 @@ properties are not fully supported as of Keyman 15: `font`, `fontsize`, `pad`, and `width`. * `id` -: A string identifier for the longpress key. See `key` object for details. + +: String. Required. An identifier for the longpress key. See `key` object for + details. * `text` diff --git a/common/schemas/keyman-touch-layout/keyman-touch-layout.clean.spec.json b/common/schemas/keyman-touch-layout/keyman-touch-layout.clean.spec.json index 4647b57c53..9a3e7ee933 100644 --- a/common/schemas/keyman-touch-layout/keyman-touch-layout.clean.spec.json +++ b/common/schemas/keyman-touch-layout/keyman-touch-layout.clean.spec.json @@ -11,7 +11,9 @@ "tablet": { "$ref": "#/definitions/platform" }, "phone": { "$ref": "#/definitions/platform" }, "desktop": { "$ref": "#/definitions/platform" } - } + }, + "minProperties": 1, + "additionalProperties": false }, "platform": { @@ -28,7 +30,8 @@ "layers": { "type": "array", - "items": { "$ref": "#/definitions/layer" } + "items": { "$ref": "#/definitions/layer" }, + "minItems": 1 }, "layer": { @@ -48,7 +51,8 @@ "rows": { "type": "array", - "items": { "$ref": "#/definitions/row" } + "items": { "$ref": "#/definitions/row" }, + "minItems": 1 }, "row": { @@ -69,7 +73,8 @@ "keys": { "type": "array", - "items": { "$ref": "#/definitions/key" } + "items": { "$ref": "#/definitions/key" }, + "minItems": 1 }, "key": { @@ -88,6 +93,13 @@ "flick": { "$ref": "#/definitions/flick" }, "multitap": { "$ref": "#/definitions/subkeys" } }, + "anyOf": [ + {"required": ["id"]}, + {"required": ["sp"]}, + {"required": ["sk"]}, + {"required": ["flick"]}, + {"required": ["multitap"]} + ], "additionalProperties": false }, @@ -98,24 +110,25 @@ "key-sp": { "type": "integer", - "enum": [0, 1, 2, 8, 9, 10], + "enum": [0, 1, 2, 8, 9, 10] }, "key-pad": { "type": "number", "minimum": 0, - "maximum": 10000 + "maximum": 100000 }, "key-width": { "type": "number", "minimum": 0, - "maximum": 1000 + "maximum": 100000 }, "subkeys": { "type": "array", - "items": { "$ref": "#/definitions/subkey" } + "items": { "$ref": "#/definitions/subkey" }, + "minItems": 1 }, "subkey": { @@ -140,6 +153,7 @@ "patternProperties": { "^(n|s|e|w|ne|nw|se|sw)$": { "$ref": "#/definitions/subkey" } }, + "minProperties": 1, "additionalProperties": false }, diff --git a/developer/src/tike/xml/layoutbuilder/builder.js b/developer/src/tike/xml/layoutbuilder/builder.js index cee6a15b38..de060b42af 100644 --- a/developer/src/tike/xml/layoutbuilder/builder.js +++ b/developer/src/tike/xml/layoutbuilder/builder.js @@ -813,7 +813,7 @@ $(function() { }; this.saveJSON = function(force) { - builder.removeEmptyRows(); + builder.cleanupKVKL(); var newJson = JSON.stringify(KVKL, null, ' '); if(force || newJson != json) { // When adding or deleting layers and platforms, we need to force because @@ -978,23 +978,6 @@ $(function() { ); }; - builder.removeEmptyRows = function() { - var json = JSON.stringify(KVKL, null, ' '); - for (var platform in KVKL) { - for (let layer of KVKL[platform].layer) { - let newRow = [], n = 1; - for(let row of layer.row) { - if(row.key.length > 0) { - row.id = n; - n++; - newRow.push(row); - } - } - layer.row = newRow; - } - } - }; - builder.textControlsInToolbar = function() { return $('body').hasClass('text-controls-in-toolbar'); } @@ -1004,6 +987,7 @@ $(function() { function initBuilder() { $(function() { + builder.cleanupKVKL(); builder.prepareKeyCapTypes(); builder.preparePlatforms(); builder.enableUndoControls(); diff --git a/developer/src/tike/xml/layoutbuilder/builder.xsl b/developer/src/tike/xml/layoutbuilder/builder.xsl index 0f28bfae03..548a38321c 100644 --- a/developer/src/tike/xml/layoutbuilder/builder.xsl +++ b/developer/src/tike/xml/layoutbuilder/builder.xsl @@ -30,6 +30,7 @@ + diff --git a/developer/src/tike/xml/layoutbuilder/cleanup.js b/developer/src/tike/xml/layoutbuilder/cleanup.js new file mode 100644 index 0000000000..3bd75bcd38 --- /dev/null +++ b/developer/src/tike/xml/layoutbuilder/cleanup.js @@ -0,0 +1,79 @@ +$(function() { + + builder.cleanupKVKL = function() { + let cleanupEmptyArrays = function() { + for (let platform in KVKL) { + for (let layer of KVKL[platform].layer) { + let newRow = [], n = 1; + for(let row of layer.row) { + if(row.key && Array.isArray(row.key)) { + for(let key of row.key) { + if(key.sk && (!Array.isArray(key.sk) || key.sk.length == 0)) { + delete key.sk; + } + } + if(row.key.length > 0) { + row.id = n; + n++; + newRow.push(row); + } + } + } + layer.row = newRow; + } + } + return true; + }; + + let cleanupNumericField = function(obj, prop) { + if(typeof obj[prop] == 'number') { + return true; + } + if(typeof obj[prop] == 'string') { + let v = obj[prop].trim(); + if(v != '') { + v = parseInt(v, 10); + if(isFinite(v)) { + obj[prop] = v; + return true; + } + } + } + delete obj[prop]; + return true; + } + + let cleanupKeyTypes = function(key) { + // Delete empty strings for id + if(typeof key.id != 'undefined') { + if(typeof key.id != 'string') delete key.id; + else if(key.id.trim() == '') delete key.id; + } + // Enforce numeric types for sp, pad, width + return cleanupNumericField(key, 'sp') && + cleanupNumericField(key, 'pad') && + cleanupNumericField(key, 'width'); + } + + let cleanupTypes = function() { + for(let platform in KVKL) { + for(let layer of KVKL[platform].layer) { + for(let row of layer.row) { + for(let key of row.key) { + cleanupKeyTypes(key); + if(key.sk) { + for(let sk of key.sk) { + cleanupKeyTypes(sk); + } + } + } + } + } + } + return true; + } + + return cleanupEmptyArrays() && cleanupTypes(); + }; + +}.bind(builder));