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));