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