summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--cmake/QtBuildRepoHelpers.cmake35
-rw-r--r--cmake/QtFeature.cmake5
-rw-r--r--cmake/QtPostProcessHelpers.cmake11
-rw-r--r--cmake/QtStandaloneTestsConfig.cmake.in10
-rw-r--r--cmake/QtTargetHelpers.cmake226
-rw-r--r--doc/global/html-header-offline.qdocconf20
-rw-r--r--doc/global/qt-html-templates-offline.qdocconf43
-rw-r--r--examples/corelib/bindableproperties/doc/src/bindableproperties.qdoc1
-rw-r--r--examples/corelib/ipc/doc/src/localfortuneclient.qdoc1
-rw-r--r--examples/corelib/ipc/doc/src/localfortuneserver.qdoc1
-rw-r--r--examples/corelib/ipc/doc/src/sharedmemory.qdoc1
-rw-r--r--examples/corelib/mimetypes/doc/src/mimetypebrowser.qdoc1
-rw-r--r--examples/corelib/platform/androidnotifier/doc/src/androidnotifier-example.qdoc1
-rw-r--r--examples/corelib/serialization/cbordump/doc/src/cbordump.qdoc1
-rw-r--r--examples/corelib/serialization/convert/doc/src/convert.qdoc1
-rw-r--r--examples/corelib/serialization/savegame/doc/src/savegame.qdoc1
-rw-r--r--examples/corelib/serialization/streambookmarks/doc/src/qxmlstreambookmarks.qdoc1
-rw-r--r--examples/corelib/threads/doc/src/mandelbrot.qdoc1
-rw-r--r--examples/corelib/threads/doc/src/queuedcustomtype.qdoc1
-rw-r--r--examples/corelib/threads/doc/src/semaphores.qdoc1
-rw-r--r--examples/corelib/threads/doc/src/waitconditions.qdoc1
-rw-r--r--examples/corelib/time/calendarbackendplugin/doc/src/calendarbackendplugin.qdoc1
-rw-r--r--examples/corelib/tools/doc/src/contiguouscache.qdoc1
-rw-r--r--src/3rdparty/libpng/ANNOUNCE47
-rw-r--r--src/3rdparty/libpng/CHANGES26
-rw-r--r--src/3rdparty/libpng/README2
-rw-r--r--src/3rdparty/libpng/libpng-manual.txt15
-rw-r--r--src/3rdparty/libpng/png.c20
-rw-r--r--src/3rdparty/libpng/png.h14
-rw-r--r--src/3rdparty/libpng/pngconf.h2
-rw-r--r--src/3rdparty/libpng/pngdebug.h3
-rw-r--r--src/3rdparty/libpng/pngerror.c138
-rw-r--r--src/3rdparty/libpng/pnglibconf.h2
-rw-r--r--src/3rdparty/libpng/pngpriv.h10
-rw-r--r--src/3rdparty/libpng/pngread.c75
-rw-r--r--src/3rdparty/libpng/pngrtran.c114
-rw-r--r--src/3rdparty/libpng/pngstruct.h1
-rw-r--r--src/3rdparty/libpng/pngwrite.c3
-rw-r--r--src/3rdparty/libpng/qt_attribution.json4
-rw-r--r--src/corelib/CMakeLists.txt2
-rw-r--r--src/corelib/animation/qabstractanimation.cpp4
-rw-r--r--src/corelib/animation/qabstractanimation_p.h8
-rw-r--r--src/corelib/doc/qtcore.qdocconf7
-rw-r--r--src/corelib/doc/snippets/CMakeLists.txt2
-rw-r--r--src/corelib/doc/src/qtcore.qdoc9
-rw-r--r--src/corelib/io/qfile.cpp13
-rw-r--r--src/corelib/io/qfilesystemengine.cpp8
-rw-r--r--src/corelib/io/qfilesystemengine_p.h2
-rw-r--r--src/corelib/io/qfsfileengine_p.h2
-rw-r--r--src/corelib/kernel/qmetacontainer.cpp8
-rw-r--r--src/corelib/kernel/qobject_impl.h4
-rw-r--r--src/corelib/platform/darwin/qdarwinsecurityscopedfileengine.mm552
-rw-r--r--src/corelib/platform/darwin/qdarwinsecurityscopedfileengine_p.h29
-rw-r--r--src/corelib/platform/wasm/qstdweb.cpp51
-rw-r--r--src/corelib/platform/wasm/qstdweb_p.h21
-rw-r--r--src/corelib/platform/wasm/qwasmanimationdriver.cpp129
-rw-r--r--src/corelib/platform/wasm/qwasmanimationdriver_p.h54
-rw-r--r--src/corelib/platform/wasm/qwasmsuspendresumecontrol.cpp186
-rw-r--r--src/corelib/platform/wasm/qwasmsuspendresumecontrol_p.h38
-rw-r--r--src/corelib/text/qlocale_icu.cpp12
-rw-r--r--src/corelib/tools/qcryptographichash.cpp2
-rw-r--r--src/gui/configure.cmake15
-rw-r--r--src/gui/image/qpaintengine_pic.cpp17
-rw-r--r--src/gui/image/qpicture.cpp6
-rw-r--r--src/gui/image/qpicture_p.h1
-rw-r--r--src/gui/image/qppmhandler.cpp69
-rw-r--r--src/gui/painting/qcolorspace.cpp4
-rw-r--r--src/gui/painting/qpainter.cpp17
-rw-r--r--src/network/access/http2/http2frames.cpp3
-rw-r--r--src/network/access/qhttp2connection.cpp118
-rw-r--r--src/network/access/qhttp2connection_p.h6
-rw-r--r--src/network/access/qhttpnetworkconnectionchannel.cpp14
-rw-r--r--src/network/socket/qabstractsocket.cpp39
-rw-r--r--src/network/socket/qabstractsocket.h5
-rw-r--r--src/network/socket/qabstractsocketengine_p.h3
-rw-r--r--src/network/socket/qnativesocketengine_unix.cpp24
-rw-r--r--src/network/socket/qnativesocketengine_win.cpp21
-rw-r--r--src/plugins/platforms/cocoa/qcocoaapplicationdelegate.mm66
-rw-r--r--src/plugins/platforms/cocoa/qcocoafiledialoghelper.mm34
-rw-r--r--src/plugins/platforms/cocoa/qnsview_mouse.mm3
-rw-r--r--src/plugins/platforms/direct2d/CMakeLists.txt1
-rw-r--r--src/plugins/platforms/ios/qiosapplicationdelegate.mm4
-rw-r--r--src/plugins/platforms/ios/qiosdocumentpickercontroller.mm62
-rw-r--r--src/plugins/platforms/ios/qiosfiledialog.mm10
-rw-r--r--src/plugins/platforms/wasm/qwasmcompositor.cpp43
-rw-r--r--src/plugins/platforms/wasm/qwasmcompositor.h16
-rw-r--r--src/plugins/platforms/wasm/qwasminputcontext.cpp156
-rw-r--r--src/plugins/platforms/wasm/qwasminputcontext.h5
-rw-r--r--src/plugins/platforms/wasm/qwasmwindow.cpp6
-rw-r--r--src/plugins/platforms/wayland/qwaylandinputcontext.cpp10
-rw-r--r--src/plugins/platforms/windows/CMakeLists.txt1
-rw-r--r--src/plugins/platforms/windows/qwindowscontext.cpp2
-rw-r--r--src/plugins/platforms/windows/qwindowsscreen.cpp2
-rw-r--r--src/plugins/platforms/windows/qwindowssystemtrayicon.cpp6
-rw-r--r--src/plugins/platforms/windows/qwindowstheme.cpp2
-rw-r--r--src/plugins/platforms/windows/qwindowswindow.cpp10
-rw-r--r--src/plugins/platforms/windows/qwindowswindowclassdescription.cpp78
-rw-r--r--src/plugins/platforms/windows/qwindowswindowclassdescription.h30
-rw-r--r--src/plugins/platforms/windows/qwindowswindowclassregistry.cpp114
-rw-r--r--src/plugins/platforms/windows/qwindowswindowclassregistry.h10
-rw-r--r--src/plugins/styles/modernwindows/qwindows11style.cpp41
-rw-r--r--src/widgets/widgets/qcalendarwidget.h1
-rw-r--r--src/widgets/widgets/qtabbar.cpp14
-rw-r--r--src/widgets/widgets/qtoolbutton.h1
-rw-r--r--src/widgets/widgets/qwidgetanimator.cpp5
-rw-r--r--tests/auto/corelib/kernel/qmetaobjectbuilder/tst_qmetaobjectbuilder.cpp6
-rw-r--r--tests/auto/corelib/kernel/qvariant/tst_qvariant.cpp21
-rw-r--r--tests/auto/corelib/platform/android/tst_android.cpp11
-rw-r--r--tests/auto/gui/image/qimagereader/images/image16.pgm260
-rw-r--r--tests/auto/gui/image/qimagereader/tst_qimagereader.cpp1
-rw-r--r--tests/auto/gui/kernel/qguieventdispatcher/BLACKLIST2
-rw-r--r--tests/auto/gui/kernel/qwindow/BLACKLIST1
-rw-r--r--tests/auto/network/access/qhttp2connection/tst_qhttp2connection.cpp110
-rw-r--r--tests/auto/tools/moc/allmocs_baseline_in.json4
-rw-r--r--tests/auto/tools/moc/qtbug-35657-gadget.h2
-rw-r--r--tests/auto/tools/moc/related-metaobjects-in-namespaces.h2
-rw-r--r--tests/auto/tools/moc/related-metaobjects-name-conflict.h4
-rw-r--r--tests/auto/widgets/kernel/qwidget/BLACKLIST3
-rw-r--r--tests/auto/widgets/widgets/qtabbar/tst_qtabbar.cpp25
-rw-r--r--tests/manual/sandboxed_file_access/CMakeLists.txt71
-rw-r--r--tests/manual/sandboxed_file_access/Info.plist.ios90
-rw-r--r--tests/manual/sandboxed_file_access/Info.plist.macos68
-rw-r--r--tests/manual/sandboxed_file_access/app.entitlements10
-rw-r--r--tests/manual/sandboxed_file_access/tst_sandboxed_file_access.cpp422
-rw-r--r--tests/manual/wasm/qtwasmtestlib/qtwasmtestlib.cpp8
125 files changed, 3421 insertions, 780 deletions
diff --git a/cmake/QtBuildRepoHelpers.cmake b/cmake/QtBuildRepoHelpers.cmake
index 1429448c2f8..5961424c38b 100644
--- a/cmake/QtBuildRepoHelpers.cmake
+++ b/cmake/QtBuildRepoHelpers.cmake
@@ -724,6 +724,41 @@ macro(qt_internal_find_standalone_test_config_file)
endif()
endmacro()
+# Used inside the standalone parts config file to find all requested Qt module packages.
+# standalone_parts_args_var_name should be the var name in the outer scope that contains
+# all the arguments for this function.
+macro(qt_internal_find_standalone_parts_qt_packages standalone_parts_args_var_name)
+ set(__standalone_parts_opt_args "")
+ set(__standalone_parts_single_args "")
+ set(__standalone_parts_multi_args
+ QT_MODULE_PACKAGES
+ )
+ cmake_parse_arguments(__standalone_parts
+ "${__standalone_parts_opt_args}"
+ "${__standalone_parts_single_args}"
+ "${__standalone_parts_multi_args}"
+ ${${standalone_parts_args_var_name}})
+
+ # Packages looked up in standalone tests Config files should use the same version as
+ # the one recorded on the Platform target.
+ qt_internal_get_package_version_of_target(Platform __standalone_parts_main_qt_package_version)
+
+ if(__standalone_parts_QT_MODULE_PACKAGES)
+ foreach(__standalone_parts_package_name IN LISTS __standalone_parts_QT_MODULE_PACKAGES)
+ find_package(${QT_CMAKE_EXPORT_NAMESPACE}
+ "${__standalone_parts_main_qt_package_version}"
+ COMPONENTS "${__standalone_parts_package_name}")
+ endforeach()
+ endif()
+
+ unset(__standalone_parts_opt_args)
+ unset(__standalone_parts_single_args)
+ unset(__standalone_parts_multi_args)
+ unset(__standalone_parts_QT_MODULE_PACKAGES)
+ unset(__standalone_parts_main_qt_package_version)
+ unset(__standalone_parts_package_name)
+endmacro()
+
# Used by standalone tests and standalone non-ExternalProject examples to find all installed qt
# packages.
macro(qt_internal_find_standalone_parts_config_files)
diff --git a/cmake/QtFeature.cmake b/cmake/QtFeature.cmake
index c4564cfb38d..9bbf6e700b4 100644
--- a/cmake/QtFeature.cmake
+++ b/cmake/QtFeature.cmake
@@ -1302,7 +1302,10 @@ function(qt_feature_module_end)
# Before, we didn't use to export the properties at all for INTERFACE_ libraries,
# but we need to, because certain GlobalPrivate modules have features which are used
# in configure-time conditions for tests.
- qt_internal_add_genex_properties_export("${target}" ${properties_to_export})
+ qt_internal_add_custom_properties_to_export("${target}"
+ PROPERTIES_WITHOUT_GENEXES
+ ${properties_to_export}
+ )
else()
set(propertyPrefix "")
set_property(TARGET "${target}"
diff --git a/cmake/QtPostProcessHelpers.cmake b/cmake/QtPostProcessHelpers.cmake
index 12f5c617960..b8e46085a98 100644
--- a/cmake/QtPostProcessHelpers.cmake
+++ b/cmake/QtPostProcessHelpers.cmake
@@ -819,7 +819,7 @@ function(qt_internal_create_config_file_for_standalone_tests)
# standalone tests, and it can happen that Core or Gui features are not
# imported early enough, which means FindWrapPNG will try to find a system PNG library instead
# of the bundled one.
- set(modules)
+ set(modules "")
foreach(m ${QT_REPO_KNOWN_MODULES})
get_target_property(target_type "${m}" TYPE)
@@ -835,12 +835,9 @@ function(qt_internal_create_config_file_for_standalone_tests)
endif()
endforeach()
- list(JOIN modules " " QT_REPO_KNOWN_MODULES_STRING)
- string(STRIP "${QT_REPO_KNOWN_MODULES_STRING}" QT_REPO_KNOWN_MODULES_STRING)
-
# Skip generating and installing file if no modules were built. This make sure not to install
# anything when build qtx11extras on macOS for example.
- if(NOT QT_REPO_KNOWN_MODULES_STRING)
+ if(NOT modules)
return()
endif()
@@ -848,8 +845,8 @@ function(qt_internal_create_config_file_for_standalone_tests)
# of the current repo. This is used for standalone tests.
qt_internal_get_standalone_parts_config_file_name(tests_config_file_name)
- # Standalone tests Config files should follow the main versioning scheme.
- qt_internal_get_package_version_of_target(Platform main_qt_package_version)
+ # Substitution variables.
+ list(JOIN modules "\n " QT_MODULE_PACKAGES)
configure_file(
"${QT_CMAKE_DIR}/QtStandaloneTestsConfig.cmake.in"
diff --git a/cmake/QtStandaloneTestsConfig.cmake.in b/cmake/QtStandaloneTestsConfig.cmake.in
index 39200167a58..9d548d14699 100644
--- a/cmake/QtStandaloneTestsConfig.cmake.in
+++ b/cmake/QtStandaloneTestsConfig.cmake.in
@@ -1,8 +1,8 @@
# Copyright (C) 2024 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
-# TODO: Ideally this should look for each Qt module separately, with each module's specific version,
-# bypassing the Qt6 Config file, aka find_package(Qt6SpecificFoo) repated x times. But it's not
-# critical.
-find_package(@INSTALL_CMAKE_NAMESPACE@ @main_qt_package_version@
- COMPONENTS @QT_REPO_KNOWN_MODULES_STRING@)
+set(__standalone_parts_qt_packages_args
+ QT_MODULE_PACKAGES
+ @QT_MODULE_PACKAGES@
+)
+qt_internal_find_standalone_parts_qt_packages(__standalone_parts_qt_packages_args)
diff --git a/cmake/QtTargetHelpers.cmake b/cmake/QtTargetHelpers.cmake
index eedfdbeba74..397628ba11a 100644
--- a/cmake/QtTargetHelpers.cmake
+++ b/cmake/QtTargetHelpers.cmake
@@ -1688,15 +1688,35 @@ function(qt_internal_get_target_sources_property out_var)
set(${out_var} "${${out_var}}" PARENT_SCOPE)
endfunction()
-# This function collects target properties that contain generator expressions and needs to be
-# exported. This function is needed since the CMake EXPORT_PROPERTIES property doesn't support
-# properties that contain generator expressions.
-# Usage: qt_internal_add_genex_properties_export(target properties...)
-function(qt_internal_add_genex_properties_export target)
+# This function collects target properties that need to be exported without using CMake's
+# EXPORT_PROPERTIES.
+# Use cases:
+# - Properties named INTERFACE_foo (which CMake doesn't allow exporting)
+# - Properties that contain generator expressions (need special handling for multi-config builds)
+# Usage:
+# qt_internal_add_custom_properties_to_export(target
+# PROPERTIES property1 [property2 ...]
+# PROPERTIES_WITHOUT_GENEXES property3 [property4 ...]
+# )
+# Arguments:
+# PROPERTIES
+# should contain names of properties that can differ in multi-config builds (e.g. paths)
+# PROPERTIES_WITHOUT_GENEXES
+# should contain names of properties that will always have the same value in multi config
+# builds (e.g, feature values).
+function(qt_internal_add_custom_properties_to_export target)
+ set(opt_args "")
+ set(single_args "")
+ set(multi_args
+ PROPERTIES
+ PROPERTIES_WITHOUT_GENEXES
+ )
+ cmake_parse_arguments(PARSE_ARGV 1 arg "${opt_args}" "${single_args}" "${multi_args}")
+ _qt_internal_validate_all_args_are_parsed(arg)
+
get_cmake_property(is_multi_config GENERATOR_IS_MULTI_CONFIG)
- set(config_check_begin "")
- set(config_check_end "")
+ # Prepare multi-config helper genexes.
if(is_multi_config)
list(GET CMAKE_CONFIGURATION_TYPES 0 first_config_type)
@@ -1704,7 +1724,7 @@ function(qt_internal_add_genex_properties_export target)
# The check is only applicable to the 'main' configuration. If user project doesn't use
# multi-config generator, then the check supposed to return true and the value from the
# 'main' configuration supposed to be used.
- string(JOIN "" check_if_config_empty
+ string(CONCAT check_if_config_empty
"$<1:$><NOT:"
"$<1:$><BOOL:"
"$<1:$><CONFIG$<ANGLE-R>"
@@ -1714,7 +1734,7 @@ function(qt_internal_add_genex_properties_export target)
# The genex snippet is evaluated to '$<CONFIG:'Qt config type'>' in the generated cmake
# file and checks if the config that user uses matches the generated cmake file config.
- string(JOIN "" check_user_config
+ string(CONCAT check_user_config
"$<1:$><CONFIG:$<CONFIG>$<ANGLE-R>"
)
@@ -1725,34 +1745,70 @@ function(qt_internal_add_genex_properties_export target)
# user project according to the user config type.
# All genexes need to be escaped properly to protect them from evaluation by the
# file(GENERATE call in the qt_internal_export_genex_properties function.
- string(JOIN "" config_check_begin
+ string(CONCAT config_check_begin_multi
"$<1:$><"
"$<1:$><OR:"
"${check_user_config}"
"$<$<CONFIG:${first_config_type}>:$<COMMA>${check_if_config_empty}>"
"$<ANGLE-R>:"
)
- set(config_check_end "$<ANGLE-R>")
+ set(config_check_end_multi "$<ANGLE-R>")
endif()
set(target_name "${QT_CMAKE_EXPORT_NAMESPACE}::${target}")
- foreach(property IN LISTS ARGN)
- set(target_property_genex "$<TARGET_PROPERTY:${target_name},${property}>")
- # All properties that contain lists need to be protected of processing by JOIN genex calls.
- # So this escapes the semicolons for these list.
- set(target_property_list_escape
- "$<JOIN:$<GENEX_EVAL:${target_property_genex}>,\;>")
- set(property_value
- "\"${config_check_begin}${target_property_list_escape}${config_check_end}\"")
- set_property(TARGET ${target} APPEND PROPERTY _qt_export_genex_properties_content
- "${property} ${property_value}")
+
+ set(property_sources
+ PROPERTIES
+ PROPERTIES_WITHOUT_GENEXES
+ )
+
+ foreach(property_source IN LISTS property_sources)
+ if(property_source STREQUAL "PROPERTIES")
+ # Properties with genexes need multi-config specific handling.
+ set(config_check_begin "${config_check_begin_multi}")
+ set(config_check_end "${config_check_end_multi}")
+
+ set(output_property "_qt_export_custom_properties_content")
+ elseif(property_source STREQUAL "PROPERTIES_WITHOUT_GENEXES")
+ # Properties without genexes don't need the config checks.
+ set(config_check_begin "")
+ set(config_check_end "")
+
+ set(output_property "_qt_export_custom_properties_no_genexes_content")
+ else()
+ message(FATAL_ERROR "Invalid type of property source" ${property_source}"")
+ endif()
+
+ foreach(property IN LISTS arg_${property_source})
+ set(target_property_genex "$<TARGET_PROPERTY:${target_name},${property}>")
+ # All properties that contain lists need to be protected of processing by JOIN genex
+ # calls. So this escapes the semicolons for these list.
+ set(target_property_list_escape
+ "$<JOIN:$<GENEX_EVAL:${target_property_genex}>,\;>")
+ set(property_value
+ "\"${config_check_begin}${target_property_list_escape}${config_check_end}\"")
+ set_property(TARGET ${target} APPEND PROPERTY "${output_property}"
+ "${property} ${property_value}")
+ endforeach()
endforeach()
endfunction()
-# This function executes generator expressions for the properties that are added by the
-# qt_internal_add_genex_properties_export function and sets the calculated values to the
-# corresponding properties in the generated ExtraProperties.cmake file. The file then needs to be
-# included after the target creation routines in Config.cmake files. It also supports Multi-Config
-# builds.
+# This function generates and installs ${EXPORT_NAME_PREFIX}ExportProperties-$<CONFIG>.cmake files
+# to be included from inside a FooConfig.cmake file.
+#
+# The file contains set_property(TARGET PROPERTY) assignments that append values to a given target's
+# properties as added by the qt_internal_add_custom_properties_to_export function.
+#
+# The assigned values are computed from the result of executing the generator expressions that were
+# stored in the properties, and are wrapped in config-specific genexes in a multi-config build.
+#
+# Example output:
+# set_property(TARGET Qt6::Foo PROPERTY MY_GENEX_PROP
+# "$<$<OR:$<CONFIG:RelWithDebInfo>,$<NOT:$<BOOL:$<CONFIG>>>>:OneReleaseVal>")
+# set_property(TARGET Qt6::Foo PROPERTY MY_REGULAR_PROP "SecondValue")
+# include("${CMAKE_CURRENT_LIST_DIR}/Qt6FooExtraProperties-Debug.cmake")
+# inside the include
+# set_property(TARGET Qt6::Foo APPEND PROPERTY MY_GENEX_PROP "$<$<OR:$<CONFIG:Debug>>:OneDebugVal>")
+#
# Arguments:
# EXPORT_NAME_PREFIX:
# The portion of the file name before ExtraProperties.cmake
@@ -1761,13 +1817,15 @@ endfunction()
# TARGETS:
# The internal target names.
function(qt_internal_export_genex_properties)
- set(option_args "")
+ set(opt_args "")
set(single_args
EXPORT_NAME_PREFIX
CONFIG_INSTALL_DIR
)
- set(multi_args TARGETS)
- cmake_parse_arguments(arg "${option_args}" "${single_args}" "${multi_args}" ${ARGN})
+ set(multi_args
+ TARGETS
+ )
+ cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}")
if(NOT arg_EXPORT_NAME_PREFIX)
message(FATAL_ERROR "qt_internal_export_genex_properties: "
@@ -1779,24 +1837,36 @@ function(qt_internal_export_genex_properties)
"TARGETS argument must contain at least one target")
endif()
- foreach(target IN LISTS arg_TARGETS)
- get_cmake_property(is_multi_config GENERATOR_IS_MULTI_CONFIG)
+ # TODO: Handling more than one target won't work correctly atm due to trying to create and
+ # install the same file name multiple times for each target.
+ list(LENGTH arg_TARGETS targets_count)
+ if(targets_count GREATER 1)
+ message(AUTHOR_WARNING "qt_internal_export_genex_properties: "
+ "Specifying more than one target is not fully supported yet.")
+ endif()
+ get_cmake_property(is_multi_config GENERATOR_IS_MULTI_CONFIG)
+
+ set(should_append "")
+ set(config_suffix "")
+ set(is_first_config "1")
+ if(is_multi_config)
+ list(GET CMAKE_CONFIGURATION_TYPES 0 first_config_type)
+
+ # The non-genex properties should only go to the first config file.
+ set(is_first_config "$<CONFIG:${first_config_type}>")
+
+ set(config_suffix "$<$<NOT:${is_first_config}>:-$<CONFIG>>")
+ # If the generated file belongs to the 'main' config type, we should set property
+ # but not append it.
+ string(JOIN "" should_append
+ "$<$<NOT:${is_first_config}>: APPEND>")
+ endif()
+
+ foreach(target IN LISTS arg_TARGETS)
set(output_file_base_name "${arg_EXPORT_NAME_PREFIX}ExtraProperties")
- set(should_append "")
- set(config_suffix "")
- if(is_multi_config)
- list(GET CMAKE_CONFIGURATION_TYPES 0 first_config_type)
- set(config_suffix "$<$<NOT:$<CONFIG:${first_config_type}>>:-$<CONFIG>>")
- # If the generated file belongs to the 'main' config type, we should set property
- # but not append it.
- string(JOIN "" should_append
- "$<$<NOT:$<CONFIG:${first_config_type}>>: APPEND>")
- endif()
set(file_name "${output_file_base_name}${config_suffix}.cmake")
-
- qt_path_join(output_file "${arg_CONFIG_INSTALL_DIR}"
- "${file_name}")
+ qt_path_join(output_file "${arg_CONFIG_INSTALL_DIR}" "${file_name}")
if(NOT IS_ABSOLUTE "${output_file}")
qt_path_join(output_file "${QT_BUILD_DIR}" "${output_file}")
@@ -1804,17 +1874,51 @@ function(qt_internal_export_genex_properties)
set(target_name "${QT_CMAKE_EXPORT_NAMESPACE}::${target}")
+ # Common genex helpers.
string(JOIN "" set_property_begin "set_property(TARGET "
"${target_name}${should_append} PROPERTY "
)
set(set_property_end ")")
set(set_property_glue "${set_property_end}\n${set_property_begin}")
+ set(t_prop "TARGET_PROPERTY:${target}")
+
+ # Handle the properties that contain genexes.
set(property_list
- "$<GENEX_EVAL:$<TARGET_PROPERTY:${target},_qt_export_genex_properties_content>>")
- string(JOIN "" set_property_content "${set_property_begin}"
+ "$<GENEX_EVAL:$<${t_prop},_qt_export_custom_properties_content>>")
+ set(property_has_values "$<BOOL:${property_list}>")
+ string(CONCAT set_property_content
+ "${set_property_begin}"
"$<JOIN:${property_list},${set_property_glue}>"
"${set_property_end}")
+ string(CONCAT set_property_content_conditional
+ "$<${property_has_values}:"
+ "\n${set_property_content}"
+ ">")
+
+ # We need to ensure the no genexes content only gets added to the first config file.
+ set(property_no_genexes_list
+ "$<GENEX_EVAL:$<${t_prop},_qt_export_custom_properties_no_genexes_content>>")
+ set(property_no_genexes_has_values "$<BOOL:${property_no_genexes_list}>")
+ string(CONCAT property_no_genexes_has_values_and_first_config
+ "$<AND:${property_no_genexes_has_values},${is_first_config}>")
+
+ string(CONCAT set_property_no_genexes_content
+ "${set_property_begin}"
+ "$<JOIN:${property_no_genexes_list},${set_property_glue}>"
+ "${set_property_end}")
+
+ string(CONCAT set_property_no_genexes_content_conditional
+ "$<${property_no_genexes_has_values_and_first_config}:"
+ "\n${set_property_no_genexes_content}"
+ ">")
+
+ # Final content is generated if at least one genex-carrying property has a value,
+ # or if we are in the first config and at least one no-genex property has a value.
+ set(content_available_condition
+ "$<OR:${property_has_values},${property_no_genexes_has_values_and_first_config}>")
+
+ set(config_includes_string "")
if(is_multi_config)
set(config_includes "")
foreach(config IN LISTS CMAKE_CONFIGURATION_TYPES)
@@ -1827,19 +1931,33 @@ function(qt_internal_export_genex_properties)
endforeach()
list(JOIN config_includes "\n" config_includes_string)
set(config_includes_string
- "\n$<$<CONFIG:${first_config_type}>:${config_includes_string}>")
+ "\n$<${is_first_config}:${config_includes_string}>")
+
+ # Config includes should be included if we have properties with genexes, which are
+ # config specific.
+ string(CONCAT config_includes_string_conditional
+ "$<${property_has_values}:"
+ "${config_includes_string}"
+ ">")
endif()
+ string(CONCAT final_content
+ "$<${content_available_condition}:"
+ "${set_property_content_conditional}"
+ "${set_property_no_genexes_content_conditional}"
+ "${config_includes_string_conditional}"
+ ">")
+
file(GENERATE OUTPUT "${output_file}"
- CONTENT "$<$<BOOL:${property_list}>:${set_property_content}${config_includes_string}>"
- CONDITION "$<BOOL:${property_list}>"
+ CONTENT "${final_content}"
+ CONDITION "${content_available_condition}"
)
- endforeach()
- qt_install(FILES "$<$<BOOL:${property_list}>:${output_file}>"
- DESTINATION "${arg_CONFIG_INSTALL_DIR}"
- COMPONENT Devel
- )
+ qt_install(FILES "$<${content_available_condition}:${output_file}>"
+ DESTINATION "${arg_CONFIG_INSTALL_DIR}"
+ COMPONENT Devel
+ )
+ endforeach()
endfunction()
# A small wrapper for adding the Platform target, and a building block for the PlatformXInternal
diff --git a/doc/global/html-header-offline.qdocconf b/doc/global/html-header-offline.qdocconf
index ffce22eeeec..97e13313785 100644
--- a/doc/global/html-header-offline.qdocconf
+++ b/doc/global/html-header-offline.qdocconf
@@ -1,14 +1,16 @@
#Default HTML header for QDoc builds.
-#specify the CSS file used by this template
-HTML.stylesheets = template/style/offline.css \
- template/style/offline-dark.css \
- template/style/tech_preview.svg
-
-#for including files into the qch file
-qhp.extraFiles += style/offline.css \
- style/offline-dark.css \
- style/tech_preview.svg
+# CSS used by this template (copied to <outputdir>/style)
+HTML.stylesheets += \
+ template/style/offline.css \
+ template/style/offline-dark.css \
+ template/style/tech_preview.svg
+
+# Files (relative to the output directory) to include into the qch file
+qhp.extraFiles += \
+ style/offline.css \
+ style/offline-dark.css \
+ style/tech_preview.svg
HTML.headerstyles = \
" <link rel=\"stylesheet\" type=\"text/css\" href=\"style/offline.css\" />\n"
diff --git a/doc/global/qt-html-templates-offline.qdocconf b/doc/global/qt-html-templates-offline.qdocconf
index 00fc58ba666..7cc8651bd33 100644
--- a/doc/global/qt-html-templates-offline.qdocconf
+++ b/doc/global/qt-html-templates-offline.qdocconf
@@ -8,34 +8,25 @@ defines += offlinedocs
#uncomment if navigation bar is not wanted
#HTML.nonavigationbar = "true"
-HTML.stylesheets = template/style/offline.css \
- template/style/offline-dark.css
-HTML.extraimages += template/images/ico_out.png \
- template/images/btn_prev.png \
- template/images/btn_next.png \
- template/images/bullet_dn.png \
- template/images/bullet_sq.png \
- template/images/bgrContent.png
+# Images used only in CSS or macros that are copied to <outputdir>/images
+{HTML.extraimages,DocBook.extraimages} += \
+ template/images/ico_out.png \
+ template/images/btn_prev.png \
+ template/images/btn_next.png \
+ template/images/bullet_dn.png \
+ template/images/bullet_sq.png \
+ template/images/bgrContent.png
-sourcedirs += includes
-
-#specify which files in the output directory should be packed into the qch file.
-qhp.extraFiles += style/offline.css \
- style/offline-dark.css \
- images/ico_out.png \
- images/btn_prev.png \
- images/btn_next.png \
- images/bullet_dn.png \
- images/bullet_sq.png \
- images/bgrContent.png
+# Files (relative to the output directory) to include into the qch file
+qhp.extraFiles += \
+ images/ico_out.png \
+ images/btn_prev.png \
+ images/btn_next.png \
+ images/bullet_dn.png \
+ images/bullet_sq.png \
+ images/bgrContent.png
-DocBook.extraimages += \
- images/ico_out.png \
- images/btn_prev.png \
- images/btn_next.png \
- images/bullet_dn.png \
- images/bullet_sq.png \
- images/bgrContent.png
+sourcedirs += includes
# By default, include override definitions for a simplified template/CSS,
# suited for rendering HTML with QTextBrowser. Comment out this line to
diff --git a/examples/corelib/bindableproperties/doc/src/bindableproperties.qdoc b/examples/corelib/bindableproperties/doc/src/bindableproperties.qdoc
index 476522b0865..c8e1ad84b26 100644
--- a/examples/corelib/bindableproperties/doc/src/bindableproperties.qdoc
+++ b/examples/corelib/bindableproperties/doc/src/bindableproperties.qdoc
@@ -4,6 +4,7 @@
/*!
\example bindableproperties
\examplecategory {Data Processing & I/O}
+ \ingroup corelib_examples
\title Bindable Properties
\brief Demonstrates how the usage of bindable properties can simplify
your C++ code.
diff --git a/examples/corelib/ipc/doc/src/localfortuneclient.qdoc b/examples/corelib/ipc/doc/src/localfortuneclient.qdoc
index a2bdb69b8b0..ace3f18ce8a 100644
--- a/examples/corelib/ipc/doc/src/localfortuneclient.qdoc
+++ b/examples/corelib/ipc/doc/src/localfortuneclient.qdoc
@@ -6,6 +6,7 @@
\examplecategory {Connectivity}
\title Local Fortune Client
\ingroup examples-ipc
+ \ingroup corelib_examples
\brief Demonstrates using QLocalSocket for a simple local service client.
The Local Fortune Client example shows how to create a client for a simple
diff --git a/examples/corelib/ipc/doc/src/localfortuneserver.qdoc b/examples/corelib/ipc/doc/src/localfortuneserver.qdoc
index 6b359a86805..593d505bdf3 100644
--- a/examples/corelib/ipc/doc/src/localfortuneserver.qdoc
+++ b/examples/corelib/ipc/doc/src/localfortuneserver.qdoc
@@ -6,6 +6,7 @@
\examplecategory {Connectivity}
\title Local Fortune Server
\ingroup examples-ipc
+ \ingroup corelib_examples
\brief Demonstrates using QLocalServer and QLocalSocket for serving a simple local service.
The Local Fortune Server example shows how to create a server for a simple
diff --git a/examples/corelib/ipc/doc/src/sharedmemory.qdoc b/examples/corelib/ipc/doc/src/sharedmemory.qdoc
index 80645f34954..62f9cbcca78 100644
--- a/examples/corelib/ipc/doc/src/sharedmemory.qdoc
+++ b/examples/corelib/ipc/doc/src/sharedmemory.qdoc
@@ -6,6 +6,7 @@
\examplecategory {Data Processing & I/O}
\title IPC: Shared Memory
\ingroup examples-ipc
+ \ingroup corelib_examples
\brief Demonstrates how to share image data between different processes
using the Shared Memory IPC mechanism.
diff --git a/examples/corelib/mimetypes/doc/src/mimetypebrowser.qdoc b/examples/corelib/mimetypes/doc/src/mimetypebrowser.qdoc
index cc76abe2e57..9b496647c5e 100644
--- a/examples/corelib/mimetypes/doc/src/mimetypebrowser.qdoc
+++ b/examples/corelib/mimetypes/doc/src/mimetypebrowser.qdoc
@@ -5,6 +5,7 @@
\example mimetypes/mimetypebrowser
\examplecategory {Data Processing & I/O}
\ingroup examples-mimetype
+ \ingroup corelib_examples
\title MIME Type Browser
\brief Shows the hierarchy of MIME types and
diff --git a/examples/corelib/platform/androidnotifier/doc/src/androidnotifier-example.qdoc b/examples/corelib/platform/androidnotifier/doc/src/androidnotifier-example.qdoc
index e1f76d2173f..86d395a25fe 100644
--- a/examples/corelib/platform/androidnotifier/doc/src/androidnotifier-example.qdoc
+++ b/examples/corelib/platform/androidnotifier/doc/src/androidnotifier-example.qdoc
@@ -7,6 +7,7 @@
\meta tag {widgets,android,notification}
\brief Demonstrates calling Java code from Qt in an Android application.
\ingroup androidplatform
+ \ingroup corelib_examples
\image androidnotifier.png
diff --git a/examples/corelib/serialization/cbordump/doc/src/cbordump.qdoc b/examples/corelib/serialization/cbordump/doc/src/cbordump.qdoc
index a4dc01116f3..bfb92c7cdc8 100644
--- a/examples/corelib/serialization/cbordump/doc/src/cbordump.qdoc
+++ b/examples/corelib/serialization/cbordump/doc/src/cbordump.qdoc
@@ -5,6 +5,7 @@
\example serialization/cbordump
\examplecategory {Data Processing & I/O}
\meta tag {network}
+ \ingroup corelib_examples
\title Parsing and displaying CBOR data
\brief A demonstration of how to parse files in CBOR format.
diff --git a/examples/corelib/serialization/convert/doc/src/convert.qdoc b/examples/corelib/serialization/convert/doc/src/convert.qdoc
index 187e81a85e3..67b9600d740 100644
--- a/examples/corelib/serialization/convert/doc/src/convert.qdoc
+++ b/examples/corelib/serialization/convert/doc/src/convert.qdoc
@@ -5,6 +5,7 @@
\example serialization/convert
\examplecategory {Data Processing & I/O}
\meta tag {network}
+ \ingroup corelib_examples
\title Serialization Converter
\brief How to convert between different serialization formats.
diff --git a/examples/corelib/serialization/savegame/doc/src/savegame.qdoc b/examples/corelib/serialization/savegame/doc/src/savegame.qdoc
index 46fca15b628..bbd047b0016 100644
--- a/examples/corelib/serialization/savegame/doc/src/savegame.qdoc
+++ b/examples/corelib/serialization/savegame/doc/src/savegame.qdoc
@@ -5,6 +5,7 @@
\example serialization/savegame
\examplecategory {Data Processing & I/O}
\title Saving and Loading a Game
+ \ingroup corelib_examples
\brief How to save and load a game using Qt's JSON or CBOR classes.
diff --git a/examples/corelib/serialization/streambookmarks/doc/src/qxmlstreambookmarks.qdoc b/examples/corelib/serialization/streambookmarks/doc/src/qxmlstreambookmarks.qdoc
index 8e32dd8d0b2..6393671dcf0 100644
--- a/examples/corelib/serialization/streambookmarks/doc/src/qxmlstreambookmarks.qdoc
+++ b/examples/corelib/serialization/streambookmarks/doc/src/qxmlstreambookmarks.qdoc
@@ -8,6 +8,7 @@
\title QXmlStream Bookmarks Example
\brief Demonstrates how to read and write XBEL files.
\ingroup xml-examples
+ \ingroup corelib_examples
The QXmlStream Bookmarks example provides a viewer for XML Bookmark Exchange
Language (XBEL) files. It can read bookmarks using Qt's QXmlStreamReader and
diff --git a/examples/corelib/threads/doc/src/mandelbrot.qdoc b/examples/corelib/threads/doc/src/mandelbrot.qdoc
index f0daf633704..83eedfd021d 100644
--- a/examples/corelib/threads/doc/src/mandelbrot.qdoc
+++ b/examples/corelib/threads/doc/src/mandelbrot.qdoc
@@ -6,6 +6,7 @@
\examplecategory {Data Processing & I/O}
\title Mandelbrot
\ingroup qtconcurrent-mtexamples
+ \ingroup corelib_examples
\brief The Mandelbrot example demonstrates multi-thread programming
using Qt. It shows how to use a worker thread to
diff --git a/examples/corelib/threads/doc/src/queuedcustomtype.qdoc b/examples/corelib/threads/doc/src/queuedcustomtype.qdoc
index cafab85edcb..2d8430be9f1 100644
--- a/examples/corelib/threads/doc/src/queuedcustomtype.qdoc
+++ b/examples/corelib/threads/doc/src/queuedcustomtype.qdoc
@@ -6,6 +6,7 @@
\examplecategory {Data Processing & I/O}
\title Queued Custom Type
\ingroup qtconcurrent-mtexamples
+ \ingroup corelib_examples
\brief The Queued Custom Type example shows how to send custom types between
threads with queued signals and slots.
diff --git a/examples/corelib/threads/doc/src/semaphores.qdoc b/examples/corelib/threads/doc/src/semaphores.qdoc
index f5ff90b0140..7206ac8536b 100644
--- a/examples/corelib/threads/doc/src/semaphores.qdoc
+++ b/examples/corelib/threads/doc/src/semaphores.qdoc
@@ -6,6 +6,7 @@
\examplecategory {Data Processing & I/O}
\title Producer and Consumer using Semaphores
\ingroup qtconcurrent-mtexamples
+ \ingroup corelib_examples
\brief The Producer and Consumer using Semaphores example shows how
to use QSemaphore to control access to a circular buffer shared
diff --git a/examples/corelib/threads/doc/src/waitconditions.qdoc b/examples/corelib/threads/doc/src/waitconditions.qdoc
index d46442d0797..8f2ed596079 100644
--- a/examples/corelib/threads/doc/src/waitconditions.qdoc
+++ b/examples/corelib/threads/doc/src/waitconditions.qdoc
@@ -6,6 +6,7 @@
\examplecategory {Data Processing & I/O}
\title Producer and Consumer using Wait Conditions
\ingroup qtconcurrent-mtexamples
+ \ingroup corelib_examples
\brief The Producer and Consumer using Wait Conditions example shows
how to use QWaitCondition and QMutex to control access to a circular
diff --git a/examples/corelib/time/calendarbackendplugin/doc/src/calendarbackendplugin.qdoc b/examples/corelib/time/calendarbackendplugin/doc/src/calendarbackendplugin.qdoc
index b715ccd28ad..8bff32788c1 100644
--- a/examples/corelib/time/calendarbackendplugin/doc/src/calendarbackendplugin.qdoc
+++ b/examples/corelib/time/calendarbackendplugin/doc/src/calendarbackendplugin.qdoc
@@ -6,6 +6,7 @@
\title Calendar Backend Plugin Example
\examplecategory {Data Processing & I/O}
\ingroup examples-time
+ \ingroup corelib_examples
\brief QCalendar example illustrating user-supplied custom calendars.
\image calendarwindow_transition.png
diff --git a/examples/corelib/tools/doc/src/contiguouscache.qdoc b/examples/corelib/tools/doc/src/contiguouscache.qdoc
index 9fc572927b6..b55ca2da211 100644
--- a/examples/corelib/tools/doc/src/contiguouscache.qdoc
+++ b/examples/corelib/tools/doc/src/contiguouscache.qdoc
@@ -5,6 +5,7 @@
\example tools/contiguouscache
\title Contiguous Cache Example
\examplecategory {Data Processing & I/O}
+ \ingroup corelib_examples
\brief The Contiguous Cache example shows how to use QContiguousCache to manage memory usage for
very large models. In some environments memory is limited and, even when it
diff --git a/src/3rdparty/libpng/ANNOUNCE b/src/3rdparty/libpng/ANNOUNCE
index 516e078082d..ae0b6ccc13b 100644
--- a/src/3rdparty/libpng/ANNOUNCE
+++ b/src/3rdparty/libpng/ANNOUNCE
@@ -1,5 +1,5 @@
-libpng 1.6.50 - July 1, 2025
-============================
+libpng 1.6.51 - November 21, 2025
+=================================
This is a public release of libpng, intended for use in production code.
@@ -9,13 +9,13 @@ Files available for download
Source files with LF line endings (for Unix/Linux):
- * libpng-1.6.50.tar.xz (LZMA-compressed, recommended)
- * libpng-1.6.50.tar.gz (deflate-compressed)
+ * libpng-1.6.51.tar.xz (LZMA-compressed, recommended)
+ * libpng-1.6.51.tar.gz (deflate-compressed)
Source files with CRLF line endings (for Windows):
- * lpng1650.7z (LZMA-compressed, recommended)
- * lpng1650.zip (deflate-compressed)
+ * lpng1651.7z (LZMA-compressed, recommended)
+ * lpng1651.zip (deflate-compressed)
Other information:
@@ -25,18 +25,33 @@ Other information:
* TRADEMARK.md
-Changes from version 1.6.49 to version 1.6.50
+Changes from version 1.6.50 to version 1.6.51
---------------------------------------------
- * Improved the detection of the RVV Extension on the RISC-V platform.
- (Contributed by Filip Wasil)
- * Replaced inline ASM with C intrinsics in the RVV code.
- (Contributed by Filip Wasil)
- * Fixed a decoder defect in which unknown chunks trailing IDAT, set
- to go through the unknown chunk handler, incorrectly triggered
- out-of-place IEND errors.
- (Contributed by John Bowler)
- * Fixed the CMake file for cross-platform builds that require `libm`.
+ * Fixed CVE-2025-64505 (moderate severity):
+ Heap buffer overflow in `png_do_quantize` via malformed palette index.
+ (Reported by Samsung; analyzed by Fabio Gritti.)
+ * Fixed CVE-2025-64506 (moderate severity):
+ Heap buffer over-read in `png_write_image_8bit` with 8-bit input and
+ `convert_to_8bit` enabled.
+ (Reported by Samsung and <weijinjinnihao@users.noreply.github.com>;
+ analyzed by Fabio Gritti.)
+ * Fixed CVE-2025-64720 (high severity):
+ Buffer overflow in `png_image_read_composite` via incorrect palette
+ premultiplication.
+ (Reported by Samsung; analyzed by John Bowler.)
+ * Fixed CVE-2025-65018 (high severity):
+ Heap buffer overflow in `png_combine_row` triggered via
+ `png_image_finish_read`.
+ (Reported by <yosiimich@users.noreply.github.com>.)
+ * Fixed a memory leak in `png_set_quantize`.
+ (Reported by Samsung; analyzed by Fabio Gritti.)
+ * Removed the experimental and incomplete ERROR_NUMBERS code.
+ (Contributed by Tobias Stoeckmann.)
+ * Improved the RISC-V vector extension support; required RVV 1.0 or newer.
+ (Contributed by Filip Wasil.)
+ * Added GitHub Actions workflows for automated testing.
+ * Performed various refactorings and cleanups.
Send comments/corrections/commendations to png-mng-implement at lists.sf.net.
diff --git a/src/3rdparty/libpng/CHANGES b/src/3rdparty/libpng/CHANGES
index b6499b1f34c..2478fd0fc08 100644
--- a/src/3rdparty/libpng/CHANGES
+++ b/src/3rdparty/libpng/CHANGES
@@ -6278,6 +6278,32 @@ Version 1.6.50 [July 1, 2025]
(Contributed by John Bowler)
Fixed the CMake file for cross-platform builds that require `libm`.
+Version 1.6.51 [November 21, 2025]
+ Fixed CVE-2025-64505 (moderate severity):
+ Heap buffer overflow in `png_do_quantize` via malformed palette index.
+ (Reported by Samsung; analyzed by Fabio Gritti.)
+ Fixed CVE-2025-64506 (moderate severity):
+ Heap buffer over-read in `png_write_image_8bit` with 8-bit input and
+ `convert_to_8bit` enabled.
+ (Reported by Samsung and <weijinjinnihao@users.noreply.github.com>;
+ analyzed by Fabio Gritti.)
+ Fixed CVE-2025-64720 (high severity):
+ Buffer overflow in `png_image_read_composite` via incorrect palette
+ premultiplication.
+ (Reported by Samsung; analyzed by John Bowler.)
+ Fixed CVE-2025-65018 (high severity):
+ Heap buffer overflow in `png_combine_row` triggered via
+ `png_image_finish_read`.
+ (Reported by <yosiimich@users.noreply.github.com>.)
+ Fixed a memory leak in `png_set_quantize`.
+ (Reported by Samsung; analyzed by Fabio Gritti.)
+ Removed the experimental and incomplete ERROR_NUMBERS code.
+ (Contributed by Tobias Stoeckmann.)
+ Improved the RISC-V vector extension support; required RVV 1.0 or newer.
+ (Contributed by Filip Wasil.)
+ Added GitHub Actions workflows for automated testing.
+ Performed various refactorings and cleanups.
+
Send comments/corrections/commendations to png-mng-implement at lists.sf.net.
Subscription is required; visit
https://lists.sourceforge.net/lists/listinfo/png-mng-implement
diff --git a/src/3rdparty/libpng/README b/src/3rdparty/libpng/README
index 2eb633ac0fb..5ea329ee3da 100644
--- a/src/3rdparty/libpng/README
+++ b/src/3rdparty/libpng/README
@@ -1,4 +1,4 @@
-README for libpng version 1.6.50
+README for libpng version 1.6.51
================================
See the note about version numbers near the top of `png.h`.
diff --git a/src/3rdparty/libpng/libpng-manual.txt b/src/3rdparty/libpng/libpng-manual.txt
index 6c07e1022b6..f342c18e814 100644
--- a/src/3rdparty/libpng/libpng-manual.txt
+++ b/src/3rdparty/libpng/libpng-manual.txt
@@ -9,7 +9,7 @@ libpng-manual.txt - A description on how to use and modify libpng
Based on:
- libpng version 1.6.36, December 2018, through 1.6.50 - July 2025
+ libpng version 1.6.36, December 2018, through 1.6.51 - November 2025
Updated and distributed by Cosmin Truta
Copyright (c) 2018-2025 Cosmin Truta
@@ -3355,19 +3355,6 @@ Here is an example of writing two private chunks, prVt and miNE:
/* Needed because miNE is not safe-to-copy */
png_set_keep_unknown_chunks(png, PNG_HANDLE_CHUNK_ALWAYS,
(png_bytep) "miNE", 1);
- # if PNG_LIBPNG_VER < 10600
- /* Deal with unknown chunk location bug in 1.5.x and earlier */
- png_set_unknown_chunk_location(png, info, 0, PNG_HAVE_IHDR);
- png_set_unknown_chunk_location(png, info, 1, PNG_AFTER_IDAT);
- # endif
- # if PNG_LIBPNG_VER < 10500
- /* PNG_AFTER_IDAT writes two copies of the chunk prior to libpng-1.5.0,
- * one before IDAT and another after IDAT, so don't use it; only use
- * PNG_HAVE_IHDR location. This call resets the location previously
- * set by assignment and png_set_unknown_chunk_location() for chunk 1.
- */
- png_set_unknown_chunk_location(png, info, 1, PNG_HAVE_IHDR);
- # endif
#endif
The high-level write interface
diff --git a/src/3rdparty/libpng/png.c b/src/3rdparty/libpng/png.c
index 6e21915c402..380c4c19e6a 100644
--- a/src/3rdparty/libpng/png.c
+++ b/src/3rdparty/libpng/png.c
@@ -13,7 +13,7 @@
#include "pngpriv.h"
/* Generate a compiler error if there is an old png.h in the search path. */
-typedef png_libpng_version_1_6_50 Your_png_h_is_not_version_1_6_50;
+typedef png_libpng_version_1_6_51 Your_png_h_is_not_version_1_6_51;
/* Sanity check the chunks definitions - PNG_KNOWN_CHUNKS from pngpriv.h and the
* corresponding macro definitions. This causes a compile time failure if
@@ -108,10 +108,16 @@ png_zalloc,(voidpf png_ptr, uInt items, uInt size),PNG_ALLOCATED)
if (png_ptr == NULL)
return NULL;
- if (items >= (~(png_alloc_size_t)0)/size)
+ /* This check against overflow is vestigial, dating back from
+ * the old times when png_zalloc used to be an exported function.
+ * We're still keeping it here for now, as an extra-cautious
+ * prevention against programming errors inside zlib, although it
+ * should rather be a debug-time assertion instead.
+ */
+ if (size != 0 && items >= (~(png_alloc_size_t)0) / size)
{
- png_warning (png_voidcast(png_structrp, png_ptr),
- "Potential overflow in png_zalloc()");
+ png_warning(png_voidcast(png_structrp, png_ptr),
+ "Potential overflow in png_zalloc()");
return NULL;
}
@@ -238,10 +244,6 @@ png_user_version_check(png_structrp png_ptr, png_const_charp user_png_ver)
png_warning(png_ptr, m);
#endif
-#ifdef PNG_ERROR_NUMBERS_SUPPORTED
- png_ptr->flags = 0;
-#endif
-
return 0;
}
@@ -815,7 +817,7 @@ png_get_copyright(png_const_structrp png_ptr)
return PNG_STRING_COPYRIGHT
#else
return PNG_STRING_NEWLINE \
- "libpng version 1.6.50" PNG_STRING_NEWLINE \
+ "libpng version 1.6.51" PNG_STRING_NEWLINE \
"Copyright (c) 2018-2025 Cosmin Truta" PNG_STRING_NEWLINE \
"Copyright (c) 1998-2002,2004,2006-2018 Glenn Randers-Pehrson" \
PNG_STRING_NEWLINE \
diff --git a/src/3rdparty/libpng/png.h b/src/3rdparty/libpng/png.h
index b9985e81680..fb93d2242b5 100644
--- a/src/3rdparty/libpng/png.h
+++ b/src/3rdparty/libpng/png.h
@@ -1,6 +1,6 @@
/* png.h - header file for PNG reference library
*
- * libpng version 1.6.50
+ * libpng version 1.6.51
*
* Copyright (c) 2018-2025 Cosmin Truta
* Copyright (c) 1998-2002,2004,2006-2018 Glenn Randers-Pehrson
@@ -14,7 +14,7 @@
* libpng versions 0.89, June 1996, through 0.96, May 1997: Andreas Dilger
* libpng versions 0.97, January 1998, through 1.6.35, July 2018:
* Glenn Randers-Pehrson
- * libpng versions 1.6.36, December 2018, through 1.6.50, July 2025:
+ * libpng versions 1.6.36, December 2018, through 1.6.51, November 2025:
* Cosmin Truta
* See also "Contributing Authors", below.
*/
@@ -238,7 +238,7 @@
* ...
* 1.5.30 15 10530 15.so.15.30[.0]
* ...
- * 1.6.50 16 10650 16.so.16.50[.0]
+ * 1.6.51 16 10651 16.so.16.51[.0]
*
* Henceforth the source version will match the shared-library major and
* minor numbers; the shared-library major version number will be used for
@@ -274,7 +274,7 @@
*/
/* Version information for png.h - this should match the version in png.c */
-#define PNG_LIBPNG_VER_STRING "1.6.50"
+#define PNG_LIBPNG_VER_STRING "1.6.51"
#define PNG_HEADER_VERSION_STRING " libpng version " PNG_LIBPNG_VER_STRING "\n"
/* The versions of shared library builds should stay in sync, going forward */
@@ -285,7 +285,7 @@
/* These should match the first 3 components of PNG_LIBPNG_VER_STRING: */
#define PNG_LIBPNG_VER_MAJOR 1
#define PNG_LIBPNG_VER_MINOR 6
-#define PNG_LIBPNG_VER_RELEASE 50
+#define PNG_LIBPNG_VER_RELEASE 51
/* This should be zero for a public release, or non-zero for a
* development version.
@@ -316,7 +316,7 @@
* From version 1.0.1 it is:
* XXYYZZ, where XX=major, YY=minor, ZZ=release
*/
-#define PNG_LIBPNG_VER 10650 /* 1.6.50 */
+#define PNG_LIBPNG_VER 10651 /* 1.6.51 */
/* Library configuration: these options cannot be changed after
* the library has been built.
@@ -426,7 +426,7 @@ extern "C" {
/* This triggers a compiler error in png.c, if png.c and png.h
* do not agree upon the version number.
*/
-typedef char* png_libpng_version_1_6_50;
+typedef char* png_libpng_version_1_6_51;
/* Basic control structions. Read libpng-manual.txt or libpng.3 for more info.
*
diff --git a/src/3rdparty/libpng/pngconf.h b/src/3rdparty/libpng/pngconf.h
index d1081b54ddd..981df68d87a 100644
--- a/src/3rdparty/libpng/pngconf.h
+++ b/src/3rdparty/libpng/pngconf.h
@@ -1,6 +1,6 @@
/* pngconf.h - machine-configurable file for libpng
*
- * libpng version 1.6.50
+ * libpng version 1.6.51
*
* Copyright (c) 2018-2025 Cosmin Truta
* Copyright (c) 1998-2002,2004,2006-2016,2018 Glenn Randers-Pehrson
diff --git a/src/3rdparty/libpng/pngdebug.h b/src/3rdparty/libpng/pngdebug.h
index af1ae9e8212..0337918aec3 100644
--- a/src/3rdparty/libpng/pngdebug.h
+++ b/src/3rdparty/libpng/pngdebug.h
@@ -38,9 +38,6 @@
#define PNGDEBUG_H
/* These settings control the formatting of messages in png.c and pngerror.c */
/* Moved to pngdebug.h at 1.5.0 */
-# ifndef PNG_LITERAL_SHARP
-# define PNG_LITERAL_SHARP 0x23
-# endif
# ifndef PNG_LITERAL_LEFT_SQUARE_BRACKET
# define PNG_LITERAL_LEFT_SQUARE_BRACKET 0x5b
# endif
diff --git a/src/3rdparty/libpng/pngerror.c b/src/3rdparty/libpng/pngerror.c
index 01a7ef5347e..044fa2eb68c 100644
--- a/src/3rdparty/libpng/pngerror.c
+++ b/src/3rdparty/libpng/pngerror.c
@@ -39,46 +39,6 @@ PNG_FUNCTION(void,PNGAPI
png_error,(png_const_structrp png_ptr, png_const_charp error_message),
PNG_NORETURN)
{
-#ifdef PNG_ERROR_NUMBERS_SUPPORTED
- char msg[16];
- if (png_ptr != NULL)
- {
- if ((png_ptr->flags &
- (PNG_FLAG_STRIP_ERROR_NUMBERS|PNG_FLAG_STRIP_ERROR_TEXT)) != 0)
- {
- if (*error_message == PNG_LITERAL_SHARP)
- {
- /* Strip "#nnnn " from beginning of error message. */
- int offset;
- for (offset = 1; offset<15; offset++)
- if (error_message[offset] == ' ')
- break;
-
- if ((png_ptr->flags & PNG_FLAG_STRIP_ERROR_TEXT) != 0)
- {
- int i;
- for (i = 0; i < offset - 1; i++)
- msg[i] = error_message[i + 1];
- msg[i - 1] = '\0';
- error_message = msg;
- }
-
- else
- error_message += offset;
- }
-
- else
- {
- if ((png_ptr->flags & PNG_FLAG_STRIP_ERROR_TEXT) != 0)
- {
- msg[0] = '0';
- msg[1] = '\0';
- error_message = msg;
- }
- }
- }
- }
-#endif
if (png_ptr != NULL && png_ptr->error_fn != NULL)
(*(png_ptr->error_fn))(png_constcast(png_structrp,png_ptr),
error_message);
@@ -216,21 +176,6 @@ void PNGAPI
png_warning(png_const_structrp png_ptr, png_const_charp warning_message)
{
int offset = 0;
- if (png_ptr != NULL)
- {
-#ifdef PNG_ERROR_NUMBERS_SUPPORTED
- if ((png_ptr->flags &
- (PNG_FLAG_STRIP_ERROR_NUMBERS|PNG_FLAG_STRIP_ERROR_TEXT)) != 0)
-#endif
- {
- if (*warning_message == PNG_LITERAL_SHARP)
- {
- for (offset = 1; offset < 15; offset++)
- if (warning_message[offset] == ' ')
- break;
- }
- }
- }
if (png_ptr != NULL && png_ptr->warning_fn != NULL)
(*(png_ptr->warning_fn))(png_constcast(png_structrp,png_ptr),
warning_message + offset);
@@ -712,42 +657,9 @@ png_default_error,(png_const_structrp png_ptr, png_const_charp error_message),
PNG_NORETURN)
{
#ifdef PNG_CONSOLE_IO_SUPPORTED
-#ifdef PNG_ERROR_NUMBERS_SUPPORTED
- /* Check on NULL only added in 1.5.4 */
- if (error_message != NULL && *error_message == PNG_LITERAL_SHARP)
- {
- /* Strip "#nnnn " from beginning of error message. */
- int offset;
- char error_number[16];
- for (offset = 0; offset<15; offset++)
- {
- error_number[offset] = error_message[offset + 1];
- if (error_message[offset] == ' ')
- break;
- }
-
- if ((offset > 1) && (offset < 15))
- {
- error_number[offset - 1] = '\0';
- fprintf(stderr, "libpng error no. %s: %s",
- error_number, error_message + offset + 1);
- fprintf(stderr, PNG_STRING_NEWLINE);
- }
-
- else
- {
- fprintf(stderr, "libpng error: %s, offset=%d",
- error_message, offset);
- fprintf(stderr, PNG_STRING_NEWLINE);
- }
- }
- else
-#endif
- {
- fprintf(stderr, "libpng error: %s", error_message ? error_message :
- "undefined");
- fprintf(stderr, PNG_STRING_NEWLINE);
- }
+ fprintf(stderr, "libpng error: %s", error_message ? error_message :
+ "undefined");
+ fprintf(stderr, PNG_STRING_NEWLINE);
#else
PNG_UNUSED(error_message) /* Make compiler happy */
#endif
@@ -785,40 +697,8 @@ static void /* PRIVATE */
png_default_warning(png_const_structrp png_ptr, png_const_charp warning_message)
{
#ifdef PNG_CONSOLE_IO_SUPPORTED
-# ifdef PNG_ERROR_NUMBERS_SUPPORTED
- if (*warning_message == PNG_LITERAL_SHARP)
- {
- int offset;
- char warning_number[16];
- for (offset = 0; offset < 15; offset++)
- {
- warning_number[offset] = warning_message[offset + 1];
- if (warning_message[offset] == ' ')
- break;
- }
-
- if ((offset > 1) && (offset < 15))
- {
- warning_number[offset + 1] = '\0';
- fprintf(stderr, "libpng warning no. %s: %s",
- warning_number, warning_message + offset);
- fprintf(stderr, PNG_STRING_NEWLINE);
- }
-
- else
- {
- fprintf(stderr, "libpng warning: %s",
- warning_message);
- fprintf(stderr, PNG_STRING_NEWLINE);
- }
- }
- else
-# endif
-
- {
- fprintf(stderr, "libpng warning: %s", warning_message);
- fprintf(stderr, PNG_STRING_NEWLINE);
- }
+ fprintf(stderr, "libpng warning: %s", warning_message);
+ fprintf(stderr, PNG_STRING_NEWLINE);
#else
PNG_UNUSED(warning_message) /* Make compiler happy */
#endif
@@ -866,12 +746,8 @@ png_get_error_ptr(png_const_structrp png_ptr)
void PNGAPI
png_set_strip_error_numbers(png_structrp png_ptr, png_uint_32 strip_mode)
{
- if (png_ptr != NULL)
- {
- png_ptr->flags &=
- ((~(PNG_FLAG_STRIP_ERROR_NUMBERS |
- PNG_FLAG_STRIP_ERROR_TEXT))&strip_mode);
- }
+ PNG_UNUSED(png_ptr)
+ PNG_UNUSED(strip_mode)
}
#endif
diff --git a/src/3rdparty/libpng/pnglibconf.h b/src/3rdparty/libpng/pnglibconf.h
index f15fc16dade..00432d6c033 100644
--- a/src/3rdparty/libpng/pnglibconf.h
+++ b/src/3rdparty/libpng/pnglibconf.h
@@ -1,6 +1,6 @@
/* pnglibconf.h - library build configuration */
-/* libpng version 1.6.50 */
+/* libpng version 1.6.51 */
/* Copyright (c) 2018-2025 Cosmin Truta */
/* Copyright (c) 1998-2002,2004,2006-2018 Glenn Randers-Pehrson */
diff --git a/src/3rdparty/libpng/pngpriv.h b/src/3rdparty/libpng/pngpriv.h
index e3054b90aae..fc8d461cf5f 100644
--- a/src/3rdparty/libpng/pngpriv.h
+++ b/src/3rdparty/libpng/pngpriv.h
@@ -302,7 +302,7 @@
# define PNG_LOONGARCH_LSX_IMPLEMENTATION 0
#endif
-#if PNG_RISCV_RVV_OPT > 0
+#if PNG_RISCV_RVV_OPT > 0 && __riscv_v >= 1000000
# define PNG_FILTER_OPTIMIZATIONS png_init_filter_functions_rvv
# ifndef PNG_RISCV_RVV_IMPLEMENTATION
/* Use the intrinsics code by default. */
@@ -310,7 +310,7 @@
# endif
#else
# define PNG_RISCV_RVV_IMPLEMENTATION 0
-#endif
+#endif /* PNG_RISCV_RVV_OPT > 0 && __riscv_v >= 1000000 */
/* Is this a build of a DLL where compilation of the object modules requires
* different preprocessor settings to those required for a simple library? If
@@ -710,7 +710,7 @@
/* #define PNG_FLAG_KEEP_UNKNOWN_CHUNKS 0x8000U */
/* #define PNG_FLAG_KEEP_UNSAFE_CHUNKS 0x10000U */
#define PNG_FLAG_LIBRARY_MISMATCH 0x20000U
-#define PNG_FLAG_STRIP_ERROR_NUMBERS 0x40000U
+ /* 0x40000U unused */
#define PNG_FLAG_STRIP_ERROR_TEXT 0x80000U
#define PNG_FLAG_BENIGN_ERRORS_WARN 0x100000U /* Added to libpng-1.4.0 */
#define PNG_FLAG_APP_WARNINGS_WARN 0x200000U /* Added to libpng-1.6.0 */
@@ -1546,7 +1546,7 @@ PNG_INTERNAL_FUNCTION(void,png_read_filter_row_paeth4_lsx,(png_row_infop
row_info, png_bytep row, png_const_bytep prev_row),PNG_EMPTY);
#endif
-#if PNG_RISCV_RVV_OPT > 0
+#if PNG_RISCV_RVV_IMPLEMENTATION == 1
PNG_INTERNAL_FUNCTION(void,png_read_filter_row_up_rvv,(png_row_infop
row_info, png_bytep row, png_const_bytep prev_row),PNG_EMPTY);
PNG_INTERNAL_FUNCTION(void,png_read_filter_row_sub3_rvv,(png_row_infop
@@ -2175,7 +2175,7 @@ PNG_INTERNAL_FUNCTION(void, png_init_filter_functions_lsx,
(png_structp png_ptr, unsigned int bpp), PNG_EMPTY);
#endif
-# if PNG_RISCV_RVV_OPT > 0
+# if PNG_RISCV_RVV_IMPLEMENTATION == 1
PNG_INTERNAL_FUNCTION(void, png_init_filter_functions_rvv,
(png_structp png_ptr, unsigned int bpp), PNG_EMPTY);
#endif
diff --git a/src/3rdparty/libpng/pngread.c b/src/3rdparty/libpng/pngread.c
index 212afb7d215..79917daaaf9 100644
--- a/src/3rdparty/libpng/pngread.c
+++ b/src/3rdparty/libpng/pngread.c
@@ -3129,6 +3129,54 @@ png_image_read_colormapped(png_voidp argument)
}
}
+/* Row reading for interlaced 16-to-8 bit depth conversion with local buffer. */
+static int
+png_image_read_direct_scaled(png_voidp argument)
+{
+ png_image_read_control *display = png_voidcast(png_image_read_control*,
+ argument);
+ png_imagep image = display->image;
+ png_structrp png_ptr = image->opaque->png_ptr;
+ png_bytep local_row = png_voidcast(png_bytep, display->local_row);
+ png_bytep first_row = png_voidcast(png_bytep, display->first_row);
+ ptrdiff_t row_bytes = display->row_bytes;
+ int passes;
+
+ /* Handle interlacing. */
+ switch (png_ptr->interlaced)
+ {
+ case PNG_INTERLACE_NONE:
+ passes = 1;
+ break;
+
+ case PNG_INTERLACE_ADAM7:
+ passes = PNG_INTERLACE_ADAM7_PASSES;
+ break;
+
+ default:
+ png_error(png_ptr, "unknown interlace type");
+ }
+
+ /* Read each pass using local_row as intermediate buffer. */
+ while (--passes >= 0)
+ {
+ png_uint_32 y = image->height;
+ png_bytep output_row = first_row;
+
+ for (; y > 0; --y)
+ {
+ /* Read into local_row (gets transformed 8-bit data). */
+ png_read_row(png_ptr, local_row, NULL);
+
+ /* Copy from local_row to user buffer. */
+ memcpy(output_row, local_row, (size_t)row_bytes);
+ output_row += row_bytes;
+ }
+ }
+
+ return 1;
+}
+
/* Just the row reading part of png_image_read. */
static int
png_image_read_composite(png_voidp argument)
@@ -3547,6 +3595,7 @@ png_image_read_direct(png_voidp argument)
int linear = (format & PNG_FORMAT_FLAG_LINEAR) != 0;
int do_local_compose = 0;
int do_local_background = 0; /* to avoid double gamma correction bug */
+ int do_local_scale = 0; /* for interlaced 16-to-8 bit conversion */
int passes = 0;
/* Add transforms to ensure the correct output format is produced then check
@@ -3680,8 +3729,16 @@ png_image_read_direct(png_voidp argument)
png_set_expand_16(png_ptr);
else /* 8-bit output */
+ {
png_set_scale_16(png_ptr);
+ /* For interlaced images, use local_row buffer to avoid overflow
+ * in png_combine_row() which writes using IHDR bit-depth.
+ */
+ if (png_ptr->interlaced != 0)
+ do_local_scale = 1;
+ }
+
change &= ~PNG_FORMAT_FLAG_LINEAR;
}
@@ -3957,6 +4014,24 @@ png_image_read_direct(png_voidp argument)
return result;
}
+ else if (do_local_scale != 0)
+ {
+ /* For interlaced 16-to-8 conversion, use an intermediate row buffer
+ * to avoid buffer overflows in png_combine_row. The local_row is sized
+ * for the transformed (8-bit) output, preventing the overflow that would
+ * occur if png_combine_row wrote 16-bit data directly to the user buffer.
+ */
+ int result;
+ png_voidp row = png_malloc(png_ptr, png_get_rowbytes(png_ptr, info_ptr));
+
+ display->local_row = row;
+ result = png_safe_execute(image, png_image_read_direct_scaled, display);
+ display->local_row = NULL;
+ png_free(png_ptr, row);
+
+ return result;
+ }
+
else
{
png_alloc_size_t row_bytes = (png_alloc_size_t)display->row_bytes;
diff --git a/src/3rdparty/libpng/pngrtran.c b/src/3rdparty/libpng/pngrtran.c
index 1809db70473..2f520225515 100644
--- a/src/3rdparty/libpng/pngrtran.c
+++ b/src/3rdparty/libpng/pngrtran.c
@@ -501,9 +501,19 @@ png_set_quantize(png_structrp png_ptr, png_colorp palette,
{
int i;
+ /* Initialize the array to index colors.
+ *
+ * Ensure quantize_index can fit 256 elements (PNG_MAX_PALETTE_LENGTH)
+ * rather than num_palette elements. This is to prevent buffer overflows
+ * caused by malformed PNG files with out-of-range palette indices.
+ *
+ * Be careful to avoid leaking memory. Applications are allowed to call
+ * this function more than once per png_struct.
+ */
+ png_free(png_ptr, png_ptr->quantize_index);
png_ptr->quantize_index = (png_bytep)png_malloc(png_ptr,
- (png_alloc_size_t)num_palette);
- for (i = 0; i < num_palette; i++)
+ PNG_MAX_PALETTE_LENGTH);
+ for (i = 0; i < PNG_MAX_PALETTE_LENGTH; i++)
png_ptr->quantize_index[i] = (png_byte)i;
}
@@ -515,15 +525,14 @@ png_set_quantize(png_structrp png_ptr, png_colorp palette,
* Perhaps not the best solution, but good enough.
*/
- int i;
+ png_bytep quantize_sort;
+ int i, j;
- /* Initialize an array to sort colors */
- png_ptr->quantize_sort = (png_bytep)png_malloc(png_ptr,
+ /* Initialize the local array to sort colors. */
+ quantize_sort = (png_bytep)png_malloc(png_ptr,
(png_alloc_size_t)num_palette);
-
- /* Initialize the quantize_sort array */
for (i = 0; i < num_palette; i++)
- png_ptr->quantize_sort[i] = (png_byte)i;
+ quantize_sort[i] = (png_byte)i;
/* Find the least used palette entries by starting a
* bubble sort, and running it until we have sorted
@@ -535,19 +544,18 @@ png_set_quantize(png_structrp png_ptr, png_colorp palette,
for (i = num_palette - 1; i >= maximum_colors; i--)
{
int done; /* To stop early if the list is pre-sorted */
- int j;
done = 1;
for (j = 0; j < i; j++)
{
- if (histogram[png_ptr->quantize_sort[j]]
- < histogram[png_ptr->quantize_sort[j + 1]])
+ if (histogram[quantize_sort[j]]
+ < histogram[quantize_sort[j + 1]])
{
png_byte t;
- t = png_ptr->quantize_sort[j];
- png_ptr->quantize_sort[j] = png_ptr->quantize_sort[j + 1];
- png_ptr->quantize_sort[j + 1] = t;
+ t = quantize_sort[j];
+ quantize_sort[j] = quantize_sort[j + 1];
+ quantize_sort[j + 1] = t;
done = 0;
}
}
@@ -559,18 +567,18 @@ png_set_quantize(png_structrp png_ptr, png_colorp palette,
/* Swap the palette around, and set up a table, if necessary */
if (full_quantize != 0)
{
- int j = num_palette;
+ j = num_palette;
/* Put all the useful colors within the max, but don't
* move the others.
*/
for (i = 0; i < maximum_colors; i++)
{
- if ((int)png_ptr->quantize_sort[i] >= maximum_colors)
+ if ((int)quantize_sort[i] >= maximum_colors)
{
do
j--;
- while ((int)png_ptr->quantize_sort[j] >= maximum_colors);
+ while ((int)quantize_sort[j] >= maximum_colors);
palette[i] = palette[j];
}
@@ -578,7 +586,7 @@ png_set_quantize(png_structrp png_ptr, png_colorp palette,
}
else
{
- int j = num_palette;
+ j = num_palette;
/* Move all the used colors inside the max limit, and
* develop a translation table.
@@ -586,13 +594,13 @@ png_set_quantize(png_structrp png_ptr, png_colorp palette,
for (i = 0; i < maximum_colors; i++)
{
/* Only move the colors we need to */
- if ((int)png_ptr->quantize_sort[i] >= maximum_colors)
+ if ((int)quantize_sort[i] >= maximum_colors)
{
png_color tmp_color;
do
j--;
- while ((int)png_ptr->quantize_sort[j] >= maximum_colors);
+ while ((int)quantize_sort[j] >= maximum_colors);
tmp_color = palette[j];
palette[j] = palette[i];
@@ -630,8 +638,7 @@ png_set_quantize(png_structrp png_ptr, png_colorp palette,
}
}
}
- png_free(png_ptr, png_ptr->quantize_sort);
- png_ptr->quantize_sort = NULL;
+ png_free(png_ptr, quantize_sort);
}
else
{
@@ -1774,19 +1781,51 @@ png_init_read_transformations(png_structrp png_ptr)
}
else /* if (png_ptr->trans_alpha[i] != 0xff) */
{
- png_byte v, w;
-
- v = png_ptr->gamma_to_1[palette[i].red];
- png_composite(w, v, png_ptr->trans_alpha[i], back_1.red);
- palette[i].red = png_ptr->gamma_from_1[w];
-
- v = png_ptr->gamma_to_1[palette[i].green];
- png_composite(w, v, png_ptr->trans_alpha[i], back_1.green);
- palette[i].green = png_ptr->gamma_from_1[w];
-
- v = png_ptr->gamma_to_1[palette[i].blue];
- png_composite(w, v, png_ptr->trans_alpha[i], back_1.blue);
- palette[i].blue = png_ptr->gamma_from_1[w];
+ if ((png_ptr->flags & PNG_FLAG_OPTIMIZE_ALPHA) != 0)
+ {
+ /* Premultiply only:
+ * component = round((component * alpha) / 255)
+ */
+ png_uint_32 component;
+
+ component = png_ptr->gamma_to_1[palette[i].red];
+ component =
+ (component * png_ptr->trans_alpha[i] + 128) / 255;
+ palette[i].red = png_ptr->gamma_from_1[component];
+
+ component = png_ptr->gamma_to_1[palette[i].green];
+ component =
+ (component * png_ptr->trans_alpha[i] + 128) / 255;
+ palette[i].green = png_ptr->gamma_from_1[component];
+
+ component = png_ptr->gamma_to_1[palette[i].blue];
+ component =
+ (component * png_ptr->trans_alpha[i] + 128) / 255;
+ palette[i].blue = png_ptr->gamma_from_1[component];
+ }
+ else
+ {
+ /* Composite with background color:
+ * component =
+ * alpha * component + (1 - alpha) * background
+ */
+ png_byte v, w;
+
+ v = png_ptr->gamma_to_1[palette[i].red];
+ png_composite(w, v,
+ png_ptr->trans_alpha[i], back_1.red);
+ palette[i].red = png_ptr->gamma_from_1[w];
+
+ v = png_ptr->gamma_to_1[palette[i].green];
+ png_composite(w, v,
+ png_ptr->trans_alpha[i], back_1.green);
+ palette[i].green = png_ptr->gamma_from_1[w];
+
+ v = png_ptr->gamma_to_1[palette[i].blue];
+ png_composite(w, v,
+ png_ptr->trans_alpha[i], back_1.blue);
+ palette[i].blue = png_ptr->gamma_from_1[w];
+ }
}
}
else
@@ -5009,13 +5048,8 @@ png_do_read_transformations(png_structrp png_ptr, png_row_infop row_info)
#ifdef PNG_READ_QUANTIZE_SUPPORTED
if ((png_ptr->transformations & PNG_QUANTIZE) != 0)
- {
png_do_quantize(row_info, png_ptr->row_buf + 1,
png_ptr->palette_lookup, png_ptr->quantize_index);
-
- if (row_info->rowbytes == 0)
- png_error(png_ptr, "png_do_quantize returned rowbytes=0");
- }
#endif /* READ_QUANTIZE */
#ifdef PNG_READ_EXPAND_16_SUPPORTED
diff --git a/src/3rdparty/libpng/pngstruct.h b/src/3rdparty/libpng/pngstruct.h
index 084422bc1e2..fe5fa041554 100644
--- a/src/3rdparty/libpng/pngstruct.h
+++ b/src/3rdparty/libpng/pngstruct.h
@@ -405,7 +405,6 @@ struct png_struct_def
#ifdef PNG_READ_QUANTIZE_SUPPORTED
/* The following three members were added at version 1.0.14 and 1.2.4 */
- png_bytep quantize_sort; /* working sort array */
png_bytep index_to_palette; /* where the original index currently is
in the palette */
png_bytep palette_to_index; /* which original index points to this
diff --git a/src/3rdparty/libpng/pngwrite.c b/src/3rdparty/libpng/pngwrite.c
index 35a5d17b601..83148960eff 100644
--- a/src/3rdparty/libpng/pngwrite.c
+++ b/src/3rdparty/libpng/pngwrite.c
@@ -2173,8 +2173,7 @@ png_image_write_main(png_voidp argument)
* before it is written. This only applies when the input is 16-bit and
* either there is an alpha channel or it is converted to 8-bit.
*/
- if ((linear != 0 && alpha != 0 ) ||
- (colormap == 0 && display->convert_to_8bit != 0))
+ if (linear != 0 && (alpha != 0 || display->convert_to_8bit != 0))
{
png_bytep row = png_voidcast(png_bytep, png_malloc(png_ptr,
png_get_rowbytes(png_ptr, info_ptr)));
diff --git a/src/3rdparty/libpng/qt_attribution.json b/src/3rdparty/libpng/qt_attribution.json
index 9327dee564c..fe8ba663881 100644
--- a/src/3rdparty/libpng/qt_attribution.json
+++ b/src/3rdparty/libpng/qt_attribution.json
@@ -7,8 +7,8 @@
"Description": "libpng is the official PNG reference library.",
"Homepage": "http://www.libpng.org/pub/png/libpng.html",
- "Version": "1.6.50",
- "DownloadLocation": "https://download.sourceforge.net/libpng/libpng-1.6.50.tar.xz",
+ "Version": "1.6.51",
+ "DownloadLocation": "https://download.sourceforge.net/libpng/libpng-1.6.51.tar.xz",
"PURL": "pkg:github/pnggroup/libpng@v$<VERSION>",
"CPE": "cpe:2.3:a:libpng:libpng:$<VERSION>:*:*:*:*:*:*:*",
diff --git a/src/corelib/CMakeLists.txt b/src/corelib/CMakeLists.txt
index 55d375f0350..32b70a1f288 100644
--- a/src/corelib/CMakeLists.txt
+++ b/src/corelib/CMakeLists.txt
@@ -701,6 +701,7 @@ qt_internal_extend_target(Core CONDITION APPLE
kernel/qcore_mac.mm kernel/qcore_mac_p.h
kernel/qcoreapplication_mac.cpp
kernel/qeventdispatcher_cf.mm kernel/qeventdispatcher_cf_p.h
+ platform/darwin/qdarwinsecurityscopedfileengine.mm platform/darwin/qdarwinsecurityscopedfileengine_p.h
LIBRARIES
${FWCoreFoundation}
${FWFoundation}
@@ -1510,6 +1511,7 @@ endif()
qt_internal_extend_target(Core CONDITION WASM
SOURCES
+ platform/wasm/qwasmanimationdriver.cpp platform/wasm/qwasmanimationdriver_p.h
platform/wasm/qwasmglobal.cpp platform/wasm/qwasmglobal_p.h
platform/wasm/qstdweb.cpp platform/wasm/qstdweb_p.h
platform/wasm/qwasmsocket.cpp platform/wasm/qwasmsocket_p.h
diff --git a/src/corelib/animation/qabstractanimation.cpp b/src/corelib/animation/qabstractanimation.cpp
index 17814c6756a..c3e1ba4010f 100644
--- a/src/corelib/animation/qabstractanimation.cpp
+++ b/src/corelib/animation/qabstractanimation.cpp
@@ -113,6 +113,10 @@
#include "qabstractanimation_p.h"
+#if defined(Q_OS_WASM)
+#include <QtCore/private/qwasmanimationdriver_p.h>
+#endif
+
#include <QtCore/qmath.h>
#include <QtCore/qcoreevent.h>
#include <QtCore/qpointer.h>
diff --git a/src/corelib/animation/qabstractanimation_p.h b/src/corelib/animation/qabstractanimation_p.h
index b4f462071a7..1eaa475f613 100644
--- a/src/corelib/animation/qabstractanimation_p.h
+++ b/src/corelib/animation/qabstractanimation_p.h
@@ -23,6 +23,10 @@
#include <private/qproperty_p.h>
#include <qabstractanimation.h>
+#if defined(Q_OS_WASM)
+#include <QtCore/private/qwasmanimationdriver_p.h>
+#endif
+
QT_REQUIRE_CONFIG(animation);
QT_BEGIN_NAMESPACE
@@ -184,7 +188,11 @@ private:
friend class QAnimationDriver;
QAnimationDriver *driver;
+#if defined(Q_OS_WASM)
+ QWasmAnimationDriver defaultDriver;
+#else
QDefaultAnimationDriver defaultDriver;
+#endif
QBasicTimer pauseTimer;
diff --git a/src/corelib/doc/qtcore.qdocconf b/src/corelib/doc/qtcore.qdocconf
index d2b386373a0..b3e4e9d30a9 100644
--- a/src/corelib/doc/qtcore.qdocconf
+++ b/src/corelib/doc/qtcore.qdocconf
@@ -21,7 +21,7 @@ qhp.QtCore.virtualFolder = qtcore
qhp.QtCore.indexTitle = Qt Core
qhp.QtCore.indexRoot =
-qhp.QtCore.subprojects = manual classes
+qhp.QtCore.subprojects = manual examples classes
qhp.QtCore.subprojects.manual.title = Qt Core
qhp.QtCore.subprojects.manual.indexTitle = Qt Core module topics
qhp.QtCore.subprojects.manual.type = manual
@@ -31,6 +31,11 @@ qhp.QtCore.subprojects.classes.indexTitle = Qt Core C++ Classes
qhp.QtCore.subprojects.classes.selectors = class fake:headerfile
qhp.QtCore.subprojects.classes.sortPages = true
+qhp.QtCore.subprojects.examples.title = Examples
+qhp.QtCore.subprojects.examples.indexTitle = Qt Core Examples
+qhp.QtCore.subprojects.examples.selectors = example
+qhp.QtCore.subprojects.examples.sortPages = true
+
tagfile = ../../../doc/qtcore/qtcore.tags
# Make QtCore depend on all doc modules; this ensures complete inheritance
diff --git a/src/corelib/doc/snippets/CMakeLists.txt b/src/corelib/doc/snippets/CMakeLists.txt
index 5521bf1f651..55db84ccbea 100644
--- a/src/corelib/doc/snippets/CMakeLists.txt
+++ b/src/corelib/doc/snippets/CMakeLists.txt
@@ -66,6 +66,7 @@ if(QT_FEATURE_gui)
buffer/buffer.cpp
qdebug/qdebugsnippet.cpp
)
+ add_subdirectory(eventfilters)
endif()
set_target_properties(corelib_snippets PROPERTIES COMPILE_OPTIONS "-w")
@@ -76,7 +77,6 @@ endif()
set_target_properties(corelib_snippets PROPERTIES UNITY_BUILD OFF)
-add_subdirectory(eventfilters)
add_subdirectory(qmetaobject-invokable)
add_subdirectory(qmetaobject-revision)
if(QT_FEATURE_process)
diff --git a/src/corelib/doc/src/qtcore.qdoc b/src/corelib/doc/src/qtcore.qdoc
index ec5fa564639..fbcd02aeea5 100644
--- a/src/corelib/doc/src/qtcore.qdoc
+++ b/src/corelib/doc/src/qtcore.qdoc
@@ -31,3 +31,12 @@
target_link_libraries(mytarget PRIVATE Qt6::CorePrivate)
\endcode
*/
+
+/*!
+ \group corelib_examples
+ \title Qt Core Examples
+
+ \brief Examples for the Qt Core.
+
+ To learn how to use features of the Qt Core module, see examples:
+*/
diff --git a/src/corelib/io/qfile.cpp b/src/corelib/io/qfile.cpp
index e1fc043a0ff..0184fd838aa 100644
--- a/src/corelib/io/qfile.cpp
+++ b/src/corelib/io/qfile.cpp
@@ -592,6 +592,10 @@ QFile::rename(const QString &newName)
return false;
}
+ // Keep engine for target alive during the operation
+ // FIXME: Involve the target engine in the operation
+ auto targetEngine = QFileSystemEngine::createLegacyEngine(newName);
+
// If the file exists and it is a case-changing rename ("foo" -> "Foo"),
// compare Ids to make sure it really is a different file.
// Note: this does not take file engines into account.
@@ -738,6 +742,11 @@ QFile::link(const QString &linkName)
qWarning("QFile::link: Empty or null file name");
return false;
}
+
+ // Keep engine for target alive during the operation
+ // FIXME: Involve the target engine in the operation
+ auto targetEngine = QFileSystemEngine::createLegacyEngine(linkName);
+
QFileInfo fi(linkName);
if (d->engine()->link(fi.absoluteFilePath())) {
unsetError();
@@ -771,6 +780,10 @@ bool QFilePrivate::copy(const QString &newName)
Q_ASSERT(error == QFile::NoError);
Q_ASSERT(!q->isOpen());
+ // Keep engine for target alive during the operation
+ // FIXME: Involve the target engine in the operation
+ auto targetEngine = QFileSystemEngine::createLegacyEngine(newName);
+
// Some file engines can perform this copy more efficiently (e.g., Windows
// calling CopyFile).
if (engine()->copy(newName))
diff --git a/src/corelib/io/qfilesystemengine.cpp b/src/corelib/io/qfilesystemengine.cpp
index 03da2331e05..46d4cb709e2 100644
--- a/src/corelib/io/qfilesystemengine.cpp
+++ b/src/corelib/io/qfilesystemengine.cpp
@@ -190,6 +190,14 @@ QFileSystemEngine::createLegacyEngine(QFileSystemEntry &entry, QFileSystemMetaDa
return engine;
}
+std::unique_ptr<QAbstractFileEngine>
+QFileSystemEngine::createLegacyEngine(const QString &fileName)
+{
+ QFileSystemEntry entry(fileName);
+ QFileSystemMetaData metaData;
+ return createLegacyEngine(entry, metaData);
+}
+
//static
QString QFileSystemEngine::resolveUserName(const QFileSystemEntry &entry, QFileSystemMetaData &metaData)
{
diff --git a/src/corelib/io/qfilesystemengine_p.h b/src/corelib/io/qfilesystemengine_p.h
index ee70ccc1e1b..46eeeda569e 100644
--- a/src/corelib/io/qfilesystemengine_p.h
+++ b/src/corelib/io/qfilesystemengine_p.h
@@ -161,6 +161,8 @@ public:
static std::unique_ptr<QAbstractFileEngine>
createLegacyEngine(QFileSystemEntry &entry, QFileSystemMetaData &data);
+ static std::unique_ptr<QAbstractFileEngine>
+ createLegacyEngine(const QString &fileName);
private:
static QString slowCanonicalized(const QString &path);
diff --git a/src/corelib/io/qfsfileengine_p.h b/src/corelib/io/qfsfileengine_p.h
index 2de6cb0cb73..8ad673bf0bf 100644
--- a/src/corelib/io/qfsfileengine_p.h
+++ b/src/corelib/io/qfsfileengine_p.h
@@ -82,7 +82,7 @@ public:
bool setFileTime(const QDateTime &newDate, QFile::FileTime time) override;
QDateTime fileTime(QFile::FileTime time) const override;
void setFileName(const QString &file) override;
- void setFileEntry(QFileSystemEntry &&entry);
+ virtual void setFileEntry(QFileSystemEntry &&entry);
int handle() const override;
#ifndef QT_NO_FILESYSTEMITERATOR
diff --git a/src/corelib/kernel/qmetacontainer.cpp b/src/corelib/kernel/qmetacontainer.cpp
index 4b4ea06d8b9..6173198a972 100644
--- a/src/corelib/kernel/qmetacontainer.cpp
+++ b/src/corelib/kernel/qmetacontainer.cpp
@@ -210,7 +210,7 @@ void QMetaContainer::destroyIterator(const void *iterator) const
*/
bool QMetaContainer::compareIterator(const void *i, const void *j) const
{
- return hasIterator() ? d_ptr->compareIteratorFn(i, j) : false;
+ return i == j || (hasIterator() && d_ptr->compareIteratorFn(i, j));
}
/*!
@@ -249,7 +249,7 @@ void QMetaContainer::advanceIterator(void *iterator, qsizetype step) const
*/
qsizetype QMetaContainer::diffIterator(const void *i, const void *j) const
{
- return hasIterator() ? d_ptr->diffIteratorFn(i, j) : 0;
+ return (i != j && hasIterator()) ? d_ptr->diffIteratorFn(i, j) : 0;
}
/*!
@@ -327,7 +327,7 @@ void QMetaContainer::destroyConstIterator(const void *iterator) const
*/
bool QMetaContainer::compareConstIterator(const void *i, const void *j) const
{
- return hasConstIterator() ? d_ptr->compareConstIteratorFn(i, j) : false;
+ return i == j || (hasConstIterator() && d_ptr->compareConstIteratorFn(i, j));
}
/*!
@@ -366,7 +366,7 @@ void QMetaContainer::advanceConstIterator(void *iterator, qsizetype step) const
*/
qsizetype QMetaContainer::diffConstIterator(const void *i, const void *j) const
{
- return hasConstIterator() ? d_ptr->diffConstIteratorFn(i, j) : 0;
+ return (i != j && hasConstIterator()) ? d_ptr->diffConstIteratorFn(i, j) : 0;
}
QT_END_NAMESPACE
diff --git a/src/corelib/kernel/qobject_impl.h b/src/corelib/kernel/qobject_impl.h
index b57d7e50ccd..34e6bd84f3f 100644
--- a/src/corelib/kernel/qobject_impl.h
+++ b/src/corelib/kernel/qobject_impl.h
@@ -1,8 +1,6 @@
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
-#ifndef Q_QDOC
-
#ifndef QOBJECT_H
#error Do not include qobject_impl.h directly
#endif
@@ -41,5 +39,3 @@ namespace QtPrivate {
QT_END_NAMESPACE
-
-#endif
diff --git a/src/corelib/platform/darwin/qdarwinsecurityscopedfileengine.mm b/src/corelib/platform/darwin/qdarwinsecurityscopedfileengine.mm
new file mode 100644
index 00000000000..cb38445f4fe
--- /dev/null
+++ b/src/corelib/platform/darwin/qdarwinsecurityscopedfileengine.mm
@@ -0,0 +1,552 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+// Qt-Security score:significant reason:default
+
+#include "qdarwinsecurityscopedfileengine_p.h"
+
+#include <QtCore/qloggingcategory.h>
+#include <QtCore/qstandardpaths.h>
+#include <QtCore/qreadwritelock.h>
+#include <QtCore/qscopedvaluerollback.h>
+
+#include <QtCore/private/qcore_mac_p.h>
+#include <QtCore/private/qfsfileengine_p.h>
+#include <QtCore/private/qfilesystemengine_p.h>
+
+#include <thread>
+#include <mutex>
+
+#include <CoreFoundation/CoreFoundation.h>
+#include <Foundation/NSURL.h>
+
+QT_BEGIN_NAMESPACE
+
+using namespace Qt::StringLiterals;
+
+Q_STATIC_LOGGING_CATEGORY(lcSecEngine, "qt.core.io.security-scoped-fileengine", QtCriticalMsg)
+
+template<typename T> class BackgroundLoader;
+
+/*
+ File engine handler for security scoped file paths.
+
+ Installs itself as soon as QtCore is loaded if the application
+ is sandboxed (optionally on macOS, and always on iOS and friends).
+*/
+class SecurityScopedFileEngineHandler : public QAbstractFileEngineHandler
+{
+public:
+ SecurityScopedFileEngineHandler();
+ ~SecurityScopedFileEngineHandler();
+
+ void registerPossiblySecurityScopedURL(NSURL *url);
+
+ std::unique_ptr<QAbstractFileEngine> create(const QString &fileName) const override;
+
+ static BackgroundLoader<SecurityScopedFileEngineHandler>& get();
+
+private:
+ Q_DISABLE_COPY_MOVE(SecurityScopedFileEngineHandler)
+
+ void saveBookmark(NSURL *url);
+ void saveBookmarks();
+
+ NSURL *bookmarksFile() const;
+
+ static NSString *cacheKeyForUrl(NSURL *url);
+ static NSString *cacheKeyForPath(const QString &url);
+
+ NSMutableDictionary *m_bookmarks = nullptr;
+ mutable QReadWriteLock m_bookmarkLock;
+
+ friend class SecurityScopedFileEngine;
+};
+
+/*
+ Helper class for asynchronous instantiation of types.
+*/
+template<typename T>
+class BackgroundLoader
+{
+public:
+ explicit BackgroundLoader(bool shouldLoad) {
+ if (shouldLoad) {
+ m_thread = std::thread([this]() {
+ m_instance = std::make_unique<T>();
+ });
+ }
+ }
+
+ ~BackgroundLoader()
+ {
+ std::scoped_lock lock(m_mutex);
+ if (m_thread.joinable())
+ m_thread.join();
+ }
+
+ T* operator->() const
+ {
+ std::scoped_lock lock(m_mutex);
+ if (m_thread.joinable())
+ m_thread.join();
+ return m_instance.get();
+ }
+
+ explicit operator bool() const
+ {
+ std::scoped_lock lock(m_mutex);
+ return m_thread.joinable() || m_instance;
+ }
+
+private:
+ mutable std::mutex m_mutex;
+ mutable std::thread m_thread;
+ std::unique_ptr<T> m_instance;
+};
+
+/*
+ Thread-safe background-loading of optional security scoped handler,
+ with the ability to kick off instantiation early during program load.
+*/
+BackgroundLoader<SecurityScopedFileEngineHandler>& SecurityScopedFileEngineHandler::get()
+{
+ using Handler = BackgroundLoader<SecurityScopedFileEngineHandler>;
+ static Handler handler = []() -> Handler {
+ if (!qt_apple_isSandboxed())
+ return Handler{false};
+
+ qCInfo(lcSecEngine) << "Application sandbox is active. Registering security-scoped file engine.";
+ return Handler{true};
+ }();
+ return handler;
+}
+
+static void initializeSecurityScopedFileEngineHandler()
+{
+ // Kick off loading of bookmarks early in the background
+ std::ignore = SecurityScopedFileEngineHandler::get();
+}
+Q_CONSTRUCTOR_FUNCTION(initializeSecurityScopedFileEngineHandler);
+
+/*
+ Registration function for possibly security scoped URLs.
+
+ Entry points that might provide security scoped URLs such as file
+ dialogs or drag-and-drop should use this function to ensure that
+ the security scoped file engine handler knows about the URL.
+*/
+QUrl qt_apple_urlFromPossiblySecurityScopedURL(NSURL *url)
+{
+ if (auto &handler = SecurityScopedFileEngineHandler::get())
+ handler->registerPossiblySecurityScopedURL(url);
+
+ // Note: The URL itself doesn't encode any of the bookmark data,
+ // neither in the scheme or as fragments or query parameters,
+ // as it's all handled by the bookmark cache in the file engine.
+ return QUrl(QString::fromNSString(url.absoluteString)
+ .normalized(QString::NormalizationForm_C));
+}
+
+static bool checkIfResourceIsReachable(NSURL *url)
+{
+ NSError *error = nullptr;
+ if ([url checkResourceIsReachableAndReturnError:&error])
+ return true;
+
+ // Our goal is to check whether the file exists or not, and if
+ // not, defer creating a bookmark for it. If we get any other
+ // error we want to know.
+ if (![error.domain isEqualToString:NSCocoaErrorDomain] || error.code != NSFileReadNoSuchFileError) {
+ qCWarning(lcSecEngine) << "Unexpected" << error
+ << "when resolving reachability for" << url;
+ }
+
+ return false;
+}
+
+/*
+ File engine for maintaining access lifetime of security-scoped
+ resources on sandboxed Apple platforms.
+
+ Note that there isn't necessarily a 1:1 relationship between
+ the file being operated on by the QFSFileEngine and the security
+ scoped resource that allows access to it, for example in the
+ case of a folder giving access to all files (and sub-folders)
+ within it.
+*/
+class SecurityScopedFileEngine : public QFSFileEngine
+{
+ Q_DECLARE_PRIVATE(QFSFileEngine)
+public:
+ SecurityScopedFileEngine(const QString &fileName, NSURL *securityScopedUrl)
+ : QFSFileEngine(fileName)
+ , m_securityScopedUrl([securityScopedUrl retain])
+ {
+ startAccessingSecurityScopedResource();
+ }
+
+ ~SecurityScopedFileEngine()
+ {
+ stopAccessingSecurityScopedResource();
+ [m_securityScopedUrl release];
+ }
+
+ void setFileName(const QString &fileName) override
+ {
+ QFileSystemEntry entry(fileName);
+ setFileEntry(std::move(entry));
+ }
+
+ void setFileEntry(QFileSystemEntry &&entry) override
+ {
+ // We can't rely on the new entry being accessible under the same
+ // security scope as the original path, or even that the new path
+ // is a security scoped resource, so stop access here, and start
+ // access for the new resource below if needed.
+ stopAccessingSecurityScopedResource();
+ [m_securityScopedUrl release];
+ m_securityScopedUrl = nil;
+
+ const QString fileName = entry.filePath();
+ QFSFileEngine::setFileEntry(std::move(entry));
+
+ // The new path may not be a security scoped resource, but if it is
+ // we need to establish access to it. The only way to do that is to
+ // actually create an engine for it, including resolving bookmarks.
+ auto newEngine = SecurityScopedFileEngineHandler::get()->create(fileName);
+ if (auto *engine = dynamic_cast<SecurityScopedFileEngine*>(newEngine.get())) {
+ m_securityScopedUrl = [engine->m_securityScopedUrl retain];
+ startAccessingSecurityScopedResource();
+ }
+ }
+
+private:
+ void startAccessingSecurityScopedResource()
+ {
+ if ([m_securityScopedUrl startAccessingSecurityScopedResource]) {
+ qCDebug(lcSecEngine) << "Started accessing" << m_securityScopedUrl.path
+ << "on behalf of" << fileName(DefaultName);
+
+ m_securityScopeWasReachable = securityScopeIsReachable();
+ } else {
+ qCWarning(lcSecEngine) << "Unexpectedly using security scoped"
+ << "file engine for" << m_securityScopedUrl.path
+ << "on behalf of" << fileName(DefaultName)
+ << "without needing scoped access";
+ }
+ }
+
+ void stopAccessingSecurityScopedResource()
+ {
+ if (!m_securityScopeWasReachable && securityScopeIsReachable()) {
+ // The security scoped URL didn't exist when we first started
+ // accessing it, but it does now, so persist a bookmark for it.
+ qCDebug(lcSecEngine) << "Security scoped resource has been created. Saving bookmark.";
+ SecurityScopedFileEngineHandler::get()->saveBookmark(m_securityScopedUrl);
+ }
+
+ // Note: Stopping access is a no-op if we didn't have access
+ [m_securityScopedUrl stopAccessingSecurityScopedResource];
+ qCDebug(lcSecEngine) << "Stopped accessing" << m_securityScopedUrl.path
+ << "on behalf of" << fileName(DefaultName);
+ }
+
+ bool securityScopeIsReachable() const
+ {
+ return checkIfResourceIsReachable(m_securityScopedUrl);
+ }
+
+ // See note above about relationship to fileName
+ NSURL *m_securityScopedUrl = nullptr;
+ bool m_securityScopeWasReachable = false;
+};
+
+// ----------------------------------------------------------------------
+
+SecurityScopedFileEngineHandler::SecurityScopedFileEngineHandler()
+{
+ QMacAutoReleasePool pool;
+
+ NSURL *savedBookmarks = bookmarksFile();
+ if ([NSFileManager.defaultManager fileExistsAtPath:savedBookmarks.path]) {
+ NSError *error = nullptr;
+ m_bookmarks = [[NSDictionary dictionaryWithContentsOfURL:savedBookmarks
+ error:&error] mutableCopy];
+
+ if (error) {
+ qCWarning(lcSecEngine) << "Failed to load bookmarks from"
+ << savedBookmarks << ":" << error;
+ } else {
+ qCInfo(lcSecEngine) << "Loaded existing bookmarks for" << m_bookmarks.allKeys;
+ }
+ }
+
+ if (!m_bookmarks)
+ m_bookmarks = [NSMutableDictionary new];
+}
+
+SecurityScopedFileEngineHandler::~SecurityScopedFileEngineHandler()
+{
+ [m_bookmarks release];
+}
+
+void SecurityScopedFileEngineHandler::registerPossiblySecurityScopedURL(NSURL *url)
+{
+ QMacAutoReleasePool pool;
+
+ // Start accessing the resource, to check if it's security scoped,
+ // and allow us to create a bookmark for it on both macOS and iOS.
+ if (![url startAccessingSecurityScopedResource])
+ return; // All good, not security scoped
+
+ if (checkIfResourceIsReachable(url)) {
+ // We can access the resource, which means it exists, so we can
+ // create a persistent bookmark for it right away. We want to do
+ // this as soon as possible, so that if the app is terminated the
+ // user can continue working on the file without the app needing
+ // to ask for access again via a file dialog.
+ saveBookmark(url);
+ } else {
+ // The file isn't accessible, likely because it doesn't exist.
+ // As we can only create security scoped bookmarks for files
+ // that exist we store the URL itself for now, and save it to
+ // a bookmark later when we detect that the file has been created.
+ qCInfo(lcSecEngine) << "Resource is not reachable."
+ << "Registering URL" << url << "instead";
+ QWriteLocker locker(&m_bookmarkLock);
+ m_bookmarks[cacheKeyForUrl(url)] = url;
+ }
+
+ // Balance access from above
+ [url stopAccessingSecurityScopedResource];
+
+#if defined(Q_OS_MACOS)
+ // On macOS, unlike iOS, URLs from file dialogs, etc, come with implicit
+ // access already, and we are expected to balance this access with an
+ // explicit stopAccessingSecurityScopedResource. We release the last
+ // access here to unify the behavior between macOS and iOS, and then
+ // leave it up to the SecurityScopedFileEngine to regain access, where
+ // we know the lifetime of resource use, and when to release access.
+ [url stopAccessingSecurityScopedResource];
+#endif
+}
+
+std::unique_ptr<QAbstractFileEngine> SecurityScopedFileEngineHandler::create(const QString &fileName) const
+{
+ QMacAutoReleasePool pool;
+
+ static thread_local bool recursionGuard = false;
+ if (recursionGuard)
+ return nullptr;
+
+ if (fileName.isEmpty())
+ return nullptr;
+
+ QFileSystemEntry fileSystemEntry(fileName);
+ QFileSystemMetaData metaData;
+
+ {
+ // Check if there's another engine that claims to handle the given file name.
+ // This covers non-QFSFileEngines like QTemporaryFileEngine, and QResourceFileEngine.
+ // If there isn't one, we'll get nullptr back, and know that we can access the
+ // file via our special QFSFileEngine.
+ QScopedValueRollback<bool> rollback(recursionGuard, true);
+ if (auto engine = QFileSystemEngine::createLegacyEngine(fileSystemEntry, metaData)) {
+ // Shortcut the logic of the createLegacyEngine call we're in by
+ // just returning this engine now.
+ qCDebug(lcSecEngine) << "Preferring non-QFSFileEngine engine"
+ << engine.get() << "for" << fileName;
+ return engine;
+ }
+ }
+
+ // We're mapping the file name to existing bookmarks below, so make sure
+ // we use as close as we can get to the canonical path. For files that
+ // do not exist we fall back to the cleaned absolute path.
+ auto canonicalEntry = QFileSystemEngine::canonicalName(fileSystemEntry, metaData);
+ if (canonicalEntry.isEmpty())
+ canonicalEntry = QFileSystemEngine::absoluteName(fileSystemEntry);
+
+ if (canonicalEntry.isRelative()) {
+ // We try to map relative paths to absolute above, but doing so requires
+ // knowledge of the current working directory, which we only have if the
+ // working directory has already started access through other means. We
+ // can't explicitly start access of the working directory here, as doing
+ // so requires its name, which we can't get from getcwd() without access.
+ // Fortunately all of the entry points of security scoped URLs such as
+ // file dialogs or drag-and-drop give us absolute paths, and APIs like
+ // QDir::filePath() will construct absolute URLs without needing the
+ // current working directory.
+ qCWarning(lcSecEngine) << "Could not resolve" << fileSystemEntry.filePath()
+ << "against current working working directory";
+ return nullptr;
+ }
+
+ // Clean the path as well, to remove any trailing slashes for directories
+ QString filePath = QDir::cleanPath(canonicalEntry.filePath());
+
+ // Files inside the sandbox container can always be accessed directly
+ static const QString sandboxRoot = QString::fromNSString(NSHomeDirectory());
+ if (filePath.startsWith(sandboxRoot))
+ return nullptr;
+
+ // The same applies to files inside the application's own bundle
+ static const QString bundleRoot = QString::fromNSString(NSBundle.mainBundle.bundlePath);
+ if (filePath.startsWith(bundleRoot))
+ return nullptr;
+
+ qCDebug(lcSecEngine) << "Looking up bookmark for" << filePath << "based on incoming fileName" << fileName;
+
+ // Check if we have a persisted bookmark for this fileName, or
+ // any of its containing directories (which will give us access
+ // to the file).
+ QReadLocker locker(&m_bookmarkLock);
+ auto *cacheKey = cacheKeyForPath(filePath);
+ NSObject *bookmarkData = nullptr;
+ while (cacheKey.length > 1) {
+ bookmarkData = m_bookmarks[cacheKey];
+ if (bookmarkData)
+ break;
+ cacheKey = [cacheKey stringByDeletingLastPathComponent];
+ }
+
+ // We didn't find a bookmark, so there's no point in trying to manage
+ // this file via a SecurityScopedFileEngine.
+ if (!bookmarkData) {
+ qCDebug(lcSecEngine) << "No bookmark found. Falling back to QFSFileEngine.";
+ return nullptr;
+ }
+
+ NSURL *securityScopedUrl = nullptr;
+ if ([bookmarkData isKindOfClass:NSURL.class]) {
+ securityScopedUrl = static_cast<NSURL*>(bookmarkData);
+ } else {
+ NSError *error = nullptr;
+ BOOL bookmarkDataIsStale = NO;
+ securityScopedUrl = [NSURL URLByResolvingBookmarkData:static_cast<NSData*>(bookmarkData)
+ options:
+ #if defined(Q_OS_MACOS)
+ NSURLBookmarkResolutionWithSecurityScope
+ #else
+ // iOS bookmarks are always security scoped, and we
+ // don't need or want any of the other options.
+ NSURLBookmarkResolutionOptions(0)
+ #endif
+ relativeToURL:nil /* app-scoped bookmark */
+ bookmarkDataIsStale:&bookmarkDataIsStale
+ error:&error];
+
+ if (!securityScopedUrl || error) {
+ qCWarning(lcSecEngine) << "Failed to resolve bookmark data for"
+ << fileName << ":" << error;
+ return nullptr;
+ }
+
+ if (bookmarkDataIsStale) {
+ // This occurs when for example the file has been renamed, moved,
+ // or deleted. Normally this would be the place to update the
+ // bookmark to point to the new location, but Qt clients may not
+ // be prepared for QFiles changing their file-names under their
+ // feet so we treat it as a missing file.
+ qCDebug(lcSecEngine) << "Bookmark for" << cacheKey << "was stale";
+ locker.unlock();
+ QWriteLocker writeLocker(&m_bookmarkLock);
+ [m_bookmarks removeObjectForKey:cacheKey];
+ auto *mutableThis = const_cast<SecurityScopedFileEngineHandler*>(this);
+ mutableThis->saveBookmarks();
+ return nullptr;
+ }
+ }
+
+ qCInfo(lcSecEngine) << "Resolved security scope" << securityScopedUrl
+ << "for path" << filePath;
+ return std::make_unique<SecurityScopedFileEngine>(fileName, securityScopedUrl);
+}
+
+/*
+ Create an app-scoped bookmark, and store it in our persistent cache.
+
+ We do this so that the user can continue accessing the file even after
+ application restarts.
+
+ Storing the bookmarks to disk (inside the sandbox) is safe, as only the
+ app that created the app-scoped bookmarks can obtain access to the file
+ system resource that the URL points to. Specifically, a bookmark created
+ with security scope fails to resolve if the caller does not have the same
+ code signing identity as the caller that created the bookmark.
+*/
+void SecurityScopedFileEngineHandler::saveBookmark(NSURL *url)
+{
+ NSError *error = nullptr;
+ NSData *bookmarkData = [url bookmarkDataWithOptions:
+ #if defined(Q_OS_MACOS)
+ NSURLBookmarkCreationWithSecurityScope
+ #else
+ // iOS bookmarks are always security scoped, and we
+ // don't need or want any of the other options.
+ NSURLBookmarkCreationOptions(0)
+ #endif
+ includingResourceValuesForKeys:nil
+ relativeToURL:nil /* app-scoped bookmark */
+ error:&error];
+
+ if (bookmarkData) {
+ QWriteLocker locker(&m_bookmarkLock);
+ NSString *cacheKey = cacheKeyForUrl(url);
+ qCInfo(lcSecEngine)
+ << (m_bookmarks[cacheKey] ? "Updating" : "Registering")
+ << "bookmark for" << cacheKey;
+ m_bookmarks[cacheKey] = bookmarkData;
+ saveBookmarks();
+ } else {
+ qCWarning(lcSecEngine) << "Failed to create bookmark data for" << url << error;
+ }
+}
+
+/*
+ Saves the bookmarks cache to disk.
+
+ We do this preemptively whenever we create a bookmark, to ensure
+ the file can be accessed later on even if the app crashes.
+*/
+void SecurityScopedFileEngineHandler::saveBookmarks()
+{
+ QMacAutoReleasePool pool;
+
+ NSError *error = nullptr;
+ NSURL *bookmarksFilePath = bookmarksFile();
+ [NSFileManager.defaultManager
+ createDirectoryAtURL:[bookmarksFilePath URLByDeletingLastPathComponent]
+ withIntermediateDirectories:YES attributes:nil error:&error];
+ if (error) {
+ qCWarning(lcSecEngine) << "Failed to create bookmarks path:" << error;
+ return;
+ }
+ [m_bookmarks writeToURL:bookmarksFile() error:&error];
+ if (error) {
+ qCWarning(lcSecEngine) << "Failed to save bookmarks to"
+ << bookmarksFile() << ":" << error;
+ }
+}
+
+NSURL *SecurityScopedFileEngineHandler::bookmarksFile() const
+{
+ NSURL *appSupportDir = [[NSFileManager.defaultManager URLsForDirectory:
+ NSApplicationSupportDirectory inDomains:NSUserDomainMask] firstObject];
+ return [appSupportDir URLByAppendingPathComponent:@"SecurityScopedBookmarks.plist"];
+}
+
+NSString *SecurityScopedFileEngineHandler::cacheKeyForUrl(NSURL *url)
+{
+ return cacheKeyForPath(QString::fromNSString(url.path));
+}
+
+NSString *SecurityScopedFileEngineHandler::cacheKeyForPath(const QString &path)
+{
+ auto normalized = path.normalized(QString::NormalizationForm_D);
+ // We assume the file paths we get via file dialogs and similar
+ // are already canonical, but clean it just in case.
+ return QDir::cleanPath(normalized).toNSString();
+}
+
+QT_END_NAMESPACE
diff --git a/src/corelib/platform/darwin/qdarwinsecurityscopedfileengine_p.h b/src/corelib/platform/darwin/qdarwinsecurityscopedfileengine_p.h
new file mode 100644
index 00000000000..f6098fa977d
--- /dev/null
+++ b/src/corelib/platform/darwin/qdarwinsecurityscopedfileengine_p.h
@@ -0,0 +1,29 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+// Qt-Security score:significant reason:default
+
+#ifndef QDARWINSECURITYSCOPEDFILEENGINE_H
+#define QDARWINSECURITYSCOPEDFILEENGINE_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is part of the QPA API and is not meant to be used
+// in applications. Usage of this API may make your code
+// source and binary incompatible with future versions of Qt.
+//
+// We mean it.
+//
+
+#include <QtCore/qurl.h>
+
+Q_FORWARD_DECLARE_OBJC_CLASS(NSURL);
+
+QT_BEGIN_NAMESPACE
+
+Q_CORE_EXPORT QUrl qt_apple_urlFromPossiblySecurityScopedURL(NSURL *url);
+
+QT_END_NAMESPACE
+
+#endif // QDARWINSECURITYSCOPEDFILEENGINE_H
diff --git a/src/corelib/platform/wasm/qstdweb.cpp b/src/corelib/platform/wasm/qstdweb.cpp
index 287138bb915..4f3ecc4c6d9 100644
--- a/src/corelib/platform/wasm/qstdweb.cpp
+++ b/src/corelib/platform/wasm/qstdweb.cpp
@@ -178,12 +178,17 @@ Blob Blob::slice(uint32_t begin, uint32_t end) const
ArrayBuffer Blob::arrayBuffer_sync() const
{
emscripten::val buffer;
- uint32_t handlerIndex = qstdweb::Promise::make(m_blob, QStringLiteral("arrayBuffer"), {
- .thenFunc = [&buffer](emscripten::val arrayBuffer) {
- buffer = arrayBuffer;
+ QList<uint32_t> handlers;
+ qstdweb::Promise::make(
+ handlers,
+ m_blob,
+ QStringLiteral("arrayBuffer"),
+ {
+ .thenFunc = [&buffer](emscripten::val arrayBuffer) {
+ buffer = arrayBuffer;
}
});
- Promise::suspendExclusive(handlerIndex);
+ Promise::suspendExclusive(handlers);
return ArrayBuffer(buffer);
}
@@ -441,7 +446,7 @@ EventCallback::EventCallback(emscripten::val element, const std::string &name,
}
-uint32_t Promise::adoptPromise(emscripten::val promise, PromiseCallbacks callbacks)
+uint32_t Promise::adoptPromise(emscripten::val promise, PromiseCallbacks callbacks, QList<uint32_t> *handlers)
{
Q_ASSERT_X(!!callbacks.catchFunc || !!callbacks.finallyFunc || !!callbacks.thenFunc,
"Promise::adoptPromise", "must provide at least one callback function");
@@ -498,14 +503,21 @@ uint32_t Promise::adoptPromise(emscripten::val promise, PromiseCallbacks callbac
promise = promise.call<emscripten::val>("finally",
suspendResume->jsEventHandlerAt(*finallyIndex));
+ if (handlers) {
+ if (thenIndex)
+ handlers->push_back(*thenIndex);
+ if (catchIndex)
+ handlers->push_back(*catchIndex);
+ handlers->push_back(*finallyIndex);
+ }
return *finallyIndex;
}
-void Promise::suspendExclusive(uint32_t handlerIndex)
+void Promise::suspendExclusive(QList<uint32_t> handlerIndices)
{
QWasmSuspendResumeControl *suspendResume = QWasmSuspendResumeControl::get();
Q_ASSERT(suspendResume);
- suspendResume->suspendExclusive(handlerIndex);
+ suspendResume->suspendExclusive(handlerIndices);
suspendResume->sendPendingEvents();
}
@@ -657,11 +669,12 @@ void FileSystemWritableFileStreamIODevice::close()
return;
}
- uint32_t handlerIndex = Promise::make(m_stream.val(), QStringLiteral("close"), {
+ QList<uint32_t> handlers;
+ Promise::make(handlers, m_stream.val(), QStringLiteral("close"), {
.thenFunc = [](emscripten::val) {
}
});
- Promise::suspendExclusive(handlerIndex);
+ Promise::suspendExclusive(handlers);
QIODevice::close();
}
@@ -683,14 +696,15 @@ bool FileSystemWritableFileStreamIODevice::seek(qint64 pos)
emscripten::val seekParams = emscripten::val::object();
seekParams.set("type", std::string("seek"));
seekParams.set("position", static_cast<double>(pos));
- uint32_t handlerIndex = Promise::make(m_stream.val(), QStringLiteral("write"), {
+ QList<uint32_t> handlers;
+ Promise::make(handlers, m_stream.val(), QStringLiteral("write"), {
.thenFunc = [&success](emscripten::val) {
success = true;
},
.catchFunc = [](emscripten::val) {
}
}, seekParams);
- Promise::suspendExclusive(handlerIndex);
+ Promise::suspendExclusive(handlers);
if (!success)
return false;
@@ -708,14 +722,15 @@ qint64 FileSystemWritableFileStreamIODevice::writeData(const char *data, qint64
bool success = false;
Uint8Array array = Uint8Array::copyFrom(data, size);
- uint32_t handlerIndex = Promise::make(m_stream.val(), QStringLiteral("write"), {
+ QList<uint32_t> handlers;
+ Promise::make(handlers, m_stream.val(), QStringLiteral("write"), {
.thenFunc = [&success](emscripten::val) {
success = true;
},
.catchFunc = [](emscripten::val) {
}
}, array.val());
- Promise::suspendExclusive(handlerIndex);
+ Promise::suspendExclusive(handlers);
if (success) {
qint64 newPos = pos() + size;
@@ -770,7 +785,8 @@ bool FileSystemFileIODevice::open(QIODevice::OpenMode mode)
File file;
bool success = false;
- uint32_t handlerIndex = Promise::make(m_fileHandle.val(), QStringLiteral("getFile"), {
+ QList<uint32_t> handlers;
+ Promise::make(handlers, m_fileHandle.val(), QStringLiteral("getFile"), {
.thenFunc = [&file, &success](emscripten::val fileVal) {
file = File(fileVal);
success = true;
@@ -778,7 +794,7 @@ bool FileSystemFileIODevice::open(QIODevice::OpenMode mode)
.catchFunc = [](emscripten::val) {
}
});
- Promise::suspendExclusive(handlerIndex);
+ Promise::suspendExclusive(handlers);
if (success) {
m_blobDevice = std::make_unique<BlobIODevice>(file.slice(0, file.size()));
@@ -796,7 +812,8 @@ bool FileSystemFileIODevice::open(QIODevice::OpenMode mode)
FileSystemWritableFileStream writableStream;
bool success = false;
- uint32_t handlerIndex = Promise::make(m_fileHandle.val(), QStringLiteral("createWritable"), {
+ QList<uint32_t> handlers;
+ Promise::make(handlers, m_fileHandle.val(), QStringLiteral("createWritable"), {
.thenFunc = [&writableStream, &success](emscripten::val writable) {
writableStream = FileSystemWritableFileStream(writable);
success = true;
@@ -804,7 +821,7 @@ bool FileSystemFileIODevice::open(QIODevice::OpenMode mode)
.catchFunc = [](emscripten::val) {
}
});
- Promise::suspendExclusive(handlerIndex);
+ Promise::suspendExclusive(handlers);
if (success) {
m_writableDevice = std::make_unique<FileSystemWritableFileStreamIODevice>(writableStream);
diff --git a/src/corelib/platform/wasm/qstdweb_p.h b/src/corelib/platform/wasm/qstdweb_p.h
index 9a97370448e..b14d9e4012f 100644
--- a/src/corelib/platform/wasm/qstdweb_p.h
+++ b/src/corelib/platform/wasm/qstdweb_p.h
@@ -238,7 +238,7 @@ namespace qstdweb {
};
namespace Promise {
- uint32_t Q_CORE_EXPORT adoptPromise(emscripten::val promise, PromiseCallbacks callbacks);
+ uint32_t Q_CORE_EXPORT adoptPromise(emscripten::val promise, PromiseCallbacks callbacks, QList<uint32_t> *handlers = nullptr);
template<typename... Args>
uint32_t make(emscripten::val target,
@@ -255,7 +255,24 @@ namespace qstdweb {
return adoptPromise(std::move(promiseObject), std::move(callbacks));
}
- void Q_CORE_EXPORT suspendExclusive(uint32_t handlerIndex);
+ template<typename... Args>
+ void make(
+ QList<uint32_t> &handlers,
+ emscripten::val target,
+ QString methodName,
+ PromiseCallbacks callbacks,
+ Args... args)
+ {
+ emscripten::val promiseObject = target.call<emscripten::val>(
+ methodName.toStdString().c_str(), std::forward<Args>(args)...);
+ if (promiseObject.isUndefined() || promiseObject["constructor"]["name"].as<std::string>() != "Promise") {
+ qFatal("This function did not return a promise");
+ }
+
+ adoptPromise(std::move(promiseObject), std::move(callbacks), &handlers);
+ }
+
+ void Q_CORE_EXPORT suspendExclusive(QList<uint32_t> handlerIndices);
void Q_CORE_EXPORT all(std::vector<emscripten::val> promises, PromiseCallbacks callbacks);
};
diff --git a/src/corelib/platform/wasm/qwasmanimationdriver.cpp b/src/corelib/platform/wasm/qwasmanimationdriver.cpp
new file mode 100644
index 00000000000..ab0c8240dd1
--- /dev/null
+++ b/src/corelib/platform/wasm/qwasmanimationdriver.cpp
@@ -0,0 +1,129 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include "qwasmanimationdriver_p.h"
+#include "qwasmsuspendresumecontrol_p.h"
+
+#include <emscripten/val.h>
+
+QT_BEGIN_NAMESPACE
+
+
+// QWasmAnimationDriver drives animations using requestAnimationFrame(). This
+// ensures that animations are advanced in sync with frame update calls, which
+// again are synced to the screen refresh rate.
+
+namespace {
+ constexpr int FallbackTimerInterval = 500;
+}
+
+QWasmAnimationDriver::QWasmAnimationDriver(QUnifiedTimer *)
+ : QAnimationDriver(nullptr)
+{
+ connect(this, &QAnimationDriver::started, this, &QWasmAnimationDriver::start);
+ connect(this, &QAnimationDriver::stopped, this, &QWasmAnimationDriver::stop);
+}
+
+QWasmAnimationDriver::~QWasmAnimationDriver()
+{
+ disconnect(this, &QAnimationDriver::started, this, &QWasmAnimationDriver::start);
+ disconnect(this, &QAnimationDriver::stopped, this, &QWasmAnimationDriver::stop);
+
+ if (m_animateCallbackHandle != 0)
+ QWasmAnimationFrameMultiHandler::instance()->unregisterAnimateCallback(m_animateCallbackHandle);
+}
+
+qint64 QWasmAnimationDriver::elapsed() const
+{
+ return isRunning() ? qint64(m_currentTimestamp - m_startTimestamp) : 0;
+}
+
+double QWasmAnimationDriver::getCurrentTimeFromTimeline() const
+{
+ // Get the current timeline time, which is an equivalent time source to the
+ // animation frame time source. According to the documentation this API
+ // may be unavailable in various cases; check for null before accessing.
+ emscripten::val document = emscripten::val::global("document");
+ emscripten::val timeline = document["timeline"];
+ if (!timeline.isNull() && !timeline.isUndefined()) {
+ emscripten::val currentTime = timeline["currentTime"];
+ if (!currentTime.isNull() && !currentTime.isUndefined())
+ return currentTime.as<double>();
+ }
+ return 0;
+}
+
+void QWasmAnimationDriver::handleFallbackTimeout()
+{
+ if (!isRunning())
+ return;
+
+ // Get the current time from a timing source equivalent to the animation frame time
+ double currentTime = getCurrentTimeFromTimeline();
+ if (currentTime == 0)
+ currentTime = m_currentTimestamp + FallbackTimerInterval;
+ const double timeSinceLastFrame = currentTime - m_currentTimestamp;
+
+ // Advance animations if animations are active but there has been no rcent animation
+ // frame callback.
+ if (timeSinceLastFrame > FallbackTimerInterval * 0.8) {
+ m_currentTimestamp = currentTime;
+ advance();
+ }
+}
+
+void QWasmAnimationDriver::start()
+{
+ if (isRunning())
+ return;
+
+ // Set start timestamp to document.timeline.currentTime()
+ m_startTimestamp = getCurrentTimeFromTimeline();
+ m_currentTimestamp = m_startTimestamp;
+
+ // Register animate callback
+ m_animateCallbackHandle = QWasmAnimationFrameMultiHandler::instance()->registerAnimateCallback(
+ [this](double timestamp) { handleAnimationFrame(timestamp); });
+
+ // Start fallback timer to ensure animations advance even if animaton frame callbacks stop coming
+ fallbackTimer.setInterval(FallbackTimerInterval);
+ connect(&fallbackTimer, &QTimer::timeout, this, &QWasmAnimationDriver::handleFallbackTimeout);
+ fallbackTimer.start();
+
+ QAnimationDriver::start();
+}
+
+void QWasmAnimationDriver::stop()
+{
+ m_startTimestamp = 0;
+ m_currentTimestamp = 0;
+
+ // Stop and disconnect the fallback timer
+ fallbackTimer.stop();
+ disconnect(&fallbackTimer, &QTimer::timeout, this, &QWasmAnimationDriver::handleFallbackTimeout);
+
+ // Deregister the animation frame callback
+ if (m_animateCallbackHandle != 0) {
+ QWasmAnimationFrameMultiHandler::instance()->unregisterAnimateCallback(m_animateCallbackHandle);
+ m_animateCallbackHandle = 0;
+ }
+
+ QAnimationDriver::stop();
+}
+
+void QWasmAnimationDriver::handleAnimationFrame(double timestamp)
+{
+ if (!isRunning())
+ return;
+
+ m_currentTimestamp = timestamp;
+
+ // Fall back to setting m_startTimestamp here in cases where currentTime
+ // was not available in start() (gives 0 elapsed time for the first frame)
+ if (m_startTimestamp == 0)
+ m_startTimestamp = timestamp;
+
+ advance();
+}
+
+QT_END_NAMESPACE
diff --git a/src/corelib/platform/wasm/qwasmanimationdriver_p.h b/src/corelib/platform/wasm/qwasmanimationdriver_p.h
new file mode 100644
index 00000000000..f8435c17a9a
--- /dev/null
+++ b/src/corelib/platform/wasm/qwasmanimationdriver_p.h
@@ -0,0 +1,54 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QWASMANIMATIONDRIVER_P_H
+#define QWASMANIMATIONDRIVER_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QtCore/private/qglobal_p.h>
+#include <QtCore/qabstractanimation.h>
+#include <QtCore/qtimer.h>
+
+#include <memory>
+
+QT_BEGIN_NAMESPACE
+
+class QUnifiedTimer;
+
+class Q_CORE_EXPORT QWasmAnimationDriver : public QAnimationDriver
+{
+ Q_OBJECT
+public:
+ QWasmAnimationDriver(QUnifiedTimer *unifiedTimer);
+ ~QWasmAnimationDriver() override;
+
+ qint64 elapsed() const override;
+
+protected:
+ void start() override;
+ void stop() override;
+
+private:
+ void handleAnimationFrame(double timestamp);
+ void handleFallbackTimeout();
+ double getCurrentTimeFromTimeline() const;
+
+ QTimer fallbackTimer;
+ uint32_t m_animateCallbackHandle = 0;
+ double m_startTimestamp = 0;
+ double m_currentTimestamp = 0;
+};
+
+QT_END_NAMESPACE
+
+#endif // QWASMANIMATIONDRIVER_P_H
diff --git a/src/corelib/platform/wasm/qwasmsuspendresumecontrol.cpp b/src/corelib/platform/wasm/qwasmsuspendresumecontrol.cpp
index 093898c520a..a4bc7843380 100644
--- a/src/corelib/platform/wasm/qwasmsuspendresumecontrol.cpp
+++ b/src/corelib/platform/wasm/qwasmsuspendresumecontrol.cpp
@@ -4,6 +4,8 @@
#include "qwasmsuspendresumecontrol_p.h"
#include "qstdweb_p.h"
+#include <QtCore/qapplicationstatic.h>
+
#include <emscripten.h>
#include <emscripten/val.h>
#include <emscripten/bind.h>
@@ -75,32 +77,20 @@ void qtRegisterEventHandlerJs(int index) {
}[name];
}
- function deepShallowClone(parent, obj, depth) {
+ function deepShallowClone(obj) {
if (obj === null)
return obj;
- if (typeof obj === 'function') {
- if (obj.name !== "")
- return createNamedFunction(obj.name, parent, obj);
- }
-
- if (depth >= 1)
- return obj;
-
- if (typeof obj !== 'object')
+ if (!(obj instanceof Event))
return obj;
- if (Array.isArray(obj)) {
- const arrCopy = [];
- for (let i = 0; i < obj.length; i++)
- arrCopy[i] = deepShallowClone(obj, obj[i], depth + 1);
-
- return arrCopy;
- }
-
const objCopy = {};
- for (const key in obj)
- objCopy[key] = deepShallowClone(obj, obj[key], depth + 1);
+ for (const key in obj) {
+ if (typeof obj[key] === 'function')
+ objCopy[key] = createNamedFunction(obj[key].name, obj, obj[key]);
+ else
+ objCopy[key] = obj[key];
+ }
return objCopy;
}
@@ -110,7 +100,7 @@ void qtRegisterEventHandlerJs(int index) {
let handler = (arg) => {
// Copy the top level object, alias the rest.
// functions are copied by creating new forwarding functions.
- arg = deepShallowClone(arg, arg, 0);
+ arg = deepShallowClone(arg);
// Add event to event queue
control.pendingEvents.push({
@@ -206,9 +196,13 @@ void QWasmSuspendResumeControl::suspend()
qtSuspendJs();
}
-void QWasmSuspendResumeControl::suspendExclusive(uint32_t eventHandlerIndex)
+void QWasmSuspendResumeControl::suspendExclusive(QList<uint32_t> eventHandlerIndices)
{
- suspendResumeControlJs().set("exclusiveEventHandler", eventHandlerIndex);
+ m_eventFilter = [eventHandlerIndices](int handler) {
+ return eventHandlerIndices.contains(handler);
+ };
+
+ suspendResumeControlJs().set("exclusiveEventHandler", eventHandlerIndices.back());
qtSuspendJs();
}
@@ -221,37 +215,27 @@ int QWasmSuspendResumeControl::sendPendingEvents()
emscripten::val control = suspendResumeControlJs();
emscripten::val pendingEvents = control["pendingEvents"];
- if (control["exclusiveEventHandler"].as<int>() > 0)
- return sendPendingExclusiveEvent();
-
- if (pendingEvents["length"].as<int>() == 0)
- return 0;
-
int count = 0;
- while (pendingEvents["length"].as<int>() > 0) { // Make sure it is reentrant
- // Grab one event (handler and arg), and call it
- emscripten::val event = pendingEvents.call<val>("shift");
- auto it = m_eventHandlers.find(event["index"].as<int>());
- if (it != m_eventHandlers.end())
- it->second(event["arg"]);
- ++count;
+ for (int i = 0; i < pendingEvents["length"].as<int>();) {
+ if (!m_eventFilter(pendingEvents[i]["index"].as<int>())) {
+ ++i;
+ } else {
+ // Grab one event (handler and arg), and call it
+ emscripten::val event = pendingEvents[i];
+ pendingEvents.call<void>("splice", i, 1);
+
+ auto it = m_eventHandlers.find(event["index"].as<int>());
+ if (it != m_eventHandlers.end())
+ it->second(event["arg"]);
+ ++count;
+ }
}
- return count;
-}
-// Sends the pending exclusive event, and resets the "exclusive" state
-int QWasmSuspendResumeControl::sendPendingExclusiveEvent()
-{
- emscripten::val control = suspendResumeControlJs();
- int exclusiveHandlerIndex = control["exclusiveEventHandler"].as<int>();
- control.set("exclusiveEventHandler", 0);
- emscripten::val event = control["pendingEvents"].call<val>("pop");
- int eventHandlerIndex = event["index"].as<int>();
- Q_ASSERT(exclusiveHandlerIndex == eventHandlerIndex);
- auto it = m_eventHandlers.find(eventHandlerIndex);
- Q_ASSERT(it != m_eventHandlers.end());
- it->second(event["arg"]);
- return 1;
+ if (control["exclusiveEventHandler"].as<int>() > 0) {
+ control.set("exclusiveEventHandler", 0);
+ m_eventFilter = [](int) { return true;};
+ }
+ return count;
}
void qtSendPendingEvents()
@@ -357,3 +341,103 @@ void QWasmTimer::clearTimeout()
val::global("window").call<void>("clearTimeout", double(m_timerId));
m_timerId = 0;
}
+
+//
+// QWasmAnimationFrameMultiHandler
+//
+// Multiplexes multiple animate and draw callbacks to a single native requestAnimationFrame call.
+// Animate callbacks are called before draw callbacks to ensure animations are advanced before drawing.
+//
+QWasmAnimationFrameMultiHandler::QWasmAnimationFrameMultiHandler()
+{
+ auto wrapper = [this](val arg) {
+ handleAnimationFrame(arg.as<double>());
+ };
+ m_handlerIndex = QWasmSuspendResumeControl::get()->registerEventHandler(wrapper);
+}
+
+QWasmAnimationFrameMultiHandler::~QWasmAnimationFrameMultiHandler()
+{
+ cancelAnimationFrameRequest();
+ QWasmSuspendResumeControl::get()->removeEventHandler(m_handlerIndex);
+}
+
+Q_APPLICATION_STATIC(QWasmAnimationFrameMultiHandler, s_animationFrameHandler);
+QWasmAnimationFrameMultiHandler *QWasmAnimationFrameMultiHandler::instance()
+{
+ return s_animationFrameHandler();
+}
+
+// Registers a permanent animation callback. Call unregisterAnimateCallback() to unregister
+uint32_t QWasmAnimationFrameMultiHandler::registerAnimateCallback(Callback callback)
+{
+ uint32_t handle = ++m_nextAnimateHandle;
+ m_animateCallbacks[handle] = std::move(callback);
+ ensureAnimationFrameRequested();
+ return handle;
+}
+
+// Registers a single-shot draw callback.
+uint32_t QWasmAnimationFrameMultiHandler::registerDrawCallback(Callback callback)
+{
+ uint32_t handle = ++m_nextDrawHandle;
+ m_drawCallbacks[handle] = std::move(callback);
+ ensureAnimationFrameRequested();
+ return handle;
+}
+
+void QWasmAnimationFrameMultiHandler::unregisterAnimateCallback(uint32_t handle)
+{
+ m_animateCallbacks.erase(handle);
+ if (m_animateCallbacks.empty() && m_drawCallbacks.empty())
+ cancelAnimationFrameRequest();
+}
+
+void QWasmAnimationFrameMultiHandler::unregisterDrawCallback(uint32_t handle)
+{
+ m_drawCallbacks.erase(handle);
+ if (m_animateCallbacks.empty() && m_drawCallbacks.empty())
+ cancelAnimationFrameRequest();
+}
+
+void QWasmAnimationFrameMultiHandler::handleAnimationFrame(double timestamp)
+{
+ m_requestId = -1;
+
+ // Advance animations. Copy the callbacks list in case callbacks are
+ // unregistered during iteration
+ auto animateCallbacksCopy = m_animateCallbacks;
+ for (const auto &pair : animateCallbacksCopy)
+ pair.second(timestamp);
+
+ // Draw the frame. Note that draw callbacks are cleared after each
+ // frame, matching QWindow::requestUpdate() behavior. Copy the callbacks
+ // list in case new callbacks are registered while drawing the frame
+ auto drawCallbacksCopy = m_drawCallbacks;
+ m_drawCallbacks.clear();
+ for (const auto &pair : drawCallbacksCopy)
+ pair.second(timestamp);
+
+ // Request next frame if there are still callbacks registered
+ if (!m_animateCallbacks.empty() || !m_drawCallbacks.empty())
+ ensureAnimationFrameRequested();
+}
+
+void QWasmAnimationFrameMultiHandler::ensureAnimationFrameRequested()
+{
+ if (m_requestId != -1)
+ return;
+
+ using ReturnType = double;
+ val handler = QWasmSuspendResumeControl::get()->jsEventHandlerAt(m_handlerIndex);
+ m_requestId = int64_t(val::global("window").call<ReturnType>("requestAnimationFrame", handler));
+}
+
+void QWasmAnimationFrameMultiHandler::cancelAnimationFrameRequest()
+{
+ if (m_requestId == -1)
+ return;
+
+ val::global("window").call<void>("cancelAnimationFrame", double(m_requestId));
+ m_requestId = -1;
+}
diff --git a/src/corelib/platform/wasm/qwasmsuspendresumecontrol_p.h b/src/corelib/platform/wasm/qwasmsuspendresumecontrol_p.h
index 37c71ed8123..ff97ff3d7ea 100644
--- a/src/corelib/platform/wasm/qwasmsuspendresumecontrol_p.h
+++ b/src/corelib/platform/wasm/qwasmsuspendresumecontrol_p.h
@@ -38,15 +38,16 @@ public:
static emscripten::val suspendResumeControlJs();
void suspend();
- void suspendExclusive(uint32_t eventHandlerIndex);
+ // Accept events for all handlers, start to process events on last handler in list
+ void suspendExclusive(QList<uint32_t> eventHandlerIndices);
int sendPendingEvents();
- int sendPendingExclusiveEvent();
private:
friend void qtSendPendingEvents();
static QWasmSuspendResumeControl *s_suspendResumeControl;
std::map<int, std::function<void(emscripten::val)>> m_eventHandlers;
+ std::function<bool(int)> m_eventFilter = [](int) { return true; };
};
class Q_CORE_EXPORT QWasmEventHandler
@@ -83,4 +84,37 @@ private:
uint64_t m_timerId = 0;
};
+class Q_CORE_EXPORT QWasmAnimationFrameMultiHandler
+{
+public:
+ using Callback = std::function<void(double)>;
+
+ static QWasmAnimationFrameMultiHandler *instance();
+
+ uint32_t registerAnimateCallback(Callback callback);
+ uint32_t registerDrawCallback(Callback callback);
+
+ void unregisterAnimateCallback(uint32_t handle);
+ void unregisterDrawCallback(uint32_t handle);
+
+ QWasmAnimationFrameMultiHandler();
+ ~QWasmAnimationFrameMultiHandler();
+ QWasmAnimationFrameMultiHandler(const QWasmAnimationFrameMultiHandler&) = delete;
+ QWasmAnimationFrameMultiHandler& operator=(const QWasmAnimationFrameMultiHandler&) = delete;
+
+private:
+ void handleAnimationFrame(double timestamp);
+ void ensureAnimationFrameRequested();
+ void cancelAnimationFrameRequest();
+
+ static QWasmAnimationFrameMultiHandler *s_instance;
+
+ std::map<uint32_t, Callback> m_animateCallbacks;
+ std::map<uint32_t, Callback> m_drawCallbacks;
+ uint32_t m_nextAnimateHandle = 0;
+ uint32_t m_nextDrawHandle = 0;
+ uint32_t m_handlerIndex = 0;
+ int64_t m_requestId = -1;
+};
+
#endif
diff --git a/src/corelib/text/qlocale_icu.cpp b/src/corelib/text/qlocale_icu.cpp
index a10ae1c84b2..7e1dba5ee92 100644
--- a/src/corelib/text/qlocale_icu.cpp
+++ b/src/corelib/text/qlocale_icu.cpp
@@ -17,10 +17,10 @@ static_assert(std::is_same_v<UChar, char16_t>,
namespace QtIcuPrivate {
-enum class CaseConversion : bool { Upper, Lower };
+enum class IcuCaseConversion : bool { Upper, Lower };
static bool qt_u_strToCase(const QString &str, QString *out, const char *localeID,
- CaseConversion conv)
+ IcuCaseConversion conv)
{
Q_ASSERT(out);
@@ -34,9 +34,9 @@ static bool qt_u_strToCase(const QString &str, QString *out, const char *localeI
// try to be a completely transparent wrapper:
using R [[maybe_unused]] = decltype(u_strToUpper(std::forward<decltype(args)>(args)...));
switch (conv) {
- case CaseConversion::Upper:
+ case IcuCaseConversion::Upper:
return u_strToUpper(std::forward<decltype(args)>(args)...);
- case CaseConversion::Lower:
+ case IcuCaseConversion::Lower:
return u_strToLower(std::forward<decltype(args)>(args)...);
};
Q_UNREACHABLE_RETURN(R{0});
@@ -79,7 +79,7 @@ QString QLocalePrivate::toUpper(const QString &str, bool *ok) const
Q_ASSERT(ok);
using namespace QtIcuPrivate;
QString out;
- *ok = qt_u_strToCase(str, &out, bcp47Name('_'), CaseConversion::Upper);
+ *ok = qt_u_strToCase(str, &out, bcp47Name('_'), IcuCaseConversion::Upper);
return out;
}
@@ -88,7 +88,7 @@ QString QLocalePrivate::toLower(const QString &str, bool *ok) const
Q_ASSERT(ok);
using namespace QtIcuPrivate;
QString out;
- *ok = qt_u_strToCase(str, &out, bcp47Name('_'), CaseConversion::Lower);
+ *ok = qt_u_strToCase(str, &out, bcp47Name('_'), IcuCaseConversion::Lower);
return out;
}
diff --git a/src/corelib/tools/qcryptographichash.cpp b/src/corelib/tools/qcryptographichash.cpp
index 092ff46b084..53cca38ba7b 100644
--- a/src/corelib/tools/qcryptographichash.cpp
+++ b/src/corelib/tools/qcryptographichash.cpp
@@ -101,8 +101,6 @@ static inline int SHA384_512AddLength(SHA512Context *context, unsigned int lengt
}
#endif // !QT_CONFIG(opensslv30)
-#include "qtcore-config_p.h"
-
#if QT_CONFIG(system_libb2)
#include <blake2.h>
#else
diff --git a/src/gui/configure.cmake b/src/gui/configure.cmake
index 5001f2deeec..dac2d072e42 100644
--- a/src/gui/configure.cmake
+++ b/src/gui/configure.cmake
@@ -98,34 +98,35 @@ if((X11_SUPPORTED) OR QT_FIND_ALL_PACKAGES_ALWAYS)
qt_find_package(X11 MODULE PROVIDED_TARGETS X11::SM X11::ICE MODULE_NAME gui QMAKE_LIB x11sm)
endif()
if((X11_SUPPORTED) OR QT_FIND_ALL_PACKAGES_ALWAYS)
- qt_find_package(XCB 1.11 MODULE PROVIDED_TARGETS XCB::XCB MODULE_NAME gui QMAKE_LIB xcb)
+ qt_find_package(XCB 1.11 MODULE
+ COMPONENTS XCB PROVIDED_TARGETS XCB::XCB MODULE_NAME gui QMAKE_LIB xcb)
endif()
if((X11_SUPPORTED) OR QT_FIND_ALL_PACKAGES_ALWAYS)
- qt_find_package(XCB 0.1.1 MODULE
+ qt_find_package(XCB MODULE
COMPONENTS CURSOR PROVIDED_TARGETS XCB::CURSOR MODULE_NAME gui QMAKE_LIB xcb_cursor)
endif()
if((X11_SUPPORTED) OR QT_FIND_ALL_PACKAGES_ALWAYS)
- qt_find_package(XCB 0.3.9 MODULE
+ qt_find_package(XCB MODULE
COMPONENTS ICCCM PROVIDED_TARGETS XCB::ICCCM MODULE_NAME gui QMAKE_LIB xcb_icccm)
endif()
qt_add_qmake_lib_dependency(xcb_icccm xcb)
if((X11_SUPPORTED) OR QT_FIND_ALL_PACKAGES_ALWAYS)
- qt_find_package(XCB 0.3.8 MODULE
+ qt_find_package(XCB MODULE
COMPONENTS UTIL PROVIDED_TARGETS XCB::UTIL MODULE_NAME gui QMAKE_LIB xcb_util)
endif()
qt_add_qmake_lib_dependency(xcb_util xcb)
if((X11_SUPPORTED) OR QT_FIND_ALL_PACKAGES_ALWAYS)
- qt_find_package(XCB 0.3.9 MODULE
+ qt_find_package(XCB MODULE
COMPONENTS IMAGE PROVIDED_TARGETS XCB::IMAGE MODULE_NAME gui QMAKE_LIB xcb_image)
endif()
qt_add_qmake_lib_dependency(xcb_image xcb_shm xcb_util xcb)
if((X11_SUPPORTED) OR QT_FIND_ALL_PACKAGES_ALWAYS)
- qt_find_package(XCB 0.3.9 MODULE
+ qt_find_package(XCB MODULE
COMPONENTS KEYSYMS PROVIDED_TARGETS XCB::KEYSYMS MODULE_NAME gui QMAKE_LIB xcb_keysyms)
endif()
qt_add_qmake_lib_dependency(xcb_keysyms xcb)
if((X11_SUPPORTED) OR QT_FIND_ALL_PACKAGES_ALWAYS)
- qt_find_package(XCB 0.3.9 MODULE
+ qt_find_package(XCB MODULE
COMPONENTS RENDERUTIL PROVIDED_TARGETS XCB::RENDERUTIL MODULE_NAME gui QMAKE_LIB xcb_renderutil)
endif()
qt_add_qmake_lib_dependency(xcb_renderutil xcb xcb_render)
diff --git a/src/gui/image/qpaintengine_pic.cpp b/src/gui/image/qpaintengine_pic.cpp
index ba4888cef75..b151c3b2d18 100644
--- a/src/gui/image/qpaintengine_pic.cpp
+++ b/src/gui/image/qpaintengine_pic.cpp
@@ -31,6 +31,7 @@ public:
QDataStream s;
QPainter *pt;
QPicturePrivate *pic_d;
+ bool sizeLimitExceeded = false;
};
QPicturePaintEngine::QPicturePaintEngine()
@@ -68,6 +69,7 @@ bool QPicturePaintEngine::begin(QPaintDevice *pd)
d->s.setVersion(d->pic_d->formatMajor);
d->pic_d->pictb.open(QIODevice::WriteOnly | QIODevice::Truncate);
+ d->sizeLimitExceeded = false;
d->s.writeRawData(qt_mfhdr_tag, 4);
d->s << (quint16) 0 << (quint16) d->pic_d->formatMajor << (quint16) d->pic_d->formatMinor;
d->s << (quint8) QPicturePrivate::PdcBegin << (quint8) sizeof(qint32);
@@ -109,7 +111,7 @@ bool QPicturePaintEngine::end()
d->s << cs; // write checksum
d->pic_d->pictb.close();
setActive(false);
- return true;
+ return !d->sizeLimitExceeded;
}
#define SERIALIZE_CMD(c) \
@@ -286,6 +288,19 @@ void QPicturePaintEngine::updateRenderHints(QPainter::RenderHints hints)
void QPicturePaintEngine::writeCmdLength(int pos, const QRectF &r, bool corr)
{
Q_D(QPicturePaintEngine);
+
+ constexpr int sizeLimit = std::numeric_limits<int>::max() - 8; // Leave room for ending bytes
+ if (d->sizeLimitExceeded || d->pic_d->pictb.pos() > sizeLimit) {
+ d->pic_d->trecs--; // Remove last command added, started by SERIALIZE_CMD
+ d->pic_d->pictb.seek(pos - 2);
+ d->pic_d->pictbData.resize(pos - 2);
+ if (!d->sizeLimitExceeded) {
+ d->sizeLimitExceeded = true;
+ qWarning("QPicture: size limit exceeded, will be truncated");
+ }
+ return;
+ }
+
int newpos = d->pic_d->pictb.pos(); // new position
int length = newpos - pos;
QRectF br(r);
diff --git a/src/gui/image/qpicture.cpp b/src/gui/image/qpicture.cpp
index ac0525d7abf..db2a5fd9ba9 100644
--- a/src/gui/image/qpicture.cpp
+++ b/src/gui/image/qpicture.cpp
@@ -970,7 +970,8 @@ QPicture& QPicture::operator=(const QPicture &p)
Constructs a QPicturePrivate
*/
QPicturePrivate::QPicturePrivate()
- : in_memory_only(false)
+ : pictb(&pictbData),
+ in_memory_only(false)
{
}
@@ -980,7 +981,8 @@ QPicturePrivate::QPicturePrivate()
Copy-Constructs a QPicturePrivate. Needed when detaching.
*/
QPicturePrivate::QPicturePrivate(const QPicturePrivate &other)
- : trecs(other.trecs),
+ : pictb(&pictbData),
+ trecs(other.trecs),
formatOk(other.formatOk),
formatMinor(other.formatMinor),
brect(other.brect),
diff --git a/src/gui/image/qpicture_p.h b/src/gui/image/qpicture_p.h
index c512f49320b..1d8142f44b7 100644
--- a/src/gui/image/qpicture_p.h
+++ b/src/gui/image/qpicture_p.h
@@ -113,6 +113,7 @@ public:
bool checkFormat();
void resetFormat();
+ QByteArray pictbData;
QBuffer pictb;
int trecs;
bool formatOk;
diff --git a/src/gui/image/qppmhandler.cpp b/src/gui/image/qppmhandler.cpp
index a0a1dcdaca9..8a413ded95e 100644
--- a/src/gui/image/qppmhandler.cpp
+++ b/src/gui/image/qppmhandler.cpp
@@ -123,8 +123,8 @@ static bool read_pbm_body(QIODevice *device, char type, int w, int h, int mcc, Q
break;
case '2': // ascii PGM
case '5': // raw PGM
- nbits = 8;
- format = QImage::Format_Grayscale8;
+ nbits = mcc <= std::numeric_limits<uint8_t>::max() ? 8 : 16;
+ format = mcc <= std::numeric_limits<uint8_t>::max() ? QImage::Format_Grayscale8 : QImage::Format_Grayscale16;
break;
case '3': // ascii PPM
case '6': // raw PPM
@@ -175,20 +175,20 @@ static bool read_pbm_body(QIODevice *device, char type, int w, int h, int mcc, Q
}
}
delete[] buf24;
- } else if (nbits == 8 && mcc > 255) { // type 5 16bit
- pbm_bpl = 2*w;
+ } else if (nbits == 16) { // type 5 16bit
+ pbm_bpl = sizeof(uint16_t)*w;
uchar *buf16 = new uchar[pbm_bpl];
for (y=0; y<h; y++) {
if (device->read((char *)buf16, pbm_bpl) != pbm_bpl) {
delete[] buf16;
return false;
}
- uchar *p = outImage->scanLine(y);
- uchar *end = p + w;
- uchar *b = buf16;
+ uint16_t *p = reinterpret_cast<uint16_t *>(outImage->scanLine(y));
+ uint16_t *end = p + w;
+ uint16_t *b = reinterpret_cast<uint16_t *>(buf16);
while (p < end) {
- *p++ = (b[0] << 8 | b[1]) * 255 / mcc;
- b += 2;
+ *p++ = qFromBigEndian(*b) * std::numeric_limits<uint16_t>::max() / mcc;
+ b++;
}
}
delete[] buf16;
@@ -225,13 +225,25 @@ static bool read_pbm_body(QIODevice *device, char type, int w, int h, int mcc, Q
*p++ = b;
}
} else if (nbits == 8) {
- if (mcc == 255) {
+ if (mcc == std::numeric_limits<uint8_t>::max()) {
while (n-- && ok) {
*p++ = read_pbm_int(device, &ok);
}
} else {
while (n-- && ok) {
- *p++ = (read_pbm_int(device, &ok) & 0xffff) * 255 / mcc;
+ *p++ = (read_pbm_int(device, &ok) & 0xffff) * std::numeric_limits<uint8_t>::max() / mcc;
+ }
+ }
+ } else if (nbits == 16) {
+ uint16_t* data = reinterpret_cast<uint16_t*>(p);
+ qsizetype numPixel = n/2;
+ if (mcc == std::numeric_limits<uint16_t>::max()) {
+ while (numPixel-- && ok) {
+ *data++ = read_pbm_int(device, &ok);
+ }
+ } else {
+ while (numPixel-- && ok) {
+ *data++ = (read_pbm_int(device, &ok) & 0xffff) * std::numeric_limits<uint16_t>::max() / mcc;
}
}
} else { // 32 bits
@@ -280,7 +292,7 @@ static bool write_pbm_image(QIODevice *out, const QImage &sourceImage, QByteArra
if (format == "pbm") {
image = image.convertToFormat(QImage::Format_Mono);
} else if (gray) {
- image = image.convertToFormat(QImage::Format_Grayscale8);
+ image = image.depth() <= 8 ? image.convertToFormat(QImage::Format_Grayscale8) : image.convertToFormat(QImage::Format_Grayscale16);
} else {
switch (image.format()) {
case QImage::Format_Mono:
@@ -388,6 +400,34 @@ static bool write_pbm_image(QIODevice *out, const QImage &sourceImage, QByteArra
delete [] buf;
break;
}
+ case 16: {
+ str.insert(1, gray ? '5' : '6');
+ str.append("65535\n");
+ if (out->write(str, str.size()) != str.size())
+ return false;
+ qsizetype bpl = sizeof(uint16_t) * qsizetype(w) * (gray ? 1 : 3);
+ uchar *buf = new uchar[bpl];
+ for (uint y=0; y<h; y++) {
+ const uint16_t *b = reinterpret_cast<const uint16_t *>(image.constScanLine(y));
+ uint16_t *p = reinterpret_cast<uint16_t *>(buf);
+ uint16_t *end = reinterpret_cast<uint16_t *>(buf + bpl);
+ if (gray) {
+ while (p < end)
+ *p++ = qToBigEndian(*b++);
+ } else {
+ while (p < end) {
+ uchar color = qToBigEndian(*b++);
+ *p++ = color;
+ *p++ = color;
+ *p++ = color;
+ }
+ }
+ if (bpl != (qsizetype)out->write((char*)buf, bpl))
+ return false;
+ }
+ delete [] buf;
+ break;
+ }
case 32: {
str.insert(1, '6');
@@ -530,7 +570,10 @@ QVariant QPpmHandler::option(ImageOption option) const
break;
case '2': // ascii PGM
case '5': // raw PGM
- format = QImage::Format_Grayscale8;
+ if (mcc <= std::numeric_limits<uint8_t>::max())
+ format = QImage::Format_Grayscale8;
+ else
+ format = QImage::Format_Grayscale16;
break;
case '3': // ascii PPM
case '6': // raw PPM
diff --git a/src/gui/painting/qcolorspace.cpp b/src/gui/painting/qcolorspace.cpp
index c43d133dd1e..9149971b999 100644
--- a/src/gui/painting/qcolorspace.cpp
+++ b/src/gui/painting/qcolorspace.cpp
@@ -1206,12 +1206,12 @@ QByteArray QColorSpace::iccProfile() const
QColorSpace QColorSpace::fromIccProfile(const QByteArray &iccProfile)
{
// Must detach if input is fromRawData(); nullTerminated() is trick to do that and nothing else
- const QByteArray ownedIccProfile(iccProfile.nullTerminated());
+ QByteArray ownedIccProfile = iccProfile.nullTerminated();
QColorSpace colorSpace;
if (QIcc::fromIccProfile(ownedIccProfile, &colorSpace))
return colorSpace;
colorSpace.detach();
- colorSpace.d_ptr->iccProfile = ownedIccProfile;
+ colorSpace.d_ptr->iccProfile = std::move(ownedIccProfile);
return colorSpace;
}
diff --git a/src/gui/painting/qpainter.cpp b/src/gui/painting/qpainter.cpp
index a3f9f069b69..2b6a8a858f9 100644
--- a/src/gui/painting/qpainter.cpp
+++ b/src/gui/painting/qpainter.cpp
@@ -6474,8 +6474,9 @@ QRectF QPainter::boundingRect(const QRectF &r, const QString &text, const QTextO
and height (on both 1x and 2x displays), and produces high-resolution
output on 2x displays.
- The \a position offset is always in the painter coordinate system,
- indepentent of display devicePixelRatio.
+ The \a position offset is provided in the device independent pixels
+ relative to the top-left corner of the \a rectangle. The \a position
+ can be used to align the repeating pattern inside the \a rectangle.
\sa drawPixmap()
*/
@@ -6497,8 +6498,8 @@ void QPainter::drawTiledPixmap(const QRectF &r, const QPixmap &pixmap, const QPo
qt_painter_thread_test(d->device->devType(), d->engine->type(), "drawTiledPixmap()");
#endif
- qreal sw = pixmap.width();
- qreal sh = pixmap.height();
+ const qreal sw = pixmap.width() / pixmap.devicePixelRatio();
+ const qreal sh = pixmap.height() / pixmap.devicePixelRatio();
qreal sx = sp.x();
qreal sy = sp.y();
if (sx < 0)
@@ -6579,8 +6580,12 @@ void QPainter::drawTiledPixmap(const QRectF &r, const QPixmap &pixmap, const QPo
(\a{x}, \a{y}) specifies the top-left point in the paint device
that is to be drawn onto; with the given \a width and \a
- height. (\a{sx}, \a{sy}) specifies the top-left point in the \a
- pixmap that is to be drawn; this defaults to (0, 0).
+ height.
+
+ (\a{sx}, \a{sy}) specifies the origin inside the specified rectangle
+ where the pixmap will be drawn. The origin position is specified in
+ the device independent pixels relative to (\a{x}, \a{y}). This defaults
+ to (0, 0).
*/
#ifndef QT_NO_PICTURE
diff --git a/src/network/access/http2/http2frames.cpp b/src/network/access/http2/http2frames.cpp
index 3b52204c7d3..e6a3474d7b0 100644
--- a/src/network/access/http2/http2frames.cpp
+++ b/src/network/access/http2/http2frames.cpp
@@ -34,7 +34,8 @@ FrameType Frame::type() const
quint32 Frame::streamID() const
{
Q_ASSERT(buffer.size() >= frameHeaderSize);
- return qFromBigEndian<quint32>(&buffer[5]);
+ // RFC 9113, 4.1: 31-bit Stream ID; lastValidStreamID(0x7FFFFFFF) masks out the reserved MSB
+ return qFromBigEndian<quint32>(&buffer[5]) & lastValidStreamID;
}
FrameFlags Frame::flags() const
diff --git a/src/network/access/qhttp2connection.cpp b/src/network/access/qhttp2connection.cpp
index 1d5c0d92b63..2895e8335d2 100644
--- a/src/network/access/qhttp2connection.cpp
+++ b/src/network/access/qhttp2connection.cpp
@@ -454,7 +454,7 @@ void QHttp2Stream::internalSendDATA()
"[%p] stream %u, exhausted device %p, sent END_STREAM? %d, %ssending end stream "
"after DATA",
connection, m_streamID, m_uploadByteDevice, sentEND_STREAM,
- m_endStreamAfterDATA ? "" : "not ");
+ !sentEND_STREAM && m_endStreamAfterDATA ? "" : "not ");
if (!sentEND_STREAM && m_endStreamAfterDATA) {
// We need to send an empty DATA frame with END_STREAM since we
// have exhausted the device, but we haven't sent END_STREAM yet.
@@ -690,8 +690,9 @@ void QHttp2Stream::handleDATA(const Frame &inboundFrame)
m_recvWindow -= qint32(inboundFrame.payloadSize());
const bool endStream = inboundFrame.flags().testFlag(FrameFlag::END_STREAM);
+ const bool ignoreData = connection->streamIsIgnored(m_streamID);
// Uncompress data if needed and append it ...
- if (inboundFrame.dataSize() > 0 || endStream) {
+ if ((inboundFrame.dataSize() > 0 || endStream) && !ignoreData) {
QByteArray fragment(reinterpret_cast<const char *>(inboundFrame.dataBegin()),
inboundFrame.dataSize());
if (endStream)
@@ -1245,16 +1246,12 @@ void QHttp2Connection::connectionError(Http2Error errorCode, const char *message
{
Q_ASSERT(message);
// RFC 9113, 6.8: An endpoint MAY send multiple GOAWAY frames if circumstances change.
- // Anyway, we do not send multiple GOAWAY frames.
- if (m_goingAway)
- return;
qCCritical(qHttp2ConnectionLog, "[%p] Connection error: %s (%d)", this, message,
int(errorCode));
// RFC 9113, 6.8: Endpoints SHOULD always send a GOAWAY frame before closing a connection so
// that the remote peer can know whether a stream has been partially processed or not.
- m_goingAway = true;
sendGOAWAY(errorCode);
auto messageView = QLatin1StringView(message);
@@ -1295,6 +1292,20 @@ bool QHttp2Connection::isInvalidStream(quint32 streamID) noexcept
return (!stream || stream->wasResetbyPeer()) && !streamWasResetLocally(streamID);
}
+/*!
+ When we send a GOAWAY we also send the ID of the last stream we know about
+ at the time. Any stream that starts after this one is ignored, but we still
+ have to process HEADERS due to compression state, and DATA due to stream and
+ connection window size changes.
+ Other than that - any \a streamID for which this returns true should be
+ ignored, and deleted at the earliest convenience.
+*/
+bool QHttp2Connection::streamIsIgnored(quint32 streamID) const noexcept
+{
+ const bool streamIsRemote = (streamID & 1) == (m_connectionType == Type::Client ? 0 : 1);
+ return Q_UNLIKELY(streamIsRemote && m_lastStreamToProcess < streamID);
+}
+
bool QHttp2Connection::sendClientPreface()
{
QIODevice *socket = getSocket();
@@ -1359,9 +1370,16 @@ bool QHttp2Connection::sendWINDOW_UPDATE(quint32 streamID, quint32 delta)
bool QHttp2Connection::sendGOAWAY(Http2::Http2Error errorCode)
{
+ m_goingAway = true;
+ // If this is the first time, start the timer:
+ if (m_lastStreamToProcess == Http2::lastValidStreamID)
+ m_goawayGraceTimer.setRemainingTime(GoawayGracePeriod);
+ m_lastStreamToProcess = std::min(m_lastIncomingStreamID, m_lastStreamToProcess);
+ qCDebug(qHttp2ConnectionLog, "[%p] Sending GOAWAY frame, error code %u, last stream %u", this,
+ errorCode, m_lastStreamToProcess);
frameWriter.start(FrameType::GOAWAY, FrameFlag::EMPTY,
Http2PredefinedParameters::connectionStreamID);
- frameWriter.append(quint32(m_lastIncomingStreamID));
+ frameWriter.append(m_lastStreamToProcess);
frameWriter.append(quint32(errorCode));
return frameWriter.write(*getSocket());
}
@@ -1411,8 +1429,20 @@ void QHttp2Connection::handleDATA()
if (stream)
stream->handleDATA(inboundFrame);
- if (inboundFrame.flags().testFlag(FrameFlag::END_STREAM))
- emit receivedEND_STREAM(streamID);
+
+ if (inboundFrame.flags().testFlag(FrameFlag::END_STREAM)) {
+ const bool ignoreData = stream && streamIsIgnored(stream->streamID());
+ if (!ignoreData) {
+ emit receivedEND_STREAM(streamID);
+ } else {
+ // Stream opened after our GOAWAY cut-off. We would just drop the
+ // data, but needed to handle it enough to track sizes of streams and
+ // connection windows. Since we've now taken care of that, we can
+ // at last close and delete it.
+ stream->setState(QHttp2Stream::State::Closed);
+ delete stream;
+ }
+ }
if (sessionReceiveWindowSize < maxSessionReceiveWindowSize / 2) {
// @future[consider]: emit signal instead
@@ -1454,8 +1484,15 @@ void QHttp2Connection::handleHEADERS()
return;
}
- qCDebug(qHttp2ConnectionLog, "[%p] Created new incoming stream %d", this, streamID);
- emit newIncomingStream(newStream);
+ qCDebug(qHttp2ConnectionLog, "[%p] New incoming stream %d", this, streamID);
+ if (!streamIsIgnored(newStream->streamID())) {
+ emit newIncomingStream(newStream);
+ } else if (m_goawayGraceTimer.hasExpired()) {
+ // We gave the peer some time to handle the GOAWAY message, but they have started a new
+ // stream, so we error out.
+ connectionError(Http2Error::PROTOCOL_ERROR, "Peer refused to GOAWAY.");
+ return;
+ }
} else if (streamWasResetLocally(streamID)) {
qCDebug(qHttp2ConnectionLog,
"[%p] Received HEADERS on previously locally reset stream %d (must process but ignore)",
@@ -1500,6 +1537,9 @@ void QHttp2Connection::handlePRIORITY()
|| inboundFrame.type() == FrameType::HEADERS);
const auto streamID = inboundFrame.streamID();
+ if (streamIsIgnored(streamID))
+ return;
+
// RFC 9913, 6.3: If a PRIORITY frame is received with a stream identifier of 0x00, the
// recipient MUST respond with a connection error
if (streamID == connectionStreamID)
@@ -1534,11 +1574,14 @@ void QHttp2Connection::handleRST_STREAM()
{
Q_ASSERT(inboundFrame.type() == FrameType::RST_STREAM);
+ const auto streamID = inboundFrame.streamID();
+ if (streamIsIgnored(streamID))
+ return;
+
// RFC 9113, 6.4: RST_STREAM frames MUST be associated with a stream.
// If a RST_STREAM frame is received with a stream identifier of 0x0,
// the recipient MUST treat this as a connection error (Section 5.4.1)
// of type PROTOCOL_ERROR.
- const auto streamID = inboundFrame.streamID();
if (streamID == connectionStreamID)
return connectionError(PROTOCOL_ERROR, "RST_STREAM on 0x0");
@@ -1764,31 +1807,33 @@ void QHttp2Connection::handleGOAWAY()
Q_ASSERT(inboundFrame.payloadSize() >= 8);
const uchar *const src = inboundFrame.dataBegin();
- quint32 lastStreamID = qFromBigEndian<quint32>(src);
+ // RFC 9113, 4.1: 31-bit Stream ID; lastValidStreamID(0x7FFFFFFF) masks out the reserved MSB
+ const quint32 lastStreamID = qFromBigEndian<quint32>(src) & lastValidStreamID;
const Http2Error errorCode = Http2Error(qFromBigEndian<quint32>(src + 4));
- if (!lastStreamID) {
- // "The last stream identifier can be set to 0 if no
- // streams were processed."
- lastStreamID = 1;
- } else if (!(lastStreamID & 0x1)) {
- // 5.1.1 - we (client) use only odd numbers as stream identifiers.
+ // 6.8 "the GOAWAY contains the stream identifier of the last peer-initiated stream that was
+ // or might be processed on the sending endpoint in this connection."
+ // Alternatively, they can specify 0 as the last stream ID, meaning they are not intending to
+ // process any remaining stream(s).
+ const quint32 LocalMask = m_connectionType == Type::Client ? 1 : 0;
+ // The stream must match the LocalMask, meaning we initiated it, for the last stream ID to make
+ // sense - they are not processing their own streams.
+ if (lastStreamID != 0 && (lastStreamID & 0x1) != LocalMask)
return connectionError(PROTOCOL_ERROR, "GOAWAY with invalid last stream ID");
- } else if (lastStreamID >= m_nextStreamID) {
- // "A server that is attempting to gracefully shut down a connection SHOULD
- // send an initial GOAWAY frame with the last stream identifier set to 2^31-1
- // and a NO_ERROR code."
- if (lastStreamID != lastValidStreamID || errorCode != HTTP2_NO_ERROR)
- return connectionError(PROTOCOL_ERROR, "GOAWAY invalid stream/error code");
- } else {
- lastStreamID += 2;
- }
+ qCDebug(qHttp2ConnectionLog, "[%p] Received GOAWAY frame, error code %u, last stream %u",
+ this, errorCode, lastStreamID);
m_goingAway = true;
emit receivedGOAWAY(errorCode, lastStreamID);
- for (quint32 id = lastStreamID; id < m_nextStreamID; id += 2) {
+ // Since the embedded stream ID is the last one that was or _might be_ processed,
+ // we cancel anything that comes after it. 0 can be used in the special case that
+ // no streams at all were or will be processed.
+ const quint32 firstPossibleStream = m_connectionType == Type::Client ? 1 : 2;
+ const quint32 firstCancelledStream = lastStreamID ? lastStreamID + 2 : firstPossibleStream;
+ Q_ASSERT((firstCancelledStream & 0x1) == LocalMask);
+ for (quint32 id = firstCancelledStream; id < m_nextStreamID; id += 2) {
QHttp2Stream *stream = m_streams.value(id, nullptr);
if (stream && stream->isActive())
stream->finishWithError(errorCode, "Received GOAWAY"_L1);
@@ -1809,7 +1854,8 @@ void QHttp2Connection::handleWINDOW_UPDATE()
// errors on the connection flow-control window MUST be treated as a connection error
const bool valid = delta && delta <= quint32(std::numeric_limits<qint32>::max());
const auto streamID = inboundFrame.streamID();
-
+ if (streamIsIgnored(streamID))
+ return;
// RFC 9113, 6.9: A WINDOW_UPDATE frame with a length other than 4 octets MUST be treated
// as a connection error (Section 5.4.1) of type FRAME_SIZE_ERROR.
@@ -1939,6 +1985,18 @@ void QHttp2Connection::handleContinuedHEADERS()
if (streamWasResetLocally(streamID) || streamIt == m_streams.cend())
return; // No more processing without a stream from here on.
+ if (streamIsIgnored(streamID)) {
+ // Stream was established after GOAWAY cut-off, we ignore it, but we
+ // have to process things that alter state. That already happened, so we
+ // stop here.
+ if (continuedFrames[0].flags().testFlag(Http2::FrameFlag::END_STREAM)) {
+ if (QHttp2Stream *stream = streamIt.value()) {
+ stream->setState(QHttp2Stream::State::Closed);
+ delete stream;
+ }
+ }
+ return;
+ }
switch (firstFrameType) {
case FrameType::HEADERS:
diff --git a/src/network/access/qhttp2connection_p.h b/src/network/access/qhttp2connection_p.h
index dcdc0f91318..f3f14145278 100644
--- a/src/network/access/qhttp2connection_p.h
+++ b/src/network/access/qhttp2connection_p.h
@@ -283,6 +283,8 @@ private:
bool isInvalidStream(quint32 streamID) noexcept;
bool streamWasResetLocally(quint32 streamID) noexcept;
+ Q_ALWAYS_INLINE
+ bool streamIsIgnored(quint32 streamID) const noexcept;
void connectionError(Http2::Http2Error errorCode,
const char *message); // Connection failed to be established?
@@ -400,6 +402,10 @@ private:
bool m_goingAway = false;
bool pushPromiseEnabled = false;
quint32 m_lastIncomingStreamID = Http2::connectionStreamID;
+ // Gets lowered when/if we send GOAWAY:
+ quint32 m_lastStreamToProcess = Http2::lastValidStreamID;
+ static constexpr std::chrono::duration GoawayGracePeriod = std::chrono::seconds(60);
+ QDeadlineTimer m_goawayGraceTimer;
bool m_prefaceSent = false;
diff --git a/src/network/access/qhttpnetworkconnectionchannel.cpp b/src/network/access/qhttpnetworkconnectionchannel.cpp
index 6fe1ce38a61..9f59d11375b 100644
--- a/src/network/access/qhttpnetworkconnectionchannel.cpp
+++ b/src/network/access/qhttpnetworkconnectionchannel.cpp
@@ -5,7 +5,6 @@
#include "qhttpnetworkconnectionchannel_p.h"
#include "qhttpnetworkconnection_p.h"
-#include "qhttp2configuration.h"
#include "private/qnoncontiguousbytedevice_p.h"
#include <qdebug.h>
@@ -34,6 +33,12 @@ QT_BEGIN_NAMESPACE
// connection times out)
// We use 3 because we can get a _q_error 3 times depending on the timing:
static const int reconnectAttemptsDefault = 3;
+static const char keepAliveIdleOption[] = "QT_QNAM_TCP_KEEPIDLE";
+static const char keepAliveIntervalOption[] = "QT_QNAM_TCP_KEEPINTVL";
+static const char keepAliveCountOption[] = "QT_QNAM_TCP_KEEPCNT";
+static const int TCP_KEEPIDLE_DEF = 60;
+static const int TCP_KEEPINTVL_DEF = 10;
+static const int TCP_KEEPCNT_DEF = 5;
QHttpNetworkConnectionChannel::QHttpNetworkConnectionChannel()
: socket(nullptr)
@@ -914,6 +919,13 @@ void QHttpNetworkConnectionChannel::_q_connected_abstract_socket(QAbstractSocket
// not sure yet if it helps, but it makes sense
absSocket->setSocketOption(QAbstractSocket::KeepAliveOption, 1);
+ int kaIdleOption = qEnvironmentVariableIntegerValue(keepAliveIdleOption).value_or(TCP_KEEPIDLE_DEF);
+ int kaIntervalOption = qEnvironmentVariableIntegerValue(keepAliveIntervalOption).value_or(TCP_KEEPINTVL_DEF);
+ int kaCountOption = qEnvironmentVariableIntegerValue(keepAliveCountOption).value_or(TCP_KEEPCNT_DEF);
+ absSocket->setSocketOption(QAbstractSocket::KeepAliveIdleOption, kaIdleOption);
+ absSocket->setSocketOption(QAbstractSocket::KeepAliveIntervalOption, kaIntervalOption);
+ absSocket->setSocketOption(QAbstractSocket::KeepAliveCountOption, kaCountOption);
+
pipeliningSupported = QHttpNetworkConnectionChannel::PipeliningSupportUnknown;
// ### FIXME: if the server closes the connection unexpectedly, we shouldn't send the same broken request again!
diff --git a/src/network/socket/qabstractsocket.cpp b/src/network/socket/qabstractsocket.cpp
index 3c9cae1fe8b..975332a14ab 100644
--- a/src/network/socket/qabstractsocket.cpp
+++ b/src/network/socket/qabstractsocket.cpp
@@ -360,6 +360,21 @@
allow setting the MTU for transmission.
This enum value was introduced in Qt 5.11.
+ \value KeepAliveIdleOption The time in seconds the connection needs to
+ remain idle before TCP starts sending keepalive probes if
+ KeepAliveOption is enabled.
+ This enum value was introduced in Qt 6.11.
+
+ \value KeepAliveIntervalOption The time in seconds between individual
+ keepalive probes, if KeepAliveOption is enabled. This option is not
+ supported in all OSes.
+ This enum value was introduced in Qt 6.11.
+
+ \value KeepAliveCountOption The maximum number of keepalive probes to
+ send before TCP drops the connection, if KeepAliveOption is enabled.
+ This option is not supported in all OSes.
+ This enum value was introduced in Qt 6.11.
+
Possible values for \e{TypeOfServiceOption} are:
\table
@@ -1973,6 +1988,18 @@ void QAbstractSocket::setSocketOption(QAbstractSocket::SocketOption option, cons
case PathMtuSocketOption:
d_func()->socketEngine->setOption(QAbstractSocketEngine::PathMtuInformation, value.toInt());
break;
+
+ case KeepAliveIdleOption:
+ d_func()->socketEngine->setOption(QAbstractSocketEngine::KeepAliveIdleOption, value.toInt());
+ break;
+
+ case KeepAliveIntervalOption:
+ d_func()->socketEngine->setOption(QAbstractSocketEngine::KeepAliveIntervalOption, value.toInt());
+ break;
+
+ case KeepAliveCountOption:
+ d_func()->socketEngine->setOption(QAbstractSocketEngine::KeepAliveCountOption, value.toInt());
+ break;
}
}
@@ -2019,6 +2046,18 @@ QVariant QAbstractSocket::socketOption(QAbstractSocket::SocketOption option)
case PathMtuSocketOption:
ret = d_func()->socketEngine->option(QAbstractSocketEngine::PathMtuInformation);
break;
+
+ case KeepAliveIdleOption:
+ ret = d_func()->socketEngine->option(QAbstractSocketEngine::KeepAliveIdleOption);
+ break;
+
+ case KeepAliveIntervalOption:
+ ret = d_func()->socketEngine->option(QAbstractSocketEngine::KeepAliveIntervalOption);
+ break;
+
+ case KeepAliveCountOption:
+ ret = d_func()->socketEngine->option(QAbstractSocketEngine::KeepAliveCountOption);
+ break;
}
if (ret == -1)
return QVariant();
diff --git a/src/network/socket/qabstractsocket.h b/src/network/socket/qabstractsocket.h
index 8d2e8a299cc..e0707a8ca8a 100644
--- a/src/network/socket/qabstractsocket.h
+++ b/src/network/socket/qabstractsocket.h
@@ -108,7 +108,10 @@ public:
TypeOfServiceOption, //IP_TOS
SendBufferSizeSocketOption, //SO_SNDBUF
ReceiveBufferSizeSocketOption, //SO_RCVBUF
- PathMtuSocketOption // IP_MTU
+ PathMtuSocketOption, // IP_MTU
+ KeepAliveIdleOption, // TCP_KEEPIDLE
+ KeepAliveIntervalOption, // TCP_KEEPINTVL
+ KeepAliveCountOption // TCP_KEEPCNT
};
Q_ENUM(SocketOption)
enum BindFlag {
diff --git a/src/network/socket/qabstractsocketengine_p.h b/src/network/socket/qabstractsocketengine_p.h
index 9340df009a7..c1d9ca56a7f 100644
--- a/src/network/socket/qabstractsocketengine_p.h
+++ b/src/network/socket/qabstractsocketengine_p.h
@@ -65,6 +65,9 @@ public:
MaxStreamsSocketOption,
PathMtuInformation,
BindInterfaceIndex,
+ KeepAliveIdleOption,
+ KeepAliveIntervalOption,
+ KeepAliveCountOption,
};
enum PacketHeaderOption {
diff --git a/src/network/socket/qnativesocketengine_unix.cpp b/src/network/socket/qnativesocketengine_unix.cpp
index 430197ccc6e..e920de11c13 100644
--- a/src/network/socket/qnativesocketengine_unix.cpp
+++ b/src/network/socket/qnativesocketengine_unix.cpp
@@ -170,6 +170,26 @@ static void convertToLevelAndOption(QNativeSocketEngine::SocketOption opt,
#endif
}
break;
+ case QNativeSocketEngine::KeepAliveIdleOption:
+ level = IPPROTO_TCP;
+#ifdef TCP_KEEPALIVE
+ n = TCP_KEEPALIVE;
+#else
+ n = TCP_KEEPIDLE;
+#endif
+ break;
+ case QNativeSocketEngine::KeepAliveIntervalOption:
+#ifdef TCP_KEEPINTVL
+ level = IPPROTO_TCP;
+ n = TCP_KEEPINTVL;
+#endif
+ break;
+ case QNativeSocketEngine::KeepAliveCountOption:
+#ifdef TCP_KEEPCNT
+ level = IPPROTO_TCP;
+ n = TCP_KEEPCNT;
+#endif
+ break;
}
}
@@ -200,7 +220,7 @@ bool QNativeSocketEnginePrivate::createNewSocket(QAbstractSocket::SocketType soc
int type = (socketType == QAbstractSocket::UdpSocket) ? SOCK_DGRAM : SOCK_STREAM;
int socket = qt_safe_socket(domain, type, protocol, O_NONBLOCK);
- if (socket < 0 && socketProtocol == QAbstractSocket::AnyIPProtocol && errno == EAFNOSUPPORT) {
+ if (socket < 0 && socketProtocol == QAbstractSocket::AnyIPProtocol && (errno == EAFNOSUPPORT || errno == ENOTSUP )) {
domain = AF_INET;
socket = qt_safe_socket(domain, type, protocol, O_NONBLOCK);
socketProtocol = QAbstractSocket::IPv4Protocol;
@@ -1161,6 +1181,8 @@ qint64 QNativeSocketEnginePrivate::nativeSendDatagram(const char *data, qint64 l
sentBytes = -2;
break;
case EMSGSIZE:
+ // seen on VxWorks
+ case ENOMEM:
setError(QAbstractSocket::DatagramTooLargeError, DatagramTooLargeErrorString);
break;
case ECONNRESET:
diff --git a/src/network/socket/qnativesocketengine_win.cpp b/src/network/socket/qnativesocketengine_win.cpp
index 598467ef629..8530c6ca819 100644
--- a/src/network/socket/qnativesocketengine_win.cpp
+++ b/src/network/socket/qnativesocketengine_win.cpp
@@ -34,6 +34,15 @@ QT_BEGIN_NAMESPACE
#ifndef IP_HOPLIMIT
#define IP_HOPLIMIT 21 // Receive packet hop limit.
#endif
+#ifndef TCP_KEEPIDLE
+#define TCP_KEEPIDLE 3
+#endif
+#ifndef TCP_KEEPINTVL
+#define TCP_KEEPINTVL 17
+#endif
+#ifndef TCP_KEEPCNT
+#define TCP_KEEPCNT 16
+#endif
#if defined(QNATIVESOCKETENGINE_DEBUG)
@@ -216,6 +225,18 @@ static void convertToLevelAndOption(QNativeSocketEngine::SocketOption opt,
case QAbstractSocketEngine::PathMtuInformation:
break; // not supported on Windows
+ case QNativeSocketEngine::KeepAliveIdleOption:
+ level = IPPROTO_TCP;
+ n = TCP_KEEPIDLE; // defined in ws2ipdef.h
+ break;
+ case QNativeSocketEngine::KeepAliveIntervalOption:
+ level = IPPROTO_TCP;
+ n = TCP_KEEPINTVL; // defined in ws2ipdef.h
+ break;
+ case QNativeSocketEngine::KeepAliveCountOption:
+ level = IPPROTO_TCP;
+ n = TCP_KEEPCNT; // defined in ws2ipdef.h
+ break;
}
}
diff --git a/src/plugins/platforms/cocoa/qcocoaapplicationdelegate.mm b/src/plugins/platforms/cocoa/qcocoaapplicationdelegate.mm
index 7644867700a..7ca3e61dfa5 100644
--- a/src/plugins/platforms/cocoa/qcocoaapplicationdelegate.mm
+++ b/src/plugins/platforms/cocoa/qcocoaapplicationdelegate.mm
@@ -25,6 +25,8 @@
#include <qpa/qwindowsysteminterface.h>
#include <qwindowdefs.h>
+#include <QtCore/private/qdarwinsecurityscopedfileengine_p.h>
+
QT_USE_NAMESPACE
@implementation QCocoaApplicationDelegate {
@@ -194,6 +196,23 @@ QT_USE_NAMESPACE
QCocoaMenuBar::insertWindowMenu();
}
+/*!
+ Tells the delegate to open the specified files
+
+ Sent by the system when the user drags a file to the app's icon
+ in places like Finder or the Dock, or opens a file via the "Open
+ With" menu in Finder.
+
+ These actions can happen when the application is not running,
+ in which case the call comes in between willFinishLaunching
+ and didFinishLaunching. In this case we don't pass on the
+ incoming file paths as file open events, as the paths are
+ also part of the command line arguments, and Qt applications
+ normally expect to handle file opening via those.
+
+ \note The app must register itself as a handler for each file
+ type via the CFBundleDocumentTypes key in the Info.plist.
+ */
- (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
{
Q_UNUSED(filenames);
@@ -209,7 +228,10 @@ QT_USE_NAMESPACE
if (qApp->arguments().contains(qtFileName))
continue;
}
- QWindowSystemInterface::handleFileOpenEvent(qtFileName);
+ QUrl url = qt_apple_urlFromPossiblySecurityScopedURL([NSURL fileURLWithPath:fileName]);
+ QWindowSystemInterface::handleFileOpenEvent(url);
+ // FIXME: We're supposed to call [NSApp replyToOpenOrPrint:] here, but we
+ // don't know if the open operation succeeded, failed, or was cancelled.
}
if ([reflectionDelegate respondsToSelector:_cmd])
@@ -262,6 +284,17 @@ QT_USE_NAMESPACE
}
}
+/*!
+ Returns a Boolean value that indicates if the app responds
+ to reopen AppleEvents.
+
+ These events are sent whenever the Finder reactivates an already
+ running application because someone double-clicked it again or used
+ the dock to activate it.
+
+ We pass the activation on to Qt, and return YES, to let AppKit
+ follow its normal flow.
+ */
- (BOOL)applicationShouldHandleReopen:(NSApplication *)theApplication hasVisibleWindows:(BOOL)flag
{
if ([reflectionDelegate respondsToSelector:_cmd])
@@ -309,6 +342,25 @@ QT_USE_NAMESPACE
[self doesNotRecognizeSelector:invocationSelector];
}
+/*!
+ Callback for when the application is asked to pick up a user activity
+ from another app (also known as Handoff, which is part of the bigger
+ Continuity story for Apple operating systems).
+
+ This is normally managed by two apps by the same vendor explicitly
+ initiating a custom NSUserActivity and picking it up in another app
+ on the same or another device, which we don't have APIs for.
+
+ This is also how the system supports Universal Links, where a web page
+ can deep-link into an app. In this case the app needs to claim and
+ validate an associated domain. The resulting link will be delivered
+ as a special NSUserActivityTypeBrowsingWeb activity type, which we
+ treat as QDesktopServices::handleUrl().
+
+ Finally, for NS/UIDocument based apps (which Qt is not), the system
+ automatically handles document hand-off if the application includes
+ the NSUbiquitousDocumentUserActivityType key in its Info.plist.
+ */
- (BOOL)application:(NSApplication *)application continueUserActivity:(NSUserActivity *)userActivity
restorationHandler:(void(^)(NSArray<id<NSUserActivityRestoring>> *restorableObjects))restorationHandler
{
@@ -331,6 +383,18 @@ QT_USE_NAMESPACE
return NO;
}
+/*!
+ Callback for when the app is asked to open custom URL schemes.
+
+ We register a handler for events of type kInternetEventClass with the
+ NSAppleEventManager during application start.
+
+ The application must include the schemes in the CFBundleURLTypes
+ key of the Info.plist.
+
+ \note This callback is not used for http/https URLs, see
+ continueUserActivity above for how we handle that.
+ */
- (void)getUrl:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent
{
Q_UNUSED(replyEvent);
diff --git a/src/plugins/platforms/cocoa/qcocoafiledialoghelper.mm b/src/plugins/platforms/cocoa/qcocoafiledialoghelper.mm
index 4c4e5fac962..a79682e4e14 100644
--- a/src/plugins/platforms/cocoa/qcocoafiledialoghelper.mm
+++ b/src/plugins/platforms/cocoa/qcocoafiledialoghelper.mm
@@ -19,6 +19,7 @@
#include <QtCore/qregularexpression.h>
#include <QtCore/qpointer.h>
#include <QtCore/private/qcore_mac_p.h>
+#include <QtCore/private/qdarwinsecurityscopedfileengine_p.h>
#include <QtGui/qguiapplication.h>
#include <QtGui/private/qguiapplication_p.h>
@@ -395,14 +396,15 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions;
{
if (auto *openPanel = openpanel_cast(m_panel)) {
QList<QUrl> result;
- for (NSURL *url in openPanel.URLs) {
- QString path = QString::fromNSString(url.path).normalized(QString::NormalizationForm_C);
- result << QUrl::fromLocalFile(path);
- }
+ for (NSURL *url in openPanel.URLs)
+ result << qt_apple_urlFromPossiblySecurityScopedURL(url);
return result;
} else {
- QString filename = QString::fromNSString(m_panel.URL.path).normalized(QString::NormalizationForm_C);
- QFileInfo fileInfo(filename);
+ QUrl result = qt_apple_urlFromPossiblySecurityScopedURL(m_panel.URL);
+ if (qt_apple_isSandboxed())
+ return { result }; // Can't tweak suffix
+
+ QFileInfo fileInfo(result.toLocalFile());
if (fileInfo.suffix().isEmpty() && ![self fileInfoMatchesCurrentNameFilter:fileInfo]) {
// We end up in this situation if we accept a file name without extension
@@ -733,6 +735,26 @@ bool QCocoaFileDialogHelper::show(Qt::WindowFlags windowFlags, Qt::WindowModalit
return false;
}
+ if (qt_apple_isSandboxed()) {
+ static bool canRead = qt_mac_processHasEntitlement(
+ u"com.apple.security.files.user-selected.read-only"_s);
+ static bool canReadWrite = qt_mac_processHasEntitlement(
+ u"com.apple.security.files.user-selected.read-write"_s);
+
+ if (options()->acceptMode() == QFileDialogOptions::AcceptSave
+ && !canReadWrite) {
+ qWarning() << "Sandboxed application is missing user-selected files"
+ << "read-write entitlement. Falling back to non-native dialog";
+ return false;
+ }
+
+ if (!canReadWrite && !canRead) {
+ qWarning() << "Sandboxed application is missing user-selected files"
+ << "entitlement. Falling back to non-native dialog";
+ return false;
+ }
+ }
+
createNSOpenSavePanelDelegate();
return [m_delegate showPanel:windowModality withParent:parent];
diff --git a/src/plugins/platforms/cocoa/qnsview_mouse.mm b/src/plugins/platforms/cocoa/qnsview_mouse.mm
index 56ff5ec313e..ca7e9c808f7 100644
--- a/src/plugins/platforms/cocoa/qnsview_mouse.mm
+++ b/src/plugins/platforms/cocoa/qnsview_mouse.mm
@@ -290,8 +290,7 @@ static const QPointingDevice *pointingDeviceFor(qint64 deviceID)
if (qIsNaN(windowPoint.x) || qIsNaN(windowPoint.y)) {
screenPoint = [NSEvent mouseLocation];
} else {
- NSRect screenRect = [[theEvent window] convertRectToScreen:NSMakeRect(windowPoint.x, windowPoint.y, 1, 1)];
- screenPoint = screenRect.origin;
+ screenPoint = [theEvent.window convertPointToScreen:windowPoint];
}
} else {
screenPoint = [NSEvent mouseLocation];
diff --git a/src/plugins/platforms/direct2d/CMakeLists.txt b/src/plugins/platforms/direct2d/CMakeLists.txt
index 0b3ecf33967..38d7e4160a5 100644
--- a/src/plugins/platforms/direct2d/CMakeLists.txt
+++ b/src/plugins/platforms/direct2d/CMakeLists.txt
@@ -33,6 +33,7 @@ qt_internal_add_plugin(QWindowsDirect2DIntegrationPlugin
../windows/qwindowstheme.cpp ../windows/qwindowstheme.h
../windows/qwindowsthreadpoolrunner.h
../windows/qwindowswindow.cpp ../windows/qwindowswindow.h
+ ../windows/qwindowswindowclassdescription.cpp ../windows/qwindowswindowclassdescription.h
../windows/qwindowswindowclassregistry.cpp ../windows/qwindowswindowclassregistry.h
qwindowsdirect2dbackingstore.cpp qwindowsdirect2dbackingstore.h
qwindowsdirect2dbitmap.cpp qwindowsdirect2dbitmap.h
diff --git a/src/plugins/platforms/ios/qiosapplicationdelegate.mm b/src/plugins/platforms/ios/qiosapplicationdelegate.mm
index 380c5a588e6..7cbb4fc40f5 100644
--- a/src/plugins/platforms/ios/qiosapplicationdelegate.mm
+++ b/src/plugins/platforms/ios/qiosapplicationdelegate.mm
@@ -16,6 +16,8 @@
#include <QtCore/QtCore>
+#include <QtCore/private/qdarwinsecurityscopedfileengine_p.h>
+
@interface QIOSWindowSceneDelegate : NSObject<UIWindowSceneDelegate>
@property (nullable, nonatomic, strong) UIWindow *window;
@end
@@ -112,7 +114,7 @@
QIOSServices *iosServices = static_cast<QIOSServices *>(iosIntegration->services());
for (UIOpenURLContext *urlContext in URLContexts) {
- QUrl url = QUrl::fromNSURL(urlContext.URL);
+ QUrl url = qt_apple_urlFromPossiblySecurityScopedURL(urlContext.URL);
if (url.isLocalFile())
QWindowSystemInterface::handleFileOpenEvent(url);
else
diff --git a/src/plugins/platforms/ios/qiosdocumentpickercontroller.mm b/src/plugins/platforms/ios/qiosdocumentpickercontroller.mm
index c173aa426fc..57a5e100c9e 100644
--- a/src/plugins/platforms/ios/qiosdocumentpickercontroller.mm
+++ b/src/plugins/platforms/ios/qiosdocumentpickercontroller.mm
@@ -8,6 +8,7 @@
#include "qiosdocumentpickercontroller.h"
#include <QtCore/qpointer.h>
+#include <QtCore/private/qdarwinsecurityscopedfileengine_p.h>
@implementation QIOSDocumentPickerController {
QPointer<QIOSFileDialog> m_fileDialog;
@@ -17,9 +18,11 @@
{
NSMutableArray <UTType *> *docTypes = [[[NSMutableArray alloc] init] autorelease];
- QStringList nameFilters = fileDialog->options()->nameFilters();
- if (!nameFilters.isEmpty() && (fileDialog->options()->fileMode() != QFileDialogOptions::Directory
- || fileDialog->options()->fileMode() != QFileDialogOptions::DirectoryOnly))
+ const auto options = fileDialog->options();
+
+ const QStringList nameFilters = options->nameFilters();
+ if (!nameFilters.isEmpty() && (options->fileMode() != QFileDialogOptions::Directory
+ || options->fileMode() != QFileDialogOptions::DirectoryOnly))
{
QStringList results;
for (const QString &filter : nameFilters)
@@ -28,21 +31,8 @@
docTypes = [self computeAllowedFileTypes:results];
}
- // FIXME: Handle security scoped URLs instead of copying resource
- bool asCopy = [&]{
- switch (fileDialog->options()->fileMode()) {
- case QFileDialogOptions::AnyFile:
- case QFileDialogOptions::ExistingFile:
- case QFileDialogOptions::ExistingFiles:
- return true;
- default:
- // Folders can't be imported
- return false;
- }
- }();
-
if (!docTypes.count) {
- switch (fileDialog->options()->fileMode()) {
+ switch (options->fileMode()) {
case QFileDialogOptions::AnyFile:
case QFileDialogOptions::ExistingFile:
case QFileDialogOptions::ExistingFiles:
@@ -58,17 +48,39 @@
}
}
- if (self = [super initForOpeningContentTypes:docTypes asCopy:asCopy]) {
- m_fileDialog = fileDialog;
- self.modalPresentationStyle = UIModalPresentationFormSheet;
- self.delegate = self;
- self.presentationController.delegate = self;
+ if (options->acceptMode() == QFileDialogOptions::AcceptSave) {
+ auto selectedUrls = options->initiallySelectedFiles();
+ auto suggestedFileName = !selectedUrls.isEmpty() ? selectedUrls.first().fileName() : "Untitled";
- if (m_fileDialog->options()->fileMode() == QFileDialogOptions::ExistingFiles)
+ // Create an empty dummy file, so that the export dialog will allow us
+ // to choose the export destination, which we are then given access to
+ // write to.
+ NSURL *dummyExportFile = [NSFileManager.defaultManager.temporaryDirectory
+ URLByAppendingPathComponent:suggestedFileName.toNSString()];
+ [NSFileManager.defaultManager createFileAtPath:dummyExportFile.path contents:nil attributes:nil];
+
+ if (!(self = [super initForExportingURLs:@[dummyExportFile]]))
+ return nil;
+
+ // Note, we don't set the directoryURL, as if the directory can't be
+ // accessed, or written to, the file dialog is shown but is empty.
+ // FIXME: See comment below for open dialogs as well
+ } else {
+ if (!(self = [super initForOpeningContentTypes:docTypes asCopy:NO]))
+ return nil;
+
+ if (options->fileMode() == QFileDialogOptions::ExistingFiles)
self.allowsMultipleSelection = YES;
- self.directoryURL = m_fileDialog->options()->initialDirectory().toNSURL();
+ // FIXME: This doesn't seem to have any effect
+ self.directoryURL = options->initialDirectory().toNSURL();
}
+
+ m_fileDialog = fileDialog;
+ self.modalPresentationStyle = UIModalPresentationFormSheet;
+ self.delegate = self;
+ self.presentationController.delegate = self;
+
return self;
}
@@ -81,7 +93,7 @@
QList<QUrl> files;
for (NSURL* url in urls)
- files.append(QUrl::fromNSURL(url));
+ files.append(qt_apple_urlFromPossiblySecurityScopedURL(url));
m_fileDialog->selectedFilesChanged(files);
emit m_fileDialog->accept();
diff --git a/src/plugins/platforms/ios/qiosfiledialog.mm b/src/plugins/platforms/ios/qiosfiledialog.mm
index b7d3e488bbb..6e7c10117ed 100644
--- a/src/plugins/platforms/ios/qiosfiledialog.mm
+++ b/src/plugins/platforms/ios/qiosfiledialog.mm
@@ -49,14 +49,10 @@ bool QIOSFileDialog::show(Qt::WindowFlags windowFlags, Qt::WindowModality window
// when converted to QUrl, it becames a scheme.
const QString scheme = initialDir.scheme();
- if (acceptOpen) {
- if (directory.startsWith("assets-library:"_L1) || scheme == "assets-library"_L1)
- return showImagePickerDialog(parent);
- else
- return showNativeDocumentPickerDialog(parent);
- }
+ if (acceptOpen && (directory.startsWith("assets-library:"_L1) || scheme == "assets-library"_L1))
+ return showImagePickerDialog(parent);
- return false;
+ return showNativeDocumentPickerDialog(parent);
}
void QIOSFileDialog::showImagePickerDialog_helper(QWindow *parent)
diff --git a/src/plugins/platforms/wasm/qwasmcompositor.cpp b/src/plugins/platforms/wasm/qwasmcompositor.cpp
index 99cf8885af5..1b43ff78991 100644
--- a/src/plugins/platforms/wasm/qwasmcompositor.cpp
+++ b/src/plugins/platforms/wasm/qwasmcompositor.cpp
@@ -17,19 +17,14 @@ bool QWasmCompositor::m_requestUpdateHoldEnabled = false;
QWasmCompositor::QWasmCompositor(QWasmScreen *screen)
: QObject(screen)
-, m_animationFrameHandler(QWasmAnimationFrameHandler([this](double frametime){
- Q_UNUSED(frametime);
- this->m_requestAnimationFrameId = -1;
- this->deliverUpdateRequests();
- }))
{
QWindowSystemInterface::setSynchronousWindowSystemEvents(true);
}
QWasmCompositor::~QWasmCompositor()
{
- if (m_requestAnimationFrameId != -1)
- m_animationFrameHandler.cancelAnimationFrame(m_requestAnimationFrameId);
+ if (m_drawCallbackHandle != 0)
+ QWasmAnimationFrameMultiHandler::instance()->unregisterDrawCallback(m_drawCallbackHandle);
// TODO(mikolaj.boc): Investigate if m_isEnabled is needed at all. It seems like a frame should
// not be generated after this instead.
@@ -87,13 +82,18 @@ void QWasmCompositor::requestUpdateWindow(QWasmWindow *window, const QRect &upda
// Requests an update/new frame using RequestAnimationFrame
void QWasmCompositor::requestUpdate()
{
- if (m_requestAnimationFrameId != -1)
+ if (m_drawCallbackHandle != 0)
return;
if (m_requestUpdateHoldEnabled)
return;
- m_requestAnimationFrameId = m_animationFrameHandler.requestAnimationFrame();
+ m_drawCallbackHandle = QWasmAnimationFrameMultiHandler::instance()->registerDrawCallback(
+ [this](double frametime) {
+ Q_UNUSED(frametime);
+ m_drawCallbackHandle = 0;
+ deliverUpdateRequests();
+ });
}
void QWasmCompositor::deliverUpdateRequests()
@@ -165,28 +165,3 @@ QWasmScreen *QWasmCompositor::screen()
{
return static_cast<QWasmScreen *>(parent());
}
-
-QWasmAnimationFrameHandler::QWasmAnimationFrameHandler(std::function<void(double)> handler)
-{
- auto argCastWrapper = [handler](val arg){ handler(arg.as<double>()); };
- m_handlerIndex = QWasmSuspendResumeControl::get()->registerEventHandler(argCastWrapper);
-}
-
-QWasmAnimationFrameHandler::~QWasmAnimationFrameHandler()
-{
- QWasmSuspendResumeControl::get()->removeEventHandler(m_handlerIndex);
-}
-
-int64_t QWasmAnimationFrameHandler::requestAnimationFrame()
-{
- using ReturnType = double; // FIXME emscripten::val::call() does not support int64_t
- val handler = QWasmSuspendResumeControl::get()->jsEventHandlerAt(m_handlerIndex);
- return int64_t(val::global("window").call<ReturnType>("requestAnimationFrame", handler));
-}
-
-void QWasmAnimationFrameHandler::cancelAnimationFrame(int64_t id)
-{
- val::global("window").call<void>("cancelAnimationFrame", double(id));
-}
-
-
diff --git a/src/plugins/platforms/wasm/qwasmcompositor.h b/src/plugins/platforms/wasm/qwasmcompositor.h
index 8fc290dda3b..eb529a3d30b 100644
--- a/src/plugins/platforms/wasm/qwasmcompositor.h
+++ b/src/plugins/platforms/wasm/qwasmcompositor.h
@@ -7,6 +7,7 @@
#include "qwasmwindowstack.h"
#include <qpa/qplatformwindow.h>
+#include <private/qwasmsuspendresumecontrol_p.h>
#include <QMap>
#include <tuple>
@@ -20,18 +21,6 @@ class QWasmScreen;
enum class QWasmWindowTreeNodeChangeType;
-class QWasmAnimationFrameHandler
-{
-public:
- QWasmAnimationFrameHandler(std::function<void(double)> handler);
- ~QWasmAnimationFrameHandler();
- int64_t requestAnimationFrame();
- void cancelAnimationFrame(int64_t id);
-
-private:
- uint32_t m_handlerIndex;
-};
-
class QWasmCompositor final : public QObject
{
Q_OBJECT
@@ -65,8 +54,7 @@ private:
bool m_isEnabled = true;
QMap<QWasmWindow *, std::tuple<QRect, UpdateRequestDeliveryType>> m_requestUpdateWindows;
- QWasmAnimationFrameHandler m_animationFrameHandler;
- int64_t m_requestAnimationFrameId = -1;
+ uint32_t m_drawCallbackHandle = 0;
bool m_inDeliverUpdateRequest = false;
static bool m_requestUpdateHoldEnabled;
};
diff --git a/src/plugins/platforms/wasm/qwasminputcontext.cpp b/src/plugins/platforms/wasm/qwasminputcontext.cpp
index a0546fdc215..6dfb4284149 100644
--- a/src/plugins/platforms/wasm/qwasminputcontext.cpp
+++ b/src/plugins/platforms/wasm/qwasminputcontext.cpp
@@ -24,13 +24,13 @@ using namespace qstdweb;
void QWasmInputContext::inputCallback(emscripten::val event)
{
- qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << "isComposing : " << event["isComposing"].as<bool>();
-
emscripten::val inputType = event["inputType"];
if (inputType.isNull() || inputType.isUndefined())
return;
const auto inputTypeString = inputType.as<std::string>();
+ // also may be dataTransfer
+ // containing rich text
emscripten::val inputData = event["data"];
QString inputStr = (!inputData.isNull() && !inputData.isUndefined())
? QString::fromEcmaString(inputData) : QString();
@@ -44,10 +44,10 @@ void QWasmInputContext::inputCallback(emscripten::val event)
QInputMethodQueryEvent queryEvent(Qt::ImQueryAll);
QCoreApplication::sendEvent(m_focusObject, &queryEvent);
int cursorPosition = queryEvent.value(Qt::ImCursorPosition).toInt();
-
int deleteLength = rangesPair.second - rangesPair.first;
int deleteFrom = -1;
- if (cursorPosition > rangesPair.first) {
+
+ if (cursorPosition >= rangesPair.first) {
deleteFrom = -(cursorPosition - rangesPair.first);
}
QInputMethodEvent e;
@@ -65,21 +65,55 @@ void QWasmInputContext::inputCallback(emscripten::val event)
event.call<void>("stopImmediatePropagation");
return;
} else if (!inputTypeString.compare("insertCompositionText")) {
- qCDebug(qLcQpaWasmInputContext) << "inputString : " << inputStr;
- insertPreedit();
+ qCDebug(qLcQpaWasmInputContext) << "insertCompositionText : " << inputStr;
+ event.call<void>("stopImmediatePropagation");
+
+ QInputMethodQueryEvent queryEvent(Qt::ImQueryAll);
+ QCoreApplication::sendEvent(m_focusObject, &queryEvent);
+
+ int qCursorPosition = queryEvent.value(Qt::ImCursorPosition).toInt() ;
+ int replaceIndex = (qCursorPosition - rangesPair.first);
+ int replaceLength = rangesPair.second - rangesPair.first;
+
+ setPreeditString(inputStr, replaceIndex);
+ insertPreedit(replaceLength);
+
+ rangesPair.first = 0;
+ rangesPair.second = 0;
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);
+ // the previous input string up to the space, needs replaced with this
+ // used on iOS when continuing composition after focus change
+ // there's no range given
+
+ qCDebug(qLcQpaWasmInputContext) << "insertReplacementText >>>>" << "inputString : " << inputStr;
+ emscripten::val ranges = event.call<emscripten::val>("getTargetRanges");
+
+ m_preeditString.clear();
+ std::string elementString = m_inputElement["value"].as<std::string>();
+ QInputMethodQueryEvent queryEvent(Qt::ImQueryAll);
+ QCoreApplication::sendEvent(m_focusObject, &queryEvent);
+ QString textFieldString = queryEvent.value(Qt::ImTextBeforeCursor).toString();
+ int qCursorPosition = queryEvent.value(Qt::ImCursorPosition).toInt();
+
+ if (rangesPair.first != 0 || rangesPair.second != 0) {
+
+ int replaceIndex = (qCursorPosition - rangesPair.first);
+ int replaceLength = rangesPair.second - rangesPair.first;
+ replaceText(inputStr, -replaceIndex, replaceLength);
+ rangesPair.first = 0;
+ rangesPair.second = 0;
+
+ } else {
+ int spaceIndex = textFieldString.lastIndexOf(' ') + 1;
+ int replaceIndex = (qCursorPosition - spaceIndex);
+
+ replaceText(inputStr, -replaceIndex, replaceIndex);
+ }
event.call<void>("stopImmediatePropagation");
+
return;
} else if (!inputTypeString.compare("deleteCompositionText")) {
setPreeditString("", 0);
@@ -92,7 +126,25 @@ void QWasmInputContext::inputCallback(emscripten::val event)
event.call<void>("stopImmediatePropagation");
return;
} else if (!inputTypeString.compare("insertText")) {
- insertText(inputStr);
+ if ((rangesPair.first != 0 || rangesPair.second != 0)
+ && rangesPair.first != rangesPair.second) {
+
+ QInputMethodQueryEvent queryEvent(Qt::ImQueryAll);
+ QCoreApplication::sendEvent(m_focusObject, &queryEvent);
+
+ int qCursorPosition = queryEvent.value(Qt::ImCursorPosition).toInt();
+ int replaceIndex = (qCursorPosition - rangesPair.first);
+ int replaceLength = rangesPair.second - rangesPair.first;
+
+ replaceText(inputStr, -replaceIndex, replaceLength);
+
+ rangesPair.first = 0;
+ rangesPair.second = 0;
+
+ } else {
+ insertText(inputStr);
+ }
+
event.call<void>("stopImmediatePropagation");
#if QT_CONFIG(clipboard)
} else if (!inputTypeString.compare("insertFromPaste")) {
@@ -112,9 +164,8 @@ void QWasmInputContext::inputCallback(emscripten::val event)
void QWasmInputContext::compositionEndCallback(emscripten::val event)
{
const auto inputStr = QString::fromEcmaString(event["data"]);
- qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << inputStr;
- if (preeditString().isEmpty())
+ if (preeditString().isEmpty()) // we get final results from inputCallback
return;
if (inputStr != preeditString()) {
@@ -127,27 +178,11 @@ void QWasmInputContext::compositionEndCallback(emscripten::val event)
void QWasmInputContext::compositionStartCallback(emscripten::val event)
{
- Q_UNUSED(event);
- qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO;
+ Q_UNUSED(event);
// Do nothing when starting composition
}
-/*
-// Test implementation
-static void beforeInputCallback(emscripten::val event)
-{
- qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO;
-
- auto ranges = event.call<emscripten::val>("getTargetRanges");
- auto length = ranges["length"].as<int>();
- for (auto i = 0; i < length; i++) {
- qCDebug(qLcQpaWasmInputContext) << ranges.call<emscripten::val>("get", i)["startOffset"].as<int>();
- qCDebug(qLcQpaWasmInputContext) << ranges.call<emscripten::val>("get", i)["endOffset"].as<int>();
- }
-}
-*/
-
void QWasmInputContext::compositionUpdateCallback(emscripten::val event)
{
const auto compositionStr = QString::fromEcmaString(event["data"]);
@@ -317,18 +352,21 @@ void QWasmInputContext::hideInputPanel()
void QWasmInputContext::setPreeditString(QString preeditStr, int replaceSize)
{
+ qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << preeditStr << replaceSize;
m_preeditString = preeditStr;
- m_replaceSize = replaceSize;
+ m_replaceIndex = replaceSize;
}
-void QWasmInputContext::insertPreedit()
+void QWasmInputContext::insertPreedit(int replaceLength)
{
qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << m_preeditString;
+ if (replaceLength == 0)
+ replaceLength = m_preeditString.length();
QList<QInputMethodEvent::Attribute> attributes;
{
QInputMethodEvent::Attribute attr_cursor(QInputMethodEvent::Cursor,
- m_preeditString.length(),
+ 0,
1);
attributes.append(attr_cursor);
@@ -336,21 +374,19 @@ void QWasmInputContext::insertPreedit()
format.setFontUnderline(true);
format.setUnderlineStyle(QTextCharFormat::SingleUnderline);
QInputMethodEvent::Attribute attr_format(QInputMethodEvent::TextFormat,
- 0,
- m_preeditString.length(), format);
+ 0,
+ replaceLength, format);
attributes.append(attr_format);
}
QInputMethodEvent e(m_preeditString, attributes);
- if (m_replaceSize > 0)
- e.setCommitString("", -m_replaceSize, m_replaceSize);
+ if (m_replaceIndex > 0)
+ e.setCommitString("", -m_replaceIndex, replaceLength);
QCoreApplication::sendEvent(m_focusObject, &e);
}
void QWasmInputContext::commitPreeditAndClear()
{
- qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << m_preeditString;
-
if (m_preeditString.isEmpty())
return;
QInputMethodEvent e;
@@ -360,7 +396,8 @@ void QWasmInputContext::commitPreeditAndClear()
}
void QWasmInputContext::insertText(QString inputStr, bool replace)
-{
+{ // commitString
+ qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << inputStr << replace;
Q_UNUSED(replace);
if (!inputStr.isEmpty()) {
const int replaceLen = 0;
@@ -370,4 +407,35 @@ void QWasmInputContext::insertText(QString inputStr, bool replace)
}
}
+/* This will replace the text in the focusobject at replaceFrom position, and replaceSize length
+ with the text in inputStr. */
+
+ void QWasmInputContext::replaceText(QString inputStr, int replaceFrom, int replaceSize)
+ {
+ qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << inputStr << replaceFrom << replaceSize;
+
+ QList<QInputMethodEvent::Attribute> attributes;
+ {
+ QInputMethodEvent::Attribute attr_cursor(QInputMethodEvent::Cursor,
+ 0, // start
+ 1); // length
+ attributes.append(attr_cursor);
+
+ QTextCharFormat format;
+ format.setFontUnderline(true);
+ format.setUnderlineStyle(QTextCharFormat::SingleUnderline);
+ QInputMethodEvent::Attribute attr_format(QInputMethodEvent::TextFormat,
+ 0,
+ replaceSize,
+ format);
+ attributes.append(attr_format);
+ }
+
+ QInputMethodEvent e1(QString(), attributes);
+ e1.setCommitString(inputStr, replaceFrom, replaceSize);
+ QCoreApplication::sendEvent(m_focusObject, &e1);
+
+ m_preeditString.clear();
+ }
+
QT_END_NAMESPACE
diff --git a/src/plugins/platforms/wasm/qwasminputcontext.h b/src/plugins/platforms/wasm/qwasminputcontext.h
index 97415451b2a..006a03455d7 100644
--- a/src/plugins/platforms/wasm/qwasminputcontext.h
+++ b/src/plugins/platforms/wasm/qwasminputcontext.h
@@ -33,10 +33,11 @@ public:
const QString preeditString() { return m_preeditString; }
void setPreeditString(QString preeditStr, int replaceSize);
- void insertPreedit();
+ void insertPreedit(int repalcementLength = 0);
void commitPreeditAndClear();
void insertText(QString inputStr, bool replace = false);
+ void replaceText(QString inputString, int replaceFrom, int replaceSize);
bool usingTextInput() const { return m_inputMethodAccepted; }
void setFocusObject(QObject *object) override;
@@ -58,7 +59,7 @@ private:
private:
QString m_preeditString;
- int m_replaceSize = 0;
+ int m_replaceIndex = 0;
bool m_inputMethodAccepted = false;
QObject *m_focusObject = nullptr;
diff --git a/src/plugins/platforms/wasm/qwasmwindow.cpp b/src/plugins/platforms/wasm/qwasmwindow.cpp
index a88299975d0..d318c977a90 100644
--- a/src/plugins/platforms/wasm/qwasmwindow.cpp
+++ b/src/plugins/platforms/wasm/qwasmwindow.cpp
@@ -761,12 +761,10 @@ void QWasmWindow::handleCompositionEndEvent(emscripten::val event)
void QWasmWindow::handleBeforeInputEvent(emscripten::val event)
{
- qWarning() << Q_FUNC_INFO;
-
if (QWasmInputContext *inputContext = QWasmIntegration::get()->wasmInputContext(); inputContext->isActive())
inputContext->beforeInputCallback(event);
- // else
- // m_focusHelper.set("innerHTML", std::string());
+ else
+ m_focusHelper.set("innerHTML", std::string());
}
void QWasmWindow::handlePointerEnterLeaveEvent(const PointerEvent &event)
diff --git a/src/plugins/platforms/wayland/qwaylandinputcontext.cpp b/src/plugins/platforms/wayland/qwaylandinputcontext.cpp
index 5ab285ad97d..0ccc4dba57a 100644
--- a/src/plugins/platforms/wayland/qwaylandinputcontext.cpp
+++ b/src/plugins/platforms/wayland/qwaylandinputcontext.cpp
@@ -192,12 +192,10 @@ void QWaylandInputContext::setFocusObject(QObject *object)
if (window && window->handle()) {
if (mCurrentWindow.data() != window) {
if (!inputMethodAccepted()) {
- if (mCurrentWindow) {
- auto *surface = static_cast<QWaylandWindow *>(mCurrentWindow->handle())->wlSurface();
- if (surface)
- inputInterface->disableSurface(surface);
- mCurrentWindow.clear();
- }
+ auto *surface = static_cast<QWaylandWindow *>(window->handle())->wlSurface();
+ if (surface)
+ inputInterface->disableSurface(surface);
+ mCurrentWindow.clear();
} else {
auto *surface = static_cast<QWaylandWindow *>(window->handle())->wlSurface();
if (surface) {
diff --git a/src/plugins/platforms/windows/CMakeLists.txt b/src/plugins/platforms/windows/CMakeLists.txt
index c7563c72979..40f173c8c82 100644
--- a/src/plugins/platforms/windows/CMakeLists.txt
+++ b/src/plugins/platforms/windows/CMakeLists.txt
@@ -38,6 +38,7 @@ qt_internal_add_plugin(QWindowsIntegrationPlugin
qwindowstheme.cpp qwindowstheme.h
qwindowsthreadpoolrunner.h
qwindowswindow.cpp qwindowswindow.h
+ qwindowswindowclassdescription.cpp qwindowswindowclassdescription.h
qwindowswindowclassregistry.cpp qwindowswindowclassregistry.h
NO_UNITY_BUILD_SOURCES
qwindowspointerhandler.cpp
diff --git a/src/plugins/platforms/windows/qwindowscontext.cpp b/src/plugins/platforms/windows/qwindowscontext.cpp
index 3013de1c068..156351987cb 100644
--- a/src/plugins/platforms/windows/qwindowscontext.cpp
+++ b/src/plugins/platforms/windows/qwindowscontext.cpp
@@ -696,7 +696,7 @@ HWND QWindowsContext::createDummyWindow(const QString &classNameIn,
{
if (!wndProc)
wndProc = DefWindowProc;
- QString className = d->m_windowClassRegistry.registerWindowClass(QWindowsWindowClassRegistry::classNamePrefix() + classNameIn, wndProc);
+ QString className = d->m_windowClassRegistry.registerWindowClass(classNameIn, wndProc);
return CreateWindowEx(0, reinterpret_cast<LPCWSTR>(className.utf16()),
windowName, style,
CW_USEDEFAULT, CW_USEDEFAULT,
diff --git a/src/plugins/platforms/windows/qwindowsscreen.cpp b/src/plugins/platforms/windows/qwindowsscreen.cpp
index 9139ec0c463..0236669d6fb 100644
--- a/src/plugins/platforms/windows/qwindowsscreen.cpp
+++ b/src/plugins/platforms/windows/qwindowsscreen.cpp
@@ -704,7 +704,7 @@ void QWindowsScreenManager::initialize()
qCDebug(lcQpaScreen) << "Initializing screen manager";
auto className = QWindowsWindowClassRegistry::instance()->registerWindowClass(
- QWindowsWindowClassRegistry::classNamePrefix() + QLatin1String("ScreenChangeObserverWindow"),
+ "ScreenChangeObserverWindow"_L1,
qDisplayChangeObserverWndProc);
// HWND_MESSAGE windows do not get WM_DISPLAYCHANGE, so we need to create
diff --git a/src/plugins/platforms/windows/qwindowssystemtrayicon.cpp b/src/plugins/platforms/windows/qwindowssystemtrayicon.cpp
index a2ce1e86a4d..beeab1a089e 100644
--- a/src/plugins/platforms/windows/qwindowssystemtrayicon.cpp
+++ b/src/plugins/platforms/windows/qwindowssystemtrayicon.cpp
@@ -119,9 +119,9 @@ static inline HWND createTrayIconMessageWindow()
if (!ctx)
return nullptr;
// Register window class in the platform plugin.
- const QString className =
- ctx->registerWindowClass(QWindowsWindowClassRegistry::classNamePrefix() + "TrayIconMessageWindowClass"_L1,
- qWindowsTrayIconWndProc);
+ const QString className = ctx->registerWindowClass(
+ "TrayIconMessageWindowClass"_L1,
+ qWindowsTrayIconWndProc);
const wchar_t windowName[] = L"QTrayIconMessageWindow";
return CreateWindowEx(0, reinterpret_cast<const wchar_t *>(className.utf16()),
windowName, WS_OVERLAPPED,
diff --git a/src/plugins/platforms/windows/qwindowstheme.cpp b/src/plugins/platforms/windows/qwindowstheme.cpp
index 33d7c4124ac..b9f60f7713c 100644
--- a/src/plugins/platforms/windows/qwindowstheme.cpp
+++ b/src/plugins/platforms/windows/qwindowstheme.cpp
@@ -549,7 +549,7 @@ QWindowsTheme::QWindowsTheme()
refreshIconPixmapSizes();
auto className = QWindowsWindowClassRegistry::instance()->registerWindowClass(
- QWindowsWindowClassRegistry::classNamePrefix() + QLatin1String("ThemeChangeObserverWindow"),
+ "ThemeChangeObserverWindow"_L1,
qThemeChangeObserverWndProc);
// HWND_MESSAGE windows do not get the required theme events,
// so we use a real top-level window that we never show.
diff --git a/src/plugins/platforms/windows/qwindowswindow.cpp b/src/plugins/platforms/windows/qwindowswindow.cpp
index c49ddbb3247..b77e985c965 100644
--- a/src/plugins/platforms/windows/qwindowswindow.cpp
+++ b/src/plugins/platforms/windows/qwindowswindow.cpp
@@ -23,6 +23,7 @@
#ifdef QT_NO_CURSOR
# include "qwindowscursor.h"
#endif
+#include "qwindowswindowclassdescription.h"
#include "qwindowswindowclassregistry.h"
#include <QtGui/qguiapplication.h>
@@ -60,6 +61,8 @@
QT_BEGIN_NAMESPACE
+using namespace Qt::StringLiterals;
+
using QWindowCreationContextPtr = QSharedPointer<QWindowCreationContext>;
enum {
@@ -886,7 +889,12 @@ QWindowsWindowData
const auto appinst = reinterpret_cast<HINSTANCE>(GetModuleHandle(nullptr));
const QString windowClassName = QWindowsWindowClassRegistry::instance()->registerWindowClass(w);
- const QString windowTitlebarName = QWindowsWindowClassRegistry::instance()->registerWindowClass(QStringLiteral("_q_titlebar"), DefWindowProc, CS_VREDRAW|CS_HREDRAW, nullptr, false);
+
+ QWindowsWindowClassDescription windowTitlebarDescription;
+ windowTitlebarDescription.name = "_q_titlebar"_L1;
+ windowTitlebarDescription.style = CS_VREDRAW | CS_HREDRAW;
+ windowTitlebarDescription.shouldAddPrefix = false;
+ const QString windowTitlebarName = QWindowsWindowClassRegistry::instance()->registerWindowClass(windowTitlebarDescription);
const QScreen *screen{};
const QRect rect = QPlatformWindow::initialGeometry(w, data.geometry,
diff --git a/src/plugins/platforms/windows/qwindowswindowclassdescription.cpp b/src/plugins/platforms/windows/qwindowswindowclassdescription.cpp
new file mode 100644
index 00000000000..63e16260b62
--- /dev/null
+++ b/src/plugins/platforms/windows/qwindowswindowclassdescription.cpp
@@ -0,0 +1,78 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include "qwindowswindowclassdescription.h"
+
+#include <QtGui/qwindow.h>
+
+#include "qwindowswindowclassregistry.h"
+
+QT_BEGIN_NAMESPACE
+
+using namespace Qt::StringLiterals;
+
+QWindowsWindowClassDescription QWindowsWindowClassDescription::fromName(QString name, WNDPROC procedure)
+{
+ return { std::move(name), procedure };
+}
+
+QWindowsWindowClassDescription QWindowsWindowClassDescription::fromWindow(const QWindow *window, WNDPROC procedure)
+{
+ Q_ASSERT(window);
+
+ QWindowsWindowClassDescription description;
+ description.procedure = procedure;
+
+ const Qt::WindowFlags flags = window->flags();
+ const Qt::WindowFlags type = flags & Qt::WindowType_Mask;
+ // Determine style and icon.
+ description.style = CS_DBLCLKS;
+ description.hasIcon = true;
+ // The following will not set CS_OWNDC for any widget window, even if it contains a
+ // QOpenGLWidget or QQuickWidget later on. That cannot be detected at this stage.
+ if (window->surfaceType() == QSurface::OpenGLSurface || (flags & Qt::MSWindowsOwnDC))
+ description.style |= CS_OWNDC;
+ if (!(flags & Qt::NoDropShadowWindowHint)
+ && (type == Qt::Popup || window->property("_q_windowsDropShadow").toBool())) {
+ description.style |= CS_DROPSHADOW;
+ }
+ switch (type) {
+ case Qt::Tool:
+ case Qt::ToolTip:
+ case Qt::Popup:
+ description.style |= CS_SAVEBITS; // Save/restore background
+ description.hasIcon = false;
+ break;
+ case Qt::Dialog:
+ if (!(flags & Qt::WindowSystemMenuHint))
+ description.hasIcon = false; // QTBUG-2027, dialogs without system menu.
+ break;
+ }
+ // Create a unique name for the flag combination
+ description.name = "QWindow"_L1;
+ switch (type) {
+ case Qt::Tool:
+ description.name += "Tool"_L1;
+ break;
+ case Qt::ToolTip:
+ description.name += "ToolTip"_L1;
+ break;
+ case Qt::Popup:
+ description.name += "Popup"_L1;
+ break;
+ default:
+ break;
+ }
+ if (description.style & CS_DROPSHADOW)
+ description.name += "DropShadow"_L1;
+ if (description.style & CS_SAVEBITS)
+ description.name += "SaveBits"_L1;
+ if (description.style & CS_OWNDC)
+ description.name += "OwnDC"_L1;
+ if (description.hasIcon)
+ description.name += "Icon"_L1;
+
+ return description;
+}
+
+QT_END_NAMESPACE
diff --git a/src/plugins/platforms/windows/qwindowswindowclassdescription.h b/src/plugins/platforms/windows/qwindowswindowclassdescription.h
new file mode 100644
index 00000000000..9423abf9d2d
--- /dev/null
+++ b/src/plugins/platforms/windows/qwindowswindowclassdescription.h
@@ -0,0 +1,30 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QWINDOWSWINDOWCLASSDESCRIPTION_H
+#define QWINDOWSWINDOWCLASSDESCRIPTION_H
+
+#include "qtwindowsglobal.h"
+
+#include <QtCore/qstring.h>
+
+QT_BEGIN_NAMESPACE
+
+class QWindow;
+
+struct QWindowsWindowClassDescription
+{
+ static QWindowsWindowClassDescription fromName(QString name, WNDPROC procedure);
+ static QWindowsWindowClassDescription fromWindow(const QWindow *window, WNDPROC procedure);
+
+ QString name;
+ WNDPROC procedure{ DefWindowProc };
+ unsigned int style{ 0 };
+ HBRUSH brush{ nullptr };
+ bool hasIcon{ false };
+ bool shouldAddPrefix{ true };
+};
+
+QT_END_NAMESPACE
+
+#endif // QWINDOWSWINDOWCLASSDESCRIPTION_H
diff --git a/src/plugins/platforms/windows/qwindowswindowclassregistry.cpp b/src/plugins/platforms/windows/qwindowswindowclassregistry.cpp
index 0d405dc419a..c330720a09c 100644
--- a/src/plugins/platforms/windows/qwindowswindowclassregistry.cpp
+++ b/src/plugins/platforms/windows/qwindowswindowclassregistry.cpp
@@ -6,9 +6,9 @@
#include <QtCore/qlibraryinfo.h>
#include <QtCore/quuid.h>
-#include <QtGui/qwindow.h>
#include "qwindowscontext.h"
+#include "qwindowswindowclassdescription.h"
QT_BEGIN_NAMESPACE
@@ -55,64 +55,13 @@ QString QWindowsWindowClassRegistry::classNamePrefix()
return result;
}
-QString QWindowsWindowClassRegistry::registerWindowClass(const QWindow *w)
+QString QWindowsWindowClassRegistry::registerWindowClass(const QWindowsWindowClassDescription &description)
{
- Q_ASSERT(w);
- const Qt::WindowFlags flags = w->flags();
- const Qt::WindowFlags type = flags & Qt::WindowType_Mask;
- // Determine style and icon.
- uint style = CS_DBLCLKS;
- bool icon = true;
- // The following will not set CS_OWNDC for any widget window, even if it contains a
- // QOpenGLWidget or QQuickWidget later on. That cannot be detected at this stage.
- if (w->surfaceType() == QSurface::OpenGLSurface || (flags & Qt::MSWindowsOwnDC))
- style |= CS_OWNDC;
- if (!(flags & Qt::NoDropShadowWindowHint)
- && (type == Qt::Popup || w->property("_q_windowsDropShadow").toBool())) {
- style |= CS_DROPSHADOW;
- }
- switch (type) {
- case Qt::Tool:
- case Qt::ToolTip:
- case Qt::Popup:
- style |= CS_SAVEBITS; // Save/restore background
- icon = false;
- break;
- case Qt::Dialog:
- if (!(flags & Qt::WindowSystemMenuHint))
- icon = false; // QTBUG-2027, dialogs without system menu.
- break;
- }
- // Create a unique name for the flag combination
- QString cname = classNamePrefix();
- cname += "QWindow"_L1;
- switch (type) {
- case Qt::Tool:
- cname += "Tool"_L1;
- break;
- case Qt::ToolTip:
- cname += "ToolTip"_L1;
- break;
- case Qt::Popup:
- cname += "Popup"_L1;
- break;
- default:
- break;
- }
- if (style & CS_DROPSHADOW)
- cname += "DropShadow"_L1;
- if (style & CS_SAVEBITS)
- cname += "SaveBits"_L1;
- if (style & CS_OWNDC)
- cname += "OwnDC"_L1;
- if (icon)
- cname += "Icon"_L1;
-
- return registerWindowClass(cname, m_proc, style, nullptr, icon);
-}
+ QString className = description.name;
+
+ if (description.shouldAddPrefix)
+ className = classNamePrefix() + className;
-QString QWindowsWindowClassRegistry::registerWindowClass(QString cname, WNDPROC proc, unsigned style, HBRUSH brush, bool icon)
-{
// since multiple Qt versions can be used in one process
// each one has to have window class names with a unique name
// The first instance gets the unmodified name; if the class
@@ -122,52 +71,63 @@ QString QWindowsWindowClassRegistry::registerWindowClass(QString cname, WNDPROC
// Note: GetClassInfo() returns != 0 when a class exists.
const auto appInstance = static_cast<HINSTANCE>(GetModuleHandle(nullptr));
WNDCLASS wcinfo;
- const bool classExists = GetClassInfo(appInstance, reinterpret_cast<LPCWSTR>(cname.utf16()), &wcinfo) != FALSE
- && wcinfo.lpfnWndProc != proc;
+ const bool classExists = GetClassInfo(appInstance, reinterpret_cast<LPCWSTR>(className.utf16()), &wcinfo) != FALSE
+ && wcinfo.lpfnWndProc != description.procedure;
if (classExists)
- cname += QUuid::createUuid().toString();
+ className += QUuid::createUuid().toString();
- if (m_registeredWindowClassNames.contains(cname)) // already registered in our list
- return cname;
+ if (m_registeredWindowClassNames.contains(className)) // already registered in our list
+ return className;
WNDCLASSEX wc;
wc.cbSize = sizeof(WNDCLASSEX);
- wc.style = style;
- wc.lpfnWndProc = proc;
+ wc.style = description.style;
+ wc.lpfnWndProc = description.procedure;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = appInstance;
wc.hCursor = nullptr;
- wc.hbrBackground = brush;
- if (icon) {
+ wc.hbrBackground = description.brush;
+ if (description.hasIcon) {
wc.hIcon = static_cast<HICON>(LoadImage(appInstance, L"IDI_ICON1", IMAGE_ICON, 0, 0, LR_DEFAULTSIZE));
if (wc.hIcon) {
int sw = GetSystemMetrics(SM_CXSMICON);
int sh = GetSystemMetrics(SM_CYSMICON);
wc.hIconSm = static_cast<HICON>(LoadImage(appInstance, L"IDI_ICON1", IMAGE_ICON, sw, sh, 0));
- } else {
+ }
+ else {
wc.hIcon = static_cast<HICON>(LoadImage(nullptr, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_DEFAULTSIZE | LR_SHARED));
wc.hIconSm = nullptr;
}
- } else {
+ }
+ else {
wc.hIcon = nullptr;
wc.hIconSm = nullptr;
}
wc.lpszMenuName = nullptr;
- wc.lpszClassName = reinterpret_cast<LPCWSTR>(cname.utf16());
+ wc.lpszClassName = reinterpret_cast<LPCWSTR>(className.utf16());
ATOM atom = RegisterClassEx(&wc);
- if (!atom) {
+ if (!atom)
qErrnoWarning("QApplication::regClass: Registering window class '%s' failed.",
- qPrintable(cname));
- }
+ qPrintable(className));
+
+ m_registeredWindowClassNames.insert(className);
+ qCDebug(lcQpaWindowClass).nospace() << __FUNCTION__ << ' ' << className
+ << " style=0x" << Qt::hex << description.style << Qt::dec
+ << " brush=" << description.brush << " icon=" << description.hasIcon << " atom=" << atom;
+ return className;
+}
- m_registeredWindowClassNames.insert(cname);
- qCDebug(lcQpaWindowClass).nospace() << __FUNCTION__ << ' ' << cname
- << " style=0x" << Qt::hex << style << Qt::dec
- << " brush=" << brush << " icon=" << icon << " atom=" << atom;
- return cname;
+QString QWindowsWindowClassRegistry::registerWindowClass(const QWindow *window)
+{
+ return registerWindowClass(QWindowsWindowClassDescription::fromWindow(window, m_proc));
+}
+
+QString QWindowsWindowClassRegistry::registerWindowClass(QString name, WNDPROC procedure)
+{
+ return registerWindowClass(QWindowsWindowClassDescription::fromName(name, procedure));
}
void QWindowsWindowClassRegistry::unregisterWindowClasses()
diff --git a/src/plugins/platforms/windows/qwindowswindowclassregistry.h b/src/plugins/platforms/windows/qwindowswindowclassregistry.h
index d599497abd0..c19b4f616fb 100644
--- a/src/plugins/platforms/windows/qwindowswindowclassregistry.h
+++ b/src/plugins/platforms/windows/qwindowswindowclassregistry.h
@@ -16,6 +16,7 @@ QT_BEGIN_NAMESPACE
Q_DECLARE_LOGGING_CATEGORY(lcQpaWindowClass)
class QWindow;
+struct QWindowsWindowClassDescription;
class QWindowsWindowClassRegistry
{
@@ -26,12 +27,13 @@ public:
static QWindowsWindowClassRegistry *instance();
- static QString classNamePrefix();
-
- QString registerWindowClass(const QWindow *w);
- QString registerWindowClass(QString cname, WNDPROC proc, unsigned style = 0, HBRUSH brush = nullptr, bool icon = false);
+ QString registerWindowClass(const QWindowsWindowClassDescription &description);
+ QString registerWindowClass(const QWindow *window);
+ QString registerWindowClass(QString name, WNDPROC procedure);
private:
+ static QString classNamePrefix();
+
void unregisterWindowClasses();
static QWindowsWindowClassRegistry *m_instance;
diff --git a/src/plugins/styles/modernwindows/qwindows11style.cpp b/src/plugins/styles/modernwindows/qwindows11style.cpp
index bf4b3c6a9bc..ffba4f775c8 100644
--- a/src/plugins/styles/modernwindows/qwindows11style.cpp
+++ b/src/plugins/styles/modernwindows/qwindows11style.cpp
@@ -24,6 +24,7 @@
#if QT_CONFIG(mdiarea)
#include <QtWidgets/qmdiarea.h>
#endif
+#include <QtWidgets/qplaintextedit.h>
#include <QtWidgets/qtextedit.h>
#include <QtWidgets/qtreeview.h>
#if QT_CONFIG(datetimeedit)
@@ -1027,7 +1028,9 @@ void QWindows11Style::drawPrimitive(PrimitiveElement element, const QStyleOption
if (frame->frameShape == QFrame::NoFrame)
break;
- drawLineEditFrame(painter, rect, option, qobject_cast<const QTextEdit *>(widget) != nullptr);
+ const bool isEditable = qobject_cast<const QTextEdit *>(widget) != nullptr
+ || qobject_cast<const QPlainTextEdit *>(widget) != nullptr;
+ drawLineEditFrame(painter, rect, option, isEditable);
}
break;
}
@@ -1452,36 +1455,10 @@ void QWindows11Style::drawControl(ControlElement element, const QStyleOption *op
#endif // QT_CONFIG(progressbar)
case CE_PushButtonLabel:
if (const QStyleOptionButton *btn = qstyleoption_cast<const QStyleOptionButton *>(option)) {
- using namespace StyleOptionHelper;
- const bool isEnabled = !isDisabled(option);
-
- QRect textRect = btn->rect.marginsRemoved(QMargins(contentHMargin, 0, contentHMargin, 0));
- int tf = Qt::AlignCenter | Qt::TextShowMnemonic;
- if (!proxy()->styleHint(SH_UnderlineShortcut, btn, widget))
- tf |= Qt::TextHideMnemonic;
-
- if (!btn->icon.isNull()) {
- //Center both icon and text
- QIcon::Mode mode = isEnabled ? QIcon::Normal : QIcon::Disabled;
- if (mode == QIcon::Normal && btn->state & State_HasFocus)
- mode = QIcon::Active;
- QIcon::State state = isChecked(btn) ? QIcon::On : QIcon::Off;
-
- int iconSpacing = 4;//### 4 is currently hardcoded in QPushButton::sizeHint()
-
- QRect iconRect = QRect(textRect.x(), textRect.y(), btn->iconSize.width(), textRect.height());
- QRect vIconRect = visualRect(btn->direction, btn->rect, iconRect);
- textRect.setLeft(textRect.left() + iconRect.width() + iconSpacing);
-
- if (isChecked(btn) || isPressed(btn))
- vIconRect.translate(proxy()->pixelMetric(PM_ButtonShiftHorizontal, option, widget),
- proxy()->pixelMetric(PM_ButtonShiftVertical, option, widget));
- btn->icon.paint(painter, vIconRect, Qt::AlignCenter, mode, state);
- }
-
- auto vTextRect = visualRect(btn->direction, btn->rect, textRect);
- painter->setPen(controlTextColor(option));
- proxy()->drawItemText(painter, vTextRect, tf, option->palette, isEnabled, btn->text);
+ QStyleOptionButton btnCopy(*btn);
+ btnCopy.rect = btn->rect.marginsRemoved(QMargins(contentHMargin, 0, contentHMargin, 0));
+ btnCopy.palette.setBrush(QPalette::ButtonText, controlTextColor(option));
+ QCommonStyle::drawControl(element, &btnCopy, painter, widget);
}
break;
case CE_PushButtonBevel:
@@ -2625,7 +2602,7 @@ QIcon QWindows11Style::standardIcon(StandardPixmap standardIcon,
switch (standardIcon) {
case SP_LineEditClearButton: {
if (d->m_lineEditClearButton.isNull()) {
- auto e = new WinFontIconEngine(Clear.at(0), d->assetFont);
+ auto e = new WinFontIconEngine(Clear, d->assetFont);
d->m_lineEditClearButton = QIcon(e);
}
return d->m_lineEditClearButton;
diff --git a/src/widgets/widgets/qcalendarwidget.h b/src/widgets/widgets/qcalendarwidget.h
index 642c7de7c80..49b1180f88f 100644
--- a/src/widgets/widgets/qcalendarwidget.h
+++ b/src/widgets/widgets/qcalendarwidget.h
@@ -20,7 +20,6 @@ class QCalendarWidgetPrivate;
class Q_WIDGETS_EXPORT QCalendarWidget : public QWidget
{
Q_OBJECT
- Q_ENUMS(Qt::DayOfWeek)
Q_PROPERTY(QDate selectedDate READ selectedDate WRITE setSelectedDate)
Q_PROPERTY(QDate minimumDate READ minimumDate WRITE setMinimumDate RESET clearMinimumDate)
Q_PROPERTY(QDate maximumDate READ maximumDate WRITE setMaximumDate RESET clearMaximumDate)
diff --git a/src/widgets/widgets/qtabbar.cpp b/src/widgets/widgets/qtabbar.cpp
index 8e6f497d7f5..0f0abb6e1d5 100644
--- a/src/widgets/widgets/qtabbar.cpp
+++ b/src/widgets/widgets/qtabbar.cpp
@@ -2326,6 +2326,15 @@ void QTabBarPrivate::moveTabFinished(int index)
void QTabBar::mouseReleaseEvent(QMouseEvent *event)
{
Q_D(QTabBar);
+
+ if (d->closeButtonOnTabs && event->button() == Qt::MiddleButton) {
+ const int index = tabAt(event->pos());
+ if (index != -1) {
+ emit tabCloseRequested(index);
+ return;
+ }
+ }
+
if (event->button() != Qt::LeftButton) {
event->ignore();
return;
@@ -2559,8 +2568,9 @@ void QTabBar::setUsesScrollButtons(bool useButtons)
\since 4.5
When tabsClosable is set to true a close button will appear on the tab on
- either the left or right hand side depending upon the style. When the button
- is clicked the tab the signal tabCloseRequested will be emitted.
+ either the left or right hand side depending upon the style. When the button
+ is clicked directly, or a mouse middle-click is received anywhere in the tab,
+ the signal tabCloseRequested will be emitted.
By default the value is false.
diff --git a/src/widgets/widgets/qtoolbutton.h b/src/widgets/widgets/qtoolbutton.h
index 1f6a20cea3e..96f2ed5ba22 100644
--- a/src/widgets/widgets/qtoolbutton.h
+++ b/src/widgets/widgets/qtoolbutton.h
@@ -20,7 +20,6 @@ class QStyleOptionToolButton;
class Q_WIDGETS_EXPORT QToolButton : public QAbstractButton
{
Q_OBJECT
- Q_ENUMS(Qt::ToolButtonStyle Qt::ArrowType)
#if QT_CONFIG(menu)
Q_PROPERTY(ToolButtonPopupMode popupMode READ popupMode WRITE setPopupMode)
#endif
diff --git a/src/widgets/widgets/qwidgetanimator.cpp b/src/widgets/widgets/qwidgetanimator.cpp
index 99a051357ee..1216f535b8b 100644
--- a/src/widgets/widgets/qwidgetanimator.cpp
+++ b/src/widgets/widgets/qwidgetanimator.cpp
@@ -53,7 +53,7 @@ void QWidgetAnimator::animate(QWidget *widget, const QRect &_final_geometry, boo
//If the QStyle has animations, animate
if (const int animationDuration = widget->style()->styleHint(QStyle::SH_Widget_Animation_Duration, nullptr, widget)) {
AnimationMap::const_iterator it = m_animation_map.constFind(widget);
- if (it != m_animation_map.constEnd() && (*it)->endValue().toRect() == final_geometry)
+ if (it != m_animation_map.constEnd() && *it && (*it)->endValue().toRect() == final_geometry)
return;
QPropertyAnimation *anim = new QPropertyAnimation(widget, "geometry", widget);
@@ -76,7 +76,8 @@ void QWidgetAnimator::animate(QWidget *widget, const QRect &_final_geometry, boo
bool QWidgetAnimator::animating() const
{
- return !m_animation_map.isEmpty();
+ auto isActiveAnimation = [](const QPointer<QPropertyAnimation> &p) { return !p.isNull(); };
+ return !std::all_of(m_animation_map.begin(), m_animation_map.end(), isActiveAnimation);
}
QT_END_NAMESPACE
diff --git a/tests/auto/corelib/kernel/qmetaobjectbuilder/tst_qmetaobjectbuilder.cpp b/tests/auto/corelib/kernel/qmetaobjectbuilder/tst_qmetaobjectbuilder.cpp
index f6d08c8107f..67643606fa3 100644
--- a/tests/auto/corelib/kernel/qmetaobjectbuilder/tst_qmetaobjectbuilder.cpp
+++ b/tests/auto/corelib/kernel/qmetaobjectbuilder/tst_qmetaobjectbuilder.cpp
@@ -77,10 +77,6 @@ class SomethingOfEverything : public QObject
Q_PROPERTY(SomethingEnum eprop READ eprop)
Q_PROPERTY(SomethingFlagEnum fprop READ fprop)
Q_PROPERTY(QLocale::Language language READ language)
- Q_ENUMS(SomethingEnum)
- Q_FLAGS(SomethingFlag)
- Q_ENUMS(SomethingEnum64)
- Q_FLAGS(SomethingFlag64)
public:
Q_INVOKABLE SomethingOfEverything() {}
~SomethingOfEverything() {}
@@ -105,6 +101,7 @@ public:
UVW = 8
};
Q_DECLARE_FLAGS(SomethingFlag, SomethingFlagEnum)
+ Q_FLAG(SomethingFlag)
enum SomethingFlagEnum64 : quint64
{
@@ -112,6 +109,7 @@ public:
OPQ = Q_UINT64_C(1) << 63,
};
Q_DECLARE_FLAGS(SomethingFlag64, SomethingFlagEnum64)
+ Q_FLAG(SomethingFlag64)
Q_INVOKABLE Q_SCRIPTABLE void method1() const {}
diff --git a/tests/auto/corelib/kernel/qvariant/tst_qvariant.cpp b/tests/auto/corelib/kernel/qvariant/tst_qvariant.cpp
index 9be046c75be..b05a055252b 100644
--- a/tests/auto/corelib/kernel/qvariant/tst_qvariant.cpp
+++ b/tests/auto/corelib/kernel/qvariant/tst_qvariant.cpp
@@ -397,6 +397,7 @@ private slots:
void iterateAssociativeContainerElements_data();
void iterateAssociativeContainerElements() { runTestFunction(); }
void iterateContainerElements();
+ void emptyContainerInterface();
void pairElements_data();
void pairElements() { runTestFunction(); }
@@ -5324,6 +5325,26 @@ void tst_QVariant::iterateContainerElements()
}
}
+void tst_QVariant::emptyContainerInterface()
+{
+ // An empty container interface should implicitly be of invalid size
+ // and its begin and end iterators should be equal.
+
+ const QtMetaContainerPrivate::QMetaContainerInterface emptyContainerInterface {};
+ QIterable emptyIterable(QMetaContainer(&emptyContainerInterface), nullptr);
+
+ QCOMPARE(emptyIterable.size(), -1);
+ auto constBegin = emptyIterable.constBegin();
+ auto constEnd = emptyIterable.constEnd();
+ QVERIFY(constBegin == constEnd);
+ QCOMPARE(constEnd - constBegin, 0);
+
+ auto mutableBegin = emptyIterable.mutableBegin();
+ auto mutableEnd = emptyIterable.mutableEnd();
+ QVERIFY(mutableBegin == mutableEnd);
+ QCOMPARE(mutableEnd - mutableBegin, 0);
+}
+
template <typename Pair> static void testVariantPairElements()
{
QFETCH(std::function<void(void *)>, makeValue);
diff --git a/tests/auto/corelib/platform/android/tst_android.cpp b/tests/auto/corelib/platform/android/tst_android.cpp
index 3665f100a61..b4bb0323f8a 100644
--- a/tests/auto/corelib/platform/android/tst_android.cpp
+++ b/tests/auto/corelib/platform/android/tst_android.cpp
@@ -430,6 +430,8 @@ void tst_Android::testFullScreenDimensions()
widget.showNormal();
}
+ // TODO needs fix to work in local and CI on same fashion
+ const bool runsOnCI = qgetenv("QTEST_ENVIRONMENT").split(' ').contains("ci");
{
// Translucent
// available geometry == full display size (system bars visible but drawable under)
@@ -437,14 +439,11 @@ void tst_Android::testFullScreenDimensions()
widget.show();
QCoreApplication::processEvents();
QTRY_COMPARE(screen->availableGeometry().width(), realSize.getField<jint>("x"));
- QTRY_COMPARE(screen->availableGeometry().height(), realSize.getField<jint>("y"));
-
- QTRY_COMPARE(screen->geometry().width(), realSize.getField<jint>("x"));
- // TODO needs fix to work in local and CI on same fashion
- const bool runsOnCI = qgetenv("QTEST_ENVIRONMENT").split(' ').contains("ci");
if ((sdkVersion > __ANDROID_API_V__) && runsOnCI)
QEXPECT_FAIL("", "Fails on Android 16 (QTBUG-141712).", Continue);
+ QTRY_COMPARE(screen->availableGeometry().height(), realSize.getField<jint>("y"));
+ QTRY_COMPARE(screen->geometry().width(), realSize.getField<jint>("x"));
QTRY_COMPARE(screen->geometry().height(), realSize.getField<jint>("y"));
widget.showNormal();
}
@@ -455,6 +454,8 @@ void tst_Android::testFullScreenDimensions()
widget.showMaximized();
QCoreApplication::processEvents();
QTRY_COMPARE(screen->availableGeometry().width(), realSize.getField<jint>("x"));
+ if ((sdkVersion > __ANDROID_API_V__) && runsOnCI)
+ QEXPECT_FAIL("", "Fails on Android 16 (QTBUG-141712).", Continue);
QTRY_COMPARE(screen->availableGeometry().height(), realSize.getField<jint>("y"));
QTRY_COMPARE(screen->geometry().width(), realSize.getField<jint>("x"));
diff --git a/tests/auto/gui/image/qimagereader/images/image16.pgm b/tests/auto/gui/image/qimagereader/images/image16.pgm
new file mode 100644
index 00000000000..4e0b55131b0
--- /dev/null
+++ b/tests/auto/gui/image/qimagereader/images/image16.pgm
@@ -0,0 +1,260 @@
+P2
+16
+16
+65535
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+65535
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
+32767
diff --git a/tests/auto/gui/image/qimagereader/tst_qimagereader.cpp b/tests/auto/gui/image/qimagereader/tst_qimagereader.cpp
index de9fea78ea6..8ccaf435f0b 100644
--- a/tests/auto/gui/image/qimagereader/tst_qimagereader.cpp
+++ b/tests/auto/gui/image/qimagereader/tst_qimagereader.cpp
@@ -610,6 +610,7 @@ void tst_QImageReader::imageFormat_data()
QTest::newRow("pbm") << QString("image.pbm") << QByteArray("pbm") << QImage::Format_Mono;
QTest::newRow("pgm") << QString("image.pgm") << QByteArray("pgm") << QImage::Format_Grayscale8;
+ QTest::newRow("pgm") << QString("image16.pgm") << QByteArray("pgm") << QImage::Format_Grayscale16;
QTest::newRow("ppm-1") << QString("image.ppm") << QByteArray("ppm") << QImage::Format_RGB32;
QTest::newRow("ppm-2") << QString("teapot.ppm") << QByteArray("ppm") << QImage::Format_RGB32;
QTest::newRow("ppm-3") << QString("runners.ppm") << QByteArray("ppm") << QImage::Format_RGB32;
diff --git a/tests/auto/gui/kernel/qguieventdispatcher/BLACKLIST b/tests/auto/gui/kernel/qguieventdispatcher/BLACKLIST
new file mode 100644
index 00000000000..a8f73d73f4d
--- /dev/null
+++ b/tests/auto/gui/kernel/qguieventdispatcher/BLACKLIST
@@ -0,0 +1,2 @@
+[postEventFromThread]
+macos-26 developer-build # QTBUG-142185
diff --git a/tests/auto/gui/kernel/qwindow/BLACKLIST b/tests/auto/gui/kernel/qwindow/BLACKLIST
index 1ef54f0bfbf..55003c7ec18 100644
--- a/tests/auto/gui/kernel/qwindow/BLACKLIST
+++ b/tests/auto/gui/kernel/qwindow/BLACKLIST
@@ -25,5 +25,6 @@ android
windows-10
windows-11
android
+macos-26 # QTBUG-142157
[stateChangeSignal]
macos # QTBUG-140388
diff --git a/tests/auto/network/access/qhttp2connection/tst_qhttp2connection.cpp b/tests/auto/network/access/qhttp2connection/tst_qhttp2connection.cpp
index 8e8c90e14de..417655c31d9 100644
--- a/tests/auto/network/access/qhttp2connection/tst_qhttp2connection.cpp
+++ b/tests/auto/network/access/qhttp2connection/tst_qhttp2connection.cpp
@@ -8,6 +8,8 @@
#include <QtNetwork/private/hpack_p.h>
#include <QtNetwork/private/bitstreams_p.h>
+#include <QtCore/qregularexpression.h>
+
#include <limits>
using namespace Qt::StringLiterals;
@@ -35,6 +37,8 @@ private slots:
void connectToServer();
void WINDOW_UPDATE();
void testCONTINUATIONFrame();
+ void goaway_data();
+ void goaway();
private:
enum PeerType { Client, Server };
@@ -1051,6 +1055,112 @@ void tst_QHttp2Connection::testCONTINUATIONFrame()
}
}
+void tst_QHttp2Connection::goaway_data()
+{
+ QTest::addColumn<bool>("endStreamOnHEADERS");
+ QTest::addColumn<bool>("createNewStreamAfterDelay");
+ QTest::addRow("end-on-headers") << true << false;
+ QTest::addRow("end-after-data") << false << false;
+ QTest::addRow("end-after-new-late-stream") << false << true;
+}
+
+void tst_QHttp2Connection::goaway()
+{
+ QFETCH(const bool, endStreamOnHEADERS);
+ QFETCH(const bool, createNewStreamAfterDelay);
+ auto [client, server] = makeFakeConnectedSockets();
+ auto connection = makeHttp2Connection(client.get(), {}, Client);
+ auto serverConnection = makeHttp2Connection(server.get(), {}, Server);
+
+ QHttp2Stream *clientStream = connection->createStream().unwrap();
+ QVERIFY(clientStream);
+ QVERIFY(waitForSettingsExchange(connection, serverConnection));
+
+ QSignalSpy newIncomingStreamSpy{ serverConnection, &QHttp2Connection::newIncomingStream };
+
+ QSignalSpy clientIncomingStreamSpy{ connection, &QHttp2Connection::newIncomingStream };
+ QSignalSpy clientHeaderReceivedSpy{ clientStream, &QHttp2Stream::headersReceived };
+ QSignalSpy clientGoawaySpy{ connection, &QHttp2Connection::receivedGOAWAY };
+
+ const HPack::HttpHeader headers = getRequiredHeaders();
+ clientStream->sendHEADERS(headers, false);
+
+ QVERIFY(newIncomingStreamSpy.wait());
+ auto *serverStream = newIncomingStreamSpy.front().front().value<QHttp2Stream *>();
+ QVERIFY(serverStream);
+ QVERIFY(serverConnection->sendGOAWAY(Http2::CANCEL));
+ auto createStreamResult = serverConnection->createLocalStreamInternal();
+ QVERIFY(createStreamResult.has_error());
+ QCOMPARE(createStreamResult.error(), QHttp2Connection::CreateStreamError::ReceivedGOAWAY);
+
+ QVERIFY(clientGoawaySpy.wait());
+ QCOMPARE(clientGoawaySpy.size(), 1);
+ // The error code used:
+ QCOMPARE(clientGoawaySpy.first().first().value<Http2::Http2Error>(), Http2::CANCEL);
+ // Last ID that will be processed
+ QCOMPARE(clientGoawaySpy.first().last().value<quint32>(), clientStream->streamID());
+ clientGoawaySpy.clear();
+
+ // Test that creating a stream the normal way results in an error:
+ QH2Expected<QHttp2Stream *, QHttp2Connection::CreateStreamError>
+ invalidStream = connection->createStream();
+ QVERIFY(!invalidStream.ok());
+ QVERIFY(invalidStream.has_error());
+ QCOMPARE(invalidStream.error(), QHttp2Connection::CreateStreamError::ReceivedGOAWAY);
+
+ // Directly create a stream to avoid the GOAWAY check:
+ quint32 nextStreamId = clientStream->streamID() + 2;
+ QHttp2Stream *secondClientStream = connection->createStreamInternal_impl(nextStreamId);
+ QSignalSpy streamResetSpy{ secondClientStream, &QHttp2Stream::rstFrameReceived };
+ secondClientStream->sendHEADERS(headers, endStreamOnHEADERS);
+ // The stream should be ignored:
+ using namespace std::chrono_literals;
+ QVERIFY(!streamResetSpy.wait(100ms)); // We don't get reset because we are ignored
+ if (endStreamOnHEADERS)
+ return;
+
+ secondClientStream->sendDATA("my data", createNewStreamAfterDelay);
+ // We cheat and try to send data after the END_STREAM flag has been sent
+ if (!createNewStreamAfterDelay) {
+ // Manually send a frame with END_STREAM so the QHttp2Stream thinks it's fine to send more
+ // DATA
+ connection->frameWriter.start(Http2::FrameType::DATA, Http2::FrameFlag::END_STREAM,
+ secondClientStream->streamID());
+ connection->frameWriter.write(*connection->getSocket());
+ QVERIFY(!streamResetSpy.wait(100ms)); // We don't get reset because we are ignored
+
+ // Even without the GOAWAY this should fail (more activity after END_STREAM)
+ secondClientStream->sendDATA("my data", true);
+ QTest::ignoreMessage(QtCriticalMsg,
+ QRegularExpression(u".*Connection error: DATA on invalid stream.*"_s));
+ QVERIFY(clientGoawaySpy.wait());
+ QCOMPARE(clientGoawaySpy.size(), 1);
+ QCOMPARE(clientGoawaySpy.first().first().value<Http2::Http2Error>(),
+ Http2::ENHANCE_YOUR_CALM);
+ QCOMPARE(clientGoawaySpy.first().last().value<quint32>(), clientStream->streamID());
+ return; // connection is dead by now
+ }
+
+ // Override the deadline timer so we don't have to wait too long
+ serverConnection->m_goawayGraceTimer.setRemainingTime(50ms);
+
+ // We can create the stream whenever, it is not noticed by the server until we send something.
+ nextStreamId += 2;
+ QHttp2Stream *rejectedStream = connection->createStreamInternal_impl(nextStreamId);
+ // Sleep until the grace period is over:
+ QTRY_VERIFY(serverConnection->m_goawayGraceTimer.hasExpired());
+
+ QVERIFY(rejectedStream->sendHEADERS(headers, true));
+
+ QTest::ignoreMessage(QtCriticalMsg,
+ QRegularExpression(u".*Connection error: Peer refused to GOAWAY\\..*"_s));
+ QVERIFY(clientGoawaySpy.wait());
+ QCOMPARE(clientGoawaySpy.size(), 1);
+ QCOMPARE(clientGoawaySpy.first().first().value<Http2::Http2Error>(), Http2::PROTOCOL_ERROR);
+ // The first stream is still the last processed one:
+ QCOMPARE(clientGoawaySpy.first().last().value<quint32>(), clientStream->streamID());
+}
+
QTEST_MAIN(tst_QHttp2Connection)
#include "tst_qhttp2connection.moc"
diff --git a/tests/auto/tools/moc/allmocs_baseline_in.json b/tests/auto/tools/moc/allmocs_baseline_in.json
index 8f7757a7272..d8e6c4df538 100644
--- a/tests/auto/tools/moc/allmocs_baseline_in.json
+++ b/tests/auto/tools/moc/allmocs_baseline_in.json
@@ -2444,7 +2444,7 @@
{
"isClass": false,
"isFlag": false,
- "lineNumber": 14,
+ "lineNumber": 13,
"name": "SomeEnum",
"values": [
"SomeEnumValue"
@@ -2501,7 +2501,7 @@
{
"isClass": false,
"isFlag": false,
- "lineNumber": 14,
+ "lineNumber": 13,
"name": "SomeEnum",
"values": [
"SomeEnumValue"
diff --git a/tests/auto/tools/moc/qtbug-35657-gadget.h b/tests/auto/tools/moc/qtbug-35657-gadget.h
index d97e1f7f45e..ca225faca1c 100644
--- a/tests/auto/tools/moc/qtbug-35657-gadget.h
+++ b/tests/auto/tools/moc/qtbug-35657-gadget.h
@@ -9,9 +9,9 @@
namespace QTBUG_35657 {
class A {
Q_GADGET
- Q_ENUMS(SomeEnum)
public:
enum SomeEnum { SomeEnumValue = 0 };
+ Q_ENUM(SomeEnum)
};
}
diff --git a/tests/auto/tools/moc/related-metaobjects-in-namespaces.h b/tests/auto/tools/moc/related-metaobjects-in-namespaces.h
index efd82107673..2513094ed0c 100644
--- a/tests/auto/tools/moc/related-metaobjects-in-namespaces.h
+++ b/tests/auto/tools/moc/related-metaobjects-in-namespaces.h
@@ -9,9 +9,9 @@
namespace QTBUG_2151 {
class A : public QObject {
Q_OBJECT
- Q_ENUMS(SomeEnum)
public:
enum SomeEnum { SomeEnumValue = 0 };
+ Q_ENUM(SomeEnum)
};
class B : public QObject
diff --git a/tests/auto/tools/moc/related-metaobjects-name-conflict.h b/tests/auto/tools/moc/related-metaobjects-name-conflict.h
index cccd97e4e74..d88826f696a 100644
--- a/tests/auto/tools/moc/related-metaobjects-name-conflict.h
+++ b/tests/auto/tools/moc/related-metaobjects-name-conflict.h
@@ -9,15 +9,15 @@
#define DECLARE_GADGET_AND_OBJECT_CLASSES \
class Gadget { \
Q_GADGET \
- Q_ENUMS(SomeEnum) \
public: \
enum SomeEnum { SomeEnumValue = 0 }; \
+ Q_ENUM(SomeEnum) \
}; \
class Object : public QObject{ \
Q_OBJECT \
- Q_ENUMS(SomeEnum) \
public: \
enum SomeEnum { SomeEnumValue = 0 }; \
+ Q_ENUM(SomeEnum) \
};
#define DECLARE_DEPENDING_CLASSES \
diff --git a/tests/auto/widgets/kernel/qwidget/BLACKLIST b/tests/auto/widgets/kernel/qwidget/BLACKLIST
index dd2cb1dcee9..9651c1480c8 100644
--- a/tests/auto/widgets/kernel/qwidget/BLACKLIST
+++ b/tests/auto/widgets/kernel/qwidget/BLACKLIST
@@ -41,6 +41,9 @@ android
android
[hoverPosition]
macos-14 x86
+macos-26 # QTBUG-142157
# QTBUG-124291
[setParentChangesFocus:make dialog parentless, after]
android
+[enterLeaveOnWindowShowHide]
+macos-26 # QTBUG-142157
diff --git a/tests/auto/widgets/widgets/qtabbar/tst_qtabbar.cpp b/tests/auto/widgets/widgets/qtabbar/tst_qtabbar.cpp
index 03131cebe47..16a69e4337d 100644
--- a/tests/auto/widgets/widgets/qtabbar/tst_qtabbar.cpp
+++ b/tests/auto/widgets/widgets/qtabbar/tst_qtabbar.cpp
@@ -63,6 +63,7 @@ private slots:
void removeLastVisibleTab();
void closeButton();
+ void requestCloseOnMiddleClick();
void tabButton_data();
void tabButton();
@@ -551,6 +552,28 @@ void tst_QTabBar::closeButton()
QCOMPARE(spy.size(), 1);
}
+void tst_QTabBar::requestCloseOnMiddleClick()
+{
+ QTabBar tabbar;
+ tabbar.addTab("foo");
+ tabbar.addTab("bar");
+ QCOMPARE(tabbar.count(), 2);
+
+ QSignalSpy spy(&tabbar, SIGNAL(tabCloseRequested(int)));
+
+ QCOMPARE(tabbar.tabsClosable(), false);
+ QTest::mouseClick(&tabbar, Qt::MiddleButton, {}, tabbar.tabRect(0).center());
+ QCOMPARE(spy.size(), 0);
+
+ tabbar.setTabsClosable(true);
+ QCOMPARE(tabbar.tabsClosable(), true);
+ QTest::mouseClick(&tabbar, Qt::MiddleButton, {}, tabbar.tabRect(0).center());
+ QCOMPARE(spy.size(), 1);
+
+ QTest::mouseClick(&tabbar, Qt::MiddleButton, {}, tabbar.rect().bottomRight() * 1.1);
+ QCOMPARE(spy.size(), 1);
+}
+
Q_DECLARE_METATYPE(QTabBar::ButtonPosition)
void tst_QTabBar::tabButton_data()
{
@@ -1512,7 +1535,7 @@ void tst_QTabBar::checkPositionsAfterShapeChange()
using QTabWidget::QTabWidget;
using QTabWidget::setTabBar;
};
-
+
class TabBar : public QTabBar
{
public:
diff --git a/tests/manual/sandboxed_file_access/CMakeLists.txt b/tests/manual/sandboxed_file_access/CMakeLists.txt
new file mode 100644
index 00000000000..8df09401cf9
--- /dev/null
+++ b/tests/manual/sandboxed_file_access/CMakeLists.txt
@@ -0,0 +1,71 @@
+# Copyright (C) 2025 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+if(NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT)
+ cmake_minimum_required(VERSION 3.16)
+ project(tst_manual_sandboxed_file_access LANGUAGES CXX)
+ find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST)
+endif()
+
+qt_standard_project_setup()
+
+qt_add_executable(tst_manual_sandboxed_file_access
+ tst_sandboxed_file_access.cpp
+)
+
+target_link_libraries(tst_manual_sandboxed_file_access PRIVATE
+ Qt::CorePrivate
+ Qt::Widgets
+ Qt::Test
+)
+
+enable_language(OBJCXX)
+set_source_files_properties(tst_sandboxed_file_access.cpp PROPERTIES LANGUAGE OBJCXX)
+
+if(MACOS)
+ target_sources(tst_manual_sandboxed_file_access PRIVATE app.entitlements)
+ set_target_properties(tst_manual_sandboxed_file_access PROPERTIES
+ MACOSX_BUNDLE TRUE
+ MACOSX_BUNDLE_GUI_IDENTIFIER "io.qt.dev.tst-manual-sandboxed-file-access"
+ XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS "${CMAKE_CURRENT_SOURCE_DIR}/app.entitlements"
+ XCODE_ATTRIBUTE_COPY_PHASE_STRIP FALSE
+ )
+ if(NOT CMAKE_GENERATOR STREQUAL "Xcode")
+ set_target_properties(tst_manual_sandboxed_file_access PROPERTIES
+ RESOURCE "${CMAKE_CURRENT_SOURCE_DIR}/app.entitlements"
+ )
+ endif()
+
+ set(platform_plugin "${QT6_INSTALL_PREFIX}/${QT6_INSTALL_PLUGINS}/platforms/libqcocoa.dylib")
+ target_sources(tst_manual_sandboxed_file_access PRIVATE ${platform_plugin})
+ set_source_files_properties(${platform_plugin}
+ PROPERTIES
+ MACOSX_PACKAGE_LOCATION PlugIns/platforms
+ )
+
+ target_compile_definitions(tst_manual_sandboxed_file_access PRIVATE
+ QTEST_THROW_ON_FAIL
+ QTEST_THROW_ON_SKIP
+ )
+endif()
+
+if(IOS)
+ set(plist_path "${CMAKE_CURRENT_SOURCE_DIR}/Info.plist.ios")
+else()
+ set(plist_path "${CMAKE_CURRENT_SOURCE_DIR}/Info.plist.macos")
+endif()
+
+set_target_properties(tst_manual_sandboxed_file_access
+ PROPERTIES MACOSX_BUNDLE_INFO_PLIST "${plist_path}")
+
+install(TARGETS tst_manual_sandboxed_file_access
+ BUNDLE DESTINATION .
+ RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
+ LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
+)
+
+qt_generate_deploy_app_script(
+ TARGET tst_manual_sandboxed_file_access
+ OUTPUT_SCRIPT deploy_script
+ NO_UNSUPPORTED_PLATFORM_ERROR
+)
+install(SCRIPT ${deploy_script})
diff --git a/tests/manual/sandboxed_file_access/Info.plist.ios b/tests/manual/sandboxed_file_access/Info.plist.ios
new file mode 100644
index 00000000000..c6072cffa92
--- /dev/null
+++ b/tests/manual/sandboxed_file_access/Info.plist.ios
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>BuildMachineOSBuild</key>
+ <string>25B78</string>
+ <key>CFBundleAllowMixedLocalizations</key>
+ <true/>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleDisplayName</key>
+ <string>tst_manual_sandboxed_file_access</string>
+ <key>CFBundleExecutable</key>
+ <string>tst_manual_sandboxed_file_access</string>
+ <key>CFBundleIdentifier</key>
+ <string>io.qt.fb.tst-manual-sandboxed-file-access</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>tst_manual_sandboxed_file_access</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>6.0</string>
+ <key>CFBundleSupportedPlatforms</key>
+ <array>
+ <string>iPhoneOS</string>
+ </array>
+ <key>CFBundleVersion</key>
+ <string>6.0.0</string>
+ <key>DTCompiler</key>
+ <string>com.apple.compilers.llvm.clang.1_0</string>
+ <key>DTPlatformBuild</key>
+ <string>23B77</string>
+ <key>DTPlatformName</key>
+ <string>iphoneos</string>
+ <key>DTPlatformVersion</key>
+ <string>26.1</string>
+ <key>DTSDKBuild</key>
+ <string>23B77</string>
+ <key>DTSDKName</key>
+ <string>iphoneos26.1</string>
+ <key>DTXcode</key>
+ <string>2610</string>
+ <key>DTXcodeBuild</key>
+ <string>17B55</string>
+ <key>LSRequiresIPhoneOS</key>
+ <true/>
+ <key>MinimumOSVersion</key>
+ <string>17</string>
+ <key>NOTE</key>
+ <string>This file was generated by Qt's default CMake support.</string>
+ <key>UIDeviceFamily</key>
+ <array>
+ <integer>1</integer>
+ <integer>2</integer>
+ </array>
+ <key>UILaunchStoryboardName</key>
+ <string>LaunchScreen</string>
+ <key>UIRequiredDeviceCapabilities</key>
+ <array>
+ <string>arm64</string>
+ </array>
+ <key>UISupportedInterfaceOrientations</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationPortraitUpsideDown</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+ <key>CFBundleDocumentTypes</key>
+ <array>
+ <dict>
+ <key>CFBundleTypeRole</key>
+ <string>Editor</string>
+ <key>LSItemContentTypes</key>
+ <array>
+ <string>public.text</string>
+ </array>
+ <!-- These two don't seem to be needed to make things work -->
+ <key>LSHandlerRank</key>
+ <string>Default</string>
+ <key>CFBundleTypeName</key>
+ <string>Text files</string>
+ </dict>
+ </array>
+ <key>UISupportsDocumentBrowser</key>
+ <true/>
+</dict>
+</plist>
diff --git a/tests/manual/sandboxed_file_access/Info.plist.macos b/tests/manual/sandboxed_file_access/Info.plist.macos
new file mode 100644
index 00000000000..81a93f0353f
--- /dev/null
+++ b/tests/manual/sandboxed_file_access/Info.plist.macos
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>BuildMachineOSBuild</key>
+ <string>25B78</string>
+ <key>CFBundleAllowMixedLocalizations</key>
+ <true/>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleExecutable</key>
+ <string>tst_manual_sandboxed_file_access</string>
+ <key>CFBundleIdentifier</key>
+ <string>io.qt.dev.tst-manual-sandboxed-file-access</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>tst_manual_sandboxed_file_access</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>6.0</string>
+ <key>CFBundleSupportedPlatforms</key>
+ <array>
+ <string>MacOSX</string>
+ </array>
+ <key>CFBundleVersion</key>
+ <string>6.0.0</string>
+ <key>DTCompiler</key>
+ <string>com.apple.compilers.llvm.clang.1_0</string>
+ <key>DTPlatformBuild</key>
+ <string>25B74</string>
+ <key>DTPlatformName</key>
+ <string>macosx</string>
+ <key>DTPlatformVersion</key>
+ <string>26.1</string>
+ <key>DTSDKBuild</key>
+ <string>25B74</string>
+ <key>DTSDKName</key>
+ <string>macosx26.1</string>
+ <key>DTXcode</key>
+ <string>2610</string>
+ <key>DTXcodeBuild</key>
+ <string>17B55</string>
+ <key>LSMinimumSystemVersion</key>
+ <string>13</string>
+ <key>NSPrincipalClass</key>
+ <string>NSApplication</string>
+ <key>NSSupportsAutomaticGraphicsSwitching</key>
+ <true/>
+ <key>CFBundleDocumentTypes</key>
+ <array>
+ <dict>
+ <key>CFBundleTypeRole</key>
+ <string>Editor</string>
+ <key>LSItemContentTypes</key>
+ <array>
+ <string>public.text</string>
+ </array>
+ <!-- These two don't seem to be needed to make things work -->
+ <key>LSHandlerRank</key>
+ <string>Default</string>
+ <key>CFBundleTypeName</key>
+ <string>Text files</string>
+ </dict>
+ </array>
+</dict>
+</plist>
diff --git a/tests/manual/sandboxed_file_access/app.entitlements b/tests/manual/sandboxed_file_access/app.entitlements
new file mode 100644
index 00000000000..6d968edb4f8
--- /dev/null
+++ b/tests/manual/sandboxed_file_access/app.entitlements
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>com.apple.security.app-sandbox</key>
+ <true/>
+ <key>com.apple.security.files.user-selected.read-write</key>
+ <true/>
+</dict>
+</plist>
diff --git a/tests/manual/sandboxed_file_access/tst_sandboxed_file_access.cpp b/tests/manual/sandboxed_file_access/tst_sandboxed_file_access.cpp
new file mode 100644
index 00000000000..18381ce0c8c
--- /dev/null
+++ b/tests/manual/sandboxed_file_access/tst_sandboxed_file_access.cpp
@@ -0,0 +1,422 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#include <QtCore>
+#include <QtWidgets>
+#include <QtTest>
+
+#include <Foundation/Foundation.h>
+
+#if defined(Q_OS_MACOS) && defined(QT_BUILD_INTERNAL)
+#include <private/qcore_mac_p.h>
+Q_CONSTRUCTOR_FUNCTION(qt_mac_ensureResponsible);
+#endif
+
+class tst_SandboxedFileAccess : public QObject
+{
+ Q_OBJECT
+
+private slots:
+ void initTestCase();
+ void cleanupTestCase();
+
+ void alwaysAccessibleLocations();
+
+ void standardPaths_data();
+ void standardPaths();
+
+ void readSingleFile();
+ void writeSingleFile();
+ void writeSingleFileNonCanonical();
+
+ void removeFile();
+ void trashFile();
+
+ void readFileAfterRestart();
+
+ void directoryAccess();
+
+ void securityScopedTargetFile();
+
+ void fileOpenEvent();
+
+private:
+ void writeFile(const QString &fileName);
+ QByteArray readFile(const QString &fileName);
+
+ QString getFileName(QFileDialog::AcceptMode, QFileDialog::FileMode,
+ const QString &action = QString(), const QString &fileName = QString());
+
+ QString sandboxPath() const
+ {
+ return QStandardPaths::standardLocations(QStandardPaths::HomeLocation).first();
+ }
+
+ QString bundlePath() const
+ {
+ QString path = QCoreApplication::applicationDirPath();
+#if defined(Q_OS_MACOS)
+ path.remove("/Contents/MacOS");
+#endif
+ return path;
+ }
+
+ QStringList m_persistedFileNames;
+ QPointer<QWidget> m_widget;
+};
+
+void tst_SandboxedFileAccess::initTestCase()
+{
+ qDebug() << "đŸ“Ļ App bundle" << bundlePath();
+ qDebug() << "🔐 App container" << sandboxPath();
+
+ m_widget = new QWidget;
+ m_widget->show();
+ QVERIFY(QTest::qWaitForWindowExposed(m_widget));
+}
+
+void tst_SandboxedFileAccess::cleanupTestCase()
+{
+ NSURL *appSupportDir = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation)).toNSURL();
+ NSURL *bookmarksFile = [appSupportDir URLByAppendingPathComponent:@"SecurityScopedBookmarks.plist"];
+ NSError *error = nullptr;
+ NSMutableDictionary *bookmarks = [[NSDictionary dictionaryWithContentsOfURL:bookmarksFile
+ error:&error] mutableCopy];
+ for (NSString *path in bookmarks.allKeys) {
+ if (m_persistedFileNames.contains(QString::fromNSString(path))) {
+ qDebug() << "Keeping knowledge of persisted path" << path;
+ continue;
+ }
+ qDebug() << "Wiping knowledge of path" << path;
+ [bookmarks removeObjectForKey:path];
+ }
+ [bookmarks writeToURL:bookmarksFile error:&error];
+
+ qGuiApp->quit();
+}
+
+void tst_SandboxedFileAccess::alwaysAccessibleLocations()
+{
+ readFile(QCoreApplication::applicationFilePath());
+
+ // The documents location is inside the sandbox and writable on both iOS and macOS
+ auto documents = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
+ writeFile(documents + "/test-writable-file.txt");
+}
+
+void tst_SandboxedFileAccess::standardPaths_data()
+{
+ QTest::addColumn<QStandardPaths::StandardLocation>("location");
+ auto standardLocations = QMetaEnum::fromType<QStandardPaths::StandardLocation>();
+ for (int i = 0; i < standardLocations.keyCount(); ++i)
+ QTest::newRow(standardLocations.key(i)) << QStandardPaths::StandardLocation(standardLocations.value(i));
+}
+
+void tst_SandboxedFileAccess::standardPaths()
+{
+ QFETCH(QStandardPaths::StandardLocation, location);
+ auto writableLocation = QStandardPaths::writableLocation(location);
+
+ if (writableLocation.isEmpty())
+ QSKIP("There's no writable location for this location");
+
+ QFileInfo info(writableLocation);
+ if (info.isSymLink() && !info.symLinkTarget().startsWith(sandboxPath()))
+ QSKIP("This location is a symlink to outside the sandbox and requires access");
+
+ QVERIFY(QDir().mkpath(writableLocation));
+
+#if !defined(Q_OS_MACOS)
+ QEXPECT_FAIL("HomeLocation", "The sandbox root is not writable on iOS", Abort);
+#endif
+ writeFile(writableLocation + QString("/test-writable-file-%1.txt").arg(QTest::currentDataTag()));
+}
+
+void tst_SandboxedFileAccess::readSingleFile()
+{
+ QString filePath = getFileName(QFileDialog::AcceptOpen,
+ QFileDialog::ExistingFile, "Choose file to read");
+ readFile(filePath);
+
+ {
+ QFile file(QCoreApplication::applicationFilePath());
+ QVERIFY(file.open(QFile::ReadOnly));
+ QByteArray plistContent = file.read(100);
+ file.close();
+
+ // Check that setFileName can target a security scoped file
+ file.setFileName(filePath);
+ QVERIFY(file.open(QFile::ReadOnly));
+ QVERIFY(file.isReadable());
+ QCOMPARE_NE(file.read(100), plistContent);
+ }
+
+ QDir dir;
+ QString fileName;
+
+ {
+ QFileInfo info(filePath);
+ dir = info.path();
+ fileName = info.fileName();
+ QVERIFY(dir.exists());
+ QVERIFY(!fileName.isEmpty());
+ }
+
+ // Check that we're able to access files via non-canonical paths
+ readFile(dir.absolutePath() + "/../" + dir.dirName() + "/" + fileName);
+}
+
+QByteArray tst_SandboxedFileAccess::readFile(const QString &fileName)
+{
+ QFile file(fileName);
+ QVERIFY(file.exists());
+ QVERIFY(file.open(QFile::ReadOnly));
+ QVERIFY(file.isReadable());
+ QByteArray data = file.read(100);
+ QVERIFY(!data.isEmpty());
+ return data;
+}
+
+void tst_SandboxedFileAccess::writeSingleFile()
+{
+ QString filePath = getFileName(QFileDialog::AcceptSave, QFileDialog::AnyFile,
+ "Choose a file to write", "write-single-file.txt");
+ writeFile(filePath);
+ readFile(filePath);
+}
+
+void tst_SandboxedFileAccess::writeSingleFileNonCanonical()
+{
+ QString filePath = getFileName(QFileDialog::AcceptSave, QFileDialog::AnyFile,
+ "Choose a file to write", "write-single-file-non-canonical.txt");
+ QDir dir;
+ QString fileName;
+
+ {
+ QFileInfo info(filePath);
+ dir = info.path();
+ fileName = info.fileName();
+ QVERIFY(dir.exists());
+ QVERIFY(!fileName.isEmpty());
+ }
+
+ writeFile(dir.absolutePath() + "/../" + dir.dirName() + "/" + fileName);
+ readFile(filePath);
+}
+
+void tst_SandboxedFileAccess::writeFile(const QString &fileName)
+{
+ QFile file(fileName);
+ QVERIFY(file.open(QFile::WriteOnly));
+ QVERIFY(file.isWritable());
+ QVERIFY(file.write("Hello world"));
+}
+
+void tst_SandboxedFileAccess::removeFile()
+{
+ QString fileName = getFileName(QFileDialog::AcceptSave, QFileDialog::AnyFile,
+ "Choose a file to write and then remove", "write-and-remove-file.txt");
+ writeFile(fileName);
+
+ {
+ QFile file(fileName);
+ QVERIFY(file.remove());
+ }
+}
+
+void tst_SandboxedFileAccess::trashFile()
+{
+ QString fileName = getFileName(QFileDialog::AcceptSave, QFileDialog::AnyFile,
+ "Choose a file to write and then trash", "write-and-trash-file.txt");
+ writeFile(fileName);
+
+ {
+ QFile file(fileName);
+ QVERIFY(file.moveToTrash());
+ }
+}
+
+void tst_SandboxedFileAccess::readFileAfterRestart()
+{
+ // Every other restart of the app will save a file or load a previously saved file
+
+ QSettings settings;
+ QString savedFile = settings.value("savedFile").toString();
+ if (savedFile.isEmpty()) {
+ QString filePath = getFileName(QFileDialog::AcceptSave, QFileDialog::AnyFile,
+ "Choose a file to write for reading after restart", "write-and-read-after-restart.txt");
+ qDebug() << "Writing" << filePath << "and saving to preferences";
+ writeFile(filePath);
+ settings.setValue("savedFile", filePath);
+ m_persistedFileNames << filePath;
+ } else {
+ qDebug() << "Loading" << savedFile << "from preferences";
+ settings.remove("savedFile"); // Remove up front, in case this fails
+ readFile(savedFile);
+ QFile file(savedFile);
+ QVERIFY(file.remove());
+ }
+}
+
+void tst_SandboxedFileAccess::directoryAccess()
+{
+ // Every other restart of the app will re-establish access to the folder,
+ // or re-use previous access.
+
+ QSettings settings;
+ QString directory = settings.value("savedDirectory").toString();
+ if (directory.isEmpty()) {
+ directory = getFileName(QFileDialog::AcceptOpen, QFileDialog::Directory,
+ "Choose a directory we can create some files in");
+ auto canonical = QFileInfo(directory).canonicalFilePath();
+ QVERIFY(!canonical.isEmpty());
+ directory = canonical;
+ settings.setValue("savedDirectory", directory);
+ m_persistedFileNames << QFileInfo(directory).canonicalFilePath();
+ } else {
+ settings.remove("savedDirectory");
+ }
+ settings.sync();
+
+ QString fileInDir;
+
+ {
+ QDir dir(directory);
+ QVERIFY(dir.exists());
+ QVERIFY(dir.isReadable());
+ fileInDir = dir.filePath("file-in-dir.txt");
+ }
+
+ writeFile(fileInDir);
+ readFile(fileInDir);
+
+ {
+ QDir dir(directory);
+ QVERIFY(dir.count() > 0);
+ QVERIFY(dir.entryList().contains("file-in-dir.txt"));
+ }
+
+ {
+ QDir dir(directory);
+ QVERIFY(dir.mkdir("subdirectory"));
+ QVERIFY(dir.entryList().contains("subdirectory"));
+ fileInDir = dir.filePath("subdirectory/file-in-subdir.txt");
+ }
+
+ writeFile(fileInDir);
+ readFile(fileInDir);
+
+ // Check that we can write to a non-canonical path within the directory
+ // we have access to, and then read it from the canonical path.
+ writeFile(directory + "/subdirectory/../non-existing-non-canonical.txt");
+ readFile(directory + "/non-existing-non-canonical.txt");
+
+ {
+ QDir dir(directory);
+ QVERIFY(dir.cd("subdirectory"));
+ dir.removeRecursively();
+ }
+}
+
+void tst_SandboxedFileAccess::securityScopedTargetFile()
+{
+ // This is a non-security scoped file
+ auto documents = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
+ QString sourceFilePath = documents + "/test-security-scoped-target-file.txt";
+ writeFile(sourceFilePath);
+ QFile sourceFile(sourceFilePath);
+
+ QString directory = getFileName(QFileDialog::AcceptOpen, QFileDialog::Directory,
+ "Choose a directory we can link/copy some to");
+
+ QString subDirectory;
+ {
+ QDir dir(directory);
+ QVERIFY(dir.mkdir("subdirectory"));
+ QVERIFY(dir.entryList().contains("subdirectory"));
+ subDirectory = dir.filePath("subdirectory");
+ }
+
+ QVERIFY(sourceFile.copy(subDirectory + "/copied-file.txt"));
+ QVERIFY(sourceFile.link(subDirectory + "/linked-file.txt"));
+ QVERIFY(sourceFile.rename(subDirectory + "/renamed-file.txt"));
+
+ {
+ QDir dir(directory);
+ QVERIFY(dir.cd("subdirectory"));
+ dir.removeRecursively();
+ }
+}
+
+void tst_SandboxedFileAccess::fileOpenEvent()
+{
+ struct OpenEventFilter : public QObject
+ {
+ bool eventFilter(QObject *watched, QEvent *event) override
+ {
+ if (event->type() == QEvent::FileOpen) {
+ QFileOpenEvent *openEvent = static_cast<QFileOpenEvent *>(event);
+ fileName = openEvent->file();
+ }
+
+ return QObject::eventFilter(watched, event);
+ }
+
+ QString fileName;
+ };
+
+ OpenEventFilter openEventFilter;
+ qGuiApp->installEventFilter(&openEventFilter);
+
+ m_widget->setLayout(new QVBoxLayout);
+ QLabel label;
+ label.setWordWrap(true);
+ m_widget->layout()->addWidget(&label);
+#if defined(Q_OS_MACOS)
+ label.setText("Drag a text file to the app's Dock icon, or open in the app via Finder's 'Open With' menu");
+#else
+ label.setText("Open the Files app, and choose 'Open With' or share a text document with this app");
+#endif
+ label.show();
+
+ QTRY_VERIFY_WITH_TIMEOUT(!openEventFilter.fileName.isNull(), 30s);
+ label.setText("Got file: " + openEventFilter.fileName);
+
+ readFile(openEventFilter.fileName);
+
+ QTest::qWait(3000);
+}
+
+QString tst_SandboxedFileAccess::getFileName(QFileDialog::AcceptMode acceptMode, QFileDialog::FileMode fileMode,
+ const QString &action, const QString &fileName)
+{
+ QFileDialog dialog(m_widget);
+ dialog.setAcceptMode(acceptMode);
+ dialog.setFileMode(fileMode);
+ dialog.setWindowTitle(action);
+ dialog.setLabelText(QFileDialog::Accept, action);
+ dialog.selectFile(fileName);
+ if (!action.isEmpty())
+ qDebug() << "â„šī¸" << action;
+ dialog.exec();
+ auto selectedFiles = dialog.selectedFiles();
+ return selectedFiles.count() ? selectedFiles.first() : QString();
+}
+
+int main(int argc, char** argv)
+{
+ QApplication app(argc, argv);
+
+ tst_SandboxedFileAccess testObject;
+
+ // Run tests with QApp running
+ int testExecResult = 0;
+ QMetaObject::invokeMethod(&testObject, [&]{
+ testExecResult = QTest::qExec(&testObject, argc, argv);
+ }, Qt::QueuedConnection);
+
+ [[maybe_unused]] int appExecResult = app.exec();
+ return testExecResult;
+}
+
+#include "tst_sandboxed_file_access.moc"
diff --git a/tests/manual/wasm/qtwasmtestlib/qtwasmtestlib.cpp b/tests/manual/wasm/qtwasmtestlib/qtwasmtestlib.cpp
index 1e49847c97f..484c28a484b 100644
--- a/tests/manual/wasm/qtwasmtestlib/qtwasmtestlib.cpp
+++ b/tests/manual/wasm/qtwasmtestlib/qtwasmtestlib.cpp
@@ -9,6 +9,12 @@
#include <emscripten.h>
#include <emscripten/threading.h>
+#if QT_CONFIG(wasm_jspi)
+# define QT_WASM_EMSCRIPTEN_ASYNC ,emscripten::async()
+#else
+# define QT_WASM_EMSCRIPTEN_ASYNC
+#endif
+
namespace QtWasmTest {
namespace {
QObject *g_testObject = nullptr;
@@ -127,7 +133,7 @@ void passTest()
EMSCRIPTEN_BINDINGS(qtwebtestrunner) {
emscripten::function("cleanupTestCase", &cleanupTestCase);
emscripten::function("getTestFunctions", &getTestFunctions);
- emscripten::function("runTestFunction", &runTestFunction, emscripten::async());
+ emscripten::function("runTestFunction", &runTestFunction QT_WASM_EMSCRIPTEN_ASYNC);
emscripten::function("qtWasmFail", &failTest);
emscripten::function("qtWasmPass", &passTest);
}