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