diff options
52 files changed, 1675 insertions, 102 deletions
diff --git a/cmake/QtFlagHandlingHelpers.cmake b/cmake/QtFlagHandlingHelpers.cmake index b32acc4cfa3..8d616cebd2c 100644 --- a/cmake/QtFlagHandlingHelpers.cmake +++ b/cmake/QtFlagHandlingHelpers.cmake @@ -1495,3 +1495,87 @@ function(qt_internal_set_up_config_optimizations_like_in_qmake) "${target_link_types}") endif() endfunction() + +# Adds optimized flags to targets created in the calling directory scope. +# This is the only way to add release flags in a debug build, which would build successfully. +# The approach of just appending -O3, and thus overriding any previous -O0 works on GCC / Clang +# but not MSVC, because MSVC has other debug flags which are not compatible when -Ox optimizations +# are turned on. +function(qt_internal_add_optimized_flags_for_debug_config_in_current_scope) + qt_internal_remove_known_optimization_flags(CONFIGS DEBUG) + + # Remove incompatible Windows debug flags with release flags. + if(MSVC) + set(default_debug_flags_to_remove "/RTC1") + + qt_internal_get_enabled_languages_for_flag_manipulation(enabled_languages) + foreach(lang ${enabled_languages}) + set(flag_var_name "CMAKE_${lang}_FLAGS_DEBUG") + qt_internal_remove_flags_impl(${flag_var_name} "${default_debug_flags_to_remove}" "") + endforeach() + endif() + + qt_internal_get_optimize_full_flags(optimize_full_flags) + qt_internal_add_compiler_flags(FLAGS "${optimize_full_flags}" CONFIGS DEBUG) + + # Set the flags in the parent scope. This will apply the flags to all targets within + # that directory scope. + qt_internal_set_optimized_flags_for_debug_config_in_parent_scope() +endfunction() + +# Conditionally adds optimized flags to the calling directory scope, depending on whether +# the QT_FEATURE_optimized_tools is ON, and whether a target or project specific override was not +# set. +# Overrides can be set via: +# -DQT_FORCE_NO_OPTIMIZE_<target>=ON +# -DQT_FORCE_NO_OPTIMIZE_<project_name>=ON +# e.g -DQT_FORCE_NO_OPTIMIZE_moc=ON -DQT_FORCE_NO_OPTIMIZE_qtdeclarative=ON +function(qt_internal_add_target_optimized_flags_for_debug_config_in_current_scope target) + set(opt_args "") + set(single_args + OUT_VAR_DID_ADD + ) + set(multi_args "") + + cmake_parse_arguments(PARSE_ARGV 1 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + string(TOLOWER "${PROJECT_NAME}" project_name_lower) + + if(QT_FEATURE_optimized_tools + AND NOT QT_FORCE_NO_OPTIMIZE_${target} + AND NOT QT_FORCE_NO_OPTIMIZE_${project_name_lower} + ) + qt_internal_add_optimized_flags_for_debug_config_in_current_scope() + qt_internal_set_optimized_flags_for_debug_config_in_parent_scope() + set(did_add TRUE) + else() + set(did_add FALSE) + endif() + + if(arg_OUT_VAR_DID_ADD) + set(${arg_OUT_VAR_DID_ADD} "${did_add}" PARENT_SCOPE) + endif() +endfunction() + +# Helper to propagate the optimized flags for the debug config to the parent scope. +macro(qt_internal_set_optimized_flags_for_debug_config_in_parent_scope) + qt_internal_get_enabled_languages_for_flag_manipulation(__qt_internal_debug_enabled_languages) + foreach(__qt_internal_debug_lang IN LISTS __qt_internal_debug_enabled_languages) + set(flag_var_name "CMAKE_${__qt_internal_debug_lang}_FLAGS_DEBUG") + set(${flag_var_name} "${${flag_var_name}}" PARENT_SCOPE) + endforeach() + unset(__qt_internal_debug_lang) + unset(__qt_internal_debug_enabled_languages) +endmacro() + +# Same as qt_internal_add_target_optimized_flags_for_debug_config_in_current_scope, but also +# propagates the optimized flags to the parent scope. +macro(qt_internal_add_target_optimized_flags_for_debug_config_in_parent_scope target) + qt_internal_add_target_optimized_flags_for_debug_config_in_current_scope("${target}" + OUT_VAR_DID_ADD __qt_internal_did_add_optimized_flag_in_current_scope) + if(__qt_internal_did_add_optimized_flag_in_current_scope) + qt_internal_set_optimized_flags_for_debug_config_in_parent_scope() + endif() + unset(__qt_internal_did_add_optimized_flag_in_current_scope) +endmacro() diff --git a/cmake/QtModuleHelpers.cmake b/cmake/QtModuleHelpers.cmake index b8c9914aba5..358cdadc59c 100644 --- a/cmake/QtModuleHelpers.cmake +++ b/cmake/QtModuleHelpers.cmake @@ -326,13 +326,7 @@ function(qt_internal_add_module target) endif() endif() - if((FEATURE_ltcg OR CMAKE_INTERPROCEDURAL_OPTIMIZATION) AND GCC AND is_static_lib) - # CMake <= 3.19 appends -fno-fat-lto-objects for all library types if - # CMAKE_INTERPROCEDURAL_OPTIMIZATION is enabled. Static libraries need - # the opposite compiler option. - # (https://gitlab.kitware.com/cmake/cmake/-/issues/21696) - target_compile_options(${target} PRIVATE -ffat-lto-objects) - endif() + qt_internal_workaround_static_lib_gcc_lto_issue("${target}") qt_internal_add_target_aliases("${target}") diff --git a/cmake/QtPublicAndroidHelpers.cmake b/cmake/QtPublicAndroidHelpers.cmake index 30ffec43ad8..dbaf01f0359 100644 --- a/cmake/QtPublicAndroidHelpers.cmake +++ b/cmake/QtPublicAndroidHelpers.cmake @@ -67,7 +67,7 @@ function(_qt_internal_locate_android_jar) # This variable specifies the API level used for building Java code, it can be the same as Qt # for Android's maximum supported Android version or higher. if(NOT QT_ANDROID_API_USED_FOR_JAVA) - set(QT_ANDROID_API_USED_FOR_JAVA "android-35") + set(QT_ANDROID_API_USED_FOR_JAVA "android-36") endif() set(jar_location "${ANDROID_SDK_ROOT}/platforms/${QT_ANDROID_API_USED_FOR_JAVA}/android.jar") diff --git a/cmake/QtTargetHelpers.cmake b/cmake/QtTargetHelpers.cmake index da210820bb2..10d60807212 100644 --- a/cmake/QtTargetHelpers.cmake +++ b/cmake/QtTargetHelpers.cmake @@ -2023,3 +2023,61 @@ function(qt_internal_apply_dynamic_list_linker_flags target dynlist_template) target_link_options(${target} PRIVATE "LINKER:--dynamic-list=${dynlist_file_abspath}") endfunction() + +function(qt_internal_workaround_static_lib_gcc_lto_issue target) + get_target_property(target_type "${target}" TYPE) + get_target_property(target_lto_enabled "${target}" INTERPROCEDURAL_OPTIMIZATION) + get_target_property(target_lto_enabled_debug "${target}" INTERPROCEDURAL_OPTIMIZATION_DEBUG) + set(target_lto_enabled_debug_unset FALSE) + + if(target_lto_enabled_debug MATCHES "-NOTFOUND") + set(target_lto_enabled_debug_unset TRUE) + endif() + + set(general_lto_enabled FALSE) + + if(FEATURE_ltcg + OR QT_FEATURE_ltcg + OR CMAKE_INTERPROCEDURAL_OPTIMIZATION + OR target_lto_enabled) + set(general_lto_enabled TRUE) + endif() + + if((general_lto_enabled OR target_lto_enabled_debug) + AND GCC + AND target_type STREQUAL "STATIC_LIBRARY") + # CMake <= 3.19 appends -fno-fat-lto-objects for all library types if + # CMAKE_INTERPROCEDURAL_OPTIMIZATION is enabled. Static libraries need + # the opposite compiler option. + # (https://gitlab.kitware.com/cmake/cmake/-/issues/21696) + set(flag_name -ffat-lto-objects) + + if((general_lto_enabled AND target_lto_enabled_debug_unset) + OR (general_lto_enabled AND target_lto_enabled) + ) + set(flag_wrapped "${flag_name}") + elseif(general_lto_enabled AND NOT target_lto_enabled_debug) + set(flag_wrapped "$<$<NOT:$<CONFIG:Debug>>:${flag_name}>") + elseif(target_lto_enabled_debug) + set(flag_wrapped "$<$<CONFIG:Debug>:${flag_name}>") + endif() + + target_compile_options("${target}" PRIVATE "${flag_wrapped}") + endif() +endfunction() + +# Enable LTO for the debug configuration of optimized tool or library targets. +# Can be opted out via various options. +function(qt_internal_enable_optimized_tools_lto target) + string(TOLOWER "${PROJECT_NAME}" project_name_lower) + + if(QT_FEATURE_optimized_tools + AND NOT QT_FORCE_NO_OPTIMIZED_TOOLS_LTO + AND NOT QT_FORCE_NO_OPTIMIZE_${target} + AND NOT QT_FORCE_NO_OPTIMIZE_${project_name_lower} + AND NOT QT_FORCE_NO_LTO_${target} + AND NOT QT_FORCE_NO_LTO_${project_name_lower} + ) + set_target_properties("${target}" PROPERTIES INTERPROCEDURAL_OPTIMIZATION_DEBUG ON) + endif() +endfunction() diff --git a/cmake/QtToolHelpers.cmake b/cmake/QtToolHelpers.cmake index 5d531818ebd..16743dcd9eb 100644 --- a/cmake/QtToolHelpers.cmake +++ b/cmake/QtToolHelpers.cmake @@ -292,6 +292,9 @@ function(qt_internal_add_tool target_name) ) qt_internal_install_pdb_files(${target_name} "${install_dir}") + qt_internal_add_target_optimized_flags_for_debug_config_in_parent_scope("${target_name}") + qt_internal_enable_optimized_tools_lto("${target_name}") + if(QT_GENERATE_SBOM) _qt_internal_forward_function_args( FORWARD_APPEND diff --git a/configure.cmake b/configure.cmake index 0d4e5b04688..efec5435f85 100644 --- a/configure.cmake +++ b/configure.cmake @@ -588,6 +588,10 @@ qt_feature("optimize_full" AUTODETECT OFF ) qt_feature_config("optimize_full" QMAKE_PRIVATE_CONFIG) +qt_feature("optimized_tools" PRIVATE + LABEL "Build optimized tools for the Debug configuration" + AUTODETECT OFF +) qt_feature("msvc_obj_debug_info" LABEL "Embed debug info in object files (MSVC)" ENABLE QT_USE_CCACHE OR CMAKE_CXX_COMPILER_LAUNCHER diff --git a/mkspecs/android-clang/qmake.conf b/mkspecs/android-clang/qmake.conf index dd3b6a1d88c..47c0b88e9fc 100644 --- a/mkspecs/android-clang/qmake.conf +++ b/mkspecs/android-clang/qmake.conf @@ -44,7 +44,7 @@ isEmpty(ALL_ANDROID_ABIS): ALL_ANDROID_ABIS = arm64-v8a armeabi-v7a x86_64 x86 CONFIG += $$ANDROID_PLATFORM ANDROID_MIN_SDK_VERSION = $$replace(ANDROID_PLATFORM, "android-", "") -ANDROID_TARGET_SDK_VERSION = 35 +ANDROID_TARGET_SDK_VERSION = 36 NDK_LLVM_PATH = $$NDK_ROOT/toolchains/llvm/prebuilt/$$NDK_HOST QMAKE_CC = $$NDK_LLVM_PATH/bin/clang diff --git a/mkspecs/features/android/sdk.prf b/mkspecs/features/android/sdk.prf index 6fd1d666e02..39c557890af 100644 --- a/mkspecs/features/android/sdk.prf +++ b/mkspecs/features/android/sdk.prf @@ -1,6 +1,6 @@ ANDROID_API_USED_FOR_JAVA = $$(ANDROID_API_VERSION) isEmpty(ANDROID_API_USED_FOR_JAVA): ANDROID_API_USED_FOR_JAVA = $$API_VERSION -isEmpty(ANDROID_API_USED_FOR_JAVA): ANDROID_API_USED_FOR_JAVA = android-35 +isEmpty(ANDROID_API_USED_FOR_JAVA): ANDROID_API_USED_FOR_JAVA = android-36 ANDROID_JAR_FILE = $$ANDROID_SDK_ROOT/platforms/$$ANDROID_API_USED_FOR_JAVA/android.jar !exists($$ANDROID_JAR_FILE) { diff --git a/qmake/doc/src/qmake-manual.qdoc b/qmake/doc/src/qmake-manual.qdoc index c8595387b2e..6b8f2322cfc 100644 --- a/qmake/doc/src/qmake-manual.qdoc +++ b/qmake/doc/src/qmake-manual.qdoc @@ -1089,7 +1089,7 @@ \note This variable applies only to Android targets. Specifies the target Android API level for the project. By default, this - variable is set to API level 35. + variable is set to API level 36. \section1 ANDROID_VERSION_CODE diff --git a/qt_cmdline.cmake b/qt_cmdline.cmake index 97a225d670d..132adeb8695 100644 --- a/qt_cmdline.cmake +++ b/qt_cmdline.cmake @@ -104,7 +104,7 @@ qt_commandline_option(opensource TYPE void NAME commercial VALUE no) qt_commandline_option(optimize-debug TYPE boolean NAME optimize_debug) qt_commandline_option(optimize-size TYPE boolean NAME optimize_size) qt_commandline_option(optimized-qmake TYPE boolean NAME release_tools) -qt_commandline_option(optimized-tools TYPE boolean NAME release_tools) +qt_commandline_option(optimized-tools TYPE boolean NAME optimized_tools) qt_commandline_option(pch TYPE boolean CMAKE_VARIABLE BUILD_WITH_PCH) qt_commandline_option(pkg-config TYPE boolean) qt_commandline_option(platform TYPE string CMAKE_VARIABLE QT_QMAKE_TARGET_MKSPEC) diff --git a/src/android/jar/build.gradle b/src/android/jar/build.gradle index 74ecff6b75f..54512f6f4b3 100644 --- a/src/android/jar/build.gradle +++ b/src/android/jar/build.gradle @@ -23,7 +23,7 @@ repositories { } android { - compileSdk 35 + compileSdk 36 namespace = "org.qtproject.qt.android" defaultConfig { diff --git a/src/corelib/CMakeLists.txt b/src/corelib/CMakeLists.txt index 539ad753ca6..e50838730cb 100644 --- a/src/corelib/CMakeLists.txt +++ b/src/corelib/CMakeLists.txt @@ -1533,6 +1533,7 @@ qt_internal_extend_target(Core CONDITION WASM platform/wasm/qstdweb.cpp platform/wasm/qstdweb_p.h platform/wasm/qwasmsocket.cpp platform/wasm/qwasmsocket_p.h platform/wasm/qwasmsuspendresumecontrol.cpp platform/wasm/qwasmsuspendresumecontrol_p.h + platform/wasm/qwasmlocalfileengine.cpp platform/wasm/qwasmlocalfileengine_p.h kernel/qeventdispatcher_wasm.cpp kernel/qeventdispatcher_wasm_p.h ) diff --git a/src/corelib/Qt6AndroidGradleHelpers.cmake b/src/corelib/Qt6AndroidGradleHelpers.cmake index f71bac4e08a..fc8e009b9da 100644 --- a/src/corelib/Qt6AndroidGradleHelpers.cmake +++ b/src/corelib/Qt6AndroidGradleHelpers.cmake @@ -193,7 +193,7 @@ function(_qt_internal_android_generate_target_build_gradle target) QT_ANDROID_MIN_SDK_VERSION "28") _qt_internal_android_get_gradle_property(target_sdk_version ${target} - QT_ANDROID_TARGET_SDK_VERSION "34") + QT_ANDROID_TARGET_SDK_VERSION "36") set(target_abis "$<TARGET_PROPERTY:${target},_qt_android_abis>") set(target_abi_list "$<JOIN:${target_abis};${CMAKE_ANDROID_ARCH_ABI},'$<COMMA> '>") diff --git a/src/corelib/doc/images/modelindex-no-parent.svg b/src/corelib/doc/images/modelindex-no-parent.svg new file mode 100644 index 00000000000..4ce4b86b1f2 --- /dev/null +++ b/src/corelib/doc/images/modelindex-no-parent.svg @@ -0,0 +1,99 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + width="300" + height="160" + version="1.1" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + +<style> + svg .line-style { stroke: black; fill: none } + svg .fill-style { stroke: black; fill: #c0c0c0 } + svg .text-style { font: 12px arial; fill: black } + svg .italic-style { font: 12px arial; fill: black; font-style: italic } + svg .bold-style { font: 12px arial; fill: black; font-weight: bold } + + svg.dark .line-style { stroke: #f2f2f2; fill: none } + svg.dark .fill-style { stroke: #f2f2f2; fill: #606060 } + svg.dark .text-style { font: 12px arial; fill: #f2f2f2 } + svg.dark .italic-style { font: 12px arial; fill: #f2f2f2; font-style: italic } + svg.dark .bold-style { font: 12px arial; fill: #f2f2f2; font-weight: bold } + + [data-theme="dark"] svg .line-style { stroke: #f2f2f2; fill: none } + [data-theme="dark"] svg .fill-style { stroke: #f2f2f2; fill: #606060 } + [data-theme="dark"] svg .text-style { font: 12px arial; fill: #f2f2f2 } + [data-theme="dark"] svg .italic-style { font: 12px arial; fill: #f2f2f2; font-style: italic } + [data-theme="dark"] svg .bold-style { font: 12px arial; fill: #f2f2f2; font-weight: bold } + + [data-theme="light"] svg .line-style { stroke: black; fill: none } + [data-theme="light"] svg .fill-style { stroke: black; fill: #c0c0c0 } + [data-theme="light"] svg .text-style { font: 12px arial; fill: black } + [data-theme="light"] svg .italic-style { font: 12px arial; fill: black; font-style: italic } + [data-theme="light"] svg .bold-style { font: 12px arial; fill: black; font-weight: bold } +</style> + +<text x="25" y="49" font-family="arial" font-size="12px" + class="text-style">0</text> +<text x="25" y="79" font-family="arial" font-size="12px" + class="text-style">1</text> +<text x="25" y="109" font-family="arial" font-size="12px" + class="text-style">2</text> +<g transform="translate(30,139)"> +<text x="0" y="0" font-family="arial" font-size="12px" + class="text-style" transform="rotate(270)">...</text> +</g> + +<path d="m 40.5,30.5 h 30 v 30 h -30 z" stroke="black" fill="none" + class="line-style" /> +<path d="m 70.5,30.5 h 30 v 30 h -30 z" stroke="black" fill="none" + class="line-style" /> +<path d="m 100.5,30.5 h 30 v 30 h -30 z" stroke="black" fill="none" + class="line-style" /> +<path d="m 130.5,30.5 h 30 v 30" stroke="black" fill="none" + class="line-style" stroke-dasharray="3, 3" /> +<path d="m 40.5,60.5 h 30 v 30 h -30 z" stroke="black" fill="none" + class="line-style" /> +<path d="m 70.5,60.5 h 30 v 30 h -30 z" stroke="black" fill="none" + class="line-style" /> +<path d="m 100.5,60.5 h 30 v 30 h -30 z" stroke="black" fill="#c0c0c0" + class="fill-style" /> +<path d="m 130.5,60.5 h 30 v 30 h -30 z" stroke="black" fill="none" + class="line-style" stroke-dasharray="3, 3" /> +<path d="m 40.5,90.5 h 30 v 30 h -30 z" stroke="black" fill="none" + class="line-style" /> +<path d="m 70.5,90.5 h 30 v 30 h -30 z" stroke="black" fill="none" + class="line-style" /> +<path d="m 100.5,90.5 h 30 v 30 h -30 z" stroke="black" fill="none" + class="line-style" /> +<path d="m 70.5,120.5 h 30 v 30 h -30 z" stroke="black" fill="none" + class="line-style" stroke-dasharray="3, 3" /> +<path d="m 40.5,120.5 v 30 h 30" stroke="black" fill="none" + class="line-style" stroke-dasharray="3, 3" /> +<path d="m 100.5,150.5 h 30" stroke="black" + class="line-style" stroke-dasharray="3, 3" /> +<path d="m 130.5,120.5 h 30 v 30 h -30 z" stroke="black" fill="none" + class="line-style" stroke-dasharray="3, 3" /> +<path d="m 160.5,90.5 v 30" stroke="black" + class="line-style" stroke-dasharray="3, 3" /> + +<text x="50" y="20" font-family="arial" font-size="12px" + class="text-style">0</text> +<text x="80" y="20" font-family="arial" font-size="12px" + class="text-style">1</text> +<text x="110" y="20" font-family="arial" font-size="12px" + class="text-style">2</text> +<text x="140" y="20" font-family="arial" font-size="12px" + class="text-style">...</text> + +<text x="190" y="55" font-family="arial" font-size="12px" font-weight="bold" + class="bold-style">Model index</text> +<rect x="185.5" y="60.5" width="77" height="37" stroke="black" fill="none" + class="line-style" /> +<text x="190" y="75" font-family="arial" font-size="12px" + class="text-style">row = 1</text> +<text x="190" y="92" font-family="arial" font-size="12px" + class="text-style">column = 2</text> + +<path d="m 185.5,85.5 c -35,0 -35,-10 -70,-10" stroke="black" fill="none" + stroke-dasharray="2, 2" class="line-style" /> +</svg> diff --git a/src/corelib/doc/src/cmake/cmake-properties.qdoc b/src/corelib/doc/src/cmake/cmake-properties.qdoc index c7b1a27a4b4..310debf7edc 100644 --- a/src/corelib/doc/src/cmake/cmake-properties.qdoc +++ b/src/corelib/doc/src/cmake/cmake-properties.qdoc @@ -239,7 +239,7 @@ precedence over this CMake property. \badcode set_target_properties(${target} PROPERTIES - QT_ANDROID_COMPILE_SDK_VERSION 35 + QT_ANDROID_COMPILE_SDK_VERSION 36 ) \endcode @@ -247,7 +247,7 @@ The following format also works: \badcode set_target_properties(${target} PROPERTIES - QT_ANDROID_COMPILE_SDK_VERSION "android-35" + QT_ANDROID_COMPILE_SDK_VERSION "android-36" ) \endcode diff --git a/src/corelib/itemmodels/qabstractitemmodel.cpp b/src/corelib/itemmodels/qabstractitemmodel.cpp index 58a057009ff..8b0a05ee6ed 100644 --- a/src/corelib/itemmodels/qabstractitemmodel.cpp +++ b/src/corelib/itemmodels/qabstractitemmodel.cpp @@ -1390,7 +1390,7 @@ void QAbstractItemModel::resetInternalData() simple table of rows and columns. Each item has a unique index specified by a QModelIndex. - \image modelindex-no-parent.png {Screenshot showing a 3x3 grid with numbered + \image modelindex-no-parent.svg {Diagram showing a 3x3 grid with numbered rows and columns that shows the cell at row 1, column 2 highlighted.} Every item of data that can be accessed via a model has an associated model diff --git a/src/corelib/kernel/qeventdispatcher_wasm.cpp b/src/corelib/kernel/qeventdispatcher_wasm.cpp index f7edf3285f8..25a27a0c22b 100644 --- a/src/corelib/kernel/qeventdispatcher_wasm.cpp +++ b/src/corelib/kernel/qeventdispatcher_wasm.cpp @@ -150,6 +150,18 @@ bool QEventDispatcherWasm::isValidEventDispatcherPointer(QEventDispatcherWasm *e bool QEventDispatcherWasm::processEvents(QEventLoop::ProcessEventsFlags flags) { + // Accept the current event if event handling recurses / event loop is reentered, + // to prevent the browser from propagating it to other event handlers. + if (useAsyncify() && isMainThreadEventDispatcher()) { + auto control = QWasmSuspendResumeControl::get(); + auto currentEvent = control->currentEvent(); + if (!currentEvent.isUndefined() && currentEvent.instanceof(emscripten::val::global("Event"))) { + currentEvent.call<void>("preventDefault"); + currentEvent.call<void>("stopPropagation"); + control->setCurrentEvent(emscripten::val::undefined()); + } + } + emit awake(); if (!useAsyncify() && isMainThreadEventDispatcher()) diff --git a/src/corelib/kernel/qeventdispatcher_wasm_p.h b/src/corelib/kernel/qeventdispatcher_wasm_p.h index 674af85dac2..1946bf63ede 100644 --- a/src/corelib/kernel/qeventdispatcher_wasm_p.h +++ b/src/corelib/kernel/qeventdispatcher_wasm_p.h @@ -40,7 +40,6 @@ public: ~QEventDispatcherWasm(); bool processEvents(QEventLoop::ProcessEventsFlags flags) override; - bool sendAllEvents(QEventLoop::ProcessEventsFlags flag); void registerTimer(Qt::TimerId timerId, Duration interval, Qt::TimerType timerType, QObject *object) override final; @@ -69,6 +68,7 @@ protected: virtual bool sendPostedEvents(); private: + bool sendAllEvents(QEventLoop::ProcessEventsFlags flag); bool isMainThreadEventDispatcher(); bool isSecondaryThreadEventDispatcher(); bool isValidEventDispatcher(); diff --git a/src/corelib/platform/wasm/qwasmlocalfileengine.cpp b/src/corelib/platform/wasm/qwasmlocalfileengine.cpp new file mode 100644 index 00000000000..58e662d5fde --- /dev/null +++ b/src/corelib/platform/wasm/qwasmlocalfileengine.cpp @@ -0,0 +1,422 @@ +// Copyright (C) 2025 Qt Group +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#include "qwasmlocalfileengine_p.h" +#include <QtCore/QDebug> +#include <QtCore/QUrl> + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +// Custom URL scheme for files handled by this file engine. A complete file URL can +// look like +// +// "weblocalfile:/n/file.ext" +// +// where n is a counter to ensure uniqueness, needed since web platform gives us the file name only +// and does not provide the file path. +// +// The scheme may be visible to end users if the application displays it, so +// we avoid using "wasm" and instead say "web", which should be more recognizable. +static constexpr QLatin1StringView wasmlocalfileScheme = "weblocalfile"_L1; + +// Instantiate the engine, the system will pick it up automatically +// Never destroy it to avoid problems with static destruction. +// The OS will reclaim the memory anyway, +// TODO: cleanup on QApplication destruction and re-init on QApplication re-creation. +static QWasmFileEngineHandler *singleton = new QWasmFileEngineHandler(); + +QWasmFileEngineHandler::QWasmFileEngineHandler() +{ +} + +QWasmFileEngineHandler::~QWasmFileEngineHandler() +{ +} + +std::unique_ptr<QAbstractFileEngine> QWasmFileEngineHandler::create(const QString &fileName) const +{ + if (!QWasmFileEngineHandler::isWasmFileName(fileName)) + return {}; + + // Check if it's a File or FileSystemFileHandle + if (singleton->m_files.contains(fileName)) { + qstdweb::File file = singleton->m_files.value(fileName); + return std::make_unique<QWasmFileEngine>(fileName, file); + } else if (singleton->m_fileSystemFiles.contains(fileName)) { + qstdweb::FileSystemFileHandle file = singleton->m_fileSystemFiles.value(fileName); + return std::make_unique<QWasmFileEngine>(fileName, file); + } + + // Not an error, this function will be called with partial paths like "weblocalfile:/1/" + return {}; +} + +// Check if this is a wasm filename by checking the URL scheme. +bool QWasmFileEngineHandler::isWasmFileName(const QString& fileName) +{ + return QUrl(fileName).scheme() == wasmlocalfileScheme; +} + +// Creates a wasm filename using the custom URL scheme and a counter. +QString QWasmFileEngineHandler::makeWasmFileName(const QString &nativeFileName) +{ + static std::atomic<uint64_t> sid = 0; + const uint64_t id = ++sid; + return wasmlocalfileScheme + QStringLiteral(":/%1/%2").arg(id).arg(nativeFileName); +} + +// Extracts the native filename from the custom URL (removes scheme and counter). +QString QWasmFileEngineHandler::nativeFileName(const QString &wasmFileName) +{ + QUrl url(wasmFileName); + if (url.scheme() == wasmlocalfileScheme) { + QString path = url.path(); + // Path is "/n/filename", find the second '/' and extract the filename + const qsizetype idx = path.indexOf(u'/', 1); + if (idx != -1) + return path.mid(idx + 1); + } + return wasmFileName; +} + +// Adds a File to the set of open files. Returns a prefixed wasm file name. +QString QWasmFileEngineHandler::addFile(qstdweb::File file) +{ + QString nativeFileName = QString::fromStdString(file.name()); + QString wasmFileName = makeWasmFileName(nativeFileName); + singleton->m_files.insert(wasmFileName, file); + return wasmFileName; +} + +// Adds a FileSystemFileHandle to the set of open files. Returns a prefixed wasm file name. +QString QWasmFileEngineHandler::addFile(qstdweb::FileSystemFileHandle file) +{ + QString nativeFileName = QString::fromStdString(file.name()); + QString wasmFileName = makeWasmFileName(nativeFileName); + singleton->m_fileSystemFiles.insert(wasmFileName, file); + return wasmFileName; +} + +// Removes a File or FileSystemFileHandle from the set of open file handlers +void QWasmFileEngineHandler::removeFile(const QString fileName) +{ + singleton->m_files.remove(fileName); + singleton->m_fileSystemFiles.remove(fileName); +} + +qstdweb::File QWasmFileEngineHandler::getFile(const QString fileName) +{ + return singleton->m_files.value(fileName); +} + +qstdweb::FileSystemFileHandle QWasmFileEngineHandler::getFileSystemFile(const QString fileName) +{ + return singleton->m_fileSystemFiles.value(fileName); +} + +/*! + \class QWasmFileEngine + \brief The QWasmFileEngine class provides a QAbstractFileEngine + for files that has the prefix ':weblocalfile/'. +*/ + +// Constructs a QWasmFileEngine with a File for read-only access +QWasmFileEngine::QWasmFileEngine(const QString &fileName, qstdweb::File file) + : m_fileName(fileName), + m_blobDevice(std::make_unique<qstdweb::BlobIODevice>(file.slice(0, file.size()))) +{ + // TODO use m_blobDevice in unbuffered mode? if there is already a buffer higher up. +} + +// Constructs a QWasmFileEngine with a FileSystemFileHandle for read-write access +QWasmFileEngine::QWasmFileEngine(const QString &fileName, qstdweb::FileSystemFileHandle file) + : m_fileName(fileName), + m_fileDevice(std::make_unique<qstdweb::FileSystemFileIODevice>(file)) +{ + +} + +QWasmFileEngine::~QWasmFileEngine() +{ + close(); +} + +bool QWasmFileEngine::open(QIODevice::OpenMode openMode, std::optional<QFile::Permissions> permissions) +{ + Q_UNUSED(permissions); + m_openMode = openMode; + + if (m_fileDevice) + return m_fileDevice->open(openMode); + else if (m_blobDevice) + return m_blobDevice->open(openMode); + + return false; +} + +bool QWasmFileEngine::close() +{ + if (m_openMode == QIODevice::NotOpen) + return false; + + bool success = true; + if (m_fileDevice) { + m_fileDevice->close(); + } + if (m_blobDevice) { + m_blobDevice->close(); + } + + m_openMode = QIODevice::NotOpen; + return success; +} + +bool QWasmFileEngine::flush() +{ + return true; +} + +bool QWasmFileEngine::syncToDisk() +{ + return true; +} + +qint64 QWasmFileEngine::size() const +{ + if (m_fileDevice) + return m_fileDevice->size(); + if (m_blobDevice) + return m_blobDevice->size(); + return 0; +} + +qint64 QWasmFileEngine::pos() const +{ + if (m_fileDevice) + return m_fileDevice->pos(); + if (m_blobDevice) + return m_blobDevice->pos(); + return 0; +} + +bool QWasmFileEngine::seek(qint64 pos) +{ + if (m_fileDevice) + return m_fileDevice->seek(pos); + if (m_blobDevice) + return m_blobDevice->seek(pos); + return false; +} + +bool QWasmFileEngine::isSequential() const +{ + return false; +} + +bool QWasmFileEngine::remove() +{ + return false; +} + +bool QWasmFileEngine::copy(const QString &newName) +{ + Q_UNUSED(newName); + return false; +} + +bool QWasmFileEngine::rename(const QString &newName) +{ + Q_UNUSED(newName); + return false; +} + +bool QWasmFileEngine::renameOverwrite(const QString &newName) +{ + Q_UNUSED(newName); + return false; +} + +bool QWasmFileEngine::link(const QString &newName) +{ + Q_UNUSED(newName); + return false; +} + +bool QWasmFileEngine::mkdir(const QString &dirName, bool createParentDirectories, + std::optional<QFile::Permissions> permissions) const +{ + Q_UNUSED(dirName); + Q_UNUSED(createParentDirectories); + Q_UNUSED(permissions); + return false; +} + +bool QWasmFileEngine::rmdir(const QString &dirName, bool recurseParentDirectories) const +{ + Q_UNUSED(dirName); + Q_UNUSED(recurseParentDirectories); + return false; +} + +bool QWasmFileEngine::setSize(qint64 size) +{ + Q_UNUSED(size); + return false; +} + +bool QWasmFileEngine::caseSensitive() const +{ + return true; +} + +bool QWasmFileEngine::isRelativePath() const +{ + return false; +} + + +QAbstractFileEngine::FileFlags QWasmFileEngine::fileFlags(FileFlags type) const +{ + return type & (QAbstractFileEngine::FileFlag::ExistsFlag | + QAbstractFileEngine::FileFlag::FileType | + QAbstractFileEngine::FileFlag::ReadOwnerPerm | + QAbstractFileEngine::FileFlag::WriteOwnerPerm); +} + +bool QWasmFileEngine::setPermissions(uint perms) +{ + Q_UNUSED(perms); + return false; +} + +QByteArray QWasmFileEngine::id() const +{ + return {}; +} + +QString QWasmFileEngine::fileName(FileName file) const +{ + switch (file) { + case DefaultName: + case AbsoluteName: + case CanonicalName: + return m_fileName; + case BaseName: { + QString native = QWasmFileEngineHandler::nativeFileName(m_fileName); + QFileInfo info(native); + return info.fileName(); + } + case PathName: + case AbsolutePathName: + case CanonicalPathName: { + QString native = QWasmFileEngineHandler::nativeFileName(m_fileName); + QFileInfo info(native); + QString path = info.path(); + return path.isEmpty() ? "."_L1 : path; + } + default: + return QString(); + } +} + +uint QWasmFileEngine::ownerId(FileOwner) const +{ + return 0; +} + +QString QWasmFileEngine::owner(FileOwner) const +{ + return {}; +} + +bool QWasmFileEngine::setFileTime(const QDateTime &newDate, QFile::FileTime time) +{ + Q_UNUSED(newDate); + Q_UNUSED(time); + return false; +} + +QDateTime QWasmFileEngine::fileTime(QFile::FileTime time) const +{ + Q_UNUSED(time); + return {}; +} + +void QWasmFileEngine::setFileName(const QString &file) +{ + if (m_fileName == file) + return; + close(); + m_fileName = file; +} + +int QWasmFileEngine::handle() const +{ + return -1; +} + +QAbstractFileEngine::TriStateResult QWasmFileEngine::cloneTo(QAbstractFileEngine *target) +{ + Q_UNUSED(target); + return QAbstractFileEngine::TriStateResult::NotSupported; +} + +QAbstractFileEngine::IteratorUniquePtr QWasmFileEngine::beginEntryList(const QString &path, QDirListing::IteratorFlags filters, + const QStringList &filterNames) +{ + Q_UNUSED(path); + Q_UNUSED(filters); + Q_UNUSED(filterNames); + return nullptr; +} + +qint64 QWasmFileEngine::read(char *data, qint64 maxlen) +{ + if (!(m_openMode & QIODevice::ReadOnly)) + return -1; + + if (m_fileDevice) + return m_fileDevice->read(data, maxlen); + if (m_blobDevice) + return m_blobDevice->read(data, maxlen); + return -1; +} + +qint64 QWasmFileEngine::readLine(char *data, qint64 maxlen) +{ + if (!(m_openMode & QIODevice::ReadOnly)) + return -1; + + if (m_fileDevice) + return m_fileDevice->readLine(data, maxlen); + if (m_blobDevice) + return m_blobDevice->readLine(data, maxlen); + return -1; +} + +qint64 QWasmFileEngine::write(const char *data, qint64 len) +{ + if (!(m_openMode & QIODevice::WriteOnly)) + return -1; + + if (m_fileDevice) + return m_fileDevice->write(data, len); + return -1; +} + +bool QWasmFileEngine::extension(Extension extension, const ExtensionOption *option, ExtensionReturn *output) +{ + Q_UNUSED(extension); + Q_UNUSED(option); + Q_UNUSED(output); + return false; +} + +bool QWasmFileEngine::supportsExtension(Extension extension) const +{ + Q_UNUSED(extension); + return false; +} + +QT_END_NAMESPACE diff --git a/src/corelib/platform/wasm/qwasmlocalfileengine_p.h b/src/corelib/platform/wasm/qwasmlocalfileengine_p.h new file mode 100644 index 00000000000..dd82788dd90 --- /dev/null +++ b/src/corelib/platform/wasm/qwasmlocalfileengine_p.h @@ -0,0 +1,102 @@ +// Copyright (C) 2024 Qt Group +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#ifndef QWASMLOCALFILEENGINE_P_H +#define QWASMLOCALFILEENGINE_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of other Qt classes. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/private/qabstractfileengine_p.h> +#include <QtCore/private/qstdweb_p.h> + +QT_BEGIN_NAMESPACE + +class QWasmFileEngine; + +class QWasmFileEngineHandler: public QAbstractFileEngineHandler +{ +public: + Q_DISABLE_COPY_MOVE(QWasmFileEngineHandler) + + QWasmFileEngineHandler(); + virtual ~QWasmFileEngineHandler() override; + virtual std::unique_ptr<QAbstractFileEngine> create(const QString &fileName) const override; + + static bool isWasmFileName(const QString& fileName); + static QString makeWasmFileName(const QString &fileName); + static QString nativeFileName(const QString &wasmFileName); + + static QString addFile(qstdweb::File file); + static QString addFile(qstdweb::FileSystemFileHandle file); + static void removeFile(const QString fileName); + qstdweb::File getFile(const QString fileName); + qstdweb::FileSystemFileHandle getFileSystemFile(const QString fileName); + +private: + QHash<QString, qstdweb::File> m_files; + QHash<QString, qstdweb::FileSystemFileHandle> m_fileSystemFiles; +}; + +class QWasmFileEngine: public QAbstractFileEngine +{ +public: + explicit QWasmFileEngine(const QString &fileName, qstdweb::File file); + explicit QWasmFileEngine(const QString &fileName, qstdweb::FileSystemFileHandle file); + ~QWasmFileEngine() override; + + virtual bool open(QIODevice::OpenMode openMode, std::optional<QFile::Permissions> permissions) override; + virtual bool close() override; + virtual bool flush() override; + virtual bool syncToDisk() override; + virtual qint64 size() const override; + virtual qint64 pos() const override; + virtual bool seek(qint64 pos) override; + virtual bool isSequential() const override; + virtual bool remove() override; + virtual bool copy(const QString &newName) override; + virtual bool rename(const QString &newName) override; + virtual bool renameOverwrite(const QString &newName) override; + virtual bool link(const QString &newName) override; + virtual bool mkdir(const QString &dirName, bool createParentDirectories, + std::optional<QFile::Permissions> permissions = std::nullopt) const override; + virtual bool rmdir(const QString &dirName, bool recurseParentDirectories) const override; + virtual bool setSize(qint64 size) override; + virtual bool caseSensitive() const override; + virtual bool isRelativePath() const override; + virtual FileFlags fileFlags(FileFlags type=FileInfoAll) const override; + virtual bool setPermissions(uint perms) override; + virtual QByteArray id() const override; + virtual QString fileName(FileName file = DefaultName) const override; + virtual uint ownerId(FileOwner) const override; + virtual QString owner(FileOwner) const override; + virtual bool setFileTime(const QDateTime &newDate, QFile::FileTime time) override; + virtual QDateTime fileTime(QFile::FileTime time) const override; + virtual void setFileName(const QString &file) override; + virtual int handle() const override; + virtual TriStateResult cloneTo(QAbstractFileEngine *target) override; + virtual IteratorUniquePtr beginEntryList(const QString &path, QDirListing::IteratorFlags filters, + const QStringList &filterNames) override; + virtual qint64 read(char *data, qint64 maxlen) override; + virtual qint64 readLine(char *data, qint64 maxlen) override; + virtual qint64 write(const char *data, qint64 len) override; + virtual bool extension(Extension extension, const ExtensionOption *option = nullptr, + ExtensionReturn *output = nullptr) override; + virtual bool supportsExtension(Extension extension) const override; + +private: + QString m_fileName; + QIODevice::OpenMode m_openMode = QIODevice::NotOpen; + std::unique_ptr<qstdweb::BlobIODevice> m_blobDevice; + std::unique_ptr<qstdweb::FileSystemFileIODevice> m_fileDevice; +}; + +QT_END_NAMESPACE +#endif // QWASMFILEENGINEHANDLER_H diff --git a/src/corelib/platform/wasm/qwasmsuspendresumecontrol.cpp b/src/corelib/platform/wasm/qwasmsuspendresumecontrol.cpp index a4bc7843380..85358d1daf4 100644 --- a/src/corelib/platform/wasm/qwasmsuspendresumecontrol.cpp +++ b/src/corelib/platform/wasm/qwasmsuspendresumecontrol.cpp @@ -225,8 +225,11 @@ int QWasmSuspendResumeControl::sendPendingEvents() pendingEvents.call<void>("splice", i, 1); auto it = m_eventHandlers.find(event["index"].as<int>()); - if (it != m_eventHandlers.end()) - it->second(event["arg"]); + if (it != m_eventHandlers.end()) { + setCurrentEvent(event["arg"]); + it->second(currentEvent()); + setCurrentEvent(emscripten::val::undefined()); + } ++count; } } diff --git a/src/corelib/platform/wasm/qwasmsuspendresumecontrol_p.h b/src/corelib/platform/wasm/qwasmsuspendresumecontrol_p.h index ff97ff3d7ea..4e75d5674be 100644 --- a/src/corelib/platform/wasm/qwasmsuspendresumecontrol_p.h +++ b/src/corelib/platform/wasm/qwasmsuspendresumecontrol_p.h @@ -42,12 +42,22 @@ public: void suspendExclusive(QList<uint32_t> eventHandlerIndices); int sendPendingEvents(); + emscripten::val currentEvent() const + { + return m_currentEvent; + } + void setCurrentEvent(emscripten::val currentEvent) + { + m_currentEvent = currentEvent; + } + private: friend void qtSendPendingEvents(); static QWasmSuspendResumeControl *s_suspendResumeControl; std::map<int, std::function<void(emscripten::val)>> m_eventHandlers; std::function<bool(int)> m_eventFilter = [](int) { return true; }; + emscripten::val m_currentEvent = emscripten::val::undefined(); }; class Q_CORE_EXPORT QWasmEventHandler diff --git a/src/gui/platform/wasm/qwasmlocalfileaccess.cpp b/src/gui/platform/wasm/qwasmlocalfileaccess.cpp index 051e2b1a04e..8d06821d4c1 100644 --- a/src/gui/platform/wasm/qwasmlocalfileaccess.cpp +++ b/src/gui/platform/wasm/qwasmlocalfileaccess.cpp @@ -285,6 +285,44 @@ void saveFile(const char *content, size_t size, const std::string &fileNameHint) }); } +void showOpenFileDialog(const std::string &accept, + const std::function<void (bool accepted, std::vector<qstdweb::File> files)> fileDialogClosed) +{ + FileDialog::showOpen(makeFilterList(accept), FileSelectMode::MultipleFiles, { + .thenFunc = [=](emscripten::val result) { + if (result.isUndefined() || result.isNull()) { + fileDialogClosed(false, std::vector<qstdweb::File>()); + } else { + std::vector<qstdweb::File> files; + int length = result["length"].as<int>(); + files.reserve(length); + for (int i = 0; i < length; ++i) { + emscripten::val fileVal = result[i]; + if (!fileVal.isUndefined() && !fileVal.isNull()) { + files.push_back(qstdweb::File(fileVal)); + } + } + fileDialogClosed(true, files); + } + }, + .catchFunc = [=](emscripten::val) { + fileDialogClosed(false, std::vector<qstdweb::File>()); + } + }); +} + +void showSaveFileDialog(const std::string &fileNameHint, const std::function<void (bool accepted, qstdweb::FileSystemFileHandle fileHandle)> fileDialogClosed) +{ + FileDialog::showSave(fileNameHint, { + .thenFunc = [=](emscripten::val result) { + fileDialogClosed(true, qstdweb::FileSystemFileHandle(result)); + }, + .catchFunc = [=](emscripten::val) { + fileDialogClosed(false, qstdweb::FileSystemFileHandle()); + } + }); +} + } // namespace QWasmLocalFileAccess QT_END_NAMESPACE diff --git a/src/gui/platform/wasm/qwasmlocalfileaccess_p.h b/src/gui/platform/wasm/qwasmlocalfileaccess_p.h index 77b14577f7e..5597746a5f3 100644 --- a/src/gui/platform/wasm/qwasmlocalfileaccess_p.h +++ b/src/gui/platform/wasm/qwasmlocalfileaccess_p.h @@ -16,6 +16,7 @@ // #include <private/qglobal_p.h> +#include <private/qstdweb_p.h> #include <cstdint> #include <functional> @@ -38,6 +39,12 @@ Q_CORE_EXPORT void openFile(const std::string &accept, Q_CORE_EXPORT void saveFile(const QByteArray &data, const std::string &fileNameHint); Q_CORE_EXPORT void saveFile(const char *content, size_t size, const std::string &fileNameHint); +Q_CORE_EXPORT void showOpenFileDialog(const std::string &accept, + const std::function<void (bool accepted, std::vector<qstdweb::File> files)> fileDialogClosed); +Q_CORE_EXPORT void showSaveFileDialog(const std::string &fileNameHint, + const std::function<void (bool accepted, qstdweb::FileSystemFileHandle fileHandle)> fileDialogClosed); + + } // namespace QWasmLocalFileAccess QT_END_NAMESPACE diff --git a/src/network/android/jar/build.gradle b/src/network/android/jar/build.gradle index f1f470b6635..580e15b92f6 100644 --- a/src/network/android/jar/build.gradle +++ b/src/network/android/jar/build.gradle @@ -23,7 +23,7 @@ repositories { } android { - compileSdk 35 + compileSdk 36 defaultConfig { minSdkVersion 28 diff --git a/src/plugins/networkinformation/android/jar/build.gradle b/src/plugins/networkinformation/android/jar/build.gradle index f1f470b6635..580e15b92f6 100644 --- a/src/plugins/networkinformation/android/jar/build.gradle +++ b/src/plugins/networkinformation/android/jar/build.gradle @@ -23,7 +23,7 @@ repositories { } android { - compileSdk 35 + compileSdk 36 defaultConfig { minSdkVersion 28 diff --git a/src/plugins/platforms/cocoa/qnsview.mm b/src/plugins/platforms/cocoa/qnsview.mm index a6d96df0fbb..b00e6375e1a 100644 --- a/src/plugins/platforms/cocoa/qnsview.mm +++ b/src/plugins/platforms/cocoa/qnsview.mm @@ -269,6 +269,23 @@ QT_NAMESPACE_ALIAS_OBJC_CLASS(QNSViewMenuHelper); qCDebug(lcQpaWindow) << "Done moving" << self << "to" << self.window; } +// QWindow::setParent() promises that the child window will be clipped +// to its parent, which we rely on in e.g. Qt Widgets when a native window +// is added to a scroll area. We try to be smart and only enable clipping +// if we have potential child QWindows that rely on this behavior. +// FIXME: Be even smarter, and only consider QWindow based subviews, +// in a way that also includes foreign windows. + +- (void)didAddSubview:(NSView *)subview +{ + self.clipsToBounds = YES; +} + +- (void)willRemoveSubview:(NSView *)subview +{ + self.clipsToBounds = self.subviews.count > 1; +} + // ---------------------------------------------------------------------------- - (QWindow *)topLevelWindow diff --git a/src/plugins/platforms/wasm/CMakeLists.txt b/src/plugins/platforms/wasm/CMakeLists.txt index 03cb0d52ca1..7e9beb7e832 100644 --- a/src/plugins/platforms/wasm/CMakeLists.txt +++ b/src/plugins/platforms/wasm/CMakeLists.txt @@ -33,6 +33,7 @@ qt_internal_add_plugin(QWasmIntegrationPlugin qwasmwindownonclientarea.cpp qwasmwindownonclientarea.h qwasminputcontext.cpp qwasminputcontext.h qwasmwindowstack.h + qwasmfiledialoghelper.cpp qwasmfiledialoghelper.h DEFINES QT_EGL_NO_X11 QT_NO_FOREACH diff --git a/src/plugins/platforms/wasm/qwasmdom.cpp b/src/plugins/platforms/wasm/qwasmdom.cpp index 71efcee7887..eefd18154aa 100644 --- a/src/plugins/platforms/wasm/qwasmdom.cpp +++ b/src/plugins/platforms/wasm/qwasmdom.cpp @@ -10,6 +10,7 @@ #include <QtCore/qrect.h> #include <QtGui/qimage.h> #include <private/qstdweb_p.h> +#include <private/qwasmlocalfileengine_p.h> #include <QtCore/qurl.h> #include <utility> @@ -112,11 +113,14 @@ void DataTransfer::toMimeDataWithFile(std::function<void(QMimeData *)> callback) 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 temporary files; we expect that the user callback reads/copies + // file content before returning.// Fixme: tie file lifetime to lifetime of the QMimeData? + // Note: QWasmFileEngine files (weblocalfile://) are managed by QWasmFileEngine + // and are not deleted here + for (QUrl fileUrl: fileUrls) { + if (!QWasmFileEngineHandler::isWasmFileName(fileUrl.toString())) + QFile(fileUrl.toLocalFile()).remove(); + } delete this; } @@ -144,49 +148,56 @@ void DataTransfer::toMimeDataWithFile(std::function<void(QMimeData *)> callback) 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(); + // Add a file access url for the local file. If asyncify is available, + // add a QWasmFileEngine managed url. Else fall back to placing a copy + // of the file at /tmp on Emsripten's in-memory file system. + if (qstdweb::haveAsyncify()) { + QUrl fileUrl(QWasmFileEngineHandler::addFile(webfile)); + mimeContext->fileUrls.append(fileUrl); mimeContext->deref(); - continue; - } + } else { + // Limit in-memory file size to 1 GB + if (webfile.size() > 1e+9) { + 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()); + // Read file content + QByteArray fileContent(webfile.size(), Qt::Uninitialized); + webfile.stream(fileContent.data(), [=]() { + 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(); + }); - // 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 + // 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. + QString mimeFormat = QString::fromStdString(webfile.type()); 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: diff --git a/src/plugins/platforms/wasm/qwasmfiledialoghelper.cpp b/src/plugins/platforms/wasm/qwasmfiledialoghelper.cpp new file mode 100644 index 00000000000..1e6e2b1f644 --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmfiledialoghelper.cpp @@ -0,0 +1,157 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "qwasmfiledialoghelper.h" + +#include <QtCore/QDebug> +#include <QtCore/QUrl> +#include <QtGui/private/qwasmlocalfileaccess_p.h> +#include <QtCore/private/qwasmlocalfileengine_p.h> + +QT_BEGIN_NAMESPACE + +QWasmFileDialogHelper::QWasmFileDialogHelper() + : m_eventLoop(nullptr) +{ + +} + +QWasmFileDialogHelper::~QWasmFileDialogHelper() +{ + +} + +bool QWasmFileDialogHelper::defaultNameFilterDisables() const +{ + return false; +} + +void QWasmFileDialogHelper::setDirectory(const QUrl &directory) +{ + Q_UNUSED(directory) +} + +QUrl QWasmFileDialogHelper::directory() const +{ + return QUrl(); +} + +void QWasmFileDialogHelper::selectFile(const QUrl &file) +{ + m_selectedFiles.clear(); + m_selectedFiles.append(file); +} + +QList<QUrl> QWasmFileDialogHelper::selectedFiles() const +{ + return m_selectedFiles; +} + +void QWasmFileDialogHelper::setFilter() +{ + +} + +void QWasmFileDialogHelper::selectNameFilter(const QString &filter) +{ + Q_UNUSED(filter); + // TODO +} + +QString QWasmFileDialogHelper::selectedNameFilter() const +{ + return QString(); +} + +void QWasmFileDialogHelper::exec() +{ + QEventLoop eventLoop; + m_eventLoop = &eventLoop; + eventLoop.exec(); + m_eventLoop = nullptr; +} + +bool QWasmFileDialogHelper::show(Qt::WindowFlags flags, Qt::WindowModality modality, QWindow *parent) +{ + Q_UNUSED(flags) + Q_UNUSED(modality) + Q_UNUSED(parent) + showFileDialog(); + return true; +} + +void QWasmFileDialogHelper::hide() +{ + +} + +void QWasmFileDialogHelper::showFileDialog() +{ + if (options()->acceptMode() == QFileDialogOptions::AcceptOpen) { + // Use name filters from options + QString nameFilter = options()->nameFilters().join(";;"); + if (nameFilter.isEmpty()) + nameFilter = "*"; + + QWasmLocalFileAccess::showOpenFileDialog(nameFilter.toStdString(), [this](bool accepted, std::vector<qstdweb::File> files) { + onOpenDialogClosed(accepted, files); + }); + } else if (options()->acceptMode() == QFileDialogOptions::AcceptSave) { + QString suggestion = m_selectedFiles.isEmpty() ? QString() : QUrl(m_selectedFiles.first()).fileName(); + m_selectedFiles.clear(); + + QWasmLocalFileAccess::showSaveFileDialog(suggestion.toStdString(), [this](bool accepted, qstdweb::FileSystemFileHandle file){ + onSaveDialogClosed(accepted, file); + }); + } +} + +void QWasmFileDialogHelper::onOpenDialogClosed(bool accepted, std::vector<qstdweb::File> files) +{ + m_selectedFiles.clear(); + + if (!accepted) { + emit reject(); + return; + } + + // Track opened files + for (const auto &file : files) { + QString wasmFileName = QWasmFileEngineHandler::addFile(file); + QUrl fileUrl(wasmFileName); + m_selectedFiles.append(fileUrl); + } + + // Emit signals + if (m_selectedFiles.size() > 0) { + emit fileSelected(m_selectedFiles.first()); + emit filesSelected(m_selectedFiles); + } + emit accept(); + + // exit exec() if in exec() + if (m_eventLoop) + m_eventLoop->quit(); +} + +void QWasmFileDialogHelper::onSaveDialogClosed(bool accepted, qstdweb::FileSystemFileHandle file) +{ + if (!accepted) { + emit reject(); + return; + } + + // Track save file + QString wasmFileName = QWasmFileEngineHandler::addFile(file); + QUrl fileUrl(wasmFileName); + m_selectedFiles.append(fileUrl); + + // Emit signals + emit fileSelected(m_selectedFiles.first()); + emit accept(); + + if (m_eventLoop) + m_eventLoop->quit(); +} + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmfiledialoghelper.h b/src/plugins/platforms/wasm/qwasmfiledialoghelper.h new file mode 100644 index 00000000000..c5a5b57e518 --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmfiledialoghelper.h @@ -0,0 +1,49 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef QWASMFILEDIALOGHELPER_H +#define QWASMFILEDIALOGHELPER_H + +#include <QtCore/QObject> +#include <QtCore/QUrl> +#include <QtCore/QEventLoop> +#include <QtGui/qpa/qplatformdialoghelper.h> +#include <QtGui/private/qwasmlocalfileaccess_p.h> + +QT_BEGIN_NAMESPACE + +class QWasmFileDialogHelper : public QPlatformFileDialogHelper +{ + Q_OBJECT +public: + QWasmFileDialogHelper(); + ~QWasmFileDialogHelper(); +public: + virtual void exec() override; + virtual bool show(Qt::WindowFlags windowFlags, + Qt::WindowModality windowModality, + QWindow *parent) override; + virtual void hide() override; + virtual bool defaultNameFilterDisables() const override; + virtual void setDirectory(const QUrl &directory) override; + virtual QUrl directory() const override; + virtual void selectFile(const QUrl &filename) override; + virtual QList<QUrl> selectedFiles() const override; + virtual void setFilter() override; + virtual void selectNameFilter(const QString &filter) override; + virtual QString selectedNameFilter() const override; + static QStringList cleanFilterList(const QString &filter); +signals: + void fileDone(const QUrl &); +private: + void showFileDialog(); + void onOpenDialogClosed(bool accepted, std::vector<qstdweb::File> files); + void onSaveDialogClosed(bool accepted, qstdweb::FileSystemFileHandle file); + + QList<QUrl> m_selectedFiles; + QEventLoop *m_eventLoop; +}; + +QT_END_NAMESPACE + +#endif // QWASMFILEDIALOGHELPER_H diff --git a/src/plugins/platforms/wasm/qwasmtheme.cpp b/src/plugins/platforms/wasm/qwasmtheme.cpp index b9340f31275..b1e5c208c6c 100644 --- a/src/plugins/platforms/wasm/qwasmtheme.cpp +++ b/src/plugins/platforms/wasm/qwasmtheme.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "qwasmtheme.h" +#include "qwasmfiledialoghelper.h" #include <QtCore/qvariant.h> #include <QFontDatabase> #include <QList> @@ -127,6 +128,18 @@ const QFont *QWasmTheme::font(Font type) const return nullptr; } +bool QWasmTheme::usePlatformNativeDialog(DialogType type) const +{ + return (type == DialogType::FileDialog); +} + +QPlatformDialogHelper *QWasmTheme::createPlatformDialogHelper(DialogType type) const +{ + if (type == DialogType::FileDialog) + return new QWasmFileDialogHelper(); + return nullptr; +} + void QWasmTheme::onColorSchemeChange() { auto colorScheme = getColorSchemeFromMedia(); diff --git a/src/plugins/platforms/wasm/qwasmtheme.h b/src/plugins/platforms/wasm/qwasmtheme.h index 4eaad874c76..8b8dd6ebd97 100644 --- a/src/plugins/platforms/wasm/qwasmtheme.h +++ b/src/plugins/platforms/wasm/qwasmtheme.h @@ -57,6 +57,9 @@ public: Qt::ContrastPreference contrastPreference() const override; QVariant themeHint(ThemeHint hint) const override; const QFont *font(Font type) const override; + bool usePlatformNativeDialog(DialogType type) const override; + QPlatformDialogHelper *createPlatformDialogHelper(DialogType type) const override; + QFont *fixedFont = nullptr; void onColorSchemeChange(); diff --git a/src/plugins/platforms/wasm/qwasmwindow.cpp b/src/plugins/platforms/wasm/qwasmwindow.cpp index 6e8bd46ca58..7f1dd7eb34c 100644 --- a/src/plugins/platforms/wasm/qwasmwindow.cpp +++ b/src/plugins/platforms/wasm/qwasmwindow.cpp @@ -925,7 +925,7 @@ bool QWasmWindow::deliverPointerEvent(const PointerEvent &event) std::back_inserter(touchPointList), [](const QWindowSystemInterface::TouchPoint &val) { return val; }); - if (event.type == EventType::PointerUp) + if (event.type == EventType::PointerUp || event.type == EventType::PointerCancel) m_pointerIdToTouchPoints.remove(event.pointerId); return event.type == EventType::PointerCancel diff --git a/src/plugins/platforms/windows/qwindowswindow.cpp b/src/plugins/platforms/windows/qwindowswindow.cpp index 2816982b1a8..9459e8bdfd3 100644 --- a/src/plugins/platforms/windows/qwindowswindow.cpp +++ b/src/plugins/platforms/windows/qwindowswindow.cpp @@ -848,7 +848,7 @@ void WindowCreationData::fromWindow(const QWindow *w, const Qt::WindowFlags flag // make mouse events fall through this window // NOTE: WS_EX_TRANSPARENT flag can make mouse inputs fall through a layered window if (flagsIn & Qt::WindowTransparentForInput) - exStyle |= WS_EX_LAYERED | WS_EX_TRANSPARENT; + exStyle |= WS_EX_TRANSPARENT; // Currently only compatible with D3D surfaces, use it with care. if (qEnvironmentVariableIntValue("QT_QPA_DISABLE_REDIRECTION_SURFACE")) diff --git a/src/tools/androiddeployqt/main.cpp b/src/tools/androiddeployqt/main.cpp index 7f24df7eac2..c4e7101b372 100644 --- a/src/tools/androiddeployqt/main.cpp +++ b/src/tools/androiddeployqt/main.cpp @@ -173,7 +173,7 @@ struct Options QString versionName; QString versionCode; QByteArray minSdkVersion{"28"}; - QByteArray targetSdkVersion{"35"}; + QByteArray targetSdkVersion{"36"}; // lib c++ path QString stdCppPath; diff --git a/src/tools/bootstrap/CMakeLists.txt b/src/tools/bootstrap/CMakeLists.txt index e6f920dcf32..c12475d69af 100644 --- a/src/tools/bootstrap/CMakeLists.txt +++ b/src/tools/bootstrap/CMakeLists.txt @@ -220,6 +220,10 @@ target_link_libraries(Bootstrap PRIVATE PlatformCommonInternal) qt_internal_apply_gc_binaries(Bootstrap PUBLIC) set_target_properties(Bootstrap PROPERTIES AUTOMOC OFF AUTOUIC OFF AUTORCC OFF) qt_internal_add_target_aliases(Bootstrap) +qt_internal_add_target_optimized_flags_for_debug_config_in_current_scope(Bootstrap) +qt_internal_enable_optimized_tools_lto(Bootstrap) +qt_internal_workaround_static_lib_gcc_lto_issue(Bootstrap) + qt_set_msvc_cplusplus_options(Bootstrap PUBLIC) qt_set_common_target_properties(Bootstrap) qt_internal_apply_intel_cet(Bootstrap PUBLIC) diff --git a/src/tools/rcc/rcc.cpp b/src/tools/rcc/rcc.cpp index 40bfa9c2863..6a6027d44f2 100644 --- a/src/tools/rcc/rcc.cpp +++ b/src/tools/rcc/rcc.cpp @@ -1,10 +1,12 @@ // Copyright (C) 2018 The Qt Company Ltd. // Copyright (C) 2018 Intel Corporation. +// Copyright (C) 2024 Christoph Cullmann <christoph@cullmann.io> // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "rcc.h" #include <qbytearray.h> +#include <qcryptographichash.h> #include <qdatetime.h> #include <qdebug.h> #include <qdir.h> @@ -90,8 +92,28 @@ public: QString resourceName() const; + struct DeduplicationKey { + RCCResourceLibrary::CompressionAlgorithm compressAlgo; + int compressLevel; + int compressThreshold; + QByteArray hash; + + bool operator==(const DeduplicationKey &other) const + { + return compressAlgo == other.compressAlgo && + compressLevel == other.compressLevel && + compressThreshold == other.compressThreshold && + hash == other.hash; + } + }; + + typedef QMultiHash<DeduplicationKey, RCCFileInfo*> DeduplicationMultiHash; + public: - qint64 writeDataBlob(RCCResourceLibrary &lib, qint64 offset, QString *errorMessage); + qint64 writeDataBlob(RCCResourceLibrary &lib, + qint64 offset, + DeduplicationMultiHash &dedupByContent, + QString *errorMessage); qint64 writeDataName(RCCResourceLibrary &, qint64 offset); void writeDataInfo(RCCResourceLibrary &lib); @@ -114,6 +136,11 @@ public: qint64 m_childOffset = 0; }; +static size_t qHash(const RCCFileInfo::DeduplicationKey &key, size_t seed) noexcept +{ + return qHashMulti(seed, key.compressAlgo, key.compressLevel, key.compressThreshold, key.hash); +} + RCCFileInfo::RCCFileInfo(const QString &name, const QFileInfo &fileInfo, QLocale::Language language, QLocale::Territory territory, uint flags, RCCResourceLibrary::CompressionAlgorithm compressAlgo, int compressLevel, @@ -217,8 +244,10 @@ void RCCFileInfo::writeDataInfo(RCCResourceLibrary &lib) } } -qint64 RCCFileInfo::writeDataBlob(RCCResourceLibrary &lib, qint64 offset, - QString *errorMessage) +qint64 RCCFileInfo::writeDataBlob(RCCResourceLibrary &lib, + qint64 offset, + DeduplicationMultiHash &dedupByContent, + QString *errorMessage) { const bool text = lib.m_format == RCCResourceLibrary::C_Code; const bool pass1 = lib.m_format == RCCResourceLibrary::Pass1; @@ -230,24 +259,58 @@ qint64 RCCFileInfo::writeDataBlob(RCCResourceLibrary &lib, qint64 offset, m_dataOffset = offset; QByteArray data; + // determine compession algorithm & level early as used in de-duplication keys + // this avoid corruption for the two pass variants (QTBUG-137546) +#if QT_CONFIG(zstd) + if (m_compressAlgo == RCCResourceLibrary::CompressionAlgorithm::Best && !m_noZstd) { + m_compressAlgo = RCCResourceLibrary::CompressionAlgorithm::Zstd; + m_compressLevel = 19; // not ZSTD_maxCLevel(), as 20+ are experimental + } +#endif +#ifndef QT_NO_COMPRESS + if (m_compressAlgo == RCCResourceLibrary::CompressionAlgorithm::Best) { + m_compressAlgo = RCCResourceLibrary::CompressionAlgorithm::Zlib; + m_compressLevel = 9; + } +#endif + if (!m_isEmpty) { - //find the data to be written - QFile file(m_fileInfo.absoluteFilePath()); + // find the data to be written + const QString absoluteFilePath = m_fileInfo.absoluteFilePath(); + QFile file(absoluteFilePath); if (!file.open(QFile::ReadOnly)) { - *errorMessage = msgOpenReadFailed(m_fileInfo.absoluteFilePath(), file.errorString()); + *errorMessage = msgOpenReadFailed(absoluteFilePath, file.errorString()); return 0; } - data = file.readAll(); + + // de-duplicate the same file content, we can re-use already written data + // we only do that if we have the same compression settings + const QByteArray hash = QCryptographicHash::hash(data, QCryptographicHash::Sha256); + const DeduplicationKey key{m_compressAlgo, m_compressLevel, m_compressThreshold, hash}; + const QList<RCCFileInfo *> potentialCandidates = dedupByContent.values(key); + for (const RCCFileInfo *candidate : potentialCandidates) { + // check real content, we can have collisions + QFile candidateFile(candidate->m_fileInfo.absoluteFilePath()); + if (!candidateFile.open(QFile::ReadOnly)) { + *errorMessage = msgOpenReadFailed(candidate->m_fileInfo.absoluteFilePath(), + candidateFile.errorString()); + return 0; + } + if (data != candidateFile.readAll()) + continue; + // just remember the offset & flags with final compression state + // of the already written data and be done + m_dataOffset = candidate->m_dataOffset; + m_flags = candidate->m_flags; + return offset; + } + dedupByContent.insert(key, this); } // Check if compression is useful for this file if (data.size() != 0) { #if QT_CONFIG(zstd) - if (m_compressAlgo == RCCResourceLibrary::CompressionAlgorithm::Best && !m_noZstd) { - m_compressAlgo = RCCResourceLibrary::CompressionAlgorithm::Zstd; - m_compressLevel = 19; // not ZSTD_maxCLevel(), as 20+ are experimental - } if (m_compressAlgo == RCCResourceLibrary::CompressionAlgorithm::Zstd && !m_noZstd) { if (lib.m_zstdCCtx == nullptr) lib.m_zstdCCtx = ZSTD_createCCtx(); @@ -292,10 +355,6 @@ qint64 RCCFileInfo::writeDataBlob(RCCResourceLibrary &lib, qint64 offset, } #endif #ifndef QT_NO_COMPRESS - if (m_compressAlgo == RCCResourceLibrary::CompressionAlgorithm::Best) { - m_compressAlgo = RCCResourceLibrary::CompressionAlgorithm::Zlib; - m_compressLevel = 9; - } if (m_compressAlgo == RCCResourceLibrary::CompressionAlgorithm::Zlib) { QByteArray compressed = qCompress(reinterpret_cast<uchar *>(data.data()), data.size(), m_compressLevel); @@ -1168,6 +1227,7 @@ bool RCCResourceLibrary::writeDataBlobs() QStack<RCCFileInfo*> pending; pending.push(m_root); qint64 offset = 0; + RCCFileInfo::DeduplicationMultiHash dedupByContent; QString errorMessage; while (!pending.isEmpty()) { RCCFileInfo *file = pending.pop(); @@ -1176,7 +1236,8 @@ bool RCCResourceLibrary::writeDataBlobs() if (child->m_flags & RCCFileInfo::Directory) pending.push(child); else { - offset = child->writeDataBlob(*this, offset, &errorMessage); + offset = child->writeDataBlob(*this, offset, + dedupByContent, &errorMessage); if (offset == 0) { m_errorDevice->write(errorMessage.toUtf8()); return false; diff --git a/src/widgets/kernel/qapplication.cpp b/src/widgets/kernel/qapplication.cpp index d2b0fb724ac..a735696adcb 100644 --- a/src/widgets/kernel/qapplication.cpp +++ b/src/widgets/kernel/qapplication.cpp @@ -3741,6 +3741,14 @@ void QApplicationPrivate::cleanupMultitouch_sys() { } +/*! \internal + Check the target widgets of the active touchpoints of the given \a device, + and choose the widget that is closest to any of the points. This widget + will then get all the touchpoints, even if it would not otherwise be the + target for some of them. + + \sa translateRawTouchEvent() +*/ QWidget *QApplicationPrivate::findClosestTouchPointTarget(const QPointingDevice *device, const QEventPoint &touchPoint) { const QPointF globalPos = touchPoint.globalPosition(); @@ -3754,7 +3762,10 @@ QWidget *QApplicationPrivate::findClosestTouchPointTarget(const QPointingDevice qreal dx = globalPos.x() - pt.globalPosition().x(); qreal dy = globalPos.y() - pt.globalPosition().y(); qreal distance = dx * dx + dy * dy; - if (closestTouchPointId == -1 || distance < closestDistance) { + // closestTouchPointId is -1 at the beginning. + // closestTouchPointId may be 0 if + // a synth-mouse eventPoint was found in activePoints: that's not relevant here. + if (closestTouchPointId <= 0 || distance < closestDistance) { closestTouchPointId = pt.id(); closestDistance = distance; closestTarget = QMutableEventPoint::target(pt); diff --git a/src/widgets/styles/qfusionstyle_p_p.h b/src/widgets/styles/qfusionstyle_p_p.h index dcb79f9e93c..821be49b2fa 100644 --- a/src/widgets/styles/qfusionstyle_p_p.h +++ b/src/widgets/styles/qfusionstyle_p_p.h @@ -94,7 +94,8 @@ public: QColor buttonColor = pal.button().color(); int val = qGray(buttonColor.rgb()); buttonColor = buttonColor.lighter(100 + qMax(1, (180 - val)/6)); - buttonColor.setHsv(buttonColor.hue(), buttonColor.saturation() * 0.75, buttonColor.value()); + buttonColor.setHsv(buttonColor.hue(), buttonColor.saturation() * 0.75, + buttonColor.value(), buttonColor.alpha()); return buttonColor; } diff --git a/tests/auto/corelib/kernel/qeventdispatcher/tst_qeventdispatcher.cpp b/tests/auto/corelib/kernel/qeventdispatcher/tst_qeventdispatcher.cpp index d25a6d60cca..cae462c526b 100644 --- a/tests/auto/corelib/kernel/qeventdispatcher/tst_qeventdispatcher.cpp +++ b/tests/auto/corelib/kernel/qeventdispatcher/tst_qeventdispatcher.cpp @@ -381,8 +381,17 @@ void tst_QEventDispatcher::postEventFromThread() QAtomicInt hadToQuit = false; QAtomicInt done = false; + int threadCount = 500; + const int timeout = (1000 +#if defined(QT_GUI_LIB) + // Aggressively posting events may on some platforms rate limit us to + // the display's refresh rate, so give us enough time if that happens. + + ((1000.0 / qGuiApp->primaryScreen()->refreshRate()) * threadCount) +#endif + ); + threadPool->start([&]{ - int loop = 1000 / 10; // give it a second + int loop = timeout / 10; while (!done && --loop) QThread::sleep(std::chrono::milliseconds{10}); if (done) @@ -399,8 +408,7 @@ void tst_QEventDispatcher::postEventFromThread() } } receiver; - int count = 500; - while (!hadToQuit && --count) { + while (!hadToQuit && --threadCount) { threadPool->start([&receiver]{ QCoreApplication::postEvent(&receiver, new QEvent(QEvent::User)); }); diff --git a/tests/auto/corelib/platform/android/tst_android.cpp b/tests/auto/corelib/platform/android/tst_android.cpp index b4bb0323f8a..93d3f1102c9 100644 --- a/tests/auto/corelib/platform/android/tst_android.cpp +++ b/tests/auto/corelib/platform/android/tst_android.cpp @@ -251,6 +251,12 @@ void tst_Android::safeAreaWithWindowFlagsAndStates() QFETCH(Qt::WindowStates, windowStates); QFETCH(Qt::WindowFlags, windowFlags); + if ((QNativeInterface::QAndroidApplication::sdkVersion() > __ANDROID_API_V__) && + qgetenv("QTEST_ENVIRONMENT").split(' ').contains("ci") & + (!(windowFlags & Qt::ExpandedClientAreaHint) && + !(windowStates & Qt::WindowFullScreen))) + QSKIP("Normal fails on Android 16 (QTBUG-140846)."); + QWidget widget; QPalette palette = widget.palette(); palette.setColor(QPalette::Window, Qt::red); @@ -338,6 +344,10 @@ void tst_Android::safeAreaWithWindowFlagsAndStates() // QTBUG-107604 void tst_Android::testFullScreenDimensions() { + if ((QNativeInterface::QAndroidApplication::sdkVersion() > __ANDROID_API_V__) && + qgetenv("QTEST_ENVIRONMENT").split(' ').contains("ci") ) + QSKIP("Keep on failing on Android 16 (QTBUG-141712)."); + QJniObject activity = QNativeInterface::QAndroidApplication::context(); QVERIFY(activity.isValid()); diff --git a/tests/auto/gui/kernel/qguieventdispatcher/BLACKLIST b/tests/auto/gui/kernel/qguieventdispatcher/BLACKLIST index a8f73d73f4d..e69de29bb2d 100644 --- a/tests/auto/gui/kernel/qguieventdispatcher/BLACKLIST +++ b/tests/auto/gui/kernel/qguieventdispatcher/BLACKLIST @@ -1,2 +0,0 @@ -[postEventFromThread] -macos-26 developer-build # QTBUG-142185 diff --git a/tests/auto/other/android/deployment_settings/CMakeLists.txt b/tests/auto/other/android/deployment_settings/CMakeLists.txt index d4546c0ca94..ed373493f97 100644 --- a/tests/auto/other/android/deployment_settings/CMakeLists.txt +++ b/tests/auto/other/android/deployment_settings/CMakeLists.txt @@ -33,11 +33,11 @@ set_target_properties(${target} PROPERTIES my_package_source_dir "path/to/source/dir" my_libs_property "some/path/to/lib2.so;some/path/to/lib3.so" my_plugins_property "some/path/to/plugin2.so;some/path/to/plugin3.so" - - QT_ANDROID_SDK_BUILD_TOOLS_REVISION "23.0.2" + # Below build tools should match to minimum supported OS API level version + QT_ANDROID_SDK_BUILD_TOOLS_REVISION "28.0.3" QT_ANDROID_MIN_SDK_VERSION "1" QT_ANDROID_TARGET_SDK_VERSION "2" - QT_ANDROID_COMPILE_SDK_VERSION "35" + QT_ANDROID_COMPILE_SDK_VERSION "36" QT_ANDROID_APP_NAME "Android Deployment Settings Test" QT_ANDROID_PACKAGE_NAME "org.qtproject.android_deployment_settings_test" QT_ANDROID_DEPLOYMENT_DEPENDENCIES "dep1.so;dep2.so;dep3.so" @@ -62,10 +62,11 @@ qt6_policy(SET QTP0002 OLD) set(target tst_android_deployment_settings_old) qt6_add_executable(${target} MANUAL_FINALIZATION EXCLUDE_FROM_ALL noop.cpp) set_target_properties(${target} PROPERTIES - QT_ANDROID_SDK_BUILD_TOOLS_REVISION "23.0.2" + # Below build tools should match to minimum supported OS API level version + QT_ANDROID_SDK_BUILD_TOOLS_REVISION "28.0.3" QT_ANDROID_MIN_SDK_VERSION "1" QT_ANDROID_TARGET_SDK_VERSION "2" - QT_ANDROID_COMPILE_SDK_VERSION "35" + QT_ANDROID_COMPILE_SDK_VERSION "36" QT_ANDROID_APP_NAME "Android Deployment Settings Test" QT_ANDROID_PACKAGE_NAME "org.qtproject.android_deployment_settings_test" QT_ANDROID_DEPLOYMENT_DEPENDENCIES "dep1.so;dep2.so;dep3.so" diff --git a/tests/auto/other/android/deployment_settings/tst_android_deployment_settings.cpp b/tests/auto/other/android/deployment_settings/tst_android_deployment_settings.cpp index 03724b7a225..6eed5506324 100644 --- a/tests/auto/other/android/deployment_settings/tst_android_deployment_settings.cpp +++ b/tests/auto/other/android/deployment_settings/tst_android_deployment_settings.cpp @@ -60,7 +60,7 @@ void tst_android_deployment_settings::DeploymentSettings_data() QTest::addColumn<QString>("value"); QTest::newRow("sdkBuildToolsRevision") << "sdkBuildToolsRevision" - << "23.0.2"; + << "28.0.3"; QTest::newRow("deployment-dependencies") << "deployment-dependencies" << "dep1.so,dep2.so,dep3.so"; QTest::newRow("android-extra-plugins") @@ -78,7 +78,7 @@ void tst_android_deployment_settings::DeploymentSettings_data() QTest::newRow("android-target-sdk-version") << "android-target-sdk-version" << "2"; QTest::newRow("android-compile-sdk-version") << "android-compile-sdk-version" - << "35"; + << "36"; QTest::newRow("android-package-name") << "android-package-name" << "org.qtproject.android_deployment_settings_test"; QTest::newRow("android-app-name") << "android-app-name" diff --git a/tests/auto/tools/rcc/data/deduplication/deduplication.expected b/tests/auto/tools/rcc/data/deduplication/deduplication.expected new file mode 100644 index 00000000000..bd873437b46 --- /dev/null +++ b/tests/auto/tools/rcc/data/deduplication/deduplication.expected @@ -0,0 +1,157 @@ +/**************************************************************************** +** Resource object code +** +IGNORE:** Created by: The Resource Compiler for Qt version 6.9.0 +** +** WARNING! All changes made in this file will be lost! +*****************************************************************************/ + +#ifdef _MSC_VER +// disable informational message "function ... selected for automatic inline expansion" +#pragma warning (disable: 4711) +#endif + +static const unsigned char qt_resource_data[] = { + // b.txt + 0x0,0x0,0x0,0xb, + 0x62, + 0x20,0x74,0x65,0x73,0x74,0x20,0x66,0x69,0x6c,0x65, + // c_with_a_content.txt + 0x0,0x0,0x0,0xb, + 0x61, + 0x20,0x74,0x65,0x73,0x74,0x20,0x66,0x69,0x6c,0x65, + // b.txt + 0x0,0x0,0x0,0xb, + 0x62, + 0x20,0x74,0x65,0x73,0x74,0x20,0x66,0x69,0x6c,0x65, + +}; + +static const unsigned char qt_resource_name[] = { + // files + 0x0,0x5, + 0x0,0x6d,0x2,0xc3, + 0x0,0x66, + 0x0,0x69,0x0,0x6c,0x0,0x65,0x0,0x73, + // b.txt + 0x0,0x5, + 0x0,0x65,0x5b,0xf4, + 0x0,0x62, + 0x0,0x2e,0x0,0x74,0x0,0x78,0x0,0x74, + // c_with_a_content.txt + 0x0,0x14, + 0x1,0x61,0x1d,0x34, + 0x0,0x63, + 0x0,0x5f,0x0,0x77,0x0,0x69,0x0,0x74,0x0,0x68,0x0,0x5f,0x0,0x61,0x0,0x5f,0x0,0x63,0x0,0x6f,0x0,0x6e,0x0,0x74,0x0,0x65,0x0,0x6e,0x0,0x74,0x0,0x2e, + 0x0,0x74,0x0,0x78,0x0,0x74, + // a.txt + 0x0,0x5, + 0x0,0x64,0x5b,0xf4, + 0x0,0x61, + 0x0,0x2e,0x0,0x74,0x0,0x78,0x0,0x74, + // alias_of_b_compress9.txt + 0x0,0x18, + 0xb,0x26,0xf,0xb4, + 0x0,0x61, + 0x0,0x6c,0x0,0x69,0x0,0x61,0x0,0x73,0x0,0x5f,0x0,0x6f,0x0,0x66,0x0,0x5f,0x0,0x62,0x0,0x5f,0x0,0x63,0x0,0x6f,0x0,0x6d,0x0,0x70,0x0,0x72,0x0,0x65, + 0x0,0x73,0x0,0x73,0x0,0x39,0x0,0x2e,0x0,0x74,0x0,0x78,0x0,0x74, + // alias_of_b.txt + 0x0,0xe, + 0x1,0xa4,0x6d,0x34, + 0x0,0x61, + 0x0,0x6c,0x0,0x69,0x0,0x61,0x0,0x73,0x0,0x5f,0x0,0x6f,0x0,0x66,0x0,0x5f,0x0,0x62,0x0,0x2e,0x0,0x74,0x0,0x78,0x0,0x74, + // alias_of_b_compress9_dupe.txt + 0x0,0x1d, + 0x9,0x4,0x7a,0x14, + 0x0,0x61, + 0x0,0x6c,0x0,0x69,0x0,0x61,0x0,0x73,0x0,0x5f,0x0,0x6f,0x0,0x66,0x0,0x5f,0x0,0x62,0x0,0x5f,0x0,0x63,0x0,0x6f,0x0,0x6d,0x0,0x70,0x0,0x72,0x0,0x65, + 0x0,0x73,0x0,0x73,0x0,0x39,0x0,0x5f,0x0,0x64,0x0,0x75,0x0,0x70,0x0,0x65,0x0,0x2e,0x0,0x74,0x0,0x78,0x0,0x74, + +}; + +static const unsigned char qt_resource_struct[] = { + // : + 0x0,0x0,0x0,0x0,0x0,0x2,0x0,0x0,0x0,0x1,0x0,0x0,0x0,0x1, +0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0, + // :/files + 0x0,0x0,0x0,0x0,0x0,0x2,0x0,0x0,0x0,0x6,0x0,0x0,0x0,0x2, +0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0, + // :/files/a.txt + 0x0,0x0,0x0,0x4e,0x0,0x0,0x0,0x0,0x0,0x1,0x0,0x0,0x0,0xf, +TIMESTAMP:files/a.txt + // :/files/b.txt + 0x0,0x0,0x0,0x10,0x0,0x0,0x0,0x0,0x0,0x1,0x0,0x0,0x0,0x0, +TIMESTAMP:files/b.txt + // :/files/c_with_a_content.txt + 0x0,0x0,0x0,0x20,0x0,0x0,0x0,0x0,0x0,0x1,0x0,0x0,0x0,0xf, +TIMESTAMP:files/c_with_a_content.txt + // :/files/alias_of_b.txt + 0x0,0x0,0x0,0x94,0x0,0x0,0x0,0x0,0x0,0x1,0x0,0x0,0x0,0x0, +TIMESTAMP:files/b.txt + // :/files/alias_of_b_compress9_dupe.txt + 0x0,0x0,0x0,0xb6,0x0,0x0,0x0,0x0,0x0,0x1,0x0,0x0,0x0,0x1e, +TIMESTAMP:files/b.txt + // :/files/alias_of_b_compress9.txt + 0x0,0x0,0x0,0x5e,0x0,0x0,0x0,0x0,0x0,0x1,0x0,0x0,0x0,0x1e, +TIMESTAMP:files/b.txt + +}; + +#ifdef QT_NAMESPACE +# define QT_RCC_PREPEND_NAMESPACE(name) ::QT_NAMESPACE::name +# define QT_RCC_MANGLE_NAMESPACE0(x) x +# define QT_RCC_MANGLE_NAMESPACE1(a, b) a##_##b +# define QT_RCC_MANGLE_NAMESPACE2(a, b) QT_RCC_MANGLE_NAMESPACE1(a,b) +# define QT_RCC_MANGLE_NAMESPACE(name) QT_RCC_MANGLE_NAMESPACE2( \ + QT_RCC_MANGLE_NAMESPACE0(name), QT_RCC_MANGLE_NAMESPACE0(QT_NAMESPACE)) +#else +# define QT_RCC_PREPEND_NAMESPACE(name) name +# define QT_RCC_MANGLE_NAMESPACE(name) name +#endif + +#if defined(QT_INLINE_NAMESPACE) +inline namespace QT_NAMESPACE { +#elif defined(QT_NAMESPACE) +namespace QT_NAMESPACE { +#endif + +bool qRegisterResourceData(int, const unsigned char *, const unsigned char *, const unsigned char *); +bool qUnregisterResourceData(int, const unsigned char *, const unsigned char *, const unsigned char *); + +#ifdef QT_NAMESPACE +} +#endif + +int QT_RCC_MANGLE_NAMESPACE(qInitResources)(); +int QT_RCC_MANGLE_NAMESPACE(qInitResources)() +{ + int version = 3; + QT_RCC_PREPEND_NAMESPACE(qRegisterResourceData) + (version, qt_resource_struct, qt_resource_name, qt_resource_data); + return 1; +} + +int QT_RCC_MANGLE_NAMESPACE(qCleanupResources)(); +int QT_RCC_MANGLE_NAMESPACE(qCleanupResources)() +{ + int version = 3; + QT_RCC_PREPEND_NAMESPACE(qUnregisterResourceData) + (version, qt_resource_struct, qt_resource_name, qt_resource_data); + return 1; +} + +#ifdef __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wexit-time-destructors" +#endif + +namespace { + struct initializer { + initializer() { QT_RCC_MANGLE_NAMESPACE(qInitResources)(); } + ~initializer() { QT_RCC_MANGLE_NAMESPACE(qCleanupResources)(); } + } dummy; +} + +#ifdef __clang__ +# pragma clang diagnostic pop +#endif diff --git a/tests/auto/tools/rcc/data/deduplication/deduplication.qrc b/tests/auto/tools/rcc/data/deduplication/deduplication.qrc new file mode 100644 index 00000000000..fd8a776503e --- /dev/null +++ b/tests/auto/tools/rcc/data/deduplication/deduplication.qrc @@ -0,0 +1,10 @@ +<!DOCTYPE RCC><RCC version="1.0"> +<qresource> + <file>files/a.txt</file> + <file>files/b.txt</file> + <file alias="files/alias_of_b.txt">files/b.txt</file> + <file>files/c_with_a_content.txt</file> + <file alias="files/alias_of_b_compress9.txt" compress="9">files/b.txt</file> + <file alias="files/alias_of_b_compress9_dupe.txt" compress="9">files/b.txt</file> +</qresource> +</RCC> diff --git a/tests/auto/tools/rcc/data/deduplication/files/a.txt b/tests/auto/tools/rcc/data/deduplication/files/a.txt new file mode 100644 index 00000000000..abd91bd4652 --- /dev/null +++ b/tests/auto/tools/rcc/data/deduplication/files/a.txt @@ -0,0 +1 @@ +a test file
\ No newline at end of file diff --git a/tests/auto/tools/rcc/data/deduplication/files/b.txt b/tests/auto/tools/rcc/data/deduplication/files/b.txt new file mode 100644 index 00000000000..01e4d76fc57 --- /dev/null +++ b/tests/auto/tools/rcc/data/deduplication/files/b.txt @@ -0,0 +1 @@ +b test file
\ No newline at end of file diff --git a/tests/auto/tools/rcc/data/deduplication/files/c_with_a_content.txt b/tests/auto/tools/rcc/data/deduplication/files/c_with_a_content.txt new file mode 100644 index 00000000000..abd91bd4652 --- /dev/null +++ b/tests/auto/tools/rcc/data/deduplication/files/c_with_a_content.txt @@ -0,0 +1 @@ +a test file
\ No newline at end of file diff --git a/tests/auto/tools/rcc/tst_rcc.cpp b/tests/auto/tools/rcc/tst_rcc.cpp index f5edfbcaa51..ba7e5841185 100644 --- a/tests/auto/tools/rcc/tst_rcc.cpp +++ b/tests/auto/tools/rcc/tst_rcc.cpp @@ -152,6 +152,11 @@ void tst_rcc::rcc_data() QTest::newRow("legal") << m_dataPath + QLatin1StringView("/legal") << "legal.qrc" << "rcc_legal.cpp"; + + if (sizeof(size_t) == 8) { + const QString deduplicationPath = m_dataPath + QLatin1String("/deduplication"); + QTest::newRow("deduplication") << deduplicationPath << "deduplication.qrc" << "deduplication.expected"; + } } static QStringList readLinesFromFile(const QString &fileName, diff --git a/tests/manual/wasm/localfiles/main.cpp b/tests/manual/wasm/localfiles/main.cpp index 862bff50a47..90e8c2e90f6 100644 --- a/tests/manual/wasm/localfiles/main.cpp +++ b/tests/manual/wasm/localfiles/main.cpp @@ -5,6 +5,81 @@ #include <emscripten/val.h> #include <emscripten.h> +class DropZone : public QLabel +{ + Q_OBJECT +public: + explicit DropZone(QWidget *parent = nullptr) : QLabel(parent) + { + setAcceptDrops(true); + setFrameStyle(QFrame::Box | QFrame::Sunken); + setAlignment(Qt::AlignCenter); + setText("Drop files here\n(will read first file)"); + setMinimumSize(400, 150); + setStyleSheet("QLabel { background-color: #f0f0f0; border: 2px dashed #999; padding: 20px; }"); + } + +Q_SIGNALS: + void filesDropped(const QList<QUrl> &urls); + +protected: + void dragEnterEvent(QDragEnterEvent *event) override + { + if (event->mimeData()->hasUrls()) { + event->acceptProposedAction(); + setStyleSheet("QLabel { background-color: #e0f0ff; border: 2px dashed #0066cc; padding: 20px; }"); + } + } + + void dragLeaveEvent(QDragLeaveEvent *event) override + { + Q_UNUSED(event); + setStyleSheet("QLabel { background-color: #f0f0f0; border: 2px dashed #999; padding: 20px; }"); + } + + void dropEvent(QDropEvent *event) override + { + const QMimeData *mimeData = event->mimeData(); + + if (mimeData->hasUrls()) { + QList<QUrl> urls = mimeData->urls(); + + qDebug() << "=== Files Dropped ==="; + qDebug() << "Number of files:" << urls.size(); + + for (int i = 0; i < urls.size(); ++i) { + const QUrl &url = urls.at(i); + qDebug() << "\n--- File" << (i + 1) << "---"; + qDebug() << "URL:" << url; + qDebug() << "URL toString:" << url.toString(); + qDebug() << "URL scheme:" << url.scheme(); + qDebug() << "URL path:" << url.path(); + qDebug() << "URL fileName:" << url.fileName(); + qDebug() << "isLocalFile:" << url.isLocalFile(); + + if (url.isLocalFile()) { + QString filePath = url.toLocalFile(); + qDebug() << "Local file path:" << filePath; + + QFileInfo fileInfo(filePath); + qDebug() << "File name:" << fileInfo.fileName(); + qDebug() << "File size:" << fileInfo.size(); + qDebug() << "File exists:" << fileInfo.exists(); + qDebug() << "Is readable:" << fileInfo.isReadable(); + qDebug() << "Absolute path:" << fileInfo.absoluteFilePath(); + qDebug() << "Last modified:" << fileInfo.lastModified().toString(); + } + } + qDebug() << "===================\n"; + + event->acceptProposedAction(); + emit filesDropped(urls); + } + + setStyleSheet("QLabel { background-color: #f0f0f0; border: 2px dashed #999; padding: 20px; }"); + } +}; + class AppWindow : public QObject { Q_OBJECT @@ -12,21 +87,39 @@ public: AppWindow() : m_layout(new QVBoxLayout(&m_loadFileUi)), m_window(emscripten::val::global("window")), m_showOpenFilePickerFunction(m_window["showOpenFilePicker"]), - m_showSaveFilePickerFunction(m_window["showSaveFilePicker"]) + m_showSaveFilePickerFunction(m_window["showSaveFilePicker"]), + m_fileDialog(new QFileDialog(&m_loadFileUi)), + m_isLoadOperation(true) { - addWidget<QLabel>("Filename filter"); - const bool localFileApiAvailable = !m_showOpenFilePickerFunction.isUndefined() && !m_showSaveFilePickerFunction.isUndefined(); m_useLocalFileApisCheckbox = addWidget<QCheckBox>("Use the window.showXFilePicker APIs"); m_useLocalFileApisCheckbox->setEnabled(localFileApiAvailable); m_useLocalFileApisCheckbox->setChecked(localFileApiAvailable); + connect(m_useLocalFileApisCheckbox, &QCheckBox::toggled, + std::bind(&AppWindow::onUseLocalFileApisCheckboxToggled, this)); + + m_useStandardFileDialogCheckbox = addWidget<QCheckBox>("Use standard QFileDialog API"); + connect(m_useStandardFileDialogCheckbox, &QCheckBox::toggled, + std::bind(&AppWindow::onUseStandardFileDialogCheckboxToggled, this)); + m_useStandardFileDialogCheckbox->setChecked(true); + + m_useExecModeCheckbox = addWidget<QCheckBox>("Use exec() instead of open()"); + m_useExecModeCheckbox->setChecked(false); + + addWidget<QLabel>("Filename filter"); - m_filterEdit = addWidget<QLineEdit>("Images (*.png *.jpg);;PDF (*.pdf);;*.txt"); + m_filterCombo = addWidget<QComboBox>(); + m_filterCombo->addItem("*"); + m_filterCombo->addItem("Images (*.png *.jpg);;PDF (*.pdf);;*.txt"); + m_filterCombo->setCurrentIndex(0); // Make "*" the default auto* loadFile = addWidget<QPushButton>("Load File"); + m_dropZone = addWidget<DropZone>(); + connect(m_dropZone, &DropZone::filesDropped, this, &AppWindow::onFilesDropped); + m_fileInfo = addWidget<QLabel>("Opened file:"); m_fileInfo->setTextInteractionFlags(Qt::TextSelectableByMouse); @@ -43,11 +136,13 @@ public: m_loadFileUi.setLayout(m_layout); - QObject::connect(m_useLocalFileApisCheckbox, &QCheckBox::toggled, std::bind(&AppWindow::onUseLocalFileApisCheckboxToggled, this)); QObject::connect(loadFile, &QPushButton::clicked, this, &AppWindow::onLoadClicked); - QObject::connect(m_saveFile, &QPushButton::clicked, std::bind(&AppWindow::onSaveClicked, this)); + + // Connect to both fileSelected and accepted signals for compatibility + QObject::connect(m_fileDialog, &QFileDialog::fileSelected, this, &AppWindow::onFileSelected); + QObject::connect(m_fileDialog, &QFileDialog::accepted, this, &AppWindow::onDialogAccepted); } void show() { @@ -67,6 +162,29 @@ private Q_SLOTS: m_showSaveFilePickerFunction : emscripten::val::undefined()); } + void onUseStandardFileDialogCheckboxToggled() + { + m_useLocalFileApisCheckbox->setChecked(m_useStandardFileDialogCheckbox->isChecked()); + } + + void onFilesDropped(const QList<QUrl> &urls) + { + if (urls.isEmpty()) + return; + + // Load the first dropped file + const QUrl &url = urls.first(); + + if (url.isLocalFile()) { + QString filePath = url.toLocalFile(); + loadFileWithQFile(filePath); + } else { + // Try using the URL string directly for non-file:// URLs (like weblocalfile://) + QString urlString = url.toString(); + loadFileWithQFile(urlString); + } + } + void onFileContentReady(const QString &fileName, const QByteArray &fileContents) { m_fileContent = fileContents; @@ -89,16 +207,108 @@ private Q_SLOTS: void onLoadClicked() { - QFileDialog::getOpenFileContent( - m_filterEdit->text(), - std::bind(&AppWindow::onFileContentReady, this, std::placeholders::_1, std::placeholders::_2), - &m_loadFileUi); + if (m_useStandardFileDialogCheckbox->isChecked()) { + m_isLoadOperation = true; + m_fileDialog->setFileMode(QFileDialog::ExistingFile); + m_fileDialog->setAcceptMode(QFileDialog::AcceptOpen); + m_fileDialog->setNameFilter(m_filterCombo->currentText()); + m_fileDialog->setWindowTitle("Open File"); + + if (m_useExecModeCheckbox->isChecked()) { + qDebug() << "Using exec() mode"; + int result = m_fileDialog->exec(); + if (result == QDialog::Accepted) { + QStringList files = m_fileDialog->selectedFiles(); + if (!files.isEmpty()) { + onFileSelected(files.first()); + } + } + } else { + qDebug() << "Using open() mode"; + m_fileDialog->open(); + } + } else { + QFileDialog::getOpenFileContent( + m_filterCombo->currentText(), + std::bind(&AppWindow::onFileContentReady, this, std::placeholders::_1, std::placeholders::_2), + &m_loadFileUi); + } } void onSaveClicked() { - m_fileInfo->setText("Saving file... (no result information with current API)"); - QFileDialog::saveFileContent(m_fileContent, m_savedFileNameEdit->text()); + if (m_useStandardFileDialogCheckbox->isChecked()) { + m_isLoadOperation = false; + m_fileDialog->setFileMode(QFileDialog::AnyFile); + m_fileDialog->setAcceptMode(QFileDialog::AcceptSave); + m_fileDialog->setNameFilter(m_filterCombo->currentText()); + m_fileDialog->setWindowTitle("Save File"); + m_fileDialog->selectFile(m_savedFileNameEdit->text()); + + if (m_useExecModeCheckbox->isChecked()) { + qDebug() << "Using exec() mode for save"; + int result = m_fileDialog->exec(); + if (result == QDialog::Accepted) { + QStringList files = m_fileDialog->selectedFiles(); + if (!files.isEmpty()) { + onFileSelected(files.first()); + } + } + } else { + qDebug() << "Using open() mode for save"; + m_fileDialog->open(); + } + } else { + m_fileInfo->setText("Saving file... (no result information with current API)"); + QFileDialog::saveFileContent(m_fileContent, m_savedFileNameEdit->text()); + } + } + + void onDialogAccepted() + { + QStringList files = m_fileDialog->selectedFiles(); + if (!files.isEmpty()) { + onFileSelected(files.first()); + } + } + + void onFileSelected(const QString &fileName) + { + qDebug() << "onFileSelected" << fileName; + + if (m_isLoadOperation) { + loadFileWithQFile(fileName); + } else { + saveFileWithQFile(fileName); + } + } + + void loadFileWithQFile(const QString &fileName) + { + qDebug() << "loadFileWithQFile" << fileName; + + QFile file(fileName); + if (file.open(QIODevice::ReadOnly)) { + qDebug() << "loadFileWithQFile" << fileName; + QByteArray fileContents = file.readAll(); + file.close(); + onFileContentReady(QFileInfo(fileName).fileName(), fileContents); + } else { + m_fileInfo->setText(QString("Failed to open file: %1").arg(file.errorString())); + } + } + + void saveFileWithQFile(const QString &fileName) + { + QFile file(fileName); + if (file.open(QIODevice::WriteOnly)) { + qint64 bytesWritten = file.write(m_fileContent); + file.close(); + bool success = (bytesWritten == m_fileContent.size()); + m_fileInfo->setText(QString("File save result: %1").arg(success ? "success" : "failed")); + } else { + m_fileInfo->setText(QString("Failed to save file: %1").arg(file.errorString())); + } } private: @@ -113,7 +323,10 @@ private: QWidget m_loadFileUi; QCheckBox* m_useLocalFileApisCheckbox; - QLineEdit* m_filterEdit; + QCheckBox* m_useStandardFileDialogCheckbox; + QCheckBox* m_useExecModeCheckbox; + DropZone* m_dropZone; + QComboBox* m_filterCombo; QVBoxLayout *m_layout; QLabel* m_fileInfo; QLabel* m_fileHash; @@ -124,6 +337,9 @@ private: emscripten::val m_showOpenFilePickerFunction; emscripten::val m_showSaveFilePickerFunction; + QFileDialog* m_fileDialog; + bool m_isLoadOperation; + QByteArray m_fileContent; }; |
