diff --git a/web/history.md b/web/history.md index dd9e80727b..3b1fcca013 100644 --- a/web/history.md +++ b/web/history.md @@ -3,6 +3,9 @@ ## 13.0 alpha * Start version 13.0 +## 2019-09-06 12.0.81 beta +* For keyboards compiled with Developer 12.0 onward, default key caps will no longer be provided. (#2045) + ## 2019-09-04 12.0.80 beta * Fixed issue with 'pt' and unitless font size specifications for keyboard layouts (#2033) * Slightly enhanced error logging for internal events (#2037) diff --git a/web/source/osk/defaultLayouts.ts b/web/source/osk/defaultLayouts.ts index f8624b3f79..f6088746ff 100644 --- a/web/source/osk/defaultLayouts.ts +++ b/web/source/osk/defaultLayouts.ts @@ -3,6 +3,8 @@ Copyright 2017 SIL International ***/ +/// + namespace com.keyman.osk { let Codes = com.keyman.text.Codes; @@ -109,12 +111,13 @@ namespace com.keyman.osk { /** * Build a default layout for keyboards with no explicit layout * - * @param {Object} PVK keyboard object (as loaded) - * @param {number} kbdBitmask keyboard modifier bitmask + * @param {Object} PVK keyboard object (as loaded) + * @param {Object} kbdDevVersion object representing the version of Developer that compiled the keyboard + * @param {number} kbdBitmask keyboard modifier bitmask * @param {string} formFactor * @return {Object} */ - static buildDefaultLayout(PVK, kbdBitmask: number, formFactor: string): LayoutFormFactor { + static buildDefaultLayout(PVK, kbdDevVersion: utils.Version, kbdBitmask: number, formFactor: string): LayoutFormFactor { let keyman = com.keyman.singleton; let util = keyman.util; @@ -248,9 +251,9 @@ namespace com.keyman.osk { if(kx >= 0 && kx < layerSpec.length) key['text']=layerSpec[kx]; } - // Fall back to US English keycap text as default for the base two layers if not otherwise defined. - // (Any 'ghost' keys must be explicitly defined in layout for these layers.) - if(isDefault) { + // Legacy (pre 12.0) behavior: fall back to US English keycap text as default for the base two layers + // if a key cap is not otherwise defined. (Any intentional 'ghost' keys must be explicitly defined.) + if(isDefault && kbdDevVersion.precedes(utils.Version.NO_DEFAULT_KEYCAPS)) { if(key['id'] != 'K_SPACE' && kx+65 * isShift < Layouts.dfltText.length && key['text'] !== null) { key['text'] = key['text'] || Layouts.dfltText[kx+65*isShift]; } diff --git a/web/source/osk/visualKeyboard.ts b/web/source/osk/visualKeyboard.ts index ab629036d6..beffc245cc 100644 --- a/web/source/osk/visualKeyboard.ts +++ b/web/source/osk/visualKeyboard.ts @@ -1,4 +1,5 @@ /// +/// namespace com.keyman.osk { let Codes = com.keyman.text.Codes; @@ -591,7 +592,8 @@ namespace com.keyman.osk { // Build a layout using the default for the device if(typeof layout != 'object' || layout == null) { - layout=Layouts.buildDefaultLayout(PVK,kbdBitmask, formFactor); + let kbdDevVersion = new utils.Version(activeKeyboard['KVER']); + layout=Layouts.buildDefaultLayout(PVK, kbdDevVersion, kbdBitmask, formFactor); } // Create the collection of HTML elements from the device-dependent layout object @@ -2093,7 +2095,8 @@ namespace com.keyman.osk { // Else get a default layout for the device for this keyboard if(layout == null && PVK != null) { - layout=Layouts.buildDefaultLayout(PVK,keymanweb.keyboardManager.getKeyboardModifierBitmask(PKbd),formFactor); + let kbdDevVersion = new utils.Version(PKbd['KVER']); + layout=Layouts.buildDefaultLayout(PVK, kbdDevVersion, keymanweb.keyboardManager.getKeyboardModifierBitmask(PKbd),formFactor); } // Cannot create an OSK if no layout defined, just return empty DIV diff --git a/web/source/utils/version.ts b/web/source/utils/version.ts new file mode 100644 index 0000000000..3d43ee7ea8 --- /dev/null +++ b/web/source/utils/version.ts @@ -0,0 +1,101 @@ +namespace com.keyman.utils { + // Dotted-decimal version + export class Version { + // Represents a default version value for keyboards compiled before this was compiled into keyboards. + // The exact version is unknown at this point, but the value is "good enough" for what we need. + public static readonly DEVELOPER_VERSION_FALLBACK = new Version([9, 0, 0]); + + // For 12.0, the old default behavior of adding missing keycaps to the default layers was removed, + // as it results in unexpected, bug-like behavior for keyboard designers when it is unwanted. + public static readonly NO_DEFAULT_KEYCAPS = new Version([12, 0]); + + private readonly components: number[] + + /** + * Parses version information, preparing it for use in comparisons. + * @param text Either a string representing a version number (ex: "9.0.0") or an array representing + * its components (ex: [9, 0, 0]). + */ + constructor(text: String | number[]) { + // If a keyboard doesn't specify a version, use the DEVELOPER_VERSION_FALLBACK values. + if(text === undefined || text === null) { + this.components = [].concat(Version.DEVELOPER_VERSION_FALLBACK.components); + return; + } + + if(Array.isArray(text)) { + let components = text as number[]; + if(components.length < 2) { + throw new Error("Version string must have at least a major and minor component!"); + } else { + this.components = [].concat(components); + return; + } + } + + // else, standard constructor path. + let parts = text.split('.'); + let componentArray: number[] = []; + + if(parts.length < 2) { + throw new Error("Version string must have at least a major and minor component!"); + } + + for(let i=0; i < parts.length; i++) { + let value = parseInt(parts[i], 10); + if(isNaN(value)) { + throw new Error("Version string components must be numerical!"); + } + + componentArray.push(value); + } + + this.components = componentArray; + } + + get major(): number { + return this.components[0]; + } + + get minor(): number { + return this.components[1]; + } + + toString(): string { + return this.components.join('.'); + } + + equals(other: Version): boolean { + return this.compareTo(other) == 0; + } + + precedes(other: Version): boolean { + return this.compareTo(other) < 0; + } + + compareTo(other: Version): number { + // If the version info depth differs, we need a flag to indicate which instance is shorter. + var isShorter: boolean = this.components.length < other.components.length; + var maxDepth: number = (this.components.length < other.components.length) ? this.components.length : other.components.length; + + var i: number; + for(i = 0; i < maxDepth; i++) { + let delta = this.components[i] - other.components[i]; + if(delta != 0) { + return delta; + } + } + + var longList = isShorter ? other.components : this.components; + do { + if(longList[i] > 0) { + return isShorter ? -1 : 1; + } + i++; + } while (i < longList.length); + + // Equal. + return 0; + } + } +} \ No newline at end of file diff --git a/web/unit_tests/cases/versions.js b/web/unit_tests/cases/versions.js new file mode 100644 index 0000000000..bd77a9039d --- /dev/null +++ b/web/unit_tests/cases/versions.js @@ -0,0 +1,53 @@ +var assert = chai.assert; + +describe('Version Logic', function() { + this.timeout(kmwconfig.timeouts.standard); + + before(function(done) { + this.timeout(kmwconfig.timeouts.scriptLoad); + setupKMW(null, done, kmwconfig.timeouts.scriptLoad); + }); + + after(function() { + teardownKMW(); + }); + + it('Should provide a default, fallback value when nothing is specified', function() { + var fallback = new com.keyman.utils.Version(undefined); + assert.isTrue(fallback.equals(com.keyman.utils.Version.DEVELOPER_VERSION_FALLBACK)); + }); + + it('Should properly process a simple major.minor version string.', function() { + var version = new com.keyman.utils.Version("1.2"); + assert.equal(version.major, 1); + assert.equal(version.minor, 2); + }); + + it('Should handle long/deep version specifications.', function() { + var version = new com.keyman.utils.Version("1.2.3.4.5.6"); + assert.equal(version.components.length, 6); + assert.equal(version.major, 1); + assert.equal(version.minor, 2); + }); + + it('Should properly compare two versions.', function() { + var v9_0_1 = new com.keyman.utils.Version("9.0.1"); + var v9_1_0 = new com.keyman.utils.Version("9.1.0"); + var v10_0 = new com.keyman.utils.Version("10.0"); + var v10_0_0 = new com.keyman.utils.Version("10.0.0"); + + // "Precede" checks + assert.equal(v9_0_1.compareTo(v9_1_0), -1); + assert.equal(v9_1_0.compareTo(v10_0_0), -1); + assert.equal(v9_0_1.compareTo(v10_0_0), -1); + + // Equality checks + assert.equal(v9_0_1.compareTo(v9_0_1), 0); + // Tests equal versions where one omits the build number. + assert.equal( v10_0.compareTo(v10_0_0), 0); + + // Ensures the first "precede" check's return is flipped when the order's flipped. + assert.equal(v9_1_0.compareTo(v9_0_1), 1); + }); + +}); \ No newline at end of file