mirror of
https://github.com/keymanapp/keyman.git
synced 2026-08-10 02:45:32 +00:00
Fixes #6320. Part 1 of moving from lerna to a simpler, maintained monorepo solution, using TypeScript Projects and NPM Workspaces. There is more work to be done here. At this point, KeymanWeb builds and runs without errors, but the built file is substantially different, mostly in include order. Using TypeScript Projects, we move away from the need to run build scripts in various locations for almost all the Typescript modules. TODO: Embedded versions and tests have not been verified. TODO: developer/server is not yet verified. TODO: developer/js (needs a rename!) is not yet verified. TODO: Currently, the predictive-text folder needs refactoring to move the construction of the worker wrapper out of the Predictive Text build and into the final assembly of keymanweb.js (as it should be valid to run it as a separate .js anyway). TODO: Most of the `<reference>` paths need to be re-verified. Ideally there should be no references outside the current module for any given .ts. TODO: The embedded vs browser vs node (headless) builds should be tidied up for consistency so that it's obvious what depends on what. This is currently messiest in the predictive-text folder, where the output names diverge from the filenames and the various files are mixed in the same folder (as evidenced by the exclusions listed in each tsconfig.json). TODO: `npm install` should be removed from most build scripts and instead `npm ci` (#6196) should be run only once from the top-level folder for any given build. I've had eliminated side-effects from the `install` action for npm, which makes it easier to reason about state. TODO: verify_npm_setup and related functions can probably be eliminated. TODO: most of the build scripts should be largely eliminated for web. TODO: several ts projects use inconsistent output folders. TODO: it may be possible to generate a .d.ts for models/types so that we can use a consistent reference for those as well. TODO: build.sh, tsconfig.json should always be in the module's top-level folder, not in a subfolder such as src (e.g. see input-processor/src, keyboard-processor/src, web/source). TODO: resources/web-environment should be in common/web. TODO: other js node_modules imports should be wrapped like es6-shim. TODO: fix up the publish code for npm modules TODO: eliminate version numbers from package.json if possible? Whew, that's most of the stuff I noticed!
298 lines
No EOL
11 KiB
TypeScript
298 lines
No EOL
11 KiB
TypeScript
// Includes KeymanWeb's Device class, as it's quite a useful resource for KMW-related projects.
|
|
/// <reference path="../source/kmwdevice.ts" />
|
|
// Ensure that Promises are within scope.
|
|
/// <reference path="@keymanapp/es6-shim/build/index.d.ts" />
|
|
|
|
type KeyboardMap = {[id: string]: any};
|
|
|
|
namespace com.keyman.renderer {
|
|
export class BatchRenderer {
|
|
static divMaster: HTMLDivElement;
|
|
static dummy: HTMLInputElement;
|
|
static captureStream: any;
|
|
static video: any;
|
|
static boundingRect: DOMRect;
|
|
static allLayers = false;
|
|
static keyboardMatch: RegExp;
|
|
|
|
async startCapture() {
|
|
let captureStream = null;
|
|
|
|
try {
|
|
captureStream = await (navigator.mediaDevices as any).getDisplayMedia({
|
|
audio: false,
|
|
video: {
|
|
width: screen.width,
|
|
height: screen.height,
|
|
frameRate: 60,
|
|
}
|
|
});
|
|
} catch(err) {
|
|
console.error("Error: " + err);
|
|
}
|
|
|
|
const video = document.createElement("video");
|
|
video.srcObject = captureStream;
|
|
video.play();
|
|
|
|
const result = await new Promise<void>((resolve, reject) => {
|
|
video.onloadedmetadata = function () {
|
|
resolve();
|
|
}
|
|
});
|
|
|
|
return {captureStream: captureStream, video: video};
|
|
}
|
|
|
|
// 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(!id.match(BatchRenderer.keyboardMatch)) continue;
|
|
if(id.match(/^Keyboard_bod/)) continue; // bod keyboards currently don't load on Chrome and break the test run; they need rebuild.
|
|
if(keyboardMap[id]) {
|
|
continue;
|
|
} else {
|
|
keyboardMap[id] = kbds[i];
|
|
}
|
|
}
|
|
|
|
return keyboardMap;
|
|
}
|
|
|
|
private render(ele: HTMLElement, isMobile?: boolean): Promise<HTMLImageElement> {
|
|
const capture = async () => {
|
|
const result = await new Promise<HTMLImageElement>((resolve, reject) => {
|
|
// from: https://github.com/kasprownik/electron-screencapture/blob/master/index.js
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = BatchRenderer.boundingRect.width;
|
|
canvas.height = BatchRenderer.boundingRect.height;
|
|
const context = canvas.getContext('2d');
|
|
// see: https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement
|
|
context.drawImage(BatchRenderer.video,
|
|
BatchRenderer.boundingRect.left,
|
|
BatchRenderer.boundingRect.top,
|
|
canvas.width, canvas.height,
|
|
0, 0, canvas.width, canvas.height);
|
|
|
|
const frame = canvas.toDataURL("image/png");
|
|
let imgOut = document.createElement('img');
|
|
imgOut.src = frame;
|
|
|
|
resolve(imgOut);
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
return capture();
|
|
}
|
|
|
|
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'].core.activeKeyboard._legacyLayoutSpec.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<void> = 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;
|
|
BatchRenderer.boundingRect = box.getBoundingClientRect();
|
|
|
|
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.vkbd.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.core.activeKeyboard._legacyLayoutSpec.KLS;
|
|
}
|
|
|
|
if(!BatchRenderer.allLayers && layers.length) layers = [layers[0]];
|
|
|
|
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(keyman.osk.vkbd) {
|
|
if(isMobile) {
|
|
keyman.core.keyboardProcessor.layerId = layers[i].id;
|
|
} else {
|
|
keyman.core.keyboardProcessor.layerId = Object.keys(layers)[i];
|
|
}
|
|
} else {
|
|
console.error("Error - keyman.osk.vkbd is undefined!");
|
|
}
|
|
// Make sure the active element's still set!
|
|
renderer.setActiveDummy();
|
|
keyman.osk.show(true);
|
|
|
|
(document as any).fonts.ready.then(function() {
|
|
window.setTimeout(function() {
|
|
renderer.render(box, isMobile).then(function(imgEle: HTMLImageElement) {
|
|
let eleLayer = document.createElement('div');
|
|
let eleLayerId = document.createElement('p');
|
|
eleLayerId.textContent = 'Layer ID: ' + (isMobile ? layers[i].id : Object.keys(layers)[i]);
|
|
|
|
eleLayer.appendChild(eleLayerId);
|
|
eleLayer.appendChild(imgEle);
|
|
eleLayer.appendChild(document.createElement('br'));
|
|
|
|
divRenders.appendChild(eleLayer);
|
|
resolve(i);
|
|
});
|
|
}, 100);
|
|
});
|
|
});
|
|
};
|
|
|
|
// The resulting Promise will only call it's `.then()` once all of this keyboard's renders have been completed.
|
|
return renderer.arrayPromiseIteration(renderLayer, isMobile ? 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<any>, length: number): Promise<any> {
|
|
let iteration = function(index: number): Promise<any> {
|
|
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['dom']['DOMEventHandlers'].states._activeElement = BatchRenderer.dummy;
|
|
}
|
|
|
|
async run(allLayers, filter) {
|
|
BatchRenderer.allLayers = allLayers;
|
|
BatchRenderer.keyboardMatch = new RegExp('^Keyboard_('+filter+')', 'i');
|
|
if(window['keyman']) {
|
|
let keyman = window['keyman'];
|
|
|
|
let cc = await this.startCapture();
|
|
BatchRenderer.captureStream = cc.captureStream;
|
|
BatchRenderer.video = cc.video;
|
|
|
|
// Establish a 'dummy' element to bypass the 'nothing's active' check KMW usually uses.
|
|
BatchRenderer.dummy = document.createElement('input');
|
|
this.setActiveDummy();
|
|
|
|
BatchRenderer.divMaster = <HTMLDivElement> 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();
|
|
})();
|
|
} |