diff options
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); } |
