diff options
Diffstat (limited to 'src/plugins/platforms/wasm')
66 files changed, 7122 insertions, 4574 deletions
diff --git a/src/plugins/platforms/wasm/CMakeLists.txt b/src/plugins/platforms/wasm/CMakeLists.txt index 10e247b7dd..775946aaf9 100644 --- a/src/plugins/platforms/wasm/CMakeLists.txt +++ b/src/plugins/platforms/wasm/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from wasm.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QWasmIntegrationPlugin Plugin: @@ -6,30 +7,39 @@ qt_internal_add_plugin(QWasmIntegrationPlugin OUTPUT_NAME qwasm - DEFAULT_IF ${QT_QPA_DEFAULT_PLATFORM} MATCHES wasm # special case - TYPE platforms - STATIC + DEFAULT_IF "wasm" IN_LIST QT_QPA_PLATFORMS + PLUGIN_TYPE platforms SOURCES main.cpp + qwasmaccessibility.cpp qwasmaccessibility.h + qwasmbase64iconstore.cpp qwasmbase64iconstore.h qwasmclipboard.cpp qwasmclipboard.h qwasmcompositor.cpp qwasmcompositor.h + qwasmcssstyle.cpp qwasmcssstyle.h qwasmcursor.cpp qwasmcursor.h + qwasmdom.cpp qwasmdom.h + qwasmevent.cpp qwasmevent.h qwasmeventdispatcher.cpp qwasmeventdispatcher.h - qwasmeventtranslator.cpp qwasmeventtranslator.h qwasmfontdatabase.cpp qwasmfontdatabase.h qwasmintegration.cpp qwasmintegration.h + qwasmkeytranslator.cpp qwasmkeytranslator.h qwasmoffscreensurface.cpp qwasmoffscreensurface.h qwasmopenglcontext.cpp qwasmopenglcontext.h + qwasmplatform.cpp qwasmplatform.h qwasmscreen.cpp qwasmscreen.h qwasmservices.cpp qwasmservices.h - qwasmstring.cpp qwasmstring.h - qwasmstylepixmaps_p.h qwasmtheme.cpp qwasmtheme.h qwasmwindow.cpp qwasmwindow.h + qwasmwindowclientarea.cpp qwasmwindowclientarea.h + qwasmwindowtreenode.cpp qwasmwindowtreenode.h + qwasmwindownonclientarea.cpp qwasmwindownonclientarea.h + qwasminputcontext.cpp qwasminputcontext.h + qwasmwindowstack.cpp qwasmwindowstack.h + qwasmdrag.cpp qwasmdrag.h DEFINES QT_EGL_NO_X11 QT_NO_FOREACH - PUBLIC_LIBRARIES + LIBRARIES Qt::Core Qt::CorePrivate Qt::Gui @@ -37,42 +47,61 @@ qt_internal_add_plugin(QWasmIntegrationPlugin ) # Resources: -set_source_files_properties("${QT_SOURCE_TREE}/src/3rdparty/wasm/Vera.ttf" PROPERTIES QT_RESOURCE_ALIAS "Vera.ttf") -set_source_files_properties("${QT_SOURCE_TREE}/src/3rdparty/wasm/DejaVuSans.ttf" PROPERTIES QT_RESOURCE_ALIAS "DejaVuSans.ttf") -set_source_files_properties("${QT_SOURCE_TREE}/src/3rdparty/wasm/DejaVuSansMono.ttf" PROPERTIES QT_RESOURCE_ALIAS "DejaVuSansMono.ttf") - set(wasmfonts_resource_files - "${QT_SOURCE_TREE}/src/3rdparty/wasm/Vera.ttf" - "${QT_SOURCE_TREE}/src/3rdparty/wasm/DejaVuSans.ttf" - "${QT_SOURCE_TREE}/src/3rdparty/wasm/DejaVuSansMono.ttf" + "${QtBase_SOURCE_DIR}/src/3rdparty/wasm/DejaVuSans.ttf" + "${QtBase_SOURCE_DIR}/src/3rdparty/wasm/DejaVuSansMono.ttf" ) qt_internal_add_resource(QWasmIntegrationPlugin "wasmfonts" PREFIX "/fonts" + BASE + "${QtBase_SOURCE_DIR}/src/3rdparty/wasm" FILES ${wasmfonts_resource_files} ) + qt_internal_extend_target(QWasmIntegrationPlugin CONDITION QT_FEATURE_opengl SOURCES qwasmbackingstore.cpp qwasmbackingstore.h - PUBLIC_LIBRARIES + LIBRARIES Qt::OpenGL Qt::OpenGLPrivate ) -#### Keys ignored in scope 4:.:.:wasm.pro:NOT TARGET___equals____ss_QT_DEFAULT_QPA_PLUGIN: # PLUGIN_EXTENDS = "-" -qt_copy_or_install(FILES +set(wasm_support_files wasm_shell.html - DESTINATION "${CMAKE_INSTALL_PREFIX}/plugins/platforms/" -) -qt_copy_or_install(FILES qtloader.js - DESTINATION "${CMAKE_INSTALL_PREFIX}/plugins/platforms/" + resources/qtlogo.svg +) + +set(wasmwindow_resource_files + "resources/maximize.svg" + "resources/qtlogo.svg" + "resources/restore.svg" + "resources/x.svg" +) + +qt_internal_add_resource(QWasmIntegrationPlugin "wasmwindow" + PREFIX + "/wasm-window" + BASE + "resources" + FILES + ${wasmwindow_resource_files} ) + +qt_path_join(destination ${QT_INSTALL_DIR} "plugins/platforms") qt_copy_or_install(FILES - qtlogo.svg - DESTINATION "${CMAKE_INSTALL_PREFIX}/plugins/platforms/" + ${wasm_support_files} + DESTINATION "${destination}" ) +# Need to copy the support files to the build dir in a top-level prefix build +# So _qt_internal_wasm_add_target_helpers finds them. +if(QT_WILL_INSTALL) + foreach(wasm_support_file ${wasm_support_files}) + file(COPY "${wasm_support_file}" DESTINATION "${QT_BUILD_DIR}/plugins/platforms") + endforeach() +endif() diff --git a/src/plugins/platforms/wasm/main.cpp b/src/plugins/platforms/wasm/main.cpp index a4d23b4e0d..f32ef5aab8 100644 --- a/src/plugins/platforms/wasm/main.cpp +++ b/src/plugins/platforms/wasm/main.cpp @@ -1,37 +1,13 @@ -/**************************************************************************** -** -** 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$ -** -****************************************************************************/ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include <qpa/qplatformintegrationplugin.h> #include "qwasmintegration.h" QT_BEGIN_NAMESPACE +using namespace Qt::Literals::StringLiterals; + class QWasmIntegrationPlugin : public QPlatformIntegrationPlugin { Q_OBJECT @@ -43,7 +19,7 @@ public: QPlatformIntegration *QWasmIntegrationPlugin::create(const QString& system, const QStringList& paramList) { Q_UNUSED(paramList); - if (!system.compare(QStringLiteral("wasm"), Qt::CaseInsensitive)) + if (!system.compare("wasm"_L1, Qt::CaseInsensitive)) return new QWasmIntegration; return nullptr; diff --git a/src/plugins/platforms/wasm/qtloader.js b/src/plugins/platforms/wasm/qtloader.js index b159cd63ab..8027dd8fa9 100644 --- a/src/plugins/platforms/wasm/qtloader.js +++ b/src/plugins/platforms/wasm/qtloader.js @@ -1,577 +1,301 @@ -/**************************************************************************** -** -** 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) +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +/** + * Loads the instance of a WASM module. + * + * @param config May contain any key normally accepted by emscripten and the 'qt' extra key, with + * the following sub-keys: + * - environment: { [name:string] : string } + * environment variables set on the instance + * - onExit: (exitStatus: { text: string, code?: number, crashed: bool }) => void + * called when the application has exited for any reason. There are two cases: + * aborted: crashed is true, text contains an error message. + * exited: crashed is false, code contians the exit code. + * + * Note that by default Emscripten does not exit when main() returns. This behavior + * is controlled by the EXIT_RUNTIME linker flag; set "-s EXIT_RUNTIME=1" to make + * Emscripten tear down the runtime and exit when main() returns. + * + * - containerElements: HTMLDivElement[] + * Array of host elements for Qt screens. Each of these elements is mapped to a QScreen on + * launch. + * - fontDpi: number + * Specifies font DPI for the instance + * - onLoaded: () => void + * Called when the module has loaded, at the point in time where any loading placeholder + * should be hidden and the application window should be shown. + * - entryFunction: (emscriptenConfig: object) => Promise<EmscriptenModule> + * Qt always uses emscripten's MODULARIZE option. This is the MODULARIZE entry function. + * - module: Promise<WebAssembly.Module> + * The module to create the instance from (optional). Specifying the module allows optimizing + * use cases where several instances are created from a single WebAssembly source. + * - qtdir: string + * Path to Qt installation. This path will be used for loading Qt shared libraries and plugins. + * The path is set to 'qt' by default, and is relative to the path of the web page's html file. + * This property is not in use when static linking is used, since this build mode includes all + * libraries and plugins in the wasm file. + * - preload: [string]: Array of file paths to json-encoded files which specifying which files to preload. + * The preloaded files will be downloaded at application startup and copied to the in-memory file + * system provided by Emscripten. + * + * Each json file must contain an array of source, destination objects: + * [ + * { + * "source": "path/to/source", + * "destination": "/path/to/destination" + * }, + * ... + * ] + * The source path is relative to the html file path. The destination path must be + * an absolute path. + * + * $QTDIR may be used as a placeholder for the "qtdir" configuration property (see @qtdir), for instance: + * "source": "$QTDIR/plugins/imageformats/libqjpeg.so" + * - localFonts.requestPermission: bool + * Whether Qt should request for local fonts access permission on startup (default false). + * - localFonts.familiesCollection string + * Specifies a collection of local fonts to load. Possible values are: + * "NoFontFamilies" : Don't load any font families + * "DefaultFontFamilies" : A subset of available font families; currently the "web-safe" fonts (default). + * "AllFontFamilies" : All local font families (not reccomended) + * - localFonts.extraFamilies: [string] + * Adds additional font families to be loaded at startup. + * + * @return Promise<instance: EmscriptenModule> + * The promise is resolved when the module has been instantiated and its main function has been + * called. + * + * @see https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/emscripten for + * EmscriptenModule + */ +async function qtLoad(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; + const throwIfEnvUsedButNotExported = (instance, config) => + { + const environment = config.environment; + if (!environment || Object.keys(environment).length === 0) + return; + const isEnvExported = typeof instance.ENV === 'object'; + if (!isEnvExported) + throw new Error('ENV must be exported if environment variables are passed'); + }; + + if (typeof config !== 'object') + throw new Error('config is required, expected an object'); + if (typeof config.qt !== 'object') + throw new Error('config.qt is required, expected an object'); + if (typeof config.qt.entryFunction !== 'function') + throw new Error('config.qt.entryFunction is required, expected a function'); + + config.qt.qtdir ??= 'qt'; + config.qt.preload ??= []; + + config.qtContainerElements = config.qt.containerElements; + delete config.qt.containerElements; + config.qtFontDpi = config.qt.fontDpi; + delete config.qt.fontDpi; + + // Make Emscripten not call main(); this gives us more control over + // the startup sequence. + const originalNoInitialRun = config.noInitialRun; + const originalArguments = config.arguments; + config.noInitialRun = true; + + // Used for rejecting a failed load's promise where emscripten itself does not allow it, + // like in instantiateWasm below. This allows us to throw in case of a load error instead of + // hanging on a promise to entry function, which emscripten unfortunately does. + let circuitBreakerReject; + const circuitBreaker = new Promise((_, reject) => { circuitBreakerReject = reject; }); + + // If module async getter is present, use it so that module reuse is possible. + if (config.qt.module) { + config.instantiateWasm = async (imports, successCallback) => + { + try { + const module = await config.qt.module; + successCallback( + await WebAssembly.instantiate(module, imports), module); + } catch (e) { + circuitBreakerReject(e); + } } } - - 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; + const fetchJsonHelper = async path => (await fetch(path)).json(); + const filesToPreload = (await Promise.all(config.qt.preload.map(fetchJsonHelper))).flat(); + const qtPreRun = (instance) => { + // Copy qt.environment to instance.ENV + throwIfEnvUsedButNotExported(instance, config); + for (const [name, value] of Object.entries(config.qt.environment ?? {})) + instance.ENV[name] = value; + + // Preload files from qt.preload + const makeDirs = (FS, filePath) => { + const parts = filePath.split("/"); + let path = "/"; + for (let i = 0; i < parts.length - 1; ++i) { + const part = parts[i]; + if (part == "") + continue; + path += part + "/"; + try { + FS.mkdir(path); + } catch (error) { + const EEXIST = 20; + if (error.errno != EEXIST) + throw error; + } + } } - 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); + const extractFilenameAndDir = (path) => { + const parts = path.split('/'); + const filename = parts.pop(); + const dir = parts.join('/'); + return { + filename: filename, + dir: dir + }; } - - 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; + const preloadFile = (file) => { + makeDirs(instance.FS, file.destination); + const source = file.source.replace('$QTDIR', config.qt.qtdir); + const filenameAndDir = extractFilenameAndDir(file.destination); + instance.FS.createPreloadedFile(filenameAndDir.dir, filenameAndDir.filename, source, true, true); } + const isFsExported = typeof instance.FS === 'object'; + if (!isFsExported) + throw new Error('FS must be exported if preload is used'); + filesToPreload.forEach(preloadFile); } - 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 = {}; + if (!config.preRun) + config.preRun = []; + config.preRun.push(qtPreRun); - 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; - - self.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; - } - }); + const originalOnRuntimeInitialized = config.onRuntimeInitialized; + config.onRuntimeInitialized = () => { + originalOnRuntimeInitialized?.(); + config.qt.onLoaded?.(); } - function fetchText(filePath) { - return fetchResource(filePath).then(function(response) { - return response.text(); - }); + const originalLocateFile = config.locateFile; + config.locateFile = filename => { + const originalLocatedFilename = originalLocateFile ? originalLocateFile(filename) : filename; + if (originalLocatedFilename.startsWith('libQt6')) + return `${config.qt.qtdir}/lib/${originalLocatedFilename}`; + return originalLocatedFilename; } - 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); - } - }); - } + let onExitCalled = false; + const originalOnExit = config.onExit; + config.onExit = code => { + originalOnExit?.(); - 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"); + if (!onExitCalled) { + onExitCalled = true; + config.qt.onExit?.({ + code, + crashed: false }); - 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); + const originalOnAbort = config.onAbort; + config.onAbort = text => + { + originalOnAbort?.(); + + if (!onExitCalled) { + onExitCalled = true; + config.qt.onExit?.({ + text, + crashed: true + }); } - } - - function setCanvasContent() { - if (config.containerElements === undefined) { - if (config.showCanvas !== undefined) - config.showCanvas(); + }; + + // Call app/emscripten module entry function. It may either come from the emscripten + // runtime script or be customized as needed. + let instance; + try { + instance = await Promise.race( + [circuitBreaker, config.qt.entryFunction(config)]); + + // Call main after creating the instance. We've opted into manually + // calling main() by setting noInitialRun in the config. Thie Works around + // issue where Emscripten suppresses all exceptions thrown during main. + if (!originalNoInitialRun) + instance.callMain(originalArguments); + } catch (e) { + // If this is the exception thrown by app.exec() then that is a normal + // case and we suppress it. + if (e == "unwind") // not much to go on 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); + if (!onExitCalled) { + onExitCalled = true; + config.qt.onExit?.({ + text: e.message, + crashed: true + }); } + throw e; } - 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; + return instance; +} - for (container of config.containerElements) { - var loaderElement = config.showExit(publicAPI.crashed, publicAPI.exitCode, container); - if (loaderElement !== undefined) - container.appendChild(loaderElement); - } +// Compatibility API. This API is deprecated, +// and will be removed in a future version of Qt. +function QtLoader(qtConfig) { + + const warning = 'Warning: The QtLoader API is deprecated and will be removed in ' + + 'a future version of Qt. Please port to the new qtLoad() API.'; + console.warn(warning); + + let emscriptenConfig = qtConfig.moduleConfig || {} + qtConfig.moduleConfig = undefined; + const showLoader = qtConfig.showLoader; + qtConfig.showLoader = undefined; + const showError = qtConfig.showError; + qtConfig.showError = undefined; + const showExit = qtConfig.showExit; + qtConfig.showExit = undefined; + const showCanvas = qtConfig.showCanvas; + qtConfig.showCanvas = undefined; + if (qtConfig.canvasElements) { + qtConfig.containerElements = qtConfig.canvasElements + qtConfig.canvasElements = undefined; + } else { + qtConfig.containerElements = qtConfig.containerElements; + qtConfig.containerElements = undefined; } - - 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(); + emscriptenConfig.qt = qtConfig; + + let qtloader = { + exitCode: undefined, + exitText: "", + loadEmscriptenModule: _name => { + try { + qtLoad(emscriptenConfig); + } catch (e) { + showError?.(e.message); } } - - // 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); + qtConfig.onLoaded = () => { + showCanvas?.(); } - function setFontDpi(dpi) { - Module.qtFontDpi = dpi; - if (publicAPI.status == "Running") - Module.qtSetFontDpi(dpi); + qtConfig.onExit = exit => { + qtloader.exitCode = exit.code + qtloader.exitText = exit.text; + showExit?.(); } - function fontDpi() { - return Module.qtFontDpi; - } - - setStatus("Created"); + showLoader?.("Loading"); - return publicAPI; -} + return qtloader; +}; diff --git a/src/plugins/platforms/wasm/qtlogo.svg b/src/plugins/platforms/wasm/qtlogo.svg deleted file mode 100644 index ad7c7776bf..0000000000 --- a/src/plugins/platforms/wasm/qtlogo.svg +++ /dev/null @@ -1,40 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:cc="http://creativecommons.org/ns#" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns="http://www.w3.org/2000/svg" - width="462pt" - height="339pt" - viewBox="0 0 462 339" - version="1.1"> - <metadata - id="metadata20"> - <rdf:RDF> - <cc:Work - rdf:about=""> - <dc:format>image/svg+xml</dc:format> - <dc:type - rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> - <dc:title></dc:title> - </cc:Work> - </rdf:RDF> - </metadata> - <path - fill="#41cd52" - d=" M 63.50 0.00 L 462.00 0.00 L 462.00 274.79 C 440.60 296.26 419.13 317.66 397.61 339.00 L 0.00 339.00 L 0.00 63.39 C 21.08 42.18 42.34 21.13 63.50 0.00 Z" - id="path6" /> - <path - d=" M 122.37 71.33 C 137.50 61.32 156.21 58.79 174.00 58.95 C 190.94 59.16 208.72 62.13 222.76 72.24 C 232.96 79.41 239.59 90.48 244.01 101.93 C 251.16 120.73 253.26 141.03 253.50 161.01 C 253.53 181.13 252.62 201.69 245.96 220.86 C 241.50 233.90 233.01 245.48 221.81 253.52 C 229.87 266.58 238.09 279.54 246.15 292.60 C 236.02 297.27 225.92 301.97 215.78 306.62 C 207.15 292.38 198.56 278.11 189.90 263.89 C 178.19 265.81 166.21 265.66 154.44 264.36 C 140.34 262.67 125.97 258.37 115.09 248.88 C 106.73 241.64 101.48 231.51 97.89 221.21 C 92.01 203.79 90.43 185.25 90.16 166.97 C 90.02 147.21 91.28 127.14 97.24 108.18 C 101.85 93.92 109.48 79.69 122.37 71.33 Z" - id="path8" - fill="#ffffff" /> - <path - d=" M 294.13 70.69 C 304.73 70.68 315.33 70.68 325.93 70.69 C 325.96 84.71 325.92 98.72 325.95 112.74 C 339.50 112.76 353.05 112.74 366.60 112.75 C 366.37 121.85 366.12 130.95 365.86 140.05 C 352.32 140.08 338.79 140.04 325.25 140.07 C 325.28 163.05 325.18 186.03 325.30 209.01 C 325.56 215.30 325.42 221.94 328.19 227.75 C 330.21 232.23 335.65 233.38 340.08 233.53 C 348.43 233.50 356.77 233.01 365.12 232.86 C 365.63 241.22 366.12 249.59 366.60 257.95 C 349.99 260.74 332.56 264.08 316.06 258.86 C 309.11 256.80 302.63 252.19 299.81 245.32 C 294.76 233.63 294.35 220.62 294.13 208.07 C 294.11 185.40 294.13 162.74 294.12 140.07 C 286.73 140.05 279.34 140.08 271.95 140.05 C 271.93 130.96 271.93 121.86 271.95 112.76 C 279.34 112.73 286.72 112.77 294.11 112.74 C 294.14 98.72 294.10 84.71 294.13 70.69 Z" - id="path10" - fill="#ffffff" /> - <path - fill="#41cd52" - d=" M 160.51 87.70 C 170.80 86.36 181.60 86.72 191.34 90.61 C 199.23 93.73 205.93 99.84 209.47 107.58 C 214.90 119.31 216.98 132.26 218.03 145.05 C 219.17 162.07 219.01 179.25 216.66 196.17 C 215.01 206.24 212.66 216.85 205.84 224.79 C 198.92 232.76 188.25 236.18 178.01 236.98 C 167.21 237.77 155.82 236.98 146.07 231.87 C 140.38 228.84 135.55 224.09 132.73 218.27 C 129.31 211.30 127.43 203.69 126.11 196.07 C 122.13 171.91 121.17 146.91 126.61 122.89 C 128.85 113.83 132.11 104.53 138.73 97.70 C 144.49 91.85 152.51 88.83 160.51 87.70 Z" - id="path12" /> -</svg> diff --git a/src/plugins/platforms/wasm/qwasmaccessibility.cpp b/src/plugins/platforms/wasm/qwasmaccessibility.cpp new file mode 100644 index 0000000000..2e430176be --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmaccessibility.cpp @@ -0,0 +1,793 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "qwasmaccessibility.h" +#include "qwasmscreen.h" +#include "qwasmwindow.h" +#include "qwasmintegration.h" +#include <QtGui/qwindow.h> + +#if QT_CONFIG(accessibility) + +#include <QtGui/private/qaccessiblebridgeutils_p.h> + +Q_LOGGING_CATEGORY(lcQpaAccessibility, "qt.qpa.accessibility") + +// Qt WebAssembly a11y backend +// +// This backend implements accessibility support by creating "shadowing" html +// elements for each Qt UI element. We access the DOM by using Emscripten's +// val.h API. +// +// Currently, html elements are created in response to notifyAccessibilityUpdate +// events. In addition or alternatively, we could also walk the accessibility tree +// from setRootObject(). + +QWasmAccessibility::QWasmAccessibility() +{ + + s_instance = this; +} + +QWasmAccessibility::~QWasmAccessibility() +{ + s_instance = nullptr; +} + +QWasmAccessibility *QWasmAccessibility::s_instance = nullptr; + +QWasmAccessibility* QWasmAccessibility::get() +{ + return s_instance; +} + +void QWasmAccessibility::addAccessibilityEnableButton(QWindow *window) +{ + get()->addAccessibilityEnableButtonImpl(window); +} + +void QWasmAccessibility::removeAccessibilityEnableButton(QWindow *window) +{ + get()->removeAccessibilityEnableButtonImpl(window); +} + +void QWasmAccessibility::addAccessibilityEnableButtonImpl(QWindow *window) +{ + if (m_accessibilityEnabled) + return; + + emscripten::val container = getContainer(window); + emscripten::val document = getDocument(container); + emscripten::val button = document.call<emscripten::val>("createElement", std::string("button")); + button.set("innerText", std::string("Enable Screen Reader")); + button["classList"].call<void>("add", emscripten::val("hidden-visually-read-by-screen-reader")); + container.call<void>("appendChild", button); + + auto enableContext = std::make_tuple(button, std::make_unique<qstdweb::EventCallback> + (button, std::string("click"), [this](emscripten::val) { enableAccessibility(); })); + m_enableButtons.insert(std::make_pair(window, std::move(enableContext))); +} + +void QWasmAccessibility::removeAccessibilityEnableButtonImpl(QWindow *window) +{ + auto it = m_enableButtons.find(window); + if (it == m_enableButtons.end()) + return; + + // Remove button + auto [element, callback] = it->second; + Q_UNUSED(callback); + element["parentElement"].call<void>("removeChild", element); + m_enableButtons.erase(it); +} + +void QWasmAccessibility::enableAccessibility() +{ + // Enable accessibility globally for the applicaton. Remove all "enable" + // buttons and populate the accessibility tree, starting from the root object. + + Q_ASSERT(!m_accessibilityEnabled); + m_accessibilityEnabled = true; + for (const auto& [key, value] : m_enableButtons) { + const auto &[element, callback] = value; + Q_UNUSED(key); + Q_UNUSED(callback); + element["parentElement"].call<void>("removeChild", element); + } + m_enableButtons.clear(); + populateAccessibilityTree(QAccessible::queryAccessibleInterface(m_rootObject)); +} + +emscripten::val QWasmAccessibility::getContainer(QWindow *window) +{ + return window ? static_cast<QWasmWindow *>(window->handle())->a11yContainer() + : emscripten::val::undefined(); +} + +emscripten::val QWasmAccessibility::getContainer(QAccessibleInterface *iface) +{ + if (!iface) + return emscripten::val::undefined(); + return getContainer(getWindow(iface)); +} + +QWindow *QWasmAccessibility::getWindow(QAccessibleInterface *iface) +{ + QWindow *window = iface->window(); + // this is needed to add tabs as the window is not available + if (!window && iface->parent()) + window = iface->parent()->window(); + return window; +} + +emscripten::val QWasmAccessibility::getDocument(const emscripten::val &container) +{ + if (container.isUndefined()) + return emscripten::val::global("document"); + return container["ownerDocument"]; +} + +emscripten::val QWasmAccessibility::getDocument(QAccessibleInterface *iface) +{ + return getDocument(getContainer(iface)); +} + +emscripten::val QWasmAccessibility::createHtmlElement(QAccessibleInterface *iface) +{ + // Get the html container element for the interface; this depends on which + // QScreen it is on. If the interface is not on a screen yet we get an undefined + // container, and the code below handles that case as well. + emscripten::val container = getContainer(iface); + + // Get the correct html document for the container, or fall back + // to the global document. TODO: Does using the correct document actually matter? + emscripten::val document = getDocument(container); + + // Translate the Qt a11y elemen role into html element type + ARIA role. + // Here we can either create <div> elements with a spesific ARIA role, + // or create e.g. <button> elements which should have built-in accessibility. + emscripten::val element = [this, iface, document] { + + emscripten::val element = emscripten::val::undefined(); + + switch (iface->role()) { + + case QAccessible::Button: { + element = document.call<emscripten::val>("createElement", std::string("button")); + element.call<void>("addEventListener", emscripten::val("click"), + emscripten::val::module_property("qtEventReceived"), true); + } break; + case QAccessible::CheckBox: { + element = document.call<emscripten::val>("createElement", std::string("input")); + element.call<void>("setAttribute", std::string("type"), std::string("checkbox")); + if (iface->state().checked) { + element.call<void>("setAttribute", std::string("checked"), std::string("true")); + } + element.call<void>("addEventListener", emscripten::val("change"), + emscripten::val::module_property("qtEventReceived"), true); + + } break; + + case QAccessible::RadioButton: { + element = document.call<emscripten::val>("createElement", std::string("input")); + element.call<void>("setAttribute", std::string("type"), std::string("radio")); + if (iface->state().checked) { + element.call<void>("setAttribute", std::string("checked"), std::string("true")); + } + element.set(std::string("name"), std::string("buttonGroup")); + element.call<void>("addEventListener", emscripten::val("change"), + emscripten::val::module_property("qtEventReceived"), true); + } break; + + case QAccessible::SpinBox: { + element = document.call<emscripten::val>("createElement", std::string("input")); + element.call<void>("setAttribute", std::string("type"), std::string("number")); + std::string valueString = iface->valueInterface()->currentValue().toString().toStdString(); + element.call<void>("setAttribute", std::string("value"), valueString); + element.call<void>("addEventListener", emscripten::val("change"), + emscripten::val::module_property("qtEventReceived"), true); + } break; + + case QAccessible::Slider: { + element = document.call<emscripten::val>("createElement", std::string("input")); + element.call<void>("setAttribute", std::string("type"), std::string("range")); + std::string valueString = iface->valueInterface()->currentValue().toString().toStdString(); + element.call<void>("setAttribute", std::string("value"), valueString); + element.call<void>("addEventListener", emscripten::val("change"), + emscripten::val::module_property("qtEventReceived"), true); + } break; + + case QAccessible::PageTabList:{ + element = document.call<emscripten::val>("createElement", std::string("div")); + element.call<void>("setAttribute", std::string("role"), std::string("tablist")); + QString idName = iface->text(QAccessible::Name).replace(" ", "_"); + idName += "_tabList"; + element.call<void>("setAttribute", std::string("id"), idName.toStdString()); + + for (int i = 0; i < iface->childCount(); ++i) { + if (iface->child(i)->role() == QAccessible::PageTab){ + emscripten::val elementTab = emscripten::val::undefined(); + elementTab = ensureHtmlElement(iface->child(i)); + elementTab.call<void>("setAttribute", std::string("aria-owns"), idName.toStdString()); + setHtmlElementGeometry(iface->child(i)); + } + } + } break; + + case QAccessible::PageTab:{ + element = document.call<emscripten::val>("createElement", std::string("button")); + element.call<void>("setAttribute", std::string("role"), std::string("tab")); + QString text = iface->text(QAccessible::Name); + element.call<void>("setAttribute", std::string("title"), text.toStdString()); + element.call<void>("addEventListener", emscripten::val("click"), + emscripten::val::module_property("qtEventReceived"), true); + } break; + + case QAccessible::ScrollBar: { + element = document.call<emscripten::val>("createElement", std::string("div")); + element.call<void>("setAttribute", std::string("role"), std::string("scrollbar")); + std::string valueString = iface->valueInterface()->currentValue().toString().toStdString(); + element.call<void>("setAttribute", std::string("aria-valuenow"), valueString); + element.call<void>("addEventListener", emscripten::val("change"), + emscripten::val::module_property("qtEventReceived"), true); + } break; + + case QAccessible::StaticText: { + element = document.call<emscripten::val>("createElement", std::string("textarea")); + element.call<void>("setAttribute", std::string("readonly"), std::string("true")); + + } break; + case QAccessible::Dialog: { + element = document.call<emscripten::val>("createElement", std::string("dialog")); + }break; + case QAccessible::ToolBar:{ + element = document.call<emscripten::val>("createElement", std::string("div")); + QString text = iface->text(QAccessible::Name); + + element.call<void>("setAttribute", std::string("role"), std::string("toolbar")); + element.call<void>("setAttribute", std::string("title"), text.toStdString()); + element.call<void>("addEventListener", emscripten::val("click"), + emscripten::val::module_property("qtEventReceived"), true); + }break; + case QAccessible::MenuItem: + case QAccessible::ButtonMenu: { + element = document.call<emscripten::val>("createElement", std::string("button")); + QString text = iface->text(QAccessible::Name); + + element.call<void>("setAttribute", std::string("role"), std::string("menuitem")); + element.call<void>("setAttribute", std::string("title"), text.toStdString()); + element.call<void>("addEventListener", emscripten::val("click"), + emscripten::val::module_property("qtEventReceived"), true); + }break; + case QAccessible::MenuBar: + case QAccessible::PopupMenu: { + element = document.call<emscripten::val>("createElement",std::string("div")); + QString text = iface->text(QAccessible::Name); + element.call<void>("setAttribute", std::string("role"), std::string("menubar")); + element.call<void>("setAttribute", std::string("title"), text.toStdString()); + for (int i = 0; i < iface->childCount(); ++i) { + emscripten::val childElement = emscripten::val::undefined(); + childElement= ensureHtmlElement(iface->child(i)); + childElement.call<void>("setAttribute", std::string("aria-owns"), text.toStdString()); + setHtmlElementTextName(iface->child(i)); + setHtmlElementGeometry(iface->child(i)); + } + }break; + case QAccessible::EditableText: { + element = document.call<emscripten::val>("createElement", std::string("input")); + element.call<void>("setAttribute", std::string("type"),std::string("text")); + element.call<void>("addEventListener", emscripten::val("input"), + emscripten::val::module_property("qtEventReceived"), true); + } break; + default: + qCDebug(lcQpaAccessibility) << "TODO: createHtmlElement() handle" << iface->role(); + element = document.call<emscripten::val>("createElement", std::string("div")); + } + + QString id = QAccessibleBridgeUtils::accessibleId(iface); + if (iface->role() != QAccessible::PageTabList) + element.call<void>("setAttribute", std::string("id"), id.toStdString()); + + return element; + + }(); + + // Add the html element to the container if we have one. If not there + // is a second chance when handling the ObjectShow event. + if (!container.isUndefined()) + container.call<void>("appendChild", element); + + return element; +} + +void QWasmAccessibility::destroyHtmlElement(QAccessibleInterface *iface) +{ + Q_UNUSED(iface); + qCDebug(lcQpaAccessibility) << "TODO destroyHtmlElement"; +} + +emscripten::val QWasmAccessibility::ensureHtmlElement(QAccessibleInterface *iface) +{ + auto it = m_elements.find(iface); + if (it != m_elements.end()) + return it.value(); + + emscripten::val element = createHtmlElement(iface); + m_elements.insert(iface, element); + + return element; +} + +void QWasmAccessibility::setHtmlElementVisibility(QAccessibleInterface *iface, bool visible) +{ + emscripten::val element = ensureHtmlElement(iface); + emscripten::val container = getContainer(iface); + + if (container.isUndefined()) { + qCDebug(lcQpaAccessibility) << "TODO: setHtmlElementVisibility: unable to find html container for element" << iface; + return; + } + + container.call<void>("appendChild", element); + + element.set("ariaHidden", !visible); // ariaHidden mean completely hidden; maybe some sort of soft-hidden should be used. +} + +void QWasmAccessibility::setHtmlElementGeometry(QAccessibleInterface *iface) +{ + emscripten::val element = ensureHtmlElement(iface); + + // QAccessibleInterface gives us the geometry in global (screen) coordinates. Translate that + // to window geometry in order to position elements relative to window origin. + QWindow *window = getWindow(iface); + if (!window) + qCWarning(lcQpaAccessibility) << "Unable to find window for" << iface << "setting null geometry"; + QRect screenGeometry = iface->rect(); + QPoint windowPos = window ? window->mapFromGlobal(screenGeometry.topLeft()) : QPoint(); + QRect windowGeometry(windowPos, screenGeometry.size()); + + setHtmlElementGeometry(element, windowGeometry); +} + +void QWasmAccessibility::setHtmlElementGeometry(emscripten::val element, QRect geometry) +{ + // Position the element using "position: absolute" in order to place + // it under the corresponding Qt element in the screen. + emscripten::val style = element["style"]; + style.set("position", std::string("absolute")); + style.set("z-index", std::string("-1")); // FIXME: "0" should be sufficient to order beheind the + // screen element, but isn't + style.set("left", std::to_string(geometry.x()) + "px"); + style.set("top", std::to_string(geometry.y()) + "px"); + style.set("width", std::to_string(geometry.width()) + "px"); + style.set("height", std::to_string(geometry.height()) + "px"); +} + +void QWasmAccessibility::setHtmlElementTextName(QAccessibleInterface *iface) +{ + emscripten::val element = ensureHtmlElement(iface); + QString text = iface->text(QAccessible::Name); + element.set("innerHTML", text.toStdString()); // FIXME: use something else than innerHTML +} + +void QWasmAccessibility::setHtmlElementTextNameLE(QAccessibleInterface *iface) { + emscripten::val element = ensureHtmlElement(iface); + QString text = iface->text(QAccessible::Name); + element.call<void>("setAttribute", std::string("name"), text.toStdString()); + QString value = iface->text(QAccessible::Value); + element.set("innerHTML", value.toStdString()); +} + +void QWasmAccessibility::setHtmlElementDescription(QAccessibleInterface *iface) { + emscripten::val element = ensureHtmlElement(iface); + QString desc = iface->text(QAccessible::Description); + element.call<void>("setAttribute", std::string("aria-description"), desc.toStdString()); +} + +void QWasmAccessibility::handleStaticTextUpdate(QAccessibleEvent *event) +{ + switch (event->type()) { + case QAccessible::NameChanged: { + setHtmlElementTextName(event->accessibleInterface()); + } break; + case QAccessible::DescriptionChanged: { + setHtmlElementDescription(event->accessibleInterface()); + } break; + default: + qCDebug(lcQpaAccessibility) << "TODO: implement handleStaticTextUpdate for event" << event->type(); + break; + } +} + +void QWasmAccessibility::handleLineEditUpdate(QAccessibleEvent *event) { + + switch (event->type()) { + case QAccessible::NameChanged: { + setHtmlElementTextName(event->accessibleInterface()); + } break; + case QAccessible::Focus: + case QAccessible::TextRemoved: + case QAccessible::TextInserted: + case QAccessible::TextCaretMoved: { + setHtmlElementTextNameLE(event->accessibleInterface()); + } break; + case QAccessible::DescriptionChanged: { + setHtmlElementDescription(event->accessibleInterface()); + } break; + default: + qCDebug(lcQpaAccessibility) << "TODO: implement handleLineEditUpdate for event" << event->type(); + break; + } +} + +void QWasmAccessibility::handleEventFromHtmlElement(const emscripten::val event) +{ + + QAccessibleInterface *iface = m_elements.key(event["target"]); + if (iface == nullptr) { + return; + } else { + QString eventType = QString::fromStdString(event["type"].as<std::string>()); + const auto& actionNames = QAccessibleBridgeUtils::effectiveActionNames(iface); + if (actionNames.contains(QAccessibleActionInterface::pressAction())) { + + iface->actionInterface()->doAction(QAccessibleActionInterface::pressAction()); + + } else if (actionNames.contains(QAccessibleActionInterface::toggleAction())) { + + iface->actionInterface()->doAction(QAccessibleActionInterface::toggleAction()); + + } else if (actionNames.contains(QAccessibleActionInterface::increaseAction()) || + actionNames.contains(QAccessibleActionInterface::decreaseAction())) { + + QString val = QString::fromStdString(event["target"]["value"].as<std::string>()); + + iface->valueInterface()->setCurrentValue(val.toInt()); + + } else if (eventType == "input") { + + // as EditableTextInterface is not implemented in qml accessibility + // so we need to check the role for text to update in the textbox during accessibility + + if (iface->editableTextInterface() || iface->role() == QAccessible::EditableText) { + std::string insertText = event["target"]["value"].as<std::string>(); + iface->setText(QAccessible::Value, QString::fromStdString(insertText)); + } + } + } +} + +void QWasmAccessibility::handleButtonUpdate(QAccessibleEvent *event) +{ + qCDebug(lcQpaAccessibility) << "TODO: implement handleButtonUpdate for event" << event->type(); +} + +void QWasmAccessibility::handleCheckBoxUpdate(QAccessibleEvent *event) +{ + switch (event->type()) { + case QAccessible::Focus: + case QAccessible::NameChanged: { + setHtmlElementTextName(event->accessibleInterface()); + } break; + case QAccessible::StateChanged: { + QAccessibleInterface *accessible = event->accessibleInterface(); + emscripten::val element = ensureHtmlElement(accessible); + bool checkedString = accessible->state().checked ? true : false; + element.call<void>("setAttribute", std::string("checked"), checkedString); + } break; + case QAccessible::DescriptionChanged: { + setHtmlElementDescription(event->accessibleInterface()); + } break; + default: + qCDebug(lcQpaAccessibility) << "TODO: implement handleCheckBoxUpdate for event" << event->type(); + break; + } +} +void QWasmAccessibility::handleToolUpdate(QAccessibleEvent *event) +{ + QAccessibleInterface *iface = event->accessibleInterface(); + QString text = iface->text(QAccessible::Name); + QString desc = iface->text(QAccessible::Description); + switch (event->type()) { + case QAccessible::NameChanged: + case QAccessible::StateChanged:{ + emscripten::val element = ensureHtmlElement(iface); + element.call<void>("setAttribute", std::string("title"), text.toStdString()); + } break; + case QAccessible::DescriptionChanged: { + setHtmlElementDescription(event->accessibleInterface()); + } break; + default: + qCDebug(lcQpaAccessibility) << "TODO: implement handleToolUpdate for event" << event->type(); + break; + } +} +void QWasmAccessibility::handleMenuUpdate(QAccessibleEvent *event) +{ + QAccessibleInterface *iface = event->accessibleInterface(); + QString text = iface->text(QAccessible::Name); + QString desc = iface->text(QAccessible::Description); + switch (event->type()) { + case QAccessible::Focus: + case QAccessible::NameChanged: + case QAccessible::MenuStart ://"TODO: To implement later + case QAccessible::PopupMenuStart://"TODO: To implement later + case QAccessible::StateChanged:{ + emscripten::val element = ensureHtmlElement(iface); + element.call<void>("setAttribute", std::string("title"), text.toStdString()); + } break; + case QAccessible::DescriptionChanged: { + setHtmlElementDescription(event->accessibleInterface()); + } break; + default: + qCDebug(lcQpaAccessibility) << "TODO: implement handleMenuUpdate for event" << event->type(); + break; + } +} +void QWasmAccessibility::handleDialogUpdate(QAccessibleEvent *event) { + + switch (event->type()) { + case QAccessible::NameChanged: + case QAccessible::Focus: + case QAccessible::DialogStart: + case QAccessible::StateChanged: { + setHtmlElementTextName(event->accessibleInterface()); + } break; + case QAccessible::DescriptionChanged: { + setHtmlElementDescription(event->accessibleInterface()); + } break; + default: + qCDebug(lcQpaAccessibility) << "TODO: implement handleLineEditUpdate for event" << event->type(); + break; + } +} + +void QWasmAccessibility::populateAccessibilityTree(QAccessibleInterface *iface) +{ + if (!iface) + return; + + // Create html element for the interface, sync up properties. + ensureHtmlElement(iface); + const bool visible = !iface->state().invisible; + setHtmlElementVisibility(iface, visible); + setHtmlElementGeometry(iface); + setHtmlElementTextName(iface); + setHtmlElementDescription(iface); + + for (int i = 0; i < iface->childCount(); ++i) + populateAccessibilityTree(iface->child(i)); +} + +void QWasmAccessibility::handleRadioButtonUpdate(QAccessibleEvent *event) +{ + switch (event->type()) { + case QAccessible::Focus: + case QAccessible::NameChanged: { + setHtmlElementTextName(event->accessibleInterface()); + } break; + case QAccessible::StateChanged: { + QAccessibleInterface *accessible = event->accessibleInterface(); + emscripten::val element = ensureHtmlElement(accessible); + std::string checkedString = accessible->state().checked ? "true" : "false"; + element.call<void>("setAttribute", std::string("checked"), checkedString); + } break; + case QAccessible::DescriptionChanged: { + setHtmlElementDescription(event->accessibleInterface()); + } break; + default: + qDebug() << "TODO: implement handleRadioButtonUpdate for event" << event->type(); + break; + } +} + +void QWasmAccessibility::handleSpinBoxUpdate(QAccessibleEvent *event) +{ + switch (event->type()) { + case QAccessible::Focus: + case QAccessible::NameChanged: { + setHtmlElementTextName(event->accessibleInterface()); + } break; + case QAccessible::ValueChanged: { + QAccessibleInterface *accessible = event->accessibleInterface(); + emscripten::val element = ensureHtmlElement(accessible); + std::string valueString = accessible->valueInterface()->currentValue().toString().toStdString(); + element.call<void>("setAttribute", std::string("value"), valueString); + } break; + case QAccessible::DescriptionChanged: { + setHtmlElementDescription(event->accessibleInterface()); + } break; + default: + qDebug() << "TODO: implement handleSpinBoxUpdate for event" << event->type(); + break; + } +} + +void QWasmAccessibility::handleSliderUpdate(QAccessibleEvent *event) +{ + switch (event->type()) { + case QAccessible::Focus: + case QAccessible::NameChanged: { + setHtmlElementTextName(event->accessibleInterface()); + } break; + case QAccessible::ValueChanged: { + QAccessibleInterface *accessible = event->accessibleInterface(); + emscripten::val element = ensureHtmlElement(accessible); + std::string valueString = accessible->valueInterface()->currentValue().toString().toStdString(); + element.call<void>("setAttribute", std::string("value"), valueString); + } break; + case QAccessible::DescriptionChanged: { + setHtmlElementDescription(event->accessibleInterface()); + } break; + default: + qDebug() << "TODO: implement handleSliderUpdate for event" << event->type(); + break; + } +} + +void QWasmAccessibility::handleScrollBarUpdate(QAccessibleEvent *event) +{ + switch (event->type()) { + case QAccessible::Focus: + case QAccessible::NameChanged: { + setHtmlElementTextName(event->accessibleInterface()); + } break; + case QAccessible::ValueChanged: { + QAccessibleInterface *accessible = event->accessibleInterface(); + emscripten::val element = ensureHtmlElement(accessible); + std::string valueString = accessible->valueInterface()->currentValue().toString().toStdString(); + element.call<void>("setAttribute", std::string("aria-valuenow"), valueString); + } break; + case QAccessible::DescriptionChanged: { + setHtmlElementDescription(event->accessibleInterface()); + } break; + default: + qDebug() << "TODO: implement handleSliderUpdate for event" << event->type(); + break; + } + +} + +void QWasmAccessibility::handlePageTabUpdate(QAccessibleEvent *event) +{ + switch (event->type()) { + case QAccessible::NameChanged: { + setHtmlElementTextName(event->accessibleInterface()); + } break; + case QAccessible::Focus: { + setHtmlElementTextName(event->accessibleInterface()); + } break; + case QAccessible::DescriptionChanged: { + setHtmlElementDescription(event->accessibleInterface()); + } break; + default: + qDebug() << "TODO: implement handlePageTabUpdate for event" << event->type(); + break; + } +} + +void QWasmAccessibility::handlePageTabListUpdate(QAccessibleEvent *event) +{ + switch (event->type()) { + case QAccessible::NameChanged: { + setHtmlElementTextName(event->accessibleInterface()); + } break; + case QAccessible::Focus: { + setHtmlElementTextName(event->accessibleInterface()); + } break; + case QAccessible::DescriptionChanged: { + setHtmlElementDescription(event->accessibleInterface()); + } break; + default: + qDebug() << "TODO: implement handlePageTabUpdate for event" << event->type(); + break; + } +} + +void QWasmAccessibility::notifyAccessibilityUpdate(QAccessibleEvent *event) +{ + if (!m_accessibilityEnabled) + return; + + QAccessibleInterface *iface = event->accessibleInterface(); + if (!iface) { + qWarning() << "notifyAccessibilityUpdate with null a11y interface" ; + return; + } + + // Handle some common event types. See + // https://doc.qt.io/qt-5/qaccessible.html#Event-enum + switch (event->type()) { + case QAccessible::ObjectShow: + setHtmlElementVisibility(iface, true); + + // Sync up properties on show; + setHtmlElementGeometry(iface); + setHtmlElementTextName(iface); + setHtmlElementDescription(iface); + + return; + break; + case QAccessible::ObjectHide: + setHtmlElementVisibility(iface, false); + return; + break; + // TODO: maybe handle more types here + default: + break; + }; + + // Switch on interface role, see + // https://doc.qt.io/qt-5/qaccessibleinterface.html#role + switch (iface->role()) { + case QAccessible::StaticText: + handleStaticTextUpdate(event); + break; + case QAccessible::Button: + handleStaticTextUpdate(event); + break; + case QAccessible::CheckBox: + handleCheckBoxUpdate(event); + break; + case QAccessible::EditableText: + handleLineEditUpdate(event); + break; + case QAccessible::Dialog: + handleDialogUpdate(event); + break; + case QAccessible::MenuItem: + case QAccessible::MenuBar: + case QAccessible::PopupMenu: + handleMenuUpdate(event); + break; + case QAccessible::ToolBar: + case QAccessible::ButtonMenu: + handleToolUpdate(event); + case QAccessible::RadioButton: + handleRadioButtonUpdate(event); + break; + case QAccessible::SpinBox: + handleSpinBoxUpdate(event); + break; + case QAccessible::Slider: + handleSliderUpdate(event); + break; + case QAccessible::PageTab: + handlePageTabUpdate(event); + break; + case QAccessible::PageTabList: + handlePageTabListUpdate(event); + break; + case QAccessible::ScrollBar: + handleScrollBarUpdate(event); + break; + default: + qCDebug(lcQpaAccessibility) << "TODO: implement notifyAccessibilityUpdate for role" << iface->role(); + }; +} + +void QWasmAccessibility::setRootObject(QObject *root) +{ + m_rootObject = root; +} + +void QWasmAccessibility::initialize() +{ + +} + +void QWasmAccessibility::cleanup() +{ + +} + +void QWasmAccessibility::onHtmlEventReceived(emscripten::val event) +{ + static_cast<QWasmAccessibility *>(QWasmIntegration::get()->accessibility())->handleEventFromHtmlElement(event); +} + +EMSCRIPTEN_BINDINGS(qtButtonEvent) { + function("qtEventReceived", &QWasmAccessibility::onHtmlEventReceived); +} + +#endif // QT_CONFIG(accessibility) diff --git a/src/plugins/platforms/wasm/qwasmaccessibility.h b/src/plugins/platforms/wasm/qwasmaccessibility.h new file mode 100644 index 0000000000..c4be7f0d72 --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmaccessibility.h @@ -0,0 +1,92 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef QWASMACCESIBILITY_H +#define QWASMACCESIBILITY_H + +#include <QtCore/qtconfigmacros.h> +#include <QtGui/qtguiglobal.h> + +#if QT_CONFIG(accessibility) + +#include <QtCore/qhash.h> +#include <private/qstdweb_p.h> +#include <qpa/qplatformaccessibility.h> + +#include <emscripten/val.h> +#include <QLoggingCategory> + +#include <map> +#include <emscripten/bind.h> + +Q_DECLARE_LOGGING_CATEGORY(lcQpaAccessibility) + +class QWasmAccessibility : public QPlatformAccessibility +{ +public: + QWasmAccessibility(); + ~QWasmAccessibility(); + + static QWasmAccessibility* get(); + + static void addAccessibilityEnableButton(QWindow *window); + static void removeAccessibilityEnableButton(QWindow *window); + +private: + void addAccessibilityEnableButtonImpl(QWindow *window); + void removeAccessibilityEnableButtonImpl(QWindow *window); + void enableAccessibility(); + + static emscripten::val getContainer(QWindow *window); + static emscripten::val getContainer(QAccessibleInterface *iface); + static emscripten::val getDocument(const emscripten::val &container); + static emscripten::val getDocument(QAccessibleInterface *iface); + static QWindow *getWindow(QAccessibleInterface *iface); + + emscripten::val createHtmlElement(QAccessibleInterface *iface); + void destroyHtmlElement(QAccessibleInterface *iface); + emscripten::val ensureHtmlElement(QAccessibleInterface *iface); + void setHtmlElementVisibility(QAccessibleInterface *iface, bool visible); + void setHtmlElementGeometry(QAccessibleInterface *iface); + void setHtmlElementGeometry(emscripten::val element, QRect geometry); + void setHtmlElementTextName(QAccessibleInterface *iface); + void setHtmlElementTextNameLE(QAccessibleInterface *iface); + void setHtmlElementDescription(QAccessibleInterface *iface); + + void handleStaticTextUpdate(QAccessibleEvent *event); + void handleButtonUpdate(QAccessibleEvent *event); + void handleCheckBoxUpdate(QAccessibleEvent *event); + void handleDialogUpdate(QAccessibleEvent *event); + void handleMenuUpdate(QAccessibleEvent *event); + void handleToolUpdate(QAccessibleEvent *event); + void handleLineEditUpdate(QAccessibleEvent *event); + void handleRadioButtonUpdate(QAccessibleEvent *event); + void handleSpinBoxUpdate(QAccessibleEvent *event); + void handlePageTabUpdate(QAccessibleEvent *event); + void handleSliderUpdate(QAccessibleEvent *event); + void handleScrollBarUpdate(QAccessibleEvent *event); + void handlePageTabListUpdate(QAccessibleEvent *event); + + void handleEventFromHtmlElement(const emscripten::val event); + + void populateAccessibilityTree(QAccessibleInterface *iface); + void notifyAccessibilityUpdate(QAccessibleEvent *event) override; + void setRootObject(QObject *o) override; + void initialize() override; + void cleanup() override; + +public: // public for EMSCRIPTEN_BINDINGS + static void onHtmlEventReceived(emscripten::val event); + +private: + static QWasmAccessibility *s_instance; + QObject *m_rootObject = nullptr; + bool m_accessibilityEnabled = false; + std::map<QWindow *, std::tuple<emscripten::val, std::shared_ptr<qstdweb::EventCallback>>> m_enableButtons; + QHash<QAccessibleInterface *, emscripten::val> m_elements; + +}; + +#endif // QT_CONFIG(accessibility) + +#endif diff --git a/src/plugins/platforms/wasm/qwasmbackingstore.cpp b/src/plugins/platforms/wasm/qwasmbackingstore.cpp index 3d667ccf97..a3c1ae8a50 100644 --- a/src/plugins/platforms/wasm/qwasmbackingstore.cpp +++ b/src/plugins/platforms/wasm/qwasmbackingstore.cpp @@ -1,50 +1,21 @@ -/**************************************************************************** -** -** 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$ -** -****************************************************************************/ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "qwasmbackingstore.h" #include "qwasmwindow.h" #include "qwasmcompositor.h" +#include "qwasmdom.h" -#include <QtOpenGL/qopengltexture.h> -#include <QtGui/qmatrix4x4.h> #include <QtGui/qpainter.h> -#include <private/qguiapplication_p.h> -#include <qpa/qplatformscreen.h> -#include <QtGui/qoffscreensurface.h> #include <QtGui/qbackingstore.h> +#include <emscripten.h> +#include <emscripten/wire.h> + QT_BEGIN_NAMESPACE QWasmBackingStore::QWasmBackingStore(QWasmCompositor *compositor, QWindow *window) - : QPlatformBackingStore(window) - , m_compositor(compositor) - , m_texture(new QOpenGLTexture(QOpenGLTexture::Target2D)) + : QPlatformBackingStore(window), m_compositor(compositor) { QWasmWindow *wasmWindow = static_cast<QWasmWindow *>(window->handle()); if (wasmWindow) @@ -55,29 +26,11 @@ QWasmBackingStore::~QWasmBackingStore() { auto window = this->window(); QWasmIntegration::get()->removeBackingStore(window); - destroy(); QWasmWindow *wasmWindow = static_cast<QWasmWindow *>(window->handle()); if (wasmWindow) wasmWindow->setBackingStore(nullptr); } -void QWasmBackingStore::destroy() -{ - if (m_texture->isCreated()) { - auto context = m_compositor->context(); - auto currentContext = QOpenGLContext::currentContext(); - if (!currentContext || !QOpenGLContext::areSharing(context, currentContext)) { - QOffscreenSurface offScreenSurface(m_compositor->screen()->screen()); - offScreenSurface.setFormat(context->format()); - offScreenSurface.create(); - context->makeCurrent(&offScreenSurface); - m_texture->destroy(); - } else { - m_texture->destroy(); - } - } -} - QPaintDevice *QWasmBackingStore::paintDevice() { return &m_image; @@ -90,33 +43,24 @@ void QWasmBackingStore::flush(QWindow *window, const QRegion ®ion, const QPoi Q_UNUSED(offset); m_dirty |= region; - m_compositor->requestRedraw(); + m_compositor->handleBackingStoreFlush(window); } -void QWasmBackingStore::updateTexture() +void QWasmBackingStore::updateTexture(QWasmWindow *window) { if (m_dirty.isNull()) return; - if (m_recreateTexture) { - m_recreateTexture = false; - destroy(); + if (m_webImageDataArray.isUndefined()) { + m_webImageDataArray = window->context2d().call<emscripten::val>( + "createImageData", emscripten::val(m_image.width()), + emscripten::val(m_image.height())); } - if (!m_texture->isCreated()) { - m_texture->setMinificationFilter(QOpenGLTexture::Nearest); - m_texture->setMagnificationFilter(QOpenGLTexture::Nearest); - m_texture->setWrapMode(QOpenGLTexture::ClampToEdge); - m_texture->setData(m_image, QOpenGLTexture::DontGenerateMipMaps); - m_texture->create(); - } - m_texture->bind(); - - QRegion fixed; + QRegion clippedDpiScaledRegion; QRect imageRect = m_image.rect(); for (const QRect &rect : m_dirty) { - // Convert device-independent dirty region to device region qreal dpr = m_image.devicePixelRatio(); QRect deviceRect = QRect(rect.topLeft() * dpr, rect.size() * dpr); @@ -129,21 +73,11 @@ void QWasmBackingStore::updateTexture() r.setWidth(imageRect.width()); } - fixed |= r; + clippedDpiScaledRegion |= r; } - for (const QRect &rect : fixed) { - // if the sub-rect is full-width we can pass the image data directly to - // OpenGL instead of copying, since there is no gap between scanlines - if (rect.width() == imageRect.width()) { - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, rect.y(), rect.width(), rect.height(), GL_RGBA, GL_UNSIGNED_BYTE, - m_image.constScanLine(rect.y())); - } else { - glTexSubImage2D(GL_TEXTURE_2D, 0, rect.x(), rect.y(), rect.width(), rect.height(), GL_RGBA, GL_UNSIGNED_BYTE, - m_image.copy(rect).constBits()); - } - } - /* End of code taken from QEGLPlatformBackingStore */ + for (const QRect &dirtyRect : clippedDpiScaledRegion) + dom::drawImageToWebImageDataArray(m_image, m_webImageDataArray, dirtyRect); m_dirty = QRegion(); } @@ -152,28 +86,33 @@ void QWasmBackingStore::beginPaint(const QRegion ®ion) { m_dirty |= region; // Keep backing store device pixel ratio in sync with window - if (m_image.devicePixelRatio() != window()->devicePixelRatio()) + if (m_image.devicePixelRatio() != window()->handle()->devicePixelRatio()) resize(backingStore()->size(), backingStore()->staticContents()); QPainter painter(&m_image); - painter.setCompositionMode(QPainter::CompositionMode_Source); - const QColor blank = Qt::transparent; - for (const QRect &rect : region) - painter.fillRect(rect, blank); + + if (m_image.hasAlphaChannel()) { + painter.setCompositionMode(QPainter::CompositionMode_Source); + const QColor blank = Qt::transparent; + for (const QRect &rect : region) + painter.fillRect(rect, blank); + } } void QWasmBackingStore::resize(const QSize &size, const QRegion &staticContents) { Q_UNUSED(staticContents); - m_image = QImage(size * window()->devicePixelRatio(), QImage::Format_RGB32); - m_image.setDevicePixelRatio(window()->devicePixelRatio()); - m_recreateTexture = true; + QImage::Format format = QImage::Format_RGBA8888; + const auto platformScreenDPR = window()->handle()->devicePixelRatio(); + m_image = QImage(size * platformScreenDPR, format); + m_image.setDevicePixelRatio(platformScreenDPR); + m_webImageDataArray = emscripten::val::undefined(); } QImage QWasmBackingStore::toImage() const { - // used by QPlatformBackingStore::composeAndFlush + // used by QPlatformBackingStore::rhiFlush return m_image; } @@ -182,10 +121,10 @@ const QImage &QWasmBackingStore::getImageRef() const return m_image; } -const QOpenGLTexture *QWasmBackingStore::getUpdatedTexture() +emscripten::val QWasmBackingStore::getUpdatedWebImage(QWasmWindow *window) { - updateTexture(); - return m_texture.data(); + updateTexture(window); + return m_webImageDataArray; } QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmbackingstore.h b/src/plugins/platforms/wasm/qwasmbackingstore.h index b93c96b483..54e9fe4cb3 100644 --- a/src/plugins/platforms/wasm/qwasmbackingstore.h +++ b/src/plugins/platforms/wasm/qwasmbackingstore.h @@ -1,31 +1,5 @@ -/**************************************************************************** -** -** 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$ -** -****************************************************************************/ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #ifndef QWASMBACKINGSTORE_H #define QWASMBACKINGSTORE_H @@ -33,18 +7,20 @@ #include <qpa/qplatformbackingstore.h> #include <QtGui/qimage.h> +#include <emscripten/val.h> + QT_BEGIN_NAMESPACE class QOpenGLTexture; class QRegion; class QWasmCompositor; +class QWasmWindow; class QWasmBackingStore : public QPlatformBackingStore { public: QWasmBackingStore(QWasmCompositor *compositor, QWindow *window); ~QWasmBackingStore(); - void destroy(); QPaintDevice *paintDevice() override; @@ -54,17 +30,16 @@ public: QImage toImage() const override; const QImage &getImageRef() const; - const QOpenGLTexture *getUpdatedTexture(); + emscripten::val getUpdatedWebImage(QWasmWindow *window); protected: - void updateTexture(); + void updateTexture(QWasmWindow *window); private: QWasmCompositor *m_compositor; QImage m_image; - QScopedPointer<QOpenGLTexture> m_texture; QRegion m_dirty; - bool m_recreateTexture = false; + emscripten::val m_webImageDataArray = emscripten::val::undefined(); }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmbase64iconstore.cpp b/src/plugins/platforms/wasm/qwasmbase64iconstore.cpp new file mode 100644 index 0000000000..8f05f082ea --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmbase64iconstore.cpp @@ -0,0 +1,40 @@ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "qwasmbase64iconstore.h" + +#include <QtCore/qfile.h> + +QT_BEGIN_NAMESPACE + +Q_GLOBAL_STATIC(Base64IconStore, globalWasmWindowIconStore); + +Base64IconStore::Base64IconStore() +{ + QString iconSources[static_cast<size_t>(IconType::Size)] = { + QStringLiteral(":/wasm-window/maximize.svg"), QStringLiteral(":/wasm-window/qtlogo.svg"), + QStringLiteral(":/wasm-window/restore.svg"), QStringLiteral(":/wasm-window/x.svg") + }; + + for (size_t iconType = static_cast<size_t>(IconType::First); + iconType < static_cast<size_t>(IconType::Size); ++iconType) { + QFile svgFile(iconSources[static_cast<size_t>(iconType)]); + if (!svgFile.open(QIODevice::ReadOnly)) + Q_ASSERT(false); // A resource should always be opened. + m_storage[static_cast<size_t>(iconType)] = svgFile.readAll().toBase64(); + } +} + +Base64IconStore::~Base64IconStore() = default; + +Base64IconStore *Base64IconStore::get() +{ + return globalWasmWindowIconStore(); +} + +std::string_view Base64IconStore::getIcon(IconType type) const +{ + return m_storage[static_cast<size_t>(type)]; +} + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmbase64iconstore.h b/src/plugins/platforms/wasm/qwasmbase64iconstore.h new file mode 100644 index 0000000000..89704f2d2c --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmbase64iconstore.h @@ -0,0 +1,37 @@ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef QWASMBASE64IMAGESTORE_H +#define QWASMBASE64IMAGESTORE_H + +#include <string> +#include <string_view> + +#include <QtCore/qtconfigmacros.h> + +QT_BEGIN_NAMESPACE +class Base64IconStore +{ +public: + enum class IconType { + Maximize, + First = Maximize, + QtLogo, + Restore, + X, + Size, + }; + + Base64IconStore(); + ~Base64IconStore(); + + static Base64IconStore *get(); + + std::string_view getIcon(IconType type) const; + +private: + std::string m_storage[static_cast<size_t>(IconType::Size)]; +}; + +QT_END_NAMESPACE +#endif // QWASMBASE64IMAGESTORE_H diff --git a/src/plugins/platforms/wasm/qwasmclipboard.cpp b/src/plugins/platforms/wasm/qwasmclipboard.cpp index 713adee8f9..1aa3ffa5b3 100644 --- a/src/plugins/platforms/wasm/qwasmclipboard.cpp +++ b/src/plugins/platforms/wasm/qwasmclipboard.cpp @@ -1,123 +1,79 @@ -/**************************************************************************** -** -** 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$ -** -****************************************************************************/ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "qwasmclipboard.h" +#include "qwasmdom.h" +#include "qwasmevent.h" #include "qwasmwindow.h" -#include "qwasmstring.h" -#include <emscripten.h> -#include <emscripten/html5.h> -#include <emscripten/bind.h> +#include <private/qstdweb_p.h> #include <QCoreApplication> #include <qpa/qwindowsysteminterface.h> +#include <QBuffer> +#include <QString> -using namespace emscripten; - -// there has got to be a better way... -static QString g_clipboardText; -static QString g_clipboardFormat; +#include <emscripten/val.h> -static val getClipboardData() -{ - return QWasmString::fromQString(g_clipboardText); -} +QT_BEGIN_NAMESPACE +using namespace emscripten; -static val getClipboardFormat() +static void commonCopyEvent(val event) { - return QWasmString::fromQString(g_clipboardFormat); -} + QMimeData *_mimes = QWasmIntegration::get()->getWasmClipboard()->mimeData(QClipboard::Clipboard); + if (!_mimes) + return; + + // doing it this way seems to sanitize the text better that calling data() like down below + if (_mimes->hasText()) { + event["clipboardData"].call<void>("setData", val("text/plain"), + _mimes->text().toEcmaString()); + } + if (_mimes->hasHtml()) { + event["clipboardData"].call<void>("setData", val("text/html"), _mimes->html().toEcmaString()); + } -static void pasteClipboardData(emscripten::val format, emscripten::val dataPtr) -{ - QString formatString = QWasmString::toQString(format); - QByteArray dataArray = QByteArray::fromStdString(dataPtr.as<std::string>()); - QMimeData *mMimeData = new QMimeData; - mMimeData->setData(formatString, dataArray); - QWasmClipboard::qWasmClipboardPaste(mMimeData); -} + for (auto mimetype : _mimes->formats()) { + if (mimetype.contains("text/")) + continue; + QByteArray ba = _mimes->data(mimetype); + if (!ba.isEmpty()) + event["clipboardData"].call<void>("setData", mimetype.toEcmaString(), + val(ba.constData())); + } -static void qClipboardPromiseResolve(emscripten::val something) -{ - pasteClipboardData(emscripten::val("text/plain"), something); + event.call<void>("preventDefault"); } static void qClipboardCutTo(val event) { - if (!QWasmIntegration::get()->getWasmClipboard()->hasClipboardApi) { + if (!QWasmIntegration::get()->getWasmClipboard()->hasClipboardApi()) { // Send synthetic Ctrl+X to make the app cut data to Qt's clipboard - QWindowSystemInterface::handleKeyEvent<QWindowSystemInterface::SynchronousDelivery>( - 0, QEvent::KeyPress, Qt::Key_X, Qt::ControlModifier, "X"); - } + QWindowSystemInterface::handleKeyEvent( + 0, QEvent::KeyPress, Qt::Key_X, Qt::ControlModifier, "X"); + } - val module = val::global("Module"); - val clipdata = module.call<val>("qtGetClipboardData"); - val clipFormat = module.call<val>("qtGetClipboardFormat"); - event["clipboardData"].call<void>("setData", clipFormat, clipdata); - event.call<void>("preventDefault"); + commonCopyEvent(event); } static void qClipboardCopyTo(val event) { - if (!QWasmIntegration::get()->getWasmClipboard()->hasClipboardApi) { + if (!QWasmIntegration::get()->getWasmClipboard()->hasClipboardApi()) { // Send synthetic Ctrl+C to make the app copy data to Qt's clipboard - QWindowSystemInterface::handleKeyEvent<QWindowSystemInterface::SynchronousDelivery>( - 0, QEvent::KeyPress, Qt::Key_C, Qt::ControlModifier, "C"); + QWindowSystemInterface::handleKeyEvent( + 0, QEvent::KeyPress, Qt::Key_C, Qt::ControlModifier, "C"); } - - val module = val::global("Module"); - val clipdata = module.call<val>("qtGetClipboardData"); - val clipFormat = module.call<val>("qtGetClipboardFormat"); - event["clipboardData"].call<void>("setData", clipFormat, clipdata); - event.call<void>("preventDefault"); + commonCopyEvent(event); } static void qClipboardPasteTo(val event) { - bool hasClipboardApi = QWasmIntegration::get()->getWasmClipboard()->hasClipboardApi; - val clipdata = hasClipboardApi ? - val::global("Module").call<val>("qtGetClipboardData") : - event["clipboardData"].call<val>("getData", val("text")); + event.call<void>("preventDefault"); // prevent browser from handling drop event - const QString qstr = QWasmString::toQString(clipdata); - if (qstr.length() > 0) { - QMimeData *mMimeData = new QMimeData; - mMimeData->setText(qstr); - QWasmClipboard::qWasmClipboardPaste(mMimeData); - } + QWasmIntegration::get()->getWasmClipboard()->sendClipboardData(event); } EMSCRIPTEN_BINDINGS(qtClipboardModule) { - function("qtGetClipboardData", &getClipboardData); - function("qtGetClipboardFormat", &getClipboardFormat); - function("qtPasteClipboardData", &pasteClipboardData); - function("qtClipboardPromiseResolve", &qClipboardPromiseResolve); function("qtClipboardCutTo", &qClipboardCutTo); function("qtClipboardCopyTo", &qClipboardCopyTo); function("qtClipboardPasteTo", &qClipboardPasteTo); @@ -126,19 +82,19 @@ EMSCRIPTEN_BINDINGS(qtClipboardModule) { QWasmClipboard::QWasmClipboard() { val clipboard = val::global("navigator")["clipboard"]; - val permissions = val::global("navigator")["permissions"]; - hasClipboardApi = (!clipboard.isUndefined() && !permissions.isUndefined() && !clipboard["readText"].isUndefined()); - if (hasClipboardApi) - initClipboardEvents(); + + const bool hasPermissionsApi = !val::global("navigator")["permissions"].isUndefined(); + m_hasClipboardApi = !clipboard.isUndefined() && !clipboard["readText"].isUndefined(); + + if (m_hasClipboardApi && hasPermissionsApi) + initClipboardPermissions(); } QWasmClipboard::~QWasmClipboard() { - g_clipboardText.clear(); - g_clipboardFormat.clear(); } -QMimeData* QWasmClipboard::mimeData(QClipboard::Mode mode) +QMimeData *QWasmClipboard::mimeData(QClipboard::Mode mode) { if (mode != QClipboard::Clipboard) return nullptr; @@ -146,17 +102,29 @@ QMimeData* QWasmClipboard::mimeData(QClipboard::Mode mode) return QPlatformClipboard::mimeData(mode); } -void QWasmClipboard::setMimeData(QMimeData* mimeData, QClipboard::Mode mode) +void QWasmClipboard::setMimeData(QMimeData *mimeData, QClipboard::Mode mode) { - if (mimeData->hasText()) { - g_clipboardFormat = mimeData->formats().at(0); - g_clipboardText = mimeData->text(); - } else if (mimeData->hasHtml()) { - g_clipboardFormat = mimeData->formats().at(0); - g_clipboardText = mimeData->html(); - } - + // handle setText/ setData programmatically QPlatformClipboard::setMimeData(mimeData, mode); + if (m_hasClipboardApi) + writeToClipboardApi(); + else + writeToClipboard(); +} + +QWasmClipboard::ProcessKeyboardResult QWasmClipboard::processKeyboard(const KeyEvent &event) +{ + if (event.type != EventType::KeyDown || !event.modifiers.testFlag(Qt::ControlModifier)) + return ProcessKeyboardResult::Ignored; + + if (event.key != Qt::Key_C && event.key != Qt::Key_V && event.key != Qt::Key_X) + return ProcessKeyboardResult::Ignored; + + const bool isPaste = event.key == Qt::Key_V; + + return m_hasClipboardApi && !isPaste + ? ProcessKeyboardResult::NativeClipboardEventAndCopiedDataNeeded + : ProcessKeyboardResult::NativeClipboardEventNeeded; } bool QWasmClipboard::supportsMode(QClipboard::Mode mode) const @@ -170,60 +138,167 @@ bool QWasmClipboard::ownsMode(QClipboard::Mode mode) const return false; } -void QWasmClipboard::qWasmClipboardPaste(QMimeData *mData) +void QWasmClipboard::initClipboardPermissions() { - QWasmIntegration::get()->clipboard()->setMimeData(mData, QClipboard::Clipboard); + val permissions = val::global("navigator")["permissions"]; - QWindowSystemInterface::handleKeyEvent<QWindowSystemInterface::SynchronousDelivery>( - 0, QEvent::KeyPress, Qt::Key_V, Qt::ControlModifier, "V"); + qstdweb::Promise::make(permissions, "query", { .catchFunc = [](emscripten::val) {} }, ([]() { + val readPermissionsMap = val::object(); + readPermissionsMap.set("name", val("clipboard-read")); + return readPermissionsMap; + })()); + qstdweb::Promise::make(permissions, "query", { .catchFunc = [](emscripten::val) {} }, ([]() { + val readPermissionsMap = val::object(); + readPermissionsMap.set("name", val("clipboard-write")); + return readPermissionsMap; + })()); } -void QWasmClipboard::initClipboardEvents() +void QWasmClipboard::installEventHandlers(const emscripten::val &target) { - if (!hasClipboardApi) - return; - - val permissions = val::global("navigator")["permissions"]; - val readPermissionsMap = val::object(); - readPermissionsMap.set("name", val("clipboard-read")); - permissions.call<val>("query", readPermissionsMap); + emscripten::val cContext = val::undefined(); + emscripten::val isChromium = val::global("window")["chrome"]; + if (!isChromium.isUndefined()) { + cContext = val::global("document"); + } else { + cContext = target; + } + // Fallback path for browsers which do not support direct clipboard access + cContext.call<void>("addEventListener", val("cut"), + val::module_property("qtClipboardCutTo"), true); + cContext.call<void>("addEventListener", val("copy"), + val::module_property("qtClipboardCopyTo"), true); + cContext.call<void>("addEventListener", val("paste"), + val::module_property("qtClipboardPasteTo"), true); +} - val writePermissionsMap = val::object(); - writePermissionsMap.set("name", val("clipboard-write")); - permissions.call<val>("query", writePermissionsMap); +bool QWasmClipboard::hasClipboardApi() +{ + return m_hasClipboardApi; } -void QWasmClipboard::installEventHandlers(const emscripten::val &canvas) +void QWasmClipboard::writeToClipboardApi() { - if (hasClipboardApi) + Q_ASSERT(m_hasClipboardApi); + + // copy event + // browser event handler detected ctrl c if clipboard API + // or Qt call from keyboard event handler + + QMimeData *_mimes = mimeData(QClipboard::Clipboard); + if (!_mimes) return; - // Fallback path for browsers which do not support direct clipboard access - canvas.call<void>("addEventListener", val("cut"), - val::module_property("qtClipboardCutTo")); - canvas.call<void>("addEventListener", val("copy"), - val::module_property("qtClipboardCopyTo")); - canvas.call<void>("addEventListener", val("paste"), - val::module_property("qtClipboardPasteTo")); + emscripten::val clipboardWriteArray = emscripten::val::array(); + QByteArray ba; + + for (auto mimetype : _mimes->formats()) { + // we need to treat binary and text differently, as the blob method below + // fails for text mimetypes + // ignore text types + + if (mimetype.contains("STRING", Qt::CaseSensitive) || mimetype.contains("TEXT", Qt::CaseSensitive)) + continue; + + if (_mimes->hasHtml()) { // prefer html over text + ba = _mimes->html().toLocal8Bit(); + // force this mime + mimetype = "text/html"; + } else if (mimetype.contains("text/plain")) { + ba = _mimes->text().toLocal8Bit(); + } else if (mimetype.contains("image")) { + QImage img = qvariant_cast<QImage>( _mimes->imageData()); + QBuffer buffer(&ba); + buffer.open(QIODevice::WriteOnly); + img.save(&buffer, "PNG"); + mimetype = "image/png"; // chrome only allows png + // clipboard error "NotAllowedError" "Type application/x-qt-image not supported on write." + // safari silently fails + // so we use png internally for now + } else { + // DATA + ba = _mimes->data(mimetype); + } + // Create file data Blob + + const char *content = ba.data(); + int dataLength = ba.length(); + if (dataLength < 1) { + qDebug() << "no content found"; + return; + } + + emscripten::val document = emscripten::val::global("document"); + emscripten::val window = emscripten::val::global("window"); + + emscripten::val fileContentView = + emscripten::val(emscripten::typed_memory_view(dataLength, content)); + emscripten::val fileContentCopy = emscripten::val::global("ArrayBuffer").new_(dataLength); + emscripten::val fileContentCopyView = + emscripten::val::global("Uint8Array").new_(fileContentCopy); + fileContentCopyView.call<void>("set", fileContentView); + + emscripten::val contentArray = emscripten::val::array(); + contentArray.call<void>("push", fileContentCopyView); + + // we have a blob, now create a ClipboardItem + emscripten::val type = emscripten::val::array(); + type.set("type", mimetype.toEcmaString()); + + emscripten::val contentBlob = emscripten::val::global("Blob").new_(contentArray, type); + + emscripten::val clipboardItemObject = emscripten::val::object(); + clipboardItemObject.set(mimetype.toEcmaString(), contentBlob); + + val clipboardItemData = val::global("ClipboardItem").new_(clipboardItemObject); + + clipboardWriteArray.call<void>("push", clipboardItemData); + + // Clipboard write is only supported with one ClipboardItem at the moment + // but somehow this still works? + // break; + } + + val navigator = val::global("navigator"); + + qstdweb::Promise::make( + navigator["clipboard"], "write", + { + .catchFunc = [](emscripten::val error) { + qWarning() << "clipboard error" + << QString::fromStdString(error["name"].as<std::string>()) + << QString::fromStdString(error["message"].as<std::string>()); + } + }, + clipboardWriteArray); } -void QWasmClipboard::readTextFromClipboard() +void QWasmClipboard::writeToClipboard() { - if (QWasmIntegration::get()->getWasmClipboard()->hasClipboardApi) { - val navigator = val::global("navigator"); - val textPromise = navigator["clipboard"].call<val>("readText"); - val readTextResolve = val::global("Module")["qtClipboardPromiseResolve"]; - textPromise.call<val>("then", readTextResolve); - } + // this works for firefox, chrome by generating + // copy event, but not safari + // execCommand has been deemed deprecated in the docs, but browsers do not seem + // interested in removing it. There is no replacement, so we use it here. + val document = val::global("document"); + document.call<val>("execCommand", val("copy")); } -void QWasmClipboard::writeTextToClipboard() +void QWasmClipboard::sendClipboardData(emscripten::val event) { - if (QWasmIntegration::get()->getWasmClipboard()->hasClipboardApi) { - val module = val::global("Module"); - val txt = module.call<val>("qtGetClipboardData"); - val format = module.call<val>("qtGetClipboardFormat"); - val navigator = val::global("navigator"); - navigator["clipboard"].call<void>("writeText", txt); - } + qDebug() << "sendClipboardData"; + + dom::DataTransfer *transfer = new dom::DataTransfer(event["clipboardData"]); + const auto mimeCallback = std::function([transfer](QMimeData *data) { + + // Persist clipboard data so that the app can read it when handling the CTRL+V + QWasmIntegration::get()->clipboard()->QPlatformClipboard::setMimeData(data, QClipboard::Clipboard); + QWindowSystemInterface::handleKeyEvent(0, QEvent::KeyPress, Qt::Key_V, + Qt::ControlModifier, "V"); + QWindowSystemInterface::handleKeyEvent(0, QEvent::KeyRelease, Qt::Key_V, + Qt::ControlModifier, "V"); + delete transfer; + }); + + transfer->toMimeDataWithFile(mimeCallback); } +QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmclipboard.h b/src/plugins/platforms/wasm/qwasmclipboard.h index 3b28e2c381..86618dd560 100644 --- a/src/plugins/platforms/wasm/qwasmclipboard.h +++ b/src/plugins/platforms/wasm/qwasmclipboard.h @@ -1,31 +1,5 @@ -/**************************************************************************** -** -** 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$ -** -****************************************************************************/ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #ifndef QWasmClipboard_H #define QWasmClipboard_H @@ -33,14 +7,25 @@ #include <QObject> #include <qpa/qplatformclipboard.h> +#include <private/qstdweb_p.h> #include <QMimeData> #include <emscripten/bind.h> #include <emscripten/val.h> +QT_BEGIN_NAMESPACE + +struct KeyEvent; + class QWasmClipboard : public QObject, public QPlatformClipboard { public: + enum class ProcessKeyboardResult { + Ignored, + NativeClipboardEventNeeded, + NativeClipboardEventAndCopiedDataNeeded, + }; + QWasmClipboard(); virtual ~QWasmClipboard(); @@ -50,12 +35,19 @@ public: bool supportsMode(QClipboard::Mode mode) const override; bool ownsMode(QClipboard::Mode mode) const override; - static void qWasmClipboardPaste(QMimeData *mData); - void initClipboardEvents(); - void installEventHandlers(const emscripten::val &canvas); - bool hasClipboardApi; - void readTextFromClipboard(); - void writeTextToClipboard(); + ProcessKeyboardResult processKeyboard(const KeyEvent &event); + static void installEventHandlers(const emscripten::val &target); + bool hasClipboardApi(); + void sendClipboardData(emscripten::val event); + +private: + void initClipboardPermissions(); + void writeToClipboardApi(); + void writeToClipboard(); + + bool m_hasClipboardApi = false; }; +QT_END_NAMESPACE + #endif // QWASMCLIPBOARD_H diff --git a/src/plugins/platforms/wasm/qwasmcompositor.cpp b/src/plugins/platforms/wasm/qwasmcompositor.cpp index 9a16ae7719..ef460f666f 100644 --- a/src/plugins/platforms/wasm/qwasmcompositor.cpp +++ b/src/plugins/platforms/wasm/qwasmcompositor.cpp @@ -1,96 +1,43 @@ -/**************************************************************************** -** -** 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$ -** -****************************************************************************/ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "qwasmcompositor.h" #include "qwasmwindow.h" -#include "qwasmstylepixmaps_p.h" -#include <QtOpenGL/qopengltexture.h> - -#include <QtGui/private/qwindow_p.h> -#include <QtGui/qopenglcontext.h> -#include <QtGui/qopenglfunctions.h> -#include <QtGui/qoffscreensurface.h> -#include <QtGui/qpainter.h> -#include <private/qpixmapcache_p.h> - -#include <private/qguiapplication_p.h> +#include <private/qeventdispatcher_wasm_p.h> #include <qpa/qwindowsysteminterface.h> -#include <QtCore/qcoreapplication.h> -#include <QtGui/qguiapplication.h> -Q_GUI_EXPORT int qt_defaultDpiX(); +#include <emscripten/html5.h> -QWasmCompositedWindow::QWasmCompositedWindow() - : window(nullptr) - , parentWindow(nullptr) - , flushPending(false) - , visible(false) -{ -} +using namespace emscripten; + +bool QWasmCompositor::m_requestUpdateHoldEnabled = true; -QWasmCompositor::QWasmCompositor(QWasmScreen *screen) - :QObject(screen) - , m_blitter(new QOpenGLTextureBlitter) - , m_needComposit(false) - , m_inFlush(false) - , m_inResize(false) - , m_isEnabled(true) - , m_targetDevicePixelRatio(1) +QWasmCompositor::QWasmCompositor(QWasmScreen *screen) : QObject(screen) { + QWindowSystemInterface::setSynchronousWindowSystemEvents(true); } QWasmCompositor::~QWasmCompositor() { - destroy(); + if (m_requestAnimationFrameId != -1) + emscripten_cancel_animation_frame(m_requestAnimationFrameId); + + // TODO(mikolaj.boc): Investigate if m_isEnabled is needed at all. It seems like a frame should + // not be generated after this instead. + m_isEnabled = false; // prevent frame() from creating a new m_context } -void QWasmCompositor::destroy() +void QWasmCompositor::onWindowTreeChanged(QWasmWindowTreeNodeChangeType changeType, + QWasmWindow *window) { - // Destroy OpenGL resources. This is done here in a separate function - // which can be called while screen() still returns a valid screen - // (which it might not, during destruction). A valid QScreen is - // a requirement for QOffscreenSurface on Wasm since the native - // context is tied to a single canvas. - if (m_context) { - QOffscreenSurface offScreenSurface(screen()->screen()); - offScreenSurface.setFormat(m_context->format()); - offScreenSurface.create(); - m_context->makeCurrent(&offScreenSurface); - for (QWasmWindow *window : m_windowStack) - window->destroy(); - m_blitter.reset(nullptr); - m_context.reset(nullptr); - } - - m_isEnabled = false; // prevent frame() from creating a new m_context + auto allWindows = screen()->allWindows(); + setEnabled(std::any_of(allWindows.begin(), allWindows.end(), [](QWasmWindow *element) { + return !element->context2d().isUndefined(); + })); + if (changeType == QWasmWindowTreeNodeChangeType::NodeRemoval) + m_requestUpdateWindows.remove(window); } void QWasmCompositor::setEnabled(bool enabled) @@ -98,656 +45,115 @@ void QWasmCompositor::setEnabled(bool enabled) m_isEnabled = enabled; } -void QWasmCompositor::addWindow(QWasmWindow *window, QWasmWindow *parentWindow) +// requestUpdate delivery is initially disabled at startup, while Qt completes +// startup tasks such as font loading. This function enables requestUpdate delivery +// again. +bool QWasmCompositor::releaseRequestUpdateHold() { - QWasmCompositedWindow compositedWindow; - compositedWindow.window = window; - compositedWindow.parentWindow = parentWindow; - m_compositedWindows.insert(window, compositedWindow); - - if (parentWindow == 0) - m_windowStack.append(window); - else - m_compositedWindows[parentWindow].childWindows.append(window); - - notifyTopWindowChanged(window); + const bool wasEnabled = m_requestUpdateHoldEnabled; + m_requestUpdateHoldEnabled = false; + return wasEnabled; } -void QWasmCompositor::removeWindow(QWasmWindow *window) +void QWasmCompositor::requestUpdateWindow(QWasmWindow *window, UpdateRequestDeliveryType updateType) { - QWasmWindow *platformWindow = m_compositedWindows[window].parentWindow; - - if (platformWindow) { - QWasmWindow *parentWindow = window; - m_compositedWindows[parentWindow].childWindows.removeAll(window); + auto it = m_requestUpdateWindows.find(window); + if (it == m_requestUpdateWindows.end()) { + m_requestUpdateWindows.insert(window, updateType); + } else { + // Already registered, but upgrade ExposeEventDeliveryType to UpdateRequestDeliveryType. + // if needed, to make sure QWindow::updateRequest's are matched. + if (it.value() == ExposeEventDelivery && updateType == UpdateRequestDelivery) + it.value() = UpdateRequestDelivery; } - m_windowStack.removeAll(window); - m_compositedWindows.remove(window); - - notifyTopWindowChanged(window); + requestUpdate(); } -void QWasmCompositor::setVisible(QWasmWindow *window, bool visible) +// Requests an update/new frame using RequestAnimationFrame +void QWasmCompositor::requestUpdate() { - QWasmCompositedWindow &compositedWindow = m_compositedWindows[window]; - if (compositedWindow.visible == visible) + if (m_requestAnimationFrameId != -1) return; - compositedWindow.visible = visible; - compositedWindow.flushPending = true; - if (visible) - compositedWindow.damage = compositedWindow.window->geometry(); - else - m_globalDamage = compositedWindow.window->geometry(); // repaint previosly covered area. - - requestRedraw(); -} - -void QWasmCompositor::raise(QWasmWindow *window) -{ - if (m_compositedWindows.size() <= 1) - return; - - QWasmCompositedWindow &compositedWindow = m_compositedWindows[window]; - compositedWindow.damage = compositedWindow.window->geometry(); - m_windowStack.removeAll(window); - m_windowStack.append(window); - - notifyTopWindowChanged(window); -} - -void QWasmCompositor::lower(QWasmWindow *window) -{ - if (m_compositedWindows.size() <= 1) + if (m_requestUpdateHoldEnabled) return; - m_windowStack.removeAll(window); - m_windowStack.prepend(window); - QWasmCompositedWindow &compositedWindow = m_compositedWindows[window]; - m_globalDamage = compositedWindow.window->geometry(); // repaint previosly covered area. - - notifyTopWindowChanged(window); -} - -void QWasmCompositor::setParent(QWasmWindow *window, QWasmWindow *parent) -{ - m_compositedWindows[window].parentWindow = parent; - - requestRedraw(); -} - -void QWasmCompositor::flush(QWasmWindow *window, const QRegion ®ion) -{ - QWasmCompositedWindow &compositedWindow = m_compositedWindows[window]; - compositedWindow.flushPending = true; - compositedWindow.damage = region; + static auto frame = [](double frameTime, void *context) -> int { + Q_UNUSED(frameTime); - requestRedraw(); -} + QWasmCompositor *compositor = reinterpret_cast<QWasmCompositor *>(context); -int QWasmCompositor::windowCount() const -{ - return m_windowStack.count(); -} + compositor->m_requestAnimationFrameId = -1; + compositor->deliverUpdateRequests(); - -void QWasmCompositor::redrawWindowContent() -{ - // Redraw window content by sending expose events. This redraw - // will cause a backing store flush, which will call requestRedraw() - // to composit. - for (QWasmWindow *platformWindow : m_windowStack) { - QWindow *window = platformWindow->window(); - QWindowSystemInterface::handleExposeEvent<QWindowSystemInterface::SynchronousDelivery>( - window, QRect(QPoint(0, 0), window->geometry().size())); - } -} - -void QWasmCompositor::requestRedraw() -{ - if (m_needComposit) - return; - - m_needComposit = true; - QCoreApplication::postEvent(this, new QEvent(QEvent::UpdateRequest)); -} - -QWindow *QWasmCompositor::windowAt(QPoint globalPoint, int padding) const -{ - int index = m_windowStack.count() - 1; - // qDebug() << "window at" << "point" << p << "window count" << index; - - while (index >= 0) { - const QWasmCompositedWindow &compositedWindow = m_compositedWindows[m_windowStack.at(index)]; - //qDebug() << "windwAt testing" << compositedWindow.window << - - QRect geometry = compositedWindow.window->windowFrameGeometry() - .adjusted(-padding, -padding, padding, padding); - - if (compositedWindow.visible && geometry.contains(globalPoint)) - return m_windowStack.at(index)->window(); - --index; - } - - return 0; -} - -QWindow *QWasmCompositor::keyWindow() const -{ - return m_windowStack.at(m_windowStack.count() - 1)->window(); -} - -bool QWasmCompositor::event(QEvent *ev) -{ - if (ev->type() == QEvent::UpdateRequest) { - if (m_isEnabled) - frame(); - return true; - } - - return QObject::event(ev); -} - -void QWasmCompositor::blit(QOpenGLTextureBlitter *blitter, QWasmScreen *screen, const QOpenGLTexture *texture, QRect targetGeometry) -{ - QMatrix4x4 m; - m.translate(-1.0f, -1.0f); - - m.scale(2.0f / (float)screen->geometry().width(), - 2.0f / (float)screen->geometry().height()); - - m.translate((float)targetGeometry.width() / 2.0f, - (float)-targetGeometry.height() / 2.0f); - - m.translate(targetGeometry.x(), screen->geometry().height() - targetGeometry.y()); - - m.scale(0.5f * (float)targetGeometry.width(), - 0.5f * (float)targetGeometry.height()); - - blitter->blit(texture->textureId(), m, QOpenGLTextureBlitter::OriginTopLeft); -} - -void QWasmCompositor::drawWindowContent(QOpenGLTextureBlitter *blitter, QWasmScreen *screen, QWasmWindow *window) -{ - QWasmBackingStore *backingStore = window->backingStore(); - if (!backingStore) - return; - - QOpenGLTexture const *texture = backingStore->getUpdatedTexture(); - QPoint windowCanvasPosition = window->geometry().topLeft() - screen->geometry().topLeft(); - QRect windowCanvasGeometry = QRect(windowCanvasPosition, window->geometry().size()); - blit(blitter, screen, texture, windowCanvasGeometry); -} - -QPalette QWasmCompositor::makeWindowPalette() -{ - QPalette palette; - palette.setColor(QPalette::Active, QPalette::Highlight, - palette.color(QPalette::Active, QPalette::Highlight)); - palette.setColor(QPalette::Active, QPalette::Base, - palette.color(QPalette::Active, QPalette::Highlight)); - palette.setColor(QPalette::Inactive, QPalette::Highlight, - palette.color(QPalette::Inactive, QPalette::Dark)); - palette.setColor(QPalette::Inactive, QPalette::Base, - palette.color(QPalette::Inactive, QPalette::Dark)); - palette.setColor(QPalette::Inactive, QPalette::HighlightedText, - palette.color(QPalette::Inactive, QPalette::Window)); - - return palette; -} - -QRect QWasmCompositor::titlebarRect(QWasmTitleBarOptions tb, QWasmCompositor::SubControls subcontrol) -{ - QRect ret; - const int controlMargin = 2; - const int controlHeight = tb.rect.height() - controlMargin *2; - const int delta = controlHeight + controlMargin; - int offset = 0; - - bool isMinimized = tb.state & Qt::WindowMinimized; - bool isMaximized = tb.state & Qt::WindowMaximized; - - ret = tb.rect; - switch (subcontrol) { - case SC_TitleBarLabel: - if (tb.flags & Qt::WindowSystemMenuHint) - ret.adjust(delta, 0, -delta, 0); - break; - case SC_TitleBarCloseButton: - if (tb.flags & Qt::WindowSystemMenuHint) { - ret.adjust(0, 0, -delta, 0); - offset += delta; - } - break; - case SC_TitleBarMaxButton: - if (!isMaximized && tb.flags & Qt::WindowMaximizeButtonHint) { - ret.adjust(0, 0, -delta*2, 0); - offset += (delta +delta); - } - break; - case SC_TitleBarNormalButton: - if (isMinimized && (tb.flags & Qt::WindowMinimizeButtonHint)) { - offset += delta; - } else if (isMaximized && (tb.flags & Qt::WindowMaximizeButtonHint)) { - ret.adjust(0, 0, -delta*2, 0); - offset += (delta +delta); - } - break; - case SC_TitleBarSysMenu: - if (tb.flags & Qt::WindowSystemMenuHint) { - ret.setRect(tb.rect.left() + controlMargin, tb.rect.top() + controlMargin, - controlHeight, controlHeight); - } - break; - default: - break; + return 0; }; - - if (subcontrol != SC_TitleBarLabel && subcontrol != SC_TitleBarSysMenu) { - ret.setRect(tb.rect.right() - offset, tb.rect.top() + controlMargin, - controlHeight, controlHeight); - } - - if (qApp->layoutDirection() == Qt::LeftToRight) - return ret; - - QRect rect = ret; - rect.translate(2 * (tb.rect.right() - ret.right()) + - ret.width() - tb.rect.width(), 0); - - return rect; + m_requestAnimationFrameId = emscripten_request_animation_frame(frame, this); } -int dpiScaled(qreal value) +void QWasmCompositor::deliverUpdateRequests() { - return value * (qreal(qt_defaultDpiX()) / 96.0); -} - -QWasmCompositor::QWasmTitleBarOptions QWasmCompositor::makeTitleBarOptions(const QWasmWindow *window) -{ - int width = window->windowFrameGeometry().width(); - int border = window->borderWidth(); - - QWasmTitleBarOptions titleBarOptions; - - titleBarOptions.rect = QRect(border, border, width - 2 * border, window->titleHeight()); - titleBarOptions.flags = window->window()->flags(); - titleBarOptions.state = window->window()->windowState(); + // We may get new update requests during the window content update below: + // prepare for recording the new update set by setting aside the current + // update set. + auto requestUpdateWindows = m_requestUpdateWindows; + m_requestUpdateWindows.clear(); - bool isMaximized = titleBarOptions.state & Qt::WindowMaximized; // this gets reset when maximized - - if (titleBarOptions.flags & (Qt::WindowTitleHint)) - titleBarOptions.subControls |= SC_TitleBarLabel; - if (titleBarOptions.flags & Qt::WindowMaximizeButtonHint) { - if (isMaximized) - titleBarOptions.subControls |= SC_TitleBarNormalButton; - else - titleBarOptions.subControls |= SC_TitleBarMaxButton; - } - if (titleBarOptions.flags & Qt::WindowSystemMenuHint) { - titleBarOptions.subControls |= SC_TitleBarCloseButton; - titleBarOptions.subControls |= SC_TitleBarSysMenu; + // Update window content, either all windows or a spesific set of windows. Use the correct + // update type: QWindow subclasses expect that requested and delivered updateRequests matches + // exactly. + m_inDeliverUpdateRequest = true; + for (auto it = requestUpdateWindows.constBegin(); it != requestUpdateWindows.constEnd(); ++it) { + auto *window = it.key(); + UpdateRequestDeliveryType updateType = it.value(); + deliverUpdateRequest(window, updateType); } - - titleBarOptions.palette = QWasmCompositor::makeWindowPalette(); - - if (window->window()->isActive()) - titleBarOptions.palette.setCurrentColorGroup(QPalette::Active); - else - titleBarOptions.palette.setCurrentColorGroup(QPalette::Inactive); - - if (window->activeSubControl() != QWasmCompositor::SC_None) - titleBarOptions.subControls = window->activeSubControl(); - - if (!window->window()->title().isEmpty()) - titleBarOptions.titleBarOptionsString = window->window()->title(); - - return titleBarOptions; -} - -void QWasmCompositor::drawWindowDecorations(QOpenGLTextureBlitter *blitter, QWasmScreen *screen, QWasmWindow *window) -{ - int width = window->windowFrameGeometry().width(); - int height = window->windowFrameGeometry().height(); - qreal dpr = window->devicePixelRatio(); - - QImage image(QSize(width * dpr, height * dpr), QImage::Format_RGB32); - image.setDevicePixelRatio(dpr); - QPainter painter(&image); - painter.fillRect(QRect(0, 0, width, height), painter.background()); - - QWasmTitleBarOptions titleBarOptions = makeTitleBarOptions(window); - - drawTitlebarWindow(titleBarOptions, &painter); - - QWasmFrameOptions frameOptions; - frameOptions.rect = QRect(0, 0, width, height); - frameOptions.lineWidth = dpiScaled(4.); - - drawFrameWindow(frameOptions, &painter); - - painter.end(); - - QOpenGLTexture texture(QOpenGLTexture::Target2D); - texture.setMinificationFilter(QOpenGLTexture::Nearest); - texture.setMagnificationFilter(QOpenGLTexture::Nearest); - texture.setWrapMode(QOpenGLTexture::ClampToEdge); - texture.setData(image, QOpenGLTexture::DontGenerateMipMaps); - texture.create(); - texture.bind(); - - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, image.width(), image.height(), GL_RGBA, GL_UNSIGNED_BYTE, - image.constScanLine(0)); - - blit(blitter, screen, &texture, QRect(window->windowFrameGeometry().topLeft(), QSize(width, height))); -} - -void QWasmCompositor::drawFrameWindow(QWasmFrameOptions options, QPainter *painter) -{ - int x = options.rect.x(); - int y = options.rect.y(); - int w = options.rect.width(); - int h = options.rect.height(); - const QColor &c1 = options.palette.light().color(); - const QColor &c2 = options.palette.shadow().color(); - const QColor &c3 = options.palette.midlight().color(); - const QColor &c4 = options.palette.dark().color(); - const QBrush *fill = nullptr; - - const qreal devicePixelRatio = painter->device()->devicePixelRatio(); - if (!qFuzzyCompare(devicePixelRatio, qreal(1))) { - const qreal inverseScale = qreal(1) / devicePixelRatio; - painter->scale(inverseScale, inverseScale); - x = qRound(devicePixelRatio * x); - y = qRound(devicePixelRatio * y); - w = qRound(devicePixelRatio * w); - h = qRound(devicePixelRatio * h); - } - - QPen oldPen = painter->pen(); - QPoint a[3] = { QPoint(x, y+h-2), QPoint(x, y), QPoint(x+w-2, y) }; - painter->setPen(c1); - painter->drawPolyline(a, 3); - QPoint b[3] = { QPoint(x, y+h-1), QPoint(x+w-1, y+h-1), QPoint(x+w-1, y) }; - painter->setPen(c2); - painter->drawPolyline(b, 3); - if (w > 4 && h > 4) { - QPoint c[3] = { QPoint(x+1, y+h-3), QPoint(x+1, y+1), QPoint(x+w-3, y+1) }; - painter->setPen(c3); - painter->drawPolyline(c, 3); - QPoint d[3] = { QPoint(x+1, y+h-2), QPoint(x+w-2, y+h-2), QPoint(x+w-2, y+1) }; - painter->setPen(c4); - painter->drawPolyline(d, 3); - if (fill) - painter->fillRect(QRect(x+2, y+2, w-4, h-4), *fill); - } - painter->setPen(oldPen); -} - -//from commonstyle.cpp -static QPixmap cachedPixmapFromXPM(const char * const *xpm) -{ - QPixmap result; - const QString tag = QString::asprintf("xpm:0x%p", static_cast<const void*>(xpm)); - if (!QPixmapCache::find(tag, &result)) { - result = QPixmap(xpm); - QPixmapCache::insert(tag, result); - } - return result; -} - -void QWasmCompositor::drawItemPixmap(QPainter *painter, const QRect &rect, int alignment, - const QPixmap &pixmap) const -{ - qreal scale = pixmap.devicePixelRatio(); - QSize size = pixmap.size() / scale; - int x = rect.x(); - int y = rect.y(); - int w = size.width(); - int h = size.height(); - if ((alignment & Qt::AlignVCenter) == Qt::AlignVCenter) - y += rect.size().height()/2 - h/2; - else if ((alignment & Qt::AlignBottom) == Qt::AlignBottom) - y += rect.size().height() - h; - if ((alignment & Qt::AlignRight) == Qt::AlignRight) - x += rect.size().width() - w; - else if ((alignment & Qt::AlignHCenter) == Qt::AlignHCenter) - x += rect.size().width()/2 - w/2; - - QRect aligned = QRect(x, y, w, h); - QRect inter = aligned.intersected(rect); - - painter->drawPixmap(inter.x(), inter.y(), pixmap, inter.x() - aligned.x(), inter.y() - aligned.y(), inter.width() * scale, inter.height() *scale); -} - - -void QWasmCompositor::drawTitlebarWindow(QWasmTitleBarOptions tb, QPainter *painter) -{ - QRect ir; - if (tb.subControls.testFlag(SC_TitleBarLabel)) { - QColor left = tb.palette.highlight().color(); - QColor right = tb.palette.base().color(); - - QBrush fillBrush(left); - if (left != right) { - QPoint p1(tb.rect.x(), tb.rect.top() + tb.rect.height()/2); - QPoint p2(tb.rect.right(), tb.rect.top() + tb.rect.height()/2); - QLinearGradient lg(p1, p2); - lg.setColorAt(0, left); - lg.setColorAt(1, right); - fillBrush = lg; - } - - painter->fillRect(tb.rect, fillBrush); - ir = titlebarRect(tb, SC_TitleBarLabel); - painter->setPen(tb.palette.highlightedText().color()); - painter->drawText(ir.x() + 2, ir.y(), ir.width() - 2, ir.height(), - Qt::AlignLeft | Qt::AlignVCenter | Qt::TextSingleLine, tb.titleBarOptionsString); - } // SC_TitleBarLabel - - bool down = false; - QPixmap pixmap; - - if (tb.subControls.testFlag(SC_TitleBarCloseButton) - && tb.flags & Qt::WindowSystemMenuHint) { - ir = titlebarRect(tb, SC_TitleBarCloseButton); - down = tb.subControls & SC_TitleBarCloseButton && (tb.state & State_Sunken); - pixmap = cachedPixmapFromXPM(qt_close_xpm).scaled(QSize(10, 10)); - drawItemPixmap(painter, ir, Qt::AlignCenter, pixmap); - } //SC_TitleBarCloseButton - - if (tb.subControls.testFlag(SC_TitleBarMaxButton) - && tb.flags & Qt::WindowMaximizeButtonHint - && !(tb.state & Qt::WindowMaximized)) { - ir = titlebarRect(tb, SC_TitleBarMaxButton); - down = tb.subControls & SC_TitleBarMaxButton && (tb.state & State_Sunken); - pixmap = cachedPixmapFromXPM(qt_maximize_xpm).scaled(QSize(10, 10)); - drawItemPixmap(painter, ir, Qt::AlignCenter, pixmap); - } //SC_TitleBarMaxButton - - bool drawNormalButton = (tb.subControls & SC_TitleBarNormalButton) - && (((tb.flags & Qt::WindowMinimizeButtonHint) - && (tb.flags & Qt::WindowMinimized)) - || ((tb.flags & Qt::WindowMaximizeButtonHint) - && (tb.flags & Qt::WindowMaximized))); - - if (drawNormalButton) { - ir = titlebarRect(tb, SC_TitleBarNormalButton); - down = tb.subControls & SC_TitleBarNormalButton && (tb.state & State_Sunken); - pixmap = cachedPixmapFromXPM(qt_normalizeup_xpm).scaled( QSize(10, 10)); - - drawItemPixmap(painter, ir, Qt::AlignCenter, pixmap); - } // SC_TitleBarNormalButton - - if (tb.subControls & SC_TitleBarSysMenu && tb.flags & Qt::WindowSystemMenuHint) { - ir = titlebarRect(tb, SC_TitleBarSysMenu); - pixmap = cachedPixmapFromXPM(qt_menu_xpm).scaled(QSize(10, 10)); - drawItemPixmap(painter, ir, Qt::AlignCenter, pixmap); - } + m_inDeliverUpdateRequest = false; + frame(requestUpdateWindows.keys()); } -void QWasmCompositor::drawShadePanel(QWasmTitleBarOptions options, QPainter *painter) +void QWasmCompositor::deliverUpdateRequest(QWasmWindow *window, UpdateRequestDeliveryType updateType) { - int lineWidth = 1; - QPalette palette = options.palette; - const QBrush *fill = &options.palette.brush(QPalette::Button); - - int x = options.rect.x(); - int y = options.rect.y(); - int w = options.rect.width(); - int h = options.rect.height(); - - const qreal devicePixelRatio = painter->device()->devicePixelRatio(); - if (!qFuzzyCompare(devicePixelRatio, qreal(1))) { - const qreal inverseScale = qreal(1) / devicePixelRatio; - painter->scale(inverseScale, inverseScale); - - x = qRound(devicePixelRatio * x); - y = qRound(devicePixelRatio * y); - w = qRound(devicePixelRatio * w); - h = qRound(devicePixelRatio * h); - lineWidth = qRound(devicePixelRatio * lineWidth); - } - - QColor shade = palette.dark().color(); - QColor light = palette.light().color(); - - if (fill) { - if (fill->color() == shade) - shade = palette.shadow().color(); - if (fill->color() == light) - light = palette.midlight().color(); - } - QPen oldPen = painter->pen(); - QList<QLineF> lines; - lines.reserve(2*lineWidth); - - painter->setPen(light); - int x1, y1, x2, y2; - int i; - x1 = x; - y1 = y2 = y; - x2 = x + w - 2; - for (i = 0; i < lineWidth; i++) // top shadow - lines << QLineF(x1, y1++, x2--, y2++); + QWindow *qwindow = window->window(); - x2 = x1; - y1 = y + h - 2; - for (i = 0; i < lineWidth; i++) // left shado - lines << QLineF(x1++, y1, x2++, y2--); + // Make sure the DPR value for the window is up to date on expose/repaint. + // FIXME: listen to native DPR change events instead, if/when available. + QWindowSystemInterface::handleWindowDevicePixelRatioChanged(qwindow); - painter->drawLines(lines); - lines.clear(); - painter->setPen(shade); - x1 = x; - y1 = y2 = y+h-1; - x2 = x+w-1; - for (i=0; i<lineWidth; i++) { // bottom shadow - lines << QLineF(x1++, y1--, x2, y2--); + // Update by deliverUpdateRequest and expose event according to requested update + // type. If the window has not yet been exposed then we must expose it first regardless + // of update type. The deliverUpdateRequest must still be sent in this case in order + // to maintain correct window update state. + QRect updateRect(QPoint(0, 0), qwindow->geometry().size()); + if (updateType == UpdateRequestDelivery) { + if (qwindow->isExposed() == false) + QWindowSystemInterface::handleExposeEvent(qwindow, updateRect); + window->deliverUpdateRequest(); + } else { + QWindowSystemInterface::handleExposeEvent(qwindow, updateRect); } - x1 = x2; - y1 = y; - y2 = y + h - lineWidth - 1; - for (i = 0; i < lineWidth; i++) // right shadow - lines << QLineF(x1--, y1++, x2--, y2); - - painter->drawLines(lines); - if (fill) // fill with fill color - painter->fillRect(x+lineWidth, y+lineWidth, w-lineWidth*2, h-lineWidth*2, *fill); - painter->setPen(oldPen); // restore pen - } -void QWasmCompositor::drawWindow(QOpenGLTextureBlitter *blitter, QWasmScreen *screen, QWasmWindow *window) +void QWasmCompositor::handleBackingStoreFlush(QWindow *window) { - if (window->window()->type() != Qt::Popup && !(window->m_windowState & Qt::WindowFullScreen)) - drawWindowDecorations(blitter, screen, window); - drawWindowContent(blitter, screen, window); + // Request update to flush the updated backing store content, unless we are currently + // processing an update, in which case the new content will flushed as a part of that update. + if (!m_inDeliverUpdateRequest) + requestUpdateWindow(static_cast<QWasmWindow *>(window->handle())); } -void QWasmCompositor::frame() +void QWasmCompositor::frame(const QList<QWasmWindow *> &windows) { - if (!m_needComposit) - return; - - m_needComposit = false; - - if (!m_isEnabled || m_windowStack.empty() || !screen()) + if (!m_isEnabled || !screen()) return; - QWasmWindow *someWindow = nullptr; - - for (QWasmWindow *window : qAsConst(m_windowStack)) { - if (window->window()->surfaceClass() == QSurface::Window - && qt_window_private(static_cast<QWindow *>(window->window()))->receivedExpose) { - someWindow = window; - break; - } - } - - if (!someWindow) - return; - - if (m_context.isNull()) { - m_context.reset(new QOpenGLContext()); - m_context->setFormat(someWindow->window()->requestedFormat()); - m_context->setScreen(screen()->screen()); - m_context->create(); - } - - bool ok = m_context->makeCurrent(someWindow->window()); - if (!ok) - return; - - if (!m_blitter->isCreated()) - m_blitter->create(); - - qreal dpr = screen()->devicePixelRatio(); - glViewport(0, 0, screen()->geometry().width() * dpr, screen()->geometry().height() * dpr); - - m_context->functions()->glClearColor(0.2, 0.2, 0.2, 1.0); - m_context->functions()->glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); - - m_blitter->bind(); - m_blitter->setRedBlueSwizzle(true); - - for (QWasmWindow *window : qAsConst(m_windowStack)) { - QWasmCompositedWindow &compositedWindow = m_compositedWindows[window]; - - if (!compositedWindow.visible) - continue; - - drawWindow(m_blitter.data(), screen(), window); - } - - m_blitter->release(); - - if (someWindow && someWindow->window()->surfaceType() == QSurface::OpenGLSurface) - m_context->swapBuffers(someWindow->window()); -} - -void QWasmCompositor::notifyTopWindowChanged(QWasmWindow *window) -{ - QWindow *modalWindow; - bool blocked = QGuiApplicationPrivate::instance()->isWindowBlocked(window->window(), &modalWindow); - - if (blocked) { - raise(static_cast<QWasmWindow*>(modalWindow->handle())); - return; - } - - requestRedraw(); - QWindowSystemInterface::handleWindowActivated(window->window()); + for (QWasmWindow *window : windows) + window->paint(); } QWasmScreen *QWasmCompositor::screen() { return static_cast<QWasmScreen *>(parent()); } - -QOpenGLContext *QWasmCompositor::context() -{ - return m_context.data(); -} diff --git a/src/plugins/platforms/wasm/qwasmcompositor.h b/src/plugins/platforms/wasm/qwasmcompositor.h index a07c747a98..4953d65233 100644 --- a/src/plugins/platforms/wasm/qwasmcompositor.h +++ b/src/plugins/platforms/wasm/qwasmcompositor.h @@ -1,166 +1,59 @@ -/**************************************************************************** -** -** 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$ -** -****************************************************************************/ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #ifndef QWASMCOMPOSITOR_H #define QWASMCOMPOSITOR_H -#include <QtGui/qregion.h> +#include "qwasmwindowstack.h" + #include <qpa/qplatformwindow.h> -#include <QtOpenGL/qopengltextureblitter.h> -#include <QtGui/qpalette.h> -#include <QtGui/qpainter.h> +#include <QMap> QT_BEGIN_NAMESPACE class QWasmWindow; class QWasmScreen; -class QOpenGLContext; -class QOpenGLTexture; -class QWasmCompositedWindow -{ -public: - QWasmCompositedWindow(); - - QWasmWindow *window; - QWasmWindow *parentWindow; - QRegion damage; - bool flushPending; - bool visible; - QList<QWasmWindow *> childWindows; -}; +enum class QWasmWindowTreeNodeChangeType; -class QWasmCompositor : public QObject +class QWasmCompositor final : public QObject { Q_OBJECT public: QWasmCompositor(QWasmScreen *screen); - ~QWasmCompositor(); - void destroy(); - - enum QWasmSubControl { - SC_None = 0x00000000, - SC_TitleBarSysMenu = 0x00000001, - SC_TitleBarMinButton = 0x00000002, - SC_TitleBarMaxButton = 0x00000004, - SC_TitleBarCloseButton = 0x00000008, - SC_TitleBarNormalButton = 0x00000010, - SC_TitleBarLabel = 0x00000100 - }; - Q_DECLARE_FLAGS(SubControls, QWasmSubControl) - - enum QWasmStateFlag { - State_None = 0x00000000, - State_Enabled = 0x00000001, - State_Raised = 0x00000002, - State_Sunken = 0x00000004 - }; - Q_DECLARE_FLAGS(StateFlags, QWasmStateFlag) - - struct QWasmTitleBarOptions { - QRect rect; - Qt::WindowFlags flags; - int state; - QPalette palette; - QString titleBarOptionsString; - QWasmCompositor::SubControls subControls; - }; - - struct QWasmFrameOptions { - QRect rect; - int lineWidth; - QPalette palette; - }; - - void setEnabled(bool enabled); - - void addWindow(QWasmWindow *window, QWasmWindow *parentWindow = nullptr); - void removeWindow(QWasmWindow *window); + ~QWasmCompositor() final; void setVisible(QWasmWindow *window, bool visible); - void raise(QWasmWindow *window); - void lower(QWasmWindow *window); - void setParent(QWasmWindow *window, QWasmWindow *parent); - void flush(QWasmWindow *surface, const QRegion ®ion); + void onScreenDeleting(); - int windowCount() const; + QWasmScreen *screen(); + void setEnabled(bool enabled); - void redrawWindowContent(); - void requestRedraw(); + static bool releaseRequestUpdateHold(); - QWindow *windowAt(QPoint globalPoint, int padding = 0) const; - QWindow *keyWindow() const; + void requestUpdate(); + enum UpdateRequestDeliveryType { ExposeEventDelivery, UpdateRequestDelivery }; + void requestUpdateWindow(QWasmWindow *window, UpdateRequestDeliveryType updateType = ExposeEventDelivery); - bool event(QEvent *event); + void handleBackingStoreFlush(QWindow *window); + void onWindowTreeChanged(QWasmWindowTreeNodeChangeType changeType, QWasmWindow *window); - static QWasmTitleBarOptions makeTitleBarOptions(const QWasmWindow *window); - static QRect titlebarRect(QWasmTitleBarOptions tb, QWasmCompositor::SubControls subcontrol); +private: + void frame(const QList<QWasmWindow *> &windows); - QWasmScreen *screen(); - QOpenGLContext *context(); + void deregisterEventHandlers(); -private slots: - void frame(); + void deliverUpdateRequests(); + void deliverUpdateRequest(QWasmWindow *window, UpdateRequestDeliveryType updateType); -private: - void notifyTopWindowChanged(QWasmWindow *window); - void drawWindow(QOpenGLTextureBlitter *blitter, QWasmScreen *screen, QWasmWindow *window); - void drawWindowContent(QOpenGLTextureBlitter *blitter, QWasmScreen *screen, QWasmWindow *window); - void blit(QOpenGLTextureBlitter *blitter, QWasmScreen *screen, const QOpenGLTexture *texture, QRect targetGeometry); - - void drawWindowDecorations(QOpenGLTextureBlitter *blitter, QWasmScreen *screen, QWasmWindow *window); - void drwPanelButton(); - - QScopedPointer<QOpenGLContext> m_context; - QScopedPointer<QOpenGLTextureBlitter> m_blitter; - - QHash<QWasmWindow *, QWasmCompositedWindow> m_compositedWindows; - QList<QWasmWindow *> m_windowStack; - QRegion m_globalDamage; // damage caused by expose, window close, etc. - bool m_needComposit; - bool m_inFlush; - bool m_inResize; - bool m_isEnabled; - QSize m_targetSize; - qreal m_targetDevicePixelRatio; - - static QPalette makeWindowPalette(); - - void drawFrameWindow(QWasmFrameOptions options, QPainter *painter); - void drawTitlebarWindow(QWasmTitleBarOptions options, QPainter *painter); - void drawShadePanel(QWasmTitleBarOptions options, QPainter *painter); - void drawItemPixmap(QPainter *painter, const QRect &rect, - int alignment, const QPixmap &pixmap) const; + bool m_isEnabled = true; + QMap<QWasmWindow *, UpdateRequestDeliveryType> m_requestUpdateWindows; + int m_requestAnimationFrameId = -1; + bool m_inDeliverUpdateRequest = false; + static bool m_requestUpdateHoldEnabled; }; -Q_DECLARE_OPERATORS_FOR_FLAGS(QWasmCompositor::SubControls) QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmcssstyle.cpp b/src/plugins/platforms/wasm/qwasmcssstyle.cpp new file mode 100644 index 0000000000..e0e1a99f48 --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmcssstyle.cpp @@ -0,0 +1,248 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "qwasmcssstyle.h" + +#include "qwasmbase64iconstore.h" + +#include <QtCore/qstring.h> +#include <QtCore/qfile.h> + +QT_BEGIN_NAMESPACE + +namespace { +const char *Style = R"css( +.qt-screen { + --border-width: 4px; + --resize-outline-width: 8px; + --resize-outline-half-width: var(--resize-outline-width) / 2; + + position: relative; + border: none; + caret-color: transparent; + cursor: default; + width: 100%; + height: 100%; + overflow: hidden; +} + +.qt-screen div { + touch-action: none; +} + +.qt-window { + position: absolute; + background-color: lightgray; +} + +.qt-window-contents { + overflow: hidden; + position: relative; +} + +.qt-window.transparent-for-input { + pointer-events: none; +} + +.qt-window.has-shadow { + box-shadow: rgb(0 0 0 / 20%) 0px 10px 16px 0px, rgb(0 0 0 / 19%) 0px 6px 20px 0px; +} + +.qt-window.has-border { + border: var(--border-width) solid lightgray; + caret-color: transparent; +} + +.qt-window.frameless { + background-color: transparent; +} + +.resize-outline { + position: absolute; + display: none; +} + +.qt-window.no-resize > .resize-outline { display: none; } + +.qt-window.has-border:not(.maximized):not(.no-resize) .resize-outline { + display: block; +} + +.resize-outline.nw { + left: calc(-1 * var(--resize-outline-half-width) - var(--border-width)); + top: calc(-1 * var(--resize-outline-half-width) - var(--border-width)); + width: var(--resize-outline-width); + height: var(--resize-outline-width); + cursor: nwse-resize; +} + +.resize-outline.n { + left: var(--resize-outline-half-width); + top: calc(-1 * var(--resize-outline-half-width) - var(--border-width)); + height: var(--resize-outline-width); + width: calc(100% + 2 * var(--border-width) - var(--resize-outline-width)); + cursor: ns-resize; +} + +.resize-outline.ne { + left: calc(100% + var(--border-width) - var(--resize-outline-half-width)); + top: calc(-1 * var(--resize-outline-half-width) - var(--border-width)); + width: var(--resize-outline-width); + height: var(--resize-outline-width); + cursor: nesw-resize; +} + +.resize-outline.w { + left: calc(-1 * var(--resize-outline-half-width) - var(--border-width)); + top: 0; + height: calc(100% + 2 * var(--border-width) - var(--resize-outline-width)); + width: var(--resize-outline-width); + cursor: ew-resize; +} + +.resize-outline.e { + left: calc(100% + var(--border-width) - var(--resize-outline-half-width)); + top: 0; + height: calc(100% + 2 * var(--border-width) - var(--resize-outline-width)); + width: var(--resize-outline-width); + cursor: ew-resize; +} + +.resize-outline.sw { + left: calc(-1 * var(--resize-outline-half-width) - var(--border-width)); + top: calc(100% + var(--border-width) - var(--resize-outline-half-width)); + width: var(--resize-outline-width); + height: var(--resize-outline-width); + cursor: nesw-resize; +} + +.resize-outline.s { + left: var(--resize-outline-half-width); + top: calc(100% + var(--border-width) - var(--resize-outline-half-width)); + height: var(--resize-outline-width); + width: calc(100% + 2 * var(--border-width) - var(--resize-outline-width)); + cursor: ns-resize; +} + +.resize-outline.se { + left: calc(100% + var(--border-width) - var(--resize-outline-half-width)); + top: calc(100% + var(--border-width) - var(--resize-outline-half-width)); + width: var(--resize-outline-width); + height: var(--resize-outline-width); + cursor: nwse-resize; +} + +.title-bar { + display: none; + align-items: center; + overflow: hidden; + height: 18px; + padding-bottom: 4px; +} + +.qt-window.has-border > .title-bar { + display: flex; +} + +.title-bar .window-name { + display: none; + font-family: 'Lucida Grande'; + white-space: nowrap; + user-select: none; + overflow: hidden; +} + + +.qt-window.has-title .title-bar .window-name { + display: block; +} + +.title-bar .spacer { + flex-grow: 1 +} + +.qt-window.inactive .title-bar { + opacity: 0.35; +} + +.qt-window-canvas-container { + display: flex; + pointer-events: none; +} + +.title-bar div { + pointer-events: none; +} + +.qt-window-a11y-container { + position: absolute; + z-index: -1; +} + +.title-bar .image-button { + width: 18px; + height: 18px; + display: flex; + justify-content: center; + user-select: none; + align-items: center; +} + +.title-bar .image-button img { + width: 10px; + height: 10px; + user-select: none; + pointer-events: none; + -webkit-user-drag: none; + background-size: 10px 10px; +} + +.title-bar .action-button { + pointer-events: all; +} + +.qt-window.blocked div { + pointer-events: none; +} + +.title-bar .action-button img { + transition: filter 0.08s ease-out; +} + +.title-bar .action-button:hover img { + filter: invert(0.45); +} + +.title-bar .action-button:active img { + filter: invert(0.6); +} + +/* This will clip the content within 50% frame in 1x1 pixel area, preventing it + from being rendered on the page, but it should still be read by modern + screen readers */ +.hidden-visually-read-by-screen-reader { + visibility: visible; + clip: rect(1px, 1px, 1px, 1px); + clip-path: inset(50%); + height: 1px; + width: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; +} + +)css"; + +} // namespace + +emscripten::val QWasmCSSStyle::createStyleElement(emscripten::val parent) +{ + auto document = parent["ownerDocument"]; + auto screenStyle = document.call<emscripten::val>("createElement", emscripten::val("style")); + + screenStyle.set("textContent", std::string(Style)); + return screenStyle; +} + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmcssstyle.h b/src/plugins/platforms/wasm/qwasmcssstyle.h new file mode 100644 index 0000000000..fc4cc2d54c --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmcssstyle.h @@ -0,0 +1,18 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef QWASMCSSSTYLE_H +#define QWASMCSSSTYLE_H + +#include <QtCore/qglobal.h> + +#include <emscripten/val.h> + +QT_BEGIN_NAMESPACE + +namespace QWasmCSSStyle { +emscripten::val createStyleElement(emscripten::val parent); +} + +QT_END_NAMESPACE +#endif // QWASMINLINESTYLEREGISTRY_H diff --git a/src/plugins/platforms/wasm/qwasmcursor.cpp b/src/plugins/platforms/wasm/qwasmcursor.cpp index 61204517ce..c258befa77 100644 --- a/src/plugins/platforms/wasm/qwasmcursor.cpp +++ b/src/plugins/platforms/wasm/qwasmcursor.cpp @@ -1,141 +1,101 @@ -/**************************************************************************** -** -** 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$ -** -****************************************************************************/ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "qwasmcursor.h" #include "qwasmscreen.h" -#include "qwasmstring.h" +#include "qwasmwindow.h" +#include <QtCore/qbuffer.h> #include <QtCore/qdebug.h> +#include <QtCore/qstring.h> #include <QtGui/qwindow.h> #include <emscripten/emscripten.h> #include <emscripten/bind.h> +QT_BEGIN_NAMESPACE using namespace emscripten; -void QWasmCursor::changeCursor(QCursor *windowCursor, QWindow *window) -{ - if (!windowCursor || !window) - return; - QScreen *screen = window->screen(); - if (!screen) - return; - - // Bitmap and custom cursors are not implemented (will fall back to "auto") - if (windowCursor->shape() == Qt::BitmapCursor || windowCursor->shape() >= Qt::CustomCursor) - qWarning() << "QWasmCursor: bitmap and custom cursors are not supported"; - - QByteArray htmlCursorName = cursorShapeToHtml(windowCursor->shape()); - - if (htmlCursorName.isEmpty()) - htmlCursorName = "auto"; - - // Set cursor on the canvas - val canvas = QWasmScreen::get(screen)->canvas(); - val canvasStyle = canvas["style"]; - canvasStyle.set("cursor", val(htmlCursorName.constData())); -} - -QByteArray QWasmCursor::cursorShapeToHtml(Qt::CursorShape shape) +namespace { +QByteArray cursorToCss(const QCursor *cursor) { - QByteArray cursorName; - + auto shape = cursor->shape(); switch (shape) { case Qt::ArrowCursor: - cursorName = "default"; - break; + return "default"; case Qt::UpArrowCursor: - cursorName = "n-resize"; - break; + return "n-resize"; case Qt::CrossCursor: - cursorName = "crosshair"; - break; + return "crosshair"; case Qt::WaitCursor: - cursorName = "wait"; - break; + return "wait"; case Qt::IBeamCursor: - cursorName = "text"; - break; + return "text"; case Qt::SizeVerCursor: - cursorName = "ns-resize"; - break; + return "ns-resize"; case Qt::SizeHorCursor: - cursorName = "ew-resize"; - break; + return "ew-resize"; case Qt::SizeBDiagCursor: - cursorName = "nesw-resize"; - break; + return "nesw-resize"; case Qt::SizeFDiagCursor: - cursorName = "nwse-resize"; - break; + return "nwse-resize"; case Qt::SizeAllCursor: - cursorName = "move"; - break; + return "move"; case Qt::BlankCursor: - cursorName = "none"; - break; + return "none"; case Qt::SplitVCursor: - cursorName = "row-resize"; - break; + return "row-resize"; case Qt::SplitHCursor: - cursorName = "col-resize"; - break; + return "col-resize"; case Qt::PointingHandCursor: - cursorName = "pointer"; - break; + return "pointer"; case Qt::ForbiddenCursor: - cursorName = "not-allowed"; - break; + return "not-allowed"; case Qt::WhatsThisCursor: - cursorName = "help"; - break; + return "help"; case Qt::BusyCursor: - cursorName = "progress"; - break; + return "progress"; case Qt::OpenHandCursor: - cursorName = "grab"; - break; + return "grab"; case Qt::ClosedHandCursor: - cursorName = "grabbing"; - break; + return "grabbing"; case Qt::DragCopyCursor: - cursorName = "copy"; - break; + return "copy"; case Qt::DragMoveCursor: - cursorName = "default"; - break; + return "default"; case Qt::DragLinkCursor: - cursorName = "alias"; - break; + return "alias"; + case Qt::BitmapCursor: { + auto pixmap = cursor->pixmap(); + QByteArray cursorAsPng; + QBuffer buffer(&cursorAsPng); + buffer.open(QBuffer::WriteOnly); + pixmap.save(&buffer, "PNG"); + buffer.close(); + auto cursorAsBase64 = cursorAsPng.toBase64(); + auto hotSpot = cursor->hotSpot(); + auto encodedCursor = + QString("url(data:image/png;base64,%1) %2 %3, auto") + .arg(QString::fromUtf8(cursorAsBase64), + QString::number(hotSpot.x()), + QString::number(hotSpot.y())); + return encodedCursor.toUtf8(); + } default: - break; + static_assert(Qt::CustomCursor == 25, + "New cursor type added, handle it"); + qWarning() << "QWasmCursor: " << shape << " unsupported"; + return "default"; } +} +} // namespace - return cursorName; +void QWasmCursor::changeCursor(QCursor *windowCursor, QWindow *window) +{ + if (!window) + return; + if (QWasmWindow *wasmWindow = static_cast<QWasmWindow *>(window->handle())) + wasmWindow->setWindowCursor(windowCursor ? cursorToCss(windowCursor) : "default"); } + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmcursor.h b/src/plugins/platforms/wasm/qwasmcursor.h index 516e07aa31..6873602caf 100644 --- a/src/plugins/platforms/wasm/qwasmcursor.h +++ b/src/plugins/platforms/wasm/qwasmcursor.h @@ -1,43 +1,19 @@ -/**************************************************************************** -** -** 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$ -** -****************************************************************************/ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #ifndef QWASMCURSOR_H #define QWASMCURSOR_H #include <qpa/qplatformcursor.h> +QT_BEGIN_NAMESPACE + class QWasmCursor : public QPlatformCursor { public: void changeCursor(QCursor *windowCursor, QWindow *window) override; - - QByteArray cursorShapeToHtml(Qt::CursorShape shape); }; +QT_END_NAMESPACE + #endif diff --git a/src/plugins/platforms/wasm/qwasmdom.cpp b/src/plugins/platforms/wasm/qwasmdom.cpp new file mode 100644 index 0000000000..96790ca71f --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmdom.cpp @@ -0,0 +1,310 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "qwasmdom.h" + +#include <QtCore/qdir.h> +#include <QtCore/qfile.h> +#include <QtCore/qpoint.h> +#include <QtCore/qrect.h> +#include <QtGui/qimage.h> +#include <private/qstdweb_p.h> +#include <QtCore/qurl.h> + +#include <utility> +#include <emscripten/wire.h> + +QT_BEGIN_NAMESPACE + +namespace dom { +namespace { +std::string dropActionToDropEffect(Qt::DropAction action) +{ + switch (action) { + case Qt::DropAction::CopyAction: + return "copy"; + case Qt::DropAction::IgnoreAction: + return "none"; + case Qt::DropAction::LinkAction: + return "link"; + case Qt::DropAction::MoveAction: + case Qt::DropAction::TargetMoveAction: + return "move"; + case Qt::DropAction::ActionMask: + Q_ASSERT(false); + return ""; + } +} +} // namespace + +DataTransfer::DataTransfer(emscripten::val webDataTransfer) + : webDataTransfer(webDataTransfer) { +} + +DataTransfer::~DataTransfer() = default; + +DataTransfer::DataTransfer(const DataTransfer &other) = default; + +DataTransfer::DataTransfer(DataTransfer &&other) = default; + +DataTransfer &DataTransfer::operator=(const DataTransfer &other) = default; + +DataTransfer &DataTransfer::operator=(DataTransfer &&other) = default; + +void DataTransfer::setDragImage(emscripten::val element, const QPoint &hotspot) +{ + webDataTransfer.call<void>("setDragImage", element, emscripten::val(hotspot.x()), + emscripten::val(hotspot.y())); +} + +void DataTransfer::setData(std::string format, std::string data) +{ + webDataTransfer.call<void>("setData", emscripten::val(std::move(format)), + emscripten::val(std::move(data))); +} + +void DataTransfer::setDropAction(Qt::DropAction action) +{ + webDataTransfer.set("dropEffect", emscripten::val(dropActionToDropEffect(action))); +} + +void DataTransfer::setDataFromMimeData(const QMimeData &mimeData) +{ + for (const auto &format : mimeData.formats()) { + auto data = mimeData.data(format); + + auto encoded = format.startsWith("text/") + ? QString::fromLocal8Bit(data).toStdString() + : "QB64" + QString::fromLocal8Bit(data.toBase64()).toStdString(); + + setData(format.toStdString(), std::move(encoded)); + } +} + +// Converts a DataTransfer instance to a QMimeData instance. Invokes the +// given callback when the conversion is complete. The callback takes ownership +// of the QMimeData. +void DataTransfer::toMimeDataWithFile(std::function<void(QMimeData *)> callback) +{ + enum class ItemKind { + File, + String, + }; + + class MimeContext { + + public: + MimeContext(int itemCount, std::function<void(QMimeData *)> callback) + :m_remainingItemCount(itemCount), m_callback(callback) + { + + } + + void deref() { + if (--m_remainingItemCount > 0) + return; + + QList<QUrl> allUrls; + allUrls.append(mimeData->urls()); + allUrls.append(fileUrls); + mimeData->setUrls(allUrls); + + m_callback(mimeData); + + // Delete files; we expect that the user callback reads/copies + // file content before returning. + // Fixme: tie file lifetime to lifetime of the QMimeData? + for (QUrl fileUrl: fileUrls) + QFile(fileUrl.toLocalFile()).remove(); + + delete this; + } + + QMimeData *mimeData = new QMimeData(); + QList<QUrl> fileUrls; + + private: + int m_remainingItemCount; + std::function<void(QMimeData *)> m_callback; + }; + + const auto items = webDataTransfer["items"]; + const int itemCount = items["length"].as<int>(); + const int fileCount = webDataTransfer["files"]["length"].as<int>(); + MimeContext *mimeContext = new MimeContext(itemCount, callback); + + for (int i = 0; i < itemCount; ++i) { + const auto item = items[i]; + const auto itemKind = + item["kind"].as<std::string>() == "string" ? ItemKind::String : ItemKind::File; + const auto itemMimeType = QString::fromStdString(item["type"].as<std::string>()); + + switch (itemKind) { + case ItemKind::File: { + qstdweb::File webfile(item.call<emscripten::val>("getAsFile")); + + if (webfile.size() > 1e+9) { // limit file size to 1 GB + qWarning() << "File is too large (> 1GB) and will be skipped. File size is" << webfile.size(); + mimeContext->deref(); + continue; + } + + QString mimeFormat = QString::fromStdString(webfile.type()); + QString fileName = QString::fromStdString(webfile.name()); + + // there's a file, now read it + QByteArray fileContent(webfile.size(), Qt::Uninitialized); + webfile.stream(fileContent.data(), [=]() { + + // If we get a single file, and that file is an image, then + // try to decode the image data. This handles the case where + // image data (i.e. not an image file) is pasted. The browsers + // will then create a fake "image.png" file which has the image + // data. As a side effect Qt will also decode the image for + // single-image-file drops, since there is no way to differentiate + // the fake "image.png" from a real one. + if (fileCount == 1 && mimeFormat.contains("image/")) { + QImage image; + if (image.loadFromData(fileContent)) + mimeContext->mimeData->setImageData(image); + } + + QDir qtTmpDir("/qt/tmp/"); // "tmp": indicate that these files won't stay around + qtTmpDir.mkpath(qtTmpDir.path()); + + QUrl fileUrl = QUrl::fromLocalFile(qtTmpDir.filePath(QString::fromStdString(webfile.name()))); + mimeContext->fileUrls.append(fileUrl); + + QFile file(fileUrl.toLocalFile()); + if (!file.open(QFile::WriteOnly)) { + qWarning() << "File was not opened"; + mimeContext->deref(); + return; + } + if (file.write(fileContent) < 0) { + qWarning() << "Write failed"; + file.close(); + } + mimeContext->deref(); + }); + break; + } + case ItemKind::String: + if (itemMimeType.contains("STRING", Qt::CaseSensitive) + || itemMimeType.contains("TEXT", Qt::CaseSensitive)) { + mimeContext->deref(); + break; + } + QString a; + QString data = QString::fromEcmaString(webDataTransfer.call<emscripten::val>( + "getData", emscripten::val(itemMimeType.toStdString()))); + + if (!data.isEmpty()) { + if (itemMimeType == "text/html") + mimeContext->mimeData->setHtml(data); + else if (itemMimeType.isEmpty() || itemMimeType == "text/plain") + mimeContext->mimeData->setText(data); // the type can be empty + else if (itemMimeType.isEmpty() || itemMimeType == "text/uri-list") { + QList<QUrl> urls; + urls.append(data); + mimeContext->mimeData->setUrls(urls); + } else { + // TODO improve encoding + if (data.startsWith("QB64")) { + data.remove(0, 4); + mimeContext->mimeData->setData(itemMimeType, + QByteArray::fromBase64(QByteArray::fromStdString( + data.toStdString()))); + } else { + mimeContext->mimeData->setData(itemMimeType, data.toLocal8Bit()); + } + } + } + mimeContext->deref(); + break; + } + } // for items +} + +QMimeData *DataTransfer::toMimeDataPreview() +{ + auto data = new QMimeData(); + + QList<QUrl> uriList; + for (int i = 0; i < webDataTransfer["items"]["length"].as<int>(); ++i) { + const auto item = webDataTransfer["items"][i]; + if (item["kind"].as<std::string>() == "file") { + uriList.append(QUrl("blob://placeholder")); + } else { + const auto itemMimeType = QString::fromStdString(item["type"].as<std::string>()); + data->setData(itemMimeType, QByteArray()); + } + } + data->setUrls(uriList); + return data; +} + +void syncCSSClassWith(emscripten::val element, std::string cssClassName, bool flag) +{ + if (flag) { + element["classList"].call<void>("add", emscripten::val(std::move(cssClassName))); + return; + } + + element["classList"].call<void>("remove", emscripten::val(std::move(cssClassName))); +} + +QPointF mapPoint(emscripten::val source, emscripten::val target, const QPointF &point) +{ + const auto sourceBoundingRect = + QRectF::fromDOMRect(source.call<emscripten::val>("getBoundingClientRect")); + const auto targetBoundingRect = + QRectF::fromDOMRect(target.call<emscripten::val>("getBoundingClientRect")); + + const auto offset = sourceBoundingRect.topLeft() - targetBoundingRect.topLeft(); + return point + offset; +} + +void drawImageToWebImageDataArray(const QImage &sourceImage, emscripten::val destinationImageData, + const QRect &sourceRect) +{ + Q_ASSERT_X(destinationImageData["constructor"]["name"].as<std::string>() == "ImageData", + Q_FUNC_INFO, "The destination should be an ImageData instance"); + + constexpr int BytesPerColor = 4; + if (sourceRect.width() == sourceImage.width()) { + // Copy a contiguous chunk of memory + // ............... + // OOOOOOOOOOOOOOO + // OOOOOOOOOOOOOOO -> image data + // OOOOOOOOOOOOOOO + // ............... + auto imageMemory = emscripten::typed_memory_view(sourceRect.width() * sourceRect.height() + * BytesPerColor, + sourceImage.constScanLine(sourceRect.y())); + destinationImageData["data"].call<void>( + "set", imageMemory, sourceRect.y() * sourceImage.width() * BytesPerColor); + } else { + // Go through the scanlines manually to set the individual lines in bulk. This is + // marginally less performant than the above. + // ............... + // ...OOOOOOOOO... r = 0 -> image data + // ...OOOOOOOOO... r = 1 -> image data + // ...OOOOOOOOO... r = 2 -> image data + // ............... + for (int row = 0; row < sourceRect.height(); ++row) { + auto scanlineMemory = + emscripten::typed_memory_view(sourceRect.width() * BytesPerColor, + sourceImage.constScanLine(row + sourceRect.y()) + + BytesPerColor * sourceRect.x()); + destinationImageData["data"].call<void>("set", scanlineMemory, + (sourceRect.y() + row) * sourceImage.width() + * BytesPerColor + + sourceRect.x() * BytesPerColor); + } + } +} + +} // namespace dom + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmdom.h b/src/plugins/platforms/wasm/qwasmdom.h new file mode 100644 index 0000000000..0a520815a3 --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmdom.h @@ -0,0 +1,63 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef QWASMDOM_H +#define QWASMDOM_H + +#include <QtCore/qtconfigmacros.h> +#include <QtCore/QPointF> +#include <private/qstdweb_p.h> +#include <QtCore/qnamespace.h> + +#include <emscripten/val.h> + +#include <functional> +#include <memory> +#include <string> + +#include <QMimeData> +QT_BEGIN_NAMESPACE + +namespace qstdweb { + struct CancellationFlag; +} + + +class QPoint; +class QRect; + +namespace dom { +struct DataTransfer +{ + explicit DataTransfer(emscripten::val webDataTransfer); + ~DataTransfer(); + DataTransfer(const DataTransfer &other); + DataTransfer(DataTransfer &&other); + DataTransfer &operator=(const DataTransfer &other); + DataTransfer &operator=(DataTransfer &&other); + + void toMimeDataWithFile(std::function<void(QMimeData *)> callback); + QMimeData *toMimeDataPreview(); + void setDragImage(emscripten::val element, const QPoint &hotspot); + void setData(std::string format, std::string data); + void setDropAction(Qt::DropAction dropAction); + void setDataFromMimeData(const QMimeData &mimeData); + + emscripten::val webDataTransfer; +}; + +inline emscripten::val document() +{ + return emscripten::val::global("document"); +} + +void syncCSSClassWith(emscripten::val element, std::string cssClassName, bool flag); + +QPointF mapPoint(emscripten::val source, emscripten::val target, const QPointF &point); + +void drawImageToWebImageDataArray(const QImage &source, emscripten::val destinationImageData, + const QRect &sourceRect); +} // namespace dom + +QT_END_NAMESPACE +#endif // QWASMDOM_H diff --git a/src/plugins/platforms/wasm/qwasmdrag.cpp b/src/plugins/platforms/wasm/qwasmdrag.cpp new file mode 100644 index 0000000000..d07a46618f --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmdrag.cpp @@ -0,0 +1,291 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "qwasmdrag.h" + +#include "qwasmbase64iconstore.h" +#include "qwasmdom.h" +#include "qwasmevent.h" +#include "qwasmintegration.h" + +#include <qpa/qwindowsysteminterface.h> + +#include <QtCore/private/qstdweb_p.h> +#include <QtCore/qeventloop.h> +#include <QtCore/qmimedata.h> +#include <QtCore/qtimer.h> +#include <QFile> + +#include <functional> +#include <string> +#include <utility> + +QT_BEGIN_NAMESPACE + +namespace { + +QWindow *windowForDrag(QDrag *drag) +{ + QWindow *window = qobject_cast<QWindow *>(drag->source()); + if (window) + return window; + if (drag->source()->metaObject()->indexOfMethod("_q_closestWindowHandle()") == -1) + return nullptr; + + QMetaObject::invokeMethod(drag->source(), "_q_closestWindowHandle", + Q_RETURN_ARG(QWindow *, window)); + return window; +} + +} // namespace + +struct QWasmDrag::DragState +{ + class DragImage + { + public: + DragImage(const QPixmap &pixmap, const QMimeData *mimeData, QWindow *window); + ~DragImage(); + + emscripten::val htmlElement(); + + private: + emscripten::val generateDragImage(const QPixmap &pixmap, const QMimeData *mimeData); + emscripten::val generateDragImageFromText(const QMimeData *mimeData); + emscripten::val generateDefaultDragImage(); + emscripten::val generateDragImageFromPixmap(const QPixmap &pixmap); + + emscripten::val m_imageDomElement; + emscripten::val m_temporaryImageElementParent; + }; + + DragState(QDrag *drag, QWindow *window, std::function<void()> quitEventLoopClosure); + ~DragState(); + DragState(const QWasmDrag &other) = delete; + DragState(QWasmDrag &&other) = delete; + DragState &operator=(const QWasmDrag &other) = delete; + DragState &operator=(QWasmDrag &&other) = delete; + + QDrag *drag; + QWindow *window; + std::function<void()> quitEventLoopClosure; + std::unique_ptr<DragImage> dragImage; + Qt::DropAction dropAction = Qt::DropAction::IgnoreAction; +}; + +QWasmDrag::QWasmDrag() = default; + +QWasmDrag::~QWasmDrag() = default; + +QWasmDrag *QWasmDrag::instance() +{ + return static_cast<QWasmDrag *>(QWasmIntegration::get()->drag()); +} + +Qt::DropAction QWasmDrag::drag(QDrag *drag) +{ + Q_ASSERT_X(!m_dragState, Q_FUNC_INFO, "Drag already in progress"); + + QWindow *window = windowForDrag(drag); + if (!window) + return Qt::IgnoreAction; + + Qt::DropAction dragResult = Qt::IgnoreAction; + if (qstdweb::haveJspi()) { + QEventLoop loop; + m_dragState = std::make_unique<DragState>(drag, window, [&loop]() { loop.quit(); }); + loop.exec(); + dragResult = m_dragState->dropAction; + m_dragState.reset(); + } + + if (dragResult == Qt::IgnoreAction) + dragResult = QBasicDrag::drag(drag); + + return dragResult; +} + +void QWasmDrag::onNativeDragStarted(DragEvent *event) +{ + Q_ASSERT_X(event->type == EventType::DragStart, Q_FUNC_INFO, + "The event is not a DragStart event"); + // It is possible for a drag start event to arrive from another window. + if (!m_dragState || m_dragState->window != event->targetWindow) { + event->cancelDragStart(); + return; + } + + m_dragState->dragImage = std::make_unique<DragState::DragImage>( + m_dragState->drag->pixmap(), m_dragState->drag->mimeData(), event->targetWindow); + event->dataTransfer.setDragImage(m_dragState->dragImage->htmlElement(), + m_dragState->drag->hotSpot()); + event->dataTransfer.setDataFromMimeData(*m_dragState->drag->mimeData()); +} + +void QWasmDrag::onNativeDragOver(DragEvent *event) +{ + auto mimeDataPreview = event->dataTransfer.toMimeDataPreview(); + + const Qt::DropActions actions = m_dragState + ? m_dragState->drag->supportedActions() + : (Qt::DropAction::CopyAction | Qt::DropAction::MoveAction + | Qt::DropAction::LinkAction); + + const auto dragResponse = QWindowSystemInterface::handleDrag( + event->targetWindow, &*mimeDataPreview, event->pointInPage.toPoint(), actions, + event->mouseButton, event->modifiers); + event->acceptDragOver(); + if (dragResponse.isAccepted()) { + event->dataTransfer.setDropAction(dragResponse.acceptedAction()); + } else { + event->dataTransfer.setDropAction(Qt::DropAction::IgnoreAction); + } +} + +void QWasmDrag::onNativeDrop(DragEvent *event) +{ + QWasmWindow *wasmWindow = QWasmWindow::fromWindow(event->targetWindow); + + const auto screenElementPos = dom::mapPoint( + event->target(), wasmWindow->platformScreen()->element(), event->localPoint); + const auto screenPos = + wasmWindow->platformScreen()->mapFromLocal(screenElementPos); + const QPoint targetWindowPos = event->targetWindow->mapFromGlobal(screenPos).toPoint(); + + const Qt::DropActions actions = m_dragState + ? m_dragState->drag->supportedActions() + : (Qt::DropAction::CopyAction | Qt::DropAction::MoveAction + | Qt::DropAction::LinkAction); + Qt::MouseButton mouseButton = event->mouseButton; + QFlags<Qt::KeyboardModifier> modifiers = event->modifiers; + + // Accept the native drop event: We are going to async read any dropped + // files, but the browser expects that accepted state is set before any + // async calls. + event->acceptDrop(); + + const auto dropCallback = [&m_dragState = m_dragState, wasmWindow, targetWindowPos, + actions, mouseButton, modifiers](QMimeData *mimeData) { + + auto dropResponse = std::make_shared<QPlatformDropQtResponse>(true, Qt::DropAction::CopyAction); + *dropResponse = QWindowSystemInterface::handleDrop(wasmWindow->window(), mimeData, + targetWindowPos, actions, + mouseButton, modifiers); + + if (dropResponse->isAccepted()) + m_dragState->dropAction = dropResponse->acceptedAction(); + + delete mimeData; + }; + + event->dataTransfer.toMimeDataWithFile(dropCallback); +} + +void QWasmDrag::onNativeDragFinished(DragEvent *event) +{ + m_dragState->dropAction = event->dropAction; + m_dragState->quitEventLoopClosure(); +} + +QWasmDrag::DragState::DragImage::DragImage(const QPixmap &pixmap, const QMimeData *mimeData, + QWindow *window) + : m_temporaryImageElementParent(QWasmWindow::fromWindow(window)->containerElement()) +{ + m_imageDomElement = generateDragImage(pixmap, mimeData); + + m_imageDomElement.set("className", "hidden-drag-image"); + m_temporaryImageElementParent.call<void>("appendChild", m_imageDomElement); +} + +QWasmDrag::DragState::DragImage::~DragImage() +{ + m_temporaryImageElementParent.call<void>("removeChild", m_imageDomElement); +} + +emscripten::val QWasmDrag::DragState::DragImage::generateDragImage(const QPixmap &pixmap, + const QMimeData *mimeData) +{ + if (!pixmap.isNull()) + return generateDragImageFromPixmap(pixmap); + if (mimeData->hasFormat("text/plain")) + return generateDragImageFromText(mimeData); + return generateDefaultDragImage(); +} + +emscripten::val +QWasmDrag::DragState::DragImage::generateDragImageFromText(const QMimeData *mimeData) +{ + emscripten::val dragImageElement = + emscripten::val::global("document") + .call<emscripten::val>("createElement", emscripten::val("span")); + + constexpr qsizetype MaxCharactersInDragImage = 100; + + const auto text = QString::fromUtf8(mimeData->data("text/plain")); + dragImageElement.set( + "innerText", + text.first(qMin(qsizetype(MaxCharactersInDragImage), text.length())).toStdString()); + return dragImageElement; +} + +emscripten::val QWasmDrag::DragState::DragImage::generateDefaultDragImage() +{ + emscripten::val dragImageElement = + emscripten::val::global("document") + .call<emscripten::val>("createElement", emscripten::val("div")); + + auto innerImgElement = emscripten::val::global("document") + .call<emscripten::val>("createElement", emscripten::val("img")); + innerImgElement.set("src", + "data:image/" + std::string("svg+xml") + ";base64," + + std::string(Base64IconStore::get()->getIcon( + Base64IconStore::IconType::QtLogo))); + + constexpr char DragImageSize[] = "50px"; + + dragImageElement["style"].set("width", DragImageSize); + innerImgElement["style"].set("width", DragImageSize); + dragImageElement["style"].set("display", "flex"); + + dragImageElement.call<void>("appendChild", innerImgElement); + return dragImageElement; +} + +emscripten::val QWasmDrag::DragState::DragImage::generateDragImageFromPixmap(const QPixmap &pixmap) +{ + emscripten::val dragImageElement = + emscripten::val::global("document") + .call<emscripten::val>("createElement", emscripten::val("canvas")); + dragImageElement.set("width", pixmap.width()); + dragImageElement.set("height", pixmap.height()); + + dragImageElement["style"].set( + "width", std::to_string(pixmap.width() / pixmap.devicePixelRatio()) + "px"); + dragImageElement["style"].set( + "height", std::to_string(pixmap.height() / pixmap.devicePixelRatio()) + "px"); + + auto context2d = dragImageElement.call<emscripten::val>("getContext", emscripten::val("2d")); + auto imageData = context2d.call<emscripten::val>( + "createImageData", emscripten::val(pixmap.width()), emscripten::val(pixmap.height())); + + dom::drawImageToWebImageDataArray(pixmap.toImage().convertedTo(QImage::Format::Format_RGBA8888), + imageData, QRect(0, 0, pixmap.width(), pixmap.height())); + context2d.call<void>("putImageData", imageData, emscripten::val(0), emscripten::val(0)); + + return dragImageElement; +} + +emscripten::val QWasmDrag::DragState::DragImage::htmlElement() +{ + return m_imageDomElement; +} + +QWasmDrag::DragState::DragState(QDrag *drag, QWindow *window, + std::function<void()> quitEventLoopClosure) + : drag(drag), window(window), quitEventLoopClosure(std::move(quitEventLoopClosure)) +{ +} + +QWasmDrag::DragState::~DragState() = default; + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmdrag.h b/src/plugins/platforms/wasm/qwasmdrag.h new file mode 100644 index 0000000000..146a69ebe8 --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmdrag.h @@ -0,0 +1,47 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef QWINDOWSDRAG_H +#define QWINDOWSDRAG_H + +#include <private/qstdweb_p.h> +#include <private/qsimpledrag_p.h> + +#include <qpa/qplatformdrag.h> +#include <QtGui/qdrag.h> + +#include <memory> + +QT_BEGIN_NAMESPACE + +struct DragEvent; + +class QWasmDrag final : public QSimpleDrag +{ +public: + QWasmDrag(); + ~QWasmDrag() override; + QWasmDrag(const QWasmDrag &other) = delete; + QWasmDrag(QWasmDrag &&other) = delete; + QWasmDrag &operator=(const QWasmDrag &other) = delete; + QWasmDrag &operator=(QWasmDrag &&other) = delete; + + static QWasmDrag *instance(); + + void onNativeDragOver(DragEvent *event); + void onNativeDrop(DragEvent *event); + void onNativeDragStarted(DragEvent *event); + void onNativeDragFinished(DragEvent *event); + + // QPlatformDrag: + Qt::DropAction drag(QDrag *drag) final; + +private: + struct DragState; + + std::unique_ptr<DragState> m_dragState; +}; + +QT_END_NAMESPACE + +#endif // QWINDOWSDRAG_H diff --git a/src/plugins/platforms/wasm/qwasmevent.cpp b/src/plugins/platforms/wasm/qwasmevent.cpp new file mode 100644 index 0000000000..e418263655 --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmevent.cpp @@ -0,0 +1,338 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "qwasmevent.h" + +#include "qwasmkeytranslator.h" + +#include <QtCore/private/qmakearray_p.h> +#include <QtCore/private/qstringiterator_p.h> +#include <QtCore/qregularexpression.h> + +QT_BEGIN_NAMESPACE + +namespace { +constexpr std::string_view WebDeadKeyValue = "Dead"; + +bool isDeadKeyEvent(const char *key) +{ + return qstrncmp(key, WebDeadKeyValue.data(), WebDeadKeyValue.size()) == 0; +} + +Qt::Key getKeyFromCode(const std::string &code) +{ + if (auto mapping = QWasmKeyTranslator::mapWebKeyTextToQtKey(code.c_str())) + return *mapping; + + static QRegularExpression regex(QString(QStringLiteral(R"re((?:Key|Digit)(\w))re"))); + const auto codeQString = QString::fromStdString(code); + const auto match = regex.match(codeQString); + + if (!match.hasMatch()) + return Qt::Key_unknown; + + constexpr size_t CharacterIndex = 1; + return static_cast<Qt::Key>(match.capturedView(CharacterIndex).at(0).toLatin1()); +} + +Qt::Key webKeyToQtKey(const std::string &code, const std::string &key, bool isDeadKey, + QFlags<Qt::KeyboardModifier> modifiers) +{ + if (isDeadKey) { + auto mapped = getKeyFromCode(code); + switch (mapped) { + case Qt::Key_U: + return Qt::Key_Dead_Diaeresis; + case Qt::Key_E: + return Qt::Key_Dead_Acute; + case Qt::Key_I: + return Qt::Key_Dead_Circumflex; + case Qt::Key_N: + return Qt::Key_Dead_Tilde; + case Qt::Key_QuoteLeft: + return modifiers.testFlag(Qt::ShiftModifier) ? Qt::Key_Dead_Tilde : Qt::Key_Dead_Grave; + case Qt::Key_6: + return Qt::Key_Dead_Circumflex; + case Qt::Key_Apostrophe: + return modifiers.testFlag(Qt::ShiftModifier) ? Qt::Key_Dead_Diaeresis + : Qt::Key_Dead_Acute; + case Qt::Key_AsciiTilde: + return Qt::Key_Dead_Tilde; + default: + return Qt::Key_unknown; + } + } else if (auto mapping = QWasmKeyTranslator::mapWebKeyTextToQtKey(key.c_str())) { + return *mapping; + } + + // cast to unicode key + QString str = QString::fromUtf8(key.c_str()).toUpper(); + if (str.length() > 1) + return Qt::Key_unknown; + + QStringIterator i(str); + return static_cast<Qt::Key>(i.next(0)); +} +} // namespace + +namespace KeyboardModifier +{ +template <> +QFlags<Qt::KeyboardModifier> getForEvent<EmscriptenKeyboardEvent>( + const EmscriptenKeyboardEvent& event) +{ + return internal::Helper<EmscriptenKeyboardEvent>::getModifierForEvent(event) | + (event.location == DOM_KEY_LOCATION_NUMPAD ? Qt::KeypadModifier : Qt::NoModifier); +} +} // namespace KeyboardModifier + +Event::Event(EventType type, emscripten::val webEvent) + : webEvent(webEvent), type(type) +{ +} + +Event::~Event() = default; + +Event::Event(const Event &other) = default; + +Event::Event(Event &&other) = default; + +Event &Event::operator=(const Event &other) = default; + +Event &Event::operator=(Event &&other) = default; + +KeyEvent::KeyEvent(EventType type, emscripten::val event) : Event(type, event) +{ + const auto code = event["code"].as<std::string>(); + const auto webKey = event["key"].as<std::string>(); + deadKey = isDeadKeyEvent(webKey.c_str()); + autoRepeat = event["repeat"].as<bool>(); + modifiers = KeyboardModifier::getForEvent(event); + key = webKeyToQtKey(code, webKey, deadKey, modifiers); + + text = QString::fromUtf8(webKey); + if (text.size() > 1) + text.clear(); + + if (key == Qt::Key_Tab) + text = "\t"; +} + +KeyEvent::~KeyEvent() = default; + +KeyEvent::KeyEvent(const KeyEvent &other) = default; + +KeyEvent::KeyEvent(KeyEvent &&other) = default; + +KeyEvent &KeyEvent::operator=(const KeyEvent &other) = default; + +KeyEvent &KeyEvent::operator=(KeyEvent &&other) = default; + +std::optional<KeyEvent> KeyEvent::fromWebWithDeadKeyTranslation(emscripten::val event, + QWasmDeadKeySupport *deadKeySupport) +{ + const auto eventType = ([&event]() -> std::optional<EventType> { + const auto eventTypeString = event["type"].as<std::string>(); + + if (eventTypeString == "keydown") + return EventType::KeyDown; + else if (eventTypeString == "keyup") + return EventType::KeyUp; + return std::nullopt; + })(); + if (!eventType) + return std::nullopt; + + auto result = KeyEvent(*eventType, event); + deadKeySupport->applyDeadKeyTranslations(&result); + + return result; +} + +MouseEvent::MouseEvent(EventType type, emscripten::val event) : Event(type, event) +{ + mouseButton = MouseEvent::buttonFromWeb(event["button"].as<int>()); + mouseButtons = MouseEvent::buttonsFromWeb(event["buttons"].as<unsigned short>()); + // The current button state (event.buttons) may be out of sync for some PointerDown + // events where the "down" state is very brief, for example taps on Apple trackpads. + // Qt expects that the current button state is in sync with the event, so we sync + // it up here. + if (type == EventType::PointerDown) + mouseButtons |= mouseButton; + localPoint = QPointF(event["offsetX"].as<qreal>(), event["offsetY"].as<qreal>()); + pointInPage = QPointF(event["pageX"].as<qreal>(), event["pageY"].as<qreal>()); + pointInViewport = QPointF(event["clientX"].as<qreal>(), event["clientY"].as<qreal>()); + modifiers = KeyboardModifier::getForEvent(event); +} + +MouseEvent::~MouseEvent() = default; + +MouseEvent::MouseEvent(const MouseEvent &other) = default; + +MouseEvent::MouseEvent(MouseEvent &&other) = default; + +MouseEvent &MouseEvent::operator=(const MouseEvent &other) = default; + +MouseEvent &MouseEvent::operator=(MouseEvent &&other) = default; + +PointerEvent::PointerEvent(EventType type, emscripten::val event) : MouseEvent(type, event) +{ + pointerId = event["pointerId"].as<int>(); + pointerType = ([type = event["pointerType"].as<std::string>()]() { + if (type == "mouse") + return PointerType::Mouse; + if (type == "touch") + return PointerType::Touch; + if (type == "pen") + return PointerType::Pen; + return PointerType::Other; + })(); + width = event["width"].as<qreal>(); + height = event["height"].as<qreal>(); + pressure = event["pressure"].as<qreal>(); + tiltX = event["tiltX"].as<qreal>(); + tiltY = event["tiltY"].as<qreal>(); + tangentialPressure = event["tangentialPressure"].as<qreal>(); + twist = event["twist"].as<qreal>(); + isPrimary = event["isPrimary"].as<bool>(); +} + +PointerEvent::~PointerEvent() = default; + +PointerEvent::PointerEvent(const PointerEvent &other) = default; + +PointerEvent::PointerEvent(PointerEvent &&other) = default; + +PointerEvent &PointerEvent::operator=(const PointerEvent &other) = default; + +PointerEvent &PointerEvent::operator=(PointerEvent &&other) = default; + +std::optional<PointerEvent> PointerEvent::fromWeb(emscripten::val event) +{ + const auto eventType = ([&event]() -> std::optional<EventType> { + const auto eventTypeString = event["type"].as<std::string>(); + + if (eventTypeString == "pointermove") + return EventType::PointerMove; + else if (eventTypeString == "pointerup") + return EventType::PointerUp; + else if (eventTypeString == "pointerdown") + return EventType::PointerDown; + else if (eventTypeString == "pointerenter") + return EventType::PointerEnter; + else if (eventTypeString == "pointerleave") + return EventType::PointerLeave; + return std::nullopt; + })(); + if (!eventType) + return std::nullopt; + + return PointerEvent(*eventType, event); +} + +DragEvent::DragEvent(EventType type, emscripten::val event, QWindow *window) + : MouseEvent(type, event), dataTransfer(event["dataTransfer"]), targetWindow(window) +{ + dropAction = ([event]() { + const std::string effect = event["dataTransfer"]["dropEffect"].as<std::string>(); + + if (effect == "copy") + return Qt::CopyAction; + else if (effect == "move") + return Qt::MoveAction; + else if (effect == "link") + return Qt::LinkAction; + return Qt::IgnoreAction; + })(); +} + +DragEvent::~DragEvent() = default; + +DragEvent::DragEvent(const DragEvent &other) = default; + +DragEvent::DragEvent(DragEvent &&other) = default; + +DragEvent &DragEvent::operator=(const DragEvent &other) = default; + +DragEvent &DragEvent::operator=(DragEvent &&other) = default; + +std::optional<DragEvent> DragEvent::fromWeb(emscripten::val event, QWindow *targetWindow) +{ + const auto eventType = ([&event]() -> std::optional<EventType> { + const auto eventTypeString = event["type"].as<std::string>(); + + if (eventTypeString == "dragend") + return EventType::DragEnd; + if (eventTypeString == "dragover") + return EventType::DragOver; + if (eventTypeString == "dragstart") + return EventType::DragStart; + if (eventTypeString == "drop") + return EventType::Drop; + return std::nullopt; + })(); + if (!eventType) + return std::nullopt; + return DragEvent(*eventType, event, targetWindow); +} + +void DragEvent::cancelDragStart() +{ + Q_ASSERT_X(type == EventType::DragStart, Q_FUNC_INFO, "Only supported for DragStart"); + webEvent.call<void>("preventDefault"); +} + +void DragEvent::acceptDragOver() +{ + Q_ASSERT_X(type == EventType::DragOver, Q_FUNC_INFO, "Only supported for DragOver"); + webEvent.call<void>("preventDefault"); +} + +void DragEvent::acceptDrop() +{ + Q_ASSERT_X(type == EventType::Drop, Q_FUNC_INFO, "Only supported for Drop"); + webEvent.call<void>("preventDefault"); +} + +WheelEvent::WheelEvent(EventType type, emscripten::val event) : MouseEvent(type, event) +{ + deltaMode = ([event]() { + const int deltaMode = event["deltaMode"].as<int>(); + const auto jsWheelEventType = emscripten::val::global("WheelEvent"); + if (deltaMode == jsWheelEventType["DOM_DELTA_PIXEL"].as<int>()) + return DeltaMode::Pixel; + else if (deltaMode == jsWheelEventType["DOM_DELTA_LINE"].as<int>()) + return DeltaMode::Line; + return DeltaMode::Page; + })(); + + delta = QPointF(event["deltaX"].as<qreal>(), event["deltaY"].as<qreal>()); + + webkitDirectionInvertedFromDevice = event["webkitDirectionInvertedFromDevice"].as<bool>(); +} + +WheelEvent::~WheelEvent() = default; + +WheelEvent::WheelEvent(const WheelEvent &other) = default; + +WheelEvent::WheelEvent(WheelEvent &&other) = default; + +WheelEvent &WheelEvent::operator=(const WheelEvent &other) = default; + +WheelEvent &WheelEvent::operator=(WheelEvent &&other) = default; + +std::optional<WheelEvent> WheelEvent::fromWeb(emscripten::val event) +{ + const auto eventType = ([&event]() -> std::optional<EventType> { + const auto eventTypeString = event["type"].as<std::string>(); + + if (eventTypeString == "wheel") + return EventType::Wheel; + return std::nullopt; + })(); + if (!eventType) + return std::nullopt; + return WheelEvent(*eventType, event); +} + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmevent.h b/src/plugins/platforms/wasm/qwasmevent.h new file mode 100644 index 0000000000..bd0fb39f11 --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmevent.h @@ -0,0 +1,272 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef QWASMEVENT_H +#define QWASMEVENT_H + +#include "qwasmplatform.h" +#include "qwasmdom.h" + +#include <QtCore/qglobal.h> +#include <QtCore/qnamespace.h> +#include <QtGui/qevent.h> +#include <private/qstdweb_p.h> +#include <QPoint> + +#include <emscripten/html5.h> +#include <emscripten/val.h> + +QT_BEGIN_NAMESPACE + +class QWasmDeadKeySupport; +class QWindow; + +enum class EventType { + DragEnd, + DragOver, + DragStart, + Drop, + KeyDown, + KeyUp, + PointerDown, + PointerMove, + PointerUp, + PointerEnter, + PointerLeave, + PointerCancel, + Wheel, +}; + +enum class PointerType { + Mouse, + Touch, + Pen, + Other, +}; + +enum class WindowArea { + NonClient, + Client, +}; + +enum class DeltaMode { Pixel, Line, Page }; + +namespace KeyboardModifier { +namespace internal +{ + // Check for the existence of shiftKey, ctrlKey, altKey and metaKey in a type. + // Based on that, we can safely assume we are dealing with an emscripten event type. + template<typename T> + struct IsEmscriptenEvent + { + template<typename U, EM_BOOL U::*, EM_BOOL U::*, EM_BOOL U::*, EM_BOOL U::*> + struct SFINAE {}; + template<typename U> static char Test( + SFINAE<U, &U::shiftKey, &U::ctrlKey, &U::altKey, &U::metaKey>*); + template<typename U> static int Test(...); + static const bool value = sizeof(Test<T>(0)) == sizeof(char); + }; + + template<class T, typename Enable = void> + struct Helper; + + template<class T> + struct Helper<T, std::enable_if_t<IsEmscriptenEvent<T>::value>> + { + static QFlags<Qt::KeyboardModifier> getModifierForEvent(const T& event) { + QFlags<Qt::KeyboardModifier> keyModifier = Qt::NoModifier; + if (event.shiftKey) + keyModifier |= Qt::ShiftModifier; + if (event.ctrlKey) + keyModifier |= platform() == Platform::MacOS ? Qt::MetaModifier : Qt::ControlModifier; + if (event.altKey) + keyModifier |= Qt::AltModifier; + if (event.metaKey) + keyModifier |= platform() == Platform::MacOS ? Qt::ControlModifier : Qt::MetaModifier; + + return keyModifier; + } + }; + + template<> + struct Helper<emscripten::val> + { + static QFlags<Qt::KeyboardModifier> getModifierForEvent(const emscripten::val& event) { + QFlags<Qt::KeyboardModifier> keyModifier = Qt::NoModifier; + if (event["shiftKey"].as<bool>()) + keyModifier |= Qt::ShiftModifier; + if (event["ctrlKey"].as<bool>()) + keyModifier |= platform() == Platform::MacOS ? Qt::MetaModifier : Qt::ControlModifier; + if (event["altKey"].as<bool>()) + keyModifier |= Qt::AltModifier; + if (event["metaKey"].as<bool>()) + keyModifier |= platform() == Platform::MacOS ? Qt::ControlModifier : Qt::MetaModifier; + if (event["constructor"]["name"].as<std::string>() == "KeyboardEvent" && + event["location"].as<unsigned int>() == DOM_KEY_LOCATION_NUMPAD) { + keyModifier |= Qt::KeypadModifier; + } + + return keyModifier; + } + }; +} // namespace internal + +template <typename Event> +QFlags<Qt::KeyboardModifier> getForEvent(const Event& event) +{ + return internal::Helper<Event>::getModifierForEvent(event); +} + +template <> +QFlags<Qt::KeyboardModifier> getForEvent<EmscriptenKeyboardEvent>( + const EmscriptenKeyboardEvent& event); + +} // namespace KeyboardModifier + +struct Event +{ + Event(EventType type, emscripten::val webEvent); + ~Event(); + Event(const Event &other); + Event(Event &&other); + Event &operator=(const Event &other); + Event &operator=(Event &&other); + + emscripten::val webEvent; + EventType type; + emscripten::val target() const { return webEvent["target"]; } +}; + +struct KeyEvent : public Event +{ + static std::optional<KeyEvent> + fromWebWithDeadKeyTranslation(emscripten::val webEvent, QWasmDeadKeySupport *deadKeySupport); + + KeyEvent(EventType type, emscripten::val webEvent); + ~KeyEvent(); + KeyEvent(const KeyEvent &other); + KeyEvent(KeyEvent &&other); + KeyEvent &operator=(const KeyEvent &other); + KeyEvent &operator=(KeyEvent &&other); + + Qt::Key key; + QFlags<Qt::KeyboardModifier> modifiers; + bool deadKey; + QString text; + bool autoRepeat; +}; + +struct MouseEvent : public Event +{ + MouseEvent(EventType type, emscripten::val webEvent); + ~MouseEvent(); + MouseEvent(const MouseEvent &other); + MouseEvent(MouseEvent &&other); + MouseEvent &operator=(const MouseEvent &other); + MouseEvent &operator=(MouseEvent &&other); + + static constexpr Qt::MouseButton buttonFromWeb(int webButton) { + switch (webButton) { + case 0: + return Qt::LeftButton; + case 1: + return Qt::MiddleButton; + case 2: + return Qt::RightButton; + default: + return Qt::NoButton; + } + } + + static constexpr Qt::MouseButtons buttonsFromWeb(unsigned short webButtons) { + // Coincidentally, Qt and web bitfields match. + return Qt::MouseButtons::fromInt(webButtons); + } + + static constexpr QEvent::Type mouseEventTypeFromEventType( + EventType eventType, WindowArea windowArea) { + switch (eventType) { + case EventType::PointerDown : + return windowArea == WindowArea::Client ? + QEvent::MouseButtonPress : QEvent::NonClientAreaMouseButtonPress; + case EventType::PointerUp : + return windowArea == WindowArea::Client ? + QEvent::MouseButtonRelease : QEvent::NonClientAreaMouseButtonRelease; + case EventType::PointerMove : + return windowArea == WindowArea::Client ? + QEvent::MouseMove : QEvent::NonClientAreaMouseMove; + default: + return QEvent::None; + } + } + + QPointF localPoint; + QPointF pointInPage; + QPointF pointInViewport; + Qt::MouseButton mouseButton; + Qt::MouseButtons mouseButtons; + QFlags<Qt::KeyboardModifier> modifiers; +}; + +struct PointerEvent : public MouseEvent +{ + static std::optional<PointerEvent> fromWeb(emscripten::val webEvent); + + PointerEvent(EventType type, emscripten::val webEvent); + ~PointerEvent(); + PointerEvent(const PointerEvent &other); + PointerEvent(PointerEvent &&other); + PointerEvent &operator=(const PointerEvent &other); + PointerEvent &operator=(PointerEvent &&other); + + PointerType pointerType; + int pointerId; + qreal pressure; + qreal tiltX; + qreal tiltY; + qreal tangentialPressure; + qreal twist; + qreal width; + qreal height; + bool isPrimary; +}; + +struct DragEvent : public MouseEvent +{ + static std::optional<DragEvent> fromWeb(emscripten::val webEvent, QWindow *targetQWindow); + + DragEvent(EventType type, emscripten::val webEvent, QWindow *targetQWindow); + ~DragEvent(); + DragEvent(const DragEvent &other); + DragEvent(DragEvent &&other); + DragEvent &operator=(const DragEvent &other); + DragEvent &operator=(DragEvent &&other); + + void cancelDragStart(); + void acceptDragOver(); + void acceptDrop(); + + Qt::DropAction dropAction; + dom::DataTransfer dataTransfer; + QWindow *targetWindow; +}; + +struct WheelEvent : public MouseEvent +{ + static std::optional<WheelEvent> fromWeb(emscripten::val webEvent); + + WheelEvent(EventType type, emscripten::val webEvent); + ~WheelEvent(); + WheelEvent(const WheelEvent &other); + WheelEvent(WheelEvent &&other); + WheelEvent &operator=(const WheelEvent &other); + WheelEvent &operator=(WheelEvent &&other); + + DeltaMode deltaMode; + bool webkitDirectionInvertedFromDevice; + QPointF delta; +}; + +QT_END_NAMESPACE + +#endif // QWASMEVENT_H diff --git a/src/plugins/platforms/wasm/qwasmeventdispatcher.cpp b/src/plugins/platforms/wasm/qwasmeventdispatcher.cpp index 29dbf96883..1f2d3095d6 100644 --- a/src/plugins/platforms/wasm/qwasmeventdispatcher.cpp +++ b/src/plugins/platforms/wasm/qwasmeventdispatcher.cpp @@ -1,215 +1,35 @@ -/**************************************************************************** -** -** 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$ -** -****************************************************************************/ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "qwasmeventdispatcher.h" +#include "qwasmintegration.h" -#include <QtCore/qcoreapplication.h> #include <QtGui/qpa/qwindowsysteminterface.h> -#include <emscripten.h> +QT_BEGIN_NAMESPACE -#if QT_CONFIG(thread) -#if (__EMSCRIPTEN_major__ > 1 || __EMSCRIPTEN_minor__ > 38 || __EMSCRIPTEN_minor__ == 38 && __EMSCRIPTEN_tiny__ >= 22) -# define EMSCRIPTEN_HAS_ASYNC_RUN_IN_MAIN_RUNTIME_THREAD -#endif -#endif - -#ifdef EMSCRIPTEN_HAS_ASYNC_RUN_IN_MAIN_RUNTIME_THREAD -#include <emscripten/threading.h> -#endif - -class QWasmEventDispatcherPrivate : public QEventDispatcherUNIXPrivate -{ - -}; - -QWasmEventDispatcher *g_htmlEventDispatcher; - -QWasmEventDispatcher::QWasmEventDispatcher(QObject *parent) - : QUnixEventDispatcherQPA(parent) -{ - - g_htmlEventDispatcher = this; -} - -QWasmEventDispatcher::~QWasmEventDispatcher() -{ - g_htmlEventDispatcher = nullptr; -} - -bool QWasmEventDispatcher::registerRequestUpdateCallback(std::function<void(void)> callback) -{ - if (!g_htmlEventDispatcher || !g_htmlEventDispatcher->m_hasMainLoop) - return false; - - g_htmlEventDispatcher->m_requestUpdateCallbacks.append(callback); - emscripten_resume_main_loop(); - return true; -} - -void QWasmEventDispatcher::maintainTimers() +// Note: All event dispatcher functionality is implemented in QEventDispatcherWasm +// in QtCore, except for processPostedEvents() below which uses API from QtGui. +bool QWasmEventDispatcher::processPostedEvents() { - if (!g_htmlEventDispatcher || !g_htmlEventDispatcher->m_hasMainLoop) - return; - - g_htmlEventDispatcher->doMaintainTimers(); -} - -bool QWasmEventDispatcher::processEvents(QEventLoop::ProcessEventsFlags flags) -{ - // WaitForMoreEvents is not supported (except for in combination with EventLoopExec below), - // and we don't want the unix event dispatcher base class to attempt to wait either. - flags &= ~QEventLoop::WaitForMoreEvents; - - // Handle normal processEvents. - if (!(flags & QEventLoop::EventLoopExec)) - return QUnixEventDispatcherQPA::processEvents(flags); - - // Handle processEvents from QEventLoop::exec(): - // - // At this point the application has created its root objects on - // the stack and has called app.exec() which has called into this - // function via QEventLoop. - // - // The application now expects that exec() will not return until - // app exit time. However, the browser expects that we return - // control to it periodically, also after initial setup in main(). - - // EventLoopExec for nested event loops is not supported. - Q_ASSERT(!m_hasMainLoop); - m_hasMainLoop = true; - - // Call emscripten_set_main_loop_arg() with a callback which processes - // events. Also set simulateInfiniteLoop to true which makes emscripten - // return control to the browser without unwinding the C++ stack. - auto callback = [](void *eventDispatcher) { - QWasmEventDispatcher *that = static_cast<QWasmEventDispatcher *>(eventDispatcher); - - // Save and clear updateRequest callbacks so we can register new ones - auto requestUpdateCallbacksCopy = that->m_requestUpdateCallbacks; - that->m_requestUpdateCallbacks.clear(); - - // Repaint all windows - for (auto callback : qAsConst(requestUpdateCallbacksCopy)) - callback(); - - // Pause main loop if no updates were requested. Updates will be - // restarted again by registerRequestUpdateCallback(). - if (that->m_requestUpdateCallbacks.isEmpty()) - emscripten_pause_main_loop(); - - that->doMaintainTimers(); - }; - int fps = 0; // update using requestAnimationFrame - int simulateInfiniteLoop = 1; - emscripten_set_main_loop_arg(callback, this, fps, simulateInfiniteLoop); - - // Note: the above call never returns, not even at app exit - return false; + QEventDispatcherWasm::processPostedEvents(); + return QWindowSystemInterface::sendWindowSystemEvents(QEventLoop::AllEvents); } -void QWasmEventDispatcher::doMaintainTimers() +void QWasmEventDispatcher::onLoaded() { - Q_D(QWasmEventDispatcher); - - // This functon schedules native timers in order to wake up to - // process events and activate Qt timers. This is done using the - // emscripten_async_call() API which schedules a new timer. - // There is unfortunately no way to cancel or update a current - // native timer. - - // Schedule a zero-timer to continue processing any pending events. - extern uint qGlobalPostedEventsCount(); // from qapplication.cpp - if (!m_hasZeroTimer && (qGlobalPostedEventsCount() || QWindowSystemInterface::windowSystemEventsQueued())) { - auto callback = [](void *eventDispatcher) { - QWasmEventDispatcher *that = static_cast<QWasmEventDispatcher *>(eventDispatcher); - that->m_hasZeroTimer = false; - that->QUnixEventDispatcherQPA::processEvents(QEventLoop::AllEvents); - - // Processing events may have posted new events or created new timers - that->doMaintainTimers(); - }; + // This function is called when the application is ready to paint + // the first frame. Send the qtlaoder onLoaded event first (via + // the base class implementation), and then enable/call requestUpdate + // to deliver a frame. + QEventDispatcherWasm::onLoaded(); - emscripten_async_call(callback, this, 0); - m_hasZeroTimer = true; - return; - } + // Make sure all screens have a defined size; and pick + // up size changes due to onLoaded event handling. + QWasmIntegration *wasmIntegration = QWasmIntegration::get(); + wasmIntegration->resizeAllScreens(); - auto timespecToNanosec = [](timespec ts) -> uint64_t { return ts.tv_sec * 1000 + ts.tv_nsec / (1000 * 1000); }; - - // Get current time and time-to-first-Qt-timer. This polls for system - // time, and we use this time as the current time for the duration of this call. - timespec toWait; - bool hasTimers = d->timerList.timerWait(toWait); - if (!hasTimers) - return; // no timer needed - - uint64_t currentTime = timespecToNanosec(d->timerList.currentTime); - uint64_t toWaitDuration = timespecToNanosec(toWait); - - // The currently scheduled timer target is stored in m_currentTargetTime. - // We can re-use it if the new target is equivalent or later. - uint64_t newTargetTime = currentTime + toWaitDuration; - if (newTargetTime >= m_currentTargetTime) - return; // existing timer is good - - // Schedule a native timer with a callback which processes events (and timers) - auto callback = [](void *eventDispatcher) { - QWasmEventDispatcher *that = static_cast<QWasmEventDispatcher *>(eventDispatcher); - that->m_currentTargetTime = std::numeric_limits<uint64_t>::max(); - that->QUnixEventDispatcherQPA::processEvents(QEventLoop::AllEvents); - - // Processing events may have posted new events or created new timers - that->doMaintainTimers(); - }; - emscripten_async_call(callback, this, toWaitDuration); - m_currentTargetTime = newTargetTime; + wasmIntegration->releaseRequesetUpdateHold(); } -void QWasmEventDispatcher::wakeUp() -{ -#ifdef EMSCRIPTEN_HAS_ASYNC_RUN_IN_MAIN_RUNTIME_THREAD - if (!emscripten_is_main_runtime_thread() && m_hasMainLoop) { - - // Make two-step async call to mainThreadWakeUp in order to make sure the - // call is made at a point where the main thread is idle. - void (*intermediate)(void *) = [](void *eventdispatcher){ - emscripten_async_call(QWasmEventDispatcher::mainThreadWakeUp, eventdispatcher, 0); - }; - emscripten_async_run_in_main_runtime_thread_(EM_FUNC_SIG_VI, (void *)intermediate, this); - } -#endif - QEventDispatcherUNIX::wakeUp(); -} - -void QWasmEventDispatcher::mainThreadWakeUp(void *eventDispatcher) -{ - emscripten_resume_main_loop(); // Service possible requestUpdate Calls - static_cast<QWasmEventDispatcher *>(eventDispatcher)->processEvents(QEventLoop::AllEvents); -} +QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmeventdispatcher.h b/src/plugins/platforms/wasm/qwasmeventdispatcher.h index 8420f2cf1a..cbf10482e3 100644 --- a/src/plugins/platforms/wasm/qwasmeventdispatcher.h +++ b/src/plugins/platforms/wasm/qwasmeventdispatcher.h @@ -1,64 +1,18 @@ -/**************************************************************************** -** -** 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$ -** -****************************************************************************/ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #ifndef QWASMEVENTDISPATCHER_H #define QWASMEVENTDISPATCHER_H -#include <QtCore/qhash.h> -#include <QtCore/qloggingcategory.h> -#include <QtGui/private/qunixeventdispatcher_qpa_p.h> +#include <QtCore/private/qeventdispatcher_wasm_p.h> QT_BEGIN_NAMESPACE -class QWasmEventDispatcherPrivate; - -class QWasmEventDispatcher : public QUnixEventDispatcherQPA +class QWasmEventDispatcher : public QEventDispatcherWasm { - Q_DECLARE_PRIVATE(QWasmEventDispatcher) -public: - explicit QWasmEventDispatcher(QObject *parent = nullptr); - ~QWasmEventDispatcher(); - - static bool registerRequestUpdateCallback(std::function<void(void)> callback); - static void maintainTimers(); - protected: - bool processEvents(QEventLoop::ProcessEventsFlags flags) override; - void doMaintainTimers(); - void wakeUp() override; - static void mainThreadWakeUp(void *eventDispatcher); - -private: - bool m_hasMainLoop = false; - bool m_hasZeroTimer = false; - uint64_t m_currentTargetTime = std::numeric_limits<uint64_t>::max(); - QList<std::function<void(void)>> m_requestUpdateCallbacks; + bool processPostedEvents() override; + void onLoaded() override; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmeventtranslator.cpp b/src/plugins/platforms/wasm/qwasmeventtranslator.cpp deleted file mode 100644 index de9135a086..0000000000 --- a/src/plugins/platforms/wasm/qwasmeventtranslator.cpp +++ /dev/null @@ -1,1032 +0,0 @@ -/**************************************************************************** -** -** 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$ -** -****************************************************************************/ - -#include "qwasmeventtranslator.h" -#include "qwasmeventdispatcher.h" -#include "qwasmcompositor.h" -#include "qwasmintegration.h" -#include "qwasmclipboard.h" -#include "qwasmstring.h" - -#include <QtGui/qevent.h> -#include <qpa/qwindowsysteminterface.h> -#include <QtCore/qcoreapplication.h> -#include <QtCore/qglobal.h> -#include <QtCore/qobject.h> - -#include <QtCore/qdeadlinetimer.h> -#include <private/qmakearray_p.h> -#include <QtCore/qnamespace.h> -#include <QCursor> - -#include <emscripten/bind.h> - -#include <iostream> - -using namespace emscripten; - -QT_BEGIN_NAMESPACE - -typedef struct emkb2qt { - const char *em; - unsigned int qt; - - constexpr bool operator <=(const emkb2qt &that) const noexcept - { - return !(strcmp(that) > 0); - } - - bool operator <(const emkb2qt &that) const noexcept - { - return ::strcmp(em, that.em) < 0; - } - constexpr int strcmp(const emkb2qt &that, const int i = 0) const - { - return em[i] == 0 && that.em[i] == 0 ? 0 - : em[i] == 0 ? -1 - : that.em[i] == 0 ? 1 - : em[i] < that.em[i] ? -1 - : em[i] > that.em[i] ? 1 - : strcmp(that, i + 1); - } -} emkb2qt_t; - -template<unsigned int Qt, char ... EmChar> -struct Emkb2Qt -{ - static constexpr const char storage[sizeof ... (EmChar) + 1] = {EmChar..., '\0'}; - using Type = emkb2qt_t; - static constexpr Type data() noexcept { return Type{storage, Qt}; } -}; - -template<unsigned int Qt, char ... EmChar> constexpr char Emkb2Qt<Qt, EmChar...>::storage[]; - -static constexpr const auto KeyTbl = qMakeArray( - QSortedData< - Emkb2Qt< Qt::Key_Escape, 'E','s','c','a','p','e' >, - Emkb2Qt< Qt::Key_Tab, 'T','a','b' >, - Emkb2Qt< Qt::Key_Backspace, 'B','a','c','k','s','p','a','c','e' >, - Emkb2Qt< Qt::Key_Return, 'E','n','t','e','r' >, - Emkb2Qt< Qt::Key_Insert, 'I','n','s','e','r','t' >, - Emkb2Qt< Qt::Key_Delete, 'D','e','l','e','t','e' >, - Emkb2Qt< Qt::Key_Pause, 'P','a','u','s','e' >, - Emkb2Qt< Qt::Key_Pause, 'C','l','e','a','r' >, - Emkb2Qt< Qt::Key_Home, 'H','o','m','e' >, - Emkb2Qt< Qt::Key_End, 'E','n','d' >, - Emkb2Qt< Qt::Key_Left, 'A','r','r','o','w','L','e','f','t' >, - Emkb2Qt< Qt::Key_Up, 'A','r','r','o','w','U','p' >, - Emkb2Qt< Qt::Key_Right, 'A','r','r','o','w','R','i','g','h','t' >, - Emkb2Qt< Qt::Key_Down, 'A','r','r','o','w','D','o','w','n' >, - Emkb2Qt< Qt::Key_PageUp, 'P','a','g','e','U','p' >, - Emkb2Qt< Qt::Key_PageDown, 'P','a','g','e','D','o','w','n' >, - Emkb2Qt< Qt::Key_Shift, 'S','h','i','f','t' >, - Emkb2Qt< Qt::Key_Control, 'C','o','n','t','r','o','l' >, - Emkb2Qt< Qt::Key_Meta, 'O','S'>, - Emkb2Qt< Qt::Key_Alt, 'A','l','t','L','e','f','t' >, - Emkb2Qt< Qt::Key_Alt, 'A','l','t' >, - Emkb2Qt< Qt::Key_CapsLock, 'C','a','p','s','L','o','c','k' >, - Emkb2Qt< Qt::Key_NumLock, 'N','u','m','L','o','c','k' >, - Emkb2Qt< Qt::Key_ScrollLock, 'S','c','r','o','l','l','L','o','c','k' >, - Emkb2Qt< Qt::Key_F1, 'F','1' >, - Emkb2Qt< Qt::Key_F2, 'F','2' >, - Emkb2Qt< Qt::Key_F3, 'F','3' >, - Emkb2Qt< Qt::Key_F4, 'F','4' >, - Emkb2Qt< Qt::Key_F5, 'F','5' >, - Emkb2Qt< Qt::Key_F6, 'F','6' >, - Emkb2Qt< Qt::Key_F7, 'F','7' >, - Emkb2Qt< Qt::Key_F8, 'F','8' >, - Emkb2Qt< Qt::Key_F9, 'F','9' >, - Emkb2Qt< Qt::Key_F10, 'F','1','0' >, - Emkb2Qt< Qt::Key_F11, 'F','1','1' >, - Emkb2Qt< Qt::Key_F12, 'F','1','2' >, - Emkb2Qt< Qt::Key_F13, 'F','1','3' >, - Emkb2Qt< Qt::Key_F14, 'F','1','4' >, - Emkb2Qt< Qt::Key_F15, 'F','1','5' >, - Emkb2Qt< Qt::Key_F16, 'F','1','6' >, - Emkb2Qt< Qt::Key_F17, 'F','1','7' >, - Emkb2Qt< Qt::Key_F18, 'F','1','8' >, - Emkb2Qt< Qt::Key_F19, 'F','1','9' >, - Emkb2Qt< Qt::Key_F20, 'F','2','0' >, - Emkb2Qt< Qt::Key_F21, 'F','2','1' >, - Emkb2Qt< Qt::Key_F22, 'F','2','2' >, - Emkb2Qt< Qt::Key_F23, 'F','2','3' >, - Emkb2Qt< Qt::Key_Space, ' ' >, - Emkb2Qt< Qt::Key_Comma, ',' >, - Emkb2Qt< Qt::Key_Minus, '-' >, - Emkb2Qt< Qt::Key_Period, '.' >, - Emkb2Qt< Qt::Key_Slash, '/' >, - Emkb2Qt< Qt::Key_0, 'D','i','g','i','t','0' >, - Emkb2Qt< Qt::Key_1, 'D','i','g','i','t','1' >, - Emkb2Qt< Qt::Key_2, 'D','i','g','i','t','2' >, - Emkb2Qt< Qt::Key_3, 'D','i','g','i','t','3' >, - Emkb2Qt< Qt::Key_4, 'D','i','g','i','t','4' >, - Emkb2Qt< Qt::Key_5, 'D','i','g','i','t','5' >, - Emkb2Qt< Qt::Key_6, 'D','i','g','i','t','6' >, - Emkb2Qt< Qt::Key_7, 'D','i','g','i','t','7' >, - Emkb2Qt< Qt::Key_8, 'D','i','g','i','t','8' >, - Emkb2Qt< Qt::Key_9, 'D','i','g','i','t','9' >, - Emkb2Qt< Qt::Key_Semicolon, ';' >, - Emkb2Qt< Qt::Key_Equal, '=' >, - Emkb2Qt< Qt::Key_A, 'K','e','y','A' >, - Emkb2Qt< Qt::Key_B, 'K','e','y','B' >, - Emkb2Qt< Qt::Key_C, 'K','e','y','C' >, - Emkb2Qt< Qt::Key_D, 'K','e','y','D' >, - Emkb2Qt< Qt::Key_E, 'K','e','y','E' >, - Emkb2Qt< Qt::Key_F, 'K','e','y','F' >, - Emkb2Qt< Qt::Key_G, 'K','e','y','G' >, - Emkb2Qt< Qt::Key_H, 'K','e','y','H' >, - Emkb2Qt< Qt::Key_I, 'K','e','y','I' >, - Emkb2Qt< Qt::Key_J, 'K','e','y','J' >, - Emkb2Qt< Qt::Key_K, 'K','e','y','K' >, - Emkb2Qt< Qt::Key_L, 'K','e','y','L' >, - Emkb2Qt< Qt::Key_M, 'K','e','y','M' >, - Emkb2Qt< Qt::Key_N, 'K','e','y','N' >, - Emkb2Qt< Qt::Key_O, 'K','e','y','O' >, - Emkb2Qt< Qt::Key_P, 'K','e','y','P' >, - Emkb2Qt< Qt::Key_Q, 'K','e','y','Q' >, - Emkb2Qt< Qt::Key_R, 'K','e','y','R' >, - Emkb2Qt< Qt::Key_S, 'K','e','y','S' >, - Emkb2Qt< Qt::Key_T, 'K','e','y','T' >, - Emkb2Qt< Qt::Key_U, 'K','e','y','U' >, - Emkb2Qt< Qt::Key_V, 'K','e','y','V' >, - Emkb2Qt< Qt::Key_W, 'K','e','y','W' >, - Emkb2Qt< Qt::Key_X, 'K','e','y','X' >, - Emkb2Qt< Qt::Key_Y, 'K','e','y','Y' >, - Emkb2Qt< Qt::Key_Z, 'K','e','y','Z' >, - Emkb2Qt< Qt::Key_BracketLeft, '[' >, - Emkb2Qt< Qt::Key_Backslash, '\\' >, - Emkb2Qt< Qt::Key_BracketRight, ']' >, - Emkb2Qt< Qt::Key_Apostrophe, '\'' >, - Emkb2Qt< Qt::Key_QuoteLeft, 'B','a','c','k','q','u','o','t','e' >, - Emkb2Qt< Qt::Key_multiply, 'N','u','m','p','a','d','M','u','l','t','i','p','l','y' >, - Emkb2Qt< Qt::Key_Minus, 'N','u','m','p','a','d','S','u','b','t','r','a','c','t' >, - Emkb2Qt< Qt::Key_Period, 'N','u','m','p','a','d','D','e','c','i','m','a','l' >, - Emkb2Qt< Qt::Key_Plus, 'N','u','m','p','a','d','A','d','d' >, - Emkb2Qt< Qt::Key_division, 'N','u','m','p','a','d','D','i','v','i','d','e' >, - Emkb2Qt< Qt::Key_Equal, 'N','u','m','p','a','d','E','q','u','a','l' >, - Emkb2Qt< Qt::Key_0, 'N','u','m','p','a','d','0' >, - Emkb2Qt< Qt::Key_1, 'N','u','m','p','a','d','1' >, - Emkb2Qt< Qt::Key_2, 'N','u','m','p','a','d','2' >, - Emkb2Qt< Qt::Key_3, 'N','u','m','p','a','d','3' >, - Emkb2Qt< Qt::Key_4, 'N','u','m','p','a','d','4' >, - Emkb2Qt< Qt::Key_5, 'N','u','m','p','a','d','5' >, - Emkb2Qt< Qt::Key_6, 'N','u','m','p','a','d','6' >, - Emkb2Qt< Qt::Key_7, 'N','u','m','p','a','d','7' >, - Emkb2Qt< Qt::Key_8, 'N','u','m','p','a','d','8' >, - Emkb2Qt< Qt::Key_9, 'N','u','m','p','a','d','9' >, - Emkb2Qt< Qt::Key_Comma, 'N','u','m','p','a','d','C','o','m','m','a' >, - Emkb2Qt< Qt::Key_Enter, 'N','u','m','p','a','d','E','n','t','e','r' >, - Emkb2Qt< Qt::Key_Paste, 'P','a','s','t','e' >, - Emkb2Qt< Qt::Key_AltGr, 'A','l','t','R','i','g','h','t' >, - Emkb2Qt< Qt::Key_Help, 'H','e','l','p' >, - Emkb2Qt< Qt::Key_Equal, '=' >, - Emkb2Qt< Qt::Key_yen, 'I','n','t','l','Y','e','n' >, - - Emkb2Qt< Qt::Key_Exclam, '\x21' >, - Emkb2Qt< Qt::Key_QuoteDbl, '\x22' >, - Emkb2Qt< Qt::Key_NumberSign, '\x23' >, - Emkb2Qt< Qt::Key_Dollar, '\x24' >, - Emkb2Qt< Qt::Key_Percent, '\x25' >, - Emkb2Qt< Qt::Key_Ampersand, '\x26' >, - Emkb2Qt< Qt::Key_ParenLeft, '\x28' >, - Emkb2Qt< Qt::Key_ParenRight, '\x29' >, - Emkb2Qt< Qt::Key_Asterisk, '\x2a' >, - Emkb2Qt< Qt::Key_Plus, '\x2b' >, - Emkb2Qt< Qt::Key_Colon, '\x3a' >, - Emkb2Qt< Qt::Key_Semicolon, '\x3b' >, - Emkb2Qt< Qt::Key_Less, '\x3c' >, - Emkb2Qt< Qt::Key_Equal, '\x3d' >, - Emkb2Qt< Qt::Key_Greater, '\x3e' >, - Emkb2Qt< Qt::Key_Question, '\x3f' >, - Emkb2Qt< Qt::Key_At, '\x40' >, - Emkb2Qt< Qt::Key_BracketLeft, '\x5b' >, - Emkb2Qt< Qt::Key_Backslash, '\x5c' >, - Emkb2Qt< Qt::Key_BracketRight, '\x5d' >, - Emkb2Qt< Qt::Key_AsciiCircum, '\x5e' >, - Emkb2Qt< Qt::Key_Underscore, '\x5f' >, - Emkb2Qt< Qt::Key_QuoteLeft, '\x60'>, - Emkb2Qt< Qt::Key_BraceLeft, '\x7b'>, - Emkb2Qt< Qt::Key_Bar, '\x7c'>, - Emkb2Qt< Qt::Key_BraceRight, '\x7d'>, - Emkb2Qt< Qt::Key_AsciiTilde, '\x7e'>, - Emkb2Qt< Qt::Key_Space, '\x20' >, - Emkb2Qt< Qt::Key_Comma, '\x2c' >, - Emkb2Qt< Qt::Key_Minus, '\x2d' >, - Emkb2Qt< Qt::Key_Period, '\x2e' >, - Emkb2Qt< Qt::Key_Slash, '\x2f' >, - Emkb2Qt< Qt::Key_Apostrophe, '\x27' >, - Emkb2Qt< Qt::Key_Menu, 'C','o','n','t','e','x','t','M','e','n','u' >, - - Emkb2Qt< Qt::Key_Agrave, '\xc3','\xa0' >, - Emkb2Qt< Qt::Key_Aacute, '\xc3','\xa1' >, - Emkb2Qt< Qt::Key_Acircumflex, '\xc3','\xa2' >, - Emkb2Qt< Qt::Key_Adiaeresis, '\xc3','\xa4' >, - Emkb2Qt< Qt::Key_AE, '\xc3','\xa6' >, - Emkb2Qt< Qt::Key_Atilde, '\xc3','\xa3' >, - Emkb2Qt< Qt::Key_Aring, '\xc3','\xa5' >, - Emkb2Qt< Qt::Key_Ccedilla, '\xc3','\xa7' >, - Emkb2Qt< Qt::Key_Egrave, '\xc3','\xa8' >, - Emkb2Qt< Qt::Key_Eacute, '\xc3','\xa9' >, - Emkb2Qt< Qt::Key_Ecircumflex, '\xc3','\xaa' >, - Emkb2Qt< Qt::Key_Ediaeresis, '\xc3','\xab' >, - Emkb2Qt< Qt::Key_Icircumflex, '\xc3','\xae' >, - Emkb2Qt< Qt::Key_Idiaeresis, '\xc3','\xaf' >, - Emkb2Qt< Qt::Key_Ocircumflex, '\xc3','\xb4' >, - Emkb2Qt< Qt::Key_Odiaeresis, '\xc3','\xb6' >, - Emkb2Qt< Qt::Key_Ograve, '\xc3','\xb2' >, - Emkb2Qt< Qt::Key_Oacute, '\xc3','\xb3' >, - Emkb2Qt< Qt::Key_Ooblique, '\xc3','\xb8' >, - Emkb2Qt< Qt::Key_Otilde, '\xc3','\xb5' >, - Emkb2Qt< Qt::Key_Ucircumflex, '\xc3','\xbb' >, - Emkb2Qt< Qt::Key_Udiaeresis, '\xc3','\xbc' >, - Emkb2Qt< Qt::Key_Ugrave, '\xc3','\xb9' >, - Emkb2Qt< Qt::Key_Uacute, '\xc3','\xba' >, - Emkb2Qt< Qt::Key_Ntilde, '\xc3','\xb1' >, - Emkb2Qt< Qt::Key_ydiaeresis, '\xc3','\xbf' > - >::Data{} - ); - -static constexpr const auto DeadKeyShiftTbl = qMakeArray( - QSortedData< - // shifted - Emkb2Qt< Qt::Key_Agrave, '\xc3','\x80' >, - Emkb2Qt< Qt::Key_Aacute, '\xc3','\x81' >, - Emkb2Qt< Qt::Key_Acircumflex, '\xc3','\x82' >, - Emkb2Qt< Qt::Key_Adiaeresis, '\xc3','\x84' >, - Emkb2Qt< Qt::Key_AE, '\xc3','\x86' >, - Emkb2Qt< Qt::Key_Atilde, '\xc3','\x83' >, - Emkb2Qt< Qt::Key_Aring, '\xc3','\x85' >, - Emkb2Qt< Qt::Key_Egrave, '\xc3','\x88' >, - Emkb2Qt< Qt::Key_Eacute, '\xc3','\x89' >, - Emkb2Qt< Qt::Key_Ecircumflex, '\xc3','\x8a' >, - Emkb2Qt< Qt::Key_Ediaeresis, '\xc3','\x8b' >, - Emkb2Qt< Qt::Key_Icircumflex, '\xc3','\x8e' >, - Emkb2Qt< Qt::Key_Idiaeresis, '\xc3','\x8f' >, - Emkb2Qt< Qt::Key_Ocircumflex, '\xc3','\x94' >, - Emkb2Qt< Qt::Key_Odiaeresis, '\xc3','\x96' >, - Emkb2Qt< Qt::Key_Ograve, '\xc3','\x92' >, - Emkb2Qt< Qt::Key_Oacute, '\xc3','\x93' >, - Emkb2Qt< Qt::Key_Ooblique, '\xc3','\x98' >, - Emkb2Qt< Qt::Key_Otilde, '\xc3','\x95' >, - Emkb2Qt< Qt::Key_Ucircumflex, '\xc3','\x9b' >, - Emkb2Qt< Qt::Key_Udiaeresis, '\xc3','\x9c' >, - Emkb2Qt< Qt::Key_Ugrave, '\xc3','\x99' >, - Emkb2Qt< Qt::Key_Uacute, '\xc3','\x9a' >, - Emkb2Qt< Qt::Key_Ntilde, '\xc3','\x91' >, - Emkb2Qt< Qt::Key_Ccedilla, '\xc3','\x87' >, - Emkb2Qt< Qt::Key_ydiaeresis, '\xc3','\x8f' > - >::Data{} -); - -// macOS CTRL <-> META switching. We most likely want to enable -// the existing switching code in QtGui, but for now do it here. -static bool g_usePlatformMacSpecifics = false; - -bool g_useNaturalScrolling = true; // natural scrolling is default on linux/windows - -static void mouseWheelEvent(emscripten::val event) { - - emscripten::val wheelInterted = event["webkitDirectionInvertedFromDevice"]; - - if (wheelInterted.as<bool>()) { - g_useNaturalScrolling = true; - } -} - -EMSCRIPTEN_BINDINGS(qtMouseModule) { - function("qtMouseWheelEvent", &mouseWheelEvent); -} - -QWasmEventTranslator::QWasmEventTranslator(QWasmScreen *screen) - : QObject(screen) - , draggedWindow(nullptr) - , lastWindow(nullptr) - , pressedButtons(Qt::NoButton) - , resizeMode(QWasmWindow::ResizeNone) -{ -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - touchDevice = new QPointingDevice("touchscreen", 1, QInputDevice::DeviceType::TouchScreen, - QPointingDevice::PointerType::Finger, - QPointingDevice::Capability::Position | QPointingDevice::Capability::Area | QPointingDevice::Capability::NormalizedPosition, - 10, 0); -#else - touchDevice = new QPointingDevice; - touchDevice->setType(QInputDevice::DeviceType::TouchScreen); - touchDevice->setCapabilities(QPointingDevice::Capability::Position | QPointingDevice::Capability::Area | QPointingDevice::Capability::NormalizedPosition); -#endif - QWindowSystemInterface::registerInputDevice(touchDevice); - - initEventHandlers(); -} - -QWasmEventTranslator::~QWasmEventTranslator() -{ - // deregister event handlers - QByteArray canvasSelector = "#" + screen()->canvasId().toUtf8(); - emscripten_set_keydown_callback(canvasSelector.constData(), 0, 0, NULL); - emscripten_set_keyup_callback(canvasSelector.constData(), 0, 0, NULL); - - emscripten_set_mousedown_callback(canvasSelector.constData(), 0, 0, NULL); - emscripten_set_mouseup_callback(canvasSelector.constData(), 0, 0, NULL); - emscripten_set_mousemove_callback(canvasSelector.constData(), 0, 0, NULL); - - emscripten_set_focus_callback(canvasSelector.constData(), 0, 0, NULL); - - emscripten_set_wheel_callback(canvasSelector.constData(), 0, 0, NULL); - - emscripten_set_touchstart_callback(canvasSelector.constData(), 0, 0, NULL); - emscripten_set_touchend_callback(canvasSelector.constData(), 0, 0, NULL); - emscripten_set_touchmove_callback(canvasSelector.constData(), 0, 0, NULL); - emscripten_set_touchcancel_callback(canvasSelector.constData(), 0, 0, NULL); -} - -void QWasmEventTranslator::initEventHandlers() -{ - QByteArray canvasSelector = "#" + screen()->canvasId().toUtf8(); - - // The Platform Detect: expand coverage and move as needed - enum Platform { - GenericPlatform, - MacOSPlatform - }; - Platform platform = Platform(emscripten::val::global("navigator")["platform"] - .call<bool>("includes", emscripten::val("Mac"))); - g_usePlatformMacSpecifics = (platform == MacOSPlatform); - - if (platform == MacOSPlatform) { - g_useNaturalScrolling = false; // make this !default on macOS - - if (!emscripten::val::global("window")["safari"].isUndefined()) { - val canvas = screen()->canvas(); - canvas.call<void>("addEventListener", - val("wheel"), - val::module_property("qtMouseWheelEvent")); - } - } - - emscripten_set_keydown_callback(canvasSelector.constData(), (void *)this, 1, &keyboard_cb); - emscripten_set_keyup_callback(canvasSelector.constData(), (void *)this, 1, &keyboard_cb); - - emscripten_set_mousedown_callback(canvasSelector.constData(), (void *)this, 1, &mouse_cb); - emscripten_set_mouseup_callback(canvasSelector.constData(), (void *)this, 1, &mouse_cb); - emscripten_set_mousemove_callback(canvasSelector.constData(), (void *)this, 1, &mouse_cb); - - emscripten_set_focus_callback(canvasSelector.constData(), (void *)this, 1, &focus_cb); - - emscripten_set_wheel_callback(canvasSelector.constData(), (void *)this, 1, &wheel_cb); - - emscripten_set_touchstart_callback(canvasSelector.constData(), (void *)this, 1, &touchCallback); - emscripten_set_touchend_callback(canvasSelector.constData(), (void *)this, 1, &touchCallback); - emscripten_set_touchmove_callback(canvasSelector.constData(), (void *)this, 1, &touchCallback); - emscripten_set_touchcancel_callback(canvasSelector.constData(), (void *)this, 1, &touchCallback); -} - -template <typename Event> -QFlags<Qt::KeyboardModifier> QWasmEventTranslator::translatKeyModifier(const Event *event) -{ - QFlags<Qt::KeyboardModifier> keyModifier = Qt::NoModifier; - if (event->shiftKey) - keyModifier |= Qt::ShiftModifier; - if (event->ctrlKey) { - if (g_usePlatformMacSpecifics) - keyModifier |= Qt::MetaModifier; - else - keyModifier |= Qt::ControlModifier; - } - if (event->altKey) - keyModifier |= Qt::AltModifier; - if (event->metaKey) { - if (g_usePlatformMacSpecifics) - keyModifier |= Qt::ControlModifier; - else - keyModifier |= Qt::MetaModifier; - } - return keyModifier; -} - -QFlags<Qt::KeyboardModifier> QWasmEventTranslator::translateKeyboardEventModifier(const EmscriptenKeyboardEvent *keyEvent) -{ - QFlags<Qt::KeyboardModifier> keyModifier = translatKeyModifier(keyEvent); - if (keyEvent->location == DOM_KEY_LOCATION_NUMPAD) { - keyModifier |= Qt::KeypadModifier; - } - - return keyModifier; -} - -QFlags<Qt::KeyboardModifier> QWasmEventTranslator::translateMouseEventModifier(const EmscriptenMouseEvent *mouseEvent) -{ - return translatKeyModifier(mouseEvent); -} - -int QWasmEventTranslator::keyboard_cb(int eventType, const EmscriptenKeyboardEvent *keyEvent, void *userData) -{ - QWasmEventTranslator *wasmTranslator = reinterpret_cast<QWasmEventTranslator *>(userData); - bool accepted = wasmTranslator->processKeyboard(eventType, keyEvent); - - return accepted ? 1 : 0; -} - -QWasmScreen *QWasmEventTranslator::screen() -{ - return static_cast<QWasmScreen *>(parent()); -} - -Qt::Key QWasmEventTranslator::translateEmscriptKey(const EmscriptenKeyboardEvent *emscriptKey) -{ - Qt::Key qtKey = Qt::Key_unknown; - - if (qstrncmp(emscriptKey->key, "Dead", 4) == 0 ) { - emkb2qt_t searchKey1{emscriptKey->code, 0}; - for (auto it1 = KeyTbl.cbegin(); it1 != KeyTbl.end(); ++it1) - if (it1 != KeyTbl.end() && (qstrcmp(searchKey1.em, it1->em) == 0)) { - qtKey = static_cast<Qt::Key>(it1->qt); - } - - } else if (qstrncmp(emscriptKey->code, "Key", 3) == 0 || qstrncmp(emscriptKey->code, "Numpad", 6) == 0 || - qstrncmp(emscriptKey->code, "Digit", 5) == 0) { - emkb2qt_t searchKey{emscriptKey->code, 0}; // search emcsripten code - auto it1 = std::lower_bound(KeyTbl.cbegin(), KeyTbl.cend(), searchKey); - if (it1 != KeyTbl.end() && !(searchKey < *it1)) { - qtKey = static_cast<Qt::Key>(it1->qt); - } - } - - if (qtKey == Qt::Key_unknown) { - emkb2qt_t searchKey{emscriptKey->key, 0}; // search unicode key - auto it1 = std::lower_bound(KeyTbl.cbegin(), KeyTbl.cend(), searchKey); - if (it1 != KeyTbl.end() && !(searchKey < *it1)) { - qtKey = static_cast<Qt::Key>(it1->qt); - } - } - if (qtKey == Qt::Key_unknown) {//try harder with shifted number keys - emkb2qt_t searchKey1{emscriptKey->key, 0}; - for (auto it1 = KeyTbl.cbegin(); it1 != KeyTbl.end(); ++it1) - if (it1 != KeyTbl.end() && (qstrcmp(searchKey1.em, it1->em) == 0)) { - qtKey = static_cast<Qt::Key>(it1->qt); - } - } - - return qtKey; -} - -Qt::MouseButton QWasmEventTranslator::translateMouseButton(unsigned short button) -{ - if (button == 0) - return Qt::LeftButton; - else if (button == 1) - return Qt::MiddleButton; - else if (button == 2) - return Qt::RightButton; - - return Qt::NoButton; -} - -int QWasmEventTranslator::mouse_cb(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData) -{ - QWasmEventTranslator *translator = (QWasmEventTranslator*)userData; - translator->processMouse(eventType,mouseEvent); - QWasmEventDispatcher::maintainTimers(); - return 0; -} - -void resizeWindow(QWindow *window, QWasmWindow::ResizeMode mode, - QRect startRect, QPoint amount) -{ - if (mode == QWasmWindow::ResizeNone) - return; - - bool top = mode == QWasmWindow::ResizeTopLeft || - mode == QWasmWindow::ResizeTop || - mode == QWasmWindow::ResizeTopRight; - - bool bottom = mode == QWasmWindow::ResizeBottomLeft || - mode == QWasmWindow::ResizeBottom || - mode == QWasmWindow::ResizeBottomRight; - - bool left = mode == QWasmWindow::ResizeLeft || - mode == QWasmWindow::ResizeTopLeft || - mode == QWasmWindow::ResizeBottomLeft; - - bool right = mode == QWasmWindow::ResizeRight || - mode == QWasmWindow::ResizeTopRight || - mode == QWasmWindow::ResizeBottomRight; - - int x1 = startRect.left(); - int y1 = startRect.top(); - int x2 = startRect.right(); - int y2 = startRect.bottom(); - - if (left) - x1 += amount.x(); - if (top) - y1 += amount.y(); - if (right) - x2 += amount.x(); - if (bottom) - y2 += amount.y(); - - int w = x2-x1; - int h = y2-y1; - - if (w < window->minimumWidth()) { - if (left) - x1 -= window->minimumWidth() - w; - - w = window->minimumWidth(); - } - - if (h < window->minimumHeight()) { - if (top) - y1 -= window->minimumHeight() - h; - - h = window->minimumHeight(); - } - - window->setGeometry(x1, y1, w, h); -} - -void QWasmEventTranslator::processMouse(int eventType, const EmscriptenMouseEvent *mouseEvent) -{ - QPoint targetPoint(mouseEvent->targetX, mouseEvent->targetY); - QPoint globalPoint = screen()->geometry().topLeft() + targetPoint; - - QEvent::Type buttonEventType = QEvent::None; - Qt::MouseButton button = translateMouseButton(mouseEvent->button); - Qt::KeyboardModifiers modifiers = translateMouseEventModifier(mouseEvent); - - QWindow *window2 = nullptr; - if (resizeMode == QWasmWindow::ResizeNone) - window2 = screen()->compositor()->windowAt(globalPoint, 5); - - if (lastWindow && lastWindow->cursor() != Qt::ArrowCursor) { - lastWindow->setCursor(Qt::ArrowCursor); - } - if (window2 == nullptr) { - window2 = lastWindow; - } else { - lastWindow = window2; - } - - QPoint localPoint = window2->mapFromGlobal(globalPoint); - bool interior = window2->geometry().contains(globalPoint); - - QWasmWindow *htmlWindow = static_cast<QWasmWindow*>(window2->handle()); - switch (eventType) { - case EMSCRIPTEN_EVENT_MOUSEDOWN: - { - if (window2) - window2->requestActivate(); - - pressedButtons.setFlag(button); - - if (mouseEvent->button == 0) { - pressedWindow = window2; - buttonEventType = QEvent::MouseButtonPress; - if (!(htmlWindow->m_windowState & Qt::WindowFullScreen) && !(htmlWindow->m_windowState & Qt::WindowMaximized)) { - if (htmlWindow && window2->flags().testFlag(Qt::WindowTitleHint) && htmlWindow->isPointOnTitle(globalPoint)) - draggedWindow = window2; - else if (htmlWindow && htmlWindow->isPointOnResizeRegion(globalPoint)) { - draggedWindow = window2; - resizeMode = htmlWindow->resizeModeAtPoint(globalPoint); - resizePoint = globalPoint; - resizeStartRect = window2->geometry(); - } - } - } - - htmlWindow->injectMousePressed(localPoint, globalPoint, button, modifiers); - break; - } - case EMSCRIPTEN_EVENT_MOUSEUP: - { - pressedButtons.setFlag(translateMouseButton(mouseEvent->button), false); - buttonEventType = QEvent::MouseButtonRelease; - QWasmWindow *oldWindow = nullptr; - - if (mouseEvent->button == 0 && pressedWindow) { - oldWindow = static_cast<QWasmWindow*>(pressedWindow->handle()); - pressedWindow = nullptr; - } - - if (mouseEvent->button == 0) { - draggedWindow = nullptr; - resizeMode = QWasmWindow::ResizeNone; - } - - if (oldWindow) - oldWindow->injectMouseReleased(localPoint, globalPoint, button, modifiers); - else - htmlWindow->injectMouseReleased(localPoint, globalPoint, button, modifiers); - break; - } - case EMSCRIPTEN_EVENT_MOUSEMOVE: // drag event - { - buttonEventType = QEvent::MouseMove; - - if (htmlWindow && htmlWindow->isPointOnResizeRegion(globalPoint)) - window2->setCursor(cursorForMode(htmlWindow->resizeModeAtPoint(globalPoint))); - - if (!(htmlWindow->m_windowState & Qt::WindowFullScreen) && !(htmlWindow->m_windowState & Qt::WindowMaximized)) { - if (resizeMode == QWasmWindow::ResizeNone && draggedWindow) { - draggedWindow->setX(draggedWindow->x() + mouseEvent->movementX); - draggedWindow->setY(draggedWindow->y() + mouseEvent->movementY); - } - - if (resizeMode != QWasmWindow::ResizeNone && !(htmlWindow->m_windowState & Qt::WindowFullScreen)) { - QPoint delta = QPoint(mouseEvent->targetX, mouseEvent->targetY) - resizePoint; - resizeWindow(draggedWindow, resizeMode, resizeStartRect, delta); - } - } - break; - } - default: // MOUSELEAVE MOUSEENTER - break; - }; - if (!window2 && buttonEventType == QEvent::MouseButtonRelease) { - window2 = lastWindow; - lastWindow = nullptr; - interior = true; - } - if (window2 && interior) { - QWindowSystemInterface::handleMouseEvent<QWindowSystemInterface::SynchronousDelivery>( - window2, getTimestamp(), localPoint, globalPoint, pressedButtons, button, buttonEventType, modifiers); - } -} - -int QWasmEventTranslator::focus_cb(int /*eventType*/, const EmscriptenFocusEvent */*focusEvent*/, void */*userData*/) -{ - return 0; -} - -int QWasmEventTranslator::wheel_cb(int eventType, const EmscriptenWheelEvent *wheelEvent, void *userData) -{ - Q_UNUSED(eventType); - - QWasmEventTranslator *eventTranslator = static_cast<QWasmEventTranslator *>(userData); - EmscriptenMouseEvent mouseEvent = wheelEvent->mouse; - - int scrollFactor = 0; - switch (wheelEvent->deltaMode) { - case DOM_DELTA_PIXEL://chrome safari - scrollFactor = 1; - break; - case DOM_DELTA_LINE: //firefox - scrollFactor = 12; - break; - case DOM_DELTA_PAGE: - scrollFactor = 20; - break; - }; - - if (g_useNaturalScrolling) //macOS platform has document oriented scrolling - scrollFactor = -scrollFactor; - - QWasmEventTranslator *translator = (QWasmEventTranslator*)userData; - Qt::KeyboardModifiers modifiers = translator->translateMouseEventModifier(&mouseEvent); - QPoint targetPoint(mouseEvent.targetX, mouseEvent.targetY); - QPoint globalPoint = eventTranslator->screen()->geometry().topLeft() + targetPoint; - - QWindow *window2 = eventTranslator->screen()->compositor()->windowAt(globalPoint, 5); - if (!window2) - return 0; - QPoint localPoint = window2->mapFromGlobal(globalPoint); - - QPoint pixelDelta; - - if (wheelEvent->deltaY != 0) pixelDelta.setY(wheelEvent->deltaY * scrollFactor); - if (wheelEvent->deltaX != 0) pixelDelta.setX(wheelEvent->deltaX * scrollFactor); - - QWindowSystemInterface::handleWheelEvent(window2, getTimestamp(), localPoint, - globalPoint, QPoint(), pixelDelta, modifiers); - QWasmEventDispatcher::maintainTimers(); - - return 1; -} - -int QWasmEventTranslator::touchCallback(int eventType, const EmscriptenTouchEvent *touchEvent, void *userData) -{ - auto translator = reinterpret_cast<QWasmEventTranslator*>(userData); - return translator->handleTouch(eventType, touchEvent); -} - -int QWasmEventTranslator::handleTouch(int eventType, const EmscriptenTouchEvent *touchEvent) -{ - QList<QWindowSystemInterface::TouchPoint> touchPointList; - touchPointList.reserve(touchEvent->numTouches); - QWindow *window2; - - for (int i = 0; i < touchEvent->numTouches; i++) { - - const EmscriptenTouchPoint *touches = &touchEvent->touches[i]; - - QPoint targetPoint(touches->targetX, touches->targetY); - QPoint globalPoint = screen()->geometry().topLeft() + targetPoint; - - window2 = this->screen()->compositor()->windowAt(globalPoint, 5); - if (window2 == nullptr) - continue; - - QWindowSystemInterface::TouchPoint touchPoint; - - touchPoint.area = QRect(0, 0, 8, 8); - touchPoint.id = touches->identifier; - touchPoint.pressure = 1.0; - - touchPoint.area.moveCenter(globalPoint); - - const auto tp = pressedTouchIds.constFind(touchPoint.id); - if (tp != pressedTouchIds.constEnd()) - touchPoint.normalPosition = tp.value(); - - QPointF localPoint = QPointF(window2->mapFromGlobal(globalPoint)); - QPointF normalPosition(localPoint.x() / window2->width(), - localPoint.y() / window2->height()); - - const bool stationaryTouchPoint = (normalPosition == touchPoint.normalPosition); - touchPoint.normalPosition = normalPosition; - - switch (eventType) { - case EMSCRIPTEN_EVENT_TOUCHSTART: - if (tp != pressedTouchIds.constEnd()) { - touchPoint.state = (stationaryTouchPoint - ? QEventPoint::State::Stationary - : QEventPoint::State::Updated); - } else { - touchPoint.state = QEventPoint::State::Pressed; - } - pressedTouchIds.insert(touchPoint.id, touchPoint.normalPosition); - - break; - case EMSCRIPTEN_EVENT_TOUCHEND: - touchPoint.state = QEventPoint::State::Released; - pressedTouchIds.remove(touchPoint.id); - break; - case EMSCRIPTEN_EVENT_TOUCHMOVE: - touchPoint.state = (stationaryTouchPoint - ? QEventPoint::State::Stationary - : QEventPoint::State::Updated); - - pressedTouchIds.insert(touchPoint.id, touchPoint.normalPosition); - break; - default: - break; - } - - touchPointList.append(touchPoint); - } - - QFlags<Qt::KeyboardModifier> keyModifier = translatKeyModifier(touchEvent); - - QWindowSystemInterface::handleTouchEvent<QWindowSystemInterface::SynchronousDelivery>( - window2, getTimestamp(), touchDevice, touchPointList, keyModifier); - - if (eventType == EMSCRIPTEN_EVENT_TOUCHCANCEL) - QWindowSystemInterface::handleTouchCancelEvent(window2, getTimestamp(), touchDevice, keyModifier); - - QWasmEventDispatcher::maintainTimers(); - return 1; -} - -quint64 QWasmEventTranslator::getTimestamp() -{ - return emscripten_performance_now(); -} - -struct KeyMapping { Qt::Key from, to; }; - -constexpr KeyMapping tildeKeyTable[] = { // ~ - { Qt::Key_A, Qt::Key_Atilde }, - { Qt::Key_N, Qt::Key_Ntilde }, - { Qt::Key_O, Qt::Key_Otilde }, -}; -constexpr KeyMapping graveKeyTable[] = { // ` - { Qt::Key_A, Qt::Key_Agrave }, - { Qt::Key_E, Qt::Key_Egrave }, - { Qt::Key_I, Qt::Key_Igrave }, - { Qt::Key_O, Qt::Key_Ograve }, - { Qt::Key_U, Qt::Key_Ugrave }, -}; -constexpr KeyMapping acuteKeyTable[] = { // ' - { Qt::Key_A, Qt::Key_Aacute }, - { Qt::Key_E, Qt::Key_Eacute }, - { Qt::Key_I, Qt::Key_Iacute }, - { Qt::Key_O, Qt::Key_Oacute }, - { Qt::Key_U, Qt::Key_Uacute }, - { Qt::Key_Y, Qt::Key_Yacute }, -}; -constexpr KeyMapping diaeresisKeyTable[] = { // umlaut ¨ - { Qt::Key_A, Qt::Key_Adiaeresis }, - { Qt::Key_E, Qt::Key_Ediaeresis }, - { Qt::Key_I, Qt::Key_Idiaeresis }, - { Qt::Key_O, Qt::Key_Odiaeresis }, - { Qt::Key_U, Qt::Key_Udiaeresis }, - { Qt::Key_Y, Qt::Key_ydiaeresis }, -}; -constexpr KeyMapping circumflexKeyTable[] = { // ^ - { Qt::Key_A, Qt::Key_Acircumflex }, - { Qt::Key_E, Qt::Key_Ecircumflex }, - { Qt::Key_I, Qt::Key_Icircumflex }, - { Qt::Key_O, Qt::Key_Ocircumflex }, - { Qt::Key_U, Qt::Key_Ucircumflex }, -}; - -static Qt::Key find_impl(const KeyMapping *first, const KeyMapping *last, Qt::Key key) noexcept -{ - while (first != last) { - if (first->from == key) - return first->to; - ++first; - } - return Qt::Key_unknown; -} - -template <size_t N> -static Qt::Key find(const KeyMapping (&map)[N], Qt::Key key) noexcept -{ - return find_impl(map, map + N, key); -} - -Qt::Key QWasmEventTranslator::translateDeadKey(Qt::Key deadKey, Qt::Key accentBaseKey) -{ - Qt::Key wasmKey = Qt::Key_unknown; - - if (deadKey == Qt::Key_QuoteLeft ) { - if (g_usePlatformMacSpecifics) { // ` macOS: Key_Dead_Grave - wasmKey = find(graveKeyTable, accentBaseKey); - } else { - wasmKey = find(diaeresisKeyTable, accentBaseKey); - } - return wasmKey; - } - - switch (deadKey) { - // case Qt::Key_QuoteLeft: - case Qt::Key_O: // ´ Key_Dead_Grave - wasmKey = find(graveKeyTable, accentBaseKey); - break; - case Qt::Key_E: // ´ Key_Dead_Acute - wasmKey = find(acuteKeyTable, accentBaseKey); - break; - case Qt::Key_AsciiTilde: - case Qt::Key_N:// Key_Dead_Tilde - wasmKey = find(tildeKeyTable, accentBaseKey); - break; - case Qt::Key_U:// ¨ Key_Dead_Diaeresis - wasmKey = find(diaeresisKeyTable, accentBaseKey); - break; - case Qt::Key_I:// macOS Key_Dead_Circumflex - case Qt::Key_6:// linux - case Qt::Key_Apostrophe:// linux - wasmKey = find(circumflexKeyTable, accentBaseKey); - break; - default: - break; - - }; - return wasmKey; -} - -bool QWasmEventTranslator::processKeyboard(int eventType, const EmscriptenKeyboardEvent *keyEvent) -{ - Qt::Key qtKey = translateEmscriptKey(keyEvent); - - Qt::KeyboardModifiers modifiers = translateKeyboardEventModifier(keyEvent); - - QString keyText; - QEvent::Type keyType = QEvent::None; - switch (eventType) { - case EMSCRIPTEN_EVENT_KEYPRESS: - case EMSCRIPTEN_EVENT_KEYDOWN: // down - keyType = QEvent::KeyPress; - - if (m_emDeadKey != Qt::Key_unknown) { - - Qt::Key transformedKey = translateDeadKey(m_emDeadKey, qtKey); - - if (transformedKey != Qt::Key_unknown) - qtKey = transformedKey; - - if (keyEvent->shiftKey == 0) { - for (auto it = KeyTbl.cbegin(); it != KeyTbl.end(); ++it) { - if (it != KeyTbl.end() && (qtKey == static_cast<Qt::Key>(it->qt))) { - keyText = it->em; - m_emDeadKey = Qt::Key_unknown; - break; - } - } - } else { - for (auto it = DeadKeyShiftTbl.cbegin(); it != DeadKeyShiftTbl.end(); ++it) { - if (it != DeadKeyShiftTbl.end() && (qtKey == static_cast<Qt::Key>(it->qt))) { - keyText = it->em; - m_emDeadKey = Qt::Key_unknown; - break; - } - } - } - } - if (qstrncmp(keyEvent->key, "Dead", 4) == 0 || qtKey == Qt::Key_AltGr) { - qtKey = translateEmscriptKey(keyEvent); - m_emStickyDeadKey = true; - if (keyEvent->shiftKey == 1 && qtKey == Qt::Key_QuoteLeft) - qtKey = Qt::Key_AsciiTilde; - m_emDeadKey = qtKey; - } - break; - case EMSCRIPTEN_EVENT_KEYUP: // up - keyType = QEvent::KeyRelease; - if (m_emStickyDeadKey && qtKey != Qt::Key_Alt) { - m_emStickyDeadKey = false; - } - break; - default: - break; - }; - - if (keyType == QEvent::None) - return 0; - - QFlags<Qt::KeyboardModifier> mods = translateKeyboardEventModifier(keyEvent); - - // Clipboard fallback path: cut/copy/paste are handled by clipboard event - // handlers if direct clipboard access is not available. - if (!QWasmIntegration::get()->getWasmClipboard()->hasClipboardApi && modifiers & Qt::ControlModifier && - (qtKey == Qt::Key_X || qtKey == Qt::Key_C || qtKey == Qt::Key_V)) { - return 0; - } - - bool accepted = false; - - if (keyType == QEvent::KeyPress && - mods.testFlag(Qt::ControlModifier) - && qtKey == Qt::Key_V) { - QWasmIntegration::get()->getWasmClipboard()->readTextFromClipboard(); - } else { - if (keyText.isEmpty()) - keyText = QString(keyEvent->key); - if (keyText.size() > 1) - keyText.clear(); - accepted = QWindowSystemInterface::handleKeyEvent<QWindowSystemInterface::SynchronousDelivery>( - 0, keyType, qtKey, modifiers, keyText); - } - if (keyType == QEvent::KeyPress && - mods.testFlag(Qt::ControlModifier) - && qtKey == Qt::Key_C) { - QWasmIntegration::get()->getWasmClipboard()->writeTextToClipboard(); - } - - QWasmEventDispatcher::maintainTimers(); - - return accepted; -} - -QCursor QWasmEventTranslator::cursorForMode(QWasmWindow::ResizeMode m) -{ - switch (m) { - case QWasmWindow::ResizeTopLeft: - case QWasmWindow::ResizeBottomRight: - return Qt::SizeFDiagCursor; - break; - case QWasmWindow::ResizeBottomLeft: - case QWasmWindow::ResizeTopRight: - return Qt::SizeBDiagCursor; - break; - case QWasmWindow::ResizeTop: - case QWasmWindow::ResizeBottom: - return Qt::SizeVerCursor; - break; - case QWasmWindow::ResizeLeft: - case QWasmWindow::ResizeRight: - return Qt::SizeHorCursor; - break; - } - return Qt::ArrowCursor; -} - -QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmeventtranslator.h b/src/plugins/platforms/wasm/qwasmeventtranslator.h deleted file mode 100644 index 9c30bfa5d6..0000000000 --- a/src/plugins/platforms/wasm/qwasmeventtranslator.h +++ /dev/null @@ -1,110 +0,0 @@ -/**************************************************************************** -** -** 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$ -** -****************************************************************************/ - -#ifndef QWASMEVENTTRANSLATOR_H -#define QWASMEVENTTRANSLATOR_H - -#include <QtCore/qobject.h> -#include <QtCore/qrect.h> -#include <QtCore/qpoint.h> -#include <emscripten/html5.h> -#include "qwasmwindow.h" -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) -#include <QtGui/qinputdevice.h> -#else -#include <QtGui/qpointingdevice.h> -#endif -#include <QHash> -#include <QCursor> - -QT_BEGIN_NAMESPACE - -class QWindow; - -class QWasmEventTranslator : public QObject -{ - Q_OBJECT - -public: - - explicit QWasmEventTranslator(QWasmScreen *screen); - ~QWasmEventTranslator(); - - static int keyboard_cb(int eventType, const EmscriptenKeyboardEvent *keyEvent, void *userData); - static int mouse_cb(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData); - static int focus_cb(int eventType, const EmscriptenFocusEvent *focusEvent, void *userData); - static int wheel_cb(int eventType, const EmscriptenWheelEvent *wheelEvent, void *userData); - - static int touchCallback(int eventType, const EmscriptenTouchEvent *ev, void *userData); - - void processEvents(); - void initEventHandlers(); - int handleTouch(int eventType, const EmscriptenTouchEvent *touchEvent); - -Q_SIGNALS: - void getWindowAt(const QPoint &point, QWindow **window); -private: - QWasmScreen *screen(); - Qt::Key translateEmscriptKey(const EmscriptenKeyboardEvent *emscriptKey); - template <typename Event> - QFlags<Qt::KeyboardModifier> translatKeyModifier(const Event *event); - QFlags<Qt::KeyboardModifier> translateKeyboardEventModifier(const EmscriptenKeyboardEvent *keyEvent); - QFlags<Qt::KeyboardModifier> translateMouseEventModifier(const EmscriptenMouseEvent *mouseEvent); - Qt::MouseButton translateMouseButton(unsigned short button); - - void processMouse(int eventType, const EmscriptenMouseEvent *mouseEvent); - bool processKeyboard(int eventType, const EmscriptenKeyboardEvent *keyEvent); - - Qt::Key translateDeadKey(Qt::Key deadKey, Qt::Key accentBaseKey); - - QMap <int, QPointF> pressedTouchIds; - -private: - QWindow *draggedWindow; - QWindow *pressedWindow; - QWindow *lastWindow; - Qt::MouseButtons pressedButtons; - - QWasmWindow::ResizeMode resizeMode; - QPoint resizePoint; - QRect resizeStartRect; -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - QPointingDevice *touchDevice; -#else - QTouchDevice *touchDevice; -#endif - static quint64 getTimestamp(); - - Qt::Key m_emDeadKey = Qt::Key_unknown; - bool m_emStickyDeadKey = false; - QCursor cursorForMode(QWasmWindow::ResizeMode mode); -}; - -QT_END_NAMESPACE -#endif // QWASMEVENTTRANSLATOR_H diff --git a/src/plugins/platforms/wasm/qwasmfontdatabase.cpp b/src/plugins/platforms/wasm/qwasmfontdatabase.cpp index 7623444588..3f3dc10f71 100644 --- a/src/plugins/platforms/wasm/qwasmfontdatabase.cpp +++ b/src/plugins/platforms/wasm/qwasmfontdatabase.cpp @@ -1,47 +1,272 @@ -/**************************************************************************** -** -** 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$ -** -****************************************************************************/ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "qwasmfontdatabase.h" +#include "qwasmintegration.h" #include <QtCore/qfile.h> +#include <QtCore/private/qstdweb_p.h> +#include <QtCore/private/qeventdispatcher_wasm_p.h> +#include <QtGui/private/qguiapplication_p.h> + +#include <emscripten.h> +#include <emscripten/val.h> +#include <emscripten/bind.h> + +#include <map> +#include <array> QT_BEGIN_NAMESPACE -void QWasmFontDatabase::populateFontDatabase() +using namespace emscripten; +using namespace Qt::StringLiterals; + + +namespace { + +class FontData +{ +public: + FontData(val fontData) + :m_fontData(fontData) {} + + QString family() const + { + return QString::fromStdString(m_fontData["family"].as<std::string>()); + } + + QString fullName() const + { + return QString::fromStdString(m_fontData["fullName"].as<std::string>()); + } + + QString postscriptName() const + { + return QString::fromStdString(m_fontData["postscriptName"].as<std::string>()); + } + + QString style() const + { + return QString::fromStdString(m_fontData["style"].as<std::string>()); + } + + val value() const + { + return m_fontData; + } + +private: + val m_fontData; +}; + +val makeObject(const char *key, const char *value) +{ + val obj = val::object(); + obj.set(key, std::string(value)); + return obj; +} + +void printError(val err) { + qCWarning(lcQpaFonts) + << QString::fromStdString(err["name"].as<std::string>()) + << QString::fromStdString(err["message"].as<std::string>()); + QWasmFontDatabase::endAllFontFileLoading(); +} + +void checkFontAccessPermitted(std::function<void(bool)> callback) +{ + const val permissions = val::global("navigator")["permissions"]; + if (permissions.isUndefined()) { + callback(false); + return; + } + + qstdweb::Promise::make(permissions, "query", { + .thenFunc = [callback](val status) { + callback(status["state"].as<std::string>() == "granted"); + }, + }, makeObject("name", "local-fonts")); +} + +void queryLocalFonts(std::function<void(const QList<FontData> &)> callback) +{ + emscripten::val window = emscripten::val::global("window"); + qstdweb::Promise::make(window, "queryLocalFonts", { + .thenFunc = [callback](emscripten::val fontArray) { + QList<FontData> fonts; + const int count = fontArray["length"].as<int>(); + fonts.reserve(count); + for (int i = 0; i < count; ++i) + fonts.append(FontData(fontArray.call<emscripten::val>("at", i))); + callback(fonts); + }, + .catchFunc = printError + }); +} + +void readBlob(val blob, std::function<void(const QByteArray &)> callback) +{ + qstdweb::Promise::make(blob, "arrayBuffer", { + .thenFunc = [callback](emscripten::val fontArrayBuffer) { + QByteArray fontData = qstdweb::Uint8Array(qstdweb::ArrayBuffer(fontArrayBuffer)).copyToQByteArray(); + callback(fontData); + }, + .catchFunc = printError + }); +} + +void readFont(FontData font, std::function<void(const QByteArray &)> callback) +{ + qstdweb::Promise::make(font.value(), "blob", { + .thenFunc = [callback](val blob) { + readBlob(blob, [callback](const QByteArray &data) { + callback(data); + }); + }, + .catchFunc = printError + }); +} + +emscripten::val getLocalFontsConfigProperty(const char *name) { + emscripten::val qt = val::module_property("qt"); + if (qt.isUndefined()) + return emscripten::val(); + emscripten::val localFonts = qt["localFonts"]; + if (localFonts.isUndefined()) + return emscripten::val(); + return localFonts[name]; +}; + +bool getLocalFontsBoolConfigPropertyWithDefault(const char *name, bool defaultValue) { + emscripten::val prop = getLocalFontsConfigProperty(name); + if (prop.isUndefined()) + return defaultValue; + return prop.as<bool>(); +}; + +QString getLocalFontsStringConfigPropertyWithDefault(const char *name, QString defaultValue) { + emscripten::val prop = getLocalFontsConfigProperty(name); + if (prop.isUndefined()) + return defaultValue; + return QString::fromStdString(prop.as<std::string>()); +}; + +QStringList getLocalFontsStringListConfigPropertyWithDefault(const char *name, QStringList defaultValue) { + emscripten::val array = getLocalFontsConfigProperty(name); + if (array.isUndefined()) + return defaultValue; + + QStringList list; + int size = array["length"].as<int>(); + for (int i = 0; i < size; ++i) { + emscripten::val element = array.call<emscripten::val>("at", i); + QString string = QString::fromStdString(element.as<std::string>()); + if (!string.isEmpty()) + list.append(string); + } + return list; +}; + +} // namespace + +QWasmFontDatabase::QWasmFontDatabase() +:QFreeTypeFontDatabase() +{ + m_localFontsApiSupported = val::global("window")["queryLocalFonts"].isUndefined() == false; + if (m_localFontsApiSupported) + beginFontDatabaseStartupTask(); +} + +QWasmFontDatabase *QWasmFontDatabase::get() +{ + return static_cast<QWasmFontDatabase *>(QWasmIntegration::get()->fontDatabase()); +} + +// Populates the font database with local fonts. Will make the browser ask +// the user for permission if needed. Does nothing if the Local Font Access API +// is not supported. +void QWasmFontDatabase::populateLocalfonts() { - // Load font file from resources. Currently - // all fonts needs to be bundled with the nexe - // as Qt resources. + // Decide which font families to populate based on user preferences + QStringList selectedLocalFontFamilies; + bool allFamilies = false; + + switch (m_localFontFamilyLoadSet) { + case NoFontFamilies: + default: + // keep empty selectedLocalFontFamilies + break; + case DefaultFontFamilies: { + const QStringList webSafeFontFamilies = + {"Arial", "Verdana", "Tahoma", "Trebuchet", "Times New Roman", + "Georgia", "Garamond", "Courier New"}; + selectedLocalFontFamilies = webSafeFontFamilies; + } break; + case AllFontFamilies: + allFamilies = true; + break; + } + + selectedLocalFontFamilies += m_extraLocalFontFamilies; + if (selectedLocalFontFamilies.isEmpty() && !allFamilies) { + endAllFontFileLoading(); + return; + } + + populateLocalFontFamilies(selectedLocalFontFamilies, allFamilies); +} + +namespace { + QStringList toStringList(emscripten::val array) + { + QStringList list; + int size = array["length"].as<int>(); + for (int i = 0; i < size; ++i) { + emscripten::val element = array.call<emscripten::val>("at", i); + QString string = QString::fromStdString(element.as<std::string>()); + if (!string.isEmpty()) + list.append(string); + } + return list; + } +} + +void QWasmFontDatabase::populateLocalFontFamilies(emscripten::val families) +{ + if (!m_localFontsApiSupported) + return; + populateLocalFontFamilies(toStringList(families), false); +} + +void QWasmFontDatabase::populateLocalFontFamilies(const QStringList &fontFamilies, bool allFamilies) +{ + queryLocalFonts([fontFamilies, allFamilies](const QList<FontData> &fonts) { + refFontFileLoading(); + QList<FontData> filteredFonts; + std::copy_if(fonts.begin(), fonts.end(), std::back_inserter(filteredFonts), + [fontFamilies, allFamilies](FontData fontData) { + return allFamilies || fontFamilies.contains(fontData.family()); + }); + + for (const FontData &font: filteredFonts) { + refFontFileLoading(); + readFont(font, [font](const QByteArray &fontData){ + QFreeTypeFontDatabase::registerFontFamily(font.family()); + QFreeTypeFontDatabase::addTTFile(fontData, QByteArray()); + derefFontFileLoading(); + }); + } + derefFontFileLoading(); + }); + +} + +void QWasmFontDatabase::populateFontDatabase() +{ + // Load bundled font file from resources. const QString fontFileNames[] = { QStringLiteral(":/fonts/DejaVuSansMono.ttf"), - QStringLiteral(":/fonts/Vera.ttf"), QStringLiteral(":/fonts/DejaVuSans.ttf"), }; for (const QString &fontFileName : fontFileNames) { @@ -51,11 +276,45 @@ void QWasmFontDatabase::populateFontDatabase() QFreeTypeFontDatabase::addTTFile(theFont.readAll(), fontFileName.toLatin1()); } + + // Get config options for controlling local fonts usage + m_queryLocalFontsPermission = getLocalFontsBoolConfigPropertyWithDefault("requestPermission", false); + QString fontFamilyLoadSet = getLocalFontsStringConfigPropertyWithDefault("familiesCollection", "DefaultFontFamilies"); + m_extraLocalFontFamilies = getLocalFontsStringListConfigPropertyWithDefault("extraFamilies", QStringList()); + + if (fontFamilyLoadSet == "NoFontFamilies") { + m_localFontFamilyLoadSet = NoFontFamilies; + } else if (fontFamilyLoadSet == "DefaultFontFamilies") { + m_localFontFamilyLoadSet = DefaultFontFamilies; + } else if (fontFamilyLoadSet == "AllFontFamilies") { + m_localFontFamilyLoadSet = AllFontFamilies; + } else { + m_localFontFamilyLoadSet = NoFontFamilies; + qWarning() << "Unknown fontFamilyLoadSet value" << fontFamilyLoadSet; + } + + if (!m_localFontsApiSupported) + return; + + // Populate the font database with local fonts. Either try unconditianlly + // if displyaing a fonts permissions dialog at startup is allowed, or else + // only if we already have permission. + if (m_queryLocalFontsPermission) { + populateLocalfonts(); + } else { + checkFontAccessPermitted([this](bool granted) { + if (granted) + populateLocalfonts(); + else + endAllFontFileLoading(); + }); + } } QFontEngine *QWasmFontDatabase::fontEngine(const QFontDef &fontDef, void *handle) { - return QFreeTypeFontDatabase::fontEngine(fontDef, handle); + QFontEngine *fontEngine = QFreeTypeFontDatabase::fontEngine(fontDef, handle); + return fontEngine; } QStringList QWasmFontDatabase::fallbacksForFamily(const QString &family, QFont::Style style, @@ -65,11 +324,13 @@ QStringList QWasmFontDatabase::fallbacksForFamily(const QString &family, QFont:: QStringList fallbacks = QFreeTypeFontDatabase::fallbacksForFamily(family, style, styleHint, script); - // Add the vera.ttf font (loaded in populateFontDatabase above) as a falback font + // Add the DejaVuSans.ttf font (loaded in populateFontDatabase above) as a falback font // to all other fonts (except itself). - const QString veraFontFamily = QStringLiteral("Bitstream Vera Sans"); - if (family != veraFontFamily) - fallbacks.append(veraFontFamily); + static const QString wasmFallbackFonts[] = { "DejaVu Sans" }; + for (auto wasmFallbackFont : wasmFallbackFonts) { + if (family != wasmFallbackFont && !fallbacks.contains(wasmFallbackFont)) + fallbacks.append(wasmFallbackFont); + } return fallbacks; } @@ -81,7 +342,63 @@ void QWasmFontDatabase::releaseHandle(void *handle) QFont QWasmFontDatabase::defaultFont() const { - return QFont(QLatin1String("Bitstream Vera Sans")); + return QFont("DejaVu Sans"_L1); } +namespace { + int g_pendingFonts = 0; + bool g_fontStartupTaskCompleted = false; +} + +// Registers font loading as a startup task, which makes Qt delay +// sending onLoaded event until font loading has completed. +void QWasmFontDatabase::beginFontDatabaseStartupTask() +{ + g_fontStartupTaskCompleted = false; + QEventDispatcherWasm::registerStartupTask(); +} + +// Ends the font loading startup task. +void QWasmFontDatabase::endFontDatabaseStartupTask() +{ + if (!g_fontStartupTaskCompleted) { + g_fontStartupTaskCompleted = true; + QEventDispatcherWasm::completeStarupTask(); + } +} + +// Registers that a font file will be loaded. +void QWasmFontDatabase::refFontFileLoading() +{ + g_pendingFonts += 1; +} + +// Registers that one font file has been loaded, and sends notifactions +// when all pending font files have been loaded. +void QWasmFontDatabase::derefFontFileLoading() +{ + if (--g_pendingFonts <= 0) { + QFontCache::instance()->clear(); + emit qGuiApp->fontDatabaseChanged(); + endFontDatabaseStartupTask(); + } +} + +// Unconditionally ends local font loading, for instance if there +// are no fonts to load or if there was an unexpected error. +void QWasmFontDatabase::endAllFontFileLoading() +{ + bool hadPandingfonts = g_pendingFonts > 0; + if (hadPandingfonts) { + // The hadPandingfonts counter might no longer be correct; disable counting + // and send notifications unconditionally. + g_pendingFonts = 0; + QFontCache::instance()->clear(); + emit qGuiApp->fontDatabaseChanged(); + } + + endFontDatabaseStartupTask(); +} + + QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmfontdatabase.h b/src/plugins/platforms/wasm/qwasmfontdatabase.h index 420211947c..a1c8f1ff48 100644 --- a/src/plugins/platforms/wasm/qwasmfontdatabase.h +++ b/src/plugins/platforms/wasm/qwasmfontdatabase.h @@ -1,42 +1,21 @@ -/**************************************************************************** -** -** 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$ -** -****************************************************************************/ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #ifndef QWASMFONTDATABASE_H #define QWASMFONTDATABASE_H #include <QtGui/private/qfreetypefontdatabase_p.h> +#include <emscripten/val.h> + QT_BEGIN_NAMESPACE class QWasmFontDatabase : public QFreeTypeFontDatabase { public: + QWasmFontDatabase(); + static QWasmFontDatabase *get(); + void populateFontDatabase() override; QFontEngine *fontEngine(const QFontDef &fontDef, void *handle) override; QStringList fallbacksForFamily(const QString &family, QFont::Style style, @@ -44,6 +23,27 @@ public: QChar::Script script) const override; void releaseHandle(void *handle) override; QFont defaultFont() const override; + + void populateLocalfonts(); + void populateLocalFontFamilies(emscripten::val families); + void populateLocalFontFamilies(const QStringList &famliies, bool allFamilies); + + static void beginFontDatabaseStartupTask(); + static void endFontDatabaseStartupTask(); + static void refFontFileLoading(); + static void derefFontFileLoading(); + static void endAllFontFileLoading(); + +private: + bool m_localFontsApiSupported = false; + bool m_queryLocalFontsPermission = false; + enum FontFamilyLoadSet { + NoFontFamilies, + DefaultFontFamilies, + AllFontFamilies, + }; + FontFamilyLoadSet m_localFontFamilyLoadSet; + QStringList m_extraLocalFontFamilies; }; QT_END_NAMESPACE #endif diff --git a/src/plugins/platforms/wasm/qwasminputcontext.cpp b/src/plugins/platforms/wasm/qwasminputcontext.cpp new file mode 100644 index 0000000000..ae72e7b7f9 --- /dev/null +++ b/src/plugins/platforms/wasm/qwasminputcontext.cpp @@ -0,0 +1,161 @@ +// Copyright (C) 2019 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include <emscripten/bind.h> + +#include "qwasminputcontext.h" +#include "qwasmintegration.h" +#include "qwasmplatform.h" +#include <QRectF> +#include <qpa/qplatforminputcontext.h> +#include "qwasmscreen.h" +#include <qguiapplication.h> +#include <qwindow.h> +#include <QKeySequence> +#include <qpa/qwindowsysteminterface.h> + +QT_BEGIN_NAMESPACE +using namespace qstdweb; + +static void inputCallback(emscripten::val event) +{ + // android sends all the characters typed since the user started typing in this element + int length = event["target"]["value"]["length"].as<int>(); + if (length <= 0) + return; + + emscripten::val _incomingCharVal = event["data"]; + if (_incomingCharVal != emscripten::val::undefined() && _incomingCharVal != emscripten::val::null()) { + + QString str = QString::fromStdString(_incomingCharVal.as<std::string>()); + QWasmInputContext *wasmInput = + reinterpret_cast<QWasmInputContext*>(event["target"]["data-qinputcontext"].as<quintptr>()); + wasmInput->inputStringChanged(str, EMSCRIPTEN_EVENT_KEYDOWN, wasmInput); + } + // this clears the input string, so backspaces do not send a character + // but stops suggestions + event["target"].set("value", ""); +} + +EMSCRIPTEN_BINDINGS(clipboard_module) { + function("qtInputContextCallback", &inputCallback); +} + +QWasmInputContext::QWasmInputContext() +{ + emscripten::val document = emscripten::val::global("document"); + m_inputElement = document.call<emscripten::val>("createElement", std::string("input")); + m_inputElement.set("type", "text"); + m_inputElement.set("style", "position:absolute;left:-1000px;top:-1000px"); // offscreen + m_inputElement.set("contenteditable","true"); + + if (platform() == Platform::MacOS || platform() == Platform::iOS) { + auto callback = [=](emscripten::val) { + m_inputElement["parentElement"].call<void>("removeChild", m_inputElement); + inputPanelIsOpen = false; + }; + m_blurEventHandler.reset(new EventCallback(m_inputElement, "blur", callback)); + + } else { + + const std::string inputType = platform() == Platform::Windows ? "textInput" : "input"; + + document.call<void>("addEventListener", inputType, + emscripten::val::module_property("qtInputContextCallback"), + emscripten::val(false)); + m_inputElement.set("data-qinputcontext", + emscripten::val(quintptr(reinterpret_cast<void *>(this)))); + emscripten::val body = document["body"]; + body.call<void>("appendChild", m_inputElement); + } + + QObject::connect(qGuiApp, &QGuiApplication::focusWindowChanged, this, + &QWasmInputContext::focusWindowChanged); +} + +QWasmInputContext::~QWasmInputContext() +{ + if (platform() == Platform::Android || platform() == Platform::Windows) + emscripten_set_keydown_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, 0, 0, NULL); +} + +void QWasmInputContext::focusWindowChanged(QWindow *focusWindow) +{ + m_focusWindow = focusWindow; +} + +emscripten::val QWasmInputContext::inputHandlerElementForFocusedWindow() +{ + if (!m_focusWindow) + return emscripten::val::undefined(); + return static_cast<QWasmWindow *>(m_focusWindow->handle())->inputHandlerElement(); +} + +void QWasmInputContext::update(Qt::InputMethodQueries queries) +{ + QPlatformInputContext::update(queries); +} + +void QWasmInputContext::showInputPanel() +{ + if (platform() == Platform::Windows + && !inputPanelIsOpen) { // call this only once for win32 + m_inputElement.call<void>("focus"); + return; + } + // this is called each time the keyboard is touched + + // Add the input element as a child of the screen for the + // currently focused window and give it focus. The browser + // will not display the input element, but mobile browsers + // should display the virtual keyboard. Key events will be + // captured by the keyboard event handler installed on the + // screen element. + + if (platform() == Platform::MacOS // keep for compatibility + || platform() == Platform::iOS + || platform() == Platform::Windows) { + emscripten::val inputWrapper = inputHandlerElementForFocusedWindow(); + if (inputWrapper.isUndefined()) + return; + inputWrapper.call<void>("appendChild", m_inputElement); + } + + m_inputElement.call<void>("focus"); + inputPanelIsOpen = true; +} + +void QWasmInputContext::hideInputPanel() +{ + if (QWasmIntegration::get()->touchPoints < 1) + return; + m_inputElement.call<void>("blur"); + inputPanelIsOpen = false; +} + +void QWasmInputContext::inputStringChanged(QString &inputString, int eventType, QWasmInputContext *context) +{ + Q_UNUSED(context) + QKeySequence keys = QKeySequence::fromString(inputString); + Qt::Key thisKey = keys[0].key(); + + // synthesize this keyevent as android is not normal + if (inputString.size() > 2 && (thisKey < Qt::Key_F35 + || thisKey > Qt::Key_Back)) { + inputString.clear(); + } + if (inputString == QStringLiteral("Escape")) { + thisKey = Qt::Key_Escape; + inputString.clear(); + } else if (thisKey == Qt::Key(0)) { + thisKey = Qt::Key_Return; + } + + QWindowSystemInterface::handleKeyEvent( + 0, eventType == EMSCRIPTEN_EVENT_KEYDOWN ? QEvent::KeyPress : QEvent::KeyRelease, + thisKey, keys[0].keyboardModifiers(), + eventType == EMSCRIPTEN_EVENT_KEYDOWN ? inputString : QStringLiteral("")); +} + + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasminputcontext.h b/src/plugins/platforms/wasm/qwasminputcontext.h new file mode 100644 index 0000000000..10dd1a0950 --- /dev/null +++ b/src/plugins/platforms/wasm/qwasminputcontext.h @@ -0,0 +1,48 @@ +// Copyright (C) 2019 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef QWASMINPUTCONTEXT_H +#define QWASMINPUTCONTEXT_H + + +#include <qpa/qplatforminputcontext.h> +#include <QtCore/qpointer.h> +#include <private/qstdweb_p.h> +#include <emscripten/bind.h> +#include <emscripten/html5.h> +#include <emscripten/emscripten.h> + +QT_BEGIN_NAMESPACE + +class QWasmInputContext : public QPlatformInputContext +{ + Q_DISABLE_COPY(QWasmInputContext) + Q_OBJECT +public: + explicit QWasmInputContext(); + ~QWasmInputContext() override; + + void update(Qt::InputMethodQueries) override; + + void showInputPanel() override; + void hideInputPanel() override; + bool isValid() const override { return true; } + + void focusWindowChanged(QWindow *focusWindow); + void inputStringChanged(QString &, int eventType, QWasmInputContext *context); + emscripten::val m_inputElement = emscripten::val::null(); + +private: + emscripten::val inputHandlerElementForFocusedWindow(); + + bool m_inputPanelVisible = false; + + QPointer<QWindow> m_focusWindow; + std::unique_ptr<qstdweb::EventCallback> m_blurEventHandler; + std::unique_ptr<qstdweb::EventCallback> m_inputEventHandler; + bool inputPanelIsOpen = false; +}; + +QT_END_NAMESPACE + +#endif // QWASMINPUTCONTEXT_H diff --git a/src/plugins/platforms/wasm/qwasmintegration.cpp b/src/plugins/platforms/wasm/qwasmintegration.cpp index 15d396f479..f5cc3e2eee 100644 --- a/src/plugins/platforms/wasm/qwasmintegration.cpp +++ b/src/plugins/platforms/wasm/qwasmintegration.cpp @@ -1,85 +1,61 @@ -/**************************************************************************** -** -** 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$ -** -****************************************************************************/ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "qwasmintegration.h" -#include "qwasmeventtranslator.h" #include "qwasmeventdispatcher.h" #include "qwasmcompositor.h" #include "qwasmopenglcontext.h" #include "qwasmtheme.h" #include "qwasmclipboard.h" +#include "qwasmaccessibility.h" #include "qwasmservices.h" #include "qwasmoffscreensurface.h" -#include "qwasmstring.h" - +#include "qwasmplatform.h" #include "qwasmwindow.h" -#ifndef QT_NO_OPENGL -# include "qwasmbackingstore.h" -# include <QtOpenGL/qpa/qplatformbackingstoreopenglsupport.h> -#endif +#include "qwasmbackingstore.h" #include "qwasmfontdatabase.h" -#if defined(Q_OS_UNIX) -#include <QtGui/private/qgenericunixeventdispatcher_p.h> -#endif +#include "qwasmdrag.h" + #include <qpa/qplatformwindow.h> #include <QtGui/qscreen.h> #include <qpa/qwindowsysteminterface.h> #include <QtCore/qcoreapplication.h> #include <qpa/qplatforminputcontextfactory_p.h> +#include <qpa/qwindowsysteminterface_p.h> #include <emscripten/bind.h> #include <emscripten/val.h> // this is where EGL headers are pulled in, make sure it is last #include "qwasmscreen.h" +#include <private/qsimpledrag_p.h> -using namespace emscripten; QT_BEGIN_NAMESPACE -static void browserBeforeUnload(emscripten::val) +extern void qt_set_sequence_auto_mnemonic(bool); + +using namespace emscripten; + +using namespace Qt::StringLiterals; + +static void setContainerElements(emscripten::val elementArray) { - QWasmIntegration::QWasmBrowserExit(); + QWasmIntegration::get()->setContainerElements(elementArray); } -static void addCanvasElement(emscripten::val canvas) +static void addContainerElement(emscripten::val element) { - QWasmIntegration::get()->addScreen(canvas); + QWasmIntegration::get()->addContainerElement(element); } -static void removeCanvasElement(emscripten::val canvas) +static void removeContainerElement(emscripten::val element) { - QWasmIntegration::get()->removeScreen(canvas); + QWasmIntegration::get()->removeContainerElement(element); } -static void resizeCanvasElement(emscripten::val canvas) +static void resizeContainerElement(emscripten::val element) { - QWasmIntegration::get()->resizeScreen(canvas); + QWasmIntegration::get()->resizeScreen(element); } static void qtUpdateDpi() @@ -93,83 +69,108 @@ static void resizeAllScreens(emscripten::val event) QWasmIntegration::get()->resizeAllScreens(); } +static void loadLocalFontFamilies(emscripten::val event) +{ + QWasmIntegration::get()->loadLocalFontFamilies(event); +} + EMSCRIPTEN_BINDINGS(qtQWasmIntegraton) { - function("qtBrowserBeforeUnload", &browserBeforeUnload); - function("qtAddCanvasElement", &addCanvasElement); - function("qtRemoveCanvasElement", &removeCanvasElement); - function("qtResizeCanvasElement", &resizeCanvasElement); + function("qtSetContainerElements", &setContainerElements); + function("qtAddContainerElement", &addContainerElement); + function("qtRemoveContainerElement", &removeContainerElement); + function("qtResizeContainerElement", &resizeContainerElement); function("qtUpdateDpi", &qtUpdateDpi); function("qtResizeAllScreens", &resizeAllScreens); + function("qtLoadLocalFontFamilies", &loadLocalFontFamilies); } QWasmIntegration *QWasmIntegration::s_instance; QWasmIntegration::QWasmIntegration() - : m_fontDb(nullptr), - m_desktopServices(nullptr), - m_clipboard(new QWasmClipboard) + : m_fontDb(nullptr) + , m_desktopServices(nullptr) + , m_clipboard(new QWasmClipboard) +#if QT_CONFIG(accessibility) + , m_accessibility(new QWasmAccessibility) +#endif { s_instance = this; - // We expect that qtloader.js has populated Module.qtCanvasElements with one or more canvases. - emscripten::val qtCanvaseElements = val::module_property("qtCanvasElements"); - emscripten::val canvas = val::module_property("canvas"); // TODO: remove for Qt 6.0 - - if (!qtCanvaseElements.isUndefined()) { - int screenCount = qtCanvaseElements["length"].as<int>(); - for (int i = 0; i < screenCount; ++i) { - addScreen(qtCanvaseElements[i].as<emscripten::val>()); + if (platform() == Platform::MacOS) + qt_set_sequence_auto_mnemonic(false); + + touchPoints = emscripten::val::global("navigator")["maxTouchPoints"].as<int>(); + QWindowSystemInterfacePrivate::TabletEvent::setPlatformSynthesizesMouse(false); + + // Create screens for container elements. Each container element will ultimately become a + // div element. Qt historically supported supplying canvas for screen elements - these elements + // will be transformed into divs and warnings about deprecation will be printed. See + // QWasmScreen ctor. + emscripten::val filtered = emscripten::val::array(); + emscripten::val qtContainerElements = val::module_property("qtContainerElements"); + if (qtContainerElements.isArray()) { + for (int i = 0; i < qtContainerElements["length"].as<int>(); ++i) { + emscripten::val element = qtContainerElements[i].as<emscripten::val>(); + if (element.isNull() || element.isUndefined()) + qWarning() << "Skipping null or undefined element in qtContainerElements"; + else + filtered.call<void>("push", element); } - } else if (!canvas.isUndefined()) { - qWarning() << "Module.canvas is deprecated. A future version of Qt will stop reading this property. " - << "Instead, set Module.qtCanvasElements to be an array of canvas elements, or use qtloader.js."; - addScreen(canvas); + } else { + // No screens, which may or may not be intended + qWarning() << "The qtContainerElements module property was not set or is invalid. " + "Proceeding with no screens."; } - - emscripten::val::global("window").set("onbeforeunload", val::module_property("qtBrowserBeforeUnload")); + setContainerElements(filtered); // install browser window resize handler - auto onWindowResize = [](int eventType, const EmscriptenUiEvent *e, void *userData) -> int { - Q_UNUSED(eventType); - Q_UNUSED(e); - Q_UNUSED(userData); - - // This resize event is called when the HTML window is resized. Depending - // on the page layout the canvas(es) might also have been resized, so we - // update the Qt screen sizes (and canvas render sizes). - if (QWasmIntegration *integration = QWasmIntegration::get()) - integration->resizeAllScreens(); - return 0; - }; - emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, nullptr, 1, onWindowResize); + emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, nullptr, EM_TRUE, + [](int, const EmscriptenUiEvent *, void *) -> int { + // This resize event is called when the HTML window is + // resized. Depending on the page layout the elements might + // also have been resized, so we update the Qt screen sizes + // (and canvas render sizes). + if (QWasmIntegration *integration = QWasmIntegration::get()) + integration->resizeAllScreens(); + return 0; + }); // install visualViewport resize handler which picks up size and scale change on mobile. emscripten::val visualViewport = emscripten::val::global("window")["visualViewport"]; if (!visualViewport.isUndefined()) { visualViewport.call<void>("addEventListener", val("resize"), - val::module_property("qtResizeAllScreens")); + val::module_property("qtResizeAllScreens")); } + m_drag = std::make_unique<QWasmDrag>(); } QWasmIntegration::~QWasmIntegration() { + // Remove event listener + emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, nullptr, EM_TRUE, nullptr); + emscripten::val visualViewport = emscripten::val::global("window")["visualViewport"]; + if (!visualViewport.isUndefined()) { + visualViewport.call<void>("removeEventListener", val("resize"), + val::module_property("qtResizeAllScreens")); + } + delete m_fontDb; delete m_desktopServices; + if (m_platformInputContext) + delete m_platformInputContext; +#if QT_CONFIG(accessibility) + delete m_accessibility; +#endif + + for (const auto &elementAndScreen : m_screens) + elementAndScreen.wasmScreen->deleteScreen(); - for (const auto &canvasAndScreen : m_screens) - QWindowSystemInterface::handleScreenRemoved(canvasAndScreen.second); m_screens.clear(); s_instance = nullptr; } -void QWasmIntegration::QWasmBrowserExit() -{ - QCoreApplication *app = QCoreApplication::instance(); - app->quit(); -} - bool QWasmIntegration::hasCapability(QPlatformIntegration::Capability cap) const { switch (cap) { @@ -186,20 +187,18 @@ bool QWasmIntegration::hasCapability(QPlatformIntegration::Capability cap) const QPlatformWindow *QWasmIntegration::createPlatformWindow(QWindow *window) const { - QWasmCompositor *compositor = QWasmScreen::get(window->screen())->compositor(); - return new QWasmWindow(window, compositor, m_backingStores.value(window)); + auto *wasmScreen = QWasmScreen::get(window->screen()); + QWasmCompositor *compositor = wasmScreen->compositor(); + return new QWasmWindow(window, wasmScreen->deadKeySupport(), compositor, + m_backingStores.value(window)); } QPlatformBackingStore *QWasmIntegration::createPlatformBackingStore(QWindow *window) const { -#ifndef QT_NO_OPENGL QWasmCompositor *compositor = QWasmScreen::get(window->screen())->compositor(); QWasmBackingStore *backingStore = new QWasmBackingStore(compositor, window); m_backingStores.insert(window, backingStore); return backingStore; -#else - return nullptr; -#endif } void QWasmIntegration::removeBackingStore(QWindow* window) @@ -207,18 +206,33 @@ void QWasmIntegration::removeBackingStore(QWindow* window) m_backingStores.remove(window); } +void QWasmIntegration::releaseRequesetUpdateHold() +{ + if (QWasmCompositor::releaseRequestUpdateHold()) + { + for (const auto &elementAndScreen : m_screens) { + elementAndScreen.wasmScreen->compositor()->requestUpdate(); + } + } +} + #ifndef QT_NO_OPENGL QPlatformOpenGLContext *QWasmIntegration::createPlatformOpenGLContext(QOpenGLContext *context) const { - return new QWasmOpenGLContext(context->format()); + return new QWasmOpenGLContext(context); } #endif void QWasmIntegration::initialize() { - QString icStr = QPlatformInputContextFactory::requested(); - if (!icStr.isNull()) - m_inputContext.reset(QPlatformInputContextFactory::create(icStr)); + auto icStrs = QPlatformInputContextFactory::requested(); + if (icStrs.isEmpty() && touchPoints < 1) + return; + + if (!icStrs.isEmpty()) + m_inputContext.reset(QPlatformInputContextFactory::create(icStrs)); + else + m_inputContext.reset(new QWasmInputContext()); } QPlatformInputContext *QWasmIntegration::inputContext() const @@ -228,7 +242,7 @@ QPlatformInputContext *QWasmIntegration::inputContext() const QPlatformOffscreenSurface *QWasmIntegration::createPlatformOffscreenSurface(QOffscreenSurface *surface) const { - return new QWasmOffscrenSurface(surface); + return new QWasmOffscreenSurface(surface); } QPlatformFontDatabase *QWasmIntegration::fontDatabase() const @@ -246,16 +260,20 @@ QAbstractEventDispatcher *QWasmIntegration::createEventDispatcher() const QVariant QWasmIntegration::styleHint(QPlatformIntegration::StyleHint hint) const { - if (hint == ShowIsFullScreen) + switch (hint) { + case ShowIsFullScreen: return true; - - return QPlatformIntegration::styleHint(hint); + case UnderlineShortcut: + return platform() != Platform::MacOS; + default: + return QPlatformIntegration::styleHint(hint); + } } Qt::WindowState QWasmIntegration::defaultWindowState(Qt::WindowFlags flags) const { - // Don't maximize dialogs - if (flags & Qt::Dialog & ~Qt::Window) + // Don't maximize dialogs or popups + if (flags.testFlag(Qt::Dialog) || flags.testFlag(Qt::Popup)) return Qt::WindowNoState; return QPlatformIntegration::defaultWindowState(flags); @@ -263,12 +281,12 @@ Qt::WindowState QWasmIntegration::defaultWindowState(Qt::WindowFlags flags) cons QStringList QWasmIntegration::themeNames() const { - return QStringList() << QLatin1String("webassembly"); + return QStringList() << "webassembly"_L1; } QPlatformTheme *QWasmIntegration::createPlatformTheme(const QString &name) const { - if (name == QLatin1String("webassembly")) + if (name == "webassembly"_L1) return new QWasmTheme; return QPlatformIntegration::createPlatformTheme(name); } @@ -285,37 +303,100 @@ QPlatformClipboard* QWasmIntegration::clipboard() const return m_clipboard; } -void QWasmIntegration::addScreen(const emscripten::val &canvas) +#ifndef QT_NO_ACCESSIBILITY +QPlatformAccessibility *QWasmIntegration::accessibility() const { - QWasmScreen *screen = new QWasmScreen(canvas); - m_screens.append(qMakePair(canvas, screen)); - m_clipboard->installEventHandlers(canvas); + return m_accessibility; +} +#endif + +void QWasmIntegration::setContainerElements(emscripten::val elementArray) +{ + const auto *primaryScreenBefore = m_screens.isEmpty() ? nullptr : m_screens[0].wasmScreen; + QList<ScreenMapping> newScreens; + + QList<QWasmScreen *> screensToDelete; + std::transform(m_screens.begin(), m_screens.end(), std::back_inserter(screensToDelete), + [](const ScreenMapping &mapping) { return mapping.wasmScreen; }); + + for (int i = 0; i < elementArray["length"].as<int>(); ++i) { + const auto element = elementArray[i]; + const auto it = std::find_if( + m_screens.begin(), m_screens.end(), + [&element](const ScreenMapping &screen) { return screen.emscriptenVal == element; }); + QWasmScreen *screen; + if (it != m_screens.end()) { + screen = it->wasmScreen; + screensToDelete.erase(std::remove_if(screensToDelete.begin(), screensToDelete.end(), + [screen](const QWasmScreen *removedScreen) { + return removedScreen == screen; + }), + screensToDelete.end()); + } else { + screen = new QWasmScreen(element); + QWindowSystemInterface::handleScreenAdded(screen); + } + newScreens.push_back({element, screen}); + } + + std::for_each(screensToDelete.begin(), screensToDelete.end(), + [](QWasmScreen *removed) { removed->deleteScreen(); }); + + m_screens = newScreens; + auto *primaryScreenAfter = m_screens.isEmpty() ? nullptr : m_screens[0].wasmScreen; + if (primaryScreenAfter && primaryScreenAfter != primaryScreenBefore) + QWindowSystemInterface::handlePrimaryScreenChanged(primaryScreenAfter); +} + +void QWasmIntegration::addContainerElement(emscripten::val element) +{ + Q_ASSERT_X(m_screens.end() + == std::find_if(m_screens.begin(), m_screens.end(), + [&element](const ScreenMapping &screen) { + return screen.emscriptenVal == element; + }), + Q_FUNC_INFO, "Double-add of an element"); + + QWasmScreen *screen = new QWasmScreen(element); QWindowSystemInterface::handleScreenAdded(screen); + m_screens.push_back({element, screen}); } -void QWasmIntegration::removeScreen(const emscripten::val &canvas) +void QWasmIntegration::removeContainerElement(emscripten::val element) { - auto it = std::find_if(m_screens.begin(), m_screens.end(), - [&] (const QPair<emscripten::val, QWasmScreen *> &candidate) { return candidate.first.equals(canvas); }); + const auto *primaryScreenBefore = m_screens.isEmpty() ? nullptr : m_screens[0].wasmScreen; + + const auto it = + std::find_if(m_screens.begin(), m_screens.end(), + [&element](const ScreenMapping &screen) { return screen.emscriptenVal == element; }); if (it == m_screens.end()) { - qWarning() << "Attempting to remove non-existing screen for canvas" << QWasmString::toQString(canvas["id"]);; + qWarning() << "Attempt to remove a nonexistent screen."; return; } - QWasmScreen *exScreen = it->second; - m_screens.erase(it); - exScreen->destroy(); // clean up before deleting the screen - QWindowSystemInterface::handleScreenRemoved(exScreen); + + QWasmScreen *removedScreen = it->wasmScreen; + removedScreen->deleteScreen(); + + m_screens.erase(std::remove_if(m_screens.begin(), m_screens.end(), + [removedScreen](const ScreenMapping &mapping) { + return removedScreen == mapping.wasmScreen; + }), + m_screens.end()); + auto *primaryScreenAfter = m_screens.isEmpty() ? nullptr : m_screens[0].wasmScreen; + if (primaryScreenAfter && primaryScreenAfter != primaryScreenBefore) + QWindowSystemInterface::handlePrimaryScreenChanged(primaryScreenAfter); } -void QWasmIntegration::resizeScreen(const emscripten::val &canvas) +void QWasmIntegration::resizeScreen(const emscripten::val &element) { auto it = std::find_if(m_screens.begin(), m_screens.end(), - [&] (const QPair<emscripten::val, QWasmScreen *> &candidate) { return candidate.first.equals(canvas); }); + [&] (const ScreenMapping &candidate) { return candidate.emscriptenVal.equals(element); }); if (it == m_screens.end()) { - qWarning() << "Attempting to resize non-existing screen for canvas" << QWasmString::toQString(canvas["id"]);; + qWarning() << "Attempting to resize non-existing screen for element" + << QString::fromEcmaString(element["id"]); return; } - it->second->updateQScreenAndCanvasRenderSize(); + it->wasmScreen->updateQScreenAndCanvasRenderSize(); } void QWasmIntegration::updateDpi() @@ -324,14 +405,31 @@ void QWasmIntegration::updateDpi() if (dpi.isUndefined()) return; qreal dpiValue = dpi.as<qreal>(); - for (const auto &canvasAndScreen : m_screens) - QWindowSystemInterface::handleScreenLogicalDotsPerInchChange(canvasAndScreen.second->screen(), dpiValue, dpiValue); + for (const auto &elementAndScreen : m_screens) + QWindowSystemInterface::handleScreenLogicalDotsPerInchChange(elementAndScreen.wasmScreen->screen(), dpiValue, dpiValue); } void QWasmIntegration::resizeAllScreens() { - for (const auto &canvasAndScreen : m_screens) - canvasAndScreen.second->updateQScreenAndCanvasRenderSize(); + for (const auto &elementAndScreen : m_screens) + elementAndScreen.wasmScreen->updateQScreenAndCanvasRenderSize(); +} + +void QWasmIntegration::loadLocalFontFamilies(emscripten::val families) +{ + m_fontDb->populateLocalFontFamilies(families); +} + +quint64 QWasmIntegration::getTimestamp() +{ + return emscripten_performance_now(); +} + +#if QT_CONFIG(draganddrop) +QPlatformDrag *QWasmIntegration::drag() const +{ + return m_drag.get(); } +#endif // QT_CONFIG(draganddrop) QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmintegration.h b/src/plugins/platforms/wasm/qwasmintegration.h index f527053489..870bd0d16b 100644 --- a/src/plugins/platforms/wasm/qwasmintegration.h +++ b/src/plugins/platforms/wasm/qwasmintegration.h @@ -1,43 +1,21 @@ -/**************************************************************************** -** -** 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$ -** -****************************************************************************/ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #ifndef QWASMINTEGRATION_H #define QWASMINTEGRATION_H #include "qwasmwindow.h" +#include "qwasminputcontext.h" + #include <qpa/qplatformintegration.h> #include <qpa/qplatformscreen.h> #include <qpa/qplatforminputcontext.h> #include <QtCore/qhash.h> +#include <private/qstdweb_p.h> + #include <emscripten.h> #include <emscripten/html5.h> #include <emscripten/val.h> @@ -52,7 +30,9 @@ class QWasmScreen; class QWasmCompositor; class QWasmBackingStore; class QWasmClipboard; +class QWasmAccessibility; class QWasmServices; +class QWasmDrag; class QWasmIntegration : public QObject, public QPlatformIntegration { @@ -76,30 +56,56 @@ public: QPlatformTheme *createPlatformTheme(const QString &name) const override; QPlatformServices *services() const override; QPlatformClipboard *clipboard() const override; +#ifndef QT_NO_ACCESSIBILITY + QPlatformAccessibility *accessibility() const override; +#endif void initialize() override; QPlatformInputContext *inputContext() const override; - QWasmClipboard *getWasmClipboard() { return m_clipboard; } +#if QT_CONFIG(draganddrop) + QPlatformDrag *drag() const override; +#endif + QWasmClipboard *getWasmClipboard() { return m_clipboard; } + QWasmInputContext *getWasmInputContext() { return m_platformInputContext; } static QWasmIntegration *get() { return s_instance; } - static void QWasmBrowserExit(); - void addScreen(const emscripten::val &canvas); - void removeScreen(const emscripten::val &canvas); + void setContainerElements(emscripten::val elementArray); + void addContainerElement(emscripten::val elementArray); + void removeContainerElement(emscripten::val elementArray); void resizeScreen(const emscripten::val &canvas); - void resizeAllScreens(); void updateDpi(); + void resizeAllScreens(); + void loadLocalFontFamilies(emscripten::val families); void removeBackingStore(QWindow* window); + void releaseRequesetUpdateHold(); + static quint64 getTimestamp(); + + int touchPoints; private: + struct ScreenMapping { + emscripten::val emscriptenVal; + QWasmScreen *wasmScreen; + }; + mutable QWasmFontDatabase *m_fontDb; mutable QWasmServices *m_desktopServices; mutable QHash<QWindow *, QWasmBackingStore *> m_backingStores; - QList<QPair<emscripten::val, QWasmScreen *>> m_screens; + QList<ScreenMapping> m_screens; mutable QWasmClipboard *m_clipboard; + mutable QWasmAccessibility *m_accessibility; + qreal m_fontDpi = -1; mutable QScopedPointer<QPlatformInputContext> m_inputContext; static QWasmIntegration *s_instance; + + mutable QWasmInputContext *m_platformInputContext = nullptr; + +#if QT_CONFIG(draganddrop) + std::unique_ptr<QWasmDrag> m_drag; +#endif + }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmkeytranslator.cpp b/src/plugins/platforms/wasm/qwasmkeytranslator.cpp new file mode 100644 index 0000000000..8f5240d2d0 --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmkeytranslator.cpp @@ -0,0 +1,295 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "qwasmkeytranslator.h" +#include "qwasmevent.h" + +#include <QtCore/private/qmakearray_p.h> +#include <QtCore/qglobal.h> +#include <QtCore/qobject.h> + +#include <algorithm> + +QT_BEGIN_NAMESPACE + +namespace { +struct WebKb2QtData +{ + static constexpr char StringTerminator = '\0'; + + const char *web; + unsigned int qt; + + constexpr bool operator<=(const WebKb2QtData &that) const noexcept + { + return !(strcmp(that) > 0); + } + + bool operator<(const WebKb2QtData &that) const noexcept { return ::strcmp(web, that.web) < 0; } + + constexpr bool operator==(const WebKb2QtData &that) const noexcept { return strcmp(that) == 0; } + + constexpr int strcmp(const WebKb2QtData &that, const int i = 0) const + { + return web[i] == StringTerminator && that.web[i] == StringTerminator ? 0 + : web[i] == StringTerminator ? -1 + : that.web[i] == StringTerminator ? 1 + : web[i] < that.web[i] ? -1 + : web[i] > that.web[i] ? 1 + : strcmp(that, i + 1); + } +}; + +template<unsigned int Qt, char... WebChar> +struct Web2Qt +{ + static constexpr const char storage[sizeof...(WebChar) + 1] = { WebChar..., '\0' }; + using Type = WebKb2QtData; + static constexpr Type data() noexcept { return Type{ storage, Qt }; } +}; + +template<unsigned int Qt, char... WebChar> +constexpr char Web2Qt<Qt, WebChar...>::storage[]; + +static constexpr const auto WebToQtKeyCodeMappings = qMakeArray( + QSortedData<Web2Qt<Qt::Key_Alt, 'A', 'l', 't', 'L', 'e', 'f', 't'>, + Web2Qt<Qt::Key_Alt, 'A', 'l', 't'>, + Web2Qt<Qt::Key_AltGr, 'A', 'l', 't', 'R', 'i', 'g', 'h', 't'>, + Web2Qt<Qt::Key_Apostrophe, 'Q', 'u', 'o', 't', 'e'>, + Web2Qt<Qt::Key_Backspace, 'B', 'a', 'c', 'k', 's', 'p', 'a', 'c', 'e'>, + Web2Qt<Qt::Key_CapsLock, 'C', 'a', 'p', 's', 'L', 'o', 'c', 'k'>, + Web2Qt<Qt::Key_Control, 'C', 'o', 'n', 't', 'r', 'o', 'l'>, + Web2Qt<Qt::Key_Delete, 'D', 'e', 'l', 'e', 't', 'e'>, + Web2Qt<Qt::Key_Down, 'A', 'r', 'r', 'o', 'w', 'D', 'o', 'w', 'n'>, + Web2Qt<Qt::Key_Escape, 'E', 's', 'c', 'a', 'p', 'e'>, + Web2Qt<Qt::Key_F1, 'F', '1'>, Web2Qt<Qt::Key_F2, 'F', '2'>, + Web2Qt<Qt::Key_F11, 'F', '1', '1'>, Web2Qt<Qt::Key_F12, 'F', '1', '2'>, + Web2Qt<Qt::Key_F13, 'F', '1', '3'>, Web2Qt<Qt::Key_F14, 'F', '1', '4'>, + Web2Qt<Qt::Key_F15, 'F', '1', '5'>, Web2Qt<Qt::Key_F16, 'F', '1', '6'>, + Web2Qt<Qt::Key_F17, 'F', '1', '7'>, Web2Qt<Qt::Key_F18, 'F', '1', '8'>, + Web2Qt<Qt::Key_F19, 'F', '1', '9'>, Web2Qt<Qt::Key_F20, 'F', '2', '0'>, + Web2Qt<Qt::Key_F21, 'F', '2', '1'>, Web2Qt<Qt::Key_F22, 'F', '2', '2'>, + Web2Qt<Qt::Key_F23, 'F', '2', '3'>, + Web2Qt<Qt::Key_F3, 'F', '3'>, Web2Qt<Qt::Key_F4, 'F', '4'>, + Web2Qt<Qt::Key_F5, 'F', '5'>, Web2Qt<Qt::Key_F6, 'F', '6'>, + Web2Qt<Qt::Key_F7, 'F', '7'>, Web2Qt<Qt::Key_F8, 'F', '8'>, + Web2Qt<Qt::Key_F9, 'F', '9'>, Web2Qt<Qt::Key_F10, 'F', '1', '0'>, + Web2Qt<Qt::Key_Help, 'H', 'e', 'l', 'p'>, + Web2Qt<Qt::Key_Home, 'H', 'o', 'm', 'e'>, Web2Qt<Qt::Key_End, 'E', 'n', 'd'>, + Web2Qt<Qt::Key_Insert, 'I', 'n', 's', 'e', 'r', 't'>, + Web2Qt<Qt::Key_Left, 'A', 'r', 'r', 'o', 'w', 'L', 'e', 'f', 't'>, + Web2Qt<Qt::Key_Meta, 'M', 'e', 't', 'a'>, Web2Qt<Qt::Key_Meta, 'O', 'S'>, + Web2Qt<Qt::Key_Menu, 'C', 'o', 'n', 't', 'e', 'x', 't', 'M', 'e', 'n', 'u'>, + Web2Qt<Qt::Key_NumLock, 'N', 'u', 'm', 'L', 'o', 'c', 'k'>, + Web2Qt<Qt::Key_PageDown, 'P', 'a', 'g', 'e', 'D', 'o', 'w', 'n'>, + Web2Qt<Qt::Key_PageUp, 'P', 'a', 'g', 'e', 'U', 'p'>, + Web2Qt<Qt::Key_Paste, 'P', 'a', 's', 't', 'e'>, + Web2Qt<Qt::Key_Pause, 'C', 'l', 'e', 'a', 'r'>, + Web2Qt<Qt::Key_Pause, 'P', 'a', 'u', 's', 'e'>, + Web2Qt<Qt::Key_QuoteLeft, 'B', 'a', 'c', 'k', 'q', 'u', 'o', 't', 'e'>, + Web2Qt<Qt::Key_QuoteLeft, 'I', 'n', 't', 'l', 'B', 'a', 'c', 'k', 's', 'l', 'a', 's', 'h'>, + Web2Qt<Qt::Key_Return, 'E', 'n', 't', 'e', 'r'>, + Web2Qt<Qt::Key_Right, 'A', 'r', 'r', 'o', 'w', 'R', 'i', 'g', 'h', 't'>, + Web2Qt<Qt::Key_ScrollLock, 'S', 'c', 'r', 'o', 'l', 'l', 'L', 'o', 'c', 'k'>, + Web2Qt<Qt::Key_Shift, 'S', 'h', 'i', 'f', 't'>, + Web2Qt<Qt::Key_Tab, 'T', 'a', 'b'>, + Web2Qt<Qt::Key_Up, 'A', 'r', 'r', 'o', 'w', 'U', 'p'>, + Web2Qt<Qt::Key_yen, 'I', 'n', 't', 'l', 'Y', 'e', 'n'>>::Data{}); + +static constexpr const auto DiacriticalCharsKeyToTextLowercase = qMakeArray( + QSortedData< + Web2Qt<Qt::Key_Aacute, '\xc3', '\xa1'>, + Web2Qt<Qt::Key_Acircumflex, '\xc3', '\xa2'>, + Web2Qt<Qt::Key_Adiaeresis, '\xc3', '\xa4'>, + Web2Qt<Qt::Key_AE, '\xc3', '\xa6'>, + Web2Qt<Qt::Key_Agrave, '\xc3', '\xa0'>, + Web2Qt<Qt::Key_Aring, '\xc3', '\xa5'>, + Web2Qt<Qt::Key_Atilde, '\xc3', '\xa3'>, + Web2Qt<Qt::Key_Ccedilla, '\xc3', '\xa7'>, + Web2Qt<Qt::Key_Eacute, '\xc3', '\xa9'>, + Web2Qt<Qt::Key_Ecircumflex, '\xc3', '\xaa'>, + Web2Qt<Qt::Key_Ediaeresis, '\xc3', '\xab'>, + Web2Qt<Qt::Key_Egrave, '\xc3', '\xa8'>, + Web2Qt<Qt::Key_Iacute, '\xc3', '\xad'>, + Web2Qt<Qt::Key_Icircumflex, '\xc3', '\xae'>, + Web2Qt<Qt::Key_Idiaeresis, '\xc3', '\xaf'>, + Web2Qt<Qt::Key_Igrave, '\xc3', '\xac'>, + Web2Qt<Qt::Key_Ntilde, '\xc3', '\xb1'>, + Web2Qt<Qt::Key_Oacute, '\xc3', '\xb3'>, + Web2Qt<Qt::Key_Ocircumflex, '\xc3', '\xb4'>, + Web2Qt<Qt::Key_Odiaeresis, '\xc3', '\xb6'>, + Web2Qt<Qt::Key_Ograve, '\xc3', '\xb2'>, + Web2Qt<Qt::Key_Ooblique, '\xc3', '\xb8'>, + Web2Qt<Qt::Key_Otilde, '\xc3', '\xb5'>, + Web2Qt<Qt::Key_Uacute, '\xc3', '\xba'>, + Web2Qt<Qt::Key_Ucircumflex, '\xc3', '\xbb'>, + Web2Qt<Qt::Key_Udiaeresis, '\xc3', '\xbc'>, + Web2Qt<Qt::Key_Ugrave, '\xc3', '\xb9'>, + Web2Qt<Qt::Key_Yacute, '\xc3', '\xbd'>, + Web2Qt<Qt::Key_ydiaeresis, '\xc3', '\xbf'>>::Data{}); + +static constexpr const auto DiacriticalCharsKeyToTextUppercase = qMakeArray( + QSortedData< + Web2Qt<Qt::Key_Aacute, '\xc3', '\x81'>, + Web2Qt<Qt::Key_Acircumflex, '\xc3', '\x82'>, + Web2Qt<Qt::Key_Adiaeresis, '\xc3', '\x84'>, + Web2Qt<Qt::Key_AE, '\xc3', '\x86'>, + Web2Qt<Qt::Key_Agrave, '\xc3', '\x80'>, + Web2Qt<Qt::Key_Aring, '\xc3', '\x85'>, + Web2Qt<Qt::Key_Atilde, '\xc3', '\x83'>, + Web2Qt<Qt::Key_Ccedilla, '\xc3', '\x87'>, + Web2Qt<Qt::Key_Eacute, '\xc3', '\x89'>, + Web2Qt<Qt::Key_Ecircumflex, '\xc3', '\x8a'>, + Web2Qt<Qt::Key_Ediaeresis, '\xc3', '\x8b'>, + Web2Qt<Qt::Key_Egrave, '\xc3', '\x88'>, + Web2Qt<Qt::Key_Iacute, '\xc3', '\x8d'>, + Web2Qt<Qt::Key_Icircumflex, '\xc3', '\x8e'>, + Web2Qt<Qt::Key_Idiaeresis, '\xc3', '\x8f'>, + Web2Qt<Qt::Key_Igrave, '\xc3', '\x8c'>, + Web2Qt<Qt::Key_Ntilde, '\xc3', '\x91'>, + Web2Qt<Qt::Key_Oacute, '\xc3', '\x93'>, + Web2Qt<Qt::Key_Ocircumflex, '\xc3', '\x94'>, + Web2Qt<Qt::Key_Odiaeresis, '\xc3', '\x96'>, + Web2Qt<Qt::Key_Ograve, '\xc3', '\x92'>, + Web2Qt<Qt::Key_Ooblique, '\xc3', '\x98'>, + Web2Qt<Qt::Key_Otilde, '\xc3', '\x95'>, + Web2Qt<Qt::Key_Uacute, '\xc3', '\x9a'>, + Web2Qt<Qt::Key_Ucircumflex, '\xc3', '\x9b'>, + Web2Qt<Qt::Key_Udiaeresis, '\xc3', '\x9c'>, + Web2Qt<Qt::Key_Ugrave, '\xc3', '\x99'>, + Web2Qt<Qt::Key_Yacute, '\xc3', '\x9d'>, + Web2Qt<Qt::Key_ydiaeresis, '\xc5', '\xb8'>>::Data{}); + +static_assert(DiacriticalCharsKeyToTextLowercase.size() + == DiacriticalCharsKeyToTextUppercase.size(), + "Add the new key to both arrays"); + +struct KeyMapping +{ + Qt::Key from, to; +}; + +constexpr KeyMapping tildeKeyTable[] = { + // ~ + { Qt::Key_A, Qt::Key_Atilde }, + { Qt::Key_N, Qt::Key_Ntilde }, + { Qt::Key_O, Qt::Key_Otilde }, +}; +constexpr KeyMapping graveKeyTable[] = { + // ` + { Qt::Key_A, Qt::Key_Agrave }, { Qt::Key_E, Qt::Key_Egrave }, { Qt::Key_I, Qt::Key_Igrave }, + { Qt::Key_O, Qt::Key_Ograve }, { Qt::Key_U, Qt::Key_Ugrave }, +}; +constexpr KeyMapping acuteKeyTable[] = { + // ' + { Qt::Key_A, Qt::Key_Aacute }, { Qt::Key_E, Qt::Key_Eacute }, { Qt::Key_I, Qt::Key_Iacute }, + { Qt::Key_O, Qt::Key_Oacute }, { Qt::Key_U, Qt::Key_Uacute }, { Qt::Key_Y, Qt::Key_Yacute }, +}; +constexpr KeyMapping diaeresisKeyTable[] = { + // umlaut ¨ + { Qt::Key_A, Qt::Key_Adiaeresis }, { Qt::Key_E, Qt::Key_Ediaeresis }, + { Qt::Key_I, Qt::Key_Idiaeresis }, { Qt::Key_O, Qt::Key_Odiaeresis }, + { Qt::Key_U, Qt::Key_Udiaeresis }, { Qt::Key_Y, Qt::Key_ydiaeresis }, +}; +constexpr KeyMapping circumflexKeyTable[] = { + // ^ + { Qt::Key_A, Qt::Key_Acircumflex }, { Qt::Key_E, Qt::Key_Ecircumflex }, + { Qt::Key_I, Qt::Key_Icircumflex }, { Qt::Key_O, Qt::Key_Ocircumflex }, + { Qt::Key_U, Qt::Key_Ucircumflex }, +}; + +static Qt::Key find_impl(const KeyMapping *first, const KeyMapping *last, Qt::Key key) noexcept +{ + while (first != last) { + if (first->from == key) + return first->to; + ++first; + } + return Qt::Key_unknown; +} + +template<size_t N> +static Qt::Key find(const KeyMapping (&map)[N], Qt::Key key) noexcept +{ + return find_impl(map, map + N, key); +} + +Qt::Key translateBaseKeyUsingDeadKey(Qt::Key accentBaseKey, Qt::Key deadKey) +{ + switch (deadKey) { + case Qt::Key_Dead_Grave: + return find(graveKeyTable, accentBaseKey); + case Qt::Key_Dead_Acute: + return find(acuteKeyTable, accentBaseKey); + case Qt::Key_Dead_Tilde: + return find(tildeKeyTable, accentBaseKey); + case Qt::Key_Dead_Diaeresis: + return find(diaeresisKeyTable, accentBaseKey); + case Qt::Key_Dead_Circumflex: + return find(circumflexKeyTable, accentBaseKey); + default: + return Qt::Key_unknown; + }; +} + +template<class T> +std::optional<QString> findKeyTextByKeyId(const T &mappingArray, Qt::Key qtKey) +{ + const auto it = std::find_if(mappingArray.cbegin(), mappingArray.cend(), + [qtKey](const WebKb2QtData &data) { return data.qt == qtKey; }); + return it != mappingArray.cend() ? it->web : std::optional<QString>(); +} +} // namespace + +std::optional<Qt::Key> QWasmKeyTranslator::mapWebKeyTextToQtKey(const char *toFind) +{ + const WebKb2QtData searchKey{ toFind, 0 }; + const auto it = std::lower_bound(WebToQtKeyCodeMappings.cbegin(), WebToQtKeyCodeMappings.cend(), + searchKey); + return it != WebToQtKeyCodeMappings.cend() && searchKey == *it ? static_cast<Qt::Key>(it->qt) + : std::optional<Qt::Key>(); +} + +QWasmDeadKeySupport::QWasmDeadKeySupport() = default; + +QWasmDeadKeySupport::~QWasmDeadKeySupport() = default; + +void QWasmDeadKeySupport::applyDeadKeyTranslations(KeyEvent *event) +{ + if (event->deadKey) { + m_activeDeadKey = event->key; + } else if (m_activeDeadKey != Qt::Key_unknown + && (((m_keyModifiedByDeadKeyOnPress == Qt::Key_unknown + && event->type == EventType::KeyDown)) + || (m_keyModifiedByDeadKeyOnPress == event->key + && event->type == EventType::KeyUp))) { + const Qt::Key baseKey = event->key; + const Qt::Key translatedKey = translateBaseKeyUsingDeadKey(baseKey, m_activeDeadKey); + if (translatedKey != Qt::Key_unknown) { + event->key = translatedKey; + + auto foundText = event->modifiers.testFlag(Qt::ShiftModifier) + ? findKeyTextByKeyId(DiacriticalCharsKeyToTextUppercase, event->key) + : findKeyTextByKeyId(DiacriticalCharsKeyToTextLowercase, event->key); + Q_ASSERT(foundText.has_value()); + event->text = foundText->size() == 1 ? *foundText : QString(); + } + + if (!event->text.isEmpty()) { + if (event->type == EventType::KeyDown) { + // Assume the first keypress with an active dead key is treated as modified, + // regardless of whether it has actually been modified or not. Take into account + // only events that produce actual key text. + if (!event->text.isEmpty()) + m_keyModifiedByDeadKeyOnPress = baseKey; + } else { + Q_ASSERT(event->type == EventType::KeyUp); + Q_ASSERT(m_keyModifiedByDeadKeyOnPress == baseKey); + m_keyModifiedByDeadKeyOnPress = Qt::Key_unknown; + m_activeDeadKey = Qt::Key_unknown; + } + } + } +} + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmkeytranslator.h b/src/plugins/platforms/wasm/qwasmkeytranslator.h new file mode 100644 index 0000000000..11a89e6193 --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmkeytranslator.h @@ -0,0 +1,34 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef QWASMKEYTRANSLATOR_H +#define QWASMKEYTRANSLATOR_H + +#include <QtCore/qnamespace.h> +#include <QtCore/qtypes.h> + +#include <optional> + +QT_BEGIN_NAMESPACE + +struct KeyEvent; + +namespace QWasmKeyTranslator { +std::optional<Qt::Key> mapWebKeyTextToQtKey(const char *toFind); +} + +class QWasmDeadKeySupport +{ +public: + explicit QWasmDeadKeySupport(); + ~QWasmDeadKeySupport(); + + void applyDeadKeyTranslations(KeyEvent *event); + +private: + Qt::Key m_activeDeadKey = Qt::Key_unknown; + Qt::Key m_keyModifiedByDeadKeyOnPress = Qt::Key_unknown; +}; + +QT_END_NAMESPACE +#endif // QWASMKEYTRANSLATOR_H diff --git a/src/plugins/platforms/wasm/qwasmoffscreensurface.cpp b/src/plugins/platforms/wasm/qwasmoffscreensurface.cpp index a205e5ddea..dcfc4433e6 100644 --- a/src/plugins/platforms/wasm/qwasmoffscreensurface.cpp +++ b/src/plugins/platforms/wasm/qwasmoffscreensurface.cpp @@ -1,41 +1,35 @@ -/**************************************************************************** -** -** Copyright (C) 2019 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$ -** -****************************************************************************/ +// Copyright (C) 2019 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "qwasmoffscreensurface.h" -QWasmOffscrenSurface::QWasmOffscrenSurface(QOffscreenSurface *offscreenSurface) - :QPlatformOffscreenSurface(offscreenSurface) +QT_BEGIN_NAMESPACE + +QWasmOffscreenSurface::QWasmOffscreenSurface(QOffscreenSurface *offscreenSurface) + : QPlatformOffscreenSurface(offscreenSurface), m_offscreenCanvas(emscripten::val::undefined()) { + const auto offscreenCanvasClass = emscripten::val::global("OffscreenCanvas"); + // The OffscreenCanvas is not supported on some browsers, most notably on Safari. + if (!offscreenCanvasClass) + return; + + m_offscreenCanvas = offscreenCanvasClass.new_(offscreenSurface->size().width(), + offscreenSurface->size().height()); + + m_specialTargetId = std::string("!qtoffscreen_") + std::to_string(uintptr_t(this)); + emscripten::val::module_property("specialHTMLTargets") + .set(m_specialTargetId, m_offscreenCanvas); } -QWasmOffscrenSurface::~QWasmOffscrenSurface() +QWasmOffscreenSurface::~QWasmOffscreenSurface() { + emscripten::val::module_property("specialHTMLTargets").delete_(m_specialTargetId); +} +bool QWasmOffscreenSurface::isValid() const +{ + return !m_offscreenCanvas.isNull() && !m_offscreenCanvas.isUndefined(); } + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmoffscreensurface.h b/src/plugins/platforms/wasm/qwasmoffscreensurface.h index 9d3e805be0..1c71310448 100644 --- a/src/plugins/platforms/wasm/qwasmoffscreensurface.h +++ b/src/plugins/platforms/wasm/qwasmoffscreensurface.h @@ -1,47 +1,30 @@ -/**************************************************************************** -** -** Copyright (C) 2019 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$ -** -****************************************************************************/ +// Copyright (C) 2019 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #ifndef QWASMOFFSCREENSURFACE_H #define QWASMOFFSCREENSURFACE_H #include <qpa/qplatformoffscreensurface.h> +#include <emscripten/val.h> + +#include <string> + QT_BEGIN_NAMESPACE class QOffscreenSurface; -class QWasmOffscrenSurface : public QPlatformOffscreenSurface +class QWasmOffscreenSurface final : public QPlatformOffscreenSurface { public: - explicit QWasmOffscrenSurface(QOffscreenSurface *offscreenSurface); - ~QWasmOffscrenSurface(); -private: + explicit QWasmOffscreenSurface(QOffscreenSurface *offscreenSurface); + ~QWasmOffscreenSurface() final; + const std::string &id() const { return m_specialTargetId; } + bool isValid() const override; + +private: + std::string m_specialTargetId; + emscripten::val m_offscreenCanvas; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmopenglcontext.cpp b/src/plugins/platforms/wasm/qwasmopenglcontext.cpp index c122335a57..8a4664ec8c 100644 --- a/src/plugins/platforms/wasm/qwasmopenglcontext.cpp +++ b/src/plugins/platforms/wasm/qwasmopenglcontext.cpp @@ -1,65 +1,44 @@ -/**************************************************************************** -** -** 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$ -** -****************************************************************************/ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "qwasmopenglcontext.h" + +#include "qwasmoffscreensurface.h" #include "qwasmintegration.h" #include <EGL/egl.h> +#include <emscripten/bind.h> #include <emscripten/val.h> +namespace { +void qtDoNothing(emscripten::val) { } +} // namespace + +EMSCRIPTEN_BINDINGS(qwasmopenglcontext) +{ + function("qtDoNothing", &qtDoNothing); +} + QT_BEGIN_NAMESPACE -QWasmOpenGLContext::QWasmOpenGLContext(const QSurfaceFormat &format) - : m_requestedFormat(format) +QWasmOpenGLContext::QWasmOpenGLContext(QOpenGLContext *context) + : m_actualFormat(context->format()), m_qGlContext(context) { - m_requestedFormat.setRenderableType(QSurfaceFormat::OpenGLES); + m_actualFormat.setRenderableType(QSurfaceFormat::OpenGLES); // if we set one, we need to set the other as well since in webgl, these are tied together - if (format.depthBufferSize() < 0 && format.stencilBufferSize() > 0) - m_requestedFormat.setDepthBufferSize(16); - - if (format.stencilBufferSize() < 0 && format.depthBufferSize() > 0) - m_requestedFormat.setStencilBufferSize(8); + if (m_actualFormat.depthBufferSize() < 0 && m_actualFormat.stencilBufferSize() > 0) + m_actualFormat.setDepthBufferSize(16); + if (m_actualFormat.stencilBufferSize() < 0 && m_actualFormat.depthBufferSize() > 0) + m_actualFormat.setStencilBufferSize(8); } QWasmOpenGLContext::~QWasmOpenGLContext() { - if (m_context) { - // Destroy GL context. Work around bug in emscripten_webgl_destroy_context - // which removes all event handlers on the canvas by temporarily removing - // emscripten's JSEvents global object. - emscripten::val jsEvents = emscripten::val::global("window")["JSEvents"]; - emscripten::val::global("window").set("JSEvents", emscripten::val::undefined()); - emscripten_webgl_destroy_context(m_context); - emscripten::val::global("window").set("JSEvents", jsEvents); - m_context = 0; - } + // Destroy GL context. Work around bug in emscripten_webgl_destroy_context + // which removes all event handlers on the canvas by temporarily replacing the function + // that does the removal with a function that does nothing. + destroyWebGLContext(m_ownedWebGLContext.handle); } bool QWasmOpenGLContext::isOpenGLVersionSupported(QSurfaceFormat format) @@ -72,28 +51,66 @@ bool QWasmOpenGLContext::isOpenGLVersionSupported(QSurfaceFormat format) (format.majorVersion() == 3 && format.minorVersion() == 0)); } -bool QWasmOpenGLContext::maybeCreateEmscriptenContext(QPlatformSurface *surface) +EMSCRIPTEN_WEBGL_CONTEXT_HANDLE +QWasmOpenGLContext::obtainEmscriptenContext(QPlatformSurface *surface) { - // Native emscripten/WebGL contexts are tied to a single screen/canvas. The first - // call to this function creates a native canvas for the given screen, subsequent - // calls verify that the surface is on/off the same screen. - QPlatformScreen *screen = surface->screen(); - if (m_context && !screen) - return false; // Alternative: return true to support makeCurrent on QOffScreenSurface with - // no screen. However, Qt likes to substitute QGuiApplication::primaryScreen() - // for null screens, which foils this plan. - if (!screen) - return false; - if (m_context) - return m_screen == screen; + if (m_ownedWebGLContext.surface == surface) + return m_ownedWebGLContext.handle; + + if (surface->surface()->surfaceClass() == QSurface::Offscreen) { + // Reuse the existing context for offscreen drawing, even if it happens to be a canvas + // context. This is because it is impossible to re-home an existing context to the + // new surface and works as an emulation measure. + if (m_ownedWebGLContext.handle) + return m_ownedWebGLContext.handle; + + // The non-shared offscreen context is heavily limited on WASM, but we provide it + // anyway for potential pixel readbacks. + m_ownedWebGLContext = + QOpenGLContextData{ .surface = surface, + .handle = createEmscriptenContext( + static_cast<QWasmOffscreenSurface *>(surface)->id(), + m_actualFormat) }; + } else { + destroyWebGLContext(m_ownedWebGLContext.handle); + + // Create a full on-screen context for the window canvas. + m_ownedWebGLContext = QOpenGLContextData{ + .surface = surface, + .handle = createEmscriptenContext(static_cast<QWasmWindow *>(surface)->canvasSelector(), + m_actualFormat) + }; + } + + EmscriptenWebGLContextAttributes actualAttributes; + + EMSCRIPTEN_RESULT attributesResult = emscripten_webgl_get_context_attributes(m_ownedWebGLContext.handle, &actualAttributes); + if (attributesResult == EMSCRIPTEN_RESULT_SUCCESS) { + if (actualAttributes.majorVersion == 1) { + m_actualFormat.setMajorVersion(2); + } else if (actualAttributes.majorVersion == 2) { + m_actualFormat.setMajorVersion(3); + } + m_actualFormat.setMinorVersion(0); + } + + return m_ownedWebGLContext.handle; +} - QString canvasId = QWasmScreen::get(screen)->canvasId(); - m_context = createEmscriptenContext(canvasId, m_requestedFormat); - m_screen = screen; - return true; +void QWasmOpenGLContext::destroyWebGLContext(EMSCRIPTEN_WEBGL_CONTEXT_HANDLE contextHandle) +{ + if (!contextHandle) + return; + emscripten::val jsEvents = emscripten::val::module_property("JSEvents"); + emscripten::val savedRemoveAllHandlersOnTargetFunction = jsEvents["removeAllHandlersOnTarget"]; + jsEvents.set("removeAllHandlersOnTarget", emscripten::val::module_property("qtDoNothing")); + emscripten_webgl_destroy_context(contextHandle); + jsEvents.set("removeAllHandlersOnTarget", savedRemoveAllHandlersOnTargetFunction); } -EMSCRIPTEN_WEBGL_CONTEXT_HANDLE QWasmOpenGLContext::createEmscriptenContext(const QString &canvasId, QSurfaceFormat format) +EMSCRIPTEN_WEBGL_CONTEXT_HANDLE +QWasmOpenGLContext::createEmscriptenContext(const std::string &canvasSelector, + QSurfaceFormat format) { EmscriptenWebGLContextAttributes attributes; emscripten_webgl_init_context_attributes(&attributes); // Populate with default attributes @@ -102,27 +119,30 @@ EMSCRIPTEN_WEBGL_CONTEXT_HANDLE QWasmOpenGLContext::createEmscriptenContext(cons attributes.failIfMajorPerformanceCaveat = false; attributes.antialias = true; attributes.enableExtensionsByDefault = true; - attributes.majorVersion = format.majorVersion() - 1; - attributes.minorVersion = format.minorVersion(); - + attributes.majorVersion = 2; // try highest supported version ES3.0 / WebGL 2.0 + attributes.minorVersion = 0; // emscripten only supports minor version 0 // WebGL doesn't allow separate attach buffers to STENCIL_ATTACHMENT and DEPTH_ATTACHMENT // we need both or none - bool useDepthStencil = (format.depthBufferSize() > 0 || format.stencilBufferSize() > 0); + const bool useDepthStencil = (format.depthBufferSize() > 0 || format.stencilBufferSize() > 0); // WebGL offers enable/disable control but not size control for these attributes.alpha = format.alphaBufferSize() > 0; attributes.depth = useDepthStencil; attributes.stencil = useDepthStencil; + EMSCRIPTEN_RESULT contextResult = emscripten_webgl_create_context(canvasSelector.c_str(), &attributes); - QByteArray convasSelector = "#" + canvasId.toUtf8(); - EMSCRIPTEN_WEBGL_CONTEXT_HANDLE context = emscripten_webgl_create_context(convasSelector.constData(), &attributes); - - return context; + if (contextResult <= 0) { + // fallback to opengles2/webgl1 + // for devices that do not support opengles3/webgl2 + attributes.majorVersion = 1; + contextResult = emscripten_webgl_create_context(canvasSelector.c_str(), &attributes); + } + return contextResult; } QSurfaceFormat QWasmOpenGLContext::format() const { - return m_requestedFormat; + return m_actualFormat; } GLuint QWasmOpenGLContext::defaultFramebufferObject(QPlatformSurface *surface) const @@ -132,11 +152,22 @@ GLuint QWasmOpenGLContext::defaultFramebufferObject(QPlatformSurface *surface) c bool QWasmOpenGLContext::makeCurrent(QPlatformSurface *surface) { - bool ok = maybeCreateEmscriptenContext(surface); - if (!ok) + static bool sentSharingWarning = false; + if (!sentSharingWarning && isSharing()) { + qWarning() << "The functionality for sharing OpenGL contexts is limited, see documentation"; + sentSharingWarning = true; + } + + if (auto *shareContext = m_qGlContext->shareContext()) + return shareContext->makeCurrent(surface->surface()); + + const auto context = obtainEmscriptenContext(surface); + if (!context) return false; - return emscripten_webgl_make_context_current(m_context) == EMSCRIPTEN_RESULT_SUCCESS; + m_usedWebGLContextHandle = context; + + return emscripten_webgl_make_context_current(context) == EMSCRIPTEN_RESULT_SUCCESS; } void QWasmOpenGLContext::swapBuffers(QPlatformSurface *surface) @@ -152,17 +183,17 @@ void QWasmOpenGLContext::doneCurrent() bool QWasmOpenGLContext::isSharing() const { - return false; + return m_qGlContext->shareContext(); } bool QWasmOpenGLContext::isValid() const { - if (!(isOpenGLVersionSupported(m_requestedFormat))) + if (!isOpenGLVersionSupported(m_actualFormat)) return false; // Note: we get isValid() calls before we see the surface and can // create a native context, so no context is also a valid state. - return !m_context || !emscripten_is_webgl_context_lost(m_context); + return !m_usedWebGLContextHandle || !emscripten_is_webgl_context_lost(m_usedWebGLContextHandle); } QFunctionPointer QWasmOpenGLContext::getProcAddress(const char *procName) diff --git a/src/plugins/platforms/wasm/qwasmopenglcontext.h b/src/plugins/platforms/wasm/qwasmopenglcontext.h index cf84379c36..2a8bcc5d9b 100644 --- a/src/plugins/platforms/wasm/qwasmopenglcontext.h +++ b/src/plugins/platforms/wasm/qwasmopenglcontext.h @@ -1,31 +1,8 @@ -/**************************************************************************** -** -** 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$ -** -****************************************************************************/ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef QWASMOPENGLCONTEXT_H +#define QWASMOPENGLCONTEXT_H #include <qpa/qplatformopenglcontext.h> @@ -34,11 +11,13 @@ QT_BEGIN_NAMESPACE +class QOpenGLContext; class QPlatformScreen; +class QPlatformSurface; class QWasmOpenGLContext : public QPlatformOpenGLContext { public: - QWasmOpenGLContext(const QSurfaceFormat &format); + explicit QWasmOpenGLContext(QOpenGLContext *context); ~QWasmOpenGLContext(); QSurfaceFormat format() const override; @@ -51,14 +30,25 @@ public: QFunctionPointer getProcAddress(const char *procName) override; private: + struct QOpenGLContextData + { + QPlatformSurface *surface = nullptr; + EMSCRIPTEN_WEBGL_CONTEXT_HANDLE handle = 0; + }; + static bool isOpenGLVersionSupported(QSurfaceFormat format); - bool maybeCreateEmscriptenContext(QPlatformSurface *surface); - static EMSCRIPTEN_WEBGL_CONTEXT_HANDLE createEmscriptenContext(const QString &canvasId, QSurfaceFormat format); + EMSCRIPTEN_WEBGL_CONTEXT_HANDLE obtainEmscriptenContext(QPlatformSurface *surface); + static EMSCRIPTEN_WEBGL_CONTEXT_HANDLE + createEmscriptenContext(const std::string &canvasSelector, QSurfaceFormat format); + + static void destroyWebGLContext(EMSCRIPTEN_WEBGL_CONTEXT_HANDLE contextHandle); - QSurfaceFormat m_requestedFormat; - QPlatformScreen *m_screen = nullptr; - EMSCRIPTEN_WEBGL_CONTEXT_HANDLE m_context = 0; + QSurfaceFormat m_actualFormat; + QOpenGLContext *m_qGlContext; + QOpenGLContextData m_ownedWebGLContext; + EMSCRIPTEN_WEBGL_CONTEXT_HANDLE m_usedWebGLContextHandle = 0; }; QT_END_NAMESPACE +#endif // QWASMOPENGLCONTEXT_H diff --git a/src/plugins/platforms/wasm/qwasmplatform.cpp b/src/plugins/platforms/wasm/qwasmplatform.cpp new file mode 100644 index 0000000000..e54992be1d --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmplatform.cpp @@ -0,0 +1,32 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "qwasmplatform.h" + +QT_BEGIN_NAMESPACE + +Platform platform() +{ + static const Platform qtDetectedPlatform = ([]() { + // The Platform Detect: expand coverage as needed + emscripten::val rawPlatform = emscripten::val::global("navigator")["platform"]; + + if (rawPlatform.call<bool>("includes", emscripten::val("Mac"))) + return Platform::MacOS; + if (rawPlatform.call<bool>("includes", emscripten::val("iPhone")) + || rawPlatform.call<bool>("includes", emscripten::val("iPad"))) + return Platform::iOS; + if (rawPlatform.call<bool>("includes", emscripten::val("Win32"))) + return Platform::Windows; + if (rawPlatform.call<bool>("includes", emscripten::val("Linux"))) { + emscripten::val uAgent = emscripten::val::global("navigator")["userAgent"]; + if (uAgent.call<bool>("includes", emscripten::val("Android"))) + return Platform::Android; + return Platform::Linux; + } + return Platform::Generic; + })(); + return qtDetectedPlatform; +} + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmplatform.h b/src/plugins/platforms/wasm/qwasmplatform.h new file mode 100644 index 0000000000..5b32e43633 --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmplatform.h @@ -0,0 +1,29 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef QWASMPLATFORM_H +#define QWASMPLATFORM_H + +#include <QtCore/qglobal.h> +#include <QtCore/qnamespace.h> + +#include <QPoint> + +#include <emscripten/val.h> + +QT_BEGIN_NAMESPACE + +enum class Platform { + Generic, + MacOS, + Windows, + Linux, + Android, + iOS +}; + +Platform platform(); + +QT_END_NAMESPACE + +#endif // QWASMPLATFORM_H diff --git a/src/plugins/platforms/wasm/qwasmscreen.cpp b/src/plugins/platforms/wasm/qwasmscreen.cpp index 7b1fc0d42e..0490b2bfe0 100644 --- a/src/plugins/platforms/wasm/qwasmscreen.cpp +++ b/src/plugins/platforms/wasm/qwasmscreen.cpp @@ -1,76 +1,126 @@ -/**************************************************************************** -** -** 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$ -** -****************************************************************************/ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "qwasmscreen.h" -#include "qwasmwindow.h" -#include "qwasmeventtranslator.h" + #include "qwasmcompositor.h" +#include "qwasmcssstyle.h" #include "qwasmintegration.h" -#include "qwasmstring.h" +#include "qwasmkeytranslator.h" +#include "qwasmwindow.h" #include <emscripten/bind.h> #include <emscripten/val.h> -#include <QtGui/private/qeglconvenience_p.h> -#ifndef QT_NO_OPENGL -# include <QtGui/private/qeglplatformcontext_p.h> -#endif #include <qpa/qwindowsysteminterface.h> #include <QtCore/qcoreapplication.h> #include <QtGui/qguiapplication.h> #include <private/qhighdpiscaling_p.h> -using namespace emscripten; +#include <tuple> QT_BEGIN_NAMESPACE -const char * QWasmScreen::m_canvasResizeObserverCallbackContextPropertyName = "data-qtCanvasResizeObserverCallbackContext"; +using namespace emscripten; + +const char *QWasmScreen::m_canvasResizeObserverCallbackContextPropertyName = + "data-qtCanvasResizeObserverCallbackContext"; -QWasmScreen::QWasmScreen(const emscripten::val &canvas) - : m_canvas(canvas) +QWasmScreen::QWasmScreen(const emscripten::val &containerOrCanvas) + : m_container(containerOrCanvas), + m_intermediateContainer(emscripten::val::undefined()), + m_shadowContainer(emscripten::val::undefined()), + m_compositor(new QWasmCompositor(this)), + m_deadKeySupport(std::make_unique<QWasmDeadKeySupport>()) { - m_compositor = new QWasmCompositor(this); - m_eventTranslator = new QWasmEventTranslator(this); - installCanvasResizeObserver(); + auto document = m_container["ownerDocument"]; + // Each screen is represented by a div container. All of the windows exist therein as + // its children. Qt versions < 6.5 used to represent screens as canvas. Support that by + // transforming the canvas into a div. + if (m_container["tagName"].call<std::string>("toLowerCase") == "canvas") { + qWarning() << "Support for canvas elements as an element backing screen is deprecated. The " + "canvas provided for the screen will be transformed into a div."; + auto container = document.call<emscripten::val>("createElement", emscripten::val("div")); + m_container["parentNode"].call<void>("replaceChild", container, m_container); + m_container = container; + } + + // Create an intermediate container which we can remove during cleanup in ~QWasmScreen(). + // This is required due to the attachShadow() call below; there is no corresponding + // "detachShadow()" API to return the container to its previous state. + m_intermediateContainer = document.call<emscripten::val>("createElement", emscripten::val("div")); + m_intermediateContainer.set("id", std::string("qt-shadow-container")); + emscripten::val intermediateContainerStyle = m_intermediateContainer["style"]; + intermediateContainerStyle.set("width", std::string("100%")); + intermediateContainerStyle.set("height", std::string("100%")); + m_container.call<void>("appendChild", m_intermediateContainer); + + auto shadowOptions = emscripten::val::object(); + shadowOptions.set("mode", "open"); + auto shadow = m_intermediateContainer.call<emscripten::val>("attachShadow", shadowOptions); + + m_shadowContainer = document.call<emscripten::val>("createElement", emscripten::val("div")); + + shadow.call<void>("appendChild", QWasmCSSStyle::createStyleElement(m_shadowContainer)); + + shadow.call<void>("appendChild", m_shadowContainer); + + m_shadowContainer.set("id", std::string("qt-screen-") + std::to_string(uintptr_t(this))); + + m_shadowContainer["classList"].call<void>("add", std::string("qt-screen")); + + // Disable the default context menu; Qt applications typically + // provide custom right-click behavior. + m_onContextMenu = std::make_unique<qstdweb::EventCallback>( + m_shadowContainer, "contextmenu", + [](emscripten::val event) { event.call<void>("preventDefault"); }); + // Create "specialHTMLTargets" mapping for the canvas - the element might be unreachable based + // on its id only under some conditions, like the target being embedded in a shadow DOM or a + // subframe. + emscripten::val::module_property("specialHTMLTargets") + .set(eventTargetId().toStdString(), m_shadowContainer); + + emscripten::val::module_property("specialHTMLTargets") + .set(outerScreenId().toStdString(), m_container); + updateQScreenAndCanvasRenderSize(); - m_canvas.call<void>("focus"); + m_shadowContainer.call<void>("focus"); + + m_touchDevice = std::make_unique<QPointingDevice>( + "touchscreen", 1, QInputDevice::DeviceType::TouchScreen, + QPointingDevice::PointerType::Finger, + QPointingDevice::Capability::Position | QPointingDevice::Capability::Area + | QPointingDevice::Capability::NormalizedPosition, + 10, 0); + m_tabletDevice = std::make_unique<QPointingDevice>( + "stylus", 2, QInputDevice::DeviceType::Stylus, + QPointingDevice::PointerType::Pen, + QPointingDevice::Capability::Position | QPointingDevice::Capability::Pressure + | QPointingDevice::Capability::NormalizedPosition + | QInputDevice::Capability::MouseEmulation + | QInputDevice::Capability::Hover | QInputDevice::Capability::Rotation + | QInputDevice::Capability::XTilt | QInputDevice::Capability::YTilt + | QInputDevice::Capability::TangentialPressure, + 0, 0); + + QWindowSystemInterface::registerInputDevice(m_touchDevice.get()); } QWasmScreen::~QWasmScreen() { - m_canvas.set(m_canvasResizeObserverCallbackContextPropertyName, emscripten::val(intptr_t(0))); - destroy(); + m_intermediateContainer.call<void>("remove"); + + emscripten::val::module_property("specialHTMLTargets") + .set(eventTargetId().toStdString(), emscripten::val::undefined()); + + m_shadowContainer.set(m_canvasResizeObserverCallbackContextPropertyName, + emscripten::val(intptr_t(0))); } -void QWasmScreen::destroy() +void QWasmScreen::deleteScreen() { - m_compositor->destroy(); + // Deletes |this|! + QWindowSystemInterface::handleScreenRemoved(this); } QWasmScreen *QWasmScreen::get(QPlatformScreen *screen) @@ -80,27 +130,31 @@ QWasmScreen *QWasmScreen::get(QPlatformScreen *screen) QWasmScreen *QWasmScreen::get(QScreen *screen) { + if (!screen) + return nullptr; return get(screen->handle()); } QWasmCompositor *QWasmScreen::compositor() { - return m_compositor; + return m_compositor.get(); } -QWasmEventTranslator *QWasmScreen::eventTranslator() +emscripten::val QWasmScreen::element() const { - return m_eventTranslator; + return m_shadowContainer; } -emscripten::val QWasmScreen::canvas() const +QString QWasmScreen::eventTargetId() const { - return m_canvas; + // Return a globally unique id for the canvas. We can choose any string, + // as long as it starts with a "!". + return QString("!qtcanvas_%1").arg(uintptr_t(this)); } -QString QWasmScreen::canvasId() const +QString QWasmScreen::outerScreenId() const { - return QWasmString::toQString(m_canvas["id"]); + return QString("!outerscreen_%1").arg(uintptr_t(this)); } QRect QWasmScreen::geometry() const @@ -152,7 +206,7 @@ qreal QWasmScreen::devicePixelRatio() const QString QWasmScreen::name() const { - return canvasId(); + return QString::fromEcmaString(m_shadowContainer["id"]); } QPlatformCursor *QWasmScreen::cursor() const @@ -169,12 +223,30 @@ void QWasmScreen::resizeMaximizedWindows() QWindow *QWasmScreen::topWindow() const { - return m_compositor->keyWindow(); + return activeChild() ? activeChild()->window() : nullptr; } QWindow *QWasmScreen::topLevelAt(const QPoint &p) const { - return m_compositor->windowAt(p); + const auto found = + std::find_if(childStack().begin(), childStack().end(), [&p](const QWasmWindow *window) { + const QRect geometry = window->windowFrameGeometry(); + + return window->isVisible() && geometry.contains(p); + }); + return found != childStack().end() ? (*found)->window() : nullptr; +} + +QPointF QWasmScreen::mapFromLocal(const QPointF &p) const +{ + return geometry().topLeft() + p; +} + +QPointF QWasmScreen::clipPoint(const QPointF &p) const +{ + const auto geometryF = screen()->geometry().toRectF(); + return QPointF(qBound(geometryF.left(), p.x(), geometryF.right()), + qBound(geometryF.top(), p.y(), geometryF.bottom())); } void QWasmScreen::invalidateSize() @@ -185,10 +257,23 @@ void QWasmScreen::invalidateSize() void QWasmScreen::setGeometry(const QRect &rect) { m_geometry = rect; - QWindowSystemInterface::handleScreenGeometryChange(QPlatformScreen::screen(), geometry(), availableGeometry()); + QWindowSystemInterface::handleScreenGeometryChange(QPlatformScreen::screen(), geometry(), + availableGeometry()); resizeMaximizedWindows(); } +void QWasmScreen::onSubtreeChanged(QWasmWindowTreeNodeChangeType changeType, + QWasmWindowTreeNode *parent, QWasmWindow *child) +{ + Q_UNUSED(parent); + if (changeType == QWasmWindowTreeNodeChangeType::NodeInsertion && parent == this + && childStack().size() == 1) { + child->window()->setFlag(Qt::WindowStaysOnBottomHint); + } + QWasmWindowTreeNode::onSubtreeChanged(changeType, parent, child); + m_compositor->onWindowTreeChanged(changeType, child); +} + void QWasmScreen::updateQScreenAndCanvasRenderSize() { // The HTML canvas has two sizes: the CSS size and the canvas render size. @@ -197,27 +282,26 @@ void QWasmScreen::updateQScreenAndCanvasRenderSize() // size must be set manually and is not auto-updated on CSS size change. // Setting the render size to a value larger than the CSS size enables high-dpi // rendering. - - QByteArray canvasSelector = "#" + canvasId().toUtf8(); double css_width; double css_height; - emscripten_get_element_css_size(canvasSelector.constData(), &css_width, &css_height); + emscripten_get_element_css_size(outerScreenId().toUtf8().constData(), &css_width, &css_height); QSizeF cssSize(css_width, css_height); QSizeF canvasSize = cssSize * devicePixelRatio(); - m_canvas.set("width", canvasSize.width()); - m_canvas.set("height", canvasSize.height()); + m_shadowContainer.set("width", canvasSize.width()); + m_shadowContainer.set("height", canvasSize.height()); - QPoint offset; - offset.setX(m_canvas["offsetTop"].as<int>()); - offset.setY(m_canvas["offsetLeft"].as<int>()); + // Returns the html elements document/body position + auto getElementBodyPosition = [](const emscripten::val &element) -> QPoint { + emscripten::val bodyRect = + element["ownerDocument"]["body"].call<emscripten::val>("getBoundingClientRect"); + emscripten::val canvasRect = element.call<emscripten::val>("getBoundingClientRect"); + return QPoint(canvasRect["left"].as<int>() - bodyRect["left"].as<int>(), + canvasRect["top"].as<int>() - bodyRect["top"].as<int>()); + }; - emscripten::val rect = m_canvas.call<emscripten::val>("getBoundingClientRect"); - QPoint position(rect["left"].as<int>() - offset.x(), rect["top"].as<int>() - offset.y()); - - setGeometry(QRect(position, cssSize.toSize())); - m_compositor->redrawWindowContent(); + setGeometry(QRect(getElementBodyPosition(m_shadowContainer), cssSize.toSize())); } void QWasmScreen::canvasResizeObserverCallback(emscripten::val entries, emscripten::val) @@ -226,20 +310,23 @@ void QWasmScreen::canvasResizeObserverCallback(emscripten::val entries, emscript if (count == 0) return; emscripten::val entry = entries[0]; - QWasmScreen *screen = - reinterpret_cast<QWasmScreen *>(entry["target"][m_canvasResizeObserverCallbackContextPropertyName].as<intptr_t>()); + QWasmScreen *screen = reinterpret_cast<QWasmScreen *>( + entry["target"][m_canvasResizeObserverCallbackContextPropertyName].as<intptr_t>()); if (!screen) { qWarning() << "QWasmScreen::canvasResizeObserverCallback: missing screen pointer"; return; } // We could access contentBoxSize|contentRect|devicePixelContentBoxSize on the entry here, but - // these are not universally supported across all browsers. Get the sizes from the canvas instead. + // these are not universally supported across all browsers. Get the sizes from the canvas + // instead. screen->updateQScreenAndCanvasRenderSize(); } -EMSCRIPTEN_BINDINGS(qtCanvasResizeObserverCallback) { - emscripten::function("qtCanvasResizeObserverCallback", &QWasmScreen::canvasResizeObserverCallback); +EMSCRIPTEN_BINDINGS(qtCanvasResizeObserverCallback) +{ + emscripten::function("qtCanvasResizeObserverCallback", + &QWasmScreen::canvasResizeObserverCallback); } void QWasmScreen::installCanvasResizeObserver() @@ -247,15 +334,44 @@ void QWasmScreen::installCanvasResizeObserver() emscripten::val ResizeObserver = emscripten::val::global("ResizeObserver"); if (ResizeObserver == emscripten::val::undefined()) return; // ResizeObserver API is not available - emscripten::val resizeObserver = ResizeObserver.new_(emscripten::val::module_property("qtCanvasResizeObserverCallback")); + emscripten::val resizeObserver = + ResizeObserver.new_(emscripten::val::module_property("qtCanvasResizeObserverCallback")); if (resizeObserver == emscripten::val::undefined()) return; // Something went horribly wrong // We need to get back to this instance from the (static) resize callback; // set a "data-" property on the canvas element. - m_canvas.set(m_canvasResizeObserverCallbackContextPropertyName, emscripten::val(intptr_t(this))); + m_shadowContainer.set(m_canvasResizeObserverCallbackContextPropertyName, + emscripten::val(intptr_t(this))); - resizeObserver.call<void>("observe", m_canvas); + resizeObserver.call<void>("observe", m_shadowContainer); +} + +emscripten::val QWasmScreen::containerElement() +{ + return m_shadowContainer; +} + +QWasmWindowTreeNode *QWasmScreen::parentNode() +{ + return nullptr; +} + +QList<QWasmWindow *> QWasmScreen::allWindows() +{ + QList<QWasmWindow *> windows; + for (auto *child : childStack()) { + const QWindowList list = child->window()->findChildren<QWindow *>(Qt::FindChildrenRecursively); + for (auto child : list) { + auto handle = child->handle(); + if (handle) { + auto wnd = static_cast<QWasmWindow *>(handle); + windows.push_back(wnd); + } + } + windows.push_back(child); + } + return windows; } QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmscreen.h b/src/plugins/platforms/wasm/qwasmscreen.h index 14d5a2f7d1..da171d3f50 100644 --- a/src/plugins/platforms/wasm/qwasmscreen.h +++ b/src/plugins/platforms/wasm/qwasmscreen.h @@ -1,41 +1,18 @@ -/**************************************************************************** -** -** 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$ -** -****************************************************************************/ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #ifndef QWASMSCREEN_H #define QWASMSCREEN_H #include "qwasmcursor.h" +#include "qwasmwindowtreenode.h" + #include <qpa/qplatformscreen.h> #include <QtCore/qscopedpointer.h> #include <QtCore/qtextstream.h> +#include <QtCore/private/qstdweb_p.h> #include <emscripten/val.h> @@ -45,24 +22,29 @@ class QPlatformOpenGLContext; class QWasmWindow; class QWasmBackingStore; class QWasmCompositor; -class QWasmEventTranslator; +class QWasmDeadKeySupport; class QOpenGLContext; -class QWasmScreen : public QObject, public QPlatformScreen +class QWasmScreen : public QObject, public QPlatformScreen, public QWasmWindowTreeNode { Q_OBJECT public: - QWasmScreen(const emscripten::val &canvas); + QWasmScreen(const emscripten::val &containerOrCanvas); ~QWasmScreen(); - void destroy(); + void deleteScreen(); static QWasmScreen *get(QPlatformScreen *screen); static QWasmScreen *get(QScreen *screen); - emscripten::val canvas() const; - QString canvasId() const; + emscripten::val element() const; + QString eventTargetId() const; + QString outerScreenId() const; + QPointingDevice *touchDevice() { return m_touchDevice.get(); } + QPointingDevice *tabletDevice() { return m_tabletDevice.get(); } QWasmCompositor *compositor(); - QWasmEventTranslator *eventTranslator(); + QWasmDeadKeySupport *deadKeySupport() { return m_deadKeySupport.get(); } + + QList<QWasmWindow *> allWindows(); QRect geometry() const override; int depth() const override; @@ -76,6 +58,13 @@ public: QWindow *topWindow() const; QWindow *topLevelAt(const QPoint &p) const override; + // QWasmWindowTreeNode: + emscripten::val containerElement() final; + QWasmWindowTreeNode *parentNode() final; + + QPointF mapFromLocal(const QPointF &p) const; + QPointF clipPoint(const QPointF &p) const; + void invalidateSize(); void updateQScreenAndCanvasRenderSize(); void installCanvasResizeObserver(); @@ -85,14 +74,23 @@ public slots: void setGeometry(const QRect &rect); private: - emscripten::val m_canvas; - QWasmCompositor *m_compositor = nullptr; - QWasmEventTranslator *m_eventTranslator = nullptr; + // QWasmWindowTreeNode: + void onSubtreeChanged(QWasmWindowTreeNodeChangeType changeType, QWasmWindowTreeNode *parent, + QWasmWindow *child) final; + + emscripten::val m_container; + emscripten::val m_intermediateContainer; + emscripten::val m_shadowContainer; + std::unique_ptr<QWasmCompositor> m_compositor; + std::unique_ptr<QPointingDevice> m_touchDevice; + std::unique_ptr<QPointingDevice> m_tabletDevice; + std::unique_ptr<QWasmDeadKeySupport> m_deadKeySupport; QRect m_geometry = QRect(0, 0, 100, 100); int m_depth = 32; QImage::Format m_format = QImage::Format_RGB32; QWasmCursor m_cursor; - static const char * m_canvasResizeObserverCallbackContextPropertyName; + static const char *m_canvasResizeObserverCallbackContextPropertyName; + std::unique_ptr<qstdweb::EventCallback> m_onContextMenu; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmservices.cpp b/src/plugins/platforms/wasm/qwasmservices.cpp index 4eee3fe972..e767295e41 100644 --- a/src/plugins/platforms/wasm/qwasmservices.cpp +++ b/src/plugins/platforms/wasm/qwasmservices.cpp @@ -1,34 +1,7 @@ -/**************************************************************************** -** -** 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$ -** -****************************************************************************/ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "qwasmservices.h" -#include "qwasmstring.h" #include <QtCore/QUrl> #include <QtCore/QDebug> @@ -39,8 +12,8 @@ QT_BEGIN_NAMESPACE bool QWasmServices::openUrl(const QUrl &url) { - emscripten::val jsUrl = QWasmString::fromQString(url.toString()); - emscripten::val::global("window").call<void>("open", jsUrl, emscripten::val("_blank")); + emscripten::val::global("window").call<void>("open", url.toString().toEcmaString(), + emscripten::val("_blank")); return true; } diff --git a/src/plugins/platforms/wasm/qwasmservices.h b/src/plugins/platforms/wasm/qwasmservices.h index 3b37f21f82..16d4ac5171 100644 --- a/src/plugins/platforms/wasm/qwasmservices.h +++ b/src/plugins/platforms/wasm/qwasmservices.h @@ -1,31 +1,5 @@ -/**************************************************************************** -** -** 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$ -** -****************************************************************************/ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #ifndef QWASMDESKTOPSERVICES_H #define QWASMDESKTOPSERVICES_H diff --git a/src/plugins/platforms/wasm/qwasmstring.cpp b/src/plugins/platforms/wasm/qwasmstring.cpp deleted file mode 100644 index b1be405eeb..0000000000 --- a/src/plugins/platforms/wasm/qwasmstring.cpp +++ /dev/null @@ -1,62 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2020 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$ -** -****************************************************************************/ - -#include "qwasmstring.h" - -QT_BEGIN_NAMESPACE - -using namespace emscripten; - -val QWasmString::fromQString(const QString &str) -{ - static const val UTF16ToString( - val::global("Module")["UTF16ToString"]); - - auto ptr = quintptr(str.utf16()); - return UTF16ToString(val(ptr)); -} - -QString QWasmString::toQString(const val &v) -{ - QString result; - if (!v.isString()) - return result; - - static const val stringToUTF16( - val::global("Module")["stringToUTF16"]); - static const val length("length"); - - int len = v[length].as<int>(); - result.resize(len); - auto ptr = quintptr(result.utf16()); - stringToUTF16(v, val(ptr), val((len + 1) * 2)); - return result; -} - -QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmstring.h b/src/plugins/platforms/wasm/qwasmstring.h deleted file mode 100644 index de5da92830..0000000000 --- a/src/plugins/platforms/wasm/qwasmstring.h +++ /dev/null @@ -1,45 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2020 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$ -** -****************************************************************************/ - -#pragma once - -#include <qstring.h> - -#include <emscripten/val.h> - -QT_BEGIN_NAMESPACE - -class QWasmString -{ -public: - static emscripten::val fromQString(const QString &str); - static QString toQString(const emscripten::val &v); -}; -QT_END_NAMESPACE - diff --git a/src/plugins/platforms/wasm/qwasmstylepixmaps_p.h b/src/plugins/platforms/wasm/qwasmstylepixmaps_p.h deleted file mode 100644 index 2b5860f42f..0000000000 --- a/src/plugins/platforms/wasm/qwasmstylepixmaps_p.h +++ /dev/null @@ -1,183 +0,0 @@ -/**************************************************************************** -** -** 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$ -** -****************************************************************************/ - -#ifndef QWASMSTYLEPIXMAPS_P_H -#define QWASMSTYLEPIXMAPS_P_H - -// -// W A R N I N G -// ------------- -// -// This file is not part of the Qt API. It exists purely as an -// implementation detail. This header file may change from version to -// version without notice, or even be removed. -// -// We mean it. -// - -/* XPM */ -static const char * const qt_menu_xpm[] = { -"16 16 72 1", -" c None", -". c #65AF36", -"+ c #66B036", -"@ c #77B94C", -"# c #A7D28C", -"$ c #BADBA4", -"% c #A4D088", -"& c #72B646", -"* c #9ACB7A", -"= c #7FBD56", -"- c #85C05F", -"; c #F4F9F0", -"> c #FFFFFF", -", c #E5F1DC", -"' c #ECF5E7", -") c #7ABA50", -"! c #83BF5C", -"~ c #AED595", -"{ c #D7EACA", -"] c #A9D28D", -"^ c #BCDDA8", -"/ c #C4E0B1", -"( c #81BE59", -"_ c #D0E7C2", -": c #D4E9C6", -"< c #6FB542", -"[ c #6EB440", -"} c #88C162", -"| c #98CA78", -"1 c #F4F9F1", -"2 c #8FC56C", -"3 c #F1F8EC", -"4 c #E8F3E1", -"5 c #D4E9C7", -"6 c #74B748", -"7 c #80BE59", -"8 c #73B747", -"9 c #6DB43F", -"0 c #CBE4BA", -"a c #80BD58", -"b c #6DB33F", -"c c #FEFFFE", -"d c #68B138", -"e c #F9FCF7", -"f c #91C66F", -"g c #E8F3E0", -"h c #DCEDD0", -"i c #91C66E", -"j c #A3CF86", -"k c #C9E3B8", -"l c #B0D697", -"m c #E3F0DA", -"n c #95C873", -"o c #E6F2DE", -"p c #9ECD80", -"q c #BEDEAA", -"r c #C7E2B6", -"s c #79BA4F", -"t c #6EB441", -"u c #BCDCA7", -"v c #FAFCF8", -"w c #F6FAF3", -"x c #84BF5D", -"y c #EDF6E7", -"z c #FAFDF9", -"A c #88C263", -"B c #98CA77", -"C c #CDE5BE", -"D c #67B037", -"E c #D9EBCD", -"F c #6AB23C", -"G c #77B94D", -" .++++++++++++++", -".+++++++++++++++", -"+++@#$%&+++*=+++", -"++-;>,>')+!>~+++", -"++{>]+^>/(_>:~<+", -"+[>>}+|>123>456+", -"+7>>8+->>90>~+++", -"+a>>b+a>c[0>~+++", -"+de>=+f>g+0>~+++", -"++h>i+j>k+0>~+++", -"++l>mno>p+q>rst+", -"++duv>wl++xy>zA+", -"++++B>Cb++++&D++", -"+++++0zE++++++++", -"++++++FG+++++++.", -"++++++++++++++. "}; - -static const char * const qt_close_xpm[] = { -"10 10 2 1", -"# c #000000", -". c None", -"..........", -".##....##.", -"..##..##..", -"...####...", -"....##....", -"...####...", -"..##..##..", -".##....##.", -"..........", -".........."}; - -static const char * const qt_maximize_xpm[]={ -"10 10 2 1", -"# c #000000", -". c None", -"#########.", -"#########.", -"#.......#.", -"#.......#.", -"#.......#.", -"#.......#.", -"#.......#.", -"#.......#.", -"#########.", -".........."}; - - -static const char * const qt_normalizeup_xpm[] = { -"10 10 2 1", -"# c #000000", -". c None", -"...######.", -"...######.", -"...#....#.", -".######.#.", -".######.#.", -".#....###.", -".#....#...", -".#....#...", -".######...", -".........."}; - - -#endif // QWASMSTYLEPIXMAPS_P_H diff --git a/src/plugins/platforms/wasm/qwasmtheme.cpp b/src/plugins/platforms/wasm/qwasmtheme.cpp index 438e3e1119..b188dcb4b6 100644 --- a/src/plugins/platforms/wasm/qwasmtheme.cpp +++ b/src/plugins/platforms/wasm/qwasmtheme.cpp @@ -1,38 +1,15 @@ -/**************************************************************************** -** -** 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$ -** -****************************************************************************/ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "qwasmtheme.h" #include <QtCore/qvariant.h> #include <QFontDatabase> +#include <QList> QT_BEGIN_NAMESPACE +using namespace Qt::StringLiterals; + QWasmTheme::QWasmTheme() { for (auto family : QFontDatabase::families()) @@ -49,7 +26,9 @@ QWasmTheme::~QWasmTheme() QVariant QWasmTheme::themeHint(ThemeHint hint) const { if (hint == QPlatformTheme::StyleNames) - return QVariant(QStringList() << QLatin1String("Fusion")); + return QVariant(QStringList() << "Fusion"_L1); + if (hint == QPlatformTheme::UiEffects) + return QVariant(int(HoverEffect)); return QPlatformTheme::themeHint(hint); } diff --git a/src/plugins/platforms/wasm/qwasmtheme.h b/src/plugins/platforms/wasm/qwasmtheme.h index 7123a1f3d4..90ecbe6ddf 100644 --- a/src/plugins/platforms/wasm/qwasmtheme.h +++ b/src/plugins/platforms/wasm/qwasmtheme.h @@ -1,31 +1,5 @@ -/**************************************************************************** -** -** 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$ -** -****************************************************************************/ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #ifndef QWASMTHEME_H #define QWASMTHEME_H diff --git a/src/plugins/platforms/wasm/qwasmwindow.cpp b/src/plugins/platforms/wasm/qwasmwindow.cpp index f95335f891..99e9bb22f1 100644 --- a/src/plugins/platforms/wasm/qwasmwindow.cpp +++ b/src/plugins/platforms/wasm/qwasmwindow.cpp @@ -1,96 +1,231 @@ -/**************************************************************************** -** -** 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$ -** -****************************************************************************/ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include <qpa/qwindowsysteminterface.h> #include <private/qguiapplication_p.h> -#include <QtGui/private/qopenglcontext_p.h> +#include <QtCore/qfile.h> #include <QtGui/private/qwindow_p.h> -#include <QtGui/qopenglcontext.h> - +#include <QtGui/private/qhighdpiscaling_p.h> +#include <private/qpixmapcache_p.h> +#include <QtGui/qopenglfunctions.h> +#include <QBuffer> + +#include "qwasmbase64iconstore.h" +#include "qwasmdom.h" +#include "qwasmclipboard.h" +#include "qwasmintegration.h" +#include "qwasmkeytranslator.h" #include "qwasmwindow.h" +#include "qwasmwindowclientarea.h" #include "qwasmscreen.h" #include "qwasmcompositor.h" +#include "qwasmevent.h" #include "qwasmeventdispatcher.h" +#include "qwasmaccessibility.h" +#include "qwasmclipboard.h" #include <iostream> +#include <sstream> -Q_GUI_EXPORT int qt_defaultDpiX(); +#include <emscripten/val.h> + +#include <QtCore/private/qstdweb_p.h> QT_BEGIN_NAMESPACE -QWasmWindow::QWasmWindow(QWindow *w, QWasmCompositor *compositor, QWasmBackingStore *backingStore) +namespace { +QWasmWindowStack::PositionPreference positionPreferenceFromWindowFlags(Qt::WindowFlags flags) +{ + if (flags.testFlag(Qt::WindowStaysOnTopHint)) + return QWasmWindowStack::PositionPreference::StayOnTop; + if (flags.testFlag(Qt::WindowStaysOnBottomHint)) + return QWasmWindowStack::PositionPreference::StayOnBottom; + return QWasmWindowStack::PositionPreference::Regular; +} +} // namespace + +Q_GUI_EXPORT int qt_defaultDpiX(); + +QWasmWindow::QWasmWindow(QWindow *w, QWasmDeadKeySupport *deadKeySupport, + QWasmCompositor *compositor, QWasmBackingStore *backingStore) : QPlatformWindow(w), m_window(w), m_compositor(compositor), - m_backingStore(backingStore) + m_backingStore(backingStore), + m_deadKeySupport(deadKeySupport), + m_document(dom::document()), + m_qtWindow(m_document.call<emscripten::val>("createElement", emscripten::val("div"))), + m_windowContents(m_document.call<emscripten::val>("createElement", emscripten::val("div"))), + m_canvasContainer(m_document.call<emscripten::val>("createElement", emscripten::val("div"))), + m_a11yContainer(m_document.call<emscripten::val>("createElement", emscripten::val("div"))), + m_canvas(m_document.call<emscripten::val>("createElement", emscripten::val("canvas"))) { - m_needsCompositor = w->surfaceType() != QSurface::OpenGLSurface; + m_qtWindow.set("className", "qt-window"); + m_qtWindow["style"].set("display", std::string("none")); + + m_nonClientArea = std::make_unique<NonClientArea>(this, m_qtWindow); + m_nonClientArea->titleBar()->setTitle(window()->title()); + + m_clientArea = std::make_unique<ClientArea>(this, compositor->screen(), m_windowContents); + + m_windowContents.set("className", "qt-window-contents"); + m_qtWindow.call<void>("appendChild", m_windowContents); + + m_canvas["classList"].call<void>("add", emscripten::val("qt-window-content")); + + // Set contenteditable so that the canvas gets clipboard events, + // then hide the resulting focus frame. + m_canvas.set("contentEditable", std::string("true")); + m_canvas["style"].set("outline", std::string("none")); + + QWasmClipboard::installEventHandlers(m_canvas); + + // set inputMode to none to stop mobile keyboard opening + // when user clicks anywhere on the canvas. + m_canvas.set("inputMode", std::string("none")); + + // Hide the canvas from screen readers. + m_canvas.call<void>("setAttribute", std::string("aria-hidden"), std::string("true")); + + m_windowContents.call<void>("appendChild", m_canvasContainer); + + m_canvasContainer["classList"].call<void>("add", emscripten::val("qt-window-canvas-container")); + m_canvasContainer.call<void>("appendChild", m_canvas); + + m_canvasContainer.call<void>("appendChild", m_a11yContainer); + m_a11yContainer["classList"].call<void>("add", emscripten::val("qt-window-a11y-container")); + + const bool rendersTo2dContext = w->surfaceType() != QSurface::OpenGLSurface; + if (rendersTo2dContext) + m_context2d = m_canvas.call<emscripten::val>("getContext", emscripten::val("2d")); static int serialNo = 0; - m_winid = ++serialNo; + m_winId = ++serialNo; + m_qtWindow.set("id", "qt-window-" + std::to_string(m_winId)); + emscripten::val::module_property("specialHTMLTargets").set(canvasSelector(), m_canvas); + + m_flags = window()->flags(); + + const auto pointerCallback = std::function([this](emscripten::val event) { + if (processPointer(*PointerEvent::fromWeb(event))) + event.call<void>("preventDefault"); + }); + + m_pointerEnterCallback = + std::make_unique<qstdweb::EventCallback>(m_qtWindow, "pointerenter", pointerCallback); + m_pointerLeaveCallback = + std::make_unique<qstdweb::EventCallback>(m_qtWindow, "pointerleave", pointerCallback); + + m_wheelEventCallback = std::make_unique<qstdweb::EventCallback>( + m_qtWindow, "wheel", [this](emscripten::val event) { + if (processWheel(*WheelEvent::fromWeb(event))) + event.call<void>("preventDefault"); + }); + + const auto keyCallback = std::function([this](emscripten::val event) { + if (processKey(*KeyEvent::fromWebWithDeadKeyTranslation(event, m_deadKeySupport))) + event.call<void>("preventDefault"); + event.call<void>("stopPropagation"); + }); + + emscripten::val keyFocusWindow; + if (QWasmInputContext *wasmContext = + qobject_cast<QWasmInputContext *>(QWasmIntegration::get()->inputContext())) { + // if there is an touchscreen input context, + // use that window for key input + keyFocusWindow = wasmContext->m_inputElement; + } else { + keyFocusWindow = m_qtWindow; + } - m_compositor->addWindow(this); + m_keyDownCallback = + std::make_unique<qstdweb::EventCallback>(keyFocusWindow, "keydown", keyCallback); + m_keyUpCallback = std::make_unique<qstdweb::EventCallback>(keyFocusWindow, "keyup", keyCallback); - // Pure OpenGL windows draw directly using egl, disable the compositor. - m_compositor->setEnabled(w->surfaceType() != QSurface::OpenGLSurface); + setParent(parent()); } QWasmWindow::~QWasmWindow() { - m_compositor->removeWindow(this); + emscripten::val::module_property("specialHTMLTargets").delete_(canvasSelector()); + m_canvasContainer.call<void>("removeChild", m_canvas); + m_context2d = emscripten::val::undefined(); + commitParent(nullptr); + if (m_requestAnimationFrameId > -1) + emscripten_cancel_animation_frame(m_requestAnimationFrameId); +#if QT_CONFIG(accessibility) + QWasmAccessibility::removeAccessibilityEnableButton(window()); +#endif } -void QWasmWindow::destroy() +QSurfaceFormat QWasmWindow::format() const { - if (m_backingStore) - m_backingStore->destroy(); + return window()->requestedFormat(); } -void QWasmWindow::initialize() +QWasmWindow *QWasmWindow::fromWindow(QWindow *window) { - QRect rect = windowGeometry(); + return static_cast<QWasmWindow *>(window->handle()); +} - QPlatformWindow::setGeometry(rect); +void QWasmWindow::onRestoreClicked() +{ + window()->setWindowState(Qt::WindowNoState); +} - const QSize minimumSize = windowMinimumSize(); - if (rect.width() > 0 || rect.height() > 0) { - rect.setWidth(qBound(1, rect.width(), 2000)); - rect.setHeight(qBound(1, rect.height(), 2000)); - } else if (minimumSize.width() > 0 || minimumSize.height() > 0) { - rect.setSize(minimumSize); - } +void QWasmWindow::onMaximizeClicked() +{ + window()->setWindowState(Qt::WindowMaximized); +} + +void QWasmWindow::onToggleMaximized() +{ + window()->setWindowState(m_state.testFlag(Qt::WindowMaximized) ? Qt::WindowNoState + : Qt::WindowMaximized); +} + +void QWasmWindow::onCloseClicked() +{ + window()->close(); +} + +void QWasmWindow::onNonClientAreaInteraction() +{ + requestActivateWindow(); + QGuiApplicationPrivate::instance()->closeAllPopups(); +} + +bool QWasmWindow::onNonClientEvent(const PointerEvent &event) +{ + QPointF pointInScreen = platformScreen()->mapFromLocal( + dom::mapPoint(event.target(), platformScreen()->element(), event.localPoint)); + return QWindowSystemInterface::handleMouseEvent( + window(), QWasmIntegration::getTimestamp(), window()->mapFromGlobal(pointInScreen), + pointInScreen, event.mouseButtons, event.mouseButton, + MouseEvent::mouseEventTypeFromEventType(event.type, WindowArea::NonClient), + event.modifiers); +} + +void QWasmWindow::initialize() +{ + auto initialGeometry = QPlatformWindow::initialGeometry(window(), + windowGeometry(), defaultWindowSize, defaultWindowSize); + m_normalGeometry = initialGeometry; setWindowState(window()->windowStates()); setWindowFlags(window()->flags()); setWindowTitle(window()->title()); + setMask(QHighDpi::toNativeLocalRegion(window()->mask(), window())); + if (window()->isTopLevel()) setWindowIcon(window()->icon()); - m_normalGeometry = rect; + QPlatformWindow::setGeometry(m_normalGeometry); + +#if QT_CONFIG(accessibility) + // Add accessibility-enable button. The user can activate this + // button to opt-in to accessibility. + if (window()->isTopLevel()) + QWasmAccessibility::addAccessibilityEnableButton(window()); +#endif } QWasmScreen *QWasmWindow::platformScreen() const @@ -98,314 +233,468 @@ QWasmScreen *QWasmWindow::platformScreen() const return static_cast<QWasmScreen *>(window()->screen()->handle()); } -void QWasmWindow::setGeometry(const QRect &rect) +void QWasmWindow::paint() { - QRect r = rect; - if (m_needsCompositor) { - int yMin = window()->geometry().top() - window()->frameGeometry().top(); - - if (r.y() < yMin) - r.moveTop(yMin); - } - QWindowSystemInterface::handleGeometryChange(window(), r); - QPlatformWindow::setGeometry(r); + if (!m_backingStore || !isVisible() || m_context2d.isUndefined()) + return; - QWindowSystemInterface::flushWindowSystemEvents(); - invalidate(); + auto image = m_backingStore->getUpdatedWebImage(this); + if (image.isUndefined()) + return; + m_context2d.call<void>("putImageData", image, emscripten::val(0), emscripten::val(0)); } -void QWasmWindow::setVisible(bool visible) +void QWasmWindow::setZOrder(int z) { - QRect newGeom; + m_qtWindow["style"].set("zIndex", std::to_string(z)); +} - if (visible) { - const bool forceFullScreen = !m_needsCompositor;//make gl apps fullscreen for now +void QWasmWindow::setWindowCursor(QByteArray cssCursorName) +{ + m_windowContents["style"].set("cursor", emscripten::val(cssCursorName.constData())); +} - if (forceFullScreen || (m_windowState & Qt::WindowFullScreen)) - newGeom = platformScreen()->geometry(); - else if (m_windowState & Qt::WindowMaximized) - newGeom = platformScreen()->availableGeometry(); +void QWasmWindow::setGeometry(const QRect &rect) +{ + const auto margins = frameMargins(); + + const QRect clientAreaRect = ([this, &rect, &margins]() { + if (m_state.testFlag(Qt::WindowFullScreen)) + return platformScreen()->geometry(); + if (m_state.testFlag(Qt::WindowMaximized)) + return platformScreen()->availableGeometry().marginsRemoved(frameMargins()); + + auto offset = rect.topLeft() - (!parent() ? screen()->geometry().topLeft() : QPoint()); + + // In viewport + auto containerGeometryInViewport = + QRectF::fromDOMRect(parentNode()->containerElement().call<emscripten::val>( + "getBoundingClientRect")) + .toRect(); + + auto rectInViewport = QRect(containerGeometryInViewport.topLeft() + offset, rect.size()); + + QRect cappedGeometry(rectInViewport); + if (!parent()) { + // Clamp top level windows top position to the screen bounds + cappedGeometry.moveTop( + std::max(std::min(rectInViewport.y(), containerGeometryInViewport.bottom()), + containerGeometryInViewport.y() + margins.top())); + } + cappedGeometry.setSize( + cappedGeometry.size().expandedTo(windowMinimumSize()).boundedTo(windowMaximumSize())); + return QRect(QPoint(rect.x(), rect.y() + cappedGeometry.y() - rectInViewport.y()), + rect.size()); + })(); + m_nonClientArea->onClientAreaWidthChange(clientAreaRect.width()); + + const auto frameRect = + clientAreaRect + .adjusted(-margins.left(), -margins.top(), margins.right(), margins.bottom()) + .translated(!parent() ? -screen()->geometry().topLeft() : QPoint()); + + m_qtWindow["style"].set("left", std::to_string(frameRect.left()) + "px"); + m_qtWindow["style"].set("top", std::to_string(frameRect.top()) + "px"); + m_canvasContainer["style"].set("width", std::to_string(clientAreaRect.width()) + "px"); + m_canvasContainer["style"].set("height", std::to_string(clientAreaRect.height()) + "px"); + m_a11yContainer["style"].set("width", std::to_string(clientAreaRect.width()) + "px"); + m_a11yContainer["style"].set("height", std::to_string(clientAreaRect.height()) + "px"); + + // Important for the title flexbox to shrink correctly + m_windowContents["style"].set("width", std::to_string(clientAreaRect.width()) + "px"); + + QSizeF canvasSize = clientAreaRect.size() * devicePixelRatio(); + + m_canvas.set("width", canvasSize.width()); + m_canvas.set("height", canvasSize.height()); + + bool shouldInvalidate = true; + if (!m_state.testFlag(Qt::WindowFullScreen) && !m_state.testFlag(Qt::WindowMaximized)) { + shouldInvalidate = m_normalGeometry.size() != clientAreaRect.size(); + m_normalGeometry = clientAreaRect; } - QPlatformWindow::setVisible(visible); + QWindowSystemInterface::handleGeometryChange(window(), clientAreaRect); + if (shouldInvalidate) + invalidate(); + else + m_compositor->requestUpdateWindow(this); +} - m_compositor->setVisible(this, visible); +void QWasmWindow::setVisible(bool visible) +{ + // TODO(mikolajboc): isVisible()? + const bool nowVisible = m_qtWindow["style"]["display"].as<std::string>() == "block"; + if (visible == nowVisible) + return; - if (!newGeom.isEmpty()) - setGeometry(newGeom); // may or may not generate an expose + m_compositor->requestUpdateWindow(this, QWasmCompositor::ExposeEventDelivery); + m_qtWindow["style"].set("display", visible ? "block" : "none"); + if (window()->isActive()) + m_canvas.call<void>("focus"); + if (visible) + applyWindowState(); +} - invalidate(); +bool QWasmWindow::isVisible() const +{ + return window()->isVisible(); } QMargins QWasmWindow::frameMargins() const { - int border = hasTitleBar() ? 4. * (qreal(qt_defaultDpiX()) / 96.0) : 0; - int titleBarHeight = hasTitleBar() ? titleHeight() : 0; - - QMargins margins; - margins.setLeft(border); - margins.setRight(border); - margins.setTop(2*border + titleBarHeight); - margins.setBottom(border); - - return margins; + const auto frameRect = + QRectF::fromDOMRect(m_qtWindow.call<emscripten::val>("getBoundingClientRect")); + const auto canvasRect = + QRectF::fromDOMRect(m_windowContents.call<emscripten::val>("getBoundingClientRect")); + return QMarginsF(canvasRect.left() - frameRect.left(), canvasRect.top() - frameRect.top(), + frameRect.right() - canvasRect.right(), + frameRect.bottom() - canvasRect.bottom()) + .toMargins(); } void QWasmWindow::raise() { - m_compositor->raise(this); - QWindowSystemInterface::handleExposeEvent(window(), QRect(QPoint(0, 0), geometry().size())); + bringToTop(); invalidate(); + if (QWasmIntegration::get()->inputContext()) + m_canvas.call<void>("focus"); } void QWasmWindow::lower() { - m_compositor->lower(this); - QWindowSystemInterface::handleExposeEvent(window(), QRect(QPoint(0, 0), geometry().size())); + sendToBottom(); invalidate(); } WId QWasmWindow::winId() const { - return m_winid; + return m_winId; } void QWasmWindow::propagateSizeHints() { -// get rid of base class warning + // setGeometry() will take care of minimum and maximum size constraints + setGeometry(windowGeometry()); + m_nonClientArea->propagateSizeHints(); } -void QWasmWindow::injectMousePressed(const QPoint &local, const QPoint &global, - Qt::MouseButton button, Qt::KeyboardModifiers mods) +void QWasmWindow::setOpacity(qreal level) { - Q_UNUSED(local); - Q_UNUSED(mods); + m_qtWindow["style"].set("opacity", qBound(0.0, level, 1.0)); +} - if (!hasTitleBar() || button != Qt::LeftButton) - return; +void QWasmWindow::invalidate() +{ + m_compositor->requestUpdateWindow(this); +} + +void QWasmWindow::onActivationChanged(bool active) +{ + dom::syncCSSClassWith(m_qtWindow, "inactive", !active); +} - if (maxButtonRect().contains(global)) - m_activeControl = QWasmCompositor::SC_TitleBarMaxButton; - else if (minButtonRect().contains(global)) - m_activeControl = QWasmCompositor::SC_TitleBarMinButton; - else if (closeButtonRect().contains(global)) - m_activeControl = QWasmCompositor::SC_TitleBarCloseButton; - else if (normButtonRect().contains(global)) - m_activeControl = QWasmCompositor::SC_TitleBarNormalButton; +void QWasmWindow::setWindowFlags(Qt::WindowFlags flags) +{ + if (flags.testFlag(Qt::WindowStaysOnTopHint) != m_flags.testFlag(Qt::WindowStaysOnTopHint) + || flags.testFlag(Qt::WindowStaysOnBottomHint) + != m_flags.testFlag(Qt::WindowStaysOnBottomHint)) { + onPositionPreferenceChanged(positionPreferenceFromWindowFlags(flags)); + } + m_flags = flags; + dom::syncCSSClassWith(m_qtWindow, "frameless", !hasFrame()); + dom::syncCSSClassWith(m_qtWindow, "has-border", hasBorder()); + dom::syncCSSClassWith(m_qtWindow, "has-shadow", hasShadow()); + dom::syncCSSClassWith(m_qtWindow, "has-title", hasTitleBar()); + dom::syncCSSClassWith(m_qtWindow, "transparent-for-input", + flags.testFlag(Qt::WindowTransparentForInput)); - invalidate(); + m_nonClientArea->titleBar()->setMaximizeVisible(hasMaximizeButton()); + m_nonClientArea->titleBar()->setCloseVisible(m_flags.testFlag(Qt::WindowCloseButtonHint)); } -void QWasmWindow::injectMouseReleased(const QPoint &local, const QPoint &global, - Qt::MouseButton button, Qt::KeyboardModifiers mods) +void QWasmWindow::setWindowState(Qt::WindowStates newState) { - Q_UNUSED(local); - Q_UNUSED(mods); + // Child windows can not have window states other than Qt::WindowActive + if (parent()) + newState &= Qt::WindowActive; - if (!hasTitleBar() || button != Qt::LeftButton) - return; + const Qt::WindowStates oldState = m_state; - if (closeButtonRect().contains(global) && m_activeControl == QWasmCompositor::SC_TitleBarCloseButton) { - window()->close(); + if (newState.testFlag(Qt::WindowMinimized)) { + newState.setFlag(Qt::WindowMinimized, false); + qWarning("Qt::WindowMinimized is not implemented in wasm"); + window()->setWindowStates(newState); return; } - if (maxButtonRect().contains(global) && m_activeControl == QWasmCompositor::SC_TitleBarMaxButton) { - window()->setWindowState(Qt::WindowMaximized); - platformScreen()->resizeMaximizedWindows(); - } - - if (normButtonRect().contains(global) && m_activeControl == QWasmCompositor::SC_TitleBarNormalButton) { - window()->setWindowState(Qt::WindowNoState); - setGeometry(normalGeometry()); - } + if (newState == oldState) + return; - m_activeControl = QWasmCompositor::SC_None; + m_state = newState; + m_previousWindowState = oldState; - invalidate(); + applyWindowState(); } -int QWasmWindow::titleHeight() const +void QWasmWindow::setWindowTitle(const QString &title) { - return 18. * (qreal(qt_defaultDpiX()) / 96.0);//dpiScaled(18.); + m_nonClientArea->titleBar()->setTitle(title); } -int QWasmWindow::borderWidth() const +void QWasmWindow::setWindowIcon(const QIcon &icon) { - return 4. * (qreal(qt_defaultDpiX()) / 96.0);// dpiScaled(4.); + const auto dpi = screen()->devicePixelRatio(); + auto pixmap = icon.pixmap(10 * dpi, 10 * dpi); + if (pixmap.isNull()) { + m_nonClientArea->titleBar()->setIcon( + Base64IconStore::get()->getIcon(Base64IconStore::IconType::QtLogo), "svg+xml"); + return; + } + + QByteArray bytes; + QBuffer buffer(&bytes); + pixmap.save(&buffer, "png"); + m_nonClientArea->titleBar()->setIcon(bytes.toBase64().toStdString(), "png"); } -QRegion QWasmWindow::titleGeometry() const +void QWasmWindow::applyWindowState() { - int border = borderWidth(); + QRect newGeom; - QRegion result(window()->frameGeometry().x() + border, - window()->frameGeometry().y() + border, - window()->frameGeometry().width() - 2*border, - titleHeight()); + const bool isFullscreen = m_state.testFlag(Qt::WindowFullScreen); + const bool isMaximized = m_state.testFlag(Qt::WindowMaximized); + if (isFullscreen) + newGeom = platformScreen()->geometry(); + else if (isMaximized) + newGeom = platformScreen()->availableGeometry().marginsRemoved(frameMargins()); + else + newGeom = normalGeometry(); - result -= titleControlRegion(); + dom::syncCSSClassWith(m_qtWindow, "has-border", hasBorder()); + dom::syncCSSClassWith(m_qtWindow, "maximized", isMaximized); - return result; + m_nonClientArea->titleBar()->setRestoreVisible(isMaximized); + m_nonClientArea->titleBar()->setMaximizeVisible(hasMaximizeButton()); + + if (isVisible()) + QWindowSystemInterface::handleWindowStateChanged(window(), m_state, m_previousWindowState); + setGeometry(newGeom); } -QRegion QWasmWindow::resizeRegion() const +void QWasmWindow::commitParent(QWasmWindowTreeNode *parent) { - int border = borderWidth(); - QRegion result(window()->frameGeometry().adjusted(-border, -border, border, border)); - result -= window()->frameGeometry().adjusted(border, border, -border, -border); - - return result; + onParentChanged(m_commitedParent, parent, positionPreferenceFromWindowFlags(window()->flags())); + m_commitedParent = parent; } -bool QWasmWindow::isPointOnTitle(QPoint point) const +bool QWasmWindow::processKey(const KeyEvent &event) { - bool ok = titleGeometry().contains(point); - return ok; + constexpr bool ProceedToNativeEvent = false; + Q_ASSERT(event.type == EventType::KeyDown || event.type == EventType::KeyUp); + + const auto clipboardResult = + QWasmIntegration::get()->getWasmClipboard()->processKeyboard(event); + + using ProcessKeyboardResult = QWasmClipboard::ProcessKeyboardResult; + if (clipboardResult == ProcessKeyboardResult::NativeClipboardEventNeeded) + return ProceedToNativeEvent; + + const auto result = QWindowSystemInterface::handleKeyEvent( + 0, event.type == EventType::KeyDown ? QEvent::KeyPress : QEvent::KeyRelease, event.key, + event.modifiers, event.text, event.autoRepeat); + return clipboardResult == ProcessKeyboardResult::NativeClipboardEventAndCopiedDataNeeded + ? ProceedToNativeEvent + : result; } -bool QWasmWindow::isPointOnResizeRegion(QPoint point) const +bool QWasmWindow::processPointer(const PointerEvent &event) { - if (window()->flags().testFlag(Qt::Popup)) + if (event.pointerType != PointerType::Mouse && event.pointerType != PointerType::Pen) return false; - return resizeRegion().contains(point); -} - -QWasmWindow::ResizeMode QWasmWindow::resizeModeAtPoint(QPoint point) const -{ - QPoint p1 = window()->frameGeometry().topLeft() - QPoint(5, 5); - QPoint p2 = window()->frameGeometry().bottomRight() + QPoint(5, 5); - int corner = 20; - - QRect top(p1, QPoint(p2.x(), p1.y() + corner)); - QRect middle(QPoint(p1.x(), p1.y() + corner), QPoint(p2.x(), p2.y() - corner)); - QRect bottom(QPoint(p1.x(), p2.y() - corner), p2); - - QRect left(p1, QPoint(p1.x() + corner, p2.y())); - QRect center(QPoint(p1.x() + corner, p1.y()), QPoint(p2.x() - corner, p2.y())); - QRect right(QPoint(p2.x() - corner, p1.y()), p2); - - if (top.contains(point)) { - // Top - if (left.contains(point)) - return ResizeTopLeft; - if (center.contains(point)) - return ResizeTop; - if (right.contains(point)) - return ResizeTopRight; - } else if (middle.contains(point)) { - // Middle - if (left.contains(point)) - return ResizeLeft; - if (right.contains(point)) - return ResizeRight; - } else if (bottom.contains(point)) { - // Bottom - if (left.contains(point)) - return ResizeBottomLeft; - if (center.contains(point)) - return ResizeBottom; - if (right.contains(point)) - return ResizeBottomRight; + + switch (event.type) { + case EventType::PointerEnter: { + const auto pointInScreen = platformScreen()->mapFromLocal( + dom::mapPoint(event.target(), platformScreen()->element(), event.localPoint)); + QWindowSystemInterface::handleEnterEvent( + window(), m_window->mapFromGlobal(pointInScreen), pointInScreen); + break; } + case EventType::PointerLeave: + QWindowSystemInterface::handleLeaveEvent(window()); + break; + default: + break; + } + + return false; +} + +bool QWasmWindow::processWheel(const WheelEvent &event) +{ + // Web scroll deltas are inverted from Qt deltas - negate. + const int scrollFactor = -([&event]() { + switch (event.deltaMode) { + case DeltaMode::Pixel: + return 1; + case DeltaMode::Line: + return 12; + case DeltaMode::Page: + return 20; + }; + })(); + + const auto pointInScreen = platformScreen()->mapFromLocal( + dom::mapPoint(event.target(), platformScreen()->element(), event.localPoint)); - return ResizeNone; + return QWindowSystemInterface::handleWheelEvent( + window(), QWasmIntegration::getTimestamp(), window()->mapFromGlobal(pointInScreen), + pointInScreen, (event.delta * scrollFactor).toPoint(), + (event.delta * scrollFactor).toPoint(), event.modifiers, Qt::NoScrollPhase, + Qt::MouseEventNotSynthesized, event.webkitDirectionInvertedFromDevice); } -QRect getSubControlRect(const QWasmWindow *window, QWasmCompositor::SubControls subControl) +QRect QWasmWindow::normalGeometry() const { - QWasmCompositor::QWasmTitleBarOptions options = QWasmCompositor::makeTitleBarOptions(window); + return m_normalGeometry; +} - QRect r = QWasmCompositor::titlebarRect(options, subControl); - r.translate(window->window()->frameGeometry().x(), window->window()->frameGeometry().y()); +qreal QWasmWindow::devicePixelRatio() const +{ + return screen()->devicePixelRatio(); +} - return r; +void QWasmWindow::requestUpdate() +{ + m_compositor->requestUpdateWindow(this, QWasmCompositor::UpdateRequestDelivery); } -QRect QWasmWindow::maxButtonRect() const +bool QWasmWindow::hasFrame() const { - return getSubControlRect(this, QWasmCompositor::SC_TitleBarMaxButton); + return !m_flags.testFlag(Qt::FramelessWindowHint); } -QRect QWasmWindow::minButtonRect() const +bool QWasmWindow::hasBorder() const { - return getSubControlRect(this, QWasmCompositor::SC_TitleBarMinButton); + return hasFrame() && !m_state.testFlag(Qt::WindowFullScreen) && !m_flags.testFlag(Qt::SubWindow) + && !windowIsPopupType(m_flags) && !parent(); } -QRect QWasmWindow::closeButtonRect() const +bool QWasmWindow::hasTitleBar() const { - return getSubControlRect(this, QWasmCompositor::SC_TitleBarCloseButton); + return hasBorder() && m_flags.testFlag(Qt::WindowTitleHint); } -QRect QWasmWindow::normButtonRect() const +bool QWasmWindow::hasShadow() const { - return getSubControlRect(this, QWasmCompositor::SC_TitleBarNormalButton); + return hasBorder() && !m_flags.testFlag(Qt::NoDropShadowWindowHint); } -QRect QWasmWindow::sysMenuRect() const +bool QWasmWindow::hasMaximizeButton() const { - return getSubControlRect(this, QWasmCompositor::SC_TitleBarSysMenu); + return !m_state.testFlag(Qt::WindowMaximized) && m_flags.testFlag(Qt::WindowMaximizeButtonHint); } -QRegion QWasmWindow::titleControlRegion() const +bool QWasmWindow::windowIsPopupType(Qt::WindowFlags flags) const { - QRegion result; - result += closeButtonRect(); - result += minButtonRect(); - result += maxButtonRect(); - result += sysMenuRect(); + if (flags.testFlag(Qt::Tool)) + return false; // Qt::Tool has the Popup bit set but isn't an actual Popup window - return result; + return (flags.testFlag(Qt::Popup)); } -void QWasmWindow::invalidate() +void QWasmWindow::requestActivateWindow() { - m_compositor->requestRedraw(); + QWindow *modalWindow; + if (QGuiApplicationPrivate::instance()->isWindowBlocked(window(), &modalWindow)) { + static_cast<QWasmWindow *>(modalWindow->handle())->requestActivateWindow(); + return; + } + + raise(); + setAsActiveNode(); + + if (!QWasmIntegration::get()->inputContext()) + m_canvas.call<void>("focus"); + + QPlatformWindow::requestActivateWindow(); } -QWasmCompositor::SubControls QWasmWindow::activeSubControl() const +bool QWasmWindow::setMouseGrabEnabled(bool grab) { - return m_activeControl; + Q_UNUSED(grab); + return false; } -void QWasmWindow::setWindowState(Qt::WindowStates states) +bool QWasmWindow::windowEvent(QEvent *event) { - m_windowState = Qt::WindowNoState; - if (states & Qt::WindowMinimized) - m_windowState = Qt::WindowMinimized; - else if (states & Qt::WindowFullScreen) - m_windowState = Qt::WindowFullScreen; - else if (states & Qt::WindowMaximized) - m_windowState = Qt::WindowMaximized; + switch (event->type()) { + case QEvent::WindowBlocked: + m_qtWindow["classList"].call<void>("add", emscripten::val("blocked")); + return false; // Propagate further + case QEvent::WindowUnblocked:; + m_qtWindow["classList"].call<void>("remove", emscripten::val("blocked")); + return false; // Propagate further + default: + return QPlatformWindow::windowEvent(event); + } } -QRect QWasmWindow::normalGeometry() const +void QWasmWindow::setMask(const QRegion ®ion) { - return m_normalGeometry; + if (region.isEmpty()) { + m_qtWindow["style"].set("clipPath", emscripten::val("")); + return; + } + + std::ostringstream cssClipPath; + cssClipPath << "path('"; + for (const auto &rect : region) { + const auto cssRect = rect.adjusted(0, 0, 1, 1); + cssClipPath << "M " << cssRect.left() << " " << cssRect.top() << " "; + cssClipPath << "L " << cssRect.right() << " " << cssRect.top() << " "; + cssClipPath << "L " << cssRect.right() << " " << cssRect.bottom() << " "; + cssClipPath << "L " << cssRect.left() << " " << cssRect.bottom() << " z "; + } + cssClipPath << "')"; + m_qtWindow["style"].set("clipPath", emscripten::val(cssClipPath.str())); } -qreal QWasmWindow::devicePixelRatio() const +void QWasmWindow::setParent(const QPlatformWindow *) { - return screen()->devicePixelRatio(); + commitParent(parentNode()); } -void QWasmWindow::requestUpdate() +std::string QWasmWindow::canvasSelector() const { - QPointer<QWindow> windowPointer(window()); - bool registered = QWasmEventDispatcher::registerRequestUpdateCallback([=](){ - if (windowPointer.isNull()) - return; + return "!qtwindow" + std::to_string(m_winId); +} - deliverUpdateRequest(); - }); +emscripten::val QWasmWindow::containerElement() +{ + return m_windowContents; +} + +QWasmWindowTreeNode *QWasmWindow::parentNode() +{ + if (parent()) + return static_cast<QWasmWindow *>(parent()); + return platformScreen(); +} - if (!registered) - QPlatformWindow::requestUpdate(); +QWasmWindow *QWasmWindow::asWasmWindow() +{ + return this; } -bool QWasmWindow::hasTitleBar() const +void QWasmWindow::onParentChanged(QWasmWindowTreeNode *previous, QWasmWindowTreeNode *current, + QWasmWindowStack::PositionPreference positionPreference) { - return !(m_windowState & Qt::WindowFullScreen) && (window()->flags().testFlag(Qt::WindowTitleHint) && m_needsCompositor) - && !window()->flags().testFlag(Qt::Popup); + if (previous) + previous->containerElement().call<void>("removeChild", m_qtWindow); + if (current) + current->containerElement().call<void>("appendChild", m_qtWindow); + QWasmWindowTreeNode::onParentChanged(previous, current, positionPreference); } QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmwindow.h b/src/plugins/platforms/wasm/qwasmwindow.h index a098172649..ab0dc68e83 100644 --- a/src/plugins/platforms/wasm/qwasmwindow.h +++ b/src/plugins/platforms/wasm/qwasmwindow.h @@ -1,128 +1,175 @@ -/**************************************************************************** -** -** 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$ -** -****************************************************************************/ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #ifndef QWASMWINDOW_H #define QWASMWINDOW_H #include "qwasmintegration.h" #include <qpa/qplatformwindow.h> +#include <qpa/qplatformwindow_p.h> #include <emscripten/html5.h> #include "qwasmbackingstore.h" #include "qwasmscreen.h" #include "qwasmcompositor.h" +#include "qwasmwindownonclientarea.h" +#include "qwasmwindowstack.h" +#include "qwasmwindowtreenode.h" + +#include <QtCore/private/qstdweb_p.h> +#include "QtGui/qopenglcontext.h" +#include <QtOpenGL/qopengltextureblitter.h> + +#include <emscripten/val.h> + +#include <memory> QT_BEGIN_NAMESPACE -class QWasmCompositor; +namespace qstdweb { +class EventCallback; +} + +class ClientArea; +struct KeyEvent; +struct PointerEvent; +class QWasmDeadKeySupport; +struct WheelEvent; -class QWasmWindow : public QPlatformWindow +class QWasmWindow final : public QPlatformWindow, + public QWasmWindowTreeNode, + public QNativeInterface::Private::QWasmWindow { public: - enum ResizeMode { - ResizeNone, - ResizeTopLeft, - ResizeTop, - ResizeTopRight, - ResizeRight, - ResizeBottomRight, - ResizeBottom, - ResizeBottomLeft, - ResizeLeft - }; - - QWasmWindow(QWindow *w, QWasmCompositor *compositor, QWasmBackingStore *backingStore); - ~QWasmWindow(); - void destroy(); - + QWasmWindow(QWindow *w, QWasmDeadKeySupport *deadKeySupport, QWasmCompositor *compositor, + QWasmBackingStore *backingStore); + ~QWasmWindow() final; + + static QWasmWindow *fromWindow(QWindow *window); + QSurfaceFormat format() const override; + + void paint(); + void setZOrder(int order); + void setWindowCursor(QByteArray cssCursorName); + void onActivationChanged(bool active); + bool isVisible() const; + + void onNonClientAreaInteraction(); + void onRestoreClicked(); + void onMaximizeClicked(); + void onToggleMaximized(); + void onCloseClicked(); + bool onNonClientEvent(const PointerEvent &event); + + // QPlatformWindow: void initialize() override; - void setGeometry(const QRect &) override; void setVisible(bool visible) override; QMargins frameMargins() const override; - WId winId() const override; - void propagateSizeHints() override; + void setOpacity(qreal level) override; void raise() override; void lower() override; QRect normalGeometry() const override; qreal devicePixelRatio() const override; void requestUpdate() override; + void requestActivateWindow() override; + void setWindowFlags(Qt::WindowFlags flags) override; + void setWindowState(Qt::WindowStates state) override; + void setWindowTitle(const QString &title) override; + void setWindowIcon(const QIcon &icon) override; + bool setKeyboardGrabEnabled(bool) override { return false; } + bool setMouseGrabEnabled(bool grab) final; + bool windowEvent(QEvent *event) final; + void setMask(const QRegion ®ion) final; + void setParent(const QPlatformWindow *window) final; QWasmScreen *platformScreen() const; void setBackingStore(QWasmBackingStore *store) { m_backingStore = store; } QWasmBackingStore *backingStore() const { return m_backingStore; } QWindow *window() const { return m_window; } - void injectMousePressed(const QPoint &local, const QPoint &global, - Qt::MouseButton button, Qt::KeyboardModifiers mods); - void injectMouseReleased(const QPoint &local, const QPoint &global, - Qt::MouseButton button, Qt::KeyboardModifiers mods); - - int titleHeight() const; - int borderWidth() const; - QRegion titleGeometry() const; - QRegion resizeRegion() const; - bool isPointOnTitle(QPoint point) const; - bool isPointOnResizeRegion(QPoint point) const; - ResizeMode resizeModeAtPoint(QPoint point) const; - QRect maxButtonRect() const; - QRect minButtonRect() const; - QRect closeButtonRect() const; - QRect sysMenuRect() const; - QRect normButtonRect() const; - QRegion titleControlRegion() const; - QWasmCompositor::SubControls activeSubControl() const; + std::string canvasSelector() const; - void setWindowState(Qt::WindowStates state) override; - bool setKeyboardGrabEnabled(bool) override { return false; } - bool setMouseGrabEnabled(bool) override { return false; } + emscripten::val context2d() const { return m_context2d; } + emscripten::val a11yContainer() const { return m_a11yContainer; } + emscripten::val inputHandlerElement() const { return m_windowContents; } + + // QNativeInterface::Private::QWasmWindow + emscripten::val document() const override { return m_document; } + emscripten::val clientArea() const override { return m_qtWindow; } + + // QWasmWindowTreeNode: + emscripten::val containerElement() final; + QWasmWindowTreeNode *parentNode() final; + +private: + friend class QWasmScreen; + static constexpr auto defaultWindowSize = 160; + + // QWasmWindowTreeNode: + QWasmWindow *asWasmWindow() final; + void onParentChanged(QWasmWindowTreeNode *previous, QWasmWindowTreeNode *current, + QWasmWindowStack::PositionPreference positionPreference) final; -protected: void invalidate(); + bool hasFrame() const; bool hasTitleBar() const; + bool hasBorder() const; + bool hasShadow() const; + bool hasMaximizeButton() const; + void applyWindowState(); + void commitParent(QWasmWindowTreeNode *parent); -protected: - friend class QWasmScreen; + bool processKey(const KeyEvent &event); + bool processPointer(const PointerEvent &event); + bool processWheel(const WheelEvent &event); - QWindow* m_window = nullptr; + QWindow *m_window = nullptr; QWasmCompositor *m_compositor = nullptr; QWasmBackingStore *m_backingStore = nullptr; + QWasmDeadKeySupport *m_deadKeySupport; QRect m_normalGeometry {0, 0, 0 ,0}; - Qt::WindowState m_windowState = Qt::WindowNoState; - QWasmCompositor::SubControls m_activeControl = QWasmCompositor::SC_None; - WId m_winid = 0; + emscripten::val m_document; + emscripten::val m_qtWindow; + emscripten::val m_windowContents; + emscripten::val m_canvasContainer; + emscripten::val m_a11yContainer; + emscripten::val m_canvas; + emscripten::val m_context2d = emscripten::val::undefined(); + + std::unique_ptr<NonClientArea> m_nonClientArea; + std::unique_ptr<ClientArea> m_clientArea; + + QWasmWindowTreeNode *m_commitedParent = nullptr; + + std::unique_ptr<qstdweb::EventCallback> m_keyDownCallback; + std::unique_ptr<qstdweb::EventCallback> m_keyUpCallback; + + std::unique_ptr<qstdweb::EventCallback> m_pointerLeaveCallback; + std::unique_ptr<qstdweb::EventCallback> m_pointerEnterCallback; + + std::unique_ptr<qstdweb::EventCallback> m_dropCallback; + + std::unique_ptr<qstdweb::EventCallback> m_wheelEventCallback; + + Qt::WindowStates m_state = Qt::WindowNoState; + Qt::WindowStates m_previousWindowState = Qt::WindowNoState; + + Qt::WindowFlags m_flags = Qt::Widget; + + QPoint m_lastPointerMovePoint; + + WId m_winId = 0; + bool m_wantCapture = false; bool m_hasTitle = false; bool m_needsCompositor = false; + long m_requestAnimationFrameId = -1; friend class QWasmCompositor; friend class QWasmEventTranslator; + bool windowIsPopupType(Qt::WindowFlags flags) const; }; + QT_END_NAMESPACE #endif // QWASMWINDOW_H diff --git a/src/plugins/platforms/wasm/qwasmwindowclientarea.cpp b/src/plugins/platforms/wasm/qwasmwindowclientarea.cpp new file mode 100644 index 0000000000..6da3e24c05 --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmwindowclientarea.cpp @@ -0,0 +1,195 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "qwasmwindowclientarea.h" + +#include "qwasmdom.h" +#include "qwasmevent.h" +#include "qwasmscreen.h" +#include "qwasmwindow.h" +#include "qwasmdrag.h" + +#include <QtGui/private/qguiapplication_p.h> +#include <QtGui/qpointingdevice.h> + +#include <QtCore/qassert.h> + +QT_BEGIN_NAMESPACE + +ClientArea::ClientArea(QWasmWindow *window, QWasmScreen *screen, emscripten::val element) + : m_screen(screen), m_window(window), m_element(element) +{ + const auto callback = std::function([this](emscripten::val event) { + processPointer(*PointerEvent::fromWeb(event)); + event.call<void>("preventDefault"); + event.call<void>("stopPropagation"); + }); + + m_pointerDownCallback = + std::make_unique<qstdweb::EventCallback>(element, "pointerdown", callback); + m_pointerMoveCallback = + std::make_unique<qstdweb::EventCallback>(element, "pointermove", callback); + m_pointerUpCallback = std::make_unique<qstdweb::EventCallback>(element, "pointerup", callback); + m_pointerCancelCallback = + std::make_unique<qstdweb::EventCallback>(element, "pointercancel", callback); + + element.call<void>("setAttribute", emscripten::val("draggable"), emscripten::val("true")); + + m_dragStartCallback = std::make_unique<qstdweb::EventCallback>( + element, "dragstart", [this](emscripten::val webEvent) { + webEvent.call<void>("preventDefault"); + auto event = *DragEvent::fromWeb(webEvent, m_window->window()); + QWasmDrag::instance()->onNativeDragStarted(&event); + }); + m_dragOverCallback = std::make_unique<qstdweb::EventCallback>( + element, "dragover", [this](emscripten::val webEvent) { + webEvent.call<void>("preventDefault"); + auto event = *DragEvent::fromWeb(webEvent, m_window->window()); + QWasmDrag::instance()->onNativeDragOver(&event); + }); + m_dropCallback = std::make_unique<qstdweb::EventCallback>( + element, "drop", [this](emscripten::val webEvent) { + webEvent.call<void>("preventDefault"); + auto event = *DragEvent::fromWeb(webEvent, m_window->window()); + QWasmDrag::instance()->onNativeDrop(&event); + }); + m_dragEndCallback = std::make_unique<qstdweb::EventCallback>( + element, "dragend", [this](emscripten::val webEvent) { + webEvent.call<void>("preventDefault"); + auto event = *DragEvent::fromWeb(webEvent, m_window->window()); + QWasmDrag::instance()->onNativeDragFinished(&event); + }); + +} + +bool ClientArea::processPointer(const PointerEvent &event) +{ + + switch (event.type) { + case EventType::PointerDown: + m_element.call<void>("setPointerCapture", event.pointerId); + if ((m_window->window()->flags() & Qt::WindowDoesNotAcceptFocus) + != Qt::WindowDoesNotAcceptFocus + && m_window->window()->isTopLevel()) + m_window->window()->requestActivate(); + break; + case EventType::PointerUp: + m_element.call<void>("releasePointerCapture", event.pointerId); + break; + default: + break; + }; + + const bool eventAccepted = deliverEvent(event); + if (!eventAccepted && event.type == EventType::PointerDown) + QGuiApplicationPrivate::instance()->closeAllPopups(); + return eventAccepted; +} + +bool ClientArea::deliverEvent(const PointerEvent &event) +{ + const auto pointInScreen = m_screen->mapFromLocal( + dom::mapPoint(event.target(), m_screen->element(), event.localPoint)); + + const auto geometryF = m_screen->geometry().toRectF(); + const QPointF targetPointClippedToScreen( + qBound(geometryF.left(), pointInScreen.x(), geometryF.right()), + qBound(geometryF.top(), pointInScreen.y(), geometryF.bottom())); + + if (event.pointerType == PointerType::Mouse) { + const QEvent::Type eventType = + MouseEvent::mouseEventTypeFromEventType(event.type, WindowArea::Client); + + return eventType != QEvent::None + && QWindowSystemInterface::handleMouseEvent( + m_window->window(), QWasmIntegration::getTimestamp(), + m_window->window()->mapFromGlobal(targetPointClippedToScreen), + targetPointClippedToScreen, event.mouseButtons, event.mouseButton, + eventType, event.modifiers); + } + + if (event.pointerType == PointerType::Pen) { + qreal pressure; + switch (event.type) { + case EventType::PointerDown : + case EventType::PointerMove : + pressure = event.pressure; + break; + case EventType::PointerUp : + pressure = 0.0; + break; + default: + return false; + } + // Tilt in the browser is in the range +-90, but QTabletEvent only goes to +-60. + qreal xTilt = qBound(-60.0, event.tiltX, 60.0); + qreal yTilt = qBound(-60.0, event.tiltY, 60.0); + // Barrel rotation is reported as 0 to 359, but QTabletEvent wants a signed value. + qreal rotation = event.twist > 180.0 ? 360.0 - event.twist : event.twist; + return QWindowSystemInterface::handleTabletEvent( + m_window->window(), QWasmIntegration::getTimestamp(), m_screen->tabletDevice(), + m_window->window()->mapFromGlobal(targetPointClippedToScreen), + targetPointClippedToScreen, event.mouseButtons, pressure, xTilt, yTilt, + event.tangentialPressure, rotation, event.modifiers); + } + + QWindowSystemInterface::TouchPoint *touchPoint; + + QPointF pointInTargetWindowCoords = + QPointF(m_window->window()->mapFromGlobal(targetPointClippedToScreen)); + QPointF normalPosition(pointInTargetWindowCoords.x() / m_window->window()->width(), + pointInTargetWindowCoords.y() / m_window->window()->height()); + + const auto tp = m_pointerIdToTouchPoints.find(event.pointerId); + if (event.pointerType != PointerType::Pen && tp != m_pointerIdToTouchPoints.end()) { + touchPoint = &tp.value(); + } else { + touchPoint = &m_pointerIdToTouchPoints + .insert(event.pointerId, QWindowSystemInterface::TouchPoint()) + .value(); + + // Assign touch point id. TouchPoint::id is int, but QGuiApplicationPrivate::processTouchEvent() + // will not synthesize mouse events for touch points with negative id; use the absolute value for + // the touch point id. + touchPoint->id = qAbs(event.pointerId); + + touchPoint->state = QEventPoint::State::Pressed; + } + + const bool stationaryTouchPoint = (normalPosition == touchPoint->normalPosition); + touchPoint->normalPosition = normalPosition; + touchPoint->area = QRectF(targetPointClippedToScreen, QSizeF(event.width, event.height)) + .translated(-event.width / 2, -event.height / 2); + touchPoint->pressure = event.pressure; + + switch (event.type) { + case EventType::PointerUp: + touchPoint->state = QEventPoint::State::Released; + break; + case EventType::PointerMove: + touchPoint->state = (stationaryTouchPoint ? QEventPoint::State::Stationary + : QEventPoint::State::Updated); + break; + default: + break; + } + + QList<QWindowSystemInterface::TouchPoint> touchPointList; + touchPointList.reserve(m_pointerIdToTouchPoints.size()); + std::transform(m_pointerIdToTouchPoints.begin(), m_pointerIdToTouchPoints.end(), + std::back_inserter(touchPointList), + [](const QWindowSystemInterface::TouchPoint &val) { return val; }); + + if (event.type == EventType::PointerUp) + m_pointerIdToTouchPoints.remove(event.pointerId); + + return event.type == EventType::PointerCancel + ? QWindowSystemInterface::handleTouchCancelEvent( + m_window->window(), QWasmIntegration::getTimestamp(), m_screen->touchDevice(), + event.modifiers) + : QWindowSystemInterface::handleTouchEvent( + m_window->window(), QWasmIntegration::getTimestamp(), m_screen->touchDevice(), + touchPointList, event.modifiers); +} + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmwindowclientarea.h b/src/plugins/platforms/wasm/qwasmwindowclientarea.h new file mode 100644 index 0000000000..ba745a59a8 --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmwindowclientarea.h @@ -0,0 +1,52 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef QWASMWINDOWCLIENTAREA_H +#define QWASMWINDOWCLIENTAREA_H + +#include <QtCore/qnamespace.h> +#include <qpa/qwindowsysteminterface.h> +#include <QtCore/QMap> + +#include <emscripten/val.h> + +#include <memory> + +QT_BEGIN_NAMESPACE + +namespace qstdweb { +class EventCallback; +} + +struct PointerEvent; +class QWasmScreen; +class QWasmWindow; + +class ClientArea +{ +public: + ClientArea(QWasmWindow *window, QWasmScreen *screen, emscripten::val element); + +private: + bool processPointer(const PointerEvent &event); + bool deliverEvent(const PointerEvent &event); + + std::unique_ptr<qstdweb::EventCallback> m_pointerDownCallback; + std::unique_ptr<qstdweb::EventCallback> m_pointerMoveCallback; + std::unique_ptr<qstdweb::EventCallback> m_pointerUpCallback; + std::unique_ptr<qstdweb::EventCallback> m_pointerCancelCallback; + + std::unique_ptr<qstdweb::EventCallback> m_dragOverCallback; + std::unique_ptr<qstdweb::EventCallback> m_dragStartCallback; + std::unique_ptr<qstdweb::EventCallback> m_dragEndCallback; + std::unique_ptr<qstdweb::EventCallback> m_dropCallback; + + QMap<int, QWindowSystemInterface::TouchPoint> m_pointerIdToTouchPoints; + + QWasmScreen *m_screen; + QWasmWindow *m_window; + emscripten::val m_element; +}; + +QT_END_NAMESPACE +#endif // QWASMWINDOWNONCLIENTAREA_H diff --git a/src/plugins/platforms/wasm/qwasmwindownonclientarea.cpp b/src/plugins/platforms/wasm/qwasmwindownonclientarea.cpp new file mode 100644 index 0000000000..00fa8fb236 --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmwindownonclientarea.cpp @@ -0,0 +1,460 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "qwasmwindownonclientarea.h" + +#include "qwasmbase64iconstore.h" +#include "qwasmdom.h" +#include "qwasmevent.h" +#include "qwasmintegration.h" + +#include <qpa/qwindowsysteminterface.h> + +#include <QtCore/qassert.h> + +QT_BEGIN_NAMESPACE + +WebImageButton::Callbacks::Callbacks() = default; +WebImageButton::Callbacks::Callbacks(std::function<void()> onInteraction, + std::function<void()> onClick) + : m_onInteraction(std::move(onInteraction)), m_onClick(std::move(onClick)) +{ + Q_ASSERT_X(!!m_onInteraction == !!m_onClick, Q_FUNC_INFO, + "Both callbacks need to be either null or non-null"); +} +WebImageButton::Callbacks::~Callbacks() = default; + +WebImageButton::Callbacks::Callbacks(Callbacks &&) = default; +WebImageButton::Callbacks &WebImageButton::Callbacks::operator=(Callbacks &&) = default; + +void WebImageButton::Callbacks::onInteraction() +{ + return m_onInteraction(); +} + +void WebImageButton::Callbacks::onClick() +{ + return m_onClick(); +} + +WebImageButton::WebImageButton() + : m_containerElement( + dom::document().call<emscripten::val>("createElement", emscripten::val("div"))), + m_imgElement(dom::document().call<emscripten::val>("createElement", emscripten::val("img"))) +{ + m_imgElement.set("draggable", false); + + m_containerElement["classList"].call<void>("add", emscripten::val("image-button")); + m_containerElement.call<void>("appendChild", m_imgElement); +} + +WebImageButton::~WebImageButton() = default; + +void WebImageButton::setCallbacks(Callbacks callbacks) +{ + if (callbacks) { + if (!m_webClickEventCallback) { + m_webMouseDownEventCallback = std::make_unique<qstdweb::EventCallback>( + m_containerElement, "pointerdown", [this](emscripten::val event) { + event.call<void>("preventDefault"); + event.call<void>("stopPropagation"); + m_callbacks.onInteraction(); + }); + m_webClickEventCallback = std::make_unique<qstdweb::EventCallback>( + m_containerElement, "click", [this](emscripten::val event) { + m_callbacks.onClick(); + event.call<void>("stopPropagation"); + }); + } + } else { + m_webMouseDownEventCallback.reset(); + m_webClickEventCallback.reset(); + } + dom::syncCSSClassWith(m_containerElement, "action-button", !!callbacks); + m_callbacks = std::move(callbacks); +} + +void WebImageButton::setImage(std::string_view imageData, std::string_view format) +{ + m_imgElement.set("src", + "data:image/" + std::string(format) + ";base64," + std::string(imageData)); +} + +void WebImageButton::setVisible(bool visible) +{ + m_containerElement["style"].set("display", visible ? "flex" : "none"); +} + +Resizer::ResizerElement::ResizerElement(emscripten::val parentElement, Qt::Edges edges, + Resizer *resizer) + : m_element(dom::document().call<emscripten::val>("createElement", emscripten::val("div"))), + m_edges(edges), + m_resizer(resizer) +{ + Q_ASSERT_X(m_resizer, Q_FUNC_INFO, "Resizer cannot be null"); + + m_element["classList"].call<void>("add", emscripten::val("resize-outline")); + m_element["classList"].call<void>("add", emscripten::val(cssClassNameForEdges(edges))); + + parentElement.call<void>("appendChild", m_element); + + m_mouseDownEvent = std::make_unique<qstdweb::EventCallback>( + m_element, "pointerdown", [this](emscripten::val event) { + if (!onPointerDown(*PointerEvent::fromWeb(event))) + return; + m_resizer->onInteraction(); + event.call<void>("preventDefault"); + event.call<void>("stopPropagation"); + }); + m_mouseMoveEvent = std::make_unique<qstdweb::EventCallback>( + m_element, "pointermove", [this](emscripten::val event) { + if (onPointerMove(*PointerEvent::fromWeb(event))) + event.call<void>("preventDefault"); + }); + m_mouseUpEvent = std::make_unique<qstdweb::EventCallback>( + m_element, "pointerup", [this](emscripten::val event) { + if (onPointerUp(*PointerEvent::fromWeb(event))) { + event.call<void>("preventDefault"); + event.call<void>("stopPropagation"); + } + }); +} + +Resizer::ResizerElement::~ResizerElement() +{ + m_element["parentElement"].call<emscripten::val>("removeChild", m_element); +} + +Resizer::ResizerElement::ResizerElement(ResizerElement &&other) = default; + +bool Resizer::ResizerElement::onPointerDown(const PointerEvent &event) +{ + m_element.call<void>("setPointerCapture", event.pointerId); + m_capturedPointerId = event.pointerId; + + m_resizer->startResize(m_edges, event); + return true; +} + +bool Resizer::ResizerElement::onPointerMove(const PointerEvent &event) +{ + if (m_capturedPointerId != event.pointerId) + return false; + + m_resizer->continueResize(event); + return true; +} + +bool Resizer::ResizerElement::onPointerUp(const PointerEvent &event) +{ + if (m_capturedPointerId != event.pointerId) + return false; + + m_resizer->finishResize(); + m_element.call<void>("releasePointerCapture", event.pointerId); + m_capturedPointerId = -1; + return true; +} + +Resizer::Resizer(QWasmWindow *window, emscripten::val parentElement) + : m_window(window), m_windowElement(parentElement) +{ + Q_ASSERT_X(m_window, Q_FUNC_INFO, "Window must not be null"); + + constexpr std::array<int, 8> ResizeEdges = { Qt::TopEdge | Qt::LeftEdge, + Qt::TopEdge, + Qt::TopEdge | Qt::RightEdge, + Qt::LeftEdge, + Qt::RightEdge, + Qt::BottomEdge | Qt::LeftEdge, + Qt::BottomEdge, + Qt::BottomEdge | Qt::RightEdge }; + std::transform(std::begin(ResizeEdges), std::end(ResizeEdges), std::back_inserter(m_elements), + [parentElement, this](int edges) { + return std::make_unique<ResizerElement>(parentElement, + Qt::Edges::fromInt(edges), this); + }); +} + +Resizer::~Resizer() = default; + +ResizeConstraints Resizer::getResizeConstraints() { + const auto *window = m_window->window(); + const auto minShrink = QPoint(window->minimumWidth() - window->geometry().width(), + window->minimumHeight() - window->geometry().height()); + const auto maxGrow = QPoint(window->maximumWidth() - window->geometry().width(), + window->maximumHeight() - window->geometry().height()); + + const auto frameRect = + QRectF::fromDOMRect(m_windowElement.call<emscripten::val>("getBoundingClientRect")); + auto containerGeometry = + QRectF::fromDOMRect(m_window->parentNode()->containerElement().call<emscripten::val>( + "getBoundingClientRect")); + + const int maxGrowTop = frameRect.top() - containerGeometry.top(); + + return ResizeConstraints{minShrink, maxGrow, maxGrowTop}; +} + +void Resizer::onInteraction() +{ + m_window->onNonClientAreaInteraction(); +} + +void Resizer::startResize(Qt::Edges resizeEdges, const PointerEvent &event) +{ + Q_ASSERT_X(!m_currentResizeData, Q_FUNC_INFO, "Another resize in progress"); + + m_currentResizeData.reset(new ResizeData{ + .edges = resizeEdges, + .originInScreenCoords = dom::mapPoint( + event.target(), m_window->platformScreen()->element(), event.localPoint), + }); + + const auto resizeConstraints = getResizeConstraints(); + m_currentResizeData->minShrink = resizeConstraints.minShrink; + + m_currentResizeData->maxGrow = + QPoint(resizeConstraints.maxGrow.x(), + std::min(resizeEdges & Qt::Edge::TopEdge ? resizeConstraints.maxGrowTop : INT_MAX, + resizeConstraints.maxGrow.y())); + + m_currentResizeData->initialBounds = m_window->window()->geometry(); +} + +void Resizer::continueResize(const PointerEvent &event) +{ + const auto pointInScreen = + dom::mapPoint(event.target(), m_window->platformScreen()->element(), event.localPoint); + const auto amount = (pointInScreen - m_currentResizeData->originInScreenCoords).toPoint(); + const QPoint cappedGrowVector( + std::min(m_currentResizeData->maxGrow.x(), + std::max(m_currentResizeData->minShrink.x(), + (m_currentResizeData->edges & Qt::Edge::LeftEdge) ? -amount.x() + : (m_currentResizeData->edges & Qt::Edge::RightEdge) + ? amount.x() + : 0)), + std::min(m_currentResizeData->maxGrow.y(), + std::max(m_currentResizeData->minShrink.y(), + (m_currentResizeData->edges & Qt::Edge::TopEdge) ? -amount.y() + : (m_currentResizeData->edges & Qt::Edge::BottomEdge) + ? amount.y() + : 0))); + + auto bounds = m_currentResizeData->initialBounds.adjusted( + (m_currentResizeData->edges & Qt::Edge::LeftEdge) ? -cappedGrowVector.x() : 0, + (m_currentResizeData->edges & Qt::Edge::TopEdge) ? -cappedGrowVector.y() : 0, + (m_currentResizeData->edges & Qt::Edge::RightEdge) ? cappedGrowVector.x() : 0, + (m_currentResizeData->edges & Qt::Edge::BottomEdge) ? cappedGrowVector.y() : 0); + + m_window->window()->setGeometry(bounds); +} + +void Resizer::finishResize() +{ + Q_ASSERT_X(m_currentResizeData, Q_FUNC_INFO, "No resize in progress"); + m_currentResizeData.reset(); +} + +TitleBar::TitleBar(QWasmWindow *window, emscripten::val parentElement) + : m_window(window), + m_element(dom::document().call<emscripten::val>("createElement", emscripten::val("div"))), + m_label(dom::document().call<emscripten::val>("createElement", emscripten::val("div"))) +{ + m_icon = std::make_unique<WebImageButton>(); + m_icon->setImage(Base64IconStore::get()->getIcon(Base64IconStore::IconType::QtLogo), "svg+xml"); + m_element.call<void>("appendChild", m_icon->htmlElement()); + m_element.set("className", "title-bar"); + + auto spacer = dom::document().call<emscripten::val>("createElement", emscripten::val("div")); + spacer["style"].set("width", "4px"); + m_element.call<void>("appendChild", spacer); + + m_label.set("className", "window-name"); + + m_element.call<void>("appendChild", m_label); + + spacer = dom::document().call<emscripten::val>("createElement", emscripten::val("div")); + spacer.set("className", "spacer"); + m_element.call<void>("appendChild", spacer); + + m_restore = std::make_unique<WebImageButton>(); + m_restore->setImage(Base64IconStore::get()->getIcon(Base64IconStore::IconType::Restore), + "svg+xml"); + m_restore->setCallbacks( + WebImageButton::Callbacks([this]() { m_window->onNonClientAreaInteraction(); }, + [this]() { m_window->onRestoreClicked(); })); + + m_element.call<void>("appendChild", m_restore->htmlElement()); + + m_maximize = std::make_unique<WebImageButton>(); + m_maximize->setImage(Base64IconStore::get()->getIcon(Base64IconStore::IconType::Maximize), + "svg+xml"); + m_maximize->setCallbacks( + WebImageButton::Callbacks([this]() { m_window->onNonClientAreaInteraction(); }, + [this]() { m_window->onMaximizeClicked(); })); + + m_element.call<void>("appendChild", m_maximize->htmlElement()); + + m_close = std::make_unique<WebImageButton>(); + m_close->setImage(Base64IconStore::get()->getIcon(Base64IconStore::IconType::X), "svg+xml"); + m_close->setCallbacks( + WebImageButton::Callbacks([this]() { m_window->onNonClientAreaInteraction(); }, + [this]() { m_window->onCloseClicked(); })); + + m_element.call<void>("appendChild", m_close->htmlElement()); + + parentElement.call<void>("appendChild", m_element); + + m_mouseDownEvent = std::make_unique<qstdweb::EventCallback>( + m_element, "pointerdown", [this](emscripten::val event) { + if (!onPointerDown(*PointerEvent::fromWeb(event))) + return; + m_window->onNonClientAreaInteraction(); + event.call<void>("preventDefault"); + event.call<void>("stopPropagation"); + }); + m_mouseMoveEvent = std::make_unique<qstdweb::EventCallback>( + m_element, "pointermove", [this](emscripten::val event) { + if (onPointerMove(*PointerEvent::fromWeb(event))) { + event.call<void>("preventDefault"); + } + }); + m_mouseUpEvent = std::make_unique<qstdweb::EventCallback>( + m_element, "pointerup", [this](emscripten::val event) { + if (onPointerUp(*PointerEvent::fromWeb(event))) { + event.call<void>("preventDefault"); + event.call<void>("stopPropagation"); + } + }); + m_doubleClickEvent = std::make_unique<qstdweb::EventCallback>( + m_element, "dblclick", [this](emscripten::val event) { + if (onDoubleClick()) { + event.call<void>("preventDefault"); + event.call<void>("stopPropagation"); + } + }); +} + +TitleBar::~TitleBar() +{ + m_element["parentElement"].call<emscripten::val>("removeChild", m_element); +} + +void TitleBar::setTitle(const QString &title) +{ + m_label.set("innerText", emscripten::val(title.toStdString())); +} + +void TitleBar::setRestoreVisible(bool visible) +{ + m_restore->setVisible(visible); +} + +void TitleBar::setMaximizeVisible(bool visible) +{ + m_maximize->setVisible(visible); +} + +void TitleBar::setCloseVisible(bool visible) +{ + m_close->setVisible(visible); +} + +void TitleBar::setIcon(std::string_view imageData, std::string_view format) +{ + m_icon->setImage(imageData, format); +} + +void TitleBar::setWidth(int width) +{ + m_element["style"].set("width", std::to_string(width) + "px"); +} + +QRectF TitleBar::geometry() const +{ + return QRectF::fromDOMRect(m_element.call<emscripten::val>("getBoundingClientRect")); +} + +bool TitleBar::onPointerDown(const PointerEvent &event) +{ + m_element.call<void>("setPointerCapture", event.pointerId); + m_capturedPointerId = event.pointerId; + + m_moveStartWindowPosition = m_window->window()->position(); + m_moveStartPoint = clipPointWithScreen(event.localPoint); + m_window->onNonClientEvent(event); + return true; +} + +bool TitleBar::onPointerMove(const PointerEvent &event) +{ + if (m_capturedPointerId != event.pointerId) + return false; + + const QPoint delta = (clipPointWithScreen(event.localPoint) - m_moveStartPoint).toPoint(); + + m_window->window()->setPosition(m_moveStartWindowPosition + delta); + m_window->onNonClientEvent(event); + return true; +} + +bool TitleBar::onPointerUp(const PointerEvent &event) +{ + if (m_capturedPointerId != event.pointerId) + return false; + + m_element.call<void>("releasePointerCapture", event.pointerId); + m_capturedPointerId = -1; + m_window->onNonClientEvent(event); + return true; +} + +bool TitleBar::onDoubleClick() +{ + m_window->onToggleMaximized(); + return true; +} + +QPointF TitleBar::clipPointWithScreen(const QPointF &pointInTitleBarCoords) const +{ + auto containerRect = + QRectF::fromDOMRect(m_window->parentNode()->containerElement().call<emscripten::val>( + "getBoundingClientRect")); + const auto p = dom::mapPoint(m_element, m_window->parentNode()->containerElement(), + pointInTitleBarCoords); + + auto result = QPointF(qBound(0., qreal(p.x()), containerRect.width()), + qBound(0., qreal(p.y()), containerRect.height())); + return m_window->parent() ? result : m_window->platformScreen()->mapFromLocal(result).toPoint(); +} + +NonClientArea::NonClientArea(QWasmWindow *window, emscripten::val qtWindowElement) + : m_qtWindowElement(qtWindowElement), + m_resizer(std::make_unique<Resizer>(window, m_qtWindowElement)), + m_titleBar(std::make_unique<TitleBar>(window, m_qtWindowElement)) +{ + updateResizability(); +} + +NonClientArea::~NonClientArea() = default; + +void NonClientArea::onClientAreaWidthChange(int width) +{ + m_titleBar->setWidth(width); +} + +void NonClientArea::propagateSizeHints() +{ + updateResizability(); +} + +void NonClientArea::updateResizability() +{ + const auto resizeConstraints = m_resizer->getResizeConstraints(); + const bool nonResizable = resizeConstraints.minShrink.isNull() + && resizeConstraints.maxGrow.isNull() && resizeConstraints.maxGrowTop == 0; + dom::syncCSSClassWith(m_qtWindowElement, "no-resize", nonResizable); +} + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmwindownonclientarea.h b/src/plugins/platforms/wasm/qwasmwindownonclientarea.h new file mode 100644 index 0000000000..78c77585a0 --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmwindownonclientarea.h @@ -0,0 +1,227 @@ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef QWASMWINDOWNONCLIENTAREA_H +#define QWASMWINDOWNONCLIENTAREA_H + +#include <QtCore/qrect.h> +#include <QtCore/qtconfigmacros.h> +#include <QtCore/qnamespace.h> + +#include <emscripten/val.h> + +#include <functional> +#include <memory> +#include <string_view> +#include <vector> + +QT_BEGIN_NAMESPACE + +namespace qstdweb { +class EventCallback; +} + +struct PointerEvent; +class QWindow; +class Resizer; +class TitleBar; +class QWasmWindow; + +class NonClientArea +{ +public: + NonClientArea(QWasmWindow *window, emscripten::val containerElement); + ~NonClientArea(); + + void onClientAreaWidthChange(int width); + void propagateSizeHints(); + TitleBar *titleBar() const { return m_titleBar.get(); } + +private: + void updateResizability(); + + emscripten::val m_qtWindowElement; + std::unique_ptr<Resizer> m_resizer; + std::unique_ptr<TitleBar> m_titleBar; +}; + +class WebImageButton +{ +public: + class Callbacks + { + public: + Callbacks(); + Callbacks(std::function<void()> onInteraction, std::function<void()> onClick); + ~Callbacks(); + + Callbacks(const Callbacks &) = delete; + Callbacks(Callbacks &&); + Callbacks &operator=(const Callbacks &) = delete; + Callbacks &operator=(Callbacks &&); + + operator bool() const { return !!m_onInteraction; } + + void onInteraction(); + void onClick(); + + private: + std::function<void()> m_onInteraction; + std::function<void()> m_onClick; + }; + + WebImageButton(); + ~WebImageButton(); + + void setCallbacks(Callbacks callbacks); + void setImage(std::string_view imageData, std::string_view format); + void setVisible(bool visible); + + emscripten::val htmlElement() const { return m_containerElement; } + emscripten::val imageElement() const { return m_imgElement; } + +private: + emscripten::val m_containerElement; + emscripten::val m_imgElement; + + std::unique_ptr<qstdweb::EventCallback> m_webMouseMoveEventCallback; + std::unique_ptr<qstdweb::EventCallback> m_webMouseDownEventCallback; + std::unique_ptr<qstdweb::EventCallback> m_webClickEventCallback; + + Callbacks m_callbacks; +}; + +struct ResizeConstraints { + QPoint minShrink; + QPoint maxGrow; + int maxGrowTop; +}; + +class Resizer +{ +public: + class ResizerElement + { + public: + static constexpr const char *cssClassNameForEdges(Qt::Edges edges) + { + switch (edges) { + case Qt::TopEdge | Qt::LeftEdge:; + return "nw"; + case Qt::TopEdge: + return "n"; + case Qt::TopEdge | Qt::RightEdge: + return "ne"; + case Qt::LeftEdge: + return "w"; + case Qt::RightEdge: + return "e"; + case Qt::BottomEdge | Qt::LeftEdge: + return "sw"; + case Qt::BottomEdge: + return "s"; + case Qt::BottomEdge | Qt::RightEdge: + return "se"; + default: + return ""; + } + } + + ResizerElement(emscripten::val parentElement, Qt::Edges edges, Resizer *resizer); + ~ResizerElement(); + ResizerElement(const ResizerElement &other) = delete; + ResizerElement(ResizerElement &&other); + ResizerElement &operator=(const ResizerElement &other) = delete; + ResizerElement &operator=(ResizerElement &&other) = delete; + + bool onPointerDown(const PointerEvent &event); + bool onPointerMove(const PointerEvent &event); + bool onPointerUp(const PointerEvent &event); + + private: + emscripten::val m_element; + + int m_capturedPointerId = -1; + + const Qt::Edges m_edges; + + Resizer *m_resizer; + + std::unique_ptr<qstdweb::EventCallback> m_mouseDownEvent; + std::unique_ptr<qstdweb::EventCallback> m_mouseMoveEvent; + std::unique_ptr<qstdweb::EventCallback> m_mouseUpEvent; + }; + + using ClickCallback = std::function<void()>; + + Resizer(QWasmWindow *window, emscripten::val parentElement); + ~Resizer(); + + ResizeConstraints getResizeConstraints(); + +private: + void onInteraction(); + void startResize(Qt::Edges resizeEdges, const PointerEvent &event); + void continueResize(const PointerEvent &event); + void finishResize(); + + struct ResizeData + { + Qt::Edges edges = Qt::Edges::fromInt(0); + QPointF originInScreenCoords; + QPoint minShrink; + QPoint maxGrow; + QRect initialBounds; + }; + std::unique_ptr<ResizeData> m_currentResizeData; + + QWasmWindow *m_window; + emscripten::val m_windowElement; + std::vector<std::unique_ptr<ResizerElement>> m_elements; +}; + +class TitleBar +{ +public: + TitleBar(QWasmWindow *window, emscripten::val parentElement); + ~TitleBar(); + + void setTitle(const QString &title); + void setRestoreVisible(bool visible); + void setMaximizeVisible(bool visible); + void setCloseVisible(bool visible); + void setIcon(std::string_view imageData, std::string_view format); + void setWidth(int width); + + QRectF geometry() const; + +private: + bool onPointerDown(const PointerEvent &event); + bool onPointerMove(const PointerEvent &event); + bool onPointerUp(const PointerEvent &event); + bool onDoubleClick(); + + QPointF clipPointWithScreen(const QPointF &pointInTitleBarCoords) const; + + QWasmWindow *m_window; + + emscripten::val m_element; + emscripten::val m_label; + + std::unique_ptr<WebImageButton> m_close; + std::unique_ptr<WebImageButton> m_maximize; + std::unique_ptr<WebImageButton> m_restore; + std::unique_ptr<WebImageButton> m_icon; + + int m_capturedPointerId = -1; + QPointF m_moveStartPoint; + QPoint m_moveStartWindowPosition; + + std::unique_ptr<qstdweb::EventCallback> m_mouseDownEvent; + std::unique_ptr<qstdweb::EventCallback> m_mouseMoveEvent; + std::unique_ptr<qstdweb::EventCallback> m_mouseUpEvent; + std::unique_ptr<qstdweb::EventCallback> m_doubleClickEvent; +}; + +QT_END_NAMESPACE +#endif // QWASMWINDOWNONCLIENTAREA_H diff --git a/src/plugins/platforms/wasm/qwasmwindowstack.cpp b/src/plugins/platforms/wasm/qwasmwindowstack.cpp new file mode 100644 index 0000000000..d3769c7a1b --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmwindowstack.cpp @@ -0,0 +1,203 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "qwasmwindowstack.h" + +QT_BEGIN_NAMESPACE + +QWasmWindowStack::QWasmWindowStack(WindowOrderChangedCallbackType windowOrderChangedCallback) + : m_windowOrderChangedCallback(std::move(windowOrderChangedCallback)), + m_regularWindowsBegin(m_windowStack.begin()), + m_alwaysOnTopWindowsBegin(m_windowStack.begin()) +{ +} + +QWasmWindowStack::~QWasmWindowStack() = default; + +void QWasmWindowStack::pushWindow(QWasmWindow *window, PositionPreference position) +{ + Q_ASSERT(m_windowStack.count(window) == 0); + + if (position == PositionPreference::StayOnTop) { + const auto stayOnTopDistance = + std::distance(m_windowStack.begin(), m_alwaysOnTopWindowsBegin); + const auto regularDistance = std::distance(m_windowStack.begin(), m_regularWindowsBegin); + m_windowStack.push_back(window); + m_alwaysOnTopWindowsBegin = m_windowStack.begin() + stayOnTopDistance; + m_regularWindowsBegin = m_windowStack.begin() + regularDistance; + } else if (position == PositionPreference::Regular) { + const auto regularDistance = std::distance(m_windowStack.begin(), m_regularWindowsBegin); + m_alwaysOnTopWindowsBegin = m_windowStack.insert(m_alwaysOnTopWindowsBegin, window) + 1; + m_regularWindowsBegin = m_windowStack.begin() + regularDistance; + } else { + const auto stayOnTopDistance = + std::distance(m_windowStack.begin(), m_alwaysOnTopWindowsBegin); + m_regularWindowsBegin = m_windowStack.insert(m_regularWindowsBegin, window) + 1; + m_alwaysOnTopWindowsBegin = m_windowStack.begin() + stayOnTopDistance + 1; + } + + m_windowOrderChangedCallback(); +} + +void QWasmWindowStack::removeWindow(QWasmWindow *window) +{ + Q_ASSERT(m_windowStack.count(window) == 1); + + auto it = std::find(m_windowStack.begin(), m_windowStack.end(), window); + const auto position = getWindowPositionPreference(it); + const auto stayOnTopDistance = std::distance(m_windowStack.begin(), m_alwaysOnTopWindowsBegin); + const auto regularDistance = std::distance(m_windowStack.begin(), m_regularWindowsBegin); + + m_windowStack.erase(it); + + m_alwaysOnTopWindowsBegin = m_windowStack.begin() + stayOnTopDistance + - (position != PositionPreference::StayOnTop ? 1 : 0); + m_regularWindowsBegin = m_windowStack.begin() + regularDistance + - (position == PositionPreference::StayOnBottom ? 1 : 0); + + m_windowOrderChangedCallback(); +} + +void QWasmWindowStack::raise(QWasmWindow *window) +{ + Q_ASSERT(m_windowStack.count(window) == 1); + + if (window == topWindow()) + return; + + auto it = std::find(m_windowStack.begin(), m_windowStack.end(), window); + auto itEnd = ([this, position = getWindowPositionPreference(it)]() { + switch (position) { + case PositionPreference::StayOnTop: + return m_windowStack.end(); + case PositionPreference::Regular: + return m_alwaysOnTopWindowsBegin; + case PositionPreference::StayOnBottom: + return m_regularWindowsBegin; + } + })(); + std::rotate(it, it + 1, itEnd); + m_windowOrderChangedCallback(); +} + +void QWasmWindowStack::lower(QWasmWindow *window) +{ + Q_ASSERT(m_windowStack.count(window) == 1); + + if (window == *m_windowStack.begin()) + return; + + auto it = std::find(m_windowStack.begin(), m_windowStack.end(), window); + auto itBegin = ([this, position = getWindowPositionPreference(it)]() { + switch (position) { + case PositionPreference::StayOnTop: + return m_alwaysOnTopWindowsBegin; + case PositionPreference::Regular: + return m_regularWindowsBegin; + case PositionPreference::StayOnBottom: + return m_windowStack.begin(); + } + })(); + + std::rotate(itBegin, it, it + 1); + m_windowOrderChangedCallback(); +} + +void QWasmWindowStack::windowPositionPreferenceChanged(QWasmWindow *window, + PositionPreference position) +{ + auto it = std::find(m_windowStack.begin(), m_windowStack.end(), window); + const auto currentPosition = getWindowPositionPreference(it); + + const auto zones = static_cast<int>(position) - static_cast<int>(currentPosition); + Q_ASSERT(zones != 0); + + if (zones < 0) { + // Perform right rotation so that the window lands on top of regular windows + const auto begin = std::make_reverse_iterator(it + 1); + const auto end = position == PositionPreference::Regular + ? std::make_reverse_iterator(m_alwaysOnTopWindowsBegin) + : std::make_reverse_iterator(m_regularWindowsBegin); + std::rotate(begin, begin + 1, end); + if (zones == -2) { + ++m_alwaysOnTopWindowsBegin; + ++m_regularWindowsBegin; + } else if (position == PositionPreference::Regular) { + ++m_alwaysOnTopWindowsBegin; + } else { + ++m_regularWindowsBegin; + } + } else { + // Perform left rotation so that the window lands at the bottom of always on top windows + const auto begin = it; + const auto end = position == PositionPreference::Regular ? m_regularWindowsBegin + : m_alwaysOnTopWindowsBegin; + std::rotate(begin, begin + 1, end); + if (zones == 2) { + --m_alwaysOnTopWindowsBegin; + --m_regularWindowsBegin; + } else if (position == PositionPreference::Regular) { + --m_regularWindowsBegin; + } else { + --m_alwaysOnTopWindowsBegin; + } + } + m_windowOrderChangedCallback(); +} + +QWasmWindowStack::iterator QWasmWindowStack::begin() +{ + return m_windowStack.rbegin(); +} + +QWasmWindowStack::iterator QWasmWindowStack::end() +{ + return m_windowStack.rend(); +} + +QWasmWindowStack::const_iterator QWasmWindowStack::begin() const +{ + return m_windowStack.rbegin(); +} + +QWasmWindowStack::const_iterator QWasmWindowStack::end() const +{ + return m_windowStack.rend(); +} + +QWasmWindowStack::const_reverse_iterator QWasmWindowStack::rbegin() const +{ + return m_windowStack.begin(); +} + +QWasmWindowStack::const_reverse_iterator QWasmWindowStack::rend() const +{ + return m_windowStack.end(); +} + +bool QWasmWindowStack::empty() const +{ + return m_windowStack.empty(); +} + +size_t QWasmWindowStack::size() const +{ + return m_windowStack.size(); +} + +QWasmWindow *QWasmWindowStack::topWindow() const +{ + return m_windowStack.empty() ? nullptr : m_windowStack.last(); +} + +QWasmWindowStack::PositionPreference +QWasmWindowStack::getWindowPositionPreference(StorageType::iterator windowIt) const +{ + if (windowIt >= m_alwaysOnTopWindowsBegin) + return PositionPreference::StayOnTop; + if (windowIt >= m_regularWindowsBegin) + return PositionPreference::Regular; + return PositionPreference::StayOnBottom; +} + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmwindowstack.h b/src/plugins/platforms/wasm/qwasmwindowstack.h new file mode 100644 index 0000000000..c75001157a --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmwindowstack.h @@ -0,0 +1,75 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef QWASMWINDOWSTACK_H +#define QWASMWINDOWSTACK_H + +#include <qglobal.h> +#include <QtCore/qlist.h> + +#include <vector> + +QT_BEGIN_NAMESPACE + +class QWasmWindow; + +// Maintains a z-order hierarchy for a set of windows. The first added window is always treated as +// the 'root', which always stays at the bottom. Other windows are 'regular', which means they are +// subject to z-order changes via |raise| and |lower|/ +// If the root is ever removed, all of the current and future windows in the stack are treated as +// regular. +// Access to the top element is facilitated by |topWindow|. +// Changes to the top element are signaled via the |topWindowChangedCallback| supplied at +// construction. +class Q_AUTOTEST_EXPORT QWasmWindowStack +{ +public: + using WindowOrderChangedCallbackType = std::function<void()>; + + using StorageType = QList<QWasmWindow *>; + + using iterator = StorageType::reverse_iterator; + using const_iterator = StorageType::const_reverse_iterator; + using const_reverse_iterator = StorageType::const_iterator; + + enum class PositionPreference { + StayOnBottom, + Regular, + StayOnTop, + }; + + explicit QWasmWindowStack(WindowOrderChangedCallbackType topWindowChangedCallback); + ~QWasmWindowStack(); + + void pushWindow(QWasmWindow *window, PositionPreference position); + void removeWindow(QWasmWindow *window); + void raise(QWasmWindow *window); + void lower(QWasmWindow *window); + void windowPositionPreferenceChanged(QWasmWindow *window, PositionPreference position); + + // Iterates top-to-bottom + iterator begin(); + iterator end(); + const_iterator begin() const; + const_iterator end() const; + + // Iterates bottom-to-top + const_reverse_iterator rbegin() const; + const_reverse_iterator rend() const; + + bool empty() const; + size_t size() const; + QWasmWindow *topWindow() const; + +private: + PositionPreference getWindowPositionPreference(StorageType::iterator windowIt) const; + + WindowOrderChangedCallbackType m_windowOrderChangedCallback; + QList<QWasmWindow *> m_windowStack; + StorageType::iterator m_regularWindowsBegin; + StorageType::iterator m_alwaysOnTopWindowsBegin; +}; + +QT_END_NAMESPACE + +#endif // QWASMWINDOWSTACK_H diff --git a/src/plugins/platforms/wasm/qwasmwindowtreenode.cpp b/src/plugins/platforms/wasm/qwasmwindowtreenode.cpp new file mode 100644 index 0000000000..ea8d8dbcfa --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmwindowtreenode.cpp @@ -0,0 +1,108 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "qwasmwindowtreenode.h" + +#include "qwasmwindow.h" + +QWasmWindowTreeNode::QWasmWindowTreeNode() + : m_childStack(std::bind(&QWasmWindowTreeNode::onTopWindowChanged, this)) +{ +} + +QWasmWindowTreeNode::~QWasmWindowTreeNode() = default; + +void QWasmWindowTreeNode::onParentChanged(QWasmWindowTreeNode *previousParent, + QWasmWindowTreeNode *currentParent, + QWasmWindowStack::PositionPreference positionPreference) +{ + auto *window = asWasmWindow(); + if (previousParent) { + previousParent->m_childStack.removeWindow(window); + previousParent->onSubtreeChanged(QWasmWindowTreeNodeChangeType::NodeRemoval, previousParent, + window); + } + + if (currentParent) { + currentParent->m_childStack.pushWindow(window, positionPreference); + currentParent->onSubtreeChanged(QWasmWindowTreeNodeChangeType::NodeInsertion, currentParent, + window); + } +} + +QWasmWindow *QWasmWindowTreeNode::asWasmWindow() +{ + return nullptr; +} + +void QWasmWindowTreeNode::onSubtreeChanged(QWasmWindowTreeNodeChangeType changeType, + QWasmWindowTreeNode *parent, QWasmWindow *child) +{ + if (changeType == QWasmWindowTreeNodeChangeType::NodeInsertion && parent == this + && m_childStack.topWindow() + && m_childStack.topWindow()->window()) { + + const QVariant showWithoutActivating = m_childStack.topWindow()->window()->property("_q_showWithoutActivating"); + if (!showWithoutActivating.isValid() || !showWithoutActivating.toBool()) + m_childStack.topWindow()->requestActivateWindow(); + } + + if (parentNode()) + parentNode()->onSubtreeChanged(changeType, parent, child); +} + +void QWasmWindowTreeNode::setWindowZOrder(QWasmWindow *window, int z) +{ + window->setZOrder(z); +} + +void QWasmWindowTreeNode::onPositionPreferenceChanged( + QWasmWindowStack::PositionPreference positionPreference) +{ + if (parentNode()) { + parentNode()->m_childStack.windowPositionPreferenceChanged(asWasmWindow(), + positionPreference); + } +} + +void QWasmWindowTreeNode::setAsActiveNode() +{ + if (parentNode()) + parentNode()->setActiveChildNode(asWasmWindow()); +} + +void QWasmWindowTreeNode::bringToTop() +{ + if (!parentNode()) + return; + parentNode()->m_childStack.raise(asWasmWindow()); + parentNode()->bringToTop(); +} + +void QWasmWindowTreeNode::sendToBottom() +{ + if (!parentNode()) + return; + m_childStack.lower(asWasmWindow()); +} + +void QWasmWindowTreeNode::onTopWindowChanged() +{ + constexpr int zOrderForElementInFrontOfScreen = 3; + int z = zOrderForElementInFrontOfScreen; + std::for_each(m_childStack.rbegin(), m_childStack.rend(), + [this, &z](QWasmWindow *window) { setWindowZOrder(window, z++); }); +} + +void QWasmWindowTreeNode::setActiveChildNode(QWasmWindow *activeChild) +{ + m_activeChild = activeChild; + + auto it = m_childStack.begin(); + if (it == m_childStack.end()) + return; + for (; it != m_childStack.end(); ++it) + (*it)->onActivationChanged(*it == m_activeChild); + + setAsActiveNode(); +} diff --git a/src/plugins/platforms/wasm/qwasmwindowtreenode.h b/src/plugins/platforms/wasm/qwasmwindowtreenode.h new file mode 100644 index 0000000000..344fdb43cb --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmwindowtreenode.h @@ -0,0 +1,53 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef QWASMWINDOWTREENODE_H +#define QWASMWINDOWTREENODE_H + +#include "qwasmwindowstack.h" + +namespace emscripten { +class val; +} + +class QWasmWindow; + +enum class QWasmWindowTreeNodeChangeType { + NodeInsertion, + NodeRemoval, +}; + +class QWasmWindowTreeNode +{ +public: + QWasmWindowTreeNode(); + virtual ~QWasmWindowTreeNode(); + + virtual emscripten::val containerElement() = 0; + virtual QWasmWindowTreeNode *parentNode() = 0; + +protected: + virtual void onParentChanged(QWasmWindowTreeNode *previous, QWasmWindowTreeNode *current, + QWasmWindowStack::PositionPreference positionPreference); + virtual QWasmWindow *asWasmWindow(); + virtual void onSubtreeChanged(QWasmWindowTreeNodeChangeType changeType, + QWasmWindowTreeNode *parent, QWasmWindow *child); + virtual void setWindowZOrder(QWasmWindow *window, int z); + + void onPositionPreferenceChanged(QWasmWindowStack::PositionPreference positionPreference); + void setAsActiveNode(); + void bringToTop(); + void sendToBottom(); + + const QWasmWindowStack &childStack() const { return m_childStack; } + QWasmWindow *activeChild() const { return m_activeChild; } + +private: + void onTopWindowChanged(); + void setActiveChildNode(QWasmWindow *activeChild); + + QWasmWindowStack m_childStack; + QWasmWindow *m_activeChild = nullptr; +}; + +#endif // QWASMWINDOWTREENODE_H diff --git a/src/plugins/platforms/wasm/resources/maximize.svg b/src/plugins/platforms/wasm/resources/maximize.svg new file mode 100644 index 0000000000..b5fad4f707 --- /dev/null +++ b/src/plugins/platforms/wasm/resources/maximize.svg @@ -0,0 +1 @@ +<svg width="1024" height="1024" xmlns="http://www.w3.org/2000/svg" class="svg-icon"><path stroke="null" d="M-.333-.333h1024.666v1024.666H-.333V-.333M127.75 255.833V896.25h768.5V255.833h-768.5z"/></svg>
\ No newline at end of file diff --git a/src/plugins/platforms/wasm/resources/qtlogo.svg b/src/plugins/platforms/wasm/resources/qtlogo.svg new file mode 100644 index 0000000000..bfe2493f46 --- /dev/null +++ b/src/plugins/platforms/wasm/resources/qtlogo.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="616" height="452" viewBox="0 0 462 339"><path fill="#41cd52" d="M63.5 0H462v274.79c-21.4 21.47-42.87 42.87-64.39 64.21H0V63.39C21.08 42.18 42.34 21.13 63.5 0Z"/><path d="M122.37 71.33C137.5 61.32 156.21 58.79 174 58.95c16.94.21 34.72 3.18 48.76 13.29 10.2 7.17 16.83 18.24 21.25 29.69 7.15 18.8 9.25 39.1 9.49 59.08.03 20.12-.88 40.68-7.54 59.85-4.46 13.04-12.95 24.62-24.15 32.66 8.06 13.06 16.28 26.02 24.34 39.08-10.13 4.67-20.23 9.37-30.37 14.02-8.63-14.24-17.22-28.51-25.88-42.73-11.71 1.92-23.69 1.77-35.46.47-14.1-1.69-28.47-5.99-39.35-15.48-8.36-7.24-13.61-17.37-17.2-27.67-5.88-17.42-7.46-35.96-7.73-54.24-.14-19.76 1.12-39.83 7.08-58.79 4.61-14.26 12.24-28.49 25.13-36.85ZM294.13 70.69c10.6-.01 21.2-.01 31.8 0 .03 14.02-.01 28.03.02 42.05 13.55.02 27.1 0 40.65.01-.23 9.1-.48 18.2-.74 27.3-13.54.03-27.07-.01-40.61.02.03 22.98-.07 45.96.05 68.94.26 6.29.12 12.93 2.89 18.74 2.02 4.48 7.46 5.63 11.89 5.78 8.35-.03 16.69-.52 25.04-.67.51 8.36 1 16.73 1.48 25.09-16.61 2.79-34.04 6.13-50.54.91-6.95-2.06-13.43-6.67-16.25-13.54-5.05-11.69-5.46-24.7-5.68-37.25-.02-22.67 0-45.33-.01-68-7.39-.02-14.78.01-22.17-.02-.02-9.09-.02-18.19 0-27.29 7.39-.03 14.77.01 22.16-.02.03-14.02-.01-28.03.02-42.05Z" fill="#fff"/><path fill="#41cd52" d="M160.51 87.7c10.29-1.34 21.09-.98 30.83 2.91 7.89 3.12 14.59 9.23 18.13 16.97 5.43 11.73 7.51 24.68 8.56 37.47 1.14 17.02.98 34.2-1.37 51.12-1.65 10.07-4 20.68-10.82 28.62-6.92 7.97-17.59 11.39-27.83 12.19-10.8.79-22.19 0-31.94-5.11-5.69-3.03-10.52-7.78-13.34-13.6-3.42-6.97-5.3-14.58-6.62-22.2-3.98-24.16-4.94-49.16.5-73.18 2.24-9.06 5.5-18.36 12.12-25.19 5.76-5.85 13.78-8.87 21.78-10Z"/></svg>
\ No newline at end of file diff --git a/src/plugins/platforms/wasm/resources/restore.svg b/src/plugins/platforms/wasm/resources/restore.svg new file mode 100644 index 0000000000..70ee19170b --- /dev/null +++ b/src/plugins/platforms/wasm/resources/restore.svg @@ -0,0 +1 @@ +<svg width="1024" height="1024" xmlns="http://www.w3.org/2000/svg" class="svg-icon"><path stroke="null" d="M449.191 44.905h535.142v528.951H449.191V44.906m66.893 132.237v330.594H917.44V177.143H516.084z"/><path stroke="null" d="M54.906 453.476h535.141v528.952H54.906V453.476m66.892 132.238V916.31h401.357V585.714H121.798z"/></svg>
\ No newline at end of file diff --git a/src/plugins/platforms/wasm/resources/x.svg b/src/plugins/platforms/wasm/resources/x.svg new file mode 100644 index 0000000000..1d9ba7361a --- /dev/null +++ b/src/plugins/platforms/wasm/resources/x.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 460.775 460.775" style="enable-background:new 0 0 460.775 460.775" xml:space="preserve"><path d="M285.08 230.397 456.218 59.27c6.076-6.077 6.076-15.911 0-21.986L423.511 4.565a15.55 15.55 0 0 0-21.985 0l-171.138 171.14L59.25 4.565a15.551 15.551 0 0 0-21.985 0L4.558 37.284c-6.077 6.075-6.077 15.909 0 21.986l171.138 171.128L4.575 401.505c-6.074 6.077-6.074 15.911 0 21.986l32.709 32.719a15.555 15.555 0 0 0 21.986 0l171.117-171.12 171.118 171.12a15.551 15.551 0 0 0 21.985 0l32.709-32.719c6.074-6.075 6.074-15.909 0-21.986L285.08 230.397z"/></svg>
\ No newline at end of file diff --git a/src/plugins/platforms/wasm/wasm_shell.html b/src/plugins/platforms/wasm/wasm_shell.html index f5712d0418..6e93955552 100644 --- a/src/plugins/platforms/wasm/wasm_shell.html +++ b/src/plugins/platforms/wasm/wasm_shell.html @@ -1,3 +1,8 @@ +<!-- +Copyright (C) 2024 The Qt Company Ltd. +SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only +--> + <!doctype html> <html lang="en-us"> <head> @@ -12,13 +17,8 @@ <title>@APPNAME@</title> <style> /* Make the html body cover the entire (visual) viewport with no scroll bars. */ - html, body { padding: 0; margin: 0; overflow:hidden; height: 100vh } - /* the canvas *must not* have any border or padding, or mouse coords will be wrong */ - canvas { border: 0px none; background-color: white; height:100%; width:100%; } - /* The contenteditable property is set to true for the canvas in order to support - clipboard events. Hide the resulting focus frame and set the cursor back to - the default cursor. */ - canvas { outline: 0px solid transparent; caret-color: transparent; cursor:default } + html, body { padding: 0; margin: 0; overflow: hidden; height: 100% } + #screen { width: 100%; height: 100%; } </style> </head> <body onload="init()"> @@ -30,43 +30,50 @@ <noscript>JavaScript is disabled. Please enable JavaScript to use this application.</noscript> </center> </figure> - <canvas id="qtcanvas" oncontextmenu="event.preventDefault()" contenteditable="true"></canvas> + <div id="screen"></div> + + <script type="text/javascript"> + async function init() + { + const spinner = document.querySelector('#qtspinner'); + const screen = document.querySelector('#screen'); + const status = document.querySelector('#qtstatus'); + + const showUi = (ui) => { + [spinner, screen].forEach(element => element.style.display = 'none'); + if (screen === ui) + screen.style.position = 'default'; + ui.style.display = 'block'; + } - <script type='text/javascript'> - function init() { - var spinner = document.querySelector('#qtspinner'); - var canvas = document.querySelector('#qtcanvas'); - var status = document.querySelector('#qtstatus') + try { + showUi(spinner); + status.innerHTML = 'Loading...'; - var qtLoader = QtLoader({ - canvasElements : [canvas], - showLoader: function(loaderStatus) { - spinner.style.display = 'block'; - canvas.style.display = 'none'; - status.innerHTML = loaderStatus + "..."; - }, - showError: function(errorText) { - status.innerHTML = errorText; - spinner.style.display = 'block'; - canvas.style.display = 'none'; - }, - showExit: function() { - status.innerHTML = "Application exit"; - if (qtLoader.exitCode !== undefined) - status.innerHTML += " with code " + qtLoader.exitCode; - if (qtLoader.exitText !== undefined) - status.innerHTML += " (" + qtLoader.exitText + ")"; - spinner.style.display = 'block'; - canvas.style.display = 'none'; - }, - showCanvas: function() { - spinner.style.display = 'none'; - canvas.style.display = 'block'; - }, - }); - qtLoader.loadEmscriptenModule("@APPNAME@"); - } + const instance = await qtLoad({ + qt: { + onLoaded: () => showUi(screen), + onExit: exitData => + { + status.innerHTML = 'Application exit'; + status.innerHTML += + exitData.code !== undefined ? ` with code ${exitData.code}` : ''; + status.innerHTML += + exitData.text !== undefined ? ` (${exitData.text})` : ''; + showUi(spinner); + }, + entryFunction: window.@APPEXPORTNAME@, + containerElements: [screen], + @PRELOAD@ + } + }); + } catch (e) { + console.error(e); + console.error(e.stack); + } + } </script> + <script src="@APPNAME@.js"></script> <script type="text/javascript" src="qtloader.js"></script> </body> </html> |