diff --git a/web/bulk_rendering/build.sh b/web/bulk_rendering/build.sh
new file mode 100644
index 0000000000..6b7006af15
--- /dev/null
+++ b/web/bulk_rendering/build.sh
@@ -0,0 +1,54 @@
+#! /bin/bash
+#
+# Compile the KeymanWeb bulk-renderer module for use with developing/running engine tests.
+#
+
+# Fails the build if a specified file does not exist.
+assert ( ) {
+ if ! [ -f $1 ]; then
+ fail "Build failed."
+ exit 1
+ fi
+}
+
+fail() {
+ FAILURE_MSG="$1"
+ if [[ "$FAILURE_MSG" == "" ]]; then
+ FAILURE_MSG="Unknown failure"
+ fi
+ echo "${ERROR_RED}$FAILURE_MSG${NORMAL}"
+ exit 1
+}
+
+# Ensure the dependencies are downloaded. --no-optional should help block fsevents warnings.
+echo "Node.js + dependencies check"
+npm install --no-optional
+
+if [ $? -ne 0 ]; then
+ fail "Build environment setup error detected! Please ensure Node.js is installed!"
+fi
+
+# Definition of global compile constants
+COMPILED_FILE="bulk_render.js"
+OUTPUT="../release/renderer"
+NODE_SOURCE="bulk_rendering"
+#ENGINE_TEST_OUTPUT="../unit_tests/"
+
+readonly OUTPUT
+readonly NODE_SOURCE
+#readonly ENGINE_TEST_OUTPUT
+
+# Ensures that we rely first upon the local npm-based install of Typescript.
+# (Facilitates automated setup for build agents.)
+PATH="../../node_modules/.bin:$PATH"
+
+compiler="npm run tsc --"
+compilecmd="$compiler"
+
+$compilecmd -p $NODE_SOURCE/tsconfig.json
+if [ $? -ne 0 ]; then
+ fail "Typescript compilation failed."
+fi
+
+#cp $OUTPUT/$COMPILED_FILE $ENGINE_TEST_OUTPUT
+#cp $OUTPUT/$COMPILED_FILE.map $ENGINE_TEST_OUTPUT
\ No newline at end of file
diff --git a/web/bulk_rendering/index.html b/web/bulk_rendering/index.html
new file mode 100644
index 0000000000..437ebac78d
--- /dev/null
+++ b/web/bulk_rendering/index.html
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ KeymanWeb - On-Screen Keyboard Renders
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ KeymanWeb - Bulk On-Screen Keyboard Rendering
+
+
+
+
+
diff --git a/web/bulk_rendering/renderer_core.ts b/web/bulk_rendering/renderer_core.ts
new file mode 100644
index 0000000000..3b3bfedaed
--- /dev/null
+++ b/web/bulk_rendering/renderer_core.ts
@@ -0,0 +1,242 @@
+// Includes KeymanWeb's Device class, as it's quite a useful resource for KMW-related projects.
+///
+// Needed for OSK rendering to image files.
+///
+// Ensure that Promises are within scope.
+///
+
+type KeyboardMap = {[id: string]: any};
+
+namespace com.keyman.renderer {
+ export class BatchRenderer {
+ static divMaster: HTMLDivElement;
+ static dummy: HTMLInputElement;
+
+ // Filters the keyboard array to ensure only a single entry remains, rather than an entry per language.
+ private filterKeyboards(): KeyboardMap {
+ let keyman = window['keyman'];
+
+ let kbds = keyman['getKeyboards']();
+
+ let keyboardMap = [];
+
+ for(var i = 0; i < kbds.length; i++) {
+ let id: string = kbds[i]['InternalName'];
+ if(keyboardMap[id]) {
+ continue;
+ } else {
+ keyboardMap[id] = kbds[i];
+ }
+ }
+
+ return keyboardMap;
+ }
+
+ private render(ele: HTMLElement, isMobile?: boolean): Promise {
+ let html2canvas = window['html2canvas'];
+
+ let imgOut = document.createElement('img');
+
+ let canvasParams = {
+ 'logging': false,
+ 'scale': 1,
+ 'width': window.innerWidth // Good for mobile, less-so for desktop.
+ }
+
+ // So, if it's desktop, we set more reasonable values.
+ if(!isMobile) {
+ canvasParams['width'] = 500;
+ ele.style.width = '500px';
+ }
+
+ return html2canvas(ele, canvasParams).then(function(canvas) {
+ imgOut.src = canvas.toDataURL();
+ return imgOut;
+ });
+ }
+
+ createKeyboardHeader(kbd, loaded: boolean): HTMLDivElement {
+ let divHeader = document.createElement('div');
+ let eleName = document.createElement('h2');
+
+ eleName.textContent = 'ID: ' + kbd['InternalName'];
+ divHeader.appendChild(eleName);
+
+ let eleDescription = document.createElement('p');
+
+ if(loaded) {
+
+ eleDescription.appendChild(document.createTextNode('Name: ' + kbd['Name']));
+ eleDescription.appendChild(document.createElement('br'));
+ eleDescription.appendChild(document.createTextNode('Font: ' + window['keyman'].keyboardManager.activeKeyboard.KV.F));
+
+ } else {
+ eleDescription.appendChild(document.createTextNode('Unable to load this keyboard!'));
+ }
+
+ divHeader.appendChild(eleDescription);
+
+ return divHeader;
+ }
+
+ private processKeyboard(kbd) {
+ let keyman = window['keyman'];
+ let p: Promise = keyman.setActiveKeyboard(kbd['InternalName']);
+ let isMobile = keyman.util.device.formFactor != 'desktop';
+
+ // Establish common keyboard header info.
+ let divSummary = document.createElement('div');
+ // Establishes a linkable target for this keyboard's data.
+ divSummary.id = "summary-" + kbd['InternalName'];
+
+ BatchRenderer.divMaster.insertAdjacentElement('afterbegin', divSummary);
+
+ // A nice, closure-friendly reference for use in our callbacks.
+ let renderer = this;
+
+ // Once the keyboard's loaded, we can really get started.
+ return p.then(function() {
+ let box: HTMLDivElement = keyman.osk._Box;
+
+ divSummary.appendChild(renderer.createKeyboardHeader(kbd, true));
+
+ let divRenders = document.createElement('div');
+ divSummary.appendChild(divRenders);
+
+ // Uses 'private' APIs that may be subject to change in the future. Keep it updated!
+ var layers;
+ if(isMobile) {
+ layers = keyman.osk.layers;
+ } else {
+ // The desktop OSK will be overpopulated, with a number of blank layers to display in most cases.
+ // We instead rely upon the KLS definition to ensure we keep the renders sparse.
+ layers = keyman.keyboardManager.activeKeyboard.KV.KLS;
+ }
+
+ let renderLayer = function(i: number) {
+ return new Promise(function(resolve) {
+ // (Private API) Directly sets the keyboard layer within KMW, then uses .show to force-display it.
+ if(isMobile) {
+ keyman.osk.layerId = layers[i].id;
+ } else {
+ keyman.osk.layerId = Object.keys(layers)[i];
+ }
+ // Make sure the active element's still set!
+ renderer.setActiveDummy();
+ keyman.osk.show(true);
+
+ renderer.render(box, isMobile).then(function(imgEle: HTMLImageElement) {
+ let eleLayer = document.createElement('div');
+ let eleLayerId = document.createElement('p');
+ eleLayerId.textContent = 'Layer ID: ' + (isMobile ? keyman.osk.layers[i].id : Object.keys(layers)[i]);
+
+ eleLayer.appendChild(eleLayerId);
+ eleLayer.appendChild(imgEle);
+ eleLayer.appendChild(document.createElement('br'));
+
+ divRenders.appendChild(eleLayer);
+ resolve(i);
+ });
+ })
+ };
+
+ // The resulting Promise will only call it's `.then()` once all of this keyboard's renders have been completed.
+ return renderer.arrayPromiseIteration(renderLayer, isMobile ? keyman.osk.layers.length : Object.keys(layers).length);
+ }).catch(function() {
+ console.log("Failed to load the \"" + kbd['InternalName'] + "\" keyboard for rendering!");
+ divSummary.appendChild(renderer.createKeyboardHeader(kbd, false));
+ return Promise.resolve();
+ });
+ }
+
+ // Synchronously performs asynchronous operations across a loop, one at a time.
+ // Necessary due to the nature of KMW OSK rendering.
+ private arrayPromiseIteration(promiseGenerator: (i: number) => Promise, length: number): Promise {
+ let iteration = function(index: number): Promise {
+ if(index < length) {
+ var promise = promiseGenerator(index);
+ return promise.then(function(index: number) {
+ return iteration(++index);
+ })
+ } else {
+ return Promise.resolve();
+ }
+ }
+
+ return iteration(0);
+ }
+
+ fillDeviceNotes() {
+ let description = document.createElement('p');
+ let device = new com.keyman.Device();
+ device.detect();
+
+ description.appendChild(document.createTextNode('Browser: ' + device.browser));
+ description.appendChild(document.createElement('br'));
+ description.appendChild(document.createTextNode('OS: ' + device.OS));
+ description.appendChild(document.createElement('br'));
+ description.appendChild(document.createTextNode('Form factor: ' + device.formFactor));
+ description.appendChild(document.createElement('br'));
+ description.appendChild(document.createTextNode('Touchable: ' + device.touchable));
+
+ document.getElementById('deviceNotes').appendChild(description);
+ }
+
+ setActiveDummy() {
+ com.keyman['DOMEventHandlers'].states.activeElement = BatchRenderer.dummy;
+ }
+
+ run() {
+ if(window['keyman']) {
+ let keyman = window['keyman'];
+
+ // Establish a 'dummy' element to bypass the 'nothing's active' check KMW usualy uses.
+ BatchRenderer.dummy = document.createElement('input');
+ this.setActiveDummy();
+
+ BatchRenderer.divMaster = document.getElementById('renderList');
+ if(BatchRenderer.divMaster.childElementCount > 0) {
+ console.log("Prior bulk-renderer run detected. Terminating execution.");
+ return;
+ }
+
+ // We want the renderer to control where the keyboard is displayed.
+ // Also bypasses another 'fun' OSK complication.
+ if(keyman.util.device.formFactor == 'desktop') {
+ keyman.osk.userPositioned = true;
+ }
+
+ // Assumes that the keyboards have been preloaded for us.
+ let kbds = this.filterKeyboards();
+
+ console.log("Unique keyboard ids detected: " + Object.keys(kbds).length);
+
+ let renderer = this;
+
+ let keyboardIterator = function(i) {
+ return new Promise(function(resolve) {
+ renderer.processKeyboard(kbds[Object.keys(kbds)[i]]).then(function () {
+ //console.log("Keyboard " + i + " processed!");
+ resolve(i);
+ });
+ });
+ };
+
+ this.arrayPromiseIteration(keyboardIterator, Object.keys(kbds).length).then(function() {
+ // Once all renders are done, we can now tidy the page up and prep it for final display + potential file-saving.
+
+ // This will go at the top of the page when finished, but not when actively rendering.
+ // We want to leave as much space visible as possible when actively rendering keyboards
+ // so that auto-scrolling isn't an issue.
+ renderer.fillDeviceNotes();
+ });
+ } else {
+ console.error("KeymanWeb not detected!");
+ }
+ }
+ }
+
+ (function(){
+ window['kmw_renderer'] = new com.keyman.renderer.BatchRenderer();
+ })();
+}
\ No newline at end of file
diff --git a/web/bulk_rendering/tsconfig.json b/web/bulk_rendering/tsconfig.json
new file mode 100644
index 0000000000..8c09d55693
--- /dev/null
+++ b/web/bulk_rendering/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "compilerOptions": {
+ "allowJs": true,
+ "inlineSources": true,
+ "module": "none",
+ "outFile": "../release/renderer/bulk_render.js",
+ "sourceMap": true,
+ "target": "es5"
+ },
+ "files": [
+ "renderer_core.ts"
+ ]
+}
diff --git a/web/history.md b/web/history.md
index 50589d2440..a456e7e161 100644
--- a/web/history.md
+++ b/web/history.md
@@ -4,6 +4,7 @@
* Add `setNumericLayer()` for embedded platforms to change OSK to numeric layer.
* Fixes issue where file extensions are upper-case, e.g. ".TTF"
* Fixes keyboard layout issues after mobile device rotations. (#248) (#970)
+* Adds support for Promises to init() and setActiveKeyboard(). (#100)
## 2018-07-06 10.0.103 stable
* Fixes issue for embedded Android, iOS apps where a keyboard with varying row counts in different layers could crash (#1055)
diff --git a/web/package-lock.json b/web/package-lock.json
index dfa4a15c27..90decfd74a 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -701,6 +701,15 @@
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
"dev": true
},
+ "css-line-break": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-1.0.1.tgz",
+ "integrity": "sha1-GfIGOjPpX7KDG4ZEbAuAwYivRQo=",
+ "dev": true,
+ "requires": {
+ "base64-arraybuffer": "^0.1.5"
+ }
+ },
"custom-event": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz",
@@ -1975,6 +1984,15 @@
"integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==",
"dev": true
},
+ "html2canvas": {
+ "version": "1.0.0-alpha.12",
+ "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.0.0-alpha.12.tgz",
+ "integrity": "sha1-OxmS48mz9WBjw1/WIElPN+uohRM=",
+ "dev": true,
+ "requires": {
+ "css-line-break": "1.0.1"
+ }
+ },
"http-errors": {
"version": "1.6.3",
"resolved": "http://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
@@ -3034,6 +3052,11 @@
"integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=",
"dev": true
},
+ "promise-polyfill": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.1.0.tgz",
+ "integrity": "sha512-OzSf6gcCUQ01byV4BgwyUCswlaQQ6gzXc23aLQWhicvfX9kfsUiUhgt3CCQej8jDnl8/PhGF31JdHX2/MzF3WA=="
+ },
"qjobs": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz",
diff --git a/web/package.json b/web/package.json
index 28d9caca54..a0389901f0 100644
--- a/web/package.json
+++ b/web/package.json
@@ -21,6 +21,7 @@
"chai": "^4.2.0",
"google-closure-compiler": "^20171203.0.0",
"google-closure-library": "^20171203.0.0",
+ "html2canvas": "^1.0.0-alpha.12",
"karma": "^3.1.1",
"karma-browserstack-launcher": "^1.3.0",
"karma-chai": "^0.1.0",
@@ -44,5 +45,8 @@
"test": "bash ./unit_tests/test.sh",
"karma": "karma",
"modernizr": "modernizr"
+ },
+ "dependencies": {
+ "promise-polyfill": "^8.1.0"
}
}
diff --git a/web/source/kmwbase.ts b/web/source/kmwbase.ts
index 369dab2be3..a72b2376cb 100644
--- a/web/source/kmwbase.ts
+++ b/web/source/kmwbase.ts
@@ -1,5 +1,7 @@
// Includes KMW-added property declaration extensions for HTML elements.
///
+// Includes a promise polyfill (needed for IE)
+///
// Defines the web-page interface object.
///
// Includes KMW-added property declaration extensions for HTML elements.
@@ -283,8 +285,8 @@ namespace com.keyman {
* @param {string} PInternalName Internal name
* @param {string} PLgCode Language code
*/
- ['setActiveKeyboard'](PInternalName: string, PLgCode: string) {
- this.keyboardManager.setActiveKeyboard(PInternalName,PLgCode);
+ ['setActiveKeyboard'](PInternalName: string, PLgCode: string): Promise {
+ return this.keyboardManager.setActiveKeyboard(PInternalName,PLgCode);
}
/**
@@ -391,8 +393,8 @@ namespace com.keyman {
* @param {Object} arg object array of user-defined properties
* Description KMW window initialization
*/
- ['init'](arg) {
- this.domManager.init(arg);
+ ['init'](arg): Promise {
+ return this.domManager.init(arg);
}
/**
diff --git a/web/source/kmwdom.ts b/web/source/kmwdom.ts
index 81be65ca48..32a4e02913 100644
--- a/web/source/kmwdom.ts
+++ b/web/source/kmwdom.ts
@@ -1467,7 +1467,7 @@ namespace com.keyman {
* @param {Object} arg object array of user-defined properties
* Description KMW window initialization
*/
- init(arg) {
+ init: (arg:any) => Promise = function(arg): Promise {
var i,j,c,e,p,eTextArea,eInput,opt,dTrailer,ds;
var osk = this.keyman.osk;
var util = this.keyman.util;
@@ -1496,7 +1496,7 @@ namespace com.keyman {
// Otherwise, assume relative to source path
return this.keyman.srcPath+p;
- }
+ }.bind(this);
// Explicit (user-defined) parameter initialization
opt=this.keyman.options;
@@ -1537,7 +1537,7 @@ namespace com.keyman {
// Only do remainder of initialization once!
if(this.keyman.initialized) {
- return;
+ return Promise.resolve();
}
var keyman: KeymanBase = this.keyman;
@@ -1546,10 +1546,13 @@ namespace com.keyman {
// Do not initialize until the document has been fully loaded
if(document.readyState !== 'complete')
{
- window.setTimeout(function(){
- domManager.init(arg);
- }, 50);
- return;
+ return new Promise(function(resolve) {
+ window.setTimeout(function(){
+ domManager.init(arg).then(function() {
+ resolve();
+ });
+ }, 50);
+ });
}
this.keyman._MasterDocument = window.document;
@@ -1592,7 +1595,7 @@ namespace com.keyman {
if(!this.keyman.keyboardManager.setDefaultKeyboard()) {
console.error("No keyboard stubs exist - cannot initialize keyboard!");
}
- return;
+ return Promise.resolve();
}
// Determine the default font for mapped elements
@@ -1716,7 +1719,8 @@ namespace com.keyman {
// Set exposed initialization flag to 2 to indicate deferred initialization also complete
this.keyman.setInitialized(2);
- }
+ return Promise.resolve();
+ }.bind(this);
/**
* Initialize the desktop user interface as soon as it is ready
diff --git a/web/source/kmwkeyboards.ts b/web/source/kmwkeyboards.ts
index b376a3e0b9..db9b614e84 100644
--- a/web/source/kmwkeyboards.ts
+++ b/web/source/kmwkeyboards.ts
@@ -60,7 +60,7 @@ namespace com.keyman {
'KOskFont': KeyboardFont;
// Used when loading a stub's keyboard.
- asyncLoader: any;
+ asyncLoader?: any;
constructor(id: string, langCode: string) {
this['KI'] = 'Keyboard_' + id;
@@ -305,7 +305,7 @@ namespace com.keyman {
// Fixed OSK font issue Github #7 (9/1/2015)
if(typeof(lp['oskFont']) != 'undefined') {
- sp['KOskFont'] = (typeof sp['KOskFont'] === 'undefined') ? new KeyboardFont(lp['oskfont'], fontPath) : sp['KOskFont'];
+ sp['KOskFont'] = (typeof sp['KOskFont'] === 'undefined') ? new KeyboardFont(lp['oskFont'], fontPath) : sp['KOskFont'];
}
// Update the UI
@@ -357,7 +357,7 @@ namespace com.keyman {
* @param {string} PInternalName Internal name
* @param {string} PLgCode Language code
*/
- setActiveKeyboard(PInternalName: string, PLgCode: string) {
+ setActiveKeyboard(PInternalName: string, PLgCode: string): Promise {
//TODO: This does not make sense: the callbacks should be in _SetActiveKeyboard, not here,
// since this is always called FROM the UI, which should not need notification.
// If UI callbacks are needed at all, they should be within _SetActiveKeyboard
@@ -368,7 +368,7 @@ namespace com.keyman {
}
this.doBeforeKeyboardChange(PInternalName,PLgCode);
- this._SetActiveKeyboard(PInternalName,PLgCode,true);
+ let p = this._SetActiveKeyboard(PInternalName,PLgCode,true);
if(this.keymanweb.domManager.getLastActiveElement() != null) {
this.keymanweb.domManager.focusLastActiveElement(); // TODO: Resolve without need for the cast.
}
@@ -378,6 +378,8 @@ namespace com.keyman {
// PLgCode = (keymanweb).keyboardManager.activeStub['KLC'];
// }
this.doKeyboardChange(PInternalName, PLgCode);
+
+ return p;
}
/**
@@ -395,7 +397,7 @@ namespace com.keyman {
* @param {string=} PLgCode
* @param {boolean=} saveCookie
*/
- _SetActiveKeyboard(PInternalName: string, PLgCode?: string, saveCookie?: boolean) {
+ _SetActiveKeyboard(PInternalName: string, PLgCode?: string, saveCookie?: boolean): Promise {
var n, Ln;
var util = this.keymanweb.util;
@@ -430,7 +432,7 @@ namespace com.keyman {
if(this.activeStub && this.activeKeyboard && this.activeKeyboard['KI'] == PInternalName
&& this.activeStub['KI'] == PInternalName //this part of test should not be necessary, but keep anyway
&& this.activeStub['KLC'] == PLgCode && !this.keymanweb.mustReloadKeyboard
- ) return;
+ ) return Promise.resolve();
// Check if current keyboard matches requested keyboard, but not stub
if(this.activeKeyboard && (this.activeKeyboard['KI'] == PInternalName)) {
@@ -448,7 +450,7 @@ namespace com.keyman {
if(this.keymanweb.mustReloadKeyboard) {
osk._Load();
}
- return;
+ return Promise.resolve();
}
}
}
@@ -464,7 +466,7 @@ namespace com.keyman {
util.wait(false);
}
- return;
+ return Promise.resolve();
}
// Determine if the keyboard was previously loaded but is not active and use the prior load if so.
@@ -552,12 +554,14 @@ namespace com.keyman {
// It works much more reliably if deferred (KMEW-101, build 356)
// The effect of a delay can also be tested, for example, by setting the timeout to 5000
var manager = this;
- window.setTimeout(function(){
- manager.installKeyboard(loadingStub);
- },0);
- }
+ loadingStub.asyncLoader.promise = new Promise(function(resolve, reject) {
+ window.setTimeout(function(){
+ manager.installKeyboard(resolve, reject, loadingStub);
+ },0);
+ });
+ }
this.activeStub=this.keyboardStubs[Ln];
- return;
+ return this.keyboardStubs[Ln].asyncLoader.promise;
}
}
this.keymanweb.domManager._SetTargDir(this.keymanweb.domManager.getLastActiveElement()); // I2077 - LTR/RTL timing
@@ -569,15 +573,17 @@ namespace com.keyman {
// Initialize the OSK (provided that the base code has been loaded)
osk._Load();
+ return Promise.resolve();
}
- /**
+ /**
* Install a keyboard script that has been downloaded from a keyboard server
+ * Operates as the core of a Promise, hence the 'resolve' and 'reject' parameters.
*
* @param {Object} kbdStub keyboard stub to be loaded.
*
**/
- installKeyboard(kbdStub: KeyboardStub) {
+ installKeyboard(resolve: () => void, reject: () => void, kbdStub: KeyboardStub) {
var util = this.keymanweb.util;
var osk = this.keymanweb.osk;
@@ -607,6 +613,8 @@ namespace com.keyman {
// We already know the load has failed... why wait?
kbdStub.asyncLoader.callback('Cannot find the ' + kbdName + ' keyboard for ' + kbdLang + '.', 'warn');
kbdStub.asyncLoader = null;
+
+ reject();
}, false);
@@ -644,23 +652,31 @@ namespace com.keyman {
if(!manager.keymanweb.isEmbedded) {
util.wait(false);
}
+
+ kbdStub.asyncLoader = null;
+ resolve();
// A handler portion for cases where the new