qt_demoe/docs/qtloader.js

578 lines
21 KiB
JavaScript

/****************************************************************************
**
** Copyright (C) 2018 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the plugins of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:GPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 or (at your option) any later version
** approved by the KDE Free Qt Foundation. The licenses are as published by
** the Free Software Foundation and appearing in the file LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
// QtLoader provides javascript API for managing Qt application modules.
//
// QtLoader provides API on top of Emscripten which supports common lifecycle
// tasks such as displaying placeholder content while the module downloads,
// handing application exits, and checking for browser wasm support.
//
// There are two usage modes:
// * Managed: QtLoader owns and manages the HTML display elements like
// the loader and canvas.
// * External: The embedding HTML page owns the display elements. QtLoader
// provides event callbacks which the page reacts to.
//
// Managed mode usage:
//
// var config = {
// containerElements : [$("container-id")];
// }
// var qtLoader = QtLoader(config);
// qtLoader.loadEmscriptenModule("applicationName");
//
// External mode.usage:
//
// var config = {
// canvasElements : [$("canvas-id")],
// showLoader: function() {
// loader.style.display = 'block'
// canvas.style.display = 'hidden'
// },
// showCanvas: function() {
// loader.style.display = 'hidden'
// canvas.style.display = 'block'
// return canvas;
// }
// }
// var qtLoader = QtLoader(config);
// qtLoader.loadEmscriptenModule("applicationName");
//
// Config keys
//
// containerElements : [container-element, ...]
// One or more HTML elements. QtLoader will display loader elements
// on these while loading the applicaton, and replace the loader with a
// canvas on load complete.
// canvasElements : [canvas-element, ...]
// One or more canvas elements.
// showLoader : function(status, containerElement)
// Optional loading element constructor function. Implement to create
// a custom loading screen. This function may be called multiple times,
// while preparing the application binary. "status" is a string
// containing the loading sub-status, and may be either "Downloading",
// or "Compiling". The browser may be using streaming compilation, in
// which case the wasm module is compiled during downloading and the
// there is no separate compile step.
// showCanvas : function(containerElement)
// Optional canvas constructor function. Implement to create custom
// canvas elements.
// showExit : function(crashed, exitCode, containerElement)
// Optional exited element constructor function.
// showError : function(crashed, exitCode, containerElement)
// Optional error element constructor function.
//
// path : <string>
// Prefix path for wasm file, realative to the loading HMTL file.
// restartMode : "DoNotRestart", "RestartOnExit", "RestartOnCrash"
// Controls whether the application should be reloaded on exits. The default is "DoNotRestart"
// restartType : "RestartModule", "ReloadPage"
// restartLimit : <int>
// Restart attempts limit. The default is 10.
// stdoutEnabled : <bool>
// stderrEnabled : <bool>
// environment : <object>
// key-value environment variable pairs.
//
// QtLoader object API
//
// webAssemblySupported : bool
// webGLSupported : bool
// canLoadQt : bool
// Reports if WebAssembly and WebGL are supported. These are requirements for
// running Qt applications.
// loadEmscriptenModule(applicationName)
// Loads the application from the given emscripten javascript module file and wasm file
// status
// One of "Created", "Loading", "Running", "Exited".
// crashed
// Set to true if there was an unclean exit.
// exitCode
// main()/emscripten_force_exit() return code. Valid on status change to
// "Exited", iff crashed is false.
// exitText
// Abort/exit message.
// addCanvasElement
// Add canvas at run-time. Adds a corresponding QScreen,
// removeCanvasElement
// Remove canvas at run-time. Removes the corresponding QScreen.
// resizeCanvasElement
// Signals to the application that a canvas has been resized.
// setFontDpi
// Sets the logical font dpi for the application.
var Module = {}
function QtLoader(config)
{
function webAssemblySupported() {
return typeof WebAssembly !== "undefined"
}
function webGLSupported() {
// We expect that WebGL is supported if WebAssembly is; however
// the GPU may be blacklisted.
try {
var canvas = document.createElement("canvas");
return !!(window.WebGLRenderingContext && (canvas.getContext("webgl") || canvas.getContext("experimental-webgl")));
} catch (e) {
return false;
}
}
function canLoadQt() {
// The current Qt implementation requires WebAssembly (asm.js is not in use),
// and also WebGL (there is no raster fallback).
return webAssemblySupported() && webGLSupported();
}
function removeChildren(element) {
while (element.firstChild) element.removeChild(element.firstChild);
}
function createCanvas() {
var canvas = document.createElement("canvas");
canvas.className = "QtCanvas";
canvas.style.height = "100%";
canvas.style.width = "100%";
// Set contentEditable in order to enable clipboard events; hide the resulting focus frame.
canvas.contentEditable = true;
canvas.style.outline = "0px solid transparent";
canvas.style.caretColor = "transparent";
canvas.style.cursor = "default";
return canvas;
}
// Set default state handler functions and create canvases if needed
if (config.containerElements !== undefined) {
config.canvasElements = config.containerElements.map(createCanvas);
config.showError = config.showError || function(errorText, container) {
removeChildren(container);
var errorTextElement = document.createElement("text");
errorTextElement.className = "QtError"
errorTextElement.innerHTML = errorText;
return errorTextElement;
}
config.showLoader = config.showLoader || function(loadingState, container) {
removeChildren(container);
var loadingText = document.createElement("text");
loadingText.className = "QtLoading"
loadingText.innerHTML = '<p><center> ${loadingState}...</center><p>';
return loadingText;
};
config.showCanvas = config.showCanvas || function(canvas, container) {
removeChildren(container);
}
config.showExit = config.showExit || function(crashed, exitCode, container) {
if (!crashed)
return undefined;
removeChildren(container);
var fontSize = 54;
var crashSymbols = ["\u{1F615}", "\u{1F614}", "\u{1F644}", "\u{1F928}", "\u{1F62C}",
"\u{1F915}", "\u{2639}", "\u{1F62E}", "\u{1F61E}", "\u{1F633}"];
var symbolIndex = Math.floor(Math.random() * crashSymbols.length);
var errorHtml = `<font size='${fontSize}'> ${crashSymbols[symbolIndex]} </font>`
var errorElement = document.createElement("text");
errorElement.className = "QtExit"
errorElement.innerHTML = errorHtml;
return errorElement;
}
}
config.restartMode = config.restartMode || "DoNotRestart";
config.restartLimit = config.restartLimit || 10;
if (config.stdoutEnabled === undefined) config.stdoutEnabled = true;
if (config.stderrEnabled === undefined) config.stderrEnabled = true;
// Make sure config.path is defined and ends with "/" if needed
if (config.path === undefined)
config.path = "";
if (config.path.length > 0 && !config.path.endsWith("/"))
config.path = config.path.concat("/");
if (config.environment === undefined)
config.environment = {};
var publicAPI = {};
publicAPI.webAssemblySupported = webAssemblySupported();
publicAPI.webGLSupported = webGLSupported();
publicAPI.canLoadQt = canLoadQt();
publicAPI.canLoadApplication = canLoadQt();
publicAPI.status = undefined;
publicAPI.loadEmscriptenModule = loadEmscriptenModule;
publicAPI.addCanvasElement = addCanvasElement;
publicAPI.removeCanvasElement = removeCanvasElement;
publicAPI.resizeCanvasElement = resizeCanvasElement;
publicAPI.setFontDpi = setFontDpi;
publicAPI.fontDpi = fontDpi;
restartCount = 0;
function fetchResource(filePath) {
var fullPath = config.path + filePath;
return fetch(fullPath).then(function(response) {
if (!response.ok) {
self.error = response.status + " " + response.statusText + " " + response.url;
setStatus("Error");
return Promise.reject(self.error)
} else {
return response;
}
});
}
function fetchText(filePath) {
return fetchResource(filePath).then(function(response) {
return response.text();
});
}
function fetchThenCompileWasm(response) {
return response.arrayBuffer().then(function(data) {
self.loaderSubState = "Compiling";
setStatus("Loading") // trigger loaderSubState udpate
return WebAssembly.compile(data);
});
}
function fetchCompileWasm(filePath) {
return fetchResource(filePath).then(function(response) {
if (typeof WebAssembly.compileStreaming !== "undefined") {
self.loaderSubState = "Downloading/Compiling";
setStatus("Loading");
return WebAssembly.compileStreaming(response).catch(function(error) {
// compileStreaming may/will fail if the server does not set the correct
// mime type (application/wasm) for the wasm file. Fall back to fetch,
// then compile in this case.
return fetchThenCompileWasm(response);
});
} else {
// Fall back to fetch, then compile if compileStreaming is not supported
return fetchThenCompileWasm(response);
}
});
}
function loadEmscriptenModule(applicationName) {
// Loading in qtloader.js goes through four steps:
// 1) Check prerequisites
// 2) Download resources
// 3) Configure the emscripten Module object
// 4) Start the emcripten runtime, after which emscripten takes over
// Check for Wasm & WebGL support; set error and return before downloading resources if missing
if (!webAssemblySupported()) {
self.error = "Error: WebAssembly is not supported"
setStatus("Error");
return;
}
if (!webGLSupported()) {
self.error = "Error: WebGL is not supported"
setStatus("Error");
return;
}
// Continue waiting if loadEmscriptenModule() is called again
if (publicAPI.status == "Loading")
return;
self.loaderSubState = "Downloading";
setStatus("Loading");
// Fetch emscripten generated javascript runtime
var emscriptenModuleSource = undefined
var emscriptenModuleSourcePromise = fetchText(applicationName + ".js").then(function(source) {
emscriptenModuleSource = source
});
// Fetch and compile wasm module
var wasmModule = undefined;
var wasmModulePromise = fetchCompileWasm(applicationName + ".wasm").then(function (module) {
wasmModule = module;
});
// Wait for all resources ready
Promise.all([emscriptenModuleSourcePromise, wasmModulePromise]).then(function(){
completeLoadEmscriptenModule(applicationName, emscriptenModuleSource, wasmModule);
}).catch(function(error) {
self.error = error;
setStatus("Error");
});
}
function completeLoadEmscriptenModule(applicationName, emscriptenModuleSource, wasmModule) {
// The wasm binary has been compiled into a module during resource download,
// and is ready to be instantiated. Define the instantiateWasm callback which
// emscripten will call to create the instance.
Module.instantiateWasm = function(imports, successCallback) {
WebAssembly.instantiate(wasmModule, imports).then(function(instance) {
successCallback(instance, wasmModule);
}, function(error) {
self.error = error;
setStatus("Error");
});
return {};
};
Module.locateFile = Module.locateFile || function(filename) {
return config.path + filename;
};
// Attach status callbacks
Module.setStatus = Module.setStatus || function(text) {
// Currently the only usable status update from this function
// is "Running..."
if (text.startsWith("Running"))
setStatus("Running");
};
Module.monitorRunDependencies = Module.monitorRunDependencies || function(left) {
// console.log("monitorRunDependencies " + left)
};
// Attach standard out/err callbacks.
Module.print = Module.print || function(text) {
if (config.stdoutEnabled)
console.log(text)
};
Module.printErr = Module.printErr || function(text) {
// Filter out OpenGL getProcAddress warnings. Qt tries to resolve
// all possible function/extension names at startup which causes
// emscripten to spam the console log with warnings.
if (text.startsWith !== undefined && text.startsWith("bad name in getProcAddress:"))
return;
if (config.stderrEnabled)
console.log(text)
};
// Error handling: set status to "Exited", update crashed and
// exitCode according to exit type.
// Emscripten will typically call printErr with the error text
// as well. Note that emscripten may also throw exceptions from
// async callbacks. These should be handled in window.onerror by user code.
Module.onAbort = Module.onAbort || function(text) {
publicAPI.crashed = true;
publicAPI.exitText = text;
setStatus("Exited");
};
Module.quit = Module.quit || function(code, exception) {
if (exception.name == "ExitStatus") {
// Clean exit with code
publicAPI.exitText = undefined
publicAPI.exitCode = code;
} else {
publicAPI.exitText = exception.toString();
publicAPI.crashed = true;
}
setStatus("Exited");
};
// Set environment variables
Module.preRun = Module.preRun || []
Module.preRun.push(function() {
for (var [key, value] of Object.entries(config.environment)) {
ENV[key.toUpperCase()] = value;
}
});
Module.mainScriptUrlOrBlob = new Blob([emscriptenModuleSource], {type: 'text/javascript'});
Module.qtCanvasElements = config.canvasElements;
config.restart = function() {
// Restart by reloading the page. This will wipe all state which means
// reload loops can't be prevented.
if (config.restartType == "ReloadPage") {
location.reload();
}
// Restart by readling the emscripten app module.
++self.restartCount;
if (self.restartCount > config.restartLimit) {
self.error = "Error: This application has crashed too many times and has been disabled. Reload the page to try again."
setStatus("Error");
return;
}
loadEmscriptenModule(applicationName);
};
publicAPI.exitCode = undefined;
publicAPI.exitText = undefined;
publicAPI.crashed = false;
// Finally evaluate the emscripten application script, which will
// reference the global Module object created above.
self.eval(emscriptenModuleSource); // ES5 indirect global scope eval
}
function setErrorContent() {
if (config.containerElements === undefined) {
if (config.showError !== undefined)
config.showError(self.error);
return;
}
for (container of config.containerElements) {
var errorElement = config.showError(self.error, container);
container.appendChild(errorElement);
}
}
function setLoaderContent() {
if (config.containerElements === undefined) {
if (config.showLoader !== undefined)
config.showLoader(self.loaderSubState);
return;
}
for (container of config.containerElements) {
var loaderElement = config.showLoader(self.loaderSubState, container);
container.appendChild(loaderElement);
}
}
function setCanvasContent() {
if (config.containerElements === undefined) {
if (config.showCanvas !== undefined)
config.showCanvas();
return;
}
for (var i = 0; i < config.containerElements.length; ++i) {
var container = config.containerElements[i];
var canvas = config.canvasElements[i];
config.showCanvas(canvas, container);
container.appendChild(canvas);
}
}
function setExitContent() {
// publicAPI.crashed = true;
if (publicAPI.status != "Exited")
return;
if (config.containerElements === undefined) {
if (config.showExit !== undefined)
config.showExit(publicAPI.crashed, publicAPI.exitCode);
return;
}
if (!publicAPI.crashed)
return;
for (container of config.containerElements) {
var loaderElement = config.showExit(publicAPI.crashed, publicAPI.exitCode, container);
if (loaderElement !== undefined)
container.appendChild(loaderElement);
}
}
var committedStatus = undefined;
function handleStatusChange() {
if (publicAPI.status != "Loading" && committedStatus == publicAPI.status)
return;
committedStatus = publicAPI.status;
if (publicAPI.status == "Error") {
setErrorContent();
} else if (publicAPI.status == "Loading") {
setLoaderContent();
} else if (publicAPI.status == "Running") {
setCanvasContent();
} else if (publicAPI.status == "Exited") {
if (config.restartMode == "RestartOnExit" ||
config.restartMode == "RestartOnCrash" && publicAPI.crashed) {
committedStatus = undefined;
config.restart();
} else {
setExitContent();
}
}
// Send status change notification
if (config.statusChanged)
config.statusChanged(publicAPI.status);
}
function setStatus(status) {
if (status != "Loading" && publicAPI.status == status)
return;
publicAPI.status = status;
window.setTimeout(function() { handleStatusChange(); }, 0);
}
function addCanvasElement(element) {
if (publicAPI.status == "Running")
Module.qtAddCanvasElement(element);
else
console.log("Error: addCanvasElement can only be called in the Running state");
}
function removeCanvasElement(element) {
if (publicAPI.status == "Running")
Module.qtRemoveCanvasElement(element);
else
console.log("Error: removeCanvasElement can only be called in the Running state");
}
function resizeCanvasElement(element) {
if (publicAPI.status == "Running")
Module.qtResizeCanvasElement(element);
}
function setFontDpi(dpi) {
Module.qtFontDpi = dpi;
if (publicAPI.status == "Running")
Module.qtSetFontDpi(dpi);
}
function fontDpi() {
return Module.qtFontDpi;
}
setStatus("Created");
return publicAPI;
}