diff options
80 files changed, 1883 insertions, 720 deletions
diff --git a/cmake/QtAndroidHelpers.cmake b/cmake/QtAndroidHelpers.cmake index cb7d4716f12..b473c2c331b 100644 --- a/cmake/QtAndroidHelpers.cmake +++ b/cmake/QtAndroidHelpers.cmake @@ -191,21 +191,8 @@ function(qt_internal_android_dependencies_content target file_content_out) endif() # Android Permissions - if(arg_PERMISSIONS) - foreach(permission IN LISTS arg_PERMISSIONS) - # Check if the permission has also extra attributes in addition to the permission name - list(LENGTH permission permission_len) - if(permission_len EQUAL 1) - string(APPEND file_contents "<permission name=\"${permission}\" />\n") - elseif(permission_len EQUAL 2) - list(GET permission 0 name) - list(GET permission 1 extras) - string(APPEND file_contents "<permission name=\"${name}\" extras=\"${extras}\"/>\n") - else() - message(FATAL_ERROR "Invalid permission format: ${permission} ${permission_len}") - endif() - endforeach() - endif() + _qt_internal_android_convert_permissions(permissions_string ${target} "DEPENDENCIESXML") + string(APPEND file_contents "${permissions_string}") # Android Features if(arg_FEATURES) @@ -482,14 +469,8 @@ function(qt_internal_android_add_interface_permissions target) return() endif() - set(postprocessed_permissions "") - foreach(permission IN LISTS permissions) - # TODO: skip processing extras for now, add them back once internal API - # will cover adding extras using internal function. - list(APPEND postprocessed_permissions "name\;${permission}") - endforeach() qt_internal_set_module_transitive_properties(${target} TYPE LINK PROPERTIES - INTERFACE_QT_ANDROID_PERMISSIONS "${postprocessed_permissions}") + INTERFACE_QT_ANDROID_PERMISSIONS "${permissions}") endfunction() # The function stores Android features that are required by the module target. diff --git a/cmake/QtBaseGlobalTargets.cmake b/cmake/QtBaseGlobalTargets.cmake index 405b5e22635..601592277fe 100644 --- a/cmake/QtBaseGlobalTargets.cmake +++ b/cmake/QtBaseGlobalTargets.cmake @@ -437,21 +437,6 @@ elseif(WASM) "${QT_BUILD_DIR}/${INSTALL_LIBEXECDIR}/qt-wasmtestrunner.py" @ONLY) qt_install(PROGRAMS "${QT_BUILD_DIR}/${INSTALL_LIBEXECDIR}/qt-wasmtestrunner.py" DESTINATION "${INSTALL_LIBEXECDIR}") - - if(QT_FEATURE_shared) - configure_file("${CMAKE_CURRENT_SOURCE_DIR}/util/wasm/preload/preload_qml_imports.py" - "${QT_BUILD_DIR}/${INSTALL_LIBEXECDIR}/preload_qml_imports.py" COPYONLY) - configure_file("${CMAKE_CURRENT_SOURCE_DIR}/util/wasm/preload/preload_qt_plugins.py" - "${QT_BUILD_DIR}/${INSTALL_LIBEXECDIR}/preload_qt_plugins.py" COPYONLY) - configure_file("${CMAKE_CURRENT_SOURCE_DIR}/util/wasm/preload/generate_default_preloads.sh.in" - "${QT_BUILD_DIR}/${INSTALL_LIBEXECDIR}/generate_default_preloads.sh.in" COPYONLY) - qt_install(PROGRAMS "${QT_BUILD_DIR}/${INSTALL_LIBEXECDIR}/preload_qml_imports.py" - DESTINATION "${INSTALL_LIBEXECDIR}") - qt_install(PROGRAMS "${QT_BUILD_DIR}/${INSTALL_LIBEXECDIR}/preload_qt_plugins.py" - DESTINATION "${INSTALL_LIBEXECDIR}") - qt_install(PROGRAMS "${QT_BUILD_DIR}/${INSTALL_LIBEXECDIR}/generate_default_preloads.sh.in" - DESTINATION "${INSTALL_LIBEXECDIR}") - endif() endif() # Install CI support files to libexec. diff --git a/cmake/QtBuildHelpers.cmake b/cmake/QtBuildHelpers.cmake index 1fb5c6350e1..df0aa33cda2 100644 --- a/cmake/QtBuildHelpers.cmake +++ b/cmake/QtBuildHelpers.cmake @@ -487,7 +487,6 @@ macro(qt_internal_setup_build_and_global_variables) qt_set_language_standards() qt_internal_set_use_ccache() - qt_internal_set_unity_build() qt_internal_set_allow_symlink_in_paths() qt_internal_set_skip_setup_deployment() qt_internal_set_qt_allow_download() diff --git a/cmake/QtBuildOptionsHelpers.cmake b/cmake/QtBuildOptionsHelpers.cmake index ccd054183d4..ae8cf51e016 100644 --- a/cmake/QtBuildOptionsHelpers.cmake +++ b/cmake/QtBuildOptionsHelpers.cmake @@ -444,10 +444,19 @@ endmacro() macro(qt_internal_set_unity_build) option(QT_UNITY_BUILD "Enable unity (jumbo) build") set(QT_UNITY_BUILD_BATCH_SIZE "32" CACHE STRING "Unity build batch size") - if(QT_UNITY_BUILD) - set(CMAKE_UNITY_BUILD ON) - set(CMAKE_UNITY_BUILD_BATCH_SIZE "${QT_UNITY_BUILD_BATCH_SIZE}") - endif() + + include(CMakeDependentOption) + string(TOLOWER "${PROJECT_NAME}" project_name_lower) + cmake_dependent_option( + "QT_UNITY_BUILD_PROJECT_${project_name_lower}" + "Enable unity builds for project ${PROJECT_NAME}" + TRUE + QT_UNITY_BUILD + FALSE + ) + + set(CMAKE_UNITY_BUILD "${QT_UNITY_BUILD_PROJECT_${project_name_lower}}") + set(CMAKE_UNITY_BUILD_BATCH_SIZE "${QT_UNITY_BUILD_BATCH_SIZE}") endmacro() macro(qt_internal_set_allow_symlink_in_paths) diff --git a/cmake/QtBuildRepoHelpers.cmake b/cmake/QtBuildRepoHelpers.cmake index 86107179475..70a066f11b2 100644 --- a/cmake/QtBuildRepoHelpers.cmake +++ b/cmake/QtBuildRepoHelpers.cmake @@ -355,6 +355,7 @@ macro(qt_build_repo_begin) endif() _qt_internal_sbom_auto_begin_qt_repo_project() + qt_internal_set_unity_build() endmacro() # Runs delayed actions on some of the Qt targets. diff --git a/coin/instructions/prepare_building_env.yaml b/coin/instructions/prepare_building_env.yaml index 0330369662d..ac2ad89027b 100644 --- a/coin/instructions/prepare_building_env.yaml +++ b/coin/instructions/prepare_building_env.yaml @@ -679,6 +679,9 @@ instructions: - type: AppendToEnvironmentVariable variableName: COMMON_NON_QTBASE_TARGET_CMAKE_ARGS variableValue: " -DQT_USE_VCPKG=ON -DVCPKG_INSTALLED_DIR={{.Env.VCPKG_INSTALLED_DIR}} -DVCPKG_HOST_TRIPLET={{.Env.VCPKG_HOST_TRIPLET}} -DVCPKG_TARGET_TRIPLET={{.Env.VCPKG_TARGET_TRIPLET}}" + - type: AppendToEnvironmentVariable + variableName: COMMON_TARGET_TEST_CMAKE_ARGS + variableValue: " -DQT_USE_VCPKG=ON -DVCPKG_INSTALLED_DIR={{.Env.VCPKG_INSTALLED_DIR}} -DVCPKG_HOST_TRIPLET={{.Env.VCPKG_HOST_TRIPLET}} -DVCPKG_TARGET_TRIPLET={{.Env.VCPKG_TARGET_TRIPLET}}" enable_if: condition: runtime env_var: USE_VCPKG diff --git a/configure.cmake b/configure.cmake index 8d96ba7e1f0..ddadf08ed57 100644 --- a/configure.cmake +++ b/configure.cmake @@ -1010,6 +1010,7 @@ qt_feature("wasm-jspi" PUBLIC LABEL "WebAssembly JSPI" PURPOSE "Enables WebAssembly JavaScript Promise Integration (JSPI)" AUTODETECT OFF + CONDITION QT_FEATURE_wasm_exceptions # JSPI requires wasm-exceptions ) qt_feature_definition("wasm-jspi" "QT_WASM_JSPI" VALUE "1") qt_feature_config("wasm-jspi" QMAKE_PRIVATE_CONFIG) diff --git a/src/corelib/CMakeLists.txt b/src/corelib/CMakeLists.txt index 249b9b8c1ee..cd3c03666d0 100644 --- a/src/corelib/CMakeLists.txt +++ b/src/corelib/CMakeLists.txt @@ -491,8 +491,11 @@ if(ANDROID) set_property(TARGET Core APPEND PROPERTY QT_ANDROID_LIB_DEPENDENCIES ${INSTALL_PLUGINSDIR}/platforms/libplugins_platforms_qtforandroid.so ) - set_property(TARGET Core APPEND PROPERTY QT_ANDROID_PERMISSIONS - android.permission.INTERNET android.permission.WRITE_EXTERNAL_STORAGE + qt_internal_add_android_permission(Core + NAME android.permission.WRITE_EXTERNAL_STORAGE + ) + qt_internal_add_android_permission(Core + NAME android.permission.INTERNET ) endif() diff --git a/src/corelib/Qt6AndroidMacros.cmake b/src/corelib/Qt6AndroidMacros.cmake index ed97a42c83f..e34f38a4d2b 100644 --- a/src/corelib/Qt6AndroidMacros.cmake +++ b/src/corelib/Qt6AndroidMacros.cmake @@ -1748,7 +1748,11 @@ endfunction() function(_qt_internal_android_get_target_deployment_dir out_deploy_dir target) _qt_internal_android_get_target_android_build_dir(build_dir ${target}) - set(${out_deploy_dir} "${build_dir}/app" PARENT_SCOPE) + if(QT_USE_ANDROID_MODERN_BUNDLE) + set(${out_deploy_dir} "${build_dir}/app" PARENT_SCOPE) + else() + set(${out_deploy_dir} "${build_dir}" PARENT_SCOPE) + endif() endfunction() function(_qt_internal_expose_android_package_source_dir_to_ide target) diff --git a/src/corelib/Qt6AndroidPermissionHelpers.cmake b/src/corelib/Qt6AndroidPermissionHelpers.cmake index 7f851e14667..4e17a5ca83a 100644 --- a/src/corelib/Qt6AndroidPermissionHelpers.cmake +++ b/src/corelib/Qt6AndroidPermissionHelpers.cmake @@ -25,7 +25,47 @@ # # `XML` # Generate XML content compatible with AndroidManifest.xml. +# +# `DEPENDENCIESXML` +# Generate XML content compatible with <module>-android-dependencies.xml. +# This format doesn't produce generator expression, so it should be used in +# the scope finalizer. function(_qt_internal_android_convert_permissions out_var target type) + if(type STREQUAL "DEPENDENCIESXML") + set(output "") + get_target_property(permissions ${target} QT_ANDROID_PERMISSIONS) + if(NOT permissions) + return() + endif() + foreach(permission IN LISTS permissions) + list(JOIN permission "=\"" permission) + list(LENGTH permission permission_length) + if(permission_length LESS 1) + message(FATAL_ERROR "Invalid QT_ANDROID_PERMISSIONS format for target" + " ${target}: ${arg_PERMISSIONS}") + endif() + + list(GET permission 0 permission_name) + string(APPEND output "<permission ${permission_name}\"") + + math(EXPR permission_length "${permission_length} - 1") + set(extras_string "") + if(permission_length GREATER_EQUAL 1) + set(attributes "") + foreach(i RANGE 1 ${permission_length}) + list(GET permission ${i} permission_attribute) + list(APPEND attributes "android:${permission_attribute}'") + endforeach() + list(JOIN attributes " " attributes) + string(REPLACE "\"" "'" attributes "${attributes}") + set(extras_string " extras=\"${attributes}\"") + endif() + string(APPEND output "${extras_string}/>\n") + endforeach() + set(${out_var} "${output}" PARENT_SCOPE) + return() + endif() + set(permissions_property "$<TARGET_PROPERTY:${target},QT_ANDROID_PERMISSIONS>") set(permissions_genex "$<$<BOOL:${permissions_property}>:") if(type STREQUAL "JSON") diff --git a/src/corelib/Qt6CTestMacros.cmake b/src/corelib/Qt6CTestMacros.cmake index 939901f5e20..c115e39b272 100644 --- a/src/corelib/Qt6CTestMacros.cmake +++ b/src/corelib/Qt6CTestMacros.cmake @@ -307,6 +307,7 @@ macro(_qt_internal_test_expect_pass _dir) GENERATOR MAKE_PROGRAM BUILD_TYPE + BUILD_TARGET ) set(_test_multi_args BUILD_OPTIONS @@ -364,6 +365,12 @@ macro(_qt_internal_test_expect_pass _dir) set(build_type "--build-config" "${build_type}") endif() + if(_ARGS_BUILD_TARGET) + set(build_target "--build-target" "${_ARGS_BUILD_TARGET}") + else() + set(build_target "") + endif() + # Allow skipping clean step. set(build_no_clean "") if(_ARGS_NO_CLEAN_STEP) @@ -468,6 +475,7 @@ macro(_qt_internal_test_expect_pass _dir) ${build_project} --build-options "${option_list}" "${_ARGS_BUILD_OPTIONS}" ${additional_configure_args} + ${build_target} ${test_command} ) diff --git a/src/corelib/Qt6WasmMacros.cmake b/src/corelib/Qt6WasmMacros.cmake index e185ef67490..eae356679bd 100644 --- a/src/corelib/Qt6WasmMacros.cmake +++ b/src/corelib/Qt6WasmMacros.cmake @@ -91,15 +91,6 @@ function(_qt_internal_wasm_add_target_helpers target) ${_target_directory}/qtloader.js COPYONLY) configure_file("${WASM_BUILD_DIR}/plugins/platforms/qtlogo.svg" ${_target_directory}/qtlogo.svg COPYONLY) - if(QT_FEATURE_shared) - set(TARGET_DIR "${_target_directory}") - set(SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}") - set(QT_HOST_DIR "${QT_HOST_PATH}") - set(QT_WASM_DIR "${WASM_BUILD_DIR}") - set(QT_INSTALL_DIR "${QT6_INSTALL_PREFIX}") - configure_file("${WASM_BUILD_DIR}/libexec/generate_default_preloads.sh.in" - "${_target_directory}/generate_default_preloads_for_${target}.sh" @ONLY) - endif() endif() endif() endif() diff --git a/src/corelib/compat/removed_api.cpp b/src/corelib/compat/removed_api.cpp index 4285e39a542..94879edcf72 100644 --- a/src/corelib/compat/removed_api.cpp +++ b/src/corelib/compat/removed_api.cpp @@ -1474,6 +1474,8 @@ bool QObject::doSetProperty(const char *name, const QVariant *lvalue, QVariant * #include "qstring.h" // inlined API +#include "qvariant.h" // inlined API + // #include "qotherheader.h" // // implement removed functions from qotherheader.h // order sections alphabetically to reduce chances of merge conflicts diff --git a/src/corelib/global/qcheckedint_impl.h b/src/corelib/global/qcheckedint_impl.h index 2b4d0ff0e0d..f4081da8bb0 100644 --- a/src/corelib/global/qcheckedint_impl.h +++ b/src/corelib/global/qcheckedint_impl.h @@ -302,14 +302,14 @@ public: QCheckedInt, AInt, template <typename AInt, if_is_same_int<AInt> = true>) -}; -template <typename Int, typename I, typename P> -Q_DECL_CONST_FUNCTION size_t constexpr inline qHash(QCheckedInt<Int, I, P> key, size_t seed = 0) noexcept -{ - using QT_PREPEND_NAMESPACE(qHash); - return qHash(key.value(), seed); -} +private: + friend size_t constexpr qHash(QCheckedInt key, size_t seed = 0) noexcept + { + using QT_PREPEND_NAMESPACE(qHash); // ### needed? + return qHash(key.value(), seed); + } +}; } // namespace QCheckedIntegers } // namespace QtPrivate diff --git a/src/corelib/kernel/qabstracteventdispatcher.cpp b/src/corelib/kernel/qabstracteventdispatcher.cpp index 997d6f0c98e..fde406a78d8 100644 --- a/src/corelib/kernel/qabstracteventdispatcher.cpp +++ b/src/corelib/kernel/qabstracteventdispatcher.cpp @@ -176,8 +176,9 @@ QAbstractEventDispatcher::QAbstractEventDispatcher(QAbstractEventDispatcherPriva */ QAbstractEventDispatcher::~QAbstractEventDispatcher() { - QThreadData *data = QThreadData::current(); - if (data->eventDispatcher.loadRelaxed() == this) + // don't recreate the QThreadData if it has already been destroyed + QThreadData *data = QThreadData::currentThreadData(); + if (data && data->eventDispatcher.loadRelaxed() == this) data->eventDispatcher.storeRelaxed(nullptr); } @@ -192,6 +193,7 @@ QAbstractEventDispatcher::~QAbstractEventDispatcher() */ QAbstractEventDispatcher *QAbstractEventDispatcher::instance(QThread *thread) { + // do create a QThreadData, in case this is very early in an adopted thread QThreadData *data = thread ? QThreadData::get2(thread) : QThreadData::current(); return data->eventDispatcher.loadRelaxed(); } diff --git a/src/corelib/kernel/qbasictimer.cpp b/src/corelib/kernel/qbasictimer.cpp index 60387381da6..1906457b13d 100644 --- a/src/corelib/kernel/qbasictimer.cpp +++ b/src/corelib/kernel/qbasictimer.cpp @@ -5,6 +5,8 @@ #include "qabstracteventdispatcher.h" #include "qabstracteventdispatcher_p.h" +#include <private/qthread_p.h> + using namespace std::chrono_literals; QT_BEGIN_NAMESPACE @@ -209,7 +211,12 @@ void QBasicTimer::start(Duration duration, Qt::TimerType timerType, QObject *obj void QBasicTimer::stop() { if (isActive()) { - QAbstractEventDispatcher *eventDispatcher = QAbstractEventDispatcher::instance(); + QAbstractEventDispatcher *eventDispatcher = nullptr; + + // don't create the current thread data if it's already been destroyed + if (QThreadData *data = QThreadData::currentThreadData()) + eventDispatcher = data->eventDispatcher.loadRelaxed(); + if (eventDispatcher && !eventDispatcher->unregisterTimer(m_id)) { qWarning("QBasicTimer::stop: Failed. Possibly trying to stop from a different thread"); return; diff --git a/src/corelib/kernel/qtestsupport_core.cpp b/src/corelib/kernel/qtestsupport_core.cpp index 0d11f0bd666..c0c24d9db83 100644 --- a/src/corelib/kernel/qtestsupport_core.cpp +++ b/src/corelib/kernel/qtestsupport_core.cpp @@ -7,8 +7,37 @@ using namespace std::chrono_literals; +// Assert that this instantiation of std::atomic is always lock-free so we +// know that no code will execute on destruction. +static_assert(std::atomic<std::chrono::milliseconds>::is_always_lock_free); + QT_BEGIN_NAMESPACE +// ### Qt 7: reduce the default: QTBUG-138160 +Q_CONSTINIT std::atomic<std::chrono::milliseconds> QTest::defaultTryTimeout{5s}; + +/*! + \variable QTest::defaultTryTimeout + \since 6.11 + + This global variable stores the default timeout used by the \c {QTRY_*} + functions and \l qWait. + + The most typical use case for this variable is to modify the timeout + for an entire test: + + \snippet code/src_qtestlib_qtestcase.cpp set defaultTryTimeout + + However, you can also set it for a specific scope, using + \l QAtomicScopedValueRollback: + + \snippet code/src_qtestlib_qtestcase.cpp rollback defaultTryTimeout + + To access the value, call \c load(): + + \snippet code/src_qtestlib_qtestcase.cpp get defaultTryTimeout +*/ + /*! \overload diff --git a/src/corelib/kernel/qtestsupport_core.h b/src/corelib/kernel/qtestsupport_core.h index bf43c3a814a..6613f610b78 100644 --- a/src/corelib/kernel/qtestsupport_core.h +++ b/src/corelib/kernel/qtestsupport_core.h @@ -16,15 +16,12 @@ namespace QTest { Q_CORE_EXPORT void qSleep(int ms); Q_CORE_EXPORT void qSleep(std::chrono::milliseconds msecs); -namespace Internal -{ -static inline constexpr std::chrono::milliseconds defaultTryTimeout - = std::chrono::milliseconds(5000); -} // namespace Internal +extern Q_CORE_EXPORT std::atomic<std::chrono::milliseconds> defaultTryTimeout; template <typename Functor> [[nodiscard]] bool -qWaitFor(Functor predicate, QDeadlineTimer deadline = QDeadlineTimer(Internal::defaultTryTimeout)) +qWaitFor(Functor predicate, QDeadlineTimer deadline = QDeadlineTimer( + defaultTryTimeout.load(std::memory_order_relaxed))) { // We should not spin the event loop in case the predicate is already true, // otherwise we might send new events that invalidate the predicate. diff --git a/src/corelib/kernel/qvariant.cpp b/src/corelib/kernel/qvariant.cpp index e91797a3ffe..34ea987c3a0 100644 --- a/src/corelib/kernel/qvariant.cpp +++ b/src/corelib/kernel/qvariant.cpp @@ -1030,14 +1030,11 @@ QVariant::QVariant(const QPersistentModelIndex &modelIndex) : d(std::piecewise_c */ /*! + \fn QMetaType QVariant::metaType() const \since 6.0 Returns the QMetaType of the value stored in the variant. */ -QMetaType QVariant::metaType() const -{ - return d.type(); -} /*! Assigns the value of the variant \a variant to this variant. @@ -1085,15 +1082,13 @@ void QVariant::detach() */ /*! + \fn const char *QVariant::typeName() const + Returns the name of the type stored in the variant. The returned strings describe the C++ datatype used to store the data: for example, "QFont", "QString", or "QVariantList". An Invalid variant returns 0. */ -const char *QVariant::typeName() const -{ - return d.type().name(); -} /*! Convert this variant to type QMetaType::UnknownType and free up any resources diff --git a/src/corelib/kernel/qvariant.h b/src/corelib/kernel/qvariant.h index e9d3ffb9ae3..32250ab1e15 100644 --- a/src/corelib/kernel/qvariant.h +++ b/src/corelib/kernel/qvariant.h @@ -343,7 +343,9 @@ public: return id; } + QT_CORE_INLINE_SINCE(6, 10) const char *typeName() const; + QT_CORE_INLINE_SINCE(6, 10) QMetaType metaType() const; bool canConvert(QMetaType targetType) const @@ -740,6 +742,18 @@ QT_WARNING_POP #endif +#if QT_CORE_INLINE_IMPL_SINCE(6, 10) +QMetaType QVariant::metaType() const +{ + return d.type(); +} + +const char *QVariant::typeName() const +{ + return d.type().name(); +} +#endif + inline bool QVariant::isDetached() const { return !d.is_shared || d.data.shared->ref.loadRelaxed() == 1; } diff --git a/src/corelib/serialization/qtextstream.cpp b/src/corelib/serialization/qtextstream.cpp index 8226be3d219..9e6ed1dad95 100644 --- a/src/corelib/serialization/qtextstream.cpp +++ b/src/corelib/serialization/qtextstream.cpp @@ -830,10 +830,29 @@ QTextStreamPrivate::PaddingResult QTextStreamPrivate::padding(qsizetype len) con return { left, right }; } +namespace { +template <typename StringView> +auto parseSign(StringView data, const QLocale &loc) +{ + struct R { + StringView sign, rest; + explicit operator bool() const noexcept { return !sign.isEmpty(); } + }; + // This assumes that the size in UTF-16 (return value of QLocale functions) + // and StringView is the same; in particular, it doesn't work for UTF-8! + if (const QString sign = loc.negativeSign(); data.startsWith(sign)) + return R{data.first(sign.size()), data.sliced(sign.size())}; + if (const QString sign = loc.positiveSign(); data.startsWith(sign)) + return R{data.first(sign.size()), data.sliced(sign.size())}; + return R{nullptr, data}; +} +} // unnamed namespace + /*! \internal */ -void QTextStreamPrivate::putString(QStringView data, bool number) +template <typename StringView> +void QTextStreamPrivate::putStringImpl(StringView data, bool number) { if (Q_UNLIKELY(params.fieldWidth > data.size())) { @@ -842,11 +861,10 @@ void QTextStreamPrivate::putString(QStringView data, bool number) const PaddingResult pad = padding(data.size()); if (params.fieldAlignment == QTextStream::AlignAccountingStyle && number) { - const QChar sign = data.size() > 0 ? data.front() : QChar(); - if (sign == locale.negativeSign() || sign == locale.positiveSign()) { + if (const auto r = parseSign(data, locale)) { // write the sign before the padding, then skip it later - write(sign); - data = data.sliced(1); + write(r.sign); + data = r.rest; } } @@ -863,29 +881,20 @@ void QTextStreamPrivate::putString(QStringView data, bool number) */ void QTextStreamPrivate::putString(QLatin1StringView data, bool number) { - if (Q_UNLIKELY(params.fieldWidth > data.size())) { - - // handle padding - - const PaddingResult pad = padding(data.size()); - - if (params.fieldAlignment == QTextStream::AlignAccountingStyle && number) { - const QChar sign = data.size() > 0 ? QLatin1Char(*data.data()) : QChar(); - if (sign == locale.negativeSign() || sign == locale.positiveSign()) { - // write the sign before the padding, then skip it later - write(sign); - data = QLatin1StringView(data.data() + 1, data.size() - 1); - } - } + putStringImpl(data, number); +} - writePadding(pad.left); - write(data); - writePadding(pad.right); - } else { - write(data); - } +/*! + \internal +*/ +void QTextStreamPrivate::putString(QStringView data, bool number) +{ + putStringImpl(data, number); } +/*! + \internal +*/ void QTextStreamPrivate::putString(QUtf8StringView data, bool number) { putString(data.toString(), number); diff --git a/src/corelib/serialization/qtextstream_p.h b/src/corelib/serialization/qtextstream_p.h index 6d6bfcd8365..2da7fe8e9d5 100644 --- a/src/corelib/serialization/qtextstream_p.h +++ b/src/corelib/serialization/qtextstream_p.h @@ -166,6 +166,10 @@ public: bool fillReadBuffer(qint64 maxBytes = -1); void resetReadBuffer(); void flushWriteBuffer(); + +private: + template <typename StringView> + void putStringImpl(StringView view, bool number); }; QT_END_NAMESPACE diff --git a/src/corelib/text/qstring.cpp b/src/corelib/text/qstring.cpp index 9d0631f832d..d506662fba7 100644 --- a/src/corelib/text/qstring.cpp +++ b/src/corelib/text/qstring.cpp @@ -8968,7 +8968,10 @@ QString QString::arg_impl(qulonglong a, int fieldWidth, int base, QChar fillChar \snippet code/src_corelib_text_qstring.cpp 2 \note In Qt versions prior to 6.9, this function was a regular function - taking \c double. + taking \c double. As a consequence of being a template function now, it no + longer accepts arguments that merely implicitly convert to floating-point + types. A backwards-compatible fix is to cast such types to one of the C++ + floating-point types. \sa QLocale::toString(), QLocale::FloatingPointPrecisionOption, {Number Formats} */ diff --git a/src/corelib/thread/qthread_p.h b/src/corelib/thread/qthread_p.h index b34336d7e16..c23a50d158e 100644 --- a/src/corelib/thread/qthread_p.h +++ b/src/corelib/thread/qthread_p.h @@ -374,6 +374,8 @@ public: bool requiresCoreApplication = true; private: + friend class QAbstractEventDispatcher; + friend class QBasicTimer; static Q_AUTOTEST_EXPORT QThreadData *currentThreadData() noexcept Q_DECL_PURE_FUNCTION; static Q_AUTOTEST_EXPORT QThreadData *createCurrentThreadData(); }; diff --git a/src/dbus/qdbuspendingcall.h b/src/dbus/qdbuspendingcall.h index 1907fab78b9..a78ce76d548 100644 --- a/src/dbus/qdbuspendingcall.h +++ b/src/dbus/qdbuspendingcall.h @@ -12,6 +12,8 @@ #ifndef QT_NO_DBUS +class tst_QDBusPendingReply; + QT_BEGIN_NAMESPACE @@ -58,6 +60,8 @@ protected: private: QDBusPendingCall(); // not defined + + friend class ::tst_QDBusPendingReply; }; Q_DECLARE_SHARED(QDBusPendingCall) diff --git a/src/dbus/qdbuspendingreply.cpp b/src/dbus/qdbuspendingreply.cpp index b0fb2d75af5..c6523d915fd 100644 --- a/src/dbus/qdbuspendingreply.cpp +++ b/src/dbus/qdbuspendingreply.cpp @@ -76,6 +76,13 @@ */ /*! + \fn template<typename... Types> QDBusPendingReply<Types...>::QDBusPendingReply(QDBusPendingReply &&other) + \since 6.10 + + Moves-constructs a new QDBusPendingReply from \a other. +*/ + +/*! \fn template<typename... Types> QDBusPendingReply<Types...>::QDBusPendingReply(const QDBusPendingCall &call) Creates a QDBusPendingReply object that will take its contents from @@ -104,6 +111,17 @@ */ /*! + \fn template<typename... Types> QDBusPendingReply &QDBusPendingReply<Types...>::operator=(QDBusPendingReply &&other) + \since 6.10 + + Move-assigns \a other to this QDBusPendingReply instance and drops + the reference to the current pending call. If the current reference + is to an unfinished pending call and this is the last reference, the + pending call will be canceled and there will be no way of retrieving + the reply's contents, when they arrive. +*/ + +/*! \fn template<typename... Types> QDBusPendingReply &QDBusPendingReply<Types...>::operator=(const QDBusPendingCall &call) Makes this object take its contents from the \a call pending call diff --git a/src/dbus/qdbuspendingreply.h b/src/dbus/qdbuspendingreply.h index 7b76bcd1b7c..9b6d2d67bed 100644 --- a/src/dbus/qdbuspendingreply.h +++ b/src/dbus/qdbuspendingreply.h @@ -11,6 +11,8 @@ #ifndef QT_NO_DBUS +class tst_QDBusPendingReply; + QT_BEGIN_NAMESPACE @@ -22,6 +24,8 @@ protected: ~QDBusPendingReplyBase(); QDBusPendingReplyBase(const QDBusPendingReplyBase &) = default; QDBusPendingReplyBase &operator=(const QDBusPendingReplyBase &) = default; + QDBusPendingReplyBase(QDBusPendingReplyBase &&) noexcept = default; + QDBusPendingReplyBase &operator=(QDBusPendingReplyBase &&) noexcept = default; #endif void assign(const QDBusPendingCall &call); @@ -55,24 +59,21 @@ namespace QDBusPendingReplyTypes { template<typename... Types> class QDBusPendingReply : public QDBusPendingReplyBase { + friend class ::tst_QDBusPendingReply; template<int Index> using Select = QDBusPendingReplyTypes::Select<Index, Types...>; public: enum { Count = std::is_same_v<typename Select<0>::Type, void> ? 0 : sizeof...(Types) }; inline constexpr int count() const { return Count; } - inline QDBusPendingReply() = default; - inline QDBusPendingReply(const QDBusPendingReply &other) - : QDBusPendingReplyBase(other) - { } + // Rule Of Zero applies! + inline Q_IMPLICIT QDBusPendingReply(const QDBusPendingCall &call) // required by qdbusxml2cpp-generated code { *this = call; } inline Q_IMPLICIT QDBusPendingReply(const QDBusMessage &message) { *this = message; } - inline QDBusPendingReply &operator=(const QDBusPendingReply &other) - { assign(other); return *this; } inline QDBusPendingReply &operator=(const QDBusPendingCall &call) { assign(call); return *this; } inline QDBusPendingReply &operator=(const QDBusMessage &message) @@ -136,21 +137,19 @@ private: template<> class QDBusPendingReply<> : public QDBusPendingReplyBase { + friend class ::tst_QDBusPendingReply; public: enum { Count = 0 }; inline int count() const { return Count; } inline QDBusPendingReply() = default; - inline QDBusPendingReply(const QDBusPendingReply &other) - : QDBusPendingReplyBase(other) - { } + // Rule Of Zero applies! + inline Q_IMPLICIT QDBusPendingReply(const QDBusPendingCall &call) // required by qdbusxml2cpp-generated code { *this = call; } inline Q_IMPLICIT QDBusPendingReply(const QDBusMessage &message) { *this = message; } - inline QDBusPendingReply &operator=(const QDBusPendingReply &other) - { assign(other); return *this; } inline QDBusPendingReply &operator=(const QDBusPendingCall &call) { assign(call); return *this; } inline QDBusPendingReply &operator=(const QDBusMessage &message) diff --git a/src/gui/accessible/linux/atspiadaptor.cpp b/src/gui/accessible/linux/atspiadaptor.cpp index e0cd5aee25c..d6e46a6ca74 100644 --- a/src/gui/accessible/linux/atspiadaptor.cpp +++ b/src/gui/accessible/linux/atspiadaptor.cpp @@ -1011,6 +1011,24 @@ void AtSpiAdaptor::notify(QAccessibleEvent *event) } break; } + case QAccessible::RoleChanged: { + if (sendObject || sendObject_property_change + || sendObject_property_change_accessible_role) { + QAccessibleInterface *iface = event->accessibleInterface(); + if (!iface || !iface->isValid()) { + qCDebug(lcAccessibilityAtspi, "RoleChanged event from invalid accessible."); + return; + } + + QString path = pathForInterface(iface); + QVariantList args = packDBusSignalArguments( + "accessible-role"_L1, 0, 0, + QVariant::fromValue(QDBusVariant(uint(getRole(iface))))); + sendDBusSignal(path, ATSPI_DBUS_INTERFACE_EVENT_OBJECT ""_L1, "PropertyChange"_L1, + args); + } + break; + } case QAccessible::DescriptionChanged: { if (sendObject || sendObject_property_change || sendObject_property_change_accessible_description) { QAccessibleInterface *iface = event->accessibleInterface(); diff --git a/src/gui/accessible/qaccessible.cpp b/src/gui/accessible/qaccessible.cpp index f605ada029c..004f653bb6e 100644 --- a/src/gui/accessible/qaccessible.cpp +++ b/src/gui/accessible/qaccessible.cpp @@ -230,6 +230,7 @@ Q_STATIC_LOGGING_CATEGORY(lcAccessibilityCore, "qt.accessibility.core"); \value ParentChanged An object's parent object changed. \value PopupMenuEnd A pop-up menu has closed. \value PopupMenuStart A pop-up menu has opened. + \value [since 6.11] RoleChanged The role of an object has changed. \value ScrollingEnd A scrollbar scroll operation has ended (the mouse has released the slider handle). \value ScrollingStart A scrollbar scroll operation is about to start; this may diff --git a/src/gui/accessible/qaccessible_base.h b/src/gui/accessible/qaccessible_base.h index 9538d126ede..0a1a305b76d 100644 --- a/src/gui/accessible/qaccessible_base.h +++ b/src/gui/accessible/qaccessible_base.h @@ -103,6 +103,7 @@ public: AcceleratorChanged = 0x80C0, Announcement = 0x80D0, IdentifierChanged = 0x80E0, + RoleChanged = 0x80E1, // was declared after AcceleratorChanged, without explicit value InvalidEvent = AcceleratorChanged + 1, diff --git a/src/gui/kernel/qtestsupport_gui.cpp b/src/gui/kernel/qtestsupport_gui.cpp index dfcea928fd8..ba247800e7b 100644 --- a/src/gui/kernel/qtestsupport_gui.cpp +++ b/src/gui/kernel/qtestsupport_gui.cpp @@ -70,7 +70,7 @@ bool QTest::qWaitForWindowActive(QWindow *window, QDeadlineTimer timeout) */ bool QTest::qWaitForWindowActive(QWindow *window) { - return qWaitForWindowActive(window, Internal::defaultTryTimeout); + return qWaitForWindowActive(window, defaultTryTimeout.load(std::memory_order_relaxed)); } /*! @@ -102,7 +102,7 @@ Q_GUI_EXPORT bool QTest::qWaitForWindowFocused(QWindow *window, QDeadlineTimer t */ bool QTest::qWaitForWindowFocused(QWindow *window) { - return qWaitForWindowFocused(window, Internal::defaultTryTimeout); + return qWaitForWindowFocused(window, defaultTryTimeout.load(std::memory_order_relaxed)); } /*! @@ -143,7 +143,7 @@ bool QTest::qWaitForWindowExposed(QWindow *window, QDeadlineTimer timeout) */ bool QTest::qWaitForWindowExposed(QWindow *window) { - return qWaitForWindowExposed(window, Internal::defaultTryTimeout); + return qWaitForWindowExposed(window, defaultTryTimeout.load(std::memory_order_relaxed)); } namespace QTest { diff --git a/src/plugins/networkinformation/android/CMakeLists.txt b/src/plugins/networkinformation/android/CMakeLists.txt index 07d9201bbbd..c08f1c71d2c 100644 --- a/src/plugins/networkinformation/android/CMakeLists.txt +++ b/src/plugins/networkinformation/android/CMakeLists.txt @@ -38,9 +38,6 @@ set_property( jar/Qt${QtBase_VERSION_MAJOR}AndroidNetworkInformationBackend.jar ) -set_property( - TARGET - QAndroidNetworkInformationPlugin - APPEND PROPERTY QT_ANDROID_PERMISSIONS - android.permission.ACCESS_NETWORK_STATE +qt_internal_add_android_permission(QAndroidNetworkInformationPlugin + NAME android.permission.ACCESS_NETWORK_STATE ) diff --git a/src/plugins/platforms/wasm/qtloader.js b/src/plugins/platforms/wasm/qtloader.js index dc7f4583da8..909d8da8856 100644 --- a/src/plugins/platforms/wasm/qtloader.js +++ b/src/plugins/platforms/wasm/qtloader.js @@ -190,7 +190,8 @@ async function qtLoad(config) const originalLocateFile = config.locateFile; config.locateFile = filename => { const originalLocatedFilename = originalLocateFile ? originalLocateFile(filename) : filename; - if (originalLocatedFilename.startsWith('libQt6')) + if (originalLocatedFilename.startsWith( + 'libQt6')) // wasmqtdeploy rely on this behavior, update both in case of change return `${config.qt.qtdir}/lib/${originalLocatedFilename}`; return originalLocatedFilename; } diff --git a/src/plugins/platforms/wasm/qwasmclipboard.cpp b/src/plugins/platforms/wasm/qwasmclipboard.cpp index e5392f33cd7..44db371ca4d 100644 --- a/src/plugins/platforms/wasm/qwasmclipboard.cpp +++ b/src/plugins/platforms/wasm/qwasmclipboard.cpp @@ -48,10 +48,6 @@ static void commonCopyEvent(val event) void QWasmClipboard::cut(val event) { - QWasmInputContext *wasmInput = QWasmIntegration::get()->wasmInputContext(); - if (wasmInput && wasmInput->usingTextInput()) - return; - if (!QWasmIntegration::get()->getWasmClipboard()->hasClipboardApi()) { // Send synthetic Ctrl+X to make the app cut data to Qt's clipboard QWindowSystemInterface::handleKeyEvent( @@ -63,10 +59,6 @@ void QWasmClipboard::cut(val event) void QWasmClipboard::copy(val event) { - QWasmInputContext *wasmInput = QWasmIntegration::get()->wasmInputContext(); - if (wasmInput && wasmInput->usingTextInput()) - return; - if (!QWasmIntegration::get()->getWasmClipboard()->hasClipboardApi()) { // Send synthetic Ctrl+C to make the app copy data to Qt's clipboard QWindowSystemInterface::handleKeyEvent( @@ -77,10 +69,6 @@ void QWasmClipboard::copy(val event) void QWasmClipboard::paste(val event) { - QWasmInputContext *wasmInput = QWasmIntegration::get()->wasmInputContext(); - if (wasmInput && wasmInput->usingTextInput()) - return; - event.call<void>("preventDefault"); // prevent browser from handling drop event QWasmIntegration::get()->getWasmClipboard()->sendClipboardData(event); @@ -183,86 +171,42 @@ void QWasmClipboard::writeToClipboardApi() { 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) + QMimeData *mimeData = this->mimeData(QClipboard::Clipboard); + if (!mimeData) return; - 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()); + // Support for plain text, html and images (png) are standardized, + // copy those to the clipboard data object. + emscripten::val clipboardData = emscripten::val::object(); + for (const QString &mimetype: mimeData->formats()) { + if (mimetype == QLatin1String("text/plain")) { + emscripten::val text = mimeData->text().toEcmaString(); + clipboardData.set(mimetype.toEcmaString(), text); + } else if (mimetype == QLatin1String("text/html")) { + emscripten::val html = mimeData->html().toEcmaString(); + clipboardData.set(mimetype.toEcmaString(), html); + } else if (mimetype.contains(QLatin1String("image"))) { + // Serialize the Qt image data to browser supported png + QImage img = qvariant_cast<QImage>(mimeData->imageData()); + QByteArray ba; 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; + qstdweb::Blob blob = qstdweb::Blob::fromArrayBuffer(qstdweb::Uint8Array::copyFrom(ba).buffer()); + clipboardData.set(std::string("image/png"), blob.val()); } - - 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"); + // Return if there is no data (creating an empty ClipboardItem is an error) + if (val::global("Object").call<val>("keys", clipboardData)["length"].as<int>() == 0) + return; + // Write a single clipboard item containing the data formats to the clipboard + emscripten::val clipboardItem = val::global("ClipboardItem").new_(clipboardData); + emscripten::val clipboardItemArray = emscripten::val::array(); + clipboardItemArray.call<void>("push", clipboardItem); + val navigator = val::global("navigator"); qstdweb::Promise::make( navigator["clipboard"], "write", { @@ -272,7 +216,7 @@ void QWasmClipboard::writeToClipboardApi() << QString::fromStdString(error["message"].as<std::string>()); } }, - clipboardWriteArray); + clipboardItemArray); } void QWasmClipboard::writeToClipboard() diff --git a/src/plugins/platforms/wasm/qwasminputcontext.cpp b/src/plugins/platforms/wasm/qwasminputcontext.cpp index 2df7066b8e2..191e2947629 100644 --- a/src/plugins/platforms/wasm/qwasminputcontext.cpp +++ b/src/plugins/platforms/wasm/qwasminputcontext.cpp @@ -25,105 +25,88 @@ void QWasmInputContext::inputCallback(emscripten::val event) { qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << "isComposing : " << event["isComposing"].as<bool>(); - QString inputStr = (event["data"] != emscripten::val::null() - && event["data"] != emscripten::val::undefined()) ? - QString::fromStdString(event["data"].as<std::string>()) : QString(); - - QWasmInputContext *wasmInput = - reinterpret_cast<QWasmInputContext *>(event["target"]["data-qinputcontext"].as<quintptr>()); - emscripten::val inputType = event["inputType"]; - if (inputType != emscripten::val::null() - && inputType != emscripten::val::undefined()) { - const auto inputTypeString = inputType.as<std::string>(); - // There are many inputTypes for InputEvent - // https://www.w3.org/TR/input-events-1/ - // Some of them should be implemented here later. - qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << "inputType : " << inputTypeString; - if (!inputTypeString.compare("deleteContentBackward")) { - QWindowSystemInterface::handleKeyEvent(0, - QEvent::KeyPress, - Qt::Key_Backspace, - Qt::NoModifier); - QWindowSystemInterface::handleKeyEvent(0, - QEvent::KeyRelease, - Qt::Key_Backspace, - Qt::NoModifier); - event.call<void>("stopImmediatePropagation"); - return; - } else if (!inputTypeString.compare("deleteContentForward")) { - QWindowSystemInterface::handleKeyEvent(0, - QEvent::KeyPress, - Qt::Key_Delete, - Qt::NoModifier); - QWindowSystemInterface::handleKeyEvent(0, - QEvent::KeyRelease, - Qt::Key_Delete, - Qt::NoModifier); - event.call<void>("stopImmediatePropagation"); - return; - } else if (!inputTypeString.compare("insertCompositionText")) { - qCDebug(qLcQpaWasmInputContext) << "inputString : " << inputStr; - wasmInput->insertPreedit(); - event.call<void>("stopImmediatePropagation"); - return; - } else if (!inputTypeString.compare("insertReplacementText")) { - qCDebug(qLcQpaWasmInputContext) << "inputString : " << inputStr; - //auto ranges = event.call<emscripten::val>("getTargetRanges"); - //qCDebug(qLcQpaWasmInputContext) << ranges["length"].as<int>(); - // WA For Korean IME - // insertReplacementText should have targetRanges but - // Safari cannot have it and just it seems to be supposed - // to replace previous input. - wasmInput->insertText(inputStr, true); - - event.call<void>("stopImmediatePropagation"); - return; - } else if (!inputTypeString.compare("deleteCompositionText")) { - wasmInput->setPreeditString("", 0); - wasmInput->insertPreedit(); - event.call<void>("stopImmediatePropagation"); - return; - } else if (!inputTypeString.compare("insertFromComposition")) { - wasmInput->setPreeditString(inputStr, 0); - wasmInput->insertPreedit(); - event.call<void>("stopImmediatePropagation"); - return; - } else if (!inputTypeString.compare("insertText")) { - wasmInput->insertText(inputStr); - event.call<void>("stopImmediatePropagation"); + if (inputType.isNull() || inputType.isUndefined()) + return; + const auto inputTypeString = inputType.as<std::string>(); + + emscripten::val inputData = event["data"]; + QString inputStr = (!inputData.isNull() && !inputData.isUndefined()) + ? QString::fromEcmaString(inputData) : QString(); + + // There are many inputTypes for InputEvent + // https://www.w3.org/TR/input-events-1/ + // Some of them should be implemented here later. + qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << "inputType : " << inputTypeString; + if (!inputTypeString.compare("deleteContentBackward")) { + QWindowSystemInterface::handleKeyEvent(0, QEvent::KeyPress, Qt::Key_Backspace, Qt::NoModifier); + QWindowSystemInterface::handleKeyEvent(0, QEvent::KeyRelease, Qt::Key_Backspace, Qt::NoModifier); + event.call<void>("stopImmediatePropagation"); + return; + } else if (!inputTypeString.compare("deleteContentForward")) { + QWindowSystemInterface::handleKeyEvent(0, QEvent::KeyPress, Qt::Key_Delete, Qt::NoModifier); + QWindowSystemInterface::handleKeyEvent(0, QEvent::KeyRelease, Qt::Key_Delete, Qt::NoModifier); + event.call<void>("stopImmediatePropagation"); + return; + } else if (!inputTypeString.compare("insertCompositionText")) { + qCDebug(qLcQpaWasmInputContext) << "inputString : " << inputStr; + insertPreedit(); + event.call<void>("stopImmediatePropagation"); + return; + } else if (!inputTypeString.compare("insertReplacementText")) { + qCDebug(qLcQpaWasmInputContext) << "inputString : " << inputStr; + //auto ranges = event.call<emscripten::val>("getTargetRanges"); + //qCDebug(qLcQpaWasmInputContext) << ranges["length"].as<int>(); + // WA For Korean IME + // insertReplacementText should have targetRanges but + // Safari cannot have it and just it seems to be supposed + // to replace previous input. + insertText(inputStr, true); + + event.call<void>("stopImmediatePropagation"); + return; + } else if (!inputTypeString.compare("deleteCompositionText")) { + setPreeditString("", 0); + insertPreedit(); + event.call<void>("stopImmediatePropagation"); + return; + } else if (!inputTypeString.compare("insertFromComposition")) { + setPreeditString(inputStr, 0); + insertPreedit(); + event.call<void>("stopImmediatePropagation"); + return; + } else if (!inputTypeString.compare("insertText")) { + insertText(inputStr); + event.call<void>("stopImmediatePropagation"); #if QT_CONFIG(clipboard) - } else if (!inputTypeString.compare("insertFromPaste")) { - wasmInput->insertText(QGuiApplication::clipboard()->text()); - event.call<void>("stopImmediatePropagation"); - // These can be supported here, - // But now, keyCallback in QWasmWindow - // will take them as exceptions. - //} else if (!inputTypeString.compare("deleteByCut")) { + } else if (!inputTypeString.compare("insertFromPaste")) { + insertText(QGuiApplication::clipboard()->text()); + event.call<void>("stopImmediatePropagation"); + // These can be supported here, + // But now, keyCallback in QWasmWindow + // will take them as exceptions. + //} else if (!inputTypeString.compare("deleteByCut")) { #endif - } else { - qCWarning(qLcQpaWasmInputContext) << Q_FUNC_INFO << "inputType \"" << inputType.as<std::string>() << "\" is not supported in Qt yet"; - } + } else { + qCWarning(qLcQpaWasmInputContext) << Q_FUNC_INFO << "inputType \"" << + inputType.as<std::string>() << "\" is not supported in Qt yet"; } } void QWasmInputContext::compositionEndCallback(emscripten::val event) { - const auto inputStr = QString::fromStdString(event["data"].as<std::string>()); + const auto inputStr = QString::fromEcmaString(event["data"]); qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << inputStr; - QWasmInputContext *wasmInput = - reinterpret_cast<QWasmInputContext *>(event["target"]["data-qinputcontext"].as<quintptr>()); - - if (wasmInput->preeditString().isEmpty()) + if (preeditString().isEmpty()) return; - if (inputStr != wasmInput->preeditString()) { + if (inputStr != preeditString()) { qCWarning(qLcQpaWasmInputContext) << Q_FUNC_INFO << "Composition string" << inputStr - << "is differ from" << wasmInput->preeditString(); + << "is differ from" << preeditString(); } - wasmInput->commitPreeditAndClear(); + commitPreeditAndClear(); } void QWasmInputContext::compositionStartCallback(emscripten::val event) @@ -151,19 +134,15 @@ static void beforeInputCallback(emscripten::val event) void QWasmInputContext::compositionUpdateCallback(emscripten::val event) { - const auto compositionStr = QString::fromStdString(event["data"].as<std::string>()); + const auto compositionStr = QString::fromEcmaString(event["data"]); qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << compositionStr; - QWasmInputContext *wasmInput = - reinterpret_cast<QWasmInputContext *>(event["target"]["data-qinputcontext"].as<quintptr>()); - // WA for IOS. // Not sure now because I cannot test it anymore. // int replaceSize = 0; // emscripten::val win = emscripten::val::global("window"); // emscripten::val sel = win.call<emscripten::val>("getSelection"); -// if (sel != emscripten::val::null() -// && sel != emscripten::val::undefined() +// if (!sel.isNull() && !sel.isUndefined() // && sel["rangeCount"].as<int>() > 0) { // QInputMethodQueryEvent queryEvent(Qt::ImQueryAll); // QCoreApplication::sendEvent(QGuiApplication::focusObject(), &queryEvent); @@ -172,8 +151,8 @@ void QWasmInputContext::compositionUpdateCallback(emscripten::val event) // qCDebug(qLcQpaWasmInputContext) << "Qt text before cursor: " << queryEvent.value(Qt::ImTextBeforeCursor).toString(); // qCDebug(qLcQpaWasmInputContext) << "Qt text after cursor: " << queryEvent.value(Qt::ImTextAfterCursor).toString(); // -// const QString &selectedStr = QString::fromUtf8(sel.call<emscripten::val>("toString").as<std::string>()); -// const auto &preeditStr = wasmInput->preeditString(); +// const QString &selectedStr = QString::fromEcmaString(sel.call<emscripten::val>("toString")); +// const auto &preeditStr = preeditString(); // qCDebug(qLcQpaWasmInputContext) << "Selection.type : " << sel["type"].as<std::string>(); // qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << "Selected: " << selectedStr; // qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << "PreeditString: " << preeditStr; @@ -189,90 +168,13 @@ void QWasmInputContext::compositionUpdateCallback(emscripten::val event) // qCDebug(qLcQpaWasmInputContext) << "Range.endOffset : " << range["endOffset"].as<int>(); // } // -// wasmInput->setPreeditString(compositionStr, replaceSize); - wasmInput->setPreeditString(compositionStr, 0); -} - -#if QT_CONFIG(clipboard) -static void copyCallback(emscripten::val event) -{ - qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO; - - QClipboard *clipboard = QGuiApplication::clipboard(); - QString inputStr = clipboard->text(); - qCDebug(qLcQpaWasmInputContext) << "QClipboard : " << inputStr; - event["clipboardData"].call<void>("setData", - emscripten::val("text/plain"), - inputStr.toStdString()); - event.call<void>("preventDefault"); - event.call<void>("stopImmediatePropagation"); -} - -static void cutCallback(emscripten::val event) -{ - qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO; - - QClipboard *clipboard = QGuiApplication::clipboard(); - QString inputStr = clipboard->text(); - qCDebug(qLcQpaWasmInputContext) << "QClipboard : " << inputStr; - event["clipboardData"].call<void>("setData", - emscripten::val("text/plain"), - inputStr.toStdString()); - event.call<void>("preventDefault"); - event.call<void>("stopImmediatePropagation"); +// setPreeditString(compositionStr, replaceSize); + setPreeditString(compositionStr, 0); } -static void pasteCallback(emscripten::val event) -{ - qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO; - - emscripten::val clipboardData = event["clipboardData"].call<emscripten::val>("getData", emscripten::val("text/plain")); - QString clipboardStr = QString::fromStdString(clipboardData.as<std::string>()); - qCDebug(qLcQpaWasmInputContext) << "wasm clipboard : " << clipboardStr; - QClipboard *clipboard = QGuiApplication::clipboard(); - if (clipboard->text() != clipboardStr) - clipboard->setText(clipboardStr); - - // propagate to input event (insertFromPaste) -} -#endif // QT_CONFIG(clipboard) - QWasmInputContext::QWasmInputContext() { qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO; - emscripten::val document = emscripten::val::global("document"); - // This 'input' can be an issue to handle multiple lines, - // 'textarea' can be used instead. - m_inputElement = document.call<emscripten::val>("createElement", std::string("input")); - m_inputElement.set("type", "text"); - m_inputElement.set("contenteditable","true"); - m_inputElement.call<void>("setAttribute", std::string("aria-hidden"), std::string("true")); - - m_inputElement["style"].set("position", "absolute"); - m_inputElement["style"].set("left", 0); - m_inputElement["style"].set("top", 0); - m_inputElement["style"].set("opacity", 0); - m_inputElement["style"].set("display", ""); - m_inputElement["style"].set("z-index", -2); - m_inputElement["style"].set("width", "1px"); - m_inputElement["style"].set("height", "1px"); - - m_inputElement.set("data-qinputcontext", - emscripten::val(quintptr(reinterpret_cast<void *>(this)))); - emscripten::val body = document["body"]; - body.call<void>("appendChild", m_inputElement); - - m_inputCallback = QWasmEventHandler(m_inputElement, "input", QWasmInputContext::inputCallback); - m_compositionEndCallback = QWasmEventHandler(m_inputElement, "compositionend", QWasmInputContext::compositionEndCallback); - m_compositionStartCallback = QWasmEventHandler(m_inputElement, "compositionstart", QWasmInputContext::compositionStartCallback); - m_compositionUpdateCallback = QWasmEventHandler(m_inputElement, "compositionupdate", QWasmInputContext::compositionUpdateCallback); - -#if QT_CONFIG(clipboard) - // Clipboard for InputContext - m_clipboardCut = QWasmEventHandler(m_inputElement, "cut", cutCallback); - m_clipboardCopy = QWasmEventHandler(m_inputElement, "copy", copyCallback); - m_clipboardPaste = QWasmEventHandler(m_inputElement, "paste", pasteCallback); -#endif } QWasmInputContext::~QWasmInputContext() @@ -303,6 +205,9 @@ void QWasmInputContext::showInputPanel() void QWasmInputContext::updateGeometry() { + if (m_inputElement.isNull()) + return; + const QWindow *focusWindow = QGuiApplication::focusWindow(); if (!m_focusObject || !focusWindow || !m_inputMethodAccepted) { m_inputElement["style"].set("left", "0px"); @@ -312,23 +217,12 @@ void QWasmInputContext::updateGeometry() Q_ASSERT(m_focusObject); Q_ASSERT(m_inputMethodAccepted); - // Set the geometry - QPoint globalPos; - const QRect cursorRectangle = QPlatformInputContext::cursorRectangle().toRect(); - if (cursorRectangle.isValid()) { - qCDebug(qLcQpaWasmInputContext) - << Q_FUNC_INFO << "cursorRectangle: " << cursorRectangle; - globalPos = focusWindow->mapToGlobal(cursorRectangle.topLeft()); - if (globalPos.x() > 0) - globalPos.setX(globalPos.x() - 1); - if (globalPos.y() > 0) - globalPos.setY(globalPos.y() - 1); - } - - const auto styleLeft = std::to_string(globalPos.x()) + "px"; - const auto styleTop = std::to_string(globalPos.y()) + "px"; - m_inputElement["style"].set("left", styleLeft); - m_inputElement["style"].set("top", styleTop); + const QRect inputItemRectangle = QPlatformInputContext::inputItemRectangle().toRect(); + qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << "propagating inputItemRectangle:" << inputItemRectangle; + m_inputElement["style"].set("left", std::to_string(inputItemRectangle.x()) + "px"); + m_inputElement["style"].set("top", std::to_string(inputItemRectangle.y()) + "px"); + m_inputElement["style"].set("width", std::to_string(inputItemRectangle.width()) + "px"); + m_inputElement["style"].set("height", std::to_string(inputItemRectangle.height()) + "px"); } } @@ -341,16 +235,21 @@ void QWasmInputContext::updateInputElement() updateGeometry(); // If there is no focus object, or no visible input panel, remove focus - const QWindow *focusWindow = QGuiApplication::focusWindow(); + QWasmWindow *focusWindow = QWasmWindow::fromWindow(QGuiApplication::focusWindow()); if (!m_focusObject || !focusWindow || !m_inputMethodAccepted) { - m_inputElement.set("value", ""); + if (!m_inputElement.isNull()) { + m_inputElement.set("value", ""); + m_inputElement.set("inputMode", std::string("none")); + } - if (QWasmWindow *wasmwindow = QWasmWindow::fromWindow(focusWindow)) - wasmwindow->focus(); - else - m_inputElement.call<void>("blur"); + if (focusWindow) { + focusWindow->focus(); + } else { + if (!m_inputElement.isNull()) + m_inputElement.call<void>("blur"); + } - m_inputElement.set("inputMode", std::string("none")); + m_inputElement = emscripten::val::null(); return; } @@ -358,6 +257,8 @@ void QWasmInputContext::updateInputElement() Q_ASSERT(m_focusObject); Q_ASSERT(m_inputMethodAccepted); + m_inputElement = focusWindow->inputElement(); + qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << QRectF::fromDOMRect(m_inputElement.call<emscripten::val>("getBoundingClientRect")); // Set the text input @@ -375,7 +276,15 @@ void QWasmInputContext::updateInputElement() m_inputElement.set("selectionStart", queryEvent.value(Qt::ImAnchorPosition).toUInt()); m_inputElement.set("selectionEnd", queryEvent.value(Qt::ImCursorPosition).toUInt()); + QInputMethodQueryEvent query((Qt::InputMethodQueries(Qt::ImHints))); + QCoreApplication::sendEvent(m_focusObject, &query); + if (Qt::InputMethodHints(query.value(Qt::ImHints).toInt()).testFlag(Qt::ImhHiddenText)) + m_inputElement.set("type", "password"); + else + m_inputElement.set("type", "text"); + m_inputElement.set("inputMode", std::string("text")); + m_inputElement.call<void>("focus"); } @@ -383,16 +292,6 @@ void QWasmInputContext::setFocusObject(QObject *object) { qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << object << inputMethodAccepted(); - QInputMethodQueryEvent query(Qt::InputMethodQueries(Qt::ImEnabled | Qt::ImHints)); - QCoreApplication::sendEvent(object, &query); - if (query.value(Qt::ImEnabled).toBool() - && Qt::InputMethodHints(query.value(Qt::ImHints).toInt()).testFlag(Qt::ImhHiddenText)) { - m_inputElement.set("type", "password"); - } else { - if (m_inputElement["type"].as<std::string>() != std::string("text")) - m_inputElement.set("type", "text"); - } - // Commit the previous composition before change m_focusObject if (m_focusObject && !m_preeditString.isEmpty()) commitPreeditAndClear(); diff --git a/src/plugins/platforms/wasm/qwasminputcontext.h b/src/plugins/platforms/wasm/qwasminputcontext.h index 72476adfe1b..6d24c7fea0d 100644 --- a/src/plugins/platforms/wasm/qwasminputcontext.h +++ b/src/plugins/platforms/wasm/qwasminputcontext.h @@ -35,38 +35,33 @@ public: void setPreeditString(QString preeditStr, int replaceSize); void insertPreedit(); void commitPreeditAndClear(); - emscripten::val m_inputElement = emscripten::val::null(); void insertText(QString inputStr, bool replace = false); - QWasmEventHandler m_inputCallback; - QWasmEventHandler m_compositionEndCallback; - QWasmEventHandler m_compositionStartCallback; - QWasmEventHandler m_compositionUpdateCallback; - bool usingTextInput() const { return m_inputMethodAccepted; } void setFocusObject(QObject *object) override; - static void inputCallback(emscripten::val event); - static void compositionEndCallback(emscripten::val event); - static void compositionStartCallback(emscripten::val event); - static void compositionUpdateCallback(emscripten::val event); + void inputCallback(emscripten::val event); + void compositionEndCallback(emscripten::val event); + void compositionStartCallback(emscripten::val event); + void compositionUpdateCallback(emscripten::val event); void updateGeometry(); + bool isActive() const { + return m_focusObject && m_inputMethodAccepted; + } + private: void updateInputElement(); private: - QWasmEventHandler m_clipboardCut; - QWasmEventHandler m_clipboardCopy; - QWasmEventHandler m_clipboardPaste; - QString m_preeditString; int m_replaceSize = 0; bool m_inputMethodAccepted = false; QObject *m_focusObject = nullptr; + emscripten::val m_inputElement = emscripten::val::null(); }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmwindow.cpp b/src/plugins/platforms/wasm/qwasmwindow.cpp index d690bcfe10a..d27385be723 100644 --- a/src/plugins/platforms/wasm/qwasmwindow.cpp +++ b/src/plugins/platforms/wasm/qwasmwindow.cpp @@ -51,7 +51,10 @@ QWasmWindow::QWasmWindow(QWindow *w, QWasmDeadKeySupport *deadKeySupport, m_decoratedWindow(m_document.call<emscripten::val>("createElement", emscripten::val("div"))), m_window(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_canvas(m_document.call<emscripten::val>("createElement", emscripten::val("canvas"))), + m_focusHelper(m_document.call<emscripten::val>("createElement", emscripten::val("div"))), + m_inputElement(m_document.call<emscripten::val>("createElement", emscripten::val("input"))) + { m_decoratedWindow.set("className", "qt-decorated-window"); m_decoratedWindow["style"].set("display", std::string("none")); @@ -80,17 +83,6 @@ QWasmWindow::QWasmWindow(QWindow *w, QWasmDeadKeySupport *deadKeySupport, m_canvas["classList"].call<void>("add", emscripten::val("qt-window-canvas")); - // Set contentEditable for two reasons; - // 1) so that the window gets clipboard events, - // 2) For applications who will handle keyboard events, but without having inputMethodAccepted() - // - // Set inputMode to none to avoid keyboard popping up on push buttons - // This is a tradeoff, we are not able to separate between a push button and - // a widget that reads keyboard events. - m_canvas.call<void>("setAttribute", std::string("inputmode"), std::string("none")); - m_canvas.call<void>("setAttribute", std::string("contenteditable"), std::string("true")); - m_canvas["style"].set("outline", std::string("none")); - #if QT_CONFIG(clipboard) if (QWasmClipboard::shouldInstallWindowEventHandlers()) { m_cutCallback = QWasmEventHandler(m_canvas, "cut", QWasmClipboard::cut); @@ -99,9 +91,37 @@ QWasmWindow::QWasmWindow(QWindow *w, QWasmDeadKeySupport *deadKeySupport, } #endif - // Set inputMode to none to stop the mobile keyboard from opening - // when the user clicks on the window. - m_window.set("inputMode", std::string("none")); + // Set up m_focusHelper, which is an invisible child element of the window which takes + // focus on behalf of the window any time the window has focus in general, but none + // of the special child elements such as the inputElment or a11y elements have focus. + // Set inputMode=none set to prevent the virtual keyboard from popping up. + m_focusHelper["classList"].call<void>("add", emscripten::val("qt-window-focus-helper")); + m_focusHelper.set("inputMode", std::string("none")); + m_focusHelper.call<void>("setAttribute", std::string("aria-hidden"), std::string("true")); + m_focusHelper.call<void>("setAttribute", std::string("contenteditable"), std::string("true")); + m_focusHelper["style"].set("position", "absolute"); + m_focusHelper["style"].set("left", 0); + m_focusHelper["style"].set("top", 0); + m_focusHelper["style"].set("width", "1px"); + m_focusHelper["style"].set("height", "1px"); + m_focusHelper["style"].set("z-index", -2); + m_focusHelper["style"].set("opacity", 0); + m_window.call<void>("appendChild", m_focusHelper); + + // Set up m_inputElement, which takes focus whenever a Qt text input UI element has + // foucus. + m_inputElement["classList"].call<void>("add", emscripten::val("qt-window-input-element")); + m_inputElement.set("type", "text"); + m_inputElement.call<void>("setAttribute", std::string("aria-hidden"), std::string("true")); + m_inputElement["style"].set("position", "absolute"); + m_inputElement["style"].set("left", 0); + m_inputElement["style"].set("top", 0); + m_inputElement["style"].set("width", "1px"); + m_inputElement["style"].set("height", "1px"); + m_inputElement["style"].set("z-index", -2); + m_inputElement["style"].set("opacity", 0); + m_inputElement["style"].set("display", ""); + m_window.call<void>("appendChild", m_inputElement); // Hide the canvas from screen readers. m_canvas.call<void>("setAttribute", std::string("aria-hidden"), std::string("true")); @@ -193,21 +213,20 @@ void QWasmWindow::registerEventHandlers() m_wheelEventCallback = QWasmEventHandler(m_window, "wheel", [this](emscripten::val event) { this->handleWheelEvent(event); }); - QWasmInputContext *wasmInput = QWasmIntegration::get()->wasmInputContext(); - if (wasmInput) { - m_keyDownCallbackForInputContext = - QWasmEventHandler(wasmInput->m_inputElement, "keydown", - [this](emscripten::val event) { this->handleKeyForInputContextEvent(EventType::KeyDown, event); }); - m_keyUpCallbackForInputContext = - QWasmEventHandler(wasmInput->m_inputElement, "keyup", - [this](emscripten::val event) { this->handleKeyForInputContextEvent(EventType::KeyUp, event); }); - } - - m_keyDownCallback = QWasmEventHandler(m_canvas, "keydown", + m_keyDownCallback = QWasmEventHandler(m_window, "keydown", [this](emscripten::val event) { this->handleKeyEvent(KeyEvent(EventType::KeyDown, event, m_deadKeySupport)); }); - m_keyUpCallback =QWasmEventHandler(m_canvas, "keyup", + m_keyUpCallback =QWasmEventHandler(m_window, "keyup", [this](emscripten::val event) {this->handleKeyEvent(KeyEvent(EventType::KeyUp, event, m_deadKeySupport)); }); -} + + m_inputCallback = QWasmEventHandler(m_window, "input", + [this](emscripten::val event){ handleInputEvent(event); }); + m_compositionUpdateCallback = QWasmEventHandler(m_window, "compositionupdate", + [this](emscripten::val event){ handleCompositionUpdateEvent(event); }); + m_compositionStartCallback = QWasmEventHandler(m_window, "compositionstart", + [this](emscripten::val event){ handleCompositionStartEvent(event); }); + m_compositionEndCallback = QWasmEventHandler(m_window, "compositionend", + [this](emscripten::val event){ handleCompositionEndEvent(event); }); + } QWasmWindow::~QWasmWindow() { @@ -624,10 +643,15 @@ void QWasmWindow::commitParent(QWasmWindowTreeNode *parent) void QWasmWindow::handleKeyEvent(const KeyEvent &event) { - qCDebug(qLcQpaWasmInputContext) << "processKey as KeyEvent"; - if (processKey(event)) { - event.webEvent.call<void>("preventDefault"); - event.webEvent.call<void>("stopPropagation"); + qCDebug(qLcQpaWasmInputContext) << "handleKeyEvent"; + + if (QWasmInputContext *inputContext = QWasmIntegration::get()->wasmInputContext(); inputContext->isActive()) { + handleKeyForInputContextEvent(event); + } else { + if (processKey(event)) { + event.webEvent.call<void>("preventDefault"); + event.webEvent.call<void>("stopPropagation"); + } } } @@ -658,7 +682,7 @@ bool QWasmWindow::processKey(const KeyEvent &event) #endif } -void QWasmWindow::handleKeyForInputContextEvent(EventType eventType, const emscripten::val &event) +void QWasmWindow::handleKeyForInputContextEvent(const KeyEvent &keyEvent) { // // Things to consider: @@ -668,40 +692,43 @@ void QWasmWindow::handleKeyForInputContextEvent(EventType eventType, const emscr // complex (i.e Chinese et al) input handling // Multiline text edit backspace at start of line // - const QWasmInputContext *wasmInput = QWasmIntegration::get()->wasmInputContext(); - if (wasmInput) { + emscripten::val event = keyEvent.webEvent; + bool useInputContext = [event]() -> bool { + const QWasmInputContext *wasmInput = QWasmIntegration::get()->wasmInputContext(); + if (!wasmInput) + return false; + const auto keyString = QString::fromStdString(event["key"].as<std::string>()); qCDebug(qLcQpaWasmInputContext) << "Key callback" << keyString << keyString.size(); - if (keyString == "Unidentified") { - // Android makes a bunch of KeyEvents as "Unidentified" - // They will be processed just in InputContext. - return; - } else if (event["isComposing"].as<bool>()) { - // Handled by the input context - return; - } else if (event["ctrlKey"].as<bool>() - || event["altKey"].as<bool>() - || event["metaKey"].as<bool>()) { - // Not all platforms use 'isComposing' for '~' + 'a', in this - // case send the key with state ('ctrl', 'alt', or 'meta') to - // processKeyForInputContext - - ; // fallthrough - } else if (keyString.size() != 1) { - // This is like; 'Shift','ArrowRight','AltGraph', ... - // send all of these to processKeyForInputContext - - ; // fallthrough - } else if (wasmInput->inputMethodAccepted()) { - // processed in inputContext with skipping processKey - return; - } - } - qCDebug(qLcQpaWasmInputContext) << "processKey as KeyEvent"; - if (processKeyForInputContext(KeyEvent(eventType, event, m_deadKeySupport))) - event.call<void>("preventDefault"); - event.call<void>("stopImmediatePropagation"); + // Events with isComposing set are handled by the input context + bool composing = event["isComposing"].as<bool>(); + + // Android makes a bunch of KeyEvents as "Unidentified", + // make inputContext handle those. + bool androidUnidentified = (keyString == "Unidentified"); + + // Not all platforms use 'isComposing' for '~' + 'a', in this + // case send the key with state ('ctrl', 'alt', or 'meta') to + // processKeyForInputContext + bool hasModifiers = event["ctrlKey"].as<bool>() + || event["altKey"].as<bool>() + || event["metaKey"].as<bool>(); + + // This is like; 'Shift','ArrowRight','AltGraph', ... + // send all of these to processKeyForInputContext + bool hasNoncharacterKeyString = keyString.size() != 1; + + bool overrideCompose = !hasModifiers && !hasNoncharacterKeyString && wasmInput->inputMethodAccepted(); + return composing || androidUnidentified || overrideCompose; + }(); + + if (!useInputContext) { + qCDebug(qLcQpaWasmInputContext) << "processKey as KeyEvent"; + if (processKeyForInputContext(keyEvent)) + event.call<void>("preventDefault"); + event.call<void>("stopImmediatePropagation"); + } } bool QWasmWindow::processKeyForInputContext(const KeyEvent &event) @@ -729,6 +756,30 @@ bool QWasmWindow::processKeyForInputContext(const KeyEvent &event) return result; } +void QWasmWindow::handleInputEvent(emscripten::val event) +{ + if (QWasmInputContext *inputContext = QWasmIntegration::get()->wasmInputContext(); inputContext->isActive()) + inputContext->inputCallback(event); +} + +void QWasmWindow::handleCompositionStartEvent(emscripten::val event) +{ + if (QWasmInputContext *inputContext = QWasmIntegration::get()->wasmInputContext(); inputContext->isActive()) + inputContext->compositionStartCallback(event); +} + +void QWasmWindow::handleCompositionUpdateEvent(emscripten::val event) +{ + if (QWasmInputContext *inputContext = QWasmIntegration::get()->wasmInputContext(); inputContext->isActive()) + inputContext->compositionUpdateCallback(event); +} + +void QWasmWindow::handleCompositionEndEvent(emscripten::val event) +{ + if (QWasmInputContext *inputContext = QWasmIntegration::get()->wasmInputContext(); inputContext->isActive()) + inputContext->compositionEndCallback(event); +} + void QWasmWindow::handlePointerEnterLeaveEvent(const PointerEvent &event) { if (processPointerEnterLeave(event)) @@ -1040,7 +1091,7 @@ void QWasmWindow::requestActivateWindow() void QWasmWindow::focus() { - m_canvas.call<void>("focus"); + m_focusHelper.call<void>("focus"); } bool QWasmWindow::setMouseGrabEnabled(bool grab) diff --git a/src/plugins/platforms/wasm/qwasmwindow.h b/src/plugins/platforms/wasm/qwasmwindow.h index 904e736a7e7..0c63ebdc16e 100644 --- a/src/plugins/platforms/wasm/qwasmwindow.h +++ b/src/plugins/platforms/wasm/qwasmwindow.h @@ -100,6 +100,7 @@ public: emscripten::val context2d() const { return m_context2d; } emscripten::val a11yContainer() const { return m_a11yContainer; } emscripten::val inputHandlerElement() const { return m_window; } + emscripten::val inputElement() const { return m_inputElement; } // QNativeInterface::Private::QWasmWindow emscripten::val document() const override { return m_document; } @@ -137,8 +138,13 @@ private: void handleKeyEvent(const KeyEvent &event); bool processKey(const KeyEvent &event); - void handleKeyForInputContextEvent(EventType eventType, const emscripten::val &event); + void handleKeyForInputContextEvent(const KeyEvent &event); bool processKeyForInputContext(const KeyEvent &event); + void handleInputEvent(emscripten::val event); + void handleCompositionStartEvent(emscripten::val event); + void handleCompositionUpdateEvent(emscripten::val event); + void handleCompositionEndEvent(emscripten::val event); + void handlePointerEnterLeaveEvent(const PointerEvent &event); bool processPointerEnterLeave(const PointerEvent &event); void processPointer(const PointerEvent &event); @@ -154,11 +160,14 @@ private: QWasmDeadKeySupport *m_deadKeySupport; QRect m_normalGeometry {0, 0, 0 ,0}; - emscripten::val m_document = emscripten::val::undefined(); - emscripten::val m_decoratedWindow = emscripten::val::undefined(); - emscripten::val m_window = emscripten::val::undefined(); - emscripten::val m_a11yContainer = emscripten::val::undefined(); - emscripten::val m_canvas = emscripten::val::undefined(); + emscripten::val m_document; + emscripten::val m_decoratedWindow; + emscripten::val m_window; + emscripten::val m_a11yContainer; + emscripten::val m_canvas; + emscripten::val m_focusHelper; + emscripten::val m_inputElement; + emscripten::val m_context2d = emscripten::val::undefined(); std::unique_ptr<NonClientArea> m_nonClientArea; @@ -169,6 +178,10 @@ private: QWasmEventHandler m_keyUpCallback; QWasmEventHandler m_keyDownCallbackForInputContext; QWasmEventHandler m_keyUpCallbackForInputContext; + QWasmEventHandler m_inputCallback; + QWasmEventHandler m_compositionStartCallback; + QWasmEventHandler m_compositionUpdateCallback; + QWasmEventHandler m_compositionEndCallback; QWasmEventHandler m_pointerDownCallback; QWasmEventHandler m_pointerMoveCallback; diff --git a/src/plugins/platforms/wayland/CMakeLists.txt b/src/plugins/platforms/wayland/CMakeLists.txt index 254a43c0dc6..40e85ac4b1d 100644 --- a/src/plugins/platforms/wayland/CMakeLists.txt +++ b/src/plugins/platforms/wayland/CMakeLists.txt @@ -77,7 +77,6 @@ qt_internal_add_module(WaylandClient qwaylandplatformservices.cpp qwaylandplatformservices_p.h qwaylandpointergestures.cpp qwaylandpointergestures_p.h qwaylandscreen.cpp qwaylandscreen_p.h - qwaylandsessionmanager.cpp qwaylandsessionmanager_p.h qwaylandshellsurface.cpp qwaylandshellsurface_p.h qwaylandshm.cpp qwaylandshm_p.h qwaylandshmbackingstore.cpp qwaylandshmbackingstore_p.h @@ -233,6 +232,11 @@ qt_internal_extend_target(WaylandClient CONDITION QT_FEATURE_egl AND NOT QT_FEAT QT_EGL_NO_X11 ) +qt_internal_extend_target(WaylandClient CONDITION QT_FEATURE_sessionmanager + SOURCES + qwaylandsessionmanager.cpp qwaylandsessionmanager_p.h +) + qt_internal_add_docs(WaylandClient doc/qtwaylandclient.qdocconf ) diff --git a/src/plugins/platforms/wayland/plugins/shellintegration/xdg-shell/qwaylandxdgshell_p.h b/src/plugins/platforms/wayland/plugins/shellintegration/xdg-shell/qwaylandxdgshell_p.h index 4595940508c..a12311eff74 100644 --- a/src/plugins/platforms/wayland/plugins/shellintegration/xdg-shell/qwaylandxdgshell_p.h +++ b/src/plugins/platforms/wayland/plugins/shellintegration/xdg-shell/qwaylandxdgshell_p.h @@ -119,7 +119,9 @@ private: QWaylandXdgToplevelDecorationV1 *m_decoration = nullptr; QScopedPointer<QWaylandXdgExportedV2> m_exported; QScopedPointer<QWaylandXdgDialogV1> m_xdgDialog; +#ifndef QT_NO_SESSIONMANAGER QScopedPointer<QtWayland::xx_toplevel_session_v1> m_session; +#endif }; class Positioner : public QtWayland::xdg_positioner { diff --git a/src/plugins/platforms/wayland/qwaylandintegration.cpp b/src/plugins/platforms/wayland/qwaylandintegration.cpp index d66710f4e55..4b55aa33a2d 100644 --- a/src/plugins/platforms/wayland/qwaylandintegration.cpp +++ b/src/plugins/platforms/wayland/qwaylandintegration.cpp @@ -530,11 +530,13 @@ QWaylandShellIntegration *QWaylandIntegration::createShellIntegration(const QStr } } +#ifndef QT_NO_SESSIONMANAGER QPlatformSessionManager *QWaylandIntegration::createPlatformSessionManager(const QString &id, const QString &key) const { Q_UNUSED(key); return new QWaylandSessionManager(mDisplay.data(), id); } +#endif void QWaylandIntegration::reset() { diff --git a/src/plugins/platforms/wayland/qwaylandintegration_p.h b/src/plugins/platforms/wayland/qwaylandintegration_p.h index 04a0787d1ef..2b683dc3321 100644 --- a/src/plugins/platforms/wayland/qwaylandintegration_p.h +++ b/src/plugins/platforms/wayland/qwaylandintegration_p.h @@ -132,7 +132,9 @@ private: void initializeShellIntegration(); void initializeInputDeviceIntegration(); QWaylandShellIntegration *createShellIntegration(const QString& interfaceName); +#ifndef QT_NO_SESSIONMANAGER QPlatformSessionManager *createPlatformSessionManager(const QString &id, const QString &key) const override; +#endif const QString mPlatformName; QScopedPointer<QPlatformFontDatabase> mFontDb; diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiaaccessibility.cpp b/src/plugins/platforms/windows/uiautomation/qwindowsuiaaccessibility.cpp index 638d684ce4f..8a494edb4f9 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiaaccessibility.cpp +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiaaccessibility.cpp @@ -142,6 +142,9 @@ void QWindowsUiaAccessibility::notifyAccessibilityUpdate(QAccessibleEvent *event case QAccessible::NameChanged: QWindowsUiaMainProvider::notifyNameChange(event); break; + case QAccessible::RoleChanged: + QWindowsUiaMainProvider::notifyRoleChange(event); + break; case QAccessible::SelectionAdd: QWindowsUiaMainProvider::notifySelectionChange(event); break; diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.cpp b/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.cpp index 5eaba7fbb37..de9c8e5efe1 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.cpp +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.cpp @@ -27,7 +27,6 @@ #include <QtGui/qaccessible.h> #include <QtGui/qguiapplication.h> #include <QtGui/qwindow.h> -#include <qpa/qplatforminputcontextfactory_p.h> #include <QtCore/private/qcomvariant_p.h> #if !defined(Q_CC_BOR) && !defined (Q_CC_GNU) @@ -173,6 +172,18 @@ void QWindowsUiaMainProvider::notifyNameChange(QAccessibleEvent *event) } } +void QWindowsUiaMainProvider::notifyRoleChange(QAccessibleEvent *event) +{ + if (QAccessibleInterface *accessible = event->accessibleInterface()) { + if (auto provider = providerForAccessible(accessible)) { + QComVariant oldVal; + QComVariant newVal{ roleToControlTypeId(accessible->role()) }; + UiaRaiseAutomationPropertyChangedEvent(provider.Get(), UIA_ControlTypePropertyId, + oldVal.get(), newVal.get()); + } + } +} + void QWindowsUiaMainProvider::notifySelectionChange(QAccessibleEvent *event) { if (QAccessibleInterface *accessible = event->accessibleInterface()) { @@ -570,19 +581,7 @@ HRESULT QWindowsUiaMainProvider::GetPropertyValue(PROPERTYID idProp, VARIANT *pR *pRetVal = QComVariant{ UIA_WindowControlTypeId }.release(); } else { // Control type converted from role. - auto controlType = roleToControlTypeId(accessible->role()); - - // The native OSK should be disabled if the Qt OSK is in use, - // or if disabled via application attribute. - static bool imModuleEmpty = QPlatformInputContextFactory::requested().isEmpty(); - bool nativeVKDisabled = QCoreApplication::testAttribute(Qt::AA_DisableNativeVirtualKeyboard); - - // If we want to disable the native OSK auto-showing - // we have to report text fields as non-editable. - if (controlType == UIA_EditControlTypeId && (!imModuleEmpty || nativeVKDisabled)) - controlType = UIA_TextControlTypeId; - - *pRetVal = QComVariant{ controlType }.release(); + *pRetVal = QComVariant{ roleToControlTypeId(accessible->role()) }.release(); } break; case UIA_HelpTextPropertyId: diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.h b/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.h index eae8e26901c..7d89f9ffcad 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.h +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.h @@ -33,6 +33,7 @@ public: static void notifyStateChange(QAccessibleStateChangeEvent *event); static void notifyValueChange(QAccessibleValueChangeEvent *event); static void notifyNameChange(QAccessibleEvent *event); + static void notifyRoleChange(QAccessibleEvent *event); static void notifySelectionChange(QAccessibleEvent *event); static void notifyTextChange(QAccessibleEvent *event); static void raiseNotification(QAccessibleAnnouncementEvent *event); diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiautils.cpp b/src/plugins/platforms/windows/uiautomation/qwindowsuiautils.cpp index 7536ece4dbc..4146a56b226 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiautils.cpp +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiautils.cpp @@ -10,6 +10,8 @@ #include <QtGui/qwindow.h> #include <QtGui/private/qhighdpiscaling_p.h> +#include <qpa/qplatforminputcontextfactory_p.h> + #include <cmath> QT_BEGIN_NAMESPACE @@ -153,7 +155,19 @@ long roleToControlTypeId(QAccessible::Role role) {QAccessible::BlockQuote, UIA_GroupControlTypeId}, }; - return mapping.value(role, UIA_CustomControlTypeId); + long controlType = mapping.value(role, UIA_CustomControlTypeId); + + // The native OSK should be disabled if the Qt OSK is in use, + // or if disabled via application attribute. + static bool imModuleEmpty = QPlatformInputContextFactory::requested().isEmpty(); + bool nativeVKDisabled = QCoreApplication::testAttribute(Qt::AA_DisableNativeVirtualKeyboard); + + // If we want to disable the native OSK auto-showing + // we have to report text fields as non-editable. + if (controlType == UIA_EditControlTypeId && (!imModuleEmpty || nativeVKDisabled)) + controlType = UIA_TextControlTypeId; + + return controlType; } // True if a character can be a separator for a text unit. diff --git a/src/testlib/doc/snippets/code/src_qtestlib_qtestcase.cpp b/src/testlib/doc/snippets/code/src_qtestlib_qtestcase.cpp index f810cf6241f..4f18a2a7ca7 100644 --- a/src/testlib/doc/snippets/code/src_qtestlib_qtestcase.cpp +++ b/src/testlib/doc/snippets/code/src_qtestlib_qtestcase.cpp @@ -3,6 +3,7 @@ #include <QTest> #include <QSqlDatabase> #include <QFontDatabase> +#include <QtCore/qatomicscopedvaluerollback.h> #include <initializer_list> @@ -24,7 +25,22 @@ class MyTestClass : public QObject void addSingleStringRows(); void addMultStringRows(); void addDataRow(); + + private Q_SLOTS: + void initTestCase(); + void defaultTryTimeout(); }; + +void MyTestClass::initTestCase() +{ +//! [set defaultTryTimeout] + using namespace std::chrono_literals; + // Since the atomic itself (defaultTryTimeout) is the only data, + // all reads and stores can be relaxed. + QTest::defaultTryTimeout.store(1s, std::memory_order_relaxed); +//! [set defaultTryTimeout] +} + // dummy void closeAllDatabases() { @@ -230,3 +246,18 @@ const auto restoreDefaultLocale = qScopeGuard([prior = QLocale()]() { //! [36] QLocale::setDefault(QLocale::c()); } + +void MyTestClass::defaultTryTimeout() +{ + using namespace std::chrono_literals; + +//! [rollback defaultTryTimeout] + const auto timeoutRollback = QAtomicScopedValueRollback( + QTest::defaultTryTimeout, 1s, std::memory_order_relaxed); +//! [rollback defaultTryTimeout] + +//! [get defaultTryTimeout] + // Since the atomic itself is all the data, all reads and stores can be relaxed. + QCOMPARE(QTest::defaultTryTimeout.load(std::memory_order_relaxed), 1s); +//! [get defaultTryTimeout] +} diff --git a/src/testlib/qtestcase.h b/src/testlib/qtestcase.h index c924f788106..e9d5ad90bf5 100644 --- a/src/testlib/qtestcase.h +++ b/src/testlib/qtestcase.h @@ -210,7 +210,8 @@ do { \ QVERIFY(expr); \ } while (false) -#define QTRY_VERIFY(expr) QTRY_VERIFY_WITH_TIMEOUT(expr, QTest::Internal::defaultTryTimeout) +#define QTRY_VERIFY(expr) QTRY_VERIFY_WITH_TIMEOUT( \ + expr, QTest::defaultTryTimeout.load(std::memory_order_relaxed)) // Will try to wait for the expression to become true while allowing event processing #define QTRY_VERIFY2_WITH_TIMEOUT(expr, messageExpression, timeout) \ @@ -220,7 +221,8 @@ do { \ } while (false) #define QTRY_VERIFY2(expr, messageExpression) \ - QTRY_VERIFY2_WITH_TIMEOUT(expr, messageExpression, QTest::Internal::defaultTryTimeout) + QTRY_VERIFY2_WITH_TIMEOUT(expr, messageExpression, \ + QTest::defaultTryTimeout.load(std::memory_order_relaxed)) // Will try to wait for the comparison to become successful while allowing event processing #define QTRY_COMPARE_WITH_TIMEOUT(expr, expected, timeout) \ @@ -230,7 +232,8 @@ do { \ } while (false) #define QTRY_COMPARE(expr, expected) \ - QTRY_COMPARE_WITH_TIMEOUT(expr, expected, QTest::Internal::defaultTryTimeout) + QTRY_COMPARE_WITH_TIMEOUT(expr, expected, \ + QTest::defaultTryTimeout.load(std::memory_order_relaxed)) #define QTRY_COMPARE_OP_WITH_TIMEOUT_IMPL(computed, baseline, op, opId, timeout) \ do { \ @@ -243,37 +246,43 @@ do { \ QTRY_COMPARE_OP_WITH_TIMEOUT_IMPL(computed, baseline, ==, Equal, timeout) #define QTRY_COMPARE_EQ(computed, baseline) \ - QTRY_COMPARE_EQ_WITH_TIMEOUT(computed, baseline, QTest::Internal::defaultTryTimeout) + QTRY_COMPARE_EQ_WITH_TIMEOUT(computed, baseline, \ + QTest::defaultTryTimeout.load(std::memory_order_relaxed)) #define QTRY_COMPARE_NE_WITH_TIMEOUT(computed, baseline, timeout) \ QTRY_COMPARE_OP_WITH_TIMEOUT_IMPL(computed, baseline, !=, NotEqual, timeout) #define QTRY_COMPARE_NE(computed, baseline) \ - QTRY_COMPARE_NE_WITH_TIMEOUT(computed, baseline, QTest::Internal::defaultTryTimeout) + QTRY_COMPARE_NE_WITH_TIMEOUT(computed, baseline, \ + QTest::defaultTryTimeout.load(std::memory_order_relaxed)) #define QTRY_COMPARE_LT_WITH_TIMEOUT(computed, baseline, timeout) \ QTRY_COMPARE_OP_WITH_TIMEOUT_IMPL(computed, baseline, <, LessThan, timeout) #define QTRY_COMPARE_LT(computed, baseline) \ - QTRY_COMPARE_LT_WITH_TIMEOUT(computed, baseline, QTest::Internal::defaultTryTimeout) + QTRY_COMPARE_LT_WITH_TIMEOUT(computed, baseline, \ + QTest::defaultTryTimeout.load(std::memory_order_relaxed)) #define QTRY_COMPARE_LE_WITH_TIMEOUT(computed, baseline, timeout) \ QTRY_COMPARE_OP_WITH_TIMEOUT_IMPL(computed, baseline, <=, LessThanOrEqual, timeout) #define QTRY_COMPARE_LE(computed, baseline) \ - QTRY_COMPARE_LE_WITH_TIMEOUT(computed, baseline, QTest::Internal::defaultTryTimeout) + QTRY_COMPARE_LE_WITH_TIMEOUT(computed, baseline, \ + QTest::defaultTryTimeout.load(std::memory_order_relaxed)) #define QTRY_COMPARE_GT_WITH_TIMEOUT(computed, baseline, timeout) \ QTRY_COMPARE_OP_WITH_TIMEOUT_IMPL(computed, baseline, >, GreaterThan, timeout) #define QTRY_COMPARE_GT(computed, baseline) \ - QTRY_COMPARE_GT_WITH_TIMEOUT(computed, baseline, QTest::Internal::defaultTryTimeout) + QTRY_COMPARE_GT_WITH_TIMEOUT(computed, baseline, \ + QTest::defaultTryTimeout.load(std::memory_order_relaxed)) #define QTRY_COMPARE_GE_WITH_TIMEOUT(computed, baseline, timeout) \ QTRY_COMPARE_OP_WITH_TIMEOUT_IMPL(computed, baseline, >=, GreaterThanOrEqual, timeout) #define QTRY_COMPARE_GE(computed, baseline) \ - QTRY_COMPARE_GE_WITH_TIMEOUT(computed, baseline, QTest::Internal::defaultTryTimeout) + QTRY_COMPARE_GE_WITH_TIMEOUT(computed, baseline, \ + QTest::defaultTryTimeout.load(std::memory_order_relaxed)) #define QSKIP_INTERNAL(statement) \ do {\ diff --git a/src/tools/CMakeLists.txt b/src/tools/CMakeLists.txt index 1ff52f8a84f..2f06cbcf67e 100644 --- a/src/tools/CMakeLists.txt +++ b/src/tools/CMakeLists.txt @@ -28,6 +28,10 @@ if(QT_FEATURE_macdeployqt) add_subdirectory(macdeployqt) endif() +if(QT_FEATURE_wasmdeployqt) + add_subdirectory(wasmdeployqt) +endif() + if(QT_FEATURE_windeployqt) add_subdirectory(windeployqt) endif() diff --git a/src/tools/configure.cmake b/src/tools/configure.cmake index 6a9c1b8e3f3..27ea90b89ac 100644 --- a/src/tools/configure.cmake +++ b/src/tools/configure.cmake @@ -18,6 +18,12 @@ qt_feature("macdeployqt" PRIVATE AUTODETECT CMAKE_HOST_APPLE CONDITION MACOS AND QT_FEATURE_thread) +qt_feature("wasmdeployqt" PRIVATE + SECTION "Deployment" + LABEL "WebAssembly deployment tool" + PURPOSE "The WebAssembly deployment tool is designed to automate the process of creating a deployable folder especially for dynamic linking case variant." + CONDITION QT_FEATURE_process) + qt_feature("windeployqt" PRIVATE SECTION "Deployment" LABEL "Windows deployment tool" diff --git a/src/tools/wasmdeployqt/CMakeLists.txt b/src/tools/wasmdeployqt/CMakeLists.txt new file mode 100644 index 00000000000..7305c14c269 --- /dev/null +++ b/src/tools/wasmdeployqt/CMakeLists.txt @@ -0,0 +1,19 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +##################################################################### +## wasmdeployqt Tool: +##################################################################### + +qt_get_tool_target_name(target_name wasmdeployqt) +qt_internal_add_tool(${target_name} + TOOLS_TARGET Core + USER_FACING + INSTALL_VERSIONED_LINK + TARGET_DESCRIPTION "Qt WebAssembly Deployment Tool" + SOURCES + main.cpp wasmbinary.cpp jsontools.cpp + LIBRARIES + Qt::CorePrivate +) +qt_internal_return_unless_building_tools() diff --git a/src/tools/wasmdeployqt/common.h b/src/tools/wasmdeployqt/common.h new file mode 100644 index 00000000000..258d6161e67 --- /dev/null +++ b/src/tools/wasmdeployqt/common.h @@ -0,0 +1,26 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef COMMON_H +#define COMMON_H + +#include <QHash> +#include <QString> + +struct PreloadEntry +{ + QString source; + QString destination; + + bool operator==(const PreloadEntry &other) const + { + return source == other.source && destination == other.destination; + } +}; + +inline uint qHash(const PreloadEntry &key, uint seed = 0) +{ + return qHash(key.source, seed) ^ qHash(key.destination, seed); +} + +#endif diff --git a/src/tools/wasmdeployqt/jsontools.cpp b/src/tools/wasmdeployqt/jsontools.cpp new file mode 100644 index 00000000000..d76f9190b73 --- /dev/null +++ b/src/tools/wasmdeployqt/jsontools.cpp @@ -0,0 +1,101 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include <QDir> +#include <QJsonArray> +#include <QJsonObject> + +#include "jsontools.h" +#include "common.h" + +#include <iostream> +#include <optional> + +namespace JsonTools { + +bool savePreloadFile(QSet<PreloadEntry> preload, QString destFile) +{ + + QJsonArray jsonArray; + for (const PreloadEntry &entry : preload) { + QJsonObject obj; + obj["source"] = entry.source; + obj["destination"] = entry.destination; + jsonArray.append(obj); + } + QJsonDocument doc(jsonArray); + + QFile outFile(destFile); + if (outFile.exists()) { + if (!outFile.remove()) { + std::cout << "ERROR: Failed to delete old file: " << outFile.fileName().toStdString() + << std::endl; + return false; + } + } + if (!outFile.open(QIODevice::WriteOnly | QIODevice::Text)) { + std::cout << "ERROR: Failed to open file for writing:" << outFile.fileName().toStdString() + << std::endl; + return false; + } + if (outFile.write(doc.toJson(QJsonDocument::Indented)) == -1) { + std::cout << "ERROR: Failed writing into file :" << outFile.fileName().toStdString() + << std::endl; + return false; + } + if (!outFile.flush()) { + std::cout << "ERROR: Failed flushing the file :" << outFile.fileName().toStdString() + << std::endl; + return false; + } + outFile.close(); + return true; +} + +std::optional<QSet<PreloadEntry>> getPreloadsFromQmlImportScannerOutput(QString output) +{ + QString qtLibPath = "$QTDIR/lib"; + QString qtQmlPath = "$QTDIR/qml"; + QString qtDeployQmlPath = "/qt/qml"; + QSet<PreloadEntry> res; + auto addImport = [&res](const PreloadEntry &entry) { + // qDebug() << "adding " << entry.source << "" << entry.destination; + res.insert(entry); + }; + + QJsonParseError parseError; + QJsonDocument doc = QJsonDocument::fromJson(output.toUtf8(), &parseError); + + if (parseError.error != QJsonParseError::NoError) { + std::cout << "ERROR: QmlImport JSON parse error: " << parseError.errorString().toStdString() + << std::endl; + return std::nullopt; + } + if (!doc.isArray()) { + std::cout << "ERROR: QmlImport JSON is not an array." << std::endl; + return std::nullopt; + } + + QJsonArray jsonArray = doc.array(); + for (const QJsonValue &value : jsonArray) { + if (value.isObject()) { + QJsonObject obj = value.toObject(); + auto relativePath = obj["relativePath"].toString(); + auto plugin = obj["plugin"].toString(); + if (plugin.isEmpty() || relativePath.isEmpty()) { + continue; + } + auto pluginFilename = "lib" + plugin + ".so"; + addImport(PreloadEntry{ + QDir::cleanPath(qtQmlPath + "/" + relativePath + "/" + pluginFilename), + QDir::cleanPath(qtDeployQmlPath + "/" + relativePath + "/" + pluginFilename) }); + addImport(PreloadEntry{ + QDir::cleanPath(qtQmlPath + "/" + relativePath + "/" + "qmldir"), + QDir::cleanPath(qtDeployQmlPath + "/" + relativePath + "/" + "qmldir") }); + } + } + + return res; +} + +}; // namespace JsonTools diff --git a/src/tools/wasmdeployqt/jsontools.h b/src/tools/wasmdeployqt/jsontools.h new file mode 100644 index 00000000000..a1691a2be8d --- /dev/null +++ b/src/tools/wasmdeployqt/jsontools.h @@ -0,0 +1,19 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef JSONTOOLS_H +#define JSONTOOLS_H + +#include <QFileInfo> +#include <QSet> + +#include "common.h" + +#include <optional> + +namespace JsonTools { +bool savePreloadFile(QSet<PreloadEntry> preload, QString destFile); +std::optional<QSet<PreloadEntry>> getPreloadsFromQmlImportScannerOutput(QString output); +}; // namespace JsonTools + +#endif diff --git a/src/tools/wasmdeployqt/main.cpp b/src/tools/wasmdeployqt/main.cpp new file mode 100644 index 00000000000..3bf2647cfaf --- /dev/null +++ b/src/tools/wasmdeployqt/main.cpp @@ -0,0 +1,417 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "common.h" +#include "jsontools.h" +#include "wasmbinary.h" + +#include <QCoreApplication> +#include <QDir> +#include <QDirListing> +#include <QDirIterator> +#include <QtGlobal> +#include <QLibraryInfo> +#include <QJsonDocument> +#include <QStringList> +#include <QtCore/QCommandLineOption> +#include <QtCore/QCommandLineParser> +#include <QtCore/QProcess> +#include <QQueue> +#include <QMap> +#include <QSet> + +#include <optional> +#include <iostream> +#include <ostream> + +struct Parameters +{ + std::optional<QString> argAppPath; + QString appWasmPath; + std::optional<QDir> qtHostDir; + std::optional<QDir> qtWasmDir; + QList<QDir> libPaths; + std::optional<QDir> qmlRootPath; + + QSet<QString> loadedQtLibraries; +}; + +bool parseArguments(Parameters ¶ms) +{ + QCoreApplication::setApplicationName("wasmdeployqt"); + QCoreApplication::setApplicationVersion("1.0"); + QCommandLineParser parser; + parser.setApplicationDescription( + QStringLiteral("Qt for WebAssembly deployment tool \n\n" + "Example:\n" + "wasmdeployqt app.wasm --qml-root-path=repo/myapp " + "--qt-wasm-dir=/home/user/qt/shared-qt-wasm/bin")); + parser.addHelpOption(); + + QStringList args = QCoreApplication::arguments(); + + parser.addPositionalArgument("app", "Path to the application."); + QCommandLineOption libPathOption("lib-path", "Colon-separated list of library directories.", + "paths"); + parser.addOption(libPathOption); + QCommandLineOption qtWasmDirOption("qt-wasm-dir", "Path to the Qt for WebAssembly directory.", + "dir"); + parser.addOption(qtWasmDirOption); + QCommandLineOption qtHostDirOption("qt-host-dir", "Path to the Qt host directory.", "dir"); + parser.addOption(qtHostDirOption); + QCommandLineOption qmlRootPathOption("qml-root-path", "Root directory for QML files.", "dir"); + parser.addOption(qmlRootPathOption); + parser.process(args); + + const QStringList positionalArgs = parser.positionalArguments(); + if (positionalArgs.size() > 1) { + std::cout << "ERROR: Expected only one positional argument with path to the app. Received: " + << positionalArgs.join(" ").toStdString() << std::endl; + return false; + } + if (!positionalArgs.isEmpty()) { + params.argAppPath = positionalArgs.first(); + } + + if (parser.isSet(libPathOption)) { + QStringList paths = parser.value(libPathOption).split(';', Qt::SkipEmptyParts); + for (const QString &path : paths) { + QDir dir(path); + if (dir.exists()) { + params.libPaths.append(dir); + } else { + std::cout << "ERROR: Directory does not exist: " << path.toStdString() << std::endl; + return false; + } + } + } + if (parser.isSet(qtWasmDirOption)) { + QDir dir(parser.value(qtWasmDirOption)); + if (dir.cdUp() && dir.exists()) + params.qtWasmDir = dir; + else { + std::cout << "ERROR: Directory does not exist: " << dir.absolutePath().toStdString() + << std::endl; + return false; + } + } + if (parser.isSet(qtHostDirOption)) { + QDir dir(parser.value(qtHostDirOption)); + if (dir.cdUp() && dir.exists()) + params.qtHostDir = dir; + else { + std::cout << "ERROR: Directory does not exist: " << dir.absolutePath().toStdString() + << std::endl; + return false; + } + } + if (parser.isSet(qmlRootPathOption)) { + QDir dir(parser.value(qmlRootPathOption)); + if (dir.exists()) { + params.qmlRootPath = dir; + } else { + std::cout << "ERROR: Directory specified for qml-root-path does not exist: " + << dir.absolutePath().toStdString() << std::endl; + return false; + } + } + return true; +} + +std::optional<QString> detectAppName() +{ + QDirIterator it(QDir::currentPath(), QStringList() << "*.html" << "*.wasm" << "*.js", + QDir::NoFilter); + QMap<QString, QSet<QString>> fileGroups; + while (it.hasNext()) { + QFileInfo fileInfo(it.next()); + QString baseName = fileInfo.completeBaseName(); + QString suffix = fileInfo.suffix(); + fileGroups[baseName].insert(suffix); + } + for (auto it = fileGroups.constBegin(); it != fileGroups.constEnd(); ++it) { + const QSet<QString> &extensions = it.value(); + if (extensions.contains("html") && extensions.contains("js") + && extensions.contains("wasm")) { + return it.key(); + } + } + return std::nullopt; +} + +bool verifyPaths(Parameters ¶ms) +{ + if (params.argAppPath) { + QFileInfo fileInfo(*params.argAppPath); + if (!fileInfo.exists()) { + std::cout << "ERROR: Cannot find " << params.argAppPath->toStdString() << std::endl; + std::cout << "Make sure that the path is valid." << std::endl; + return false; + } + params.appWasmPath = fileInfo.absoluteFilePath(); + } else { + auto appName = detectAppName(); + if (!appName) { + std::cout << "ERROR: Cannot find the application in current directory. Specify the " + "path as an argument:" + "wasmdeployqt <path-to-app-wasm-binary>" + << std::endl; + return false; + } + params.appWasmPath = QDir::current().filePath(*appName + ".wasm"); + std::cout << "Automatically detected " << params.appWasmPath.toStdString() << std::endl; + } + if (!params.qtWasmDir) { + std::cout << "ERROR: Please set path to Qt WebAssembly installation as " + "--qt-wasm-dir=<path_to_qt_wasm_bin>" + << std::endl; + return false; + } + if (!params.qtHostDir) { + auto qtHostPath = QLibraryInfo::path(QLibraryInfo::BinariesPath); + if (qtHostPath.length() == 0) { + std::cout << "ERROR: Cannot read Qt host path or detect it from environment. Please " + "pass it explicitly with --qt-host-dir=<path>. " + << std::endl; + } else { + auto qtHostDir = QDir(qtHostPath); + if (!qtHostDir.cdUp()) { + std::cout << "ERROR: Invalid Qt host path: " + << qtHostDir.absolutePath().toStdString() << std::endl; + return false; + } + params.qtHostDir = qtHostDir; + } + } + params.libPaths.push_front(params.qtWasmDir->filePath("lib")); + params.libPaths.push_front(*params.qtWasmDir); + return true; +} + +bool copyFile(QString srcPath, QString destPath) +{ + auto file = QFile(destPath); + if (file.exists()) { + file.remove(); + } + QFileInfo destInfo(destPath); + if (!QDir().mkpath(destInfo.path())) { + std::cout << "ERROR: Cannot create path " << destInfo.path().toStdString() << std::endl; + return false; + } + if (!QFile::copy(srcPath, destPath)) { + + std::cout << "ERROR: Failed to copy " << srcPath.toStdString() << " to " + << destPath.toStdString() << std::endl; + + return false; + } + return true; +} + +bool copyDirectDependencies(QList<QString> dependencies, const Parameters ¶ms) +{ + for (auto &&depFilename : dependencies) { + if (params.loadedQtLibraries.contains(depFilename)) { + continue; // dont copy library that has been already copied + } + + std::optional<QString> libPath; + for (auto &&libDir : params.libPaths) { + auto path = libDir.filePath(depFilename); + QFileInfo file(path); + if (file.exists()) { + libPath = path; + } + } + if (!libPath) { + std::cout << "ERROR: Cannot find required library " << depFilename.toStdString() + << std::endl; + return false; + } + if (!copyFile(*libPath, QDir::current().filePath(depFilename))) + return false; + } + std::cout << "INFO: Succesfully copied direct dependencies." << std::endl; + return true; +} + +QStringList findSoFiles(const QString &directory) +{ + QStringList soFiles; + QDir baseDir(directory); + if (!baseDir.exists()) + return soFiles; + + QDirIterator it(directory, QStringList() << "*.so", QDir::Files, QDirIterator::Subdirectories); + while (it.hasNext()) { + it.next(); + QString absPath = it.filePath(); + QString filePath = baseDir.relativeFilePath(absPath); + soFiles.append(filePath); + } + return soFiles; +} + +bool copyQtLibs(Parameters ¶ms) +{ + Q_ASSERT(params.qtWasmDir); + auto qtLibDir = *params.qtWasmDir; + if (!qtLibDir.cd("lib")) { + std::cout << "ERROR: Cannot find lib directory in Qt installation." << std::endl; + return false; + } + auto qtLibTargetDir = QDir(QDir(QDir::current().filePath("qt")).filePath("lib")); + + auto soFiles = findSoFiles(qtLibDir.absolutePath()); + for (auto &&soFilePath : soFiles) { + auto relativeFilePath = QDir("lib").filePath(soFilePath); + auto srcPath = qtLibDir.absoluteFilePath(soFilePath); + auto destPath = qtLibTargetDir.absoluteFilePath(soFilePath); + if (!copyFile(srcPath, destPath)) + return false; + params.loadedQtLibraries.insert(QFileInfo(srcPath).fileName()); + } + std::cout << "INFO: Succesfully deployed qt lib shared objects." << std::endl; + return true; +} + +bool copyPreloadPlugins(Parameters ¶ms) +{ + Q_ASSERT(params.qtWasmDir); + auto qtPluginsDir = *params.qtWasmDir; + if (!qtPluginsDir.cd("plugins")) { + std::cout << "ERROR: Cannot find plugins directory in Qt installation." << std::endl; + return false; + } + auto qtPluginsTargetDir = QDir(QDir(QDir::current().filePath("qt")).filePath("plugins")); + + // copy files + auto soFiles = findSoFiles(qtPluginsDir.absolutePath()); + for (auto &&soFilePath : soFiles) { + auto relativeFilePath = QDir("plugins").filePath(soFilePath); + params.loadedQtLibraries.insert(QFileInfo(relativeFilePath).fileName()); + auto srcPath = qtPluginsDir.absoluteFilePath(soFilePath); + auto destPath = qtPluginsTargetDir.absoluteFilePath(soFilePath); + if (!copyFile(srcPath, destPath)) + return false; + } + + // qt_plugins.json + QSet<PreloadEntry> preload{ { { "qt.conf" }, { "/qt.conf" } } }; + for (auto &&plugin : soFiles) { + PreloadEntry entry; + entry.source = QDir("$QTDIR").filePath("plugins") + QDir::separator() + + QDir(qtPluginsDir).relativeFilePath(plugin); + entry.destination = "/qt/plugins/" + QDir(qtPluginsTargetDir).relativeFilePath(plugin); + preload.insert(entry); + } + JsonTools::savePreloadFile(preload, QDir::current().filePath("qt_plugins.json")); + + QString qtconfContent = "[Paths]\nPrefix = /qt\n"; + QString filePath = QDir::current().filePath("qt.conf"); + + QFile file(filePath); + if (file.open(QIODevice::WriteOnly | QIODevice::Text)) { + QTextStream out(&file); + out << qtconfContent; + if (!file.flush()) { + std::cout << "ERROR: Failed flushing the file :" << file.fileName().toStdString() + << std::endl; + return false; + } + file.close(); + } else { + std::cout << "ERROR: Failed to write to qt.conf." << std::endl; + return false; + } + std::cout << "INFO: Succesfully deployed qt plugins." << std::endl; + return true; +} + +bool copyPreloadQmlImports(Parameters ¶ms) +{ + Q_ASSERT(params.qtWasmDir); + if (!params.qmlRootPath) { + std::cout << "WARNING: qml-root-path not specified. Skipping generating preloads for QML " + "imports." + << std::endl; + std::cout << "WARNING: This may lead to erronous behaviour if applications requires QML " + "imports." + << std::endl; + QSet<PreloadEntry> preload; + JsonTools::savePreloadFile(preload, QDir::current().filePath("qt_qml_imports.json")); + return true; + } + auto qmlImportScannerPath = params.qtHostDir + ? QDir(params.qtHostDir->filePath("libexec")).filePath("qmlimportscanner") + : "qmlimportscanner"; + QProcess process; + auto qmlImportPath = *params.qtWasmDir; + qmlImportPath.cd("qml"); + if (!qmlImportPath.exists()) { + std::cout << "ERROR: Cannot find qml import path: " + << qmlImportPath.absolutePath().toStdString() << std::endl; + return -1; + } + + QStringList args{ "-rootPath", params.qmlRootPath->absolutePath(), "-importPath", + qmlImportPath.absolutePath() }; + process.start(qmlImportScannerPath, args); + if (!process.waitForFinished()) { + std::cout << "ERROR: Failed to execute qmlImportScanner." << std::endl; + return false; + } + + QString stdoutOutput = process.readAllStandardOutput(); + auto qmlImports = JsonTools::getPreloadsFromQmlImportScannerOutput(stdoutOutput); + if (!qmlImports) { + return false; + } + JsonTools::savePreloadFile(*qmlImports, QDir::current().filePath("qt_qml_imports.json")); + for (const PreloadEntry &import : *qmlImports) { + auto relativePath = import.source; + relativePath.remove("$QTDIR/"); + + auto srcPath = params.qtWasmDir->absoluteFilePath(relativePath); + auto destPath = QDir(QDir::current().filePath("qt")).absoluteFilePath(relativePath); + if (!copyFile(srcPath, destPath)) + return false; + } + std::cout << "INFO: Succesfully deployed qml imports." << std::endl; + return true; +} + +int main(int argc, char **argv) +{ + QCoreApplication app(argc, argv); + Parameters params; + if (!parseArguments(params)) { + return -1; + } + if (!verifyPaths(params)) { + return -1; + } + std::cout << "INFO: Target: " << params.appWasmPath.toStdString() << std::endl; + WasmBinary wasmBinary(params.appWasmPath); + if (wasmBinary.type == WasmBinary::Type::INVALID) { + return -1; + } else if (wasmBinary.type == WasmBinary::Type::STATIC) { + std::cout << "INFO: This is statically linked WebAssembly binary." << std::endl; + std::cout << "INFO: No extra steps required!" << std::endl; + return 0; + } + std::cout << "INFO: Verified as shared module." << std::endl; + + if (!copyQtLibs(params)) + return -1; + if (!copyPreloadPlugins(params)) + return -1; + if (!copyPreloadQmlImports(params)) + return -1; + if (!copyDirectDependencies(wasmBinary.dependencies, params)) + return -1; + + std::cout << "INFO: Deployment done!" << std::endl; + return 0; +} diff --git a/src/tools/wasmdeployqt/wasmbinary.cpp b/src/tools/wasmdeployqt/wasmbinary.cpp new file mode 100644 index 00000000000..1a041c94066 --- /dev/null +++ b/src/tools/wasmdeployqt/wasmbinary.cpp @@ -0,0 +1,91 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "wasmbinary.h" + +#include <QFile> + +#include <iostream> + +WasmBinary::WasmBinary(QString filepath) +{ + QFile file(filepath); + if (!file.open(QIODevice::ReadOnly)) { + std::cout << "ERROR: Cannot open the file " << filepath.toStdString() << std::endl; + std::cout << file.errorString().toStdString() << std::endl; + type = WasmBinary::Type::INVALID; + return; + } + auto bytes = file.readAll(); + if (!parsePreambule(bytes)) { + type = WasmBinary::Type::INVALID; + } +} + +bool WasmBinary::parsePreambule(QByteArrayView data) +{ + const auto preambuleSize = 24; + if (data.size() < preambuleSize) { + std::cout << "ERROR: Preambule of binary shorter than expected!" << std::endl; + return false; + } + uint32_t int32View[6]; + std::memcpy(int32View, data.data(), sizeof(int32View)); + if (int32View[0] != 0x6d736100) { + std::cout << "ERROR: Magic WASM number not found in binary. Binary corrupted?" << std::endl; + return false; + } + if (data[8] != 0) { + type = WasmBinary::Type::STATIC; + return true; + } else { + type = WasmBinary::Type::SHARED; + } + const auto sectionStart = 9; + size_t offset = sectionStart; + auto sectionSize = getLeb(data, offset); + auto sectionEnd = sectionStart + sectionSize; + auto name = getString(data, offset); + if (name != "dylink.0") { + type = WasmBinary::Type::INVALID; + std::cout << "ERROR: dylink.0 was not found in supposedly dynamically linked module" + << std::endl; + return false; + } + + const auto WASM_DYLINK_NEEDED = 0x2; + while (offset < sectionEnd) { + auto subsectionType = data[offset++]; + auto subsectionSize = getLeb(data, offset); + if (subsectionType == WASM_DYLINK_NEEDED) { + auto neededDynlibsCount = getLeb(data, offset); + while (neededDynlibsCount--) { + dependencies.append(getString(data, offset)); + } + } else { + offset += subsectionSize; + } + } + return true; +} + +size_t WasmBinary::getLeb(QByteArrayView data, size_t &offset) +{ + auto ret = 0; + auto mul = 1; + while (true) { + auto byte = data[offset++]; + ret += (byte & 0x7f) * mul; + mul *= 0x80; + if (!(byte & 0x80)) + break; + } + return ret; +} + +QString WasmBinary::getString(QByteArrayView data, size_t &offset) +{ + auto length = getLeb(data, offset); + offset += length; + return QString::fromUtf8(data.sliced(offset - length, length)); +} diff --git a/src/tools/wasmdeployqt/wasmbinary.h b/src/tools/wasmdeployqt/wasmbinary.h new file mode 100644 index 00000000000..c3bb3f0eaa4 --- /dev/null +++ b/src/tools/wasmdeployqt/wasmbinary.h @@ -0,0 +1,24 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef WASMBINARY_H +#define WASMBINARY_H + +#include <QString> +#include <QList> + +class WasmBinary +{ +public: + enum class Type { INVALID, STATIC, SHARED }; + WasmBinary(QString filepath); + Type type; + QList<QString> dependencies; + +private: + bool parsePreambule(QByteArrayView data); + size_t getLeb(QByteArrayView data, size_t &offset); + QString getString(QByteArrayView data, size_t &offset); +}; + +#endif diff --git a/src/widgets/accessible/qaccessiblewidget.cpp b/src/widgets/accessible/qaccessiblewidget.cpp index 5421dc511bb..3d18117dd78 100644 --- a/src/widgets/accessible/qaccessiblewidget.cpp +++ b/src/widgets/accessible/qaccessiblewidget.cpp @@ -374,6 +374,8 @@ QStringList QAccessibleWidget::actionNames() const if (widget()->isEnabled()) { if (widget()->focusPolicy() != Qt::NoFocus) names << setFocusAction(); + if (widget()->contextMenuPolicy() == Qt::ActionsContextMenu && widget()->actions().size() > 0) + names << showMenuAction(); } return names; } @@ -388,6 +390,11 @@ void QAccessibleWidget::doAction(const QString &actionName) if (widget()->isWindow()) widget()->activateWindow(); widget()->setFocus(); + } else if (actionName == showMenuAction()) { + QContextMenuEvent e(QContextMenuEvent::Other, + QPoint(), widget()->mapToGlobal(QPoint()), + QGuiApplication::keyboardModifiers()); + QCoreApplication::sendEvent(widget(), &e); } } diff --git a/src/widgets/accessible/qaccessiblewidgets.cpp b/src/widgets/accessible/qaccessiblewidgets.cpp index 56e9c2a509e..608a99b575d 100644 --- a/src/widgets/accessible/qaccessiblewidgets.cpp +++ b/src/widgets/accessible/qaccessiblewidgets.cpp @@ -564,7 +564,7 @@ QWidget *QAccessibleCalendarWidget::navigationBar() const // If there is a custom title bar widget, that one becomes child 1, after the content 0 // (in that case the buttons are ignored) QAccessibleDockWidget::QAccessibleDockWidget(QWidget *widget) - : QAccessibleWidgetV2(widget, QAccessible::Window) + : QAccessibleWidgetV2(widget) { } @@ -636,6 +636,15 @@ QString QAccessibleDockWidget::text(QAccessible::Text t) const } return QString(); } + +QAccessible::Role QAccessibleDockWidget::role() const +{ + if (dockWidget()->isFloating()) + return QAccessible::Window; + + return QAccessible::Pane; +} + #endif // QT_CONFIG(dockwidget) #ifndef QT_NO_CURSOR diff --git a/src/widgets/accessible/qaccessiblewidgets_p.h b/src/widgets/accessible/qaccessiblewidgets_p.h index 6e3744bf993..c4bc6096c90 100644 --- a/src/widgets/accessible/qaccessiblewidgets_p.h +++ b/src/widgets/accessible/qaccessiblewidgets_p.h @@ -259,6 +259,7 @@ public: int childCount() const override; QRect rect () const override; QString text(QAccessible::Text t) const override; + QAccessible::Role role() const override; QDockWidget *dockWidget() const; protected: diff --git a/src/widgets/kernel/qtestsupport_widgets.cpp b/src/widgets/kernel/qtestsupport_widgets.cpp index ce40ba8c6dd..e0118605308 100644 --- a/src/widgets/kernel/qtestsupport_widgets.cpp +++ b/src/widgets/kernel/qtestsupport_widgets.cpp @@ -79,7 +79,7 @@ bool QTest::qWaitForWindowActive(QWidget *widget, QDeadlineTimer timeout) */ bool QTest::qWaitForWindowActive(QWidget *widget) { - return qWaitForWindowActive(widget, Internal::defaultTryTimeout); + return qWaitForWindowActive(widget, defaultTryTimeout.load(std::memory_order_relaxed)); } /*! @@ -114,7 +114,7 @@ Q_WIDGETS_EXPORT bool QTest::qWaitForWindowFocused(QWidget *widget, QDeadlineTim */ bool QTest::qWaitForWindowFocused(QWidget *widget) { - return qWaitForWindowFocused(widget, Internal::defaultTryTimeout); + return qWaitForWindowFocused(widget, defaultTryTimeout.load(std::memory_order_relaxed)); } /*! @@ -158,7 +158,7 @@ bool QTest::qWaitForWindowExposed(QWidget *widget, QDeadlineTimer timeout) */ bool QTest::qWaitForWindowExposed(QWidget *widget) { - return qWaitForWindowExposed(widget, Internal::defaultTryTimeout); + return qWaitForWindowExposed(widget, defaultTryTimeout.load(std::memory_order_relaxed)); } namespace QTest { diff --git a/src/widgets/widgets/qdockwidget.cpp b/src/widgets/widgets/qdockwidget.cpp index aa4b4441859..bf0205d0081 100644 --- a/src/widgets/widgets/qdockwidget.cpp +++ b/src/widgets/widgets/qdockwidget.cpp @@ -5,6 +5,9 @@ #include <qaction.h> #include <qapplication.h> +#if QT_CONFIG(accessibility) +#include <qaccessible.h> +#endif #include <qdrawutil.h> #include <qevent.h> #include <qfontmetrics.h> @@ -1234,6 +1237,15 @@ void QDockWidgetPrivate::setWindowState(WindowStates states, const QRect &rect) if (floating != wasFloating) { emit q->topLevelChanged(floating); +#if QT_CONFIG(accessibility) + if (QAccessible::isActive()) { + // Accessible role depends on whether QDockWidget is a top level or not, + // see QAccessibleDockWidget::role + QAccessibleEvent roleChangedEvent(q, QAccessible::RoleChanged); + QAccessible::updateAccessibility(&roleChangedEvent); + } +#endif + if (!floating && parent) { QMainWindowLayout *mwlayout = qt_mainwindow_layout_from_dock(q); if (mwlayout) diff --git a/tests/auto/corelib/serialization/qtextstream/tst_qtextstream.cpp b/tests/auto/corelib/serialization/qtextstream/tst_qtextstream.cpp index e41a4760e6a..65714036e2d 100644 --- a/tests/auto/corelib/serialization/qtextstream/tst_qtextstream.cpp +++ b/tests/auto/corelib/serialization/qtextstream/tst_qtextstream.cpp @@ -3009,14 +3009,41 @@ void tst_QTextStream::int_write_with_locale_data() QTest::addColumn<int>("numberFlags"); QTest::addColumn<int>("input"); QTest::addColumn<QString>("output"); + QTest::addColumn<int>("fieldWidth"); + QTest::addColumn<QTextStream::FieldAlignment>("fieldAlignment"); - QTest::newRow("C -123") << QString("C") << 0 << -123 << QString("-123"); - QTest::newRow("C +123") << QString("C") << (int)QTextStream::ForceSign << 123 << QString("+123"); - QTest::newRow("C 12345") << QString("C") << 0 << 12345 << QString("12345"); + const auto alignDefault = QTextStream().fieldAlignment(); + constexpr int forceSign = QTextStream::ForceSign; - QTest::newRow("de_DE -123") << QString("de_DE") << 0 << -123 << QString("-123"); - QTest::newRow("de_DE +123") << QString("de_DE") << (int)QTextStream::ForceSign << 123 << QString("+123"); - QTest::newRow("de_DE 12345") << QString("de_DE") << 0 << 12345 << QString("12.345"); + QTest::newRow("C -123") << u"C"_s << 0 << -123 << u"-123"_s << 0 << alignDefault; + QTest::newRow("C +123") << u"C"_s << forceSign << 123 << u"+123"_s << 0 << alignDefault; + QTest::newRow("C 12345") << u"C"_s << 0 << 12345 << u"12345"_s << 0 << alignDefault; + + QTest::newRow("de_DE -123") << u"de_DE"_s << 0 << -123 << u"-123"_s << 0 << alignDefault; + QTest::newRow("de_DE +123") << u"de_DE"_s << forceSign << 123 << u"+123"_s << 0 << alignDefault; + QTest::newRow("de_DE 12345") << u"de_DE"_s << 0 << 12345 << u"12.345"_s << 0 << alignDefault; + + constexpr auto alignAccountingStyle = QTextStream::FieldAlignment::AlignAccountingStyle; + + { + const QLocale loc("ar_EG"_L1); + // Arabic as spoken in Egypt has a two-code-point negativeSign(): + const auto minus = loc.negativeSign(); + QCOMPARE(minus.size(), 2); + // ditto positiveSign(): + const auto plus = loc.positiveSign(); + QCOMPARE(plus.size(), 2); + + QTest::addRow("ar_EG -123") << u"ar_EG"_s << 0 << -123 + << (minus + u" ١٢٣") + << 10 << alignAccountingStyle; + QTest::newRow("ar_EG +123") << u"ar_EG"_s << forceSign << 123 + << (plus + u" ١٢٣") + << 10 << alignAccountingStyle; + QTest::newRow("ar_EG 12345") << u"ar_EG"_s << 0 << 12345 + << u" ١٢٬٣٤٥"_s + << 10 << alignAccountingStyle; + } } void tst_QTextStream::int_write_with_locale() @@ -3025,12 +3052,18 @@ void tst_QTextStream::int_write_with_locale() QFETCH(int, numberFlags); QFETCH(int, input); QFETCH(QString, output); + QFETCH(const int, fieldWidth); + QFETCH(const QTextStream::FieldAlignment, fieldAlignment); QString result; QTextStream stream(&result); stream.setLocale(QLocale(locale)); + stream.setFieldAlignment(fieldAlignment); if (numberFlags) stream.setNumberFlags(QTextStream::NumberFlags(numberFlags)); + if (fieldWidth) + stream.setFieldWidth(fieldWidth); + QVERIFY(stream << input); QCOMPARE(result, output); } diff --git a/tests/auto/corelib/text/qstring/tst_qstring.cpp b/tests/auto/corelib/text/qstring/tst_qstring.cpp index 2cd1398e7f9..c7f4c35f413 100644 --- a/tests/auto/corelib/text/qstring/tst_qstring.cpp +++ b/tests/auto/corelib/text/qstring/tst_qstring.cpp @@ -22,8 +22,9 @@ #include <qstringmatcher.h> #include <qbytearraymatcher.h> #include <qvariant.h> - #include <qlocale.h> +#include <QtCore/qxptype_traits.h> + #include <locale.h> #include <qhash.h> #include <private/qtools_p.h> @@ -623,6 +624,7 @@ private slots: void fromUcs4(); void toUcs4(); void arg(); + void arg_negative_tests(); void number(); void number_double_data(); void number_double(); @@ -6981,6 +6983,26 @@ void tst_QString::arg() u"\u0660\u0660\u0661\u0662\u0663\u066c\u0664\u0665\u0666\u066c\u0667\u0668\u0669"); // ٠٠١٢٣٬٤٥٦٬٧٨٩ } +template <typename S, typename...Ts> +using arg_compile_test = decltype(std::declval<S>().arg(std::declval<Ts>()...)); +template <typename S, typename...Ts> +constexpr bool arg_compiles_v = qxp::is_detected_v<arg_compile_test, S, Ts...>; + +void tst_QString::arg_negative_tests() +{ + static_assert(!arg_compiles_v<QString&, QObject*>); + // QLatin1StringView::arg() is unconstrained... + // static_assert(!arg_compiles_v<QLatin1StringView&, QObject*>); + + // integral type called like an FP overload: + static_assert(!arg_compiles_v<QString&, int, int, char, int, QChar>); + static_assert(!arg_compiles_v<QString&, long, int, char, int, char16_t>); + + // strong enums don't match: + enum class Strong {}; + static_assert(!arg_compiles_v<QString&, Strong>); +} + void tst_QString::number() { QCOMPARE(QString::number(int(0)), QLatin1String("0")); diff --git a/tests/auto/dbus/qdbuspendingreply/CMakeLists.txt b/tests/auto/dbus/qdbuspendingreply/CMakeLists.txt index 52e11b3dbd9..f68df406d68 100644 --- a/tests/auto/dbus/qdbuspendingreply/CMakeLists.txt +++ b/tests/auto/dbus/qdbuspendingreply/CMakeLists.txt @@ -16,4 +16,5 @@ qt_internal_add_test(tst_qdbuspendingreply tst_qdbuspendingreply.cpp LIBRARIES Qt::DBus + Qt::DBusPrivate ) diff --git a/tests/auto/dbus/qdbuspendingreply/tst_qdbuspendingreply.cpp b/tests/auto/dbus/qdbuspendingreply/tst_qdbuspendingreply.cpp index da25f768d05..361bff0143a 100644 --- a/tests/auto/dbus/qdbuspendingreply/tst_qdbuspendingreply.cpp +++ b/tests/auto/dbus/qdbuspendingreply/tst_qdbuspendingreply.cpp @@ -11,6 +11,8 @@ #include <QDBusAbstractAdaptor> #include <QDBusPendingReply> +#include <private/qdbuspendingcall_p.h> + typedef QMap<int,QString> IntStringMap; Q_DECLARE_METATYPE(IntStringMap) @@ -67,6 +69,14 @@ private slots: void synchronousSimpleTypes(); void errors(); + + void getResultFromAnotherInstance_data(); + void getResultFromAnotherInstance(); + + void moveSemantics(); + + void copyPreservesReplySignature(); + void movePreservesReplySignature(); }; class TypesInterface: public QDBusAbstractAdaptor @@ -561,6 +571,248 @@ void tst_QDBusPendingReply::errors() QCOMPARE(dummystring, QString()); } +void tst_QDBusPendingReply::getResultFromAnotherInstance_data() +{ + QTest::addColumn<bool>("shouldMove"); + + QTest::newRow("copy") << false; + QTest::newRow("move") << true; +} + +void tst_QDBusPendingReply::getResultFromAnotherInstance() +{ + QFETCH(const bool, shouldMove); + + // void + { + QDBusPendingReply<> r = iface->asyncCall("retrieveVoid"); + + QDBusPendingReply<> other; + if (shouldMove) + other = std::move(r); + else + other = r; + other.waitForFinished(); + + QVERIFY(other.isFinished()); + QVERIFY(!other.isError()); + QCOMPARE_EQ(other.count(), 0); + } + + // multiple parameters + { + QDBusPendingReply<int, int> r = iface->asyncCall("retrieveIntInt"); + + QDBusPendingReply<int, int> other; + if (shouldMove) + other = std::move(r); + else + other = r; + other.waitForFinished(); + + QVERIFY(other.isFinished()); + QVERIFY(!other.isError()); + QCOMPARE(other.count(), 2); + + int i1 = 0; + int i2 = 0; + adaptor->retrieveIntInt(i1, i2); + + QCOMPARE_EQ(other.argumentAt<0>(), i1); + QCOMPARE_EQ(other.argumentAt<1>(), i2); + } + + // complex types + { + QDBusPendingReply<IntStringMap> r = iface->asyncCall("retrieveIntStringMap"); + + QDBusPendingReply<IntStringMap> other; + if (shouldMove) + other = std::move(r); + else + other = r; + other.waitForFinished(); + + QVERIFY(other.isFinished()); + QVERIFY(!other.isError()); + QCOMPARE_EQ(other.count(), 1); + QCOMPARE_EQ(other.value(), adaptor->retrieveIntStringMap()); + } +} + +void tst_QDBusPendingReply::moveSemantics() +{ + // void + { + QDBusPendingReply<> r = iface->asyncCall("retrieveVoid"); + r.waitForFinished(); + QVERIFY(r.isFinished()); + + QDBusPendingReply<> copy = r; + + QDBusPendingReply<> other = std::move(copy); + QCOMPARE_EQ(other.d, r.d); + + copy = std::move(other); + QCOMPARE_EQ(copy.d, r.d); + } + + // multiple parameters + { + QDBusPendingReply<int, int> r = iface->asyncCall("retrieveIntInt"); + r.waitForFinished(); + QVERIFY(r.isFinished()); + + QDBusPendingReply<int, int> copy = r; + + QDBusPendingReply<int, int> other = std::move(copy); + QCOMPARE_EQ(other.d, r.d); + + copy = std::move(other); + QCOMPARE_EQ(copy.d, r.d); + } + + // complex types + { + QDBusPendingReply<IntStringMap> r = iface->asyncCall("retrieveIntStringMap"); + r.waitForFinished(); + QVERIFY(r.isFinished()); + + QDBusPendingReply<IntStringMap> copy = r; + + QDBusPendingReply<IntStringMap> other = std::move(copy); + QCOMPARE_EQ(other.d, r.d); + + copy = std::move(other); + QCOMPARE_EQ(copy.d, r.d); + } +} + +void tst_QDBusPendingReply::copyPreservesReplySignature() +{ + // void + { + QDBusPendingCall c = iface->asyncCall("retrieveVoid"); + c.waitForFinished(); + QVERIFY(c.isFinished()); + + // QDBusPendingCall does not initialize reply signature + QVERIFY(c.d->expectedReplySignature.isNull()); + + // copy-construct + { + QDBusPendingReply<> r = c; + QVERIFY(!r.d->expectedReplySignature.isNull()); + QVERIFY(r.d->expectedReplySignature.isEmpty()); + + QDBusPendingReply<> other = r; + QCOMPARE_EQ(other.d->expectedReplySignature, r.d->expectedReplySignature); + } + + // copy-assign + { + QDBusPendingReply<> r; + r = c; + QVERIFY(!r.d->expectedReplySignature.isNull()); + QVERIFY(r.d->expectedReplySignature.isEmpty()); + + QDBusPendingReply<> other; + other = r; + QCOMPARE_EQ(other.d->expectedReplySignature, r.d->expectedReplySignature); + } + } + + // complex types + { + QDBusPendingCall c = iface->asyncCall("retrieveIntStringMap"); + c.waitForFinished(); + QVERIFY(c.isFinished()); + + // QDBusPendingCall does not initialize reply signature + QVERIFY(c.d->expectedReplySignature.isNull()); + + // copy-construct + { + QDBusPendingReply<IntStringMap> r = c; + QVERIFY(!r.d->expectedReplySignature.isEmpty()); + + QDBusPendingReply<IntStringMap> other = r; + QCOMPARE_EQ(other.d->expectedReplySignature, r.d->expectedReplySignature); + } + + // copy-assign + { + QDBusPendingReply<IntStringMap> r; + r = c; + QVERIFY(!r.d->expectedReplySignature.isEmpty()); + + QDBusPendingReply<IntStringMap> other; + other = r; + QCOMPARE_EQ(other.d->expectedReplySignature, r.d->expectedReplySignature); + } + } +} + +void tst_QDBusPendingReply::movePreservesReplySignature() +{ + // void + { + QDBusPendingCall c = iface->asyncCall("retrieveVoid"); + c.waitForFinished(); + QVERIFY(c.isFinished()); + + // QDBusPendingCall does not initialize reply signature + QVERIFY(c.d->expectedReplySignature.isNull()); + + QDBusPendingReply<> r = c; + QVERIFY(!r.d->expectedReplySignature.isNull()); + QVERIFY(r.d->expectedReplySignature.isEmpty()); + + // move-construct + { + QDBusPendingReply<> copy = r; + QDBusPendingReply<> other = std::move(copy); + QCOMPARE_EQ(other.d->expectedReplySignature, r.d->expectedReplySignature); + } + + // move-assign + { + QDBusPendingReply<> copy = r; + QDBusPendingReply<> other; + other = std::move(copy); + QCOMPARE_EQ(other.d->expectedReplySignature, r.d->expectedReplySignature); + } + } + + // complex types + { + QDBusPendingCall c = iface->asyncCall("retrieveIntStringMap"); + c.waitForFinished(); + QVERIFY(c.isFinished()); + + // QDBusPendingCall does not initialize reply signature + QVERIFY(c.d->expectedReplySignature.isNull()); + + QDBusPendingReply<IntStringMap> r = c; + QVERIFY(!r.d->expectedReplySignature.isEmpty()); + + // move-construct + { + QDBusPendingReply<IntStringMap> copy = r; + QDBusPendingReply<IntStringMap> other = std::move(copy); + QCOMPARE_EQ(other.d->expectedReplySignature, r.d->expectedReplySignature); + } + + // move-assign + { + QDBusPendingReply<IntStringMap> copy = r; + QDBusPendingReply<IntStringMap> other; + other = std::move(copy); + QCOMPARE_EQ(other.d->expectedReplySignature, r.d->expectedReplySignature); + } + } +} + QTEST_MAIN(tst_QDBusPendingReply) #include "tst_qdbuspendingreply.moc" 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 571570e370f..9297958ba31 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 @@ -87,7 +87,8 @@ void tst_android_deployment_settings::DeploymentSettings_data() << "permissions" << "[{\"maxSdkVersion\":\"34\",\"minSdkVersion\":\"32\",\"name\":\"PERMISSION_WITH_" "ATTRIBUTES\"},{\"name\":\"PERMISSION_WITHOUT_ATTRIBUTES\"},{\"name\":\"android." - "permission.INTERNET\"},{\"name\":\"android.permission.WRITE_EXTERNAL_STORAGE\"}]"; + "permission.WRITE_EXTERNAL_STORAGE\"},{\"name\":\"android." + "permission.INTERNET\"}]"; } void tst_android_deployment_settings::DeploymentSettings() diff --git a/tests/auto/other/qaccessibility/tst_qaccessibility.cpp b/tests/auto/other/qaccessibility/tst_qaccessibility.cpp index 011852c28e4..95bcc9b20e2 100644 --- a/tests/auto/other/qaccessibility/tst_qaccessibility.cpp +++ b/tests/auto/other/qaccessibility/tst_qaccessibility.cpp @@ -929,6 +929,27 @@ void tst_QAccessibility::actionTest() QCOMPARE(click_count, 1); } QTestAccessibility::clearEvents(); + + { + QCOMPARE(QAccessibleActionInterface::showMenuAction(), QString(QStringLiteral("ShowMenu"))); + + auto widgetHolder = std::make_unique<QWidget>(); + auto widget = widgetHolder.get(); + widget->addAction(new QAction("Foo")); + widget->addAction(new QAction("Bar")); + widget->show(); + + QAccessibleInterface *interface = QAccessible::queryAccessibleInterface(widget); + QVERIFY(interface); + QVERIFY(interface->isValid()); + QAccessibleActionInterface *actions = interface->actionInterface(); + QVERIFY(actions); + + QCOMPARE(actions->actionNames(), QStringList()); + widget->setContextMenuPolicy(Qt::ActionsContextMenu); + QCOMPARE(actions->actionNames(), QStringList(QAccessibleActionInterface::showMenuAction())); + } + QTestAccessibility::clearEvents(); } void tst_QAccessibility::applicationTest() @@ -3701,7 +3722,7 @@ void tst_QAccessibility::dockWidgetTest() // 1 close button // 2 float button QVERIFY(accDock1); - QCOMPARE(accDock1->role(), QAccessible::Window); + QCOMPARE(accDock1->role(), QAccessible::Pane); QCOMPARE(accDock1->text(QAccessible::Name), dock1->windowTitle()); QCOMPARE(accDock1->childCount(), 3); @@ -3731,7 +3752,7 @@ void tst_QAccessibility::dockWidgetTest() QVERIFY(!dock1Float->state().invisible); QVERIFY(accDock2); - QCOMPARE(accDock2->role(), QAccessible::Window); + QCOMPARE(accDock2->role(), QAccessible::Pane); QCOMPARE(accDock2->text(QAccessible::Name), dock2->windowTitle()); QCOMPARE(accDock2->childCount(), 3); @@ -3779,7 +3800,7 @@ void tst_QAccessibility::dockWidgetTest() QAccessibleInterface *accDock3 = accMainWindow->child(4); QVERIFY(accDock3); - QCOMPARE(accDock3->role(), QAccessible::Window); + QCOMPARE(accDock3->role(), QAccessible::Pane); QCOMPARE(accDock3->text(QAccessible::Name), dock3->windowTitle()); QCOMPARE(accDock3->childCount(), 2); QAccessibleInterface *titleWidget = accDock3->child(1); @@ -3788,6 +3809,14 @@ void tst_QAccessibility::dockWidgetTest() QAccessibleInterface *dock3Widget = accDock3->child(0); QCOMPARE(dock3Widget->text(QAccessible::Name), pb3->text()); + // check role is changed to QAccessible::Window when dock window is undocked + // and a corresponding event is sent + QTestAccessibility::clearEvents(); + dock3->setFloating(true); + QCOMPARE(accDock3->role(), QAccessible::Window); + QAccessibleEvent roleChangedEvent(dock3, QAccessible::RoleChanged); + QVERIFY(QTestAccessibility::containsEvent(&roleChangedEvent)); + QTestAccessibility::clearEvents(); #endif // QT_CONFIG(dockwidget) } diff --git a/tests/auto/testlib/selftests/cmptest/tst_cmptest.cpp b/tests/auto/testlib/selftests/cmptest/tst_cmptest.cpp index e10dd6da885..8787cc54bf6 100644 --- a/tests/auto/testlib/selftests/cmptest/tst_cmptest.cpp +++ b/tests/auto/testlib/selftests/cmptest/tst_cmptest.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include <QTest> +#include <QtCore/qatomicscopedvaluerollback.h> #include <QtCore/QCoreApplication> #include <QtCore/QTimer> #ifdef QT_GUI_LIB @@ -15,6 +16,8 @@ #endif #include <QSet> #include <vector> + +using namespace std::chrono_literals; using namespace Qt::StringLiterals; /* XPM test data for QPixmap, QImage tests (use drag cursors as example) */ @@ -105,6 +108,9 @@ public: enum class MyClassEnum { MyClassEnumValue1, MyClassEnumValue2 }; Q_ENUM(MyClassEnum) +private: + void defaultTryTimeoutData(); + private slots: void compare_unregistered_enums(); void compare_registered_enums(); @@ -152,6 +158,10 @@ private slots: void tryVerify(); void tryVerify2(); void verifyExplicitOperatorBool(); + void defaultTryVerifyTimeout_data(); + void defaultTryVerifyTimeout(); + void defaultTryCompareTimeout_data(); + void defaultTryCompareTimeout(); }; enum MyUnregisteredEnum { MyUnregisteredEnumValue1, MyUnregisteredEnumValue2 }; @@ -841,5 +851,56 @@ void tst_Cmptest::verifyExplicitOperatorBool() QVERIFY(!val2); } +void tst_Cmptest::defaultTryTimeoutData() +{ + QTest::addColumn<std::chrono::milliseconds>("timeout"); + QTest::addRow("times-out") << 1ms; + QTest::addRow("ample-time") << 1000ms; +} + +void tst_Cmptest::defaultTryVerifyTimeout_data() +{ + defaultTryTimeoutData(); +} + +void tst_Cmptest::defaultTryVerifyTimeout() +{ + QFETCH(const std::chrono::milliseconds, timeout); + + // Check that the default is what expect. + QCOMPARE(QTest::defaultTryTimeout.load(std::memory_order_relaxed), 5s); + + { + DeferredFlag trueEventually; + const auto innerScope = QAtomicScopedValueRollback(QTest::defaultTryTimeout, timeout, std::memory_order_relaxed); + QEXPECT_FAIL("times-out", "The timeout (std::chrono::milliseconds) is deliberately too short", Continue); + QTRY_VERIFY(trueEventually); + } + + // innerScope has now been destroyed, so the timeout should be back to its default. + QCOMPARE(QTest::defaultTryTimeout.load(std::memory_order_relaxed), 5s); +} + +void tst_Cmptest::defaultTryCompareTimeout_data() +{ + defaultTryTimeoutData(); +} + +void tst_Cmptest::defaultTryCompareTimeout() +{ + QFETCH(const std::chrono::milliseconds, timeout); + + DeferredFlag trueAlready(true); + { + DeferredFlag trueEventually; + const auto innerScope = QAtomicScopedValueRollback(QTest::defaultTryTimeout, timeout, std::memory_order_relaxed); + QEXPECT_FAIL("times-out", "The timeout (std::chrono::milliseconds) is deliberately too short", Continue); + QTRY_COMPARE(trueEventually, trueAlready); + } + + // innerScope has now been destroyed, so the timeout should be back to its default. + QCOMPARE(QTest::defaultTryTimeout.load(std::memory_order_relaxed), 5s); +} + QTEST_MAIN(tst_Cmptest) #include "tst_cmptest.moc" diff --git a/tests/auto/testlib/selftests/expected_cmptest.junitxml b/tests/auto/testlib/selftests/expected_cmptest.junitxml index ce6a1c0c76e..232b86117af 100644 --- a/tests/auto/testlib/selftests/expected_cmptest.junitxml +++ b/tests/auto/testlib/selftests/expected_cmptest.junitxml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="UTF-8" ?> -<testsuite name="tst_Cmptest" timestamp="@TEST_START_TIME@" hostname="@HOSTNAME@" tests="74" failures="51" errors="0" skipped="0" time="@TEST_DURATION@"> +<testsuite name="tst_Cmptest" timestamp="@TEST_START_TIME@" hostname="@HOSTNAME@" tests="78" failures="51" errors="0" skipped="0" time="@TEST_DURATION@"> <properties> <property name="QTestVersion" value="@INSERT_QT_VERSION_HERE@"/> <property name="QtVersion" value="@INSERT_QT_VERSION_HERE@"/> @@ -321,5 +321,17 @@ <failure type="fail" message="'!c' returned FALSE. (Should time out and fail)"/> </testcase> <testcase name="verifyExplicitOperatorBool" classname="tst_Cmptest" time="@TEST_DURATION@"/> + <testcase name="defaultTryVerifyTimeout(times-out)" classname="tst_Cmptest" time="@TEST_DURATION@"> + <system-out> + <![CDATA[The timeout (std::chrono::milliseconds) is deliberately too short]]> + </system-out> + </testcase> + <testcase name="defaultTryVerifyTimeout(ample-time)" classname="tst_Cmptest" time="@TEST_DURATION@"/> + <testcase name="defaultTryCompareTimeout(times-out)" classname="tst_Cmptest" time="@TEST_DURATION@"> + <system-out> + <![CDATA[The timeout (std::chrono::milliseconds) is deliberately too short]]> + </system-out> + </testcase> + <testcase name="defaultTryCompareTimeout(ample-time)" classname="tst_Cmptest" time="@TEST_DURATION@"/> <testcase name="cleanupTestCase" classname="tst_Cmptest" time="@TEST_DURATION@"/> </testsuite> diff --git a/tests/auto/testlib/selftests/expected_cmptest.lightxml b/tests/auto/testlib/selftests/expected_cmptest.lightxml index 9919dde3241..b31bc2aef7d 100644 --- a/tests/auto/testlib/selftests/expected_cmptest.lightxml +++ b/tests/auto/testlib/selftests/expected_cmptest.lightxml @@ -486,6 +486,32 @@ <Incident type="pass" file="" line="0" /> <Duration msecs="0"/> </TestFunction> + <TestFunction name="defaultTryVerifyTimeout"> + <Incident type="xfail" file="qtbase/tests/auto/testlib/selftests/cmptest/tst_cmptest.cpp" line="0"> + <DataTag><![CDATA[times-out]]></DataTag> + <Description><![CDATA[The timeout (std::chrono::milliseconds) is deliberately too short]]></Description> + </Incident> + <Incident type="pass" file="" line="0"> + <DataTag><![CDATA[times-out]]></DataTag> + </Incident> + <Incident type="pass" file="" line="0"> + <DataTag><![CDATA[ample-time]]></DataTag> + </Incident> + <Duration msecs="0"/> + </TestFunction> + <TestFunction name="defaultTryCompareTimeout"> + <Incident type="xfail" file="qtbase/tests/auto/testlib/selftests/cmptest/tst_cmptest.cpp" line="0"> + <DataTag><![CDATA[times-out]]></DataTag> + <Description><![CDATA[The timeout (std::chrono::milliseconds) is deliberately too short]]></Description> + </Incident> + <Incident type="pass" file="" line="0"> + <DataTag><![CDATA[times-out]]></DataTag> + </Incident> + <Incident type="pass" file="" line="0"> + <DataTag><![CDATA[ample-time]]></DataTag> + </Incident> + <Duration msecs="0"/> + </TestFunction> <TestFunction name="cleanupTestCase"> <Incident type="pass" file="" line="0" /> <Duration msecs="0"/> diff --git a/tests/auto/testlib/selftests/expected_cmptest.tap b/tests/auto/testlib/selftests/expected_cmptest.tap index 77118713717..180567f2112 100644 --- a/tests/auto/testlib/selftests/expected_cmptest.tap +++ b/tests/auto/testlib/selftests/expected_cmptest.tap @@ -719,8 +719,30 @@ not ok 72 - tryVerify2() line: 0 ... ok 73 - verifyExplicitOperatorBool() -ok 74 - cleanupTestCase() -1..74 -# tests 74 -# pass 23 +not ok 74 - defaultTryVerifyTimeout(times-out) # TODO The timeout (std::chrono::milliseconds) is deliberately too short + --- + extensions: + messages: + - severity: xfail + message: The timeout (std::chrono::milliseconds) is deliberately too short + at: tst_Cmptest::defaultTryVerifyTimeout() (qtbase/tests/auto/testlib/selftests/cmptest/tst_cmptest.cpp:0) + file: qtbase/tests/auto/testlib/selftests/cmptest/tst_cmptest.cpp + line: 0 + ... +ok 75 - defaultTryVerifyTimeout(ample-time) +not ok 76 - defaultTryCompareTimeout(times-out) # TODO The timeout (std::chrono::milliseconds) is deliberately too short + --- + extensions: + messages: + - severity: xfail + message: The timeout (std::chrono::milliseconds) is deliberately too short + at: tst_Cmptest::defaultTryCompareTimeout() (qtbase/tests/auto/testlib/selftests/cmptest/tst_cmptest.cpp:0) + file: qtbase/tests/auto/testlib/selftests/cmptest/tst_cmptest.cpp + line: 0 + ... +ok 77 - defaultTryCompareTimeout(ample-time) +ok 78 - cleanupTestCase() +1..78 +# tests 78 +# pass 27 # fail 51 diff --git a/tests/auto/testlib/selftests/expected_cmptest.teamcity b/tests/auto/testlib/selftests/expected_cmptest.teamcity index c48262e0376..f88e44f3ee2 100644 --- a/tests/auto/testlib/selftests/expected_cmptest.teamcity +++ b/tests/auto/testlib/selftests/expected_cmptest.teamcity @@ -216,6 +216,16 @@ ##teamcity[testFinished name='tryVerify2()' flowId='tst_Cmptest'] ##teamcity[testStarted name='verifyExplicitOperatorBool()' flowId='tst_Cmptest'] ##teamcity[testFinished name='verifyExplicitOperatorBool()' flowId='tst_Cmptest'] +##teamcity[testStarted name='defaultTryVerifyTimeout(times-out)' flowId='tst_Cmptest'] +##teamcity[testStdOut name='defaultTryVerifyTimeout(times-out)' out='XFAIL |[Loc: qtbase/tests/auto/testlib/selftests/cmptest/tst_cmptest.cpp(0)|]: The timeout (std::chrono::milliseconds) is deliberately too short' flowId='tst_Cmptest'] +##teamcity[testFinished name='defaultTryVerifyTimeout(times-out)' flowId='tst_Cmptest'] +##teamcity[testStarted name='defaultTryVerifyTimeout(ample-time)' flowId='tst_Cmptest'] +##teamcity[testFinished name='defaultTryVerifyTimeout(ample-time)' flowId='tst_Cmptest'] +##teamcity[testStarted name='defaultTryCompareTimeout(times-out)' flowId='tst_Cmptest'] +##teamcity[testStdOut name='defaultTryCompareTimeout(times-out)' out='XFAIL |[Loc: qtbase/tests/auto/testlib/selftests/cmptest/tst_cmptest.cpp(0)|]: The timeout (std::chrono::milliseconds) is deliberately too short' flowId='tst_Cmptest'] +##teamcity[testFinished name='defaultTryCompareTimeout(times-out)' flowId='tst_Cmptest'] +##teamcity[testStarted name='defaultTryCompareTimeout(ample-time)' flowId='tst_Cmptest'] +##teamcity[testFinished name='defaultTryCompareTimeout(ample-time)' flowId='tst_Cmptest'] ##teamcity[testStarted name='cleanupTestCase()' flowId='tst_Cmptest'] ##teamcity[testFinished name='cleanupTestCase()' flowId='tst_Cmptest'] ##teamcity[testSuiteFinished name='tst_Cmptest' flowId='tst_Cmptest'] diff --git a/tests/auto/testlib/selftests/expected_cmptest.txt b/tests/auto/testlib/selftests/expected_cmptest.txt index efb305654f4..4c4b340a5ca 100644 --- a/tests/auto/testlib/selftests/expected_cmptest.txt +++ b/tests/auto/testlib/selftests/expected_cmptest.txt @@ -252,6 +252,14 @@ FAIL! : tst_Cmptest::tryVerify() '!c' returned FALSE. () FAIL! : tst_Cmptest::tryVerify2() '!c' returned FALSE. (Should time out and fail) Loc: [qtbase/tests/auto/testlib/selftests/cmptest/tst_cmptest.cpp(0)] PASS : tst_Cmptest::verifyExplicitOperatorBool() +XFAIL : tst_Cmptest::defaultTryVerifyTimeout(times-out) The timeout (std::chrono::milliseconds) is deliberately too short + Loc: [qtbase/tests/auto/testlib/selftests/cmptest/tst_cmptest.cpp(0)] +PASS : tst_Cmptest::defaultTryVerifyTimeout(times-out) +PASS : tst_Cmptest::defaultTryVerifyTimeout(ample-time) +XFAIL : tst_Cmptest::defaultTryCompareTimeout(times-out) The timeout (std::chrono::milliseconds) is deliberately too short + Loc: [qtbase/tests/auto/testlib/selftests/cmptest/tst_cmptest.cpp(0)] +PASS : tst_Cmptest::defaultTryCompareTimeout(times-out) +PASS : tst_Cmptest::defaultTryCompareTimeout(ample-time) PASS : tst_Cmptest::cleanupTestCase() -Totals: 23 passed, 51 failed, 0 skipped, 0 blacklisted, 0ms +Totals: 27 passed, 51 failed, 0 skipped, 0 blacklisted, 0ms ********* Finished testing of tst_Cmptest ********* diff --git a/tests/auto/testlib/selftests/expected_cmptest.xml b/tests/auto/testlib/selftests/expected_cmptest.xml index f6ed03b0e9f..5ec93c5ce7e 100644 --- a/tests/auto/testlib/selftests/expected_cmptest.xml +++ b/tests/auto/testlib/selftests/expected_cmptest.xml @@ -488,6 +488,32 @@ <Incident type="pass" file="" line="0" /> <Duration msecs="0"/> </TestFunction> + <TestFunction name="defaultTryVerifyTimeout"> + <Incident type="xfail" file="qtbase/tests/auto/testlib/selftests/cmptest/tst_cmptest.cpp" line="0"> + <DataTag><![CDATA[times-out]]></DataTag> + <Description><![CDATA[The timeout (std::chrono::milliseconds) is deliberately too short]]></Description> + </Incident> + <Incident type="pass" file="" line="0"> + <DataTag><![CDATA[times-out]]></DataTag> + </Incident> + <Incident type="pass" file="" line="0"> + <DataTag><![CDATA[ample-time]]></DataTag> + </Incident> + <Duration msecs="0"/> + </TestFunction> + <TestFunction name="defaultTryCompareTimeout"> + <Incident type="xfail" file="qtbase/tests/auto/testlib/selftests/cmptest/tst_cmptest.cpp" line="0"> + <DataTag><![CDATA[times-out]]></DataTag> + <Description><![CDATA[The timeout (std::chrono::milliseconds) is deliberately too short]]></Description> + </Incident> + <Incident type="pass" file="" line="0"> + <DataTag><![CDATA[times-out]]></DataTag> + </Incident> + <Incident type="pass" file="" line="0"> + <DataTag><![CDATA[ample-time]]></DataTag> + </Incident> + <Duration msecs="0"/> + </TestFunction> <TestFunction name="cleanupTestCase"> <Incident type="pass" file="" line="0" /> <Duration msecs="0"/> diff --git a/tests/manual/wasm/eventloop/suspendresumecontrol_auto/main.cpp b/tests/manual/wasm/eventloop/suspendresumecontrol_auto/main.cpp index 59f1f00cc59..1fd6e0a0c4f 100644 --- a/tests/manual/wasm/eventloop/suspendresumecontrol_auto/main.cpp +++ b/tests/manual/wasm/eventloop/suspendresumecontrol_auto/main.cpp @@ -7,7 +7,7 @@ using namespace emscripten; -const int timerTimeout = 10; +const std::chrono::milliseconds timerTimeout{10}; // Test QWasmSuspendResumeControl suspend/resume and event processing, // via QWasmTimer native timer events. diff --git a/util/wasm/batchedtestrunner/emrunadapter.js b/util/wasm/batchedtestrunner/emrunadapter.js index 5b4284e18ff..73f575f9616 100644 --- a/util/wasm/batchedtestrunner/emrunadapter.js +++ b/util/wasm/batchedtestrunner/emrunadapter.js @@ -37,6 +37,7 @@ export class EmrunCommunication { // method increments the output index by 1. postOutput(output) { + output += "\n"; if (this.#nextOutputBatch) { this.#nextOutputBatch += output; } else { @@ -45,9 +46,9 @@ export class EmrunCommunication { { window.setTimeout(() => { - const toSend = this.#nextOutputBatch; + const toSend = this.#nextOutputBatch.replace(/\n$/, ''); this.#nextOutputBatch = null; - this.#post(`^out^${this.#indexOfMessage++}^${toSend}$`) + this.#post(`^out^${this.#indexOfMessage++}^${toSend}`) .finally(resolve); }, EmrunCommunication.#BATCHING_DELAY); }); diff --git a/util/wasm/preload/generate_default_preloads.sh.in b/util/wasm/preload/generate_default_preloads.sh.in deleted file mode 100644 index b38d308fa45..00000000000 --- a/util/wasm/preload/generate_default_preloads.sh.in +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash - -TARGET_DIR="@TARGET_DIR@" -SOURCE_DIR="@SOURCE_DIR@" -QT_HOST_DIR="@QT_HOST_DIR@" -QT_WASM_DIR="@QT_WASM_DIR@" -QT_INSTALL_DIR="@QT_INSTALL_DIR@" - -python3 \ - "$QT_WASM_DIR/libexec/preload_qt_plugins.py" \ - "$QT_INSTALL_DIR" \ - "$TARGET_DIR" - -python3 \ - "$QT_WASM_DIR/libexec/preload_qml_imports.py" \ - "$SOURCE_DIR" \ - "$QT_HOST_DIR" \ - "$QT_INSTALL_DIR" \ - "$TARGET_DIR" diff --git a/util/wasm/preload/preload_qml_imports.py b/util/wasm/preload/preload_qml_imports.py deleted file mode 100755 index b78ef5ee744..00000000000 --- a/util/wasm/preload/preload_qml_imports.py +++ /dev/null @@ -1,98 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (C) 2023 The Qt Company Ltd. -# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 - -import os -import sys -import subprocess -import json - -# Paths to shared libraries and qml imports on the Qt installation on the web server. -# "$QTDIR" is replaced by qtloader.js at load time (defaults to "qt"), and makes -# possible to relocate the application build relative to the Qt build on the web server. -qt_lib_path = "$QTDIR/lib" -qt_qml_path = "$QTDIR/qml" - -# Path to QML imports on the in-memory file system provided by Emscripten. This script emits -# preload commands which copies QML imports to this directory. In addition, preload_qt_plugins.py -# creates (and preloads) a qt.conf file which makes Qt load QML plugins from this location. -qt_deploy_qml_path = "/qt/qml" - - -def preload_file(source, destination): - preload_files.append({"source": source, "destination": destination}) - - -def extract_preload_files_from_imports(imports): - libraries = [] - for qml_import in imports: - try: - relative_path = qml_import["relativePath"] - plugin = qml_import["plugin"] - - # plugin .so - plugin_filename = "lib" + plugin + ".so" - so_plugin_source_path = os.path.join( - qt_qml_path, relative_path, plugin_filename - ) - so_plugin_destination_path = os.path.join( - qt_deploy_qml_path, relative_path, plugin_filename - ) - - preload_file(so_plugin_source_path, so_plugin_destination_path) - so_plugin_qt_install_path = os.path.join( - qt_wasm_path, "qml", relative_path, plugin_filename - ) - - # qmldir file - qmldir_source_path = os.path.join(qt_qml_path, relative_path, "qmldir") - qmldir_destination_path = os.path.join( - qt_deploy_qml_path, relative_path, "qmldir" - ) - preload_file(qmldir_source_path, qmldir_destination_path) - except Exception as e: - continue - return libraries - - -if __name__ == "__main__": - if len(sys.argv) != 5: - print("Usage: python preload_qml_imports.py <qml-source-path> <qt-host-path> <qt-wasm-path> <output-dir>") - sys.exit(1) - - qml_source_path = sys.argv[1] - qt_host_path = sys.argv[2] - qt_wasm_path = sys.argv[3] - output_dir = sys.argv[4] - - qml_import_path = os.path.join(qt_wasm_path, "qml") - qmlimportsscanner_path = os.path.join(qt_host_path, "libexec/qmlimportscanner") - - command = [qmlimportsscanner_path, "-rootPath", qml_source_path, "-importPath", qml_import_path] - result = subprocess.run(command, stdout=subprocess.PIPE) - imports = json.loads(result.stdout) - - preload_files = [] - libraries = extract_preload_files_from_imports(imports) - - # Deploy plugin dependencies, that is, shared libraries used by the plugins. - # Skip some of the obvious libraries which will be - skip_libraries = [ - "libQt6Core.so", - "libQt6Gui.so", - "libQt6Quick.so", - "libQt6Qml.so" "libQt6Network.so", - "libQt6OpenGL.so", - ] - - libraries = set(libraries) - set(skip_libraries) - for library in libraries: - source = os.path.join(qt_lib_path, library) - # Emscripten looks for shared libraries on "/", shared libraries - # most be deployed there instead of at /qt/lib - destination = os.path.join("/", library) - preload_file(source, destination) - - with open(f"{output_dir}/qt_qml_imports.json", "w") as f: - f.write(json.dumps(preload_files, indent=2)) - diff --git a/util/wasm/preload/preload_qt_plugins.py b/util/wasm/preload/preload_qt_plugins.py deleted file mode 100755 index 4b9b3683a70..00000000000 --- a/util/wasm/preload/preload_qt_plugins.py +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (C) 2023 The Qt Company Ltd. -# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 - -import os -import sys -import json - -# Path to plugins on the Qt installation on the web server. "$QTPATH" is replaced by qtloader.js -# at load time (defaults to "qt"), which makes it possible to relocate the application build relative -# to the Qt build on the web server. -qt_plugins_path = "$QTDIR/plugins" - -# Path to plugins on the in-memory file system provided by Emscripten. This script emits -# preload commands which copies plugins to this directory. -qt_deploy_plugins_path = "/qt/plugins" - - -def find_so_files(directory): - so_files = [] - for root, dirs, files in os.walk(directory): - for file in files: - if file.endswith(".so"): - relative_path = os.path.relpath(os.path.join(root, file), directory) - so_files.append(relative_path) - return so_files - - -if __name__ == "__main__": - if len(sys.argv) != 3: - print("Usage: python preload_qt_plugins.py <qt-wasm-path> <output-dir>") - sys.exit(1) - - qt_wasm_path = sys.argv[1] - output_dir = sys.argv[2] - - # preload all plugins - plugins = find_so_files(os.path.join(qt_wasm_path, "plugins")) - preload = [ - { - "source": os.path.join(qt_plugins_path, plugin), - "destination": os.path.join(qt_deploy_plugins_path, plugin), - } - for plugin in plugins - ] - - # Create and preload qt.conf which will tell Qt to look for plugins - # and QML imports in /qt/plugins and /qt/qml. The qt.conf file is - # written to the current directory. - qtconf = "[Paths]\nPrefix = /qt\n" - with open(f"{output_dir}/qt.conf", "w") as f: - f.write(qtconf) - preload.append({"source": "qt.conf", "destination": "/qt.conf"}) - - with open(f"{output_dir}/qt_plugins.json", "w") as f: - f.write(json.dumps(preload, indent=2)) - |