diff options
230 files changed, 7940 insertions, 17048 deletions
diff --git a/.gitignore b/.gitignore index 83c3424e9ae..20aff5f53d4 100644 --- a/.gitignore +++ b/.gitignore @@ -224,6 +224,7 @@ qtc-qmldbg/ callgrind.out.* core Makefile* +!/util/sbom/cyclonedx/Makefile !/qmake/Makefile.win32* !/qmake/Makefile.unix pcviewer.cfg diff --git a/cmake/QtBuildHelpers.cmake b/cmake/QtBuildHelpers.cmake index b6132ddae7e..7205bad5253 100644 --- a/cmake/QtBuildHelpers.cmake +++ b/cmake/QtBuildHelpers.cmake @@ -295,10 +295,13 @@ function(qt_internal_get_qt_build_public_helpers out_var) QtPublicPluginHelpers QtPublicPluginHelpers_v2 QtPublicSbomAttributionHelpers + QtPublicSbomCommonGenerationHelpers QtPublicSbomCpeHelpers + QtPublicSbomCycloneDXHelpers QtPublicSbomDepHelpers QtPublicSbomFileHelpers QtPublicSbomGenerationHelpers + QtPublicSbomGenerationCycloneDXHelpers QtPublicSbomHelpers QtPublicSbomLicenseHelpers QtPublicSbomOpsHelpers diff --git a/cmake/QtBuildInformation.cmake b/cmake/QtBuildInformation.cmake index 77c6d8184b3..5c44a7e5f48 100644 --- a/cmake/QtBuildInformation.cmake +++ b/cmake/QtBuildInformation.cmake @@ -47,6 +47,11 @@ function(qt_print_feature_summary) endforeach() endif() + # Print an SBOM section for a top-level build, or a single repo. + if(NOT QT_NO_SBOM_SUMMARY_INFO) + qt_internal_add_sbom_summary_info() + endif() + # Show which packages were found. feature_summary(INCLUDE_QUIET_PACKAGES WHAT PACKAGES_FOUND diff --git a/cmake/QtBuildRepoExamplesHelpers.cmake b/cmake/QtBuildRepoExamplesHelpers.cmake index a6d7c2b144b..1292844f132 100644 --- a/cmake/QtBuildRepoExamplesHelpers.cmake +++ b/cmake/QtBuildRepoExamplesHelpers.cmake @@ -90,6 +90,10 @@ macro(qt_examples_build_begin) add_link_options(${active_linker_flags}) endif() + if(QT_EXTRA_EXAMPLE_TARGET_DEFINES) + add_compile_definitions(${QT_EXTRA_EXAMPLE_TARGET_DEFINES}) + endif() + # Marker for warnings_as_errors. set(QT_INTERNAL_IS_EXAMPLE_IN_TREE_BUILD ON) endif() @@ -200,8 +204,11 @@ function(qt_internal_add_example subdir) # Don't show warnings for examples that were added via qt_internal_add_example. # Those that are added via add_subdirectory will see the warning, due to the parent scope # having the variable set to TRUE. - if(QT_FEATURE_developer_build AND NOT QT_NO_WARN_ABOUT_EXAMPLE_ADD_SUBDIRECTORY_WARNING) - set(QT_WARN_ABOUT_EXAMPLE_ADD_SUBDIRECTORY FALSE) + if(QT_FEATURE_developer_build) + set(QT_LINT_EXAMPLES ON) + if(NOT QT_NO_WARN_ABOUT_EXAMPLE_ADD_SUBDIRECTORY_WARNING) + set(QT_WARN_ABOUT_EXAMPLE_ADD_SUBDIRECTORY FALSE) + endif() endif() # Pre-compute unique example name based on the subdir, in case of target name clashes. @@ -354,6 +361,10 @@ function(qt_internal_add_example_external_project subdir) cmake_parse_arguments(PARSE_ARGV 1 arg "${options}" "${singleOpts}" "${multiOpts}") + if(QT_FEATURE_developer_build) + set(QT_LINT_EXAMPLES ON) + endif() + _qt_internal_get_build_vars_for_external_projects( CMAKE_DIR_VAR qt_cmake_dir PREFIXES_VAR qt_prefixes @@ -368,6 +379,10 @@ function(qt_internal_add_example_external_project subdir) list(APPEND var_defs -DCMAKE_TOOLCHAIN_FILE:FILEPATH=${qt_cmake_dir}/qt.toolchain.cmake ) + if(QT_LINT_EXAMPLES) + list(APPEND var_defs -DQT_LINT_EXAMPLES=ON) + endif() + else() list(PREPEND CMAKE_PREFIX_PATH ${qt_prefixes}) @@ -519,6 +534,18 @@ function(qt_internal_add_example_external_project subdir) "-DCMAKE_${item_type}_LINKER_FLAGS_INIT:STRING=${active_linker_flags}") endforeach() endif() + if(QT_EXTRA_EXAMPLE_TARGET_DEFINES) + unset(extra_args) + foreach(def IN LISTS QT_EXTRA_EXAMPLE_TARGET_DEFINES) + list(APPEND extra_args "-D${def}") + endforeach() + list(JOIN extra_args " " extra_defines_str) + + foreach(language IN ITEMS C CXX OBJC OBJCXX) + list(APPEND var_defs + "-DCMAKE_${language}_FLAGS_INIT:STRING=${extra_defines_str}") + endforeach() + endif() set(deps "") list(REMOVE_DUPLICATES QT_EXAMPLE_DEPENDENCIES) diff --git a/cmake/QtConfigDependencies.cmake.in b/cmake/QtConfigDependencies.cmake.in index b006f052f7f..9144512a8f3 100644 --- a/cmake/QtConfigDependencies.cmake.in +++ b/cmake/QtConfigDependencies.cmake.in @@ -31,6 +31,8 @@ set(__qt_third_party_deps "@third_party_deps@") # set _NOT_FOUND_MESSAGE which will be displayed by the includer of the Dependencies file. set(${CMAKE_FIND_PACKAGE_NAME}_FIND_REQUIRED FALSE) -_qt_internal_find_third_party_dependencies(@INSTALL_CMAKE_NAMESPACE@ __qt_third_party_deps) +if(__qt_third_party_deps) + _qt_internal_find_third_party_dependencies(@INSTALL_CMAKE_NAMESPACE@ __qt_third_party_deps) +endif() set(@INSTALL_CMAKE_NAMESPACE@_FOUND TRUE) diff --git a/cmake/QtModuleDependencies.cmake.in b/cmake/QtModuleDependencies.cmake.in index ff84817ecf9..78ada0a7425 100644 --- a/cmake/QtModuleDependencies.cmake.in +++ b/cmake/QtModuleDependencies.cmake.in @@ -31,19 +31,25 @@ endif() # note: _third_party_deps example: "ICU\\;FALSE\\;1.0\\;i18n uc data;ZLIB\\;FALSE\\;\\;" set(__qt_@target@_third_party_deps "@third_party_deps@") @third_party_deps_extra_info@ -_qt_internal_find_third_party_dependencies("@target@" __qt_@target@_third_party_deps) +if(__qt_@target@_third_party_deps) + _qt_internal_find_third_party_dependencies("@target@" __qt_@target@_third_party_deps) +endif() unset(__qt_@target@_third_party_deps) # Find Qt tool package. set(__qt_@target@_tool_deps "@main_module_tool_deps@") -_qt_internal_find_tool_dependencies("@target@" __qt_@target@_tool_deps) +if(__qt_@target@_tool_deps) + _qt_internal_find_tool_dependencies("@target@" __qt_@target@_tool_deps) +endif() unset(__qt_@target@_tool_deps) # note: target_deps example: "Qt6Core\;5.12.0;Qt6Gui\;5.12.0" set(__qt_@target@_target_deps "@target_deps@") set(__qt_@target@_find_dependency_paths "${CMAKE_CURRENT_LIST_DIR}/.." "${_qt_cmake_dir}") -_qt_internal_find_qt_dependencies("@target@" __qt_@target@_target_deps - __qt_@target@_find_dependency_paths) +if(__qt_@target@_target_deps) + _qt_internal_find_qt_dependencies("@target@" __qt_@target@_target_deps + __qt_@target@_find_dependency_paths) +endif() unset(__qt_@target@_target_deps) unset(__qt_@target@_find_dependency_paths) diff --git a/cmake/QtModuleToolsDependencies.cmake.in b/cmake/QtModuleToolsDependencies.cmake.in index 14af9c90e38..51e0636fa91 100644 --- a/cmake/QtModuleToolsDependencies.cmake.in +++ b/cmake/QtModuleToolsDependencies.cmake.in @@ -5,7 +5,11 @@ set(@INSTALL_CMAKE_NAMESPACE@@target@_FOUND TRUE) set(__qt_@target@_tool_third_party_deps "@third_party_deps@") -_qt_internal_find_third_party_dependencies("@target@" __qt_@target@_tool_third_party_deps) +if(__qt_@target@_tool_third_party_deps) + _qt_internal_find_third_party_dependencies("@target@" __qt_@target@_tool_third_party_deps) +endif() set(__qt_@target@_tool_deps "@package_deps@") -_qt_internal_find_tool_dependencies("@target@" __qt_@target@_tool_deps) +if(__qt_@target@_tool_deps) + _qt_internal_find_tool_dependencies("@target@" __qt_@target@_tool_deps) +endif() diff --git a/cmake/QtPluginDependencies.cmake.in b/cmake/QtPluginDependencies.cmake.in index d5d3398e9e3..2ea6a19a732 100644 --- a/cmake/QtPluginDependencies.cmake.in +++ b/cmake/QtPluginDependencies.cmake.in @@ -5,7 +5,9 @@ set(@target@_FOUND TRUE) # note: _third_party_deps example: "ICU\\;FALSE\\;1.0\\;i18n uc data;ZLIB\\;FALSE\\;\\;" set(__qt_@target@_third_party_deps "@third_party_deps@") -_qt_internal_find_third_party_dependencies("@target@" __qt_@target@_third_party_deps) +if(__qt_@target@_third_party_deps) + _qt_internal_find_third_party_dependencies("@target@" __qt_@target@_third_party_deps) +endif() unset(__qt_@target@_third_party_deps) set(__qt_use_no_default_path_for_qt_packages "NO_DEFAULT_PATH") @@ -16,8 +18,10 @@ endif() # note: target_deps example: "Qt6Core\;5.12.0;Qt6Gui\;5.12.0" set(__qt_@target@_target_deps "@target_deps@") set(__qt_@target@_find_dependency_paths "@find_dependency_paths@") -_qt_internal_find_qt_dependencies("@target@" __qt_@target@_target_deps - __qt_@target@_find_dependency_paths) +if(__qt_@target@_target_deps) + _qt_internal_find_qt_dependencies("@target@" __qt_@target@_target_deps + __qt_@target@_find_dependency_paths) +endif() unset(__qt_@target@_target_deps) unset(__qt_@target@_find_dependency_paths) diff --git a/cmake/QtProcessConfigureArgs.cmake b/cmake/QtProcessConfigureArgs.cmake index 7118f1f6f92..05e179ba3b3 100644 --- a/cmake/QtProcessConfigureArgs.cmake +++ b/cmake/QtProcessConfigureArgs.cmake @@ -351,12 +351,33 @@ endfunction() macro(qt_add_common_commandline_options) qt_commandline_option(headersclean TYPE boolean) qt_commandline_option(sbom TYPE boolean CMAKE_VARIABLE QT_GENERATE_SBOM) - qt_commandline_option(sbom-json TYPE boolean CMAKE_VARIABLE QT_SBOM_GENERATE_JSON) + + # Semi-public, undocumented. + qt_commandline_option(sbom-all TYPE boolean CMAKE_VARIABLE QT_SBOM_GENERATE_AND_VERIFY_ALL) + + qt_commandline_option(sbom-spdx-v2 TYPE boolean + CMAKE_VARIABLE QT_SBOM_GENERATE_SPDX_V2) + + qt_commandline_option(sbom-cyclonedx-v1_6 TYPE boolean + CMAKE_VARIABLE QT_SBOM_GENERATE_CYDX_V1_6) + + qt_commandline_option(sbom-cyclonedx-v1_6-required TYPE boolean + CMAKE_VARIABLE QT_SBOM_REQUIRE_GENERATE_CYDX_V1_6) + + qt_commandline_option(sbom-cyclonedx-v1_6-verify-required TYPE boolean + CMAKE_VARIABLE QT_SBOM_REQUIRE_VERIFY_CYDX_V1_6) + + qt_commandline_option(sbom-cyclonedx-v1_6-verbose TYPE boolean + CMAKE_VARIABLE QT_SBOM_VERBOSE_CYDX_V1_6) + + qt_commandline_option(sbom-json TYPE boolean CMAKE_VARIABLE QT_SBOM_GENERATE_SPDX_V2_JSON) qt_commandline_option(sbom-json-required TYPE boolean - CMAKE_VARIABLE QT_SBOM_REQUIRE_GENERATE_JSON + CMAKE_VARIABLE QT_SBOM_REQUIRE_GENERATE_SPDX_V2_JSON ) - qt_commandline_option(sbom-verify TYPE boolean CMAKE_VARIABLE QT_SBOM_VERIFY) - qt_commandline_option(sbom-verify-required TYPE boolean CMAKE_VARIABLE QT_SBOM_REQUIRE_VERIFY) + + qt_commandline_option(sbom-verify TYPE boolean CMAKE_VARIABLE QT_SBOM_VERIFY_SPDX_V2) + qt_commandline_option(sbom-verify-required TYPE boolean + CMAKE_VARIABLE QT_SBOM_REQUIRE_VERIFY_SPDX_V2) endmacro() function(qt_commandline_prefix arg var) diff --git a/cmake/QtPublicSbomCommonGenerationHelpers.cmake b/cmake/QtPublicSbomCommonGenerationHelpers.cmake new file mode 100644 index 00000000000..9de2055b5b6 --- /dev/null +++ b/cmake/QtPublicSbomCommonGenerationHelpers.cmake @@ -0,0 +1,668 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +# Helper function to get common project related variables for SBOM generation for both SPDX and +# CycloneDX formats. +function(_qt_internal_sbom_get_common_project_variables) + set(opt_args "") + set(single_args + # Forwarded + OUTPUT + OUTPUT_RELATIVE_PATH + COPYRIGHT + PROJECT + PROJECT_FOR_SPDX_ID + SUPPLIER + SUPPLIER_URL + + # Custom inputs + DEFAULT_SBOM_FILE_NAME_EXTENSION + + # Out vars + OUT_VAR_PROJECT_NAME + OUT_VAR_CURRENT_UTC + OUT_VAR_CURRENT_YEAR + OUT_VAR_OUTPUT + OUT_VAR_OUTPUT_RELATIVE_PATH + OUT_VAR_PROJECT_FOR_SPDX_ID + OUT_VAR_COPYRIGHT + OUT_VAR_SUPPLIER + OUT_VAR_SUPPLIER_URL + OUT_VAR_DEFAULT_PROJECT_COMMENT + ) + set(multi_args "") + cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") + + if(QT_SBOM_FAKE_TIMESTAMP) + set(current_utc "2590-01-01T11:33:55Z") + set(current_year "2590") + else() + string(TIMESTAMP current_utc UTC) + string(TIMESTAMP current_year "%Y" UTC) + endif() + + set(${arg_OUT_VAR_CURRENT_UTC} "${current_utc}" PARENT_SCOPE) + set(${arg_OUT_VAR_CURRENT_YEAR} "${current_year}" PARENT_SCOPE) + + _qt_internal_sbom_set_default_option_value(PROJECT "${PROJECT_NAME}") + set(${arg_OUT_VAR_PROJECT_NAME} "${arg_PROJECT}" PARENT_SCOPE) + + _qt_internal_sbom_get_git_version_vars() + + _qt_internal_path_join(default_sbom_file_name + "${arg_PROJECT}" + "${arg_PROJECT}-sbom-${QT_SBOM_GIT_VERSION_PATH}.${arg_DEFAULT_SBOM_FILE_NAME_EXTENSION}") + _qt_internal_path_join(default_install_sbom_path + "\${CMAKE_INSTALL_PREFIX}/" "${CMAKE_INSTALL_DATAROOTDIR}" "${default_sbom_file_name}" + ) + + _qt_internal_sbom_set_default_option_value(OUTPUT "${default_install_sbom_path}") + _qt_internal_sbom_set_default_option_value(OUTPUT_RELATIVE_PATH + "${default_sbom_file_name}") + + set(${arg_OUT_VAR_OUTPUT} "${arg_OUTPUT}" PARENT_SCOPE) + set(${arg_OUT_VAR_OUTPUT_RELATIVE_PATH} "${arg_OUTPUT_RELATIVE_PATH}" PARENT_SCOPE) + + _qt_internal_sbom_set_default_option_value(PROJECT_FOR_SPDX_ID "Package-${arg_PROJECT}") + string(REGEX REPLACE "[^A-Za-z0-9.]+" "-" arg_PROJECT_FOR_SPDX_ID "${arg_PROJECT_FOR_SPDX_ID}") + string(REGEX REPLACE "-+$" "" arg_PROJECT_FOR_SPDX_ID "${arg_PROJECT_FOR_SPDX_ID}") + + # Prevent collision with other generated SPDXID with -[0-9]+ suffix. + string(REGEX REPLACE "-([0-9]+)$" "\\1" arg_PROJECT_FOR_SPDX_ID "${arg_PROJECT_FOR_SPDX_ID}") + + set(project_spdx_id "SPDXRef-${arg_PROJECT_FOR_SPDX_ID}") + set(${arg_OUT_VAR_PROJECT_FOR_SPDX_ID} "${project_spdx_id}" PARENT_SCOPE) + + _qt_internal_sbom_set_default_option_value_and_error_if_empty(SUPPLIER "") + set(${arg_OUT_VAR_SUPPLIER} "${arg_SUPPLIER}" PARENT_SCOPE) + + _qt_internal_sbom_set_default_option_value_and_error_if_empty(SUPPLIER_URL + "${PROJECT_HOMEPAGE_URL}") + set(${arg_OUT_VAR_SUPPLIER_URL} "${arg_SUPPLIER_URL}" PARENT_SCOPE) + + _qt_internal_sbom_set_default_option_value(COPYRIGHT "${current_year} ${arg_SUPPLIER}") + set(${arg_OUT_VAR_COPYRIGHT} "${arg_COPYRIGHT}" PARENT_SCOPE) + + get_cmake_property(is_multi_config GENERATOR_IS_MULTI_CONFIG) + if(is_multi_config) + set(cmake_configs "${CMAKE_CONFIGURATION_TYPES}") + else() + set(cmake_configs "${CMAKE_BUILD_TYPE}") + endif() + + set(cmake_version "Built by CMake ${CMAKE_VERSION}") + set(system_name_and_processor "${CMAKE_SYSTEM_NAME} (${CMAKE_SYSTEM_PROCESSOR})") + set(default_project_comment + "${cmake_version} with ${cmake_configs} configuration for ${system_name_and_processor}") + set(${arg_OUT_VAR_DEFAULT_PROJECT_COMMENT} "${default_project_comment}" PARENT_SCOPE) +endfunction() + +# Helper function to save SBOM project path values like relative build and install dirs, +# in global properties. +function(_qt_internal_sbom_save_common_path_variables_in_global_properties) + set(opt_args "") + set(single_args + OUTPUT + OUTPUT_RELATIVE_PATH + SBOM_DIR + PROPERTY_SUFFIX + ) + set(multi_args "") + cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + get_filename_component(output_file_name_without_ext "${arg_OUTPUT}" NAME_WLE) + get_filename_component(output_file_ext "${arg_OUTPUT}" LAST_EXT) + + set(computed_sbom_file_name_without_ext "${output_file_name_without_ext}${multi_config_suffix}") + set(computed_sbom_file_name "${output_file_name_without_ext}${output_file_ext}") + + # In a super build and in a no-prefix build, put all the build time sboms into the same dir in, + # in the qtbase build dir. + if(QT_BUILDING_QT AND (QT_SUPERBUILD OR (NOT QT_WILL_INSTALL))) + set(build_sbom_root_dir "${QT_BUILD_DIR}") + else() + set(build_sbom_root_dir "${arg_SBOM_DIR}") + endif() + + get_filename_component(output_relative_dir "${arg_OUTPUT_RELATIVE_PATH}" DIRECTORY) + + set(build_sbom_dir "${build_sbom_root_dir}/${output_relative_dir}") + set(build_sbom_path "${build_sbom_dir}/${computed_sbom_file_name}") + set(build_sbom_path_without_ext + "${build_sbom_dir}/${computed_sbom_file_name_without_ext}") + + set(install_sbom_path "${arg_OUTPUT}") + + get_filename_component(install_sbom_dir "${install_sbom_path}" DIRECTORY) + set(install_sbom_path_without_ext "${install_sbom_dir}/${output_file_name_without_ext}") + + set(suffix "${arg_PROPERTY_SUFFIX}") + + set_property(GLOBAL PROPERTY _qt_sbom_build_output_path${suffix} "${build_sbom_path}") + set_property(GLOBAL PROPERTY _qt_sbom_build_output_path_without_ext${suffix} + "${build_sbom_path_without_ext}") + set_property(GLOBAL PROPERTY _qt_sbom_build_output_dir${suffix} "${build_sbom_dir}") + + set_property(GLOBAL PROPERTY _qt_sbom_install_output_path${suffix} "${install_sbom_path}") + set_property(GLOBAL PROPERTY _qt_sbom_install_output_path_without_ext${suffix} + "${install_sbom_path_without_ext}") + set_property(GLOBAL PROPERTY _qt_sbom_install_output_dir${suffix} "${install_sbom_dir}") +endfunction() + +# Helper function to get SBOM project path values like relative build and install dirs, +function(_qt_internal_sbom_get_common_path_variables_from_global_properties) + set(opt_args "") + set(single_args + SBOM_FORMAT + OUT_VAR_SBOM_BUILD_OUTPUT_PATH + OUT_VAR_SBOM_BUILD_OUTPUT_PATH_WITHOUT_EXT + OUT_VAR_SBOM_BUILD_OUTPUT_DIR + OUT_VAR_SBOM_INSTALL_OUTPUT_PATH + OUT_VAR_SBOM_INSTALL_OUTPUT_PATH_WITHOUT_EXT + OUT_VAR_SBOM_INSTALL_OUTPUT_DIR + ) + set(multi_args "") + cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + if(arg_SBOM_FORMAT STREQUAL "SPDX_V2") + set(suffix "") + elseif(arg_SBOM_FORMAT STREQUAL "CYDX_V1_6") + set(suffix "_cydx") + endif() + + get_property(sbom_build_output_path GLOBAL PROPERTY _qt_sbom_build_output_path${suffix}) + get_property(sbom_build_output_path_without_ext GLOBAL PROPERTY + _qt_sbom_build_output_path_without_ext${suffix}) + get_property(sbom_build_output_dir GLOBAL PROPERTY _qt_sbom_build_output_dir${suffix}) + + get_property(sbom_install_output_path GLOBAL PROPERTY _qt_sbom_install_output_path${suffix}) + get_property(sbom_install_output_path_without_ext GLOBAL PROPERTY + _qt_sbom_install_output_path_without_ext${suffix}) + get_property(sbom_install_output_dir GLOBAL PROPERTY _qt_sbom_install_output_dir${suffix}) + + if(arg_OUT_VAR_SBOM_BUILD_OUTPUT_PATH) + set(${arg_OUT_VAR_SBOM_BUILD_OUTPUT_PATH} "${sbom_build_output_path}" PARENT_SCOPE) + endif() + if(arg_OUT_VAR_SBOM_BUILD_OUTPUT_PATH_WITHOUT_EXT) + set(${arg_OUT_VAR_SBOM_BUILD_OUTPUT_PATH_WITHOUT_EXT} + "${sbom_build_output_path_without_ext}" PARENT_SCOPE) + endif() + if(arg_OUT_VAR_SBOM_BUILD_OUTPUT_DIR) + set(${arg_OUT_VAR_SBOM_BUILD_OUTPUT_DIR} "${sbom_build_output_dir}" PARENT_SCOPE) + endif() + if(arg_OUT_VAR_SBOM_INSTALL_OUTPUT_PATH) + set(${arg_OUT_VAR_SBOM_INSTALL_OUTPUT_PATH} "${sbom_install_output_path}" PARENT_SCOPE) + endif() + if(arg_OUT_VAR_SBOM_INSTALL_OUTPUT_PATH_WITHOUT_EXT) + set(${arg_OUT_VAR_SBOM_INSTALL_OUTPUT_PATH_WITHOUT_EXT} + "${sbom_install_output_path_without_ext}" PARENT_SCOPE) + endif() + if(arg_OUT_VAR_SBOM_INSTALL_OUTPUT_DIR) + set(${arg_OUT_VAR_SBOM_INSTALL_OUTPUT_DIR} "${sbom_install_output_dir}" PARENT_SCOPE) + endif() +endfunction() + +# Helper function to create a staging file for SBOM generation. +# It is the file that will be incrementally assembled by having content appended to it. +# Also creates the intro file that will add the assembled content for the SBOM project, aka for the +# main SPDX package or root CycloneDX component. +function(_qt_internal_sbom_create_sbom_staging_file) + set(opt_args "") + set(single_args + CONTENT + SBOM_FORMAT + REPO_PROJECT_NAME_LOWERCASE + OUT_VAR_CREATE_STAGING_FILE + OUT_VAR_SBOM_DIR + ) + set(multi_args "") + cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + # Create the directory that will contain all sbom related files. + _qt_internal_get_current_project_sbom_dir(sbom_dir) + file(MAKE_DIRECTORY "${sbom_dir}") + + if(arg_SBOM_FORMAT STREQUAL "SPDX_V2") + set(doc_base_name "SPDXRef-DOCUMENT") + set(doc_extension "spdx.in") + set(suffix "") + set(extra_content " + set(QT_SBOM_EXTERNAL_DOC_REFS \"\") +") + _qt_internal_get_staging_area_spdx_file_path(staging_area_file) + set(starting_message "Starting SPDX SBOM generation in build dir: ${staging_area_file}") + elseif(arg_SBOM_FORMAT STREQUAL "CYDX_V1_6") + set(doc_base_name "cydx-document") + set(doc_extension "cdx.in.toml") + set(suffix "_cydx") + set(extra_content "") + _qt_internal_get_staging_area_cydx_file_path(staging_area_file) + set(starting_message + "Starting CycloneDX SBOM TOML file generation in build dir: ${staging_area_file}") + endif() + + # Generate project document intro spdx file. + set(document_intro_file_name + "${sbom_dir}/${doc_base_name}-${arg_REPO_PROJECT_NAME_LOWERCASE}.${doc_extension}") + file(GENERATE OUTPUT "${document_intro_file_name}" CONTENT "${content}") + + get_cmake_property(is_multi_config GENERATOR_IS_MULTI_CONFIG) + if(is_multi_config) + set(multi_config_suffix "-$<CONFIG>") + else() + set(multi_config_suffix "") + endif() + + # Create cmake file to append the document intro spdx to the staging file. + set(create_staging_file + "${sbom_dir}/append_document_to_staging${suffix}${multi_config_suffix}.cmake") + + set(content " + cmake_minimum_required(VERSION 3.16) + message(STATUS \"${starting_message}\") + ${extra_content} + file(READ \"${document_intro_file_name}\" content) + # Override any previous file because we're starting from scratch. + file(WRITE \"${staging_area_file}\" \"\${content}\") +") + file(GENERATE OUTPUT "${create_staging_file}" CONTENT "${content}") + + set(${arg_OUT_VAR_CREATE_STAGING_FILE} "${create_staging_file}" PARENT_SCOPE) + set(${arg_OUT_VAR_SBOM_DIR} "${sbom_dir}" PARENT_SCOPE) +endfunction() + +# Helper function to save common project info like supplier, project name, spdx id in global +# properties. +function(_qt_internal_sbom_save_project_info_in_global_properties) + set(opt_args "") + set(single_args + SUPPLIER + SUPPLIER_URL + NAMESPACE + PROJECT + PROJECT_SPDX_ID + ) + set(multi_args "") + cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + set_property(GLOBAL PROPERTY _qt_sbom_project_supplier "${arg_SUPPLIER}") + set_property(GLOBAL PROPERTY _qt_sbom_project_supplier_url "${arg_SUPPLIER_URL}") + set_property(GLOBAL PROPERTY _qt_sbom_project_namespace "${arg_NAMESPACE}") + + set_property(GLOBAL PROPERTY _qt_sbom_project_name "${arg_PROJECT}") + set_property(GLOBAL PROPERTY _qt_sbom_project_spdx_id "${arg_PROJECT_SPDX_ID}") +endfunction() + +# Helper function to get cmake include files for SBOM generation from global properties. +function(_qt_internal_sbom_get_cmake_include_files) + set(opt_args "") + set(single_args + SBOM_FORMAT + OUT_VAR_INCLUDES + OUT_VAR_BEFORE_CHECKSUM_INCLUDES + OUT_VAR_AFTER_CHECKSUM_INCLUDES + OUT_VAR_POST_GENERATION_INCLUDES + OUT_VAR_VERIFY_INCLUDES + ) + set(multi_args "") + cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + if(arg_SBOM_FORMAT STREQUAL "SPDX_V2") + set(suffix "") + elseif(arg_SBOM_FORMAT STREQUAL "CYDX_V1_6") + set(suffix "_cydx") + endif() + + _qt_internal_sbom_collect_cmake_include_files(includes + JOIN_WITH_NEWLINES + PROPERTIES _qt_sbom_cmake_include_files${suffix} _qt_sbom_cmake_end_include_files${suffix} + ) + + # Before checksum includes are included after the verification codes have been collected + # and before their merged checksum(s) has been computed. + _qt_internal_sbom_collect_cmake_include_files(before_checksum_includes + JOIN_WITH_NEWLINES + PROPERTIES _qt_sbom_cmake_before_checksum_include_files${suffix} + ) + + # After checksum includes are included after the checksum has been computed and written to the + # QT_SBOM_VERIFICATION_CODE variable. + _qt_internal_sbom_collect_cmake_include_files(after_checksum_includes + JOIN_WITH_NEWLINES + PROPERTIES _qt_sbom_cmake_after_checksum_include_files${suffix} + ) + + # Post generation includes are included for both build and install time sboms, after + # sbom generation has finished. + _qt_internal_sbom_collect_cmake_include_files(post_generation_includes + JOIN_WITH_NEWLINES + PROPERTIES _qt_sbom_cmake_post_generation_include_files${suffix} + ) + + # Verification only makes sense on installation, where the checksums are present. + _qt_internal_sbom_collect_cmake_include_files(verify_includes + JOIN_WITH_NEWLINES + PROPERTIES _qt_sbom_cmake_verify_include_files${suffix} + ) + + if(arg_OUT_VAR_INCLUDES) + set(${arg_OUT_VAR_INCLUDES} "${includes}" PARENT_SCOPE) + endif() + if(arg_OUT_VAR_INCLUDES) + set(${arg_OUT_VAR_BEFORE_CHECKSUM_INCLUDES} "${before_checksum_includes}" PARENT_SCOPE) + endif() + if(arg_OUT_VAR_INCLUDES) + set(${arg_OUT_VAR_AFTER_CHECKSUM_INCLUDES} "${after_checksum_includes}" PARENT_SCOPE) + endif() + if(arg_OUT_VAR_INCLUDES) + set(${arg_OUT_VAR_POST_GENERATION_INCLUDES} "${post_generation_includes}" PARENT_SCOPE) + endif() + if(arg_OUT_VAR_INCLUDES) + set(${arg_OUT_VAR_VERIFY_INCLUDES} "${verify_includes}" PARENT_SCOPE) + endif() +endfunction() + +# Clears cmake include files for the current project from the global properties. +function(_qt_internal_sbom_clear_cmake_include_files) + set(opt_args "") + set(single_args + SBOM_FORMAT + ) + set(multi_args "") + cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + if(arg_SBOM_FORMAT STREQUAL "SPDX_V2") + set(suffix "") + elseif(arg_SBOM_FORMAT STREQUAL "CYDX_V1_6") + set(suffix "_cydx") + endif() + + # Clean up properties, so that they are empty for possible next repo in a top-level build. + set_property(GLOBAL PROPERTY _qt_sbom_cmake_include_files${suffix} "") + set_property(GLOBAL PROPERTY _qt_sbom_cmake_end_include_files${suffix} "") + set_property(GLOBAL PROPERTY _qt_sbom_cmake_before_checksum_include_files${suffix} "") + set_property(GLOBAL PROPERTY _qt_sbom_cmake_after_checksum_include_files${suffix} "") + set_property(GLOBAL PROPERTY _qt_sbom_cmake_post_generation_include_files${suffix} "") + set_property(GLOBAL PROPERTY _qt_sbom_cmake_verify_include_files${suffix} "") +endfunction() + +# Creates cmake build targets to create build-time SBOMs (for testing purposes only, because they +# lack checksums for installed files). +# Also creates the assemble_sbom cmake file that is used by both build-time and install-time +# sbom generation. +function(_qt_internal_sbom_create_build_time_sbom_targets) + set(opt_args "") + set(single_args + SBOM_FORMAT + REPO_PROJECT_NAME_LOWERCASE + REAL_QT_REPO_PROJECT_NAME_LOWERCASE + SBOM_BUILD_OUTPUT_PATH + SBOM_BUILD_OUTPUT_PATH_WITHOUT_EXT + SBOM_BUILD_OUTPUT_DIR + INCLUDES + POST_GENERATION_INCLUDES + OUT_VAR_ASSEMBLE_SBOM_INCLUDE_PATH + ) + set(multi_args "") + cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + if(arg_SBOM_FORMAT STREQUAL "SPDX_V2") + set(suffix "") + set(extra_content " + set(QT_SBOM_PACKAGES \"\") + set(QT_SBOM_PACKAGES_WITH_VERIFICATION_CODES \"\") +") + _qt_internal_get_staging_area_spdx_file_path(staging_area_file) + set(final_message "Finalizing SPDX SBOM generation in build dir") + set(build_comment "SPDX document") + elseif(arg_SBOM_FORMAT STREQUAL "CYDX_V1_6") + set(suffix "_cydx") + set(extra_content "") + _qt_internal_get_staging_area_cydx_file_path(staging_area_file) + set(final_message "Finalizing Cyclone DX SBOM TOML generation in build dir") + set(build_comment "Cyclone DX document") + endif() + + get_cmake_property(is_multi_config GENERATOR_IS_MULTI_CONFIG) + if(is_multi_config) + set(multi_config_suffix "-$<CONFIG>") + else() + set(multi_config_suffix "") + endif() + + _qt_internal_get_current_project_sbom_dir(sbom_dir) + set(content " + # QT_SBOM_BUILD_TIME be set to FALSE at install time, so don't override if it's set. + # This allows reusing the same cmake file for both build and install. + if(NOT DEFINED QT_SBOM_BUILD_TIME) + set(QT_SBOM_BUILD_TIME TRUE) + endif() + if(NOT QT_SBOM_OUTPUT_PATH) + set(QT_SBOM_OUTPUT_DIR \"${arg_SBOM_BUILD_OUTPUT_DIR}\") + set(QT_SBOM_OUTPUT_PATH \"${arg_SBOM_BUILD_OUTPUT_PATH}\") + set(QT_SBOM_OUTPUT_PATH_WITHOUT_EXT \"${arg_SBOM_BUILD_OUTPUT_PATH_WITHOUT_EXT}\") + file(MAKE_DIRECTORY \"${arg_SBOM_BUILD_OUTPUT_DIR}\") + endif() + ${extra_content} + ${arg_INCLUDES} + if(QT_SBOM_BUILD_TIME) + message(STATUS \"${final_message}: \${QT_SBOM_OUTPUT_PATH}\") + configure_file(\"${staging_area_file}\" \"\${QT_SBOM_OUTPUT_PATH}\") + ${arg_POST_GENERATION_INCLUDES} + endif() +") + set(assemble_sbom "${sbom_dir}/assemble_sbom${suffix}${multi_config_suffix}.cmake") + file(GENERATE OUTPUT "${assemble_sbom}" CONTENT "${content}") + + if(NOT TARGET sbom) + add_custom_target(sbom) + endif() + + # Create a build target to create a build-time sbom (no verification codes or sha1s). + set(repo_sbom_target "sbom_${arg_REPO_PROJECT_NAME_LOWERCASE}${suffix}") + set(comment "") + string(APPEND comment "Assembling build time ${build_comment} without checksums for " + "${arg_REPO_PROJECT_NAME_LOWERCASE}. Just for testing.") + add_custom_target(${repo_sbom_target} + COMMAND "${CMAKE_COMMAND}" -P "${assemble_sbom}" + COMMENT "${comment}" + VERBATIM + USES_TERMINAL # To avoid running two configs of the command in parallel + ) + + get_cmake_property(qt_repo_deps _qt_repo_deps_${arg_REAL_QT_REPO_PROJECT_NAME_LOWERCASE}) + if(qt_repo_deps) + foreach(repo_dep IN LISTS qt_repo_deps) + set(repo_dep_sbom "sbom_${repo_dep}${suffix}") + if(TARGET "${repo_dep_sbom}") + add_dependencies(${repo_sbom_target} ${repo_dep_sbom}) + endif() + endforeach() + endif() + + add_dependencies(sbom ${repo_sbom_target}) + + set(${arg_OUT_VAR_ASSEMBLE_SBOM_INCLUDE_PATH} "${assemble_sbom}" PARENT_SCOPE) +endfunction() + +# Helper function to setup install markers for multi-config generators. +# Makes sure to wait for all configurations to finish installation before actually generating +# the SBOM and removing the markers. +function(_qt_internal_sbom_setup_multi_config_install_markers) + set(opt_args "") + set(single_args + SBOM_DIR + SBOM_FORMAT + REPO_PROJECT_NAME_LOWERCASE + OUT_VAR_EXTRA_CODE_BEGIN + OUT_VAR_EXTRA_CODE_INNER_END + ) + set(multi_args "") + cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + set(extra_code_begin "") + set(extra_code_inner_end "") + + get_cmake_property(is_multi_config GENERATOR_IS_MULTI_CONFIG) + if(NOT is_multi_config) + set(${arg_OUT_VAR_EXTRA_CODE_BEGIN} "${extra_code_begin}" PARENT_SCOPE) + set(${arg_OUT_VAR_EXTRA_CODE_INNER_END} "${extra_code_inner_end}" PARENT_SCOPE) + return() + endif() + + if(arg_SBOM_FORMAT STREQUAL "SPDX_V2") + set(suffix "_spdx") + elseif(arg_SBOM_FORMAT STREQUAL "CYDX_V1_6") + set(suffix "_cydx") + endif() + + set(configs ${CMAKE_CONFIGURATION_TYPES}) + + set(install_markers_dir "${arg_SBOM_DIR}") + set(install_marker_path "${install_markers_dir}/finished_install${suffix}-$<CONFIG>.cmake") + + set(install_marker_code " + message(STATUS \"Writing install marker for config $<CONFIG>: ${install_marker_path} \") + file(WRITE \"${install_marker_path}\" \"\") +") + + install(CODE "${install_marker_code}" COMPONENT sbom) + if(QT_SUPERBUILD) + install(CODE "${install_marker_code}" + COMPONENT "sbom_${arg_REPO_PROJECT_NAME_LOWERCASE}${suffix}" + EXCLUDE_FROM_ALL) + endif() + + set(install_markers "") + foreach(config IN LISTS configs) + set(marker_path "${install_markers_dir}/finished_install${suffix}-${config}.cmake") + list(APPEND install_markers "${marker_path}") + # Remove the markers on reconfiguration, just in case there are stale ones. + if(EXISTS "${marker_path}") + file(REMOVE "${marker_path}") + endif() + endforeach() + + # Escape the semicolons in install_makers, so they don't break argument parsing in + # _qt_internal_sbom_setup_sbom_install_code when they are forwarded there. + string(REPLACE ";" "\\;" install_markers "${install_markers}") + + set(extra_code_begin " + set(QT_SBOM_INSTALL_MARKERS${suffix} \"${install_markers}\") + foreach(QT_SBOM_INSTALL_MARKER IN LISTS QT_SBOM_INSTALL_MARKERS${suffix}) + if(NOT EXISTS \"\${QT_SBOM_INSTALL_MARKER}\") + set(QT_SBOM_INSTALLED_ALL_CONFIGS${suffix} FALSE) + endif() + endforeach() +") + set(extra_code_inner_end " + foreach(QT_SBOM_INSTALL_MARKER IN LISTS QT_SBOM_INSTALL_MARKERS${suffix}) + message(STATUS + \"Removing install marker: \${QT_SBOM_INSTALL_MARKER} \") + file(REMOVE \"\${QT_SBOM_INSTALL_MARKER}\") + endforeach() +") + + set(${arg_OUT_VAR_EXTRA_CODE_BEGIN} "${extra_code_begin}" PARENT_SCOPE) + set(${arg_OUT_VAR_EXTRA_CODE_INNER_END} "${extra_code_inner_end}" PARENT_SCOPE) +endfunction() + +# Helper function to setup the fake checksum code snippet. +function(_qt_internal_sbom_setup_fake_checksum) + set(opt_args "") + set(single_args + OUT_VAR_FAKE_CHECKSUM_CODE + ) + set(multi_args "") + cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + # Allow skipping checksum computation for testing purposes, while installing just the sbom + # documents, without requiring to build and install all the actual files. + set(extra_code "") + if(QT_SBOM_FAKE_CHECKSUM) + string(APPEND extra_code " + set(QT_SBOM_FAKE_CHECKSUM TRUE)") + endif() + + set(${arg_OUT_VAR_FAKE_CHECKSUM_CODE} "${extra_code}" PARENT_SCOPE) +endfunction() + +# Helper function to setup the install-time SBOM generation code. +function(_qt_internal_sbom_setup_sbom_install_code) + set(opt_args "") + set(single_args + SBOM_FORMAT + REPO_PROJECT_NAME_LOWERCASE + + SBOM_INSTALL_OUTPUT_PATH + SBOM_INSTALL_OUTPUT_PATH_WITHOUT_EXT + SBOM_INSTALL_OUTPUT_DIR + + ASSEMBLE_SBOM_INCLUDE_PATH + + EXTRA_CODE_BEGIN + EXTRA_CODE_INNER_END + PROCESS_VERIFICATION_CODES + + BEFORE_CHECKSUM_INCLUDES + AFTER_CHECKSUM_INCLUDES + POST_GENERATION_INCLUDES + VERIFY_INCLUDES + ) + set(multi_args "") + cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + if(arg_SBOM_FORMAT STREQUAL "SPDX_V2") + set(suffix "_spdx") + _qt_internal_get_staging_area_spdx_file_path(staging_area_file) + set(final_message "Finalizing SBOM generation in install dir") + set(process_verification_codes " + include(\"${arg_PROCESS_VERIFICATION_CODES}\") +") + elseif(arg_SBOM_FORMAT STREQUAL "CYDX_V1_6") + set(suffix "_cydx") + set(final_message "Finalizing intermediate TOML generation in install dir") + + set(process_verification_codes "") + _qt_internal_get_staging_area_cydx_file_path(staging_area_file) + endif() + + set(assemble_sbom_install " + set(QT_SBOM_INSTALLED_ALL_CONFIGS${suffix} TRUE) + ${arg_EXTRA_CODE_BEGIN} + if(QT_SBOM_INSTALLED_ALL_CONFIGS${suffix}) + set(QT_SBOM_BUILD_TIME FALSE) + set(QT_SBOM_OUTPUT_DIR \"${arg_SBOM_INSTALL_OUTPUT_DIR}\") + set(QT_SBOM_OUTPUT_PATH \"${arg_SBOM_INSTALL_OUTPUT_PATH}\") + set(QT_SBOM_OUTPUT_PATH_WITHOUT_EXT \"${arg_SBOM_INSTALL_OUTPUT_PATH_WITHOUT_EXT}\") + file(MAKE_DIRECTORY \"${arg_SBOM_INSTALL_OUTPUT_DIR}\") + include(\"${arg_ASSEMBLE_SBOM_INCLUDE_PATH}\") + ${arg_BEFORE_CHECKSUM_INCLUDES} + ${process_verification_codes} + ${arg_AFTER_CHECKSUM_INCLUDES} + message(STATUS \"${final_message}: \${QT_SBOM_OUTPUT_PATH}\") + configure_file(\"${staging_area_file}\" \"\${QT_SBOM_OUTPUT_PATH}\") + ${arg_POST_GENERATION_INCLUDES} + ${arg_VERIFY_INCLUDES} + ${arg_EXTRA_CODE_INNER_END} + else() + message(STATUS \"Skipping SBOM finalization because not all configs were installed.\") + endif() +") + + install(CODE "${assemble_sbom_install}" COMPONENT sbom) + if(QT_SUPERBUILD) + install(CODE "${assemble_sbom_install}" COMPONENT "sbom_${arg_REPO_PROJECT_NAME_LOWERCASE}" + EXCLUDE_FROM_ALL) + endif() +endfunction() diff --git a/cmake/QtPublicSbomCycloneDXHelpers.cmake b/cmake/QtPublicSbomCycloneDXHelpers.cmake new file mode 100644 index 00000000000..a3dc52d4e39 --- /dev/null +++ b/cmake/QtPublicSbomCycloneDXHelpers.cmake @@ -0,0 +1,382 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +# Gets the helper python script name and relative dir in the source dir. +function(_qt_internal_sbom_get_cyclone_dx_generator_script_name + out_var_generator_name + out_var_generator_relative_dir) + set(generator_name "qt_cyclonedx_generator.py") + + _qt_internal_path_join(generator_relative_dir + "util" "sbom" "cyclonedx" "qt_cyclonedx_generator") + + set(${out_var_generator_name} "${generator_name}" PARENT_SCOPE) + set(${out_var_generator_relative_dir} "${generator_relative_dir}" PARENT_SCOPE) +endfunction() + +# Ges the path to the helper python script, which should be used to generate CycloneDX document. +# Prefers the source path over the installed path, for easier development of the script. +function(_qt_internal_sbom_get_cyclone_dx_generator_path out_var) + _qt_internal_sbom_get_cyclone_dx_generator_script_name(generator_name generator_relative_dir) + + _qt_internal_path_join(qtbase_script_path + "${QT_SOURCE_TREE}" "${generator_relative_dir}" "${generator_name}") + _qt_internal_path_join(installed_script_path + "${QT6_INSTALL_PREFIX}" "${QT6_INSTALL_LIBEXECS}" "${generator_name}") + + # qtbase sources available, always use them, regardless if it's a prefix or non-prefix build. + # Makes development easier. + if(EXISTS "${qtbase_script_path}") + set(script_path "${qtbase_script_path}") + + # qtbase sources unavailable, use installed files. + elseif(EXISTS "${installed_script_path}") + set(script_path "${installed_script_path}") + else() + message(FATAL_ERROR "Can't find ${generator_name} file.") + endif() + + set(${out_var} "${script_path}" PARENT_SCOPE) +endfunction() + +# Parses the options for a single CYDX_PROPERTY_ENTRY, and creates a toml snippet to add a +# CycloneDX property to the final toml document. +function(_qt_internal_sbom_parse_cydx_property_entry_options) + set(opt_args "") + set(single_args + CYDX_PROPERTY_NAME + CYDX_PROPERTY_VALUE + OUT_VAR + ) + set(multi_args "") + cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + if(NOT arg_CYDX_PROPERTY_NAME) + message(FATAL_ERROR "CYDX_PROPERTY_NAME is required.") + endif() + + if(NOT arg_CYDX_PROPERTY_VALUE) + message(FATAL_ERROR "CYDX_PROPERTY_VALUE is required.") + endif() + + if(NOT arg_OUT_VAR) + message(FATAL_ERROR "OUT_VAR is required.") + endif() + + set(${arg_OUT_VAR} " +[[components.properties]] +name = \\\"${arg_CYDX_PROPERTY_NAME}\\\" +value = \\\"${arg_CYDX_PROPERTY_VALUE}\\\" +" PARENT_SCOPE) +endfunction() + +# Processes a list of CycloneDX property entries, and creates their toml representation as output. +function(_qt_internal_sbom_handle_cydx_properties) + set(opt_args "") + set(single_args + OUT_VAR_CYDX_PROPERTIES_STRING + ) + set(multi_args + CYDX_PROPERTIES + ) + cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + if(NOT arg_OUT_VAR_CYDX_PROPERTIES_STRING) + message(FATAL_ERROR "OUT_VAR_CYDX_PROPERTIES_STRING is required.") + endif() + + # Collect each CYDX_PROPERTY_ENTRY args into a separate variable. + set(prop_idx -1) + set(prop_entry_indices "") + + foreach(prop_arg IN LISTS arg_CYDX_PROPERTIES) + if(prop_arg STREQUAL "CYDX_PROPERTY_ENTRY") + math(EXPR prop_idx "${prop_idx}+1") + list(APPEND prop_entry_indices "${prop_idx}") + elseif(prop_idx GREATER_EQUAL 0) + list(APPEND prop_${prop_idx}_args "${prop_arg}") + else() + message(FATAL_ERROR "Missing CYDX_PROPERTY_ENTRY keyword.") + endif() + endforeach() + + set(properties_string "") + + foreach(prop_idx IN LISTS prop_entry_indices) + _qt_internal_sbom_parse_cydx_property_entry_options( + ${prop_${prop_idx}_args} + OUT_VAR property_tuple + ) + + string(APPEND properties_string "${property_tuple}") + endforeach() + + set(${arg_OUT_VAR_CYDX_PROPERTIES_STRING} "${properties_string}" PARENT_SCOPE) +endfunction() + +# Outputs extra Cyclone DX properties based on the sbom entity type. +function(_qt_internal_sbom_handle_qt_entity_cydx_properties) + set(opt_args "") + set(single_args + SBOM_ENTITY_TYPE + OUT_CYDX_PROPERTIES + ) + set(multi_args "") + cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + if(NOT arg_SBOM_ENTITY_TYPE) + message(FATAL_ERROR "SBOM_ENTITY_TYPE is required.") + endif() + + if(NOT arg_OUT_CYDX_PROPERTIES) + message(FATAL_ERROR "OUT_CYDX_PROPERTIES is required.") + endif() + + set(cydx_properties "") + list(APPEND cydx_properties + CYDX_PROPERTY_ENTRY + CYDX_PROPERTY_NAME "qt:sbom:entity_type" + CYDX_PROPERTY_VALUE "${arg_SBOM_ENTITY_TYPE}" + ) + + _qt_internal_sbom_is_qt_entity_type("${arg_SBOM_ENTITY_TYPE}" is_qt_entity_type) + if(is_qt_entity_type) + list(APPEND cydx_properties + CYDX_PROPERTY_ENTRY + CYDX_PROPERTY_NAME "qt:sbom:is_qt_entity_type" + CYDX_PROPERTY_VALUE "true" + ) + endif() + _qt_internal_sbom_is_qt_3rd_party_entity_type("${arg_SBOM_ENTITY_TYPE}" + is_qt_3rd_party_entity_type) + if(is_qt_3rd_party_entity_type) + list(APPEND cydx_properties + CYDX_PROPERTY_ENTRY + CYDX_PROPERTY_NAME "qt:sbom:is_qt_3rd_party_entity_type" + CYDX_PROPERTY_VALUE "true" + ) + endif() + + set(${arg_OUT_CYDX_PROPERTIES} "${cydx_properties}" PARENT_SCOPE) +endfunction() + +# Maps an sbom entity type to a cyclone dx component type. +function(_qt_internal_sbom_get_cyclone_component_type out_var sbom_entity_type) + set(library_types + "QT_MODULE" + "QT_PLUGIN" + "QML_PLUGIN" + "QT_THIRD_PARTY_MODULE" + "QT_THIRD_PARTY_SOURCES" + "SYSTEM_LIBRARY" + "LIBRARY" + "THIRD_PARTY_LIBRARY" + "THIRD_PARTY_LIBRARY_WITH_FILES" + "THIRD_PARTY_SOURCES" + ) + + set(application_types + "QT_TOOL" + "QT_APP" + "EXECUTABLE" + ) + + if(sbom_entity_type IN_LIST library_types) + set(component_type "library") + elseif(sbom_entity_type IN_LIST application_types) + set(component_type "application") + else() + # Default to library for now, because it's unclear what would be a better default. + set(component_type "library") + endif() + + set(${out_var} "${component_type}" PARENT_SCOPE) +endfunction() + +# Generates a pseudo-unique serial number for a CycloneDX sbom document. +# +# The spec says that a BOM serial number must conform to RFC 4122, but doesn't specify which +# kind of uuid version should be generated. +# The upstream python library generates a version 4 uuid, which is fully random. +# CMake can only generate version 3 and 5 uuids, which are fully deterministic based on the given +# NAMESPACE and NAME values. +# Generating a fully random uuid prevents build reproducibility. The maintainer of the Cyclone DX +# spec even mentions that here: +# https://github.com/CycloneDX/specification/issues/97#issuecomment-955904904 +# And yet to to do component-wise inter-document linking using the BOM-Link mechanism, you have to +# use serial numbers. +# +# Because the spec doesn't explicitly prohibit it, we will generate a version 5 uuid based on the +# SPDX_NAMESPACE passed to the function, which is supposed to be unique enough, because it contains +# the project / document name (e.g. qtbase) and its version or git version. +# This should alleviate the reproducibility problem as well. +function(_qt_internal_sbom_get_cyclone_bom_serial_number) + set(opt_args "") + set(single_args + SPDX_NAMESPACE + OUT_VAR_UUID + OUT_VAR_SERIAL_NUMBER + ) + set(multi_args "") + cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + _qt_internal_sbom_set_default_option_value_and_error_if_empty(SPDX_NAMESPACE "") + + # This is a randomly generated uuid v4 value. To be used for all eternity. Until we change the + # implementation of the function. + set(uuid_namespace "c024642f-9853-45b2-9bfd-ab3f061a05bb") + + string(UUID uuid NAMESPACE "${uuid_namespace}" NAME "${arg_SPDX_NAMESPACE}" TYPE SHA1) + set(cyclone_dx_serial_number "urn:cdx:${uuid}") + + if(arg_OUT_VAR_UUID) + set("${arg_OUT_VAR_UUID}" "${uuid}" PARENT_SCOPE) + endif() + if(arg_OUT_VAR_SERIAL_NUMBER) + set("${arg_OUT_VAR_SERIAL_NUMBER}" "${cyclone_dx_serial_number}" PARENT_SCOPE) + endif() +endfunction() + +# See https://github.com/CycloneDX/guides/blob/main/SBOM/en/0x52-Linking.md +function(_qt_internal_sbom_get_cydx_external_bom_link target out_var) + get_target_property(spdx_id "${target}" _qt_sbom_spdx_id) + get_target_property(bom_serial_number "${target}" _qt_sbom_cydx_bom_serial_number_uuid) + + set(bom_version "1") + set(bom_link "urn:cdx:${bom_serial_number}/${bom_version}#${spdx_id}") + + set(${out_var} "${bom_link}" PARENT_SCOPE) +endfunction() + +# Records necessary details of external target dependencies in global properties, to later create +# the CycloneDX packages for them. The info collection needs to be done immediately in the directory +# scope where the targets were found, because they might not be global, and thus can't be accessed +# later. +function(_qt_internal_sbom_record_external_target_dependecies) + set(opt_args "") + set(single_args "") + set(multi_args + TARGETS + ) + cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + if(NOT arg_TARGETS) + return() + endif() + + get_property(existing_ids GLOBAL PROPERTY _qt_internal_sbom_external_target_dep_ids) + if(NOT existing_ids) + set(existing_ids "") + endif() + + foreach(target IN LISTS arg_TARGETS) + # Use the full spdx id (one prefixed with the containing DocumentRef-) because that's what + # our spdx dependency relationships use at the moment. + # Both Foo and FooPrivate map to the same spdx_id, so we need to avoid duplicates on spdx id + # level. + get_target_property(spdx_id "${target}" _qt_sbom_spdx_id) + + if(spdx_id IN_LIST existing_ids) + continue() + endif() + + list(APPEND existing_ids "${spdx_id}") + set_property(GLOBAL APPEND PROPERTY _qt_internal_sbom_external_target_dep_ids "${spdx_id}") + + # This is checked in _qt_internal_sbom_add_target, to prevent duplicate creation of + # system library targets. + set_property(GLOBAL APPEND PROPERTY _qt_internal_sbom_external_target_dependencies + "${target}") + + get_target_property(package_name "${target}" _qt_sbom_package_name) + get_target_property(sbom_entity_type "${target}" _qt_sbom_entity_type) + get_target_property(package_version "${target}" _qt_sbom_package_version) + _qt_internal_sbom_get_cydx_external_bom_link("${target}" external_bom_link) + + set_property(GLOBAL + PROPERTY "_qt_internal_sbom_external_target_dep_${spdx_id}_target" + "${target}") + set_property(GLOBAL + PROPERTY "_qt_internal_sbom_external_target_dep_${spdx_id}_package_name" + "${package_name}") + set_property(GLOBAL + PROPERTY "_qt_internal_sbom_external_target_dep_${spdx_id}_sbom_entity_type" + "${sbom_entity_type}") + set_property(GLOBAL + PROPERTY "_qt_internal_sbom_external_target_dep_${spdx_id}_package_version" + "${package_version}") + set_property(GLOBAL + PROPERTY "_qt_internal_sbom_external_target_dep_${spdx_id}_external_bom_link" + "${external_bom_link}") + endforeach() +endfunction() + +# Goes through the list of recorded external target dependencies collected during target +# dependency analysis, and adds them as CycloneDX packages to the CycloneDX document. +# This is different from SPDX v2.3, which doesn't require creating a package for dependencies that +# are defined in a different document. +function(_qt_internal_sbom_add_cydx_external_target_dependencies) + get_property(spdx_ids GLOBAL PROPERTY _qt_internal_sbom_external_target_dep_ids) + if(NOT spdx_ids) + # Clean up external target dependencies, before configuring next repo project. + set_property(GLOBAL PROPERTY _qt_internal_sbom_external_target_dep_ids "") + set_property(GLOBAL PROPERTY _qt_internal_sbom_external_target_dependencies "") + return() + endif() + + # Just in case, don't add duplicates. + set(visited_spdx_ids "") + + foreach(spdx_id IN LISTS spdx_ids) + if(spdx_id IN_LIST visited_spdx_ids) + continue() + endif() + + get_cmake_property(package_name + "_qt_internal_sbom_external_target_dep_${spdx_id}_package_name") + get_cmake_property(sbom_entity_type + "_qt_internal_sbom_external_target_dep_${spdx_id}_sbom_entity_type") + get_cmake_property(package_version + "_qt_internal_sbom_external_target_dep_${spdx_id}_package_version") + get_cmake_property(external_bom_link + "_qt_internal_sbom_external_target_dep_${spdx_id}_external_bom_link") + + _qt_internal_sbom_generate_cyclone_add_package( + PACKAGE "${package_name}" + SPDXID "${spdx_id}" + SBOM_ENTITY_TYPE "${sbom_entity_type}" + VERSION "${package_version}" + EXTERNAL_BOM_LINK "${external_bom_link}" + ) + + list(APPEND visited_spdx_ids "${spdx_id}") + endforeach() + + # Clean up external target dependencies, before configuring next repo project. + set_property(GLOBAL PROPERTY _qt_internal_sbom_external_target_dep_ids "") + set_property(GLOBAL PROPERTY _qt_internal_sbom_external_target_dependencies "") +endfunction() + +# Records a license id and its text in global properties, to be added to the CycloneDX document +# later. +function(_qt_internal_sbom_record_license_cydx) + set(opt_args "") + set(single_args + LICENSE_ID + EXTRACTED_TEXT + ) + set(multi_args "") + cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + set_property(GLOBAL APPEND PROPERTY + _qt_internal_sbom_cydx_licenses "${arg_LICENSE_ID}") + set_property(GLOBAL PROPERTY + _qt_internal_sbom_cydx_licenses_${arg_LICENSE_ID}_text "${arg_EXTRACTED_TEXT}" + ) +endfunction() diff --git a/cmake/QtPublicSbomDepHelpers.cmake b/cmake/QtPublicSbomDepHelpers.cmake index 32e3b6b832c..794f3f77db2 100644 --- a/cmake/QtPublicSbomDepHelpers.cmake +++ b/cmake/QtPublicSbomDepHelpers.cmake @@ -4,11 +4,17 @@ # Walks a target's direct dependencies and assembles a list of relationships between the packages # of the target dependencies. # Currently handles various Qt targets and system libraries. +# For SPDX documents, it collects the relationships in OUT_SPDX_RELATIONSHIPS. +# For CYDX documents, it collects the dependencies (not relationship info) in OUT_CYDX_DEPENDENCIES. +# If a CYDX dependency is in an external docuemnt, the dependency is added to +# OUT_EXTERNAL_TARGET_DEPENDENCIES instead. function(_qt_internal_sbom_handle_target_dependencies target) set(opt_args "") set(single_args SPDX_ID - OUT_RELATIONSHIPS + OUT_CYDX_DEPENDENCIES + OUT_SPDX_RELATIONSHIPS + OUT_EXTERNAL_TARGET_DEPENDENCIES ) set(multi_args LIBRARIES @@ -75,8 +81,12 @@ function(_qt_internal_sbom_handle_target_dependencies target) set(all_direct_libraries ${libraries} ${public_libraries} ${sbom_dependencies}) list(REMOVE_DUPLICATES all_direct_libraries) - set(spdx_dependencies "") + set(regular_cydx_dependencies "") + set(regular_spdx_dependencies "") + + set(external_cydx_dependencies "") set(external_spdx_dependencies "") + set(external_target_dependencies "") # Go through each direct linked lib. foreach(direct_lib IN LISTS all_direct_libraries) @@ -108,7 +118,8 @@ function(_qt_internal_sbom_handle_target_dependencies target) # Add a dependency on the vendored lib instead of the Wrap target. if(is_3rdparty_bundled_lib AND lib_spdx_id) - list(APPEND spdx_dependencies "${lib_spdx_id}") + list(APPEND regular_cydx_dependencies "${lib_spdx_id}") + list(APPEND regular_spdx_dependencies "${lib_spdx_id}") set(bundled_targets_found TRUE) endif() endforeach() @@ -148,30 +159,38 @@ function(_qt_internal_sbom_handle_target_dependencies target) if(NOT is_dependency_in_external_document) # If the target is not in the external document, it must be one built as part of the # current project. - list(APPEND spdx_dependencies "${lib_spdx_id}") + list(APPEND regular_cydx_dependencies "${lib_spdx_id}") + list(APPEND regular_spdx_dependencies "${lib_spdx_id}") else() # Refer to the package in the external document. This can be the case # in a top-level build, where a system library is reused across repos, or for any # regular dependency that was built as part of a different project. _qt_internal_sbom_add_external_target_dependency("${direct_lib}" - extra_spdx_dependencies + OUT_CYDX_DEPENDENCIES extra_cydx_dependencies + OUT_SPDX_DEPENDENCIES extra_spdx_dependencies + OUT_TARGET_DEPENDENCIES extra_target_dependencies ) if(extra_spdx_dependencies) + list(APPEND external_cydx_dependencies ${extra_cydx_dependencies}) list(APPEND external_spdx_dependencies ${extra_spdx_dependencies}) + list(APPEND external_target_dependencies ${extra_target_dependencies}) endif() endif() endforeach() - set(relationships "") + set(spdx_relationships "") + # Keep the external dependencies first, so they are neatly ordered. - foreach(dep_spdx_id IN LISTS external_spdx_dependencies spdx_dependencies) - set(relationship - "${package_spdx_id} DEPENDS_ON ${dep_spdx_id}" - ) - list(APPEND relationships "${relationship}") + foreach(dep_spdx_id IN LISTS external_spdx_dependencies regular_spdx_dependencies) + set(relationship "${package_spdx_id} DEPENDS_ON ${dep_spdx_id}") + list(APPEND spdx_relationships "${relationship}") endforeach() - set(${arg_OUT_RELATIONSHIPS} "${relationships}" PARENT_SCOPE) + set(all_cydx_dependencies ${external_cydx_dependencies} ${regular_cydx_dependencies}) + + set(${arg_OUT_CYDX_DEPENDENCIES} "${all_cydx_dependencies}" PARENT_SCOPE) + set(${arg_OUT_SPDX_RELATIONSHIPS} "${spdx_relationships}" PARENT_SCOPE) + set(${arg_OUT_EXTERNAL_TARGET_DEPENDENCIES} "${external_target_dependencies}" PARENT_SCOPE) endfunction() # Checks whether the current target will have its sbom generated into the current repo sbom @@ -205,18 +224,34 @@ function(_qt_internal_sbom_is_external_target_dependency target) endfunction() # Handles generating an external document reference SDPX element for each target package that is -# located in a different spdx document. -function(_qt_internal_sbom_add_external_target_dependency target out_spdx_dependencies) +# located in a different spdx document, and collects the reference in OUT_SPDX_DEPENDENCIES. +# In case of CycloneDX, we just collect the dependency spdx id (bom-ref) and the target name, +# which are added to OUT_CYDX_DEPENDENCIES and OUT_TARGET_DEPENDENCIES respectively. +function(_qt_internal_sbom_add_external_target_dependency target) + set(opt_args "") + set(single_args + OUT_CYDX_DEPENDENCIES + OUT_SPDX_DEPENDENCIES + OUT_TARGET_DEPENDENCIES + ) + set(multi_args "") + cmake_parse_arguments(PARSE_ARGV 1 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + _qt_internal_sbom_get_spdx_id_for_target("${target}" dep_spdx_id) if(NOT dep_spdx_id) message(DEBUG "Could not add external target dependency on ${target} " "because no spdx id could be found") - set(${out_spdx_dependencies} "" PARENT_SCOPE) + set(${arg_OUT_CYDX_DEPENDENCIES} "" PARENT_SCOPE) + set(${arg_OUT_SPDX_DEPENDENCIES} "" PARENT_SCOPE) + set(${arg_OUT_TARGET_DEPENDENCIES} "" PARENT_SCOPE) return() endif() + set(cydx_dependencies "") set(spdx_dependencies "") + set(target_depdendencies "") # Get the external document path and the repo it belongs to for the given target. get_property(relative_installed_repo_document_path TARGET ${target} @@ -232,9 +267,11 @@ function(_qt_internal_sbom_add_external_target_dependency target out_spdx_depend get_cmake_property(known_external_document _qt_known_external_documents_${external_document_ref}) - set(dependency "${external_document_ref}:${dep_spdx_id}") + set(spdx_dependency "${external_document_ref}:${dep_spdx_id}") - list(APPEND spdx_dependencies "${dependency}") + list(APPEND cydx_dependencies "${dep_spdx_id}") + list(APPEND spdx_dependencies "${spdx_dependency}") + list(APPEND target_depdendencies "${target}") # Only add a reference to the external document package, if we haven't done so already. if(NOT known_external_document) @@ -261,5 +298,7 @@ function(_qt_internal_sbom_add_external_target_dependency target out_spdx_depend "Missing spdx document path for external target dependency: ${target}") endif() - set(${out_spdx_dependencies} "${spdx_dependencies}" PARENT_SCOPE) + set(${arg_OUT_CYDX_DEPENDENCIES} "${cydx_dependencies}" PARENT_SCOPE) + set(${arg_OUT_SPDX_DEPENDENCIES} "${spdx_dependencies}" PARENT_SCOPE) + set(${arg_OUT_TARGET_DEPENDENCIES} "${target_depdendencies}" PARENT_SCOPE) endfunction() diff --git a/cmake/QtPublicSbomGenerationCycloneDXHelpers.cmake b/cmake/QtPublicSbomGenerationCycloneDXHelpers.cmake new file mode 100644 index 00000000000..59797d24b7d --- /dev/null +++ b/cmake/QtPublicSbomGenerationCycloneDXHelpers.cmake @@ -0,0 +1,425 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +# Helper to return the path to staging cydx file, where content will be incrementally appended to. +function(_qt_internal_get_staging_area_cydx_file_path out_var) + _qt_internal_get_current_project_sbom_dir(sbom_dir) + _qt_internal_sbom_get_root_project_name_lower_case(repo_project_name_lowercase) + set(staging_area_cydx_file "${sbom_dir}/staging-${repo_project_name_lowercase}.cdx.in.toml") + set(${out_var} "${staging_area_cydx_file}" PARENT_SCOPE) +endfunction() + +# Starts recording information for the generation of a CycloneDX sbom for a project. +# Similar to _qt_internal_sbom_begin_project_generate which is the SPDX variant. +function(_qt_internal_sbom_begin_project_generate_cyclone) + set(opt_args "") + set(single_args + OUTPUT + OUTPUT_RELATIVE_PATH + LICENSE + COPYRIGHT + DOWNLOAD_LOCATION + PROJECT + PROJECT_COMMENT + PROJECT_FOR_SPDX_ID + SUPPLIER + SUPPLIER_URL + NAMESPACE + BOM_SERIAL_NUMBER_UUID + CPE + OUT_VAR_PROJECT_SPDX_ID + ) + set(multi_args "") + cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + _qt_internal_forward_function_args( + FORWARD_PREFIX arg + FORWARD_OUT_VAR common_project_args + FORWARD_SINGLE + ${single_args} + ) + + _qt_internal_sbom_get_common_project_variables( + ${common_project_args} + OUT_VAR_PROJECT_NAME arg_PROJECT + OUT_VAR_CURRENT_UTC current_utc + OUT_VAR_CURRENT_YEAR current_year + DEFAULT_SBOM_FILE_NAME_EXTENSION "cdx" + OUT_VAR_OUTPUT arg_OUTPUT + OUT_VAR_OUTPUT_RELATIVE_PATH arg_OUTPUT_RELATIVE_PATH + OUT_VAR_PROJECT_FOR_SPDX_ID project_spdx_id + OUT_VAR_COPYRIGHT arg_COPYRIGHT + OUT_VAR_SUPPLIER arg_SUPPLIER + OUT_VAR_SUPPLIER_URL arg_SUPPLIER_URL + OUT_VAR_DEFAULT_PROJECT_COMMENT project_comment + ) + if(arg_OUT_VAR_PROJECT_SPDX_ID) + set(${arg_OUT_VAR_PROJECT_SPDX_ID} "${project_spdx_id}" PARENT_SCOPE) + endif() + + _qt_internal_sbom_get_git_version_vars() + + _qt_internal_sbom_set_default_option_value_and_error_if_empty(BOM_SERIAL_NUMBER_UUID "") + + if(arg_PROJECT_COMMENT) + string(APPEND project_comment "${arg_PROJECT_COMMENT}") + endif() + + _qt_internal_sbom_set_default_option_value(DOWNLOAD_LOCATION "") + set(download_location_field "") + if(arg_DOWNLOAD_LOCATION) + set(download_location_field "download_location = \"${arg_DOWNLOAD_LOCATION}\"") + endif() + + set(content " +[root_component] +name = \"${arg_PROJECT}\" # Required +spdx_id = \"${project_spdx_id}\" # Required +version = \"${QT_SBOM_GIT_VERSION}\" # Required +supplier = '${arg_SUPPLIER}' # Required +supplier_url = \"${arg_SUPPLIER_URL}\" # Required +${download_location_field} +build_date = \"${current_utc}\" +serial_number_uuid = \"${arg_BOM_SERIAL_NUMBER_UUID}\" # Required +description = '''${project_comment} +''' + +[[project_build_tools]] +name = \"cmake\" +version = \"${CMAKE_VERSION}\" +component_type = \"application\" +description = \"Build system tool used to build the project.\" +") + + _qt_internal_sbom_get_root_project_name_lower_case(repo_project_name_lowercase) + _qt_internal_sbom_create_sbom_staging_file( + CONTENT "${content}" + SBOM_FORMAT "CYDX_V1_6" + REPO_PROJECT_NAME_LOWERCASE "${repo_project_name_lowercase}" + OUT_VAR_CREATE_STAGING_FILE create_staging_file + OUT_VAR_SBOM_DIR sbom_dir + ) + + _qt_internal_sbom_save_project_info_in_global_properties( + SUPPLIER "${arg_SUPPLIER}" + SUPPLIER_URL "${arg_SUPPLIER_URL}" + NAMESPACE "${arg_NAMESPACE}" + PROJECT "${arg_PROJECT}" + PROJECT_SPDX_ID "${project_spdx_id}" + ) + + _qt_internal_sbom_save_common_path_variables_in_global_properties( + OUTPUT "${arg_OUTPUT}" + OUTPUT_RELATIVE_PATH "${arg_OUTPUT_RELATIVE_PATH}" + SBOM_DIR "${sbom_dir}" + PROPERTY_SUFFIX _cydx + ) + + set_property(GLOBAL APPEND PROPERTY _qt_sbom_cmake_include_files_cydx "${create_staging_file}") +endfunction() + +# Finalizes the CycloneDX sbom generation for a project. +function(_qt_internal_sbom_end_project_generate_cyclone) + _qt_internal_sbom_get_common_path_variables_from_global_properties( + SBOM_FORMAT "CYDX_V1_6" + OUT_VAR_SBOM_BUILD_OUTPUT_PATH sbom_build_output_path + OUT_VAR_SBOM_BUILD_OUTPUT_PATH_WITHOUT_EXT sbom_build_output_path_without_ext + OUT_VAR_SBOM_BUILD_OUTPUT_DIR sbom_build_output_dir + OUT_VAR_SBOM_INSTALL_OUTPUT_PATH sbom_install_output_path + OUT_VAR_SBOM_INSTALL_OUTPUT_PATH_WITHOUT_EXT sbom_install_output_path_without_ext + OUT_VAR_SBOM_INSTALL_OUTPUT_DIR sbom_install_output_dir + ) + + if(NOT sbom_build_output_path) + message(FATAL_ERROR "Call _qt_internal_sbom_begin_project() first") + endif() + + _qt_internal_sbom_get_root_project_name_lower_case(repo_project_name_lowercase) + _qt_internal_sbom_get_qt_repo_project_name_lower_case(real_qt_repo_project_name_lowercase) + + # Process licenses before getting the includes. + _qt_internal_sbom_add_recorded_licenses_cydx() + + _qt_internal_sbom_get_cmake_include_files( + SBOM_FORMAT "CYDX_V1_6" + OUT_VAR_INCLUDES includes + OUT_VAR_POST_GENERATION_INCLUDES post_generation_includes + ) + + _qt_internal_get_current_project_sbom_dir(sbom_dir) + + set(build_time_args "") + if(includes) + list(APPEND build_time_args INCLUDES "${includes}") + endif() + if(post_generation_includes) + list(APPEND build_time_args POST_GENERATION_INCLUDES "${post_generation_includes}") + endif() + _qt_internal_sbom_create_build_time_sbom_targets( + SBOM_FORMAT "CYDX_V1_6" + REPO_PROJECT_NAME_LOWERCASE "${repo_project_name_lowercase}" + REAL_QT_REPO_PROJECT_NAME_LOWERCASE "${real_qt_repo_project_name_lowercase}" + SBOM_BUILD_OUTPUT_PATH "${sbom_build_output_path}" + SBOM_BUILD_OUTPUT_PATH_WITHOUT_EXT "${sbom_build_output_path_without_ext}" + SBOM_BUILD_OUTPUT_DIR "${sbom_build_output_dir}" + OUT_VAR_ASSEMBLE_SBOM_INCLUDE_PATH assemble_sbom + ${build_time_args} + ) + + _qt_internal_sbom_setup_multi_config_install_markers( + SBOM_DIR "${sbom_dir}" + SBOM_FORMAT "CYDX_V1_6" + REPO_PROJECT_NAME_LOWERCASE "${repo_project_name_lowercase}" + OUT_VAR_EXTRA_CODE_BEGIN extra_code_begin + OUT_VAR_EXTRA_CODE_INNER_END extra_code_inner_end + ) + + _qt_internal_sbom_setup_fake_checksum( + OUT_VAR_FAKE_CHECKSUM_CODE extra_code_begin_fake_checksum + ) + if(extra_code_begin_fake_checksum) + string(APPEND extra_code_begin "${extra_code_begin_fake_checksum}") + endif() + + set(setup_sbom_install_args "") + if(extra_code_begin) + list(APPEND setup_sbom_install_args EXTRA_CODE_BEGIN "${extra_code_begin}") + endif() + if(extra_code_inner_end) + list(APPEND setup_sbom_install_args EXTRA_CODE_INNER_END "${extra_code_inner_end}") + endif() + if(before_checksum_includes) + list(APPEND setup_sbom_install_args BEFORE_CHECKSUM_INCLUDES "${before_checksum_includes}") + endif() + if(after_checksum_includes) + list(APPEND setup_sbom_install_args AFTER_CHECKSUM_INCLUDES "${after_checksum_includes}") + endif() + if(post_generation_includes) + list(APPEND setup_sbom_install_args POST_GENERATION_INCLUDES "${post_generation_includes}") + endif() + if(verify_includes) + list(APPEND setup_sbom_install_args VERIFY_INCLUDES "${verify_includes}") + endif() + + _qt_internal_sbom_setup_sbom_install_code( + SBOM_FORMAT "CYDX_V1_6" + REPO_PROJECT_NAME_LOWERCASE "${repo_project_name_lowercase}" + SBOM_INSTALL_OUTPUT_PATH "${sbom_install_output_path}" + SBOM_INSTALL_OUTPUT_PATH_WITHOUT_EXT "${sbom_install_output_path_without_ext}" + SBOM_INSTALL_OUTPUT_DIR "${sbom_install_output_dir}" + ASSEMBLE_SBOM_INCLUDE_PATH "${assemble_sbom}" + ${setup_sbom_install_args} + ) + + _qt_internal_sbom_clear_cmake_include_files( + SBOM_FORMAT "CYDX_V1_6" + ) +endfunction() + +# Helper to add info about a package to the sbom. Usually a package is a mapping to a cmake target. +function(_qt_internal_sbom_generate_cyclone_add_package) + set(opt_args "") + set(single_args + PACKAGE + VERSION + LICENSE_DECLARED + LICENSE_CONCLUDED + COPYRIGHT + DOWNLOAD_LOCATION + SPDXID + COMMENT + + # Additions compared to spdx function signature. + CYDX_SUPPLIER + SBOM_ENTITY_TYPE + CONTAINING_COMPONENT + EXTERNAL_BOM_LINK + ) + set(multi_args + CPE + + # Additions compared to spdx function signature. + PURL_VALUES + DEPENDENCIES + CYDX_PROPERTIES + ) + cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + _qt_internal_sbom_set_default_option_value_and_error_if_empty(PACKAGE "") + + set(check_option "") + if(arg_SPDXID) + set(check_option "CHECK" "${arg_SPDXID}") + endif() + + _qt_internal_sbom_get_and_check_spdx_id( + VARIABLE arg_SPDXID + ${check_option} + HINTS "SPDXRef-${arg_PACKAGE}" + ) + + _qt_internal_sbom_set_default_option_value(DOWNLOAD_LOCATION "") + _qt_internal_sbom_set_default_option_value(VERSION "") + _qt_internal_sbom_set_default_option_value(SUPPLIER "") + _qt_internal_sbom_set_default_option_value(LICENSE_CONCLUDED "") + _qt_internal_sbom_set_default_option_value(COPYRIGHT "") + + set(cpe_field "") + set(cpe_list ${arg_CPE}) + if(cpe_list) + # Wrap values in double quotes. + list(TRANSFORM cpe_list PREPEND "\\\"") + list(TRANSFORM cpe_list APPEND "\\\"") + list(JOIN cpe_list ", " cpe_string) + set(cpe_field "cpe_list = [${cpe_string}]") + endif() + + set(purl_field "") + set(purl_list ${arg_PURL_VALUES}) + if(purl_list) + # Wrap values in double quotes. + list(TRANSFORM purl_list PREPEND "\\\"") + list(TRANSFORM purl_list APPEND "\\\"") + list(JOIN purl_list ", " purl_string) + set(purl_field "purl_list = [${purl_string}]") + endif() + + set(name_field "name = \\\"${arg_PACKAGE}\\\"") + set(spdx_id_field "spdx_id = \\\"${arg_SPDXID}\\\"") + set(sbom_entity_type_field "sbom_entity_type = \\\"${arg_SBOM_ENTITY_TYPE}\\\"") + + _qt_internal_sbom_get_cyclone_component_type(component_type "${arg_SBOM_ENTITY_TYPE}") + set(component_type_field "component_type = \\\"${component_type}\\\"") + + set(version_field "") + if(arg_VERSION) + set(version_field "version = \\\"${arg_VERSION}\\\"") + endif() + + set(dependency_field "") + set(dependency_list ${arg_DEPENDENCIES}) + if(dependency_list) + # Wrap values in double quotes. + list(TRANSFORM dependency_list PREPEND "\\\"") + list(TRANSFORM dependency_list APPEND "\\\"") + list(JOIN dependency_list ", " dependency_string) + set(dependency_field "dependencies = [${dependency_string}]") + endif() + + set(copyright_field "") + if(arg_COPYRIGHT) + set(copyright_field "copyright = '''${arg_COPYRIGHT}'''") + endif() + + set(download_location_field "") + if(arg_DOWNLOAD_LOCATION) + set(download_location_field "download_location = \\\"${arg_DOWNLOAD_LOCATION}\\\"") + endif() + + set(license_concluded_field "") + if(arg_LICENSE_CONCLUDED) + set(license_concluded_field + "license_concluded_expression = \\\"${arg_LICENSE_CONCLUDED}\\\"") + endif() + + set(supplier_field "") + if(arg_CYDX_SUPPLIER) + set(supplier_field "supplier = '${arg_CYDX_SUPPLIER}'") + endif() + + set(containing_component_field "") + if(arg_CONTAINING_COMPONENT) + set(containing_component_field "containing_component = \\\"${arg_CONTAINING_COMPONENT}\\\"") + endif() + + set(external_bom_link_field "") + if(arg_EXTERNAL_BOM_LINK) + set(external_bom_link_field "external_bom_link = \\\"${arg_EXTERNAL_BOM_LINK}\\\"") + endif() + + set(properties_field "") + if(arg_CYDX_PROPERTIES) + _qt_internal_sbom_handle_cydx_properties( + CYDX_PROPERTIES ${arg_CYDX_PROPERTIES} + OUT_VAR_CYDX_PROPERTIES_STRING properties_field + ) + endif() + + _qt_internal_get_staging_area_cydx_file_path(staging_area_spdx_file) + + set(content " +file(APPEND \"${staging_area_spdx_file}\" +\" + +[[components]] +${name_field} +${spdx_id_field} +${sbom_entity_type_field} +${component_type_field} +${version_field} +${download_location_field} +${copyright_field} +${supplier_field} +${containing_component_field} +${external_bom_link_field} +${cpe_field} +${purl_field} +${license_concluded_field} +${dependency_field} +${properties_field} +\" +) +") + + _qt_internal_get_current_project_sbom_dir(sbom_dir) + set(package_sbom "${sbom_dir}/cydx_${arg_SPDXID}.cmake") + file(GENERATE OUTPUT "${package_sbom}" CONTENT "${content}") + + set_property(GLOBAL APPEND PROPERTY _qt_sbom_cmake_include_files_cydx "${package_sbom}") +endfunction() + +# Adds all recorded custom licenses to the toml file. +function(_qt_internal_sbom_add_recorded_licenses_cydx) + get_property(license_ids GLOBAL PROPERTY _qt_internal_sbom_cydx_licenses) + if(NOT license_ids) + return() + endif() + + set(content "") + + foreach(license_id IN LISTS license_ids) + get_property(license_text GLOBAL PROPERTY + _qt_internal_sbom_cydx_licenses_${license_id}_text) + + string(APPEND content " +[[licenses]] +license_id = \\\"${license_id}\\\" +text = '''${license_text} +''' +") + endforeach() + + _qt_internal_get_staging_area_cydx_file_path(staging_area_spdx_file) + + set(content " +file(APPEND \"${staging_area_spdx_file}\" +\" +${content} +\" +) +") + + _qt_internal_get_current_project_sbom_dir(sbom_dir) + set(snippet "${sbom_dir}/cydx_license_info.cmake") + file(GENERATE OUTPUT "${snippet}" CONTENT "${content}") + + set_property(GLOBAL APPEND PROPERTY _qt_sbom_cmake_end_include_files_cydx "${snippet}") + + # Clean up before configuring next repo project. + set_property(GLOBAL PROPERTY _qt_internal_sbom_cydx_licenses "") + foreach(license_id IN LISTS license_ids) + set_property(GLOBAL PROPERTY _qt_internal_sbom_cydx_licenses_${license_id}_text "") + endforeach() +endfunction() diff --git a/cmake/QtPublicSbomGenerationHelpers.cmake b/cmake/QtPublicSbomGenerationHelpers.cmake index 89179e77a1b..1ade46c950e 100644 --- a/cmake/QtPublicSbomGenerationHelpers.cmake +++ b/cmake/QtPublicSbomGenerationHelpers.cmake @@ -74,37 +74,36 @@ function(_qt_internal_sbom_begin_project_generate) cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") _qt_internal_validate_all_args_are_parsed(arg) - if(QT_SBOM_FAKE_TIMESTAMP) - set(current_utc "2590-01-01T11:33:55Z") - set(current_year "2590") - else() - string(TIMESTAMP current_utc UTC) - string(TIMESTAMP current_year "%Y" UTC) - endif() - - _qt_internal_sbom_set_default_option_value(PROJECT "${PROJECT_NAME}") - - _qt_internal_sbom_get_git_version_vars() - - _qt_internal_path_join(default_sbom_file_name - "${arg_PROJECT}" "${arg_PROJECT}-sbom-${QT_SBOM_GIT_VERSION_PATH}.spdx") + _qt_internal_forward_function_args( + FORWARD_PREFIX arg + FORWARD_OUT_VAR common_project_args + FORWARD_SINGLE + ${single_args} + ) - _qt_internal_path_join(default_install_sbom_path - "\${CMAKE_INSTALL_PREFIX}/" "${CMAKE_INSTALL_DATAROOTDIR}" "${default_sbom_file_name}" + _qt_internal_sbom_get_common_project_variables( + ${common_project_args} + OUT_VAR_PROJECT_NAME arg_PROJECT + OUT_VAR_CURRENT_UTC current_utc + OUT_VAR_CURRENT_YEAR current_year + DEFAULT_SBOM_FILE_NAME_EXTENSION "spdx" + OUT_VAR_OUTPUT arg_OUTPUT + OUT_VAR_OUTPUT_RELATIVE_PATH arg_OUTPUT_RELATIVE_PATH + OUT_VAR_PROJECT_FOR_SPDX_ID project_spdx_id + OUT_VAR_COPYRIGHT arg_COPYRIGHT + OUT_VAR_SUPPLIER arg_SUPPLIER + OUT_VAR_SUPPLIER_URL arg_SUPPLIER_URL + OUT_VAR_DEFAULT_PROJECT_COMMENT project_comment ) + if(arg_OUT_VAR_PROJECT_SPDX_ID) + set(${arg_OUT_VAR_PROJECT_SPDX_ID} "${project_spdx_id}" PARENT_SCOPE) + endif() - _qt_internal_sbom_set_default_option_value(OUTPUT "${default_install_sbom_path}") - _qt_internal_sbom_set_default_option_value(OUTPUT_RELATIVE_PATH - "${default_sbom_file_name}") + _qt_internal_sbom_get_git_version_vars() - _qt_internal_sbom_set_default_option_value(LICENSE "NOASSERTION") - _qt_internal_sbom_set_default_option_value(PROJECT_FOR_SPDX_ID "Package-${arg_PROJECT}") - _qt_internal_sbom_set_default_option_value_and_error_if_empty(SUPPLIER "") - _qt_internal_sbom_set_default_option_value(COPYRIGHT "${current_year} ${arg_SUPPLIER}") - _qt_internal_sbom_set_default_option_value_and_error_if_empty(SUPPLIER_URL - "${PROJECT_HOMEPAGE_URL}") _qt_internal_sbom_set_default_option_value(NAMESPACE "${arg_SUPPLIER}/spdxdocs/${arg_PROJECT}-${QT_SBOM_GIT_VERSION}") + _qt_internal_sbom_set_default_option_value(LICENSE "NOASSERTION") _qt_internal_sbom_set_default_option_value(DOCUMENT_CREATOR_TOOL "Qt Build System") if(arg_DOCUMENT_CREATOR_TOOL) @@ -132,34 +131,10 @@ ExternalRef: PACKAGE-MANAGER purl ${purl_generic_id}") PackageVersion: ${QT_SBOM_GIT_VERSION}") endif() - string(REGEX REPLACE "[^A-Za-z0-9.]+" "-" arg_PROJECT_FOR_SPDX_ID "${arg_PROJECT_FOR_SPDX_ID}") - string(REGEX REPLACE "-+$" "" arg_PROJECT_FOR_SPDX_ID "${arg_PROJECT_FOR_SPDX_ID}") - # Prevent collision with other generated SPDXID with -[0-9]+ suffix. - string(REGEX REPLACE "-([0-9]+)$" "\\1" arg_PROJECT_FOR_SPDX_ID "${arg_PROJECT_FOR_SPDX_ID}") - - set(project_spdx_id "SPDXRef-${arg_PROJECT_FOR_SPDX_ID}") - if(arg_OUT_VAR_PROJECT_SPDX_ID) - set(${arg_OUT_VAR_PROJECT_SPDX_ID} "${project_spdx_id}" PARENT_SCOPE) - endif() - get_filename_component(doc_name "${arg_OUTPUT}" NAME_WLE) - get_cmake_property(is_multi_config GENERATOR_IS_MULTI_CONFIG) - if(is_multi_config) - set(cmake_configs "${CMAKE_CONFIGURATION_TYPES}") - else() - set(cmake_configs "${CMAKE_BUILD_TYPE}") - endif() - _qt_internal_sbom_set_default_option_value(DOWNLOAD_LOCATION "NOASSERTION") - set(cmake_version "Built by CMake ${CMAKE_VERSION}") - set(system_name_and_processor "${CMAKE_SYSTEM_NAME} (${CMAKE_SYSTEM_PROCESSOR})") - set(default_project_comment - "${cmake_version} with ${cmake_configs} configuration for ${system_name_and_processor}") - - set(project_comment "${default_project_comment}") - if(arg_PROJECT_COMMENT) string(APPEND project_comment "${arg_PROJECT_COMMENT}") endif() @@ -206,82 +181,30 @@ BuiltDate: ${current_utc} Relationship: SPDXRef-DOCUMENT DESCRIBES ${project_spdx_id} ") - # Create the directory that will contain all sbom related files. - _qt_internal_get_current_project_sbom_dir(sbom_dir) - file(MAKE_DIRECTORY "${sbom_dir}") - set_property(GLOBAL APPEND PROPERTY _qt_internal_sbom_dirs "${sbom_dir}") - - # Generate project document intro spdx file. _qt_internal_sbom_get_root_project_name_lower_case(repo_project_name_lowercase) - set(document_intro_file_name - "${sbom_dir}/SPDXRef-DOCUMENT-${repo_project_name_lowercase}.spdx.in") - file(GENERATE OUTPUT "${document_intro_file_name}" CONTENT "${content}") - - # This is the file that will be incrementally assembled by having content appended to it. - _qt_internal_get_staging_area_spdx_file_path(staging_area_spdx_file) - - get_filename_component(output_file_name_without_ext "${arg_OUTPUT}" NAME_WLE) - get_filename_component(output_file_ext "${arg_OUTPUT}" LAST_EXT) - - get_cmake_property(is_multi_config GENERATOR_IS_MULTI_CONFIG) - if(is_multi_config) - set(multi_config_suffix "-$<CONFIG>") - else() - set(multi_config_suffix "") - endif() - - set(computed_sbom_file_name_without_ext "${output_file_name_without_ext}${multi_config_suffix}") - set(computed_sbom_file_name "${output_file_name_without_ext}${output_file_ext}") - - # In a super build and in a no-prefix build, put all the build time sboms into the same dir in, - # in the qtbase build dir. - if(QT_BUILDING_QT AND (QT_SUPERBUILD OR (NOT QT_WILL_INSTALL))) - set(build_sbom_root_dir "${QT_BUILD_DIR}") - else() - set(build_sbom_root_dir "${sbom_dir}") - endif() - - get_filename_component(output_relative_dir "${arg_OUTPUT_RELATIVE_PATH}" DIRECTORY) - - set(build_sbom_dir "${build_sbom_root_dir}/${output_relative_dir}") - set(build_sbom_path "${build_sbom_dir}/${computed_sbom_file_name}") - set(build_sbom_path_without_ext - "${build_sbom_dir}/${computed_sbom_file_name_without_ext}") - - set(install_sbom_path "${arg_OUTPUT}") - - get_filename_component(install_sbom_dir "${install_sbom_path}" DIRECTORY) - set(install_sbom_path_without_ext "${install_sbom_dir}/${output_file_name_without_ext}") - - # Create cmake file to append the document intro spdx to the staging file. - set(create_staging_file "${sbom_dir}/append_document_to_staging${multi_config_suffix}.cmake") - set(content " - cmake_minimum_required(VERSION 3.16) - message(STATUS \"Starting SBOM generation in build dir: ${staging_area_spdx_file}\") - set(QT_SBOM_EXTERNAL_DOC_REFS \"\") - file(READ \"${document_intro_file_name}\" content) - # Override any previous file because we're starting from scratch. - file(WRITE \"${staging_area_spdx_file}\" \"\${content}\") -") - file(GENERATE OUTPUT "${create_staging_file}" CONTENT "${content}") - - - set_property(GLOBAL PROPERTY _qt_sbom_project_supplier "${arg_SUPPLIER}") - set_property(GLOBAL PROPERTY _qt_sbom_project_supplier_url "${arg_SUPPLIER_URL}") - set_property(GLOBAL PROPERTY _qt_sbom_project_namespace "${arg_NAMESPACE}") + _qt_internal_sbom_create_sbom_staging_file( + CONTENT "${content}" + SBOM_FORMAT "SPDX_V2" + REPO_PROJECT_NAME_LOWERCASE "${repo_project_name_lowercase}" + OUT_VAR_CREATE_STAGING_FILE create_staging_file + OUT_VAR_SBOM_DIR sbom_dir + ) - set_property(GLOBAL PROPERTY _qt_sbom_project_name "${arg_PROJECT}") - set_property(GLOBAL PROPERTY _qt_sbom_project_spdx_id "${project_spdx_id}") + set_property(GLOBAL APPEND PROPERTY _qt_internal_sbom_dirs "${sbom_dir}") - set_property(GLOBAL PROPERTY _qt_sbom_build_output_path "${build_sbom_path}") - set_property(GLOBAL PROPERTY _qt_sbom_build_output_path_without_ext - "${build_sbom_path_without_ext}") - set_property(GLOBAL PROPERTY _qt_sbom_build_output_dir "${build_sbom_dir}") + _qt_internal_sbom_save_project_info_in_global_properties( + SUPPLIER "${arg_SUPPLIER}" + SUPPLIER_URL "${arg_SUPPLIER_URL}" + NAMESPACE "${arg_NAMESPACE}" + PROJECT "${arg_PROJECT}" + PROJECT_SPDX_ID "${project_spdx_id}" + ) - set_property(GLOBAL PROPERTY _qt_sbom_install_output_path "${install_sbom_path}") - set_property(GLOBAL PROPERTY _qt_sbom_install_output_path_without_ext - "${install_sbom_path_without_ext}") - set_property(GLOBAL PROPERTY _qt_sbom_install_output_dir "${install_sbom_dir}") + _qt_internal_sbom_save_common_path_variables_in_global_properties( + OUTPUT "${arg_OUTPUT}" + OUTPUT_RELATIVE_PATH "${arg_OUTPUT_RELATIVE_PATH}" + SBOM_DIR "${sbom_dir}" + ) set_property(GLOBAL APPEND PROPERTY _qt_sbom_cmake_include_files "${create_staging_file}") @@ -293,116 +216,51 @@ endfunction() # Creates an 'sbom' custom target to generate an incomplete sbom at build time (no checksums). # Creates install rules to install a complete (with checksums) sbom. function(_qt_internal_sbom_end_project_generate) - get_property(sbom_build_output_path GLOBAL PROPERTY _qt_sbom_build_output_path) - get_property(sbom_build_output_path_without_ext GLOBAL PROPERTY - _qt_sbom_build_output_path_without_ext) - get_property(sbom_build_output_dir GLOBAL PROPERTY _qt_sbom_build_output_dir) - - get_property(sbom_install_output_path GLOBAL PROPERTY _qt_sbom_install_output_path) - get_property(sbom_install_output_path_without_ext GLOBAL PROPERTY - _qt_sbom_install_output_path_without_ext) - get_property(sbom_install_output_dir GLOBAL PROPERTY _qt_sbom_install_output_dir) + _qt_internal_sbom_get_common_path_variables_from_global_properties( + SBOM_FORMAT "SPDX_V2" + OUT_VAR_SBOM_BUILD_OUTPUT_PATH sbom_build_output_path + OUT_VAR_SBOM_BUILD_OUTPUT_PATH_WITHOUT_EXT sbom_build_output_path_without_ext + OUT_VAR_SBOM_BUILD_OUTPUT_DIR sbom_build_output_dir + OUT_VAR_SBOM_INSTALL_OUTPUT_PATH sbom_install_output_path + OUT_VAR_SBOM_INSTALL_OUTPUT_PATH_WITHOUT_EXT sbom_install_output_path_without_ext + OUT_VAR_SBOM_INSTALL_OUTPUT_DIR sbom_install_output_dir + ) if(NOT sbom_build_output_path) message(FATAL_ERROR "Call _qt_internal_sbom_begin_project() first") endif() - _qt_internal_get_staging_area_spdx_file_path(staging_area_spdx_file) - - _qt_internal_sbom_collect_cmake_include_files(includes - JOIN_WITH_NEWLINES - PROPERTIES _qt_sbom_cmake_include_files _qt_sbom_cmake_end_include_files - ) - - # Before checksum includes are included after the verification codes have been collected - # and before their merged checksum(s) has been computed. - _qt_internal_sbom_collect_cmake_include_files(before_checksum_includes - JOIN_WITH_NEWLINES - PROPERTIES _qt_sbom_cmake_before_checksum_include_files - ) - - # After checksum includes are included after the checksum has been computed and written to the - # QT_SBOM_VERIFICATION_CODE variable. - _qt_internal_sbom_collect_cmake_include_files(after_checksum_includes - JOIN_WITH_NEWLINES - PROPERTIES _qt_sbom_cmake_after_checksum_include_files - ) - - # Post generation includes are included for both build and install time sboms, after - # sbom generation has finished. - _qt_internal_sbom_collect_cmake_include_files(post_generation_includes - JOIN_WITH_NEWLINES - PROPERTIES _qt_sbom_cmake_post_generation_include_files - ) + _qt_internal_sbom_get_root_project_name_lower_case(repo_project_name_lowercase) + _qt_internal_sbom_get_qt_repo_project_name_lower_case(real_qt_repo_project_name_lowercase) - # Verification only makes sense on installation, where the checksums are present. - _qt_internal_sbom_collect_cmake_include_files(verify_includes - JOIN_WITH_NEWLINES - PROPERTIES _qt_sbom_cmake_verify_include_files + _qt_internal_sbom_get_cmake_include_files( + SBOM_FORMAT "SPDX_V2" + OUT_VAR_INCLUDES includes + OUT_VAR_BEFORE_CHECKSUM_INCLUDES before_checksum_includes + OUT_VAR_AFTER_CHECKSUM_INCLUDES after_checksum_includes + OUT_VAR_POST_GENERATION_INCLUDES post_generation_includes + OUT_VAR_VERIFY_INCLUDES verify_includes ) - get_cmake_property(is_multi_config GENERATOR_IS_MULTI_CONFIG) - if(is_multi_config) - set(multi_config_suffix "-$<CONFIG>") - else() - set(multi_config_suffix "") - endif() - _qt_internal_get_current_project_sbom_dir(sbom_dir) - set(content " - # QT_SBOM_BUILD_TIME be set to FALSE at install time, so don't override if it's set. - # This allows reusing the same cmake file for both build and install. - if(NOT DEFINED QT_SBOM_BUILD_TIME) - set(QT_SBOM_BUILD_TIME TRUE) - endif() - if(NOT QT_SBOM_OUTPUT_PATH) - set(QT_SBOM_OUTPUT_DIR \"${sbom_build_output_dir}\") - set(QT_SBOM_OUTPUT_PATH \"${sbom_build_output_path}\") - set(QT_SBOM_OUTPUT_PATH_WITHOUT_EXT \"${sbom_build_output_path_without_ext}\") - file(MAKE_DIRECTORY \"${sbom_build_output_dir}\") - endif() - set(QT_SBOM_PACKAGES \"\") - set(QT_SBOM_PACKAGES_WITH_VERIFICATION_CODES \"\") - ${includes} - if(QT_SBOM_BUILD_TIME) - message(STATUS \"Finalizing SBOM generation in build dir: \${QT_SBOM_OUTPUT_PATH}\") - configure_file(\"${staging_area_spdx_file}\" \"\${QT_SBOM_OUTPUT_PATH}\") - ${post_generation_includes} - endif() -") - set(assemble_sbom "${sbom_dir}/assemble_sbom${multi_config_suffix}.cmake") - file(GENERATE OUTPUT "${assemble_sbom}" CONTENT "${content}") - if(NOT TARGET sbom) - add_custom_target(sbom) + set(build_time_args "") + if(includes) + list(APPEND build_time_args INCLUDES "${includes}") endif() - - _qt_internal_sbom_get_root_project_name_lower_case(repo_project_name_lowercase) - _qt_internal_sbom_get_qt_repo_project_name_lower_case(real_qt_repo_project_name_lowercase) - - # Create a build target to create a build-time sbom (no verification codes or sha1s). - set(repo_sbom_target "sbom_${repo_project_name_lowercase}") - set(comment "") - string(APPEND comment "Assembling build time SPDX document without checksums for " - "${repo_project_name_lowercase}. Just for testing.") - add_custom_target(${repo_sbom_target} - COMMAND "${CMAKE_COMMAND}" -P "${assemble_sbom}" - COMMENT "${comment}" - VERBATIM - USES_TERMINAL # To avoid running two configs of the command in parallel - ) - - get_cmake_property(qt_repo_deps _qt_repo_deps_${real_qt_repo_project_name_lowercase}) - if(qt_repo_deps) - foreach(repo_dep IN LISTS qt_repo_deps) - set(repo_dep_sbom "sbom_${repo_dep}") - if(TARGET "${repo_dep_sbom}") - add_dependencies(${repo_sbom_target} ${repo_dep_sbom}) - endif() - endforeach() + if(post_generation_includes) + list(APPEND build_time_args POST_GENERATION_INCLUDES "${post_generation_includes}") endif() - - add_dependencies(sbom ${repo_sbom_target}) + _qt_internal_sbom_create_build_time_sbom_targets( + SBOM_FORMAT "SPDX_V2" + REPO_PROJECT_NAME_LOWERCASE "${repo_project_name_lowercase}" + REAL_QT_REPO_PROJECT_NAME_LOWERCASE "${real_qt_repo_project_name_lowercase}" + SBOM_BUILD_OUTPUT_PATH "${sbom_build_output_path}" + SBOM_BUILD_OUTPUT_PATH_WITHOUT_EXT "${sbom_build_output_path_without_ext}" + SBOM_BUILD_OUTPUT_DIR "${sbom_build_output_dir}" + OUT_VAR_ASSEMBLE_SBOM_INCLUDE_PATH assemble_sbom + ${build_time_args} + ) # Add 'reuse lint' per-repo custom targets. if(arg_LINT_SOURCE_SBOM AND NOT QT_INTERNAL_NO_SBOM_PYTHON_OPS) @@ -420,59 +278,19 @@ function(_qt_internal_sbom_end_project_generate) add_dependencies(reuse_lint ${repo_sbom_target}_reuse_lint) endif() - set(extra_code_begin "") - set(extra_code_inner_end "") - - get_cmake_property(is_multi_config GENERATOR_IS_MULTI_CONFIG) - if(is_multi_config) - set(configs ${CMAKE_CONFIGURATION_TYPES}) - - set(install_markers_dir "${sbom_dir}") - set(install_marker_path "${install_markers_dir}/finished_install-$<CONFIG>.cmake") - - set(install_marker_code " - message(STATUS \"Writing install marker for config $<CONFIG>: ${install_marker_path} \") - file(WRITE \"${install_marker_path}\" \"\") -") - - install(CODE "${install_marker_code}" COMPONENT sbom) - if(QT_SUPERBUILD) - install(CODE "${install_marker_code}" COMPONENT "sbom_${repo_project_name_lowercase}" - EXCLUDE_FROM_ALL) - endif() - - set(install_markers "") - foreach(config IN LISTS configs) - set(marker_path "${install_markers_dir}/finished_install-${config}.cmake") - list(APPEND install_markers "${marker_path}") - # Remove the markers on reconfiguration, just in case there are stale ones. - if(EXISTS "${marker_path}") - file(REMOVE "${marker_path}") - endif() - endforeach() - - set(extra_code_begin " - set(QT_SBOM_INSTALL_MARKERS \"${install_markers}\") - foreach(QT_SBOM_INSTALL_MARKER IN LISTS QT_SBOM_INSTALL_MARKERS) - if(NOT EXISTS \"\${QT_SBOM_INSTALL_MARKER}\") - set(QT_SBOM_INSTALLED_ALL_CONFIGS FALSE) - endif() - endforeach() -") - set(extra_code_inner_end " - foreach(QT_SBOM_INSTALL_MARKER IN LISTS QT_SBOM_INSTALL_MARKERS) - message(STATUS - \"Removing install marker: \${QT_SBOM_INSTALL_MARKER} \") - file(REMOVE \"\${QT_SBOM_INSTALL_MARKER}\") - endforeach() -") - endif() + _qt_internal_sbom_setup_multi_config_install_markers( + SBOM_DIR "${sbom_dir}" + SBOM_FORMAT "SPDX_V2" + REPO_PROJECT_NAME_LOWERCASE "${repo_project_name_lowercase}" + OUT_VAR_EXTRA_CODE_BEGIN extra_code_begin + OUT_VAR_EXTRA_CODE_INNER_END extra_code_inner_end + ) - # Allow skipping checksum computation for testing purposes, while installing just the sbom - # documents, without requiring to build and install all the actual files. - if(QT_SBOM_FAKE_CHECKSUM) - string(APPEND extra_code_begin " - set(QT_SBOM_FAKE_CHECKSUM TRUE)") + _qt_internal_sbom_setup_fake_checksum( + OUT_VAR_FAKE_CHECKSUM_CODE extra_code_begin_fake_checksum + ) + if(extra_code_begin_fake_checksum) + string(APPEND extra_code_begin "${extra_code_begin_fake_checksum}") endif() set(verification_codes_content " @@ -496,42 +314,40 @@ unset(_verification_code) set(process_verification_codes "${sbom_dir}/process_verification_codes.cmake") file(GENERATE OUTPUT "${process_verification_codes}" CONTENT "${verification_codes_content}") - set(assemble_sbom_install " - set(QT_SBOM_INSTALLED_ALL_CONFIGS TRUE) - ${extra_code_begin} - if(QT_SBOM_INSTALLED_ALL_CONFIGS) - set(QT_SBOM_BUILD_TIME FALSE) - set(QT_SBOM_OUTPUT_DIR \"${sbom_install_output_dir}\") - set(QT_SBOM_OUTPUT_PATH \"${sbom_install_output_path}\") - set(QT_SBOM_OUTPUT_PATH_WITHOUT_EXT \"${sbom_install_output_path_without_ext}\") - file(MAKE_DIRECTORY \"${sbom_install_output_dir}\") - include(\"${assemble_sbom}\") - ${before_checksum_includes} - include(\"${process_verification_codes}\") - ${after_checksum_includes} - message(STATUS \"Finalizing SBOM generation in install dir: \${QT_SBOM_OUTPUT_PATH}\") - configure_file(\"${staging_area_spdx_file}\" \"\${QT_SBOM_OUTPUT_PATH}\") - ${post_generation_includes} - ${verify_includes} - ${extra_code_inner_end} - else() - message(STATUS \"Skipping SBOM finalization because not all configs were installed.\") - endif() -") - - install(CODE "${assemble_sbom_install}" COMPONENT sbom) - if(QT_SUPERBUILD) - install(CODE "${assemble_sbom_install}" COMPONENT "sbom_${repo_project_name_lowercase}" - EXCLUDE_FROM_ALL) + set(setup_sbom_install_args "") + if(extra_code_begin) + list(APPEND setup_sbom_install_args EXTRA_CODE_BEGIN "${extra_code_begin}") + endif() + if(extra_code_inner_end) + list(APPEND setup_sbom_install_args EXTRA_CODE_INNER_END "${extra_code_inner_end}") + endif() + if(before_checksum_includes) + list(APPEND setup_sbom_install_args BEFORE_CHECKSUM_INCLUDES "${before_checksum_includes}") + endif() + if(after_checksum_includes) + list(APPEND setup_sbom_install_args AFTER_CHECKSUM_INCLUDES "${after_checksum_includes}") + endif() + if(post_generation_includes) + list(APPEND setup_sbom_install_args POST_GENERATION_INCLUDES "${post_generation_includes}") + endif() + if(verify_includes) + list(APPEND setup_sbom_install_args VERIFY_INCLUDES "${verify_includes}") endif() - # Clean up properties, so that they are empty for possible next repo in a top-level build. - set_property(GLOBAL PROPERTY _qt_sbom_cmake_include_files "") - set_property(GLOBAL PROPERTY _qt_sbom_cmake_end_include_files "") - set_property(GLOBAL PROPERTY _qt_sbom_cmake_before_checksum_include_files "") - set_property(GLOBAL PROPERTY _qt_sbom_cmake_after_checksum_include_files "") - set_property(GLOBAL PROPERTY _qt_sbom_cmake_post_generation_include_files "") - set_property(GLOBAL PROPERTY _qt_sbom_cmake_verify_include_files "") + _qt_internal_sbom_setup_sbom_install_code( + SBOM_FORMAT "SPDX_V2" + REPO_PROJECT_NAME_LOWERCASE "${repo_project_name_lowercase}" + SBOM_INSTALL_OUTPUT_PATH "${sbom_install_output_path}" + SBOM_INSTALL_OUTPUT_PATH_WITHOUT_EXT "${sbom_install_output_path_without_ext}" + SBOM_INSTALL_OUTPUT_DIR "${sbom_install_output_dir}" + ASSEMBLE_SBOM_INCLUDE_PATH "${assemble_sbom}" + PROCESS_VERIFICATION_CODES "${process_verification_codes}" + ${setup_sbom_install_args} + ) + + _qt_internal_sbom_clear_cmake_include_files( + SBOM_FORMAT "SPDX_V2" + ) endfunction() # Gets a list of cmake include file paths, joins them as include() statements and returns the diff --git a/cmake/QtPublicSbomHelpers.cmake b/cmake/QtPublicSbomHelpers.cmake index 1a8702bf594..3d66e7ff783 100644 --- a/cmake/QtPublicSbomHelpers.cmake +++ b/cmake/QtPublicSbomHelpers.cmake @@ -85,7 +85,8 @@ function(_qt_internal_sbom_begin_project) _qt_internal_sbom_get_root_project_name_for_spdx_id(repo_project_name_for_spdx_id) _qt_internal_sbom_get_root_project_name_lower_case(repo_project_name_lowercase) - set(begin_project_generate_args "") + set(begin_project_generate_args_spdx "") + set(begin_project_generate_args_cydx "") if(arg_SUPPLIER_URL) set(repo_supplier_url "${arg_SUPPLIER_URL}") @@ -93,7 +94,8 @@ function(_qt_internal_sbom_begin_project) _qt_internal_sbom_get_default_supplier_url(repo_supplier_url) endif() if(repo_supplier_url) - list(APPEND begin_project_generate_args SUPPLIER_URL "${repo_supplier_url}") + list(APPEND begin_project_generate_args_spdx SUPPLIER_URL "${repo_supplier_url}") + list(APPEND begin_project_generate_args_cydx SUPPLIER_URL "${repo_supplier_url}") endif() set(sbom_project_version_args "") @@ -121,6 +123,15 @@ function(_qt_internal_sbom_begin_project) ) endif() + if(QT_SBOM_GENERATE_CYDX_V1_6) + _qt_internal_sbom_get_cyclone_bom_serial_number( + SPDX_NAMESPACE "${repo_spdx_namespace}" + OUT_VAR_UUID cyclone_dx_bom_serial_number_uuid + ) + list(APPEND begin_project_generate_args_cydx + BOM_SERIAL_NUMBER_UUID "${cyclone_dx_bom_serial_number_uuid}") + endif() + if(arg_INSTALL_SBOM_DIR) set(install_sbom_dir "${arg_INSTALL_SBOM_DIR}") elseif(INSTALL_SBOMDIR) @@ -143,17 +154,28 @@ function(_qt_internal_sbom_begin_project) list(APPEND compute_project_file_name_args VERSION_SUFFIX "${explicit_version}") endif() - _qt_internal_sbom_compute_project_file_name(repo_project_file_name + _qt_internal_sbom_compute_project_file_name(repo_project_file_name_spdx + SPDX_TAG_VALUE + PROJECT_NAME "${repo_project_name_lowercase}" + ${compute_project_file_name_args} + ) + + _qt_internal_sbom_compute_project_file_name(repo_project_file_name_cydx + CYCLONEDX_TOML PROJECT_NAME "${repo_project_name_lowercase}" ${compute_project_file_name_args} ) - _qt_internal_path_join(repo_spdx_relative_install_path - "${arg_INSTALL_SBOM_DIR}" "${repo_project_file_name}") + _qt_internal_path_join(repo_spdx_relative_install_path_spdx + "${install_sbom_dir}" "${repo_project_file_name_spdx}") + _qt_internal_path_join(repo_spdx_relative_install_path_cydx + "${install_sbom_dir}" "${repo_project_file_name_cydx}") # Prepend DESTDIR, to allow relocating installed sbom. Needed for CI. - _qt_internal_path_join(repo_spdx_install_path - "\$ENV{DESTDIR}${install_prefix}" "${repo_spdx_relative_install_path}") + _qt_internal_path_join(repo_spdx_install_path_spdx + "\$ENV{DESTDIR}${install_prefix}" "${repo_spdx_relative_install_path_spdx}") + _qt_internal_path_join(repo_spdx_install_path_cydx + "\$ENV{DESTDIR}${install_prefix}" "${repo_spdx_relative_install_path_cydx}") if(arg_LICENSE_EXPRESSION) set(repo_license "${arg_LICENSE_EXPRESSION}") @@ -164,7 +186,8 @@ function(_qt_internal_sbom_begin_project) set(repo_license "") endif() if(repo_license) - list(APPEND begin_project_generate_args LICENSE "${repo_license}") + list(APPEND begin_project_generate_args_spdx LICENSE "${repo_license}") + list(APPEND begin_project_generate_args_cydx LICENSE "${repo_license}") endif() if(arg_COPYRIGHTS) @@ -174,7 +197,8 @@ function(_qt_internal_sbom_begin_project) _qt_internal_sbom_get_default_qt_copyright_header(repo_copyright) endif() if(repo_copyright) - list(APPEND begin_project_generate_args COPYRIGHT "${repo_copyright}") + list(APPEND begin_project_generate_args_spdx COPYRIGHT "${repo_copyright}") + list(APPEND begin_project_generate_args_cydx COPYRIGHT "${repo_copyright}") endif() if(arg_SUPPLIER) @@ -184,7 +208,8 @@ function(_qt_internal_sbom_begin_project) endif() if(repo_supplier) # This must not contain spaces! - list(APPEND begin_project_generate_args SUPPLIER "${repo_supplier}") + list(APPEND begin_project_generate_args_spdx SUPPLIER "${repo_supplier}") + list(APPEND begin_project_generate_args_cydx SUPPLIER "${repo_supplier}") endif() if(arg_CPE) @@ -195,7 +220,8 @@ function(_qt_internal_sbom_begin_project) set(qt_cpe "") endif() if(qt_cpe) - list(APPEND begin_project_generate_args CPE "${qt_cpe}") + list(APPEND begin_project_generate_args_spdx CPE "${qt_cpe}") + list(APPEND begin_project_generate_args_cydx CPE "${qt_cpe}") endif() if(arg_DOWNLOAD_LOCATION) @@ -204,11 +230,12 @@ function(_qt_internal_sbom_begin_project) _qt_internal_sbom_get_qt_repo_source_download_location(download_location) endif() if(download_location) - list(APPEND begin_project_generate_args DOWNLOAD_LOCATION "${download_location}") + list(APPEND begin_project_generate_args_spdx DOWNLOAD_LOCATION "${download_location}") + list(APPEND begin_project_generate_args_cydx DOWNLOAD_LOCATION "${download_location}") endif() if(arg_DOCUMENT_CREATOR_TOOL) - list(APPEND begin_project_generate_args + list(APPEND begin_project_generate_args_spdx DOCUMENT_CREATOR_TOOL "${arg_DOCUMENT_CREATOR_TOOL}") endif() @@ -229,24 +256,42 @@ function(_qt_internal_sbom_begin_project) set(project_comment PROJECT_COMMENT "${project_comment}") endif() - _qt_internal_sbom_begin_project_generate( - OUTPUT "${repo_spdx_install_path}" - OUTPUT_RELATIVE_PATH "${repo_spdx_relative_install_path}" - PROJECT "${repo_project_name_lowercase}" - ${project_comment} - PROJECT_FOR_SPDX_ID "${repo_project_name_for_spdx_id}" - NAMESPACE "${repo_spdx_namespace}" - ${begin_project_generate_args} - OUT_VAR_PROJECT_SPDX_ID repo_project_spdx_id - ) + if(QT_SBOM_GENERATE_SPDX_V2) + _qt_internal_sbom_begin_project_generate( + OUTPUT "${repo_spdx_install_path_spdx}" + OUTPUT_RELATIVE_PATH "${repo_spdx_relative_install_path_spdx}" + PROJECT "${repo_project_name_lowercase}" + ${project_comment} + PROJECT_FOR_SPDX_ID "${repo_project_name_for_spdx_id}" + NAMESPACE "${repo_spdx_namespace}" + ${begin_project_generate_args_spdx} + OUT_VAR_PROJECT_SPDX_ID repo_project_spdx_id + ) + endif() + + if(QT_SBOM_GENERATE_CYDX_V1_6) + _qt_internal_sbom_begin_project_generate_cyclone( + OUTPUT "${repo_spdx_install_path_cydx}" + OUTPUT_RELATIVE_PATH "${repo_spdx_relative_install_path_cydx}" + PROJECT "${repo_project_name_lowercase}" + ${project_comment} + PROJECT_FOR_SPDX_ID "${repo_project_name_for_spdx_id}" + NAMESPACE "${repo_spdx_namespace}" + ${begin_project_generate_args_cydx} + OUT_VAR_PROJECT_SPDX_ID repo_project_spdx_id + ) + endif() set_property(GLOBAL PROPERTY _qt_internal_project_attribution_files "") set_property(GLOBAL PROPERTY _qt_internal_sbom_repo_document_namespace "${repo_spdx_namespace}") + set_property(GLOBAL PROPERTY _qt_internal_sbom_repo_cyclone_dx_bom_serial_number_uuid + "${cyclone_dx_bom_serial_number_uuid}") + set_property(GLOBAL PROPERTY _qt_internal_sbom_relative_installed_repo_document_path - "${repo_spdx_relative_install_path}") + "${repo_spdx_relative_install_path_spdx}") set_property(GLOBAL PROPERTY _qt_internal_sbom_repo_project_name_lowercase "${repo_project_name_lowercase}") @@ -312,7 +357,10 @@ endfunction() function(_qt_internal_sbom_setup_project_ops) set(options "") - if(QT_SBOM_GENERATE_JSON OR QT_INTERNAL_SBOM_GENERATE_JSON OR QT_INTERNAL_SBOM_DEFAULT_CHECKS) + if(QT_SBOM_GENERATE_JSON + OR QT_SBOM_GENERATE_SPDX_V2_JSON + OR QT_INTERNAL_SBOM_GENERATE_JSON + OR QT_INTERNAL_SBOM_DEFAULT_CHECKS) list(APPEND options GENERATE_JSON) endif() @@ -320,20 +368,48 @@ function(_qt_internal_sbom_setup_project_ops) # The user can explicitly request to fail the build if dependencies are not found. # error out. For internal options that the CI uses, we always want to fail the build if the # deps are not found. - if(QT_SBOM_REQUIRE_GENERATE_JSON OR QT_INTERNAL_SBOM_GENERATE_JSON + if(QT_SBOM_REQUIRE_GENERATE_JSON + OR QT_SBOM_REQUIRE_GENERATE_SPDX_V2_JSON + OR QT_INTERNAL_SBOM_GENERATE_JSON OR QT_INTERNAL_SBOM_DEFAULT_CHECKS) list(APPEND options GENERATE_JSON_REQUIRED) endif() - if(QT_SBOM_VERIFY OR QT_INTERNAL_SBOM_VERIFY OR QT_INTERNAL_SBOM_DEFAULT_CHECKS) + if(QT_SBOM_VERIFY + OR QT_SBOM_VERIFY_SPDX_V2 + OR QT_INTERNAL_SBOM_VERIFY + OR QT_INTERNAL_SBOM_DEFAULT_CHECKS) list(APPEND options VERIFY_SBOM) endif() # Do the same requirement check for SBOM verification. - if(QT_SBOM_REQUIRE_VERIFY OR QT_INTERNAL_SBOM_VERIFY OR QT_INTERNAL_SBOM_DEFAULT_CHECKS) + if(QT_SBOM_REQUIRE_VERIFY + OR QT_SBOM_REQUIRE_VERIFY_SPDX_V2 + OR QT_INTERNAL_SBOM_VERIFY + OR QT_INTERNAL_SBOM_DEFAULT_CHECKS) list(APPEND options VERIFY_SBOM_REQUIRED) endif() + if(QT_SBOM_GENERATE_CYDX_V1_6) + list(APPEND options GENERATE_CYCLONE_DX_V1_6) + endif() + + if(QT_SBOM_REQUIRE_GENERATE_CYDX_V1_6) + list(APPEND options GENERATE_CYCLONE_DX_V1_6_REQUIRED) + endif() + + if(QT_SBOM_VERIFY_CYDX_V1_6) + list(APPEND options VERIFY_CYCLONE_DX_V1_6) + endif() + + if(QT_SBOM_REQUIRE_VERIFY_CYDX_V1_6) + list(APPEND options VERIFY_CYCLONE_DX_V1_6_REQUIRED) + endif() + + if(QT_SBOM_VERBOSE_CYDX_V1_6) + list(APPEND options VERBOSE_CYCLONE_DX_V1_6) + endif() + if(QT_SBOM_VERIFY_NTIA_COMPLIANT OR QT_INTERNAL_SBOM_VERIFY_NTIA_COMPLIANT OR QT_INTERNAL_SBOM_DEFAULT_CHECKS) list(APPEND options VERIFY_NTIA_COMPLIANT) @@ -367,12 +443,20 @@ function(_qt_internal_sbom_setup_project_ops) endfunction() # Sets up SBOM generation and verification options. -# By default SBOM generation is disabled. -# By default JSON generation and SBOM verification are enabled by default, if the dependencies -# are present, otherwise they will be silently skipped. Unless the user explicitly requests to -# fail the build if the dependencies are not found. # -# The QT_GENERATE_SBOM_DEFAULT option can be set by a project to change the default value. +# By default, the main toggle for SBOM generation is disabled. The GENERATE_SBOM_DEFAULT option +# overrides that value and can be set by a project that sets up SBOM generation. +# +# If the main toggle gets enabled, we enable SPDX V2.3 tag:value generation, and try to enable +# CycloneDX V1.6 generation. Try, because CDX generation needs python dependencies. If they are +# not found, the generation is silently skipped. +# +# By default SPDX v2.3 JSON generation and verification is enabled, if the python dependencies +# are found. Otherwise they will be silently skipped. +# Unless the user explicitly requests to fail the build if the dependencies are not found. +# The same can be done for CycloneDX generation. +# +# Some older variables that were added pre-CycloneDX generation are deprecated. function(_qt_internal_setup_sbom) set(opt_args "") set(single_args @@ -388,22 +472,159 @@ function(_qt_internal_setup_sbom) set(default_value "${arg_GENERATE_SBOM_DEFAULT}") endif() - option(QT_GENERATE_SBOM "Generate SBOM documents in SPDX v2.3 tag:value format." - "${default_value}") + # Main SBOM toggle. Used to be the toggle for SPDX v2.3 only, but now would also enable Cyclone + # DX as well. + set(sbom_help_string "Generate SBOM.") + option(QT_GENERATE_SBOM "${sbom_help_string}" "${default_value}") + + + # Toggle for SPDX V2.3 generation. + set(spdx_v2_help_string "Generate SBOM documents in SPDX v2.3 tag:value format.") + option(QT_SBOM_GENERATE_SPDX_V2 "${spdx_v2_help_string}" ON) + + + # Toggles for CycloneDX V1.6 generation. + set(cydx_help_string "Generate SBOM documents in CycloneDX v1.6 JSON format.") + option(QT_SBOM_GENERATE_CYDX_V1_6 "${cydx_help_string}" ON) - string(CONCAT help_string + set(cydx_require_help_string + "Error out if CycloneDX SBOM generation dependencies are not found.") + option(QT_SBOM_REQUIRE_GENERATE_CYDX_V1_6 "${cydx_require_help_string}" OFF) + + + # Options for SPDX v2.3 JSON generation and verification. + + string(CONCAT spdx_v23_json_help_string "Generate SBOM documents in SPDX v2.3 JSON format if required python dependency " - "spdx-tools is available" + "spdx-tools is available." ) - option(QT_SBOM_GENERATE_JSON - "${help_string}" ON) - option(QT_SBOM_REQUIRE_GENERATE_JSON - "Error out if JSON SBOM generation depdendency is not found." OFF) + set(spdx_v23_json_require_help_string + "Error out if JSON SBOM generation depdendency is not found.") + + set(spdx_v23_verify_help_string + "Verify generated SBOM documents using python spdx-tools package.") - option(QT_SBOM_VERIFY "Verify generated SBOM documents using python spdx-tools package." ON) - option(QT_SBOM_REQUIRE_VERIFY - "Error out if SBOM verification dependencies are not found." OFF) + set(spdx_v23_verify_require_help_string + "Error out if SBOM verification dependencies are not found.") + + option(QT_SBOM_GENERATE_SPDX_V2_JSON "${spdx_v23_json_help_string}" ON) + option(QT_SBOM_REQUIRE_GENERATE_SPDX_V2_JSON "${spdx_v23_json_require_help_string}" OFF) + + option(QT_SBOM_VERIFY_SPDX_V2 "${spdx_v23_verify_help_string}" ON) + option(QT_SBOM_REQUIRE_VERIFY_SPDX_V2 "${spdx_v23_verify_require_help_string}" OFF) + + + # Options for CycloneDX verification and verbosity. + + set(cydx_verify_help_string + "Verify generated CycloneDX document against its json schema.") + option(QT_SBOM_VERIFY_CYDX_V1_6 "${cydx_verify_help_string}" ON) + + set(cydx_verify_require_help_string + "Error out if SBOM verification dependencies are not found.") + option(QT_SBOM_REQUIRE_VERIFY_CYDX_V1_6 "${cydx_verify_require_help_string}" OFF) + + set(cydx_verbose_help_string + "Enable verbose output for CycloneDX generation.") + option(QT_SBOM_VERBOSE_CYDX_V1_6 "${cydx_verbose_help_string}" OFF) + + + # Deprecated options, superseded by the options above. + # Only add them if the values was previously defined, but update the doc string. + + if(DEFINED QT_SBOM_GENERATE_JSON) + option(QT_SBOM_GENERATE_JSON "Deprecated: ${spdx_v23_json_help_string}" ON) + endif() + if(DEFINED QT_SBOM_REQUIRE_GENERATE_JSON) + option(QT_SBOM_REQUIRE_GENERATE_JSON "Deprecated: ${spdx_v23_json_require_help_string}" OFF) + endif() + if(DEFINED QT_SBOM_VERIFY) + option(QT_SBOM_VERIFY "Deprecated: ${spdx_v23_verify_help_string}" ON) + endif() + if(DEFINED QT_SBOM_REQUIRE_VERIFY) + option(QT_SBOM_REQUIRE_VERIFY "Deprecated: ${spdx_v23_verify_require_help_string}" OFF) + endif() + + # Semi-public, undocumented options to allow enabling all SBOM stuff, for easier testing. + if(QT_SBOM_GENERATE_AND_VERIFY_ALL) + set(QT_SBOM_GENERATE_ALL ON) + set(QT_SBOM_GENERATE_REQUIRED_ALL ON) + set(QT_SBOM_VERIFY_REQUIRED_ALL ON) + endif() + + if(QT_SBOM_GENERATE_ALL) + set(QT_GENERATE_SBOM ON CACHE BOOL "${sbom_help_string}" FORCE) + set(QT_SBOM_GENERATE_SPDX_V2 ON CACHE BOOL "${spdx_v2_help_string}" FORCE) + set(QT_SBOM_GENERATE_SPDX_V2_JSON ON CACHE BOOL "${spdx_v23_json_help_string}" FORCE) + set(QT_SBOM_GENERATE_CYDX_V1_6 ON CACHE BOOL "${cydx_help_string}" FORCE) + unset(QT_SBOM_GENERATE_ALL CACHE) + unset(QT_SBOM_GENERATE_ALL) + endif() + + if(QT_SBOM_GENERATE_REQUIRED_ALL) + set(QT_SBOM_REQUIRE_GENERATE_SPDX_V2_JSON ON CACHE BOOL + "${spdx_v23_json_require_help_string}" FORCE) + set(QT_SBOM_REQUIRE_GENERATE_CYDX_V1_6 ON CACHE BOOL "${cydx_require_help_string}" FORCE) + + unset(QT_SBOM_GENERATE_REQUIRED_ALL CACHE) + unset(QT_SBOM_GENERATE_REQUIRED_ALL) + endif() + + if(QT_SBOM_VERIFY_REQUIRED_ALL) + set(QT_SBOM_VERIFY_SPDX_V2 ON CACHE BOOL "${spdx_v23_verify_help_string}" FORCE) + set(QT_SBOM_VERIFY_CYDX_V1_6 ON CACHE BOOL "${cydx_verify_help_string}" FORCE) + + set(QT_SBOM_REQUIRE_VERIFY_SPDX_V2 ON CACHE BOOL "${spdx_v23_verify_require_help_string}" + FORCE) + set(QT_SBOM_REQUIRE_VERIFY_CYDX_V1_6 ON CACHE BOOL "${cydx_verify_require_help_string}" + FORCE) + + unset(QT_SBOM_VERIFY_ALL CACHE) + unset(QT_SBOM_VERIFY_ALL) + endif() + + # Various sanity checks. + + # Disable SPDX v2.3 JSON generation if tag:value generation is disabled. + if(QT_GENERATE_SBOM + AND QT_SBOM_GENERATE_SPDX_V2_JSON + AND NOT QT_SBOM_GENERATE_SPDX_V2) + if(NOT QT_NO_SBOM_INFORMATIONAL_MESSAGES) + message(STATUS + "Disabling SPDX v2.3 SBOM JSON generation because tag:value generation is " + "disabled and that is a requirement for JSON generation.") + endif() + set(QT_SBOM_GENERATE_SPDX_V2_JSON OFF CACHE BOOL "${spdx_v23_json_help_string}" FORCE) + set(QT_SBOM_VERIFY_SPDX_V2 OFF CACHE BOOL "${spdx_v23_verify_help_string}" FORCE) + endif() + + # Disable CycloneDX generation if dependencies are not found and it wasn't required. + if(QT_GENERATE_SBOM + AND QT_SBOM_GENERATE_CYDX_V1_6 + AND NOT QT_SBOM_REQUIRE_GENERATE_CYDX_V1_6) + _qt_internal_sbom_find_cydx_dependencies(OUT_VAR_DEPS_FOUND deps_found) + if(NOT deps_found) + if(NOT QT_NO_SBOM_INFORMATIONAL_MESSAGES) + message(STATUS + "Disabling Cyclone DX SBOM generation because dependencies were not found, " + "and generation was not marked as required.") + endif() + set(QT_SBOM_GENERATE_CYDX_V1_6 OFF CACHE BOOL "${cydx_help_string}" FORCE) + endif() + endif() + + # Disable sbom generation if none of the formats are enabled. Failing to do so will cause + # errors in _qt_internal_sbom_begin_project. + if(QT_GENERATE_SBOM + AND NOT QT_SBOM_GENERATE_SPDX_V2 + AND NOT QT_SBOM_GENERATE_CYDX_V1_6) + if(NOT QT_NO_SBOM_INFORMATIONAL_MESSAGES) + message(STATUS + "Disabling SBOM generation because none of the supported formats were enabled.") + endif() + set(QT_GENERATE_SBOM OFF CACHE BOOL "${sbom_help_string}" FORCE) + endif() endfunction() # Ends repo sbom project generation. @@ -415,10 +636,6 @@ function(_qt_internal_sbom_end_project) return() endif() - # Now that we know which system libraries are linked against because we added all - # subdirectories, we can add the recorded system libs to the sbom. - _qt_internal_sbom_add_recorded_system_libraries() - # Run sbom finalization for targets that had it scheduled, but haven't run yet. # This can happen when _qt_internal_sbom_end_project is called within the same # subdirectory scope as where the targets are meant to be finalized, but that would be too late @@ -462,7 +679,24 @@ function(_qt_internal_sbom_end_project) endif() endwhile() - _qt_internal_sbom_end_project_generate() + # Now that we know which system libraries are linked against because we added all + # subdirectories and finalized all targets, we can add the recorded system libs to the sbom. + _qt_internal_sbom_add_recorded_system_libraries() + + # Add any external target dependencies, for CycloneDX generation. + # E.g. For QtSvg, we need to create a QtCore component in the QtSvg document, so that we + # can declare a dependency on it. + if(QT_SBOM_GENERATE_CYDX_V1_6) + _qt_internal_sbom_add_cydx_external_target_dependencies() + endif() + + if(QT_SBOM_GENERATE_SPDX_V2) + _qt_internal_sbom_end_project_generate() + endif() + + if(QT_SBOM_GENERATE_CYDX_V1_6) + _qt_internal_sbom_end_project_generate_cyclone() + endif() # Clean up external document ref properties, because each repo needs to start from scratch # in a top-level build. @@ -777,7 +1011,8 @@ function(_qt_internal_sbom_add_target target) set_target_properties(${target} PROPERTIES _qt_sbom_is_qt_module TRUE) endif() - set(project_package_options "") + set(project_package_options_spdx "") + set(project_package_options_cydx "") if(arg_FRIENDLY_PACKAGE_NAME) set(package_name_for_spdx_id "${arg_FRIENDLY_PACKAGE_NAME}") @@ -887,7 +1122,8 @@ function(_qt_internal_sbom_add_target target) endif() if(license_expression) - list(APPEND project_package_options LICENSE_CONCLUDED "${license_expression}") + list(APPEND project_package_options_spdx LICENSE_CONCLUDED "${license_expression}") + list(APPEND project_package_options_cydx LICENSE_CONCLUDED "${license_expression}") endif() if(license_expression AND @@ -898,7 +1134,9 @@ function(_qt_internal_sbom_add_target target) LICENSE_CONCLUDED_EXPRESSION "${license_expression}" OUT_VAR qt_entity_license_declared_expression) if(qt_entity_license_declared_expression) - list(APPEND project_package_options + list(APPEND project_package_options_spdx + LICENSE_DECLARED "${qt_entity_license_declared_expression}") + list(APPEND project_package_options_cydx LICENSE_DECLARED "${qt_entity_license_declared_expression}") endif() endif() @@ -923,7 +1161,8 @@ function(_qt_internal_sbom_add_target target) endif() if(copyrights) list(JOIN copyrights "\n" copyrights) - list(APPEND project_package_options COPYRIGHT "<text>${copyrights}</text>") + list(APPEND project_package_options_spdx COPYRIGHT "<text>${copyrights}</text>") + list(APPEND project_package_options_cydx COPYRIGHT "${copyrights}") endif() set(package_version "") @@ -943,7 +1182,12 @@ function(_qt_internal_sbom_add_target target) endif() if(package_version) - list(APPEND project_package_options VERSION "${package_version}") + list(APPEND project_package_options_spdx VERSION "${package_version}") + list(APPEND project_package_options_cydx VERSION "${package_version}") + + # Also export the value in a target property, to make it available for cydx generation. + set_property(TARGET "${target}" PROPERTY _qt_sbom_package_version "${package_version}") + set_property(TARGET "${target}" APPEND PROPERTY EXPORT_PROPERTIES _qt_sbom_package_version) endif() set(supplier "") @@ -961,7 +1205,8 @@ function(_qt_internal_sbom_add_target target) endif() if(supplier) - list(APPEND project_package_options SUPPLIER "Organization: ${supplier}") + list(APPEND project_package_options_spdx SUPPLIER "Organization: ${supplier}") + list(APPEND project_package_options_cydx CYDX_SUPPLIER "${supplier}") endif() set(download_location "") @@ -1002,11 +1247,12 @@ function(_qt_internal_sbom_add_target target) endif() if(download_location) - list(APPEND project_package_options DOWNLOAD_LOCATION "${download_location}") + list(APPEND project_package_options_spdx DOWNLOAD_LOCATION "${download_location}") + list(APPEND project_package_options_cydx DOWNLOAD_LOCATION "${download_location}") endif() _qt_internal_sbom_get_package_purpose("${sbom_entity_type}" package_purpose) - list(APPEND project_package_options PURPOSE "${package_purpose}") + list(APPEND project_package_options_spdx PURPOSE "${package_purpose}") set(cpe_values "") @@ -1046,7 +1292,8 @@ function(_qt_internal_sbom_add_target target) endif() if(cpe_values) - list(APPEND project_package_options CPE ${cpe_values}) + list(APPEND project_package_options_spdx CPE ${cpe_values}) + list(APPEND project_package_options_cydx CPE ${cpe_values}) endif() # Assemble arguments to forward to the function that handles purl options. @@ -1088,12 +1335,16 @@ function(_qt_internal_sbom_add_target target) list(APPEND purl_args PURL_VALUES ${qa_purls_replaced}) endif() - list(APPEND purl_args OUT_VAR purl_package_options) + list(APPEND purl_args + OUT_VAR_PURL_VALUES purl_values + OUT_VAR_SPDX_EXT_REF_VALUES spdx_ext_ref_values + ) _qt_internal_sbom_handle_purl_values(${target} ${purl_args}) - if(purl_package_options) - list(APPEND project_package_options ${purl_package_options}) + if(spdx_ext_ref_values) + list(APPEND project_package_options_spdx ${spdx_ext_ref_values}) + list(APPEND project_package_options_cydx PURL_VALUES ${purl_values}) endif() if(arg_USE_ATTRIBUTION_FILES) @@ -1136,32 +1387,75 @@ function(_qt_internal_sbom_add_target target) endif() if(package_comment) - list(APPEND project_package_options COMMENT "<text>\n${package_comment}</text>") + list(APPEND project_package_options_spdx COMMENT "<text>\n${package_comment}</text>") + list(APPEND project_package_options_cydx COMMENT "\n${package_comment}") endif() _qt_internal_sbom_handle_target_dependencies("${target}" SPDX_ID "${package_spdx_id}" LIBRARIES "${arg_LIBRARIES}" PUBLIC_LIBRARIES "${arg_PUBLIC_LIBRARIES}" - OUT_RELATIONSHIPS relationships + OUT_CYDX_DEPENDENCIES cydx_dependencies + OUT_SPDX_RELATIONSHIPS spdx_relationships + OUT_EXTERNAL_TARGET_DEPENDENCIES external_target_dependencies ) + if(cydx_dependencies) + list(APPEND project_package_options_cydx DEPENDENCIES ${cydx_dependencies}) + endif() + + # These are processed at the end of document generation. + if(external_target_dependencies) + _qt_internal_sbom_record_external_target_dependecies( + TARGETS ${external_target_dependencies} + ) + endif() get_cmake_property(project_spdx_id _qt_internal_sbom_project_spdx_id) - list(APPEND relationships "${project_spdx_id} CONTAINS ${package_spdx_id}") + list(APPEND spdx_relationships "${project_spdx_id} CONTAINS ${package_spdx_id}") if(arg_SBOM_RELATIONSHIPS) - list(APPEND relationships "${arg_SBOM_RELATIONSHIPS}") + list(APPEND spdx_relationships "${arg_SBOM_RELATIONSHIPS}") endif() - list(REMOVE_DUPLICATES relationships) - list(JOIN relationships "\nRelationship: " relationships) - list(APPEND project_package_options RELATIONSHIP "${relationships}") + list(REMOVE_DUPLICATES spdx_relationships) + list(JOIN spdx_relationships "\nRelationship: " relationships) + list(APPEND project_package_options_spdx RELATIONSHIP "${relationships}") - _qt_internal_sbom_generate_add_package( - PACKAGE "${package_name_for_spdx_id}" - SPDXID "${package_spdx_id}" - ${project_package_options} - ) + if(QT_SBOM_GENERATE_SPDX_V2) + _qt_internal_sbom_generate_add_package( + PACKAGE "${package_name_for_spdx_id}" + SPDXID "${package_spdx_id}" + ${project_package_options_spdx} + ) + endif() + + if(QT_SBOM_GENERATE_CYDX_V1_6) + get_property(external_targets + GLOBAL PROPERTY _qt_internal_sbom_external_target_dependencies) + + # Prevent against case when a system library is also an external target dependency, + # which would lead to its creation twice, once via + # _qt_internal_sbom_add_recorded_system_libraries + # and second time via + # _qt_internal_sbom_add_cydx_external_target_dependencies. + # Skip the case when it's done via the first function, and only allow the second. + # TODO: Can this be done better somehow? + if(NOT target IN_LIST external_targets) + _qt_internal_sbom_handle_qt_entity_cydx_properties( + SBOM_ENTITY_TYPE "${sbom_entity_type}" + OUT_CYDX_PROPERTIES cydx_properties + ) + + _qt_internal_sbom_generate_cyclone_add_package( + PACKAGE "${package_name_for_spdx_id}" + SPDXID "${package_spdx_id}" + SBOM_ENTITY_TYPE "${sbom_entity_type}" + ${project_package_options_cydx} + CONTAINING_COMPONENT "${project_spdx_id}" + CYDX_PROPERTIES ${cydx_properties} + ) + endif() + endif() set(no_install_option "") if(arg_NO_INSTALL) @@ -1199,25 +1493,27 @@ function(_qt_internal_sbom_add_target target) set(license_option LICENSE_EXPRESSION "${license_expression}") endif() - _qt_internal_sbom_handle_target_binary_files("${target}" - ${no_install_option} - ${framework_option} - ${install_prefix_option} - SBOM_ENTITY_TYPE "${sbom_entity_type}" - ${target_binary_multi_config_args} - SPDX_ID "${package_spdx_id}" - ${copyrights_option} - ${license_option} - ) + if(QT_SBOM_GENERATE_SPDX_V2) + _qt_internal_sbom_handle_target_binary_files("${target}" + ${no_install_option} + ${framework_option} + ${install_prefix_option} + SBOM_ENTITY_TYPE "${sbom_entity_type}" + ${target_binary_multi_config_args} + SPDX_ID "${package_spdx_id}" + ${copyrights_option} + ${license_option} + ) - _qt_internal_sbom_handle_target_custom_files("${target}" - ${no_install_option} - ${install_prefix_option} - PACKAGE_TYPE "${sbom_entity_type}" - PACKAGE_SPDX_ID "${package_spdx_id}" - ${copyrights_option} - ${license_option} - ) + _qt_internal_sbom_handle_target_custom_files("${target}" + ${no_install_option} + ${install_prefix_option} + PACKAGE_TYPE "${sbom_entity_type}" + PACKAGE_SPDX_ID "${package_spdx_id}" + ${copyrights_option} + ${license_option} + ) + endif() endfunction() # Helper to add sbom information for a possibly non-existing target. @@ -1576,7 +1872,11 @@ function(_qt_internal_sbom_record_target_spdx_id target) SBOM_ENTITY_TYPE "${arg_SBOM_ENTITY_TYPE}" PACKAGE_NAME "${package_name_for_spdx_id}" ) - _qt_internal_sbom_save_spdx_id_for_target("${target}" "${package_spdx_id}") + _qt_internal_sbom_save_spdx_id_for_target("${target}" + SPDX_ID "${package_spdx_id}" + PACKAGE_NAME "${package_name_for_spdx_id}" + SBOM_ENTITY_TYPE "${arg_SBOM_ENTITY_TYPE}" + ) _qt_internal_sbom_is_qt_entity_type("${arg_SBOM_ENTITY_TYPE}" is_qt_entity_type) _qt_internal_sbom_save_spdx_id_for_qt_entity_type( @@ -1615,10 +1915,36 @@ function(_qt_internal_sbom_generate_target_package_spdx_id out_var) endfunction() # Save a spdx id for a target inside its target properties. -# Also saves the repo document namespace and relative installed repo document path. # These are used when generating a SPDX external document reference for exported targets, to # include them in relationships. -function(_qt_internal_sbom_save_spdx_id_for_target target spdx_id) +# Also saves the repo document namespace and relative installed repo document path. +# Also saves the sbom entity type and package name, because it's needed when creating CycloneDX +# components where the target is in an external document. +function(_qt_internal_sbom_save_spdx_id_for_target target) + set(opt_args "") + set(single_args + SPDX_ID + PACKAGE_NAME + SBOM_ENTITY_TYPE + ) + set(multi_args "") + cmake_parse_arguments(PARSE_ARGV 1 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + if(NOT arg_SPDX_ID) + message(FATAL_ERROR "arg_SPDX_ID must be set") + endif() + + if(NOT arg_PACKAGE_NAME) + message(FATAL_ERROR "PACKAGE_NAME must be set") + endif() + + if(NOT arg_SBOM_ENTITY_TYPE) + message(FATAL_ERROR "SBOM_ENTITY_TYPE must be set") + endif() + + set(spdx_id "${arg_SPDX_ID}") + message(DEBUG "Saving spdx id for target ${target}: ${spdx_id}") set(target_unaliased "${target}") @@ -1631,6 +1957,9 @@ function(_qt_internal_sbom_save_spdx_id_for_target target spdx_id) get_property(repo_document_namespace GLOBAL PROPERTY _qt_internal_sbom_repo_document_namespace) + get_property(bom_serial_number_uuid + GLOBAL PROPERTY _qt_internal_sbom_repo_cyclone_dx_bom_serial_number_uuid) + get_property(relative_installed_repo_document_path GLOBAL PROPERTY _qt_internal_sbom_relative_installed_repo_document_path) @@ -1643,6 +1972,10 @@ function(_qt_internal_sbom_save_spdx_id_for_target target spdx_id) "${repo_document_namespace}") set_property(TARGET ${target_unaliased} PROPERTY + _qt_sbom_cydx_bom_serial_number_uuid + "${bom_serial_number_uuid}") + + set_property(TARGET ${target_unaliased} PROPERTY _qt_sbom_spdx_relative_installed_repo_document_path "${relative_installed_repo_document_path}") @@ -1650,11 +1983,22 @@ function(_qt_internal_sbom_save_spdx_id_for_target target spdx_id) _qt_sbom_spdx_repo_project_name_lowercase "${project_name_lowercase}") + set_property(TARGET ${target_unaliased} PROPERTY + _qt_sbom_package_name + "${arg_PACKAGE_NAME}") + + set_property(TARGET ${target_unaliased} PROPERTY + _qt_sbom_entity_type + "${arg_SBOM_ENTITY_TYPE}") + # Export the properties, so they can be queried by other repos. # We also do it for versionless targets. set(export_properties + _qt_sbom_entity_type + _qt_sbom_package_name _qt_sbom_spdx_id _qt_sbom_spdx_repo_document_namespace + _qt_sbom_cydx_bom_serial_number_uuid _qt_sbom_spdx_relative_installed_repo_document_path _qt_sbom_spdx_repo_project_name_lowercase ) @@ -1719,11 +2063,18 @@ function(_qt_internal_sbom_save_spdx_id_for_qt_entity_type target is_qt_entity_t versionless_target versionless_private_target ) + + get_property(package_name TARGET "${target}" PROPERTY _qt_sbom_package_name) + get_property(sbom_entity_type TARGET "${target}" PROPERTY _qt_sbom_entity_type) endif() foreach(target_name IN LISTS ${target_names}) if(TARGET "${target_name}") - _qt_internal_sbom_save_spdx_id_for_target("${target_name}" "${package_spdx_id}") + _qt_internal_sbom_save_spdx_id_for_target("${target_name}" + SPDX_ID "${package_spdx_id}" + PACKAGE_NAME "${package_name}" + SBOM_ENTITY_TYPE "${sbom_entity_type}" + ) endif() endforeach() endfunction() @@ -2025,7 +2376,12 @@ endfunction() function(_qt_internal_sbom_compute_project_file_name out_var) set(opt_args - EXTENSION_JSON + SPDX_TAG_VALUE + SPDX_JSON + CYCLONEDX_JSON + CYCLONEDX_TOML + + EXTENSION_JSON # deprecated, used by WebEngine ) set(single_args PROJECT_NAME @@ -2040,6 +2396,16 @@ function(_qt_internal_sbom_compute_project_file_name out_var) message(FATAL_ERROR "PROJECT_NAME must be set") endif() + if(NOT arg_SPDX_JSON + AND NOT arg_SPDX_TAG_VALUE + AND NOT arg_CYCLONEDX_TOML + AND NOT arg_CYCLONEDX_JSON + AND NOT arg_EXTENSION_JSON + ) + message(FATAL_ERROR "One of the following options should be set: " + "SPDX_TAG_VALUE, SPDX_JSON, CYCLONEDX_JSON, CYCLONEDX_TOML") + endif() + string(TOLOWER "${arg_PROJECT_NAME}" project_name_lowercase) set(version_suffix "") @@ -2050,10 +2416,16 @@ function(_qt_internal_sbom_compute_project_file_name out_var) set(version_suffix "-${QT_REPO_MODULE_VERSION}") endif() - if(arg_EXTENSION_JSON) + if(arg_SPDX_TAG_VALUE) + set(extension "spdx") + elseif(arg_SPDX_JSON OR arg_EXTENSION_JSON) set(extension "spdx.json") + elseif(arg_CYCLONEDX_TOML) + set(extension "cdx.toml") + elseif(arg_CYCLONEDX_JSON) + set(extension "cdx.json") else() - set(extension "spdx") + message(FATAL_ERROR "Unknown file extension for SBOM generation.") endif() set(result diff --git a/cmake/QtPublicSbomLicenseHelpers.cmake b/cmake/QtPublicSbomLicenseHelpers.cmake index a91f4c2cefa..a9529fbe3d7 100644 --- a/cmake/QtPublicSbomLicenseHelpers.cmake +++ b/cmake/QtPublicSbomLicenseHelpers.cmake @@ -44,10 +44,20 @@ function(_qt_internal_sbom_add_license) set(license_id "LicenseRef-${license_id}") endif() - _qt_internal_sbom_generate_add_license( - LICENSE_ID "${license_id}" - EXTRACTED_TEXT "<text>${text}</text>" - ) + + if(QT_SBOM_GENERATE_SPDX_V2) + _qt_internal_sbom_generate_add_license( + LICENSE_ID "${license_id}" + EXTRACTED_TEXT "<text>${text}</text>" + ) + endif() + + if(QT_SBOM_GENERATE_CYDX_V1_6) + _qt_internal_sbom_record_license_cydx( + LICENSE_ID "${license_id}" + EXTRACTED_TEXT "${text}" + ) + endif() endfunction() # Get a qt spdx license expression given the id. diff --git a/cmake/QtPublicSbomOpsHelpers.cmake b/cmake/QtPublicSbomOpsHelpers.cmake index 58af2a57f1f..78c4eaaa5e6 100644 --- a/cmake/QtPublicSbomOpsHelpers.cmake +++ b/cmake/QtPublicSbomOpsHelpers.cmake @@ -6,14 +6,28 @@ # like NTIA validation, auditing, json generation, etc. function(_qt_internal_sbom_setup_project_ops_generation) set(opt_args + # spdx options GENERATE_JSON GENERATE_JSON_REQUIRED + + # cydx options + GENERATE_CYCLONE_DX_V1_6 + GENERATE_CYCLONE_DX_V1_6_REQUIRED + VERIFY_CYCLONE_DX_V1_6 + VERIFY_CYCLONE_DX_V1_6_REQUIRED + VERBOSE_CYCLONE_DX_V1_6 + + # source spdx options GENERATE_SOURCE_SBOM + LINT_SOURCE_SBOM + LINT_SOURCE_SBOM_NO_ERROR + + # Extra spdx verification VERIFY_SBOM VERIFY_SBOM_REQUIRED VERIFY_NTIA_COMPLIANT - LINT_SOURCE_SBOM - LINT_SOURCE_SBOM_NO_ERROR + + # Extra spdx info SHOW_TABLE AUDIT AUDIT_NO_ERROR @@ -38,6 +52,33 @@ function(_qt_internal_sbom_setup_project_ops_generation) endif() endif() + if(arg_GENERATE_CYCLONE_DX_V1_6 AND NOT QT_INTERNAL_NO_SBOM_PYTHON_OPS) + set(op_args + OUT_VAR_DEPS_FOUND deps_found + ) + if(arg_GENERATE_CYCLONE_DX_V1_6_REQUIRED) + list(APPEND op_args REQUIRED) + endif() + + set(gen_args "") + if(arg_VERIFY_CYCLONE_DX_V1_6) + list(APPEND gen_args VERIFY) + endif() + + if(arg_VERIFY_CYCLONE_DX_V1_6_REQUIRED) + list(APPEND gen_args VERIFY_REQUIRED) + endif() + + if(arg_VERBOSE_CYCLONE_DX_V1_6) + list(APPEND gen_args VERBOSE) + endif() + + _qt_internal_sbom_find_cydx_dependencies(${op_args}) + if(deps_found) + _qt_internal_sbom_generate_cydx_json(${gen_args}) + endif() + endif() + if(arg_VERIFY_SBOM AND NOT QT_INTERNAL_NO_SBOM_PYTHON_OPS) set(op_args OP_KEY "VERIFY_SBOM" @@ -90,12 +131,119 @@ function(_qt_internal_sbom_setup_project_ops_generation) endif() endfunction() +# Tries to find the dependencies for generating CycloneDX v1.6 SBOMs. +# Sets OUT_VAR_DEPS_FOUND to TRUE if found, FALSE otherwise. +function(_qt_internal_sbom_find_cydx_dependencies) + set(opt_args + REQUIRED + ) + set(single_args + OUT_VAR_DEPS_FOUND + ) + set(multi_args "") + cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") + + if(NOT arg_OUT_VAR_DEPS_FOUND) + message(FATAL_ERROR "OUT_VAR_DEPS_FOUND is required") + endif() + + set(op_args + OP_KEY "GENERATE_CYCLONE_DX_V1_6" + OUT_VAR_DEPS_FOUND deps_found + ) + + if(arg_REQUIRED) + list(APPEND op_args REQUIRED) + endif() + + _qt_internal_sbom_find_and_handle_sbom_op_dependencies(${op_args}) + + set(${arg_OUT_VAR_DEPS_FOUND} "${deps_found}" PARENT_SCOPE) +endfunction() + +# Helper to output a debug or error message when python or one of its dependencies are not found. +# When found, it also caches the found python interpreter path and version, as well as the +# DEPS_FOUND_FOR_$OP_KEY var. +function(_qt_internal_sbom_verify_if_sbom_op_dependencies_are_found) + set(opt_args + REQUIRED + PYTHON_FOUND + DEP_FOUND + EVERYTHING_FOUND + ) + set(single_args + REQUIRED_VERSION + PYTHON_PATH + PYTHON_VERSION + DEP_PACKAGE_NAME + DEP_FIND_OUTPUT + OP_KEY + ) + set(multi_args + PYTHON_COMMON_ARGS + ) + cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") + + if(arg_EVERYTHING_FOUND) + message(DEBUG "Python Dependencies found. " + "Using Python '${arg_PYTHON_PATH}' for running SBOM op '${arg_OP_KEY}'.") + + if(NOT QT_INTERNAL_SBOM_PYTHON_EXECUTABLE) + message(DEBUG "Setting QT_INTERNAL_SBOM_PYTHON_EXECUTABLE to ${arg_PYTHON_PATH}") + message(DEBUG "Setting QT_INTERNAL_SBOM_PYTHON_VERSION to ${arg_PYTHON_VERSION}") + set(QT_INTERNAL_SBOM_PYTHON_EXECUTABLE "${arg_PYTHON_PATH}" CACHE INTERNAL + "Python interpeter used for SBOM generation.") + set(QT_INTERNAL_SBOM_PYTHON_VERSION "${arg_PYTHON_VERSION}" CACHE INTERNAL + "Python interpeter version used for SBOM generation.") + endif() + + set(QT_INTERNAL_SBOM_DEPS_FOUND_FOR_${arg_OP_KEY} "TRUE" CACHE INTERNAL + "All dependencies found to run SBOM op '${arg_OP_KEY}'") + return() + endif() + + if(arg_REQUIRED) + set(message_type "FATAL_ERROR") + else() + set(message_type "DEBUG") + endif() + + if(NOT arg_PYTHON_FOUND) + # Look for python one more time, this time without QUIET, to show an error why it + # wasn't found. + if(arg_REQUIRED) + _qt_internal_sbom_find_python_helper(${arg_PYTHON_COMMON_ARGS} + OUT_VAR_PYTHON_PATH unused_python + OUT_VAR_PYTHON_FOUND unused_found + ) + endif() + message(${message_type} + "Python ${arg_REQUIRED_VERSION} for running SBOM op '${arg_OP_KEY}' NOT found.") + elseif(NOT arg_DEP_FOUND) + + set(extra_install_message "") + if(message_type STREQUAL "FATAL_ERROR") + set(extra_install_message + "Install it using pip install '${arg_DEP_PACKAGE_NAME}' \n") + endif() + message(${message_type} + "Python dependency for running SBOM op '${arg_OP_KEY}' NOT found:\n" + ${extra_install_message} + "Details:\n" + "Python interpreter: ${arg_PYTHON_PATH}\n" + "Error output: \n${arg_DEP_FIND_OUTPUT}\n\n" + ) + endif() +endfunction() + # Helper to find a python interpreter and a specific python dependency, e.g. to be able to generate # a SPDX JSON SBOM, or run post-installation steps like NTIA verification. # The exact dependency should be specified as the OP_KEY. # # Caches the found python executable in a separate cache var QT_INTERNAL_SBOM_PYTHON_EXECUTABLE, to # avoid conflicts with any other found python interpreter. +# Also caches the python version found in QT_INTERNAL_SBOM_PYTHON_VERSION, so we can show it +# in the configure summary. function(_qt_internal_sbom_find_and_handle_sbom_op_dependencies) set(opt_args REQUIRED @@ -112,12 +260,22 @@ function(_qt_internal_sbom_find_and_handle_sbom_op_dependencies) message(FATAL_ERROR "OP_KEY is required") endif() - set(supported_ops "GENERATE_JSON" "VERIFY_SBOM" "RUN_NTIA") + set(supported_ops + "GENERATE_JSON" + "GENERATE_CYCLONE_DX_V1_6" + "VERIFY_SBOM" + "RUN_NTIA" + ) if(arg_OP_KEY STREQUAL "GENERATE_JSON" OR arg_OP_KEY STREQUAL "VERIFY_SBOM") set(import_statement "import spdx_tools.spdx.clitools.pyspdxtools") + set(dep_package_name "spdx-tools") + elseif(arg_OP_KEY STREQUAL "GENERATE_CYCLONE_DX_V1_6") + set(import_statement "from cyclonedx.output.json import JsonV1Dot6") + set(dep_package_name "cyclonedx-python-lib[json-validation]") elseif(arg_OP_KEY STREQUAL "RUN_NTIA") set(import_statement "import ntia_conformance_checker.main") + set(dep_package_name "ntia-conformance-checker") else() message(FATAL_ERROR "OP_KEY must be one of ${supported_ops}") endif() @@ -145,48 +303,81 @@ function(_qt_internal_sbom_find_and_handle_sbom_op_dependencies) # non-framework python found. if(CMAKE_HOST_APPLE) set(extra_python_args SEARCH_IN_FRAMEWORKS QUIET) + message(DEBUG "Looking for Python and dependencies for op '${arg_OP_KEY}' " + "in the system framework location first.") _qt_internal_sbom_find_python_and_dependency_helper_lambda() + if(NOT everything_found) + # Assemble args to show what wasn't found. + set(verify_args "") + if(python_found) + list(APPEND verify_args PYTHON_FOUND) + endif() + if(dep_found) + list(APPEND verify_args DEP_FOUND) + endif() + if(everything_found) + list(APPEND verify_args EVERYTHING_FOUND) + endif() + if(python_path) + list(APPEND verify_args PYTHON_PATH "${python_path}") + endif() + if(python_version) + list(APPEND verify_args PYTHON_VERSION "${python_version}") + endif() + if(dep_find_output) + list(APPEND verify_args DEP_FIND_OUTPUT "${dep_find_output}") + endif() + + _qt_internal_sbom_verify_if_sbom_op_dependencies_are_found( + ${verify_args} + REQUIRED_VERSION "${required_version}" + OP_KEY "${arg_OP_KEY}" + DEP_PACKAGE_NAME "${dep_package_name}" + PYTHON_COMMON_ARGS ${python_common_args} + ) + + message(DEBUG "Initial Apple-specific SBOM Python search did not find all dependencies " + "for op ${arg_OP_KEY}, looking again outside of the system framework location.") + endif() endif() + # Looking again if something wasn't found, or a search was not done yet. if(NOT everything_found) set(extra_python_args QUIET) + message(DEBUG "Looking for Python and dependencies for op '${arg_OP_KEY}'.") _qt_internal_sbom_find_python_and_dependency_helper_lambda() endif() - # Always save the python interpreter path if it is found, even if the dependencies are not - # found. This improves the error message workflow. - if(python_found AND NOT QT_INTERNAL_SBOM_PYTHON_EXECUTABLE) - set(QT_INTERNAL_SBOM_PYTHON_EXECUTABLE "${python_path}" CACHE INTERNAL - "Python interpeter used for SBOM generation.") + # Assemble args to show what was or wasn't found. + set(verify_args "") + if(arg_REQUIRED) + list(APPEND verify_args REQUIRED) endif() - - if(NOT everything_found) - if(arg_REQUIRED) - set(message_type "FATAL_ERROR") - else() - set(message_type "DEBUG") - endif() - - if(NOT python_found) - # Look for python one more time, this time without QUIET, to show an error why it - # wasn't found. - if(arg_REQUIRED) - _qt_internal_sbom_find_python_helper(${python_common_args} - OUT_VAR_PYTHON_PATH unused_python - OUT_VAR_PYTHON_FOUND unused_found - ) - endif() - message(${message_type} "Python ${required_version} for running SBOM ops not found.") - elseif(NOT dep_found) - message(${message_type} "Python dependency for running SBOM op ${arg_OP_KEY} " - "not found:\n Python: ${python_path} \n Output: \n${dep_find_output}") - endif() - else() - message(DEBUG "Using Python ${python_path} for running SBOM ops.") - - set(QT_INTERNAL_SBOM_DEPS_FOUND_FOR_${arg_OP_KEY} "TRUE" CACHE INTERNAL - "All dependencies found to run SBOM OP ${arg_OP_KEY}") + if(python_found) + list(APPEND verify_args PYTHON_FOUND) + endif() + if(dep_found) + list(APPEND verify_args DEP_FOUND) + endif() + if(everything_found) + list(APPEND verify_args EVERYTHING_FOUND) + endif() + if(python_path) + list(APPEND verify_args PYTHON_PATH "${python_path}") endif() + if(python_version) + list(APPEND verify_args PYTHON_VERSION "${python_version}") + endif() + if(dep_find_output) + list(APPEND verify_args DEP_FIND_OUTPUT "${dep_find_output}") + endif() + _qt_internal_sbom_verify_if_sbom_op_dependencies_are_found( + ${verify_args} + REQUIRED_VERSION "${required_version}" + OP_KEY "${arg_OP_KEY}" + DEP_PACKAGE_NAME "${dep_package_name}" + PYTHON_COMMON_ARGS ${python_common_args} + ) if(arg_OUT_VAR_DEPS_FOUND) set(${arg_OUT_VAR_DEPS_FOUND} "${QT_INTERNAL_SBOM_DEPS_FOUND_FOR_${arg_OP_KEY}}" @@ -298,7 +489,7 @@ function(_qt_internal_sbom_check_python_dependency_available) _qt_internal_validate_all_args_are_parsed(arg) set(failure_message - "Required Python dependencies not found: ") + "Required Python dependencies NOT found: ") if(arg_FAILURE_MESSAGE_PREFIX) list(PREPEND failure_message ${arg_FAILURE_MESSAGE_PREFIX}) @@ -393,6 +584,67 @@ function(_qt_internal_sbom_generate_json) set_property(GLOBAL APPEND PROPERTY _qt_sbom_cmake_verify_include_files "${verify_sbom}") endfunction() +# Helper to generate a CycloneDX JSON file from the intermediate .toml file created by the build +# system. +function(_qt_internal_sbom_generate_cydx_json) + set(opt_args + VERIFY + VERIFY_REQUIRED + VERBOSE + ) + set(single_args "") + set(multi_args "") + cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + set(error_message_prefix "Failed to generate a CycloneDX json file.") + _qt_internal_sbom_assert_python_interpreter_available("${error_message_prefix}") + _qt_internal_sbom_assert_python_dependency_available(GENERATE_CYCLONE_DX_V1_6 + "cyclonedx" ${error_message_prefix}) + + _qt_internal_sbom_get_cyclone_dx_generator_path(generator_path) + + set(extra_args "") + if(arg_VERIFY) + list(APPEND extra_args "--validate-json") + endif() + if(arg_VERIFY_REQUIRED) + list(APPEND extra_args "--validate-json-required") + endif() + if(arg_VERBOSE) + list(APPEND extra_args "--verbose") + endif() + list(JOIN extra_args " " extra_args) + + set(content " + message(STATUS + \"Generating final CycloneDX JSON: \${QT_SBOM_OUTPUT_PATH_WITHOUT_EXT}.json\") + execute_process( + COMMAND \"${QT_INTERNAL_SBOM_PYTHON_EXECUTABLE}\" \"${generator_path}\" + --input-path \"\${QT_SBOM_OUTPUT_PATH}\" + --output-path \"\${QT_SBOM_OUTPUT_PATH_WITHOUT_EXT}.json\" + ${extra_args} + RESULT_VARIABLE res + ) + if(NOT res EQUAL 0) + message(FATAL_ERROR \"CycloneDX JSON generation failed: \${res}\") + endif() + # Remove the intermediate toml file, there's no point in installing it. + # Keep the one in the build dir, it's useful for debugging. + if(NOT QT_SBOM_BUILD_TIME) + message(STATUS \"Removing intermediate TOML file \${QT_SBOM_OUTPUT_PATH}\") + file(REMOVE \"\${QT_SBOM_OUTPUT_PATH}\") + endif() +") + + _qt_internal_get_current_project_sbom_dir(sbom_dir) + set(generate_cydx "${sbom_dir}/generate_cyclone_dx.cmake") + file(GENERATE OUTPUT "${generate_cydx}" CONTENT "${content}") + + set_property(GLOBAL APPEND PROPERTY + _qt_sbom_cmake_post_generation_include_files_cydx "${generate_cydx}") +endfunction() + # Helper to query whether the all required dependencies are available to generate a tag / value # document from a json one. function(_qt_internal_sbom_verify_deps_for_generate_tag_value_spdx_document) diff --git a/cmake/QtPublicSbomPurlHelpers.cmake b/cmake/QtPublicSbomPurlHelpers.cmake index 6f91ea36fed..f6738a2431f 100644 --- a/cmake/QtPublicSbomPurlHelpers.cmake +++ b/cmake/QtPublicSbomPurlHelpers.cmake @@ -86,20 +86,31 @@ endmacro() # default purls will be generated. # # There is no limit to the number of purls that can be added to a target. +# The created purls are saved in: +# - OUT_VAR_PURL_VALUES as plain purl values, to be used for CycloneDX genereation. +# - OUT_VAR_SPDX_EXT_REF_VALUES as SPDX ExtRef entries, to be used for SPDX v2.3 generation. function(_qt_internal_sbom_handle_purl_values target) _qt_internal_get_sbom_purl_handling_options(opt_args single_args multi_args) - list(APPEND single_args OUT_VAR) + list(APPEND single_args + OUT_VAR_SPDX_EXT_REF_VALUES + OUT_VAR_PURL_VALUES + ) cmake_parse_arguments(PARSE_ARGV 1 arg "${opt_args}" "${single_args}" "${multi_args}") _qt_internal_validate_all_args_are_parsed(arg) - if(NOT arg_OUT_VAR) - message(FATAL_ERROR "OUT_VAR must be set") + if(NOT arg_OUT_VAR_PURL_VALUES) + message(FATAL_ERROR "OUT_VAR_PURL_VALUES must be set") + endif() + + if(NOT arg_OUT_VAR_SPDX_EXT_REF_VALUES) + message(FATAL_ERROR "OUT_VAR_SPDX_EXT_REF_VALUES must be set") endif() _qt_internal_get_sbom_purl_parsing_options(purl_opt_args purl_single_args purl_multi_args) - set(project_package_options "") + set(purl_values "") + set(spdx_ext_ref_values "") # Collect each PURL_ENTRY args into a separate variable. set(purl_idx -1) @@ -140,6 +151,16 @@ function(_qt_internal_sbom_handle_purl_values target) endif() endif() + set(qt_entity_cydx_purl_values "") + set(qt_entity_spdx_purl_ext_refs "") + + # When generating purls for Qt entities targeting Cyclone DX, we prefer the generic purl first, + # otherwise DependencyTrack gets confused with lots of components having the same purls, + # because it doesn't take into account the '#' part of the purl. + # Keep these separate and append them in the right order later. + set(qt_entity_cydx_purl_for_github_id "") + set(qt_entity_cydx_purl_for_generic_id "") + foreach(purl_idx IN LISTS purl_entry_indices) # Clear previous values. foreach(option_name IN LISTS purl_opt_args purl_single_args purl_multi_args) @@ -172,11 +193,13 @@ function(_qt_internal_sbom_handle_purl_values target) ${purl_multi_args} ) + set(is_qt_entity_purl FALSE) # Qt entity types get special treatment to gather the required args. if(arg___QT_INTERNAL_HANDLE_QT_ENTITY_TYPE_PURL AND arg_PURL_ID AND arg_PURL_ID IN_LIST qt_purl_ids) + set(is_qt_entity_purl TRUE) _qt_internal_sbom_handle_qt_entity_purl("${target}" ${purl_handling_args} PURL_ID "${arg_PURL_ID}" @@ -189,29 +212,57 @@ function(_qt_internal_sbom_handle_purl_values target) _qt_internal_sbom_assemble_purl(${target} ${purl_args} - OUT_VAR package_manager_external_ref + OUT_VAR purl_bare + OUT_VAR_SPDX_EXT_REF package_manager_external_ref_purl ) - list(APPEND project_package_options ${package_manager_external_ref}) + + if(is_qt_entity_purl) + if(arg_PURL_ID STREQUAL "GENERIC") + set(qt_entity_cydx_purl_for_generic_id "${purl_bare}") + elseif(arg_PURL_ID STREQUAL "GITHUB") + set(qt_entity_cydx_purl_for_github_id "${purl_bare}") + else() + list(APPEND purl_values "${purl_bare}") + endif() + else() + list(APPEND purl_values "${purl_bare}") + endif() + + list(APPEND spdx_ext_ref_values ${package_manager_external_ref_purl}) endforeach() + # Add the custom qt entity purls at the front in the right order for CycloneDX. + # If they are empty (for non-Qt entities), nothing will be prepended. + set(qt_entity_cydx_purl_values + ${qt_entity_cydx_purl_for_generic_id} + ${qt_entity_cydx_purl_for_github_id} + ) + list(PREPEND purl_values ${qt_entity_cydx_purl_values}) + foreach(purl_value IN LISTS arg_PURL_VALUES) - _qt_internal_sbom_get_purl_value_extref( - VALUE "${purl_value}" OUT_VAR package_manager_external_ref) + _qt_internal_sbom_get_purl_value_extref(VALUE "${purl_value}" + OUT_VAR package_manager_external_ref_purl) # The order in which the purls are generated, matters for tools that consume the SBOM. # Some tools can only handle one PURL per package, so the first one should be the # important one. # For now, I deem that the directly specified ones (probably via a qt_attribution.json # file) are the more important ones. So we prepend them. - list(PREPEND project_package_options ${package_manager_external_ref}) + list(PREPEND purl_values ${purl_value}) + list(PREPEND spdx_ext_ref_values ${package_manager_external_ref_purl}) endforeach() - set(${arg_OUT_VAR} "${project_package_options}" PARENT_SCOPE) + set(${arg_OUT_VAR_PURL_VALUES} "${purl_values}" PARENT_SCOPE) + set(${arg_OUT_VAR_SPDX_EXT_REF_VALUES} "${spdx_ext_ref_values}" PARENT_SCOPE) endfunction() # Assembles an external reference purl identifier. +# # PURL_TYPE and PURL_NAME are required. -# Stores the result in the OUT_VAR. +# +# Stores the bare purl in the OUT_VAR. +# Stores the SPDX External Reference purl in the OUT_VAR_SPDX_EXT_REF. +# # Accepted options: # PURL_TYPE # PURL_NAME @@ -223,6 +274,7 @@ function(_qt_internal_sbom_assemble_purl target) set(opt_args "") set(single_args OUT_VAR + OUT_VAR_SPDX_EXT_REF ) set(multi_args "") @@ -273,9 +325,10 @@ function(_qt_internal_sbom_assemble_purl target) string(APPEND purl "#${arg_PURL_SUBPATH}") endif() - _qt_internal_sbom_get_purl_value_extref(VALUE "${purl}" OUT_VAR result) + _qt_internal_sbom_get_purl_value_extref(VALUE "${purl}" OUT_VAR ext_ref_result) - set(${arg_OUT_VAR} "${result}" PARENT_SCOPE) + set(${arg_OUT_VAR} "${purl}" PARENT_SCOPE) + set(${arg_OUT_VAR_SPDX_EXT_REF} "${ext_ref_result}" PARENT_SCOPE) endfunction() # Takes a PURL VALUE and returns an SBOM purl external reference in OUT_VAR. diff --git a/cmake/QtPublicSbomPythonHelpers.cmake b/cmake/QtPublicSbomPythonHelpers.cmake index f83314916f9..fef87af9ee4 100644 --- a/cmake/QtPublicSbomPythonHelpers.cmake +++ b/cmake/QtPublicSbomPythonHelpers.cmake @@ -12,6 +12,7 @@ macro(_qt_internal_sbom_find_python_and_dependency_helper_lambda) DEPENDENCY_IMPORT_STATEMENT "${import_statement}" OUT_VAR_PYTHON_PATH python_path OUT_VAR_PYTHON_FOUND python_found + OUT_VAR_PYTHON_VERSION python_version OUT_VAR_DEP_FOUND dep_found OUT_VAR_PYTHON_AND_DEP_FOUND everything_found OUT_VAR_DEP_FIND_OUTPUT dep_find_output @@ -27,6 +28,7 @@ function(_qt_internal_sbom_find_python_and_dependency_helper) set(single_args OUT_VAR_PYTHON_PATH OUT_VAR_PYTHON_FOUND + OUT_VAR_PYTHON_VERSION OUT_VAR_DEP_FOUND OUT_VAR_PYTHON_AND_DEP_FOUND OUT_VAR_DEP_FIND_OUTPUT @@ -65,6 +67,7 @@ function(_qt_internal_sbom_find_python_and_dependency_helper) ${arg_PYTHON_ARGS} OUT_VAR_PYTHON_PATH python_path_inner OUT_VAR_PYTHON_FOUND python_found_inner + OUT_VAR_PYTHON_VERSION python_version_inner ) if(python_found_inner AND python_path_inner) @@ -82,6 +85,9 @@ function(_qt_internal_sbom_find_python_and_dependency_helper) set(${arg_OUT_VAR_PYTHON_PATH} "${python_path_inner}" PARENT_SCOPE) set(${arg_OUT_VAR_PYTHON_FOUND} "${python_found_inner}" PARENT_SCOPE) + if(arg_OUT_VAR_PYTHON_VERSION) + set(${arg_OUT_VAR_PYTHON_VERSION} "${python_version_inner}" PARENT_SCOPE) + endif() set(${arg_OUT_VAR_DEP_FOUND} "${dep_found_inner}" PARENT_SCOPE) set(${arg_OUT_VAR_PYTHON_AND_DEP_FOUND} "${everything_found_inner}" PARENT_SCOPE) set(${arg_OUT_VAR_DEP_FIND_OUTPUT} "${dep_find_output_inner}" PARENT_SCOPE) @@ -90,7 +96,8 @@ endfunction() # Tries to find the python intrepreter, given the QT_SBOM_PYTHON_INTERP path hint, as well as # other options. # Ignores any previously found python. -# Returns the python interpreter path and whether it was successfully found. +# Returns the python interpreter path and whether it was successfully found, along with the version +# found. # # This is intentionally a function, and not a macro, to prevent overriding the Python3_EXECUTABLE # non-cache variable in a global scope in case if a different python is found and used for a @@ -109,6 +116,7 @@ function(_qt_internal_sbom_find_python_helper) VERSION OUT_VAR_PYTHON_PATH OUT_VAR_PYTHON_FOUND + OUT_VAR_PYTHON_VERSION ) set(multi_args "") cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") @@ -154,6 +162,9 @@ function(_qt_internal_sbom_find_python_helper) set(${arg_OUT_VAR_PYTHON_PATH} "${Python3_EXECUTABLE}" PARENT_SCOPE) set(${arg_OUT_VAR_PYTHON_FOUND} "${Python3_Interpreter_FOUND}" PARENT_SCOPE) + if(arg_OUT_VAR_PYTHON_VERSION) + set(${arg_OUT_VAR_PYTHON_VERSION} "${Python3_VERSION}" PARENT_SCOPE) + endif() endfunction() # Helper that takes an python import statement to run using the given python interpreter path, @@ -186,7 +197,7 @@ function(_qt_internal_sbom_find_python_dependency_helper) set(python_path "${arg_PYTHON_PATH}") execute_process( COMMAND - ${python_path} -c "${arg_DEPENDENCY_IMPORT_STATEMENT}" + "${python_path}" -c "${arg_DEPENDENCY_IMPORT_STATEMENT}" RESULT_VARIABLE res OUTPUT_VARIABLE output ERROR_VARIABLE output @@ -197,7 +208,7 @@ function(_qt_internal_sbom_find_python_dependency_helper) set(output "${output}") else() set(found FALSE) - string(CONCAT output "SBOM Python dependency ${arg_DEPENDENCY_IMPORT_STATEMENT} not found. " + string(CONCAT output "SBOM Python dependency ${arg_DEPENDENCY_IMPORT_STATEMENT} NOT found. " "Error:\n${output}") endif() @@ -245,6 +256,6 @@ function(_qt_internal_sbom_find_python_dependency_program) set(message_type "STATUS") set(prefix "Optional ") endif() - message(${message_type} "${prefix}SBOM python program '${program_name}' not found.") + message(${message_type} "${prefix}SBOM python program '${program_name}' NOT found.") endif() endfunction() diff --git a/cmake/QtPublicSbomQtEntityHelpers.cmake b/cmake/QtPublicSbomQtEntityHelpers.cmake index dfe3bf2be0a..d8d61eef42c 100644 --- a/cmake/QtPublicSbomQtEntityHelpers.cmake +++ b/cmake/QtPublicSbomQtEntityHelpers.cmake @@ -393,9 +393,11 @@ endfunction() function(_qt_internal_sbom_handle_qt_entity_purl_entries) _qt_internal_get_sbom_purl_handling_options(opt_args single_args multi_args) list(APPEND single_args - OUT_VAR # This is unused, but added by the calling function. + OUT_VAR_SPDX_EXT_REF_VALUES # This is unused, but added by the calling function. + OUT_VAR_PURL_VALUES # This is unused, but added by the calling function. OUT_VAR_IDS ) + cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") _qt_internal_validate_all_args_are_parsed(arg) @@ -418,7 +420,8 @@ endfunction() function(_qt_internal_sbom_handle_qt_entity_purl target) _qt_internal_get_sbom_purl_handling_options(opt_args single_args multi_args) list(APPEND single_args - OUT_VAR # This is unused, but added by the calling function. + OUT_VAR_SPDX_EXT_REF_VALUES # This is unused, but added by the calling function. + OUT_VAR_PURL_VALUES # This is unused, but added by the calling function. OUT_PURL_ARGS PURL_ID ) diff --git a/cmake/QtSbomHelpers.cmake b/cmake/QtSbomHelpers.cmake index 75694179fd1..c3ddc581427 100644 --- a/cmake/QtSbomHelpers.cmake +++ b/cmake/QtSbomHelpers.cmake @@ -194,3 +194,101 @@ function(qt_internal_extend_qt_entity_sbom target) qt_internal_sbom_get_default_sbom_args("${target}" sbom_extra_args ${ARGN}) _qt_internal_extend_sbom(${target} ${ARGN} ${sbom_extra_args}) endfunction() + +# Helper function to convert a boolean SBOM option into a "yes" / "no" string. +function(qt_internal_is_sbom_option_enabled var_name out_var) + if("${${var_name}}") + set(value "yes") + else() + set(value "no") + endif() + set(${out_var} "${value}" PARENT_SCOPE) +endfunction() + +# Helper function to get a summary suffix for SBOM options that are enabled, but might be skipped +# if their dependencies are missing. +function(qt_internal_get_sbom_option_required_suffix var_name out_var) + if("${${var_name}}") + set(value "") + else() + set(value " (skipped if dependencies are missing)") + endif() + set(${out_var} "${value}" PARENT_SCOPE) +endfunction() + +# Adds SBOM summary info to the configuration summary. +function(qt_internal_add_sbom_summary_info) + qt_configure_add_summary_section(NAME "SBOM") + + # Build SBOM info. + qt_internal_is_sbom_option_enabled(QT_GENERATE_SBOM value) + qt_configure_add_summary_entry(ARGS "Generate SBOM" TYPE "message" MESSAGE "${value}") + + # Only show the details if generation is enabled. + if(QT_GENERATE_SBOM) + qt_internal_is_sbom_option_enabled(QT_SBOM_GENERATE_SPDX_V2 value) + qt_configure_add_summary_entry(ARGS "Generate SPDX v2.3" + TYPE "message" MESSAGE "${value}") + + qt_internal_is_sbom_option_enabled(QT_SBOM_GENERATE_SPDX_V2_JSON value) + qt_internal_get_sbom_option_required_suffix(QT_SBOM_REQUIRE_GENERATE_SPDX_V2_JSON suffix) + qt_configure_add_summary_entry(ARGS "Generate SPDX v2.3 JSON" + TYPE "message" MESSAGE "${value}${suffix}") + + qt_internal_is_sbom_option_enabled(QT_SBOM_VERIFY_SPDX_V2 value) + qt_internal_get_sbom_option_required_suffix(QT_SBOM_REQUIRE_VERIFY_SPDX_V2 suffix) + qt_configure_add_summary_entry(ARGS "Verify SPDX v2.3 JSON" + TYPE "message" MESSAGE "${value}${suffix}") + + qt_internal_is_sbom_option_enabled(QT_SBOM_GENERATE_CYDX_V1_6 value) + qt_internal_get_sbom_option_required_suffix(QT_SBOM_REQUIRE_GENERATE_CYDX_V1_6 suffix) + qt_configure_add_summary_entry(ARGS "Generate CyloneDX v1.6" + TYPE "message" MESSAGE "${value}${suffix}") + + qt_internal_is_sbom_option_enabled(QT_SBOM_VERIFY_CYDX_V1_6 value) + qt_internal_get_sbom_option_required_suffix(QT_SBOM_REQUIRE_VERIFY_CYDX_V1_6 suffix) + qt_configure_add_summary_entry(ARGS "Verify CyloneDX v1.6" + TYPE "message" MESSAGE "${value}${suffix}") + + # Python interpreter info. + if(QT_INTERNAL_SBOM_PYTHON_EXECUTABLE) + set(value "${QT_INTERNAL_SBOM_PYTHON_EXECUTABLE}") + string(APPEND value " (version ${QT_INTERNAL_SBOM_PYTHON_VERSION})") + else() + set(value "Not found") + endif() + qt_configure_add_summary_entry(ARGS "SBOM Python interpreter" + TYPE "message" MESSAGE "${value}") + + # These are kinda internal, so only show them when found. + if(QT_SBOM_PROGRAM_SBOM2DOC) + set(value "${QT_SBOM_PROGRAM_SBOM2DOC}") + qt_configure_add_summary_entry(ARGS "sbom2doc path" TYPE "message" MESSAGE "${value}") + endif() + + if(QT_SBOM_PROGRAM_SBOMAUDIT) + set(value "${QT_SBOM_PROGRAM_SBOMAUDIT}") + qt_configure_add_summary_entry(ARGS "sbomaudit path" TYPE "message" MESSAGE "${value}") + endif() + endif() + + # Source SBOM info. + qt_internal_is_sbom_option_enabled(QT_GENERATE_SOURCE_SBOM value) + qt_configure_add_summary_entry(ARGS "Generate source SPDX SBOM" + TYPE "message" MESSAGE "${value}") + + if(QT_GENERATE_SOURCE_SBOM) + qt_internal_is_sbom_option_enabled(QT_LINT_SOURCE_SBOM value) + qt_configure_add_summary_entry(ARGS "Verify source SPDX SBOM" + TYPE "message" MESSAGE "${value}") + + if(QT_SBOM_PROGRAM_REUSE) + set(value "${QT_SBOM_PROGRAM_REUSE}") + else() + set(value "Not found") + endif() + qt_configure_add_summary_entry(ARGS "reuse path" TYPE "message" MESSAGE "${value}") + endif() + + qt_configure_end_summary_section() +endfunction() diff --git a/cmake/QtTargetHelpers.cmake b/cmake/QtTargetHelpers.cmake index 632fb5b5644..7b2d51d69a7 100644 --- a/cmake/QtTargetHelpers.cmake +++ b/cmake/QtTargetHelpers.cmake @@ -841,13 +841,12 @@ function(qt_internal_export_additional_targets_file) qt_internal_append_export_additional_targets() - set_property(GLOBAL APPEND PROPERTY _qt_export_additional_targets_ids "${id}") set_property(GLOBAL APPEND PROPERTY _qt_export_additional_targets_export_name_prefix_${id} "${arg_EXPORT_NAME_PREFIX}") set_property(GLOBAL APPEND PROPERTY _qt_export_additional_targets_config_install_dir_${id} "${arg_CONFIG_INSTALL_DIR}") - qt_add_list_file_finalizer(qt_internal_export_additional_targets_file_finalizer) + qt_add_list_file_finalizer(qt_internal_export_additional_targets_file_finalizer "${id}") endfunction() function(qt_internal_get_export_additional_targets_id export_name out_var) @@ -913,19 +912,9 @@ function(qt_internal_validate_export_additional_targets) set(arg_TARGET_EXPORT_NAMES "${arg_TARGET_EXPORT_NAMES}" PARENT_SCOPE) endfunction() -# The finalizer might be called multiple times in the same scope, but only the first one will -# process all the ids. -function(qt_internal_export_additional_targets_file_finalizer) - get_property(ids GLOBAL PROPERTY _qt_export_additional_targets_ids) - - foreach(id ${ids}) - qt_internal_export_additional_targets_file_handler("${id}") - endforeach() - - set_property(GLOBAL PROPERTY _qt_export_additional_targets_ids "") -endfunction() - -function(qt_internal_export_additional_targets_file_handler id) +# The finalizer might be called multiple times in the same directory scope, but it will only process +# one specific id. +function(qt_internal_export_additional_targets_file_finalizer id) get_property(arg_EXPORT_NAME_PREFIX GLOBAL PROPERTY _qt_export_additional_targets_export_name_prefix_${id}) get_property(arg_CONFIG_INSTALL_DIR GLOBAL PROPERTY diff --git a/cmake/QtWrapperScriptHelpers.cmake b/cmake/QtWrapperScriptHelpers.cmake index ecdc043c49c..f76d52bca91 100644 --- a/cmake/QtWrapperScriptHelpers.cmake +++ b/cmake/QtWrapperScriptHelpers.cmake @@ -235,6 +235,7 @@ export CMAKE_GENERATOR=Xcode qt_internal_create_qt_configure_part_wrapper_script("STANDALONE_TESTS") qt_internal_create_qt_configure_part_wrapper_script("STANDALONE_EXAMPLES") + qt_internal_create_cyclone_dx_sbom_generator_script() if(NOT CMAKE_CROSSCOMPILING) qt_internal_create_qt_android_runner_wrapper_script() @@ -381,6 +382,23 @@ function(qt_internal_create_qt_configure_redo_script) set_property(GLOBAL PROPERTY _qt_configure_redo_script_created TRUE) endfunction() +function(qt_internal_create_cyclone_dx_sbom_generator_script) + _qt_internal_sbom_get_cyclone_dx_generator_script_name(generator_name generator_relative_dir) + + qt_path_join(build_dir_destination "${QT_BUILD_DIR}" "${INSTALL_LIBEXECDIR}") + qt_path_join(install_destination "${QT_INSTALL_DIR}" "${INSTALL_LIBEXECDIR}") + qt_path_join(script_path + "${CMAKE_CURRENT_SOURCE_DIR}" "${generator_relative_dir}" "${generator_name}") + + if(QT_WILL_INSTALL) + file(COPY "${script_path}" + DESTINATION "${build_dir_destination}" + ) + endif() + + qt_copy_or_install(PROGRAMS "${script_path}" DESTINATION "${install_destination}") +endfunction() + function(qt_internal_create_qt_android_runner_wrapper_script) qt_path_join(android_runner_destination "${QT_INSTALL_DIR}" "${INSTALL_LIBEXECDIR}") qt_path_join(android_runner "${CMAKE_CURRENT_SOURCE_DIR}" "libexec" "qt-android-runner.py") diff --git a/cmake/configure-cmake-mapping.md b/cmake/configure-cmake-mapping.md index fadc03c831b..72a30cd87b3 100644 --- a/cmake/configure-cmake-mapping.md +++ b/cmake/configure-cmake-mapping.md @@ -45,11 +45,16 @@ The following table describes the mapping of configure options to CMake argument | -device-option <key=value> | -DQT_QMAKE_DEVICE_OPTIONS=key1=value1;key2=value2 | Only used for generation qmake-compatibility files. | | | | The device options are written into mkspecs/qdevice.pri. | | -appstore-compliant | -DFEATURE_appstore_compliant=ON | | -| -sbom | -DQT_GENERATE_SBOM=ON | Enables generation and installation of a SPDX SBOM documents | -| -sbom-json | -DQT_SBOM_GENERATE_JSON=ON | Enables generation of SPDX SBOM in JSON format | -| -sbom-json-required | -DQT_SBOM_REQUIRE_GENERATE_JSON=ON | Fails the build if Python deps are not found | -| -sbom-verify | -DQT_SBOM_VERIFY=ON | Enables verification of generated SBOMs | -| -sbom-verify-required | -DQT_SBOM_REQUIRE_VERIFY=ON | Fails the build if Python deps are not found | +| -sbom | -DQT_GENERATE_SBOM=ON | Enables generation of SBOM documents (multiple formats) | +| -sbom-spdx-v2 | -DQT_SBOM_GENERATE_SPDX_V2=ON | Enables generation of SPDX v2.3 SBOM in tag:value format | +| -sbom-cyclonedx-v1_6 | -DQT_SBOM_GENERATE_CYDX_V1_6=ON | Enables generation of CycloneDX v1.6 SBOM in JSON format | +| -sbom-cyclonedx-v1_6-required | -DQT_SBOM_REQUIRE_GENERATE_CYDX_V1_6=ON | Fails the build if Cyclone DX Python deps are not found | +| -sbom-cyclonedx-v1_6-verify | -DQT_SBOM_VERIFY_CYDX_V1_6=ON | Enables verification of generated CycloneDX v1.6 SBOMs | +| -sbom-cyclonedx-v1_6-verify-required | -DQT_SBOM_REQUIRE_VERIFY_CYDX_V1_6=ON | Fails the build if Cyclone DX Python verify deps are not found | +| -sbom-json | -DQT_SBOM_GENERATE_SPDX_V2_JSON=ON | Enables generation of SPDX v2.3 SBOM in JSON format | +| -sbom-json-required | -DQT_SBOM_REQUIRE_GENERATE_SPDX_V2_JSON=ON | Fails the build if SPDX Python deps are not found | +| -sbom-verify | -DQT_SBOM_VERIFY_SPDX_V2=ON | Enables verification of generated SPDX v2.3 SBOMs | +| -sbom-verify-required | -DQT_SBOM_REQUIRE_VERIFY_SPDX_V2=ON | Fails the build if SPDX Python verify deps are not found | | -sbomdir <dir> | -DINSTALL_SBOMDIR=<dir> | Installation location of SBOM files. | | -qtinlinenamespace | -DQT_INLINE_NAMESPACE=ON | Make the namespace specified by -qtnamespace an inline one. | | -qtnamespace <name> | -DQT_NAMESPACE=<name> | | diff --git a/coin/instructions/prepare_building_env.yaml b/coin/instructions/prepare_building_env.yaml index 2cc63b136e4..e1d30ce64b4 100644 --- a/coin/instructions/prepare_building_env.yaml +++ b/coin/instructions/prepare_building_env.yaml @@ -487,15 +487,16 @@ instructions: property: host.os contains_value: Windows - # Enable warnings are errors + # Enable warnings are errors, including qml linting warnings for examples that should not regress + # in qmllint warnings - type: Group instructions: - type: AppendToEnvironmentVariable variableName: COMMON_CMAKE_ARGS - variableValue: " -DWARNINGS_ARE_ERRORS=ON" + variableValue: " -DWARNINGS_ARE_ERRORS=ON -DQT_LINT_EXAMPLES=ON" - type: AppendToEnvironmentVariable variableName: COMMON_TARGET_CMAKE_ARGS - variableValue: " -DWARNINGS_ARE_ERRORS=ON" + variableValue: " -DWARNINGS_ARE_ERRORS=ON -DQT_LINT_EXAMPLES=ON" enable_if: condition: property property: features @@ -549,18 +550,25 @@ instructions: property: features contains_value: GenerateSBOM instructions: + - type: EnvironmentVariable + variableName: SBOM_COMMON_ARGS + variableValue: >- + -DQT_GENERATE_SBOM=ON + -DQT_GENERATE_SOURCE_SBOM=ON + -DQT_SBOM_REQUIRE_GENERATE_CYDX_V1_6=ON + -DQT_SBOM_VERIFY_CYDX_V1_6=ON - type: AppendToEnvironmentVariable variableName: COMMON_CMAKE_ARGS - variableValue: " -DQT_GENERATE_SBOM=ON -DQT_GENERATE_SOURCE_SBOM=ON" + variableValue: " {{.Env.SBOM_COMMON_ARGS}} " - type: AppendToEnvironmentVariable variableName: COMMON_NON_QTBASE_CMAKE_ARGS - variableValue: " -DQT_GENERATE_SBOM=ON -DQT_GENERATE_SOURCE_SBOM=ON" + variableValue: " {{.Env.SBOM_COMMON_ARGS}} " - type: AppendToEnvironmentVariable variableName: COMMON_TARGET_CMAKE_ARGS - variableValue: " -DQT_GENERATE_SBOM=ON -DQT_GENERATE_SOURCE_SBOM=ON" + variableValue: " {{.Env.SBOM_COMMON_ARGS}} " - type: AppendToEnvironmentVariable variableName: COMMON_NON_QTBASE_TARGET_CMAKE_ARGS - variableValue: " -DQT_GENERATE_SBOM=ON -DQT_GENERATE_SOURCE_SBOM=ON" + variableValue: " {{.Env.SBOM_COMMON_ARGS}} " # SBOM Python apps path. On Windows python-installed apps are # in the same directory where pip is, aka Scripts sub-directory. diff --git a/coin/instructions/vxworks_test_env_setup.yaml b/coin/instructions/vxworks_test_env_setup.yaml index 9ff710fbc7f..9e40b21acc4 100644 --- a/coin/instructions/vxworks_test_env_setup.yaml +++ b/coin/instructions/vxworks_test_env_setup.yaml @@ -67,6 +67,7 @@ instructions: env_vars["QTEST_FUNCTION_TIMEOUT"]="1000000" env_vars["QT_PLUGIN_PATH"]="/home/qt/work/install/target/plugins" env_vars["QT_QPA_PLATFORM_PLUGIN_PATH"]="/home/qt/work/install/target/plugins/platforms" + env_vars["TZ"]="UTC" for i in "${!hosts[@]}" do diff --git a/doc/global/config.qdocconf b/doc/global/config.qdocconf index 0adfe36f516..0baf9a15478 100644 --- a/doc/global/config.qdocconf +++ b/doc/global/config.qdocconf @@ -42,5 +42,8 @@ qhp = true # Disable writing host-specific paths into .index files locationinfo = false +# Automatically mark classes declared in private headers as \internal +internalfilepatterns = *_p.h + # Include the warninglimit used for documentation testing in CI include(warninglimit.qdocconf) diff --git a/examples/widgets/doc/images/treemodel-structure.svg b/examples/widgets/doc/images/treemodel-structure.svg index f8bc24803bf..8f86ff33862 100644 --- a/examples/widgets/doc/images/treemodel-structure.svg +++ b/examples/widgets/doc/images/treemodel-structure.svg @@ -1,93 +1,77 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg - preserveAspectRatio="none" - width="192" - height="350" - viewBox="-25 -30 153.6 280" - version="1.1" - xmlns="http://www.w3.org/2000/svg"> + viewBox="0 0 165 300" + width="165" + height="300" + version="1.1" + id="svg25" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + +<style> + svg .line-style { stroke: black; fill: none } + svg .text-style { font: 12px arial; fill: black } + svg .bold-style { font: 12px arial; fill: black; font-weight: bold } + svg .item-style { font: 16px arial; fill: black } + + [data-theme="dark"] svg .line-style { stroke: #f2f2f2; fill: none } + [data-theme="dark"] svg .text-style { font: 12px arial; fill: #f2f2f2 } + [data-theme="dark"] svg .bold-style { font: 12px arial; fill: #f2f2f2; font-weight: bold } + [data-theme="dark"] svg .item-style { font: 14px arial; fill: #f2f2f2 } + + [data-theme="light"] svg .line-style { stroke: black; fill: none } + [data-theme="light"] svg .text-style { font: 12px arial; fill: black } + [data-theme="light"] svg .bold-style { font: 12px arial; fill: black; font-weight: bold } + [data-theme="light"] svg .item-style { font: 14px arial; fill: black } +</style> <!-- Root node --> -<g transform="translate(0, 0)"> -<path fill="none" stroke="#333" stroke-dasharray="4" d="M -10 -10 l 20 0 l 0 20 l -20 0 z" /> -<text x="15" y="0" font-size="12" font-family="Arial" fill="#333" text-anchor="left" dominant-baseline="central" font-weight="bold"> - Root item (empty) -</text> -</g> +<text x="50" y="29" class="bold-style">Root item (empty)</text> +<path d="M 10,10 L 40,10 L 40,40 L 10,40 Z" class="line-style" + stroke-dasharray="5 5" /> <!-- Root spine vertical line solid part --> -<path fill="none" stroke="#333" d="M 0 10 l 0 190" /> - +<path d="M 25,40 L 25,265" class="line-style" /> <!-- Root spine vertical line dotted continuation --> -<path fill="none" stroke="#333" stroke-dasharray="4" d="M 0 200 l 0 30" /> +<path d="M 25,265 L 25,290" class="line-style" stroke-dasharray="3 3" /> -<!-- Horizontal lines from Root spine to children A, E, F --> -<path fill="none" stroke="#333" d="M 0 40 l 20 0" /> -<path fill="none" stroke="#333" d="M 0 160 l 20 0" /> -<path fill="none" stroke="#333" d="M 0 185 l 20 0" /> +<!-- Horizontal lines from Root spine to children A, C, ... --> +<path d="M 25,65 L 45,65" class="line-style" /> +<path d="M 25,225 L 45,225" class="line-style" /> +<path d="M 25,265 L 45,265" class="line-style" /> <!-- Node A with label [0] --> -<g transform="translate(30, 40)"> -<path fill="none" stroke="#333" d="M -10 -10 l 20 0 l 0 20 l -20 0 z"/> -<text x="0" y="0" font-size="12" font-family="Arial" fill="#333" text-anchor="middle" dominant-baseline="central"> - A -</text> -<text x="15" y="0" font-size="12" font-family="Arial" fill="#333" text-anchor="start" dominant-baseline="central"> - row = 0 -</text> -</g> +<path d="M 45,50 L 75,50 L 75,80 L 45,80 Z" class="line-style" /> +<text x="55" y="70" class="item-style">A</text> +<text x="82" y="69" class="text-style">row = 0</text> <!-- Vertical line from A to its children --> -<path fill="none" stroke="#333" d="M 30 50 l 0 80" /> +<path d="M 60,80 L 60,185" class="line-style" /> -<!-- Horizontal lines from A's spine to B, C, D --> -<path fill="none" stroke="#333" d="M 30 70 l 20 0" /> -<path fill="none" stroke="#333" d="M 30 100 l 20 0" /> -<path fill="none" stroke="#333" d="M 30 130 l 20 0" /> +<!-- Horizontal lines from A's spine to ..., B, ... --> +<path d="M 60,105 L 80,105" class="line-style" /> +<path d="M 60,145 L 80,145" class="line-style" /> +<path d="M 60,185 L 80,185" class="line-style" /> -<!-- Node B --> -<g transform="translate(60, 70)"> -<path fill="none" stroke="#333" d="M -10 -10 l 20 0 l 0 20 l -20 0 z"/> -<text x="15" y="0" font-size="12" font-family="Arial" fill="#333" text-anchor="start" dominant-baseline="central"> - row = 0 -</text> -</g> +<!-- First unnamed child node of A --> +<path d="M 80,90 L 110,90 L 110,120 L 80,120 Z" class="line-style" /> +<text x="117" y="109" class="text-style">row = 0</text> -<!-- Node C --> -<g transform="translate(60, 100)"> -<path fill="none" stroke="#333" d="M -10 -10 l 20 0 l 0 20 l -20 0 z"/> -<text x="0" y="0" font-size="12" font-family="Arial" fill="#333" text-anchor="middle" dominant-baseline="central"> - B -</text> -<text x="15" y="0" font-size="12" font-family="Arial" fill="#333" text-anchor="start" dominant-baseline="central"> - row = 1 -</text> -</g> - -<!-- Node D --> -<g transform="translate(60, 130)"> -<path fill="none" stroke="#333" d="M -10 -10 l 20 0 l 0 20 l -20 0 z"/> -<text x="15" y="0" font-size="12" font-family="Arial" fill="#333" text-anchor="start" dominant-baseline="central"> - row = 2 -</text> -</g> +<!-- Node B --> +<path d="M 80,130 L 110,130 L 110,160 L 80,160 Z" class="line-style" /> +<text x="90" y="150" class="item-style">B</text> +<text x="117" y="149" class="text-style">row = 1</text> -<!-- Node E --> -<g transform="translate(30, 160)"> -<path fill="none" stroke="#333" d="M -10 -10 l 20 0 l 0 20 l -20 0 z"/> -<text x="0" y="0" font-size="12" font-family="Arial" fill="#333" text-anchor="middle" dominant-baseline="central"> - C -</text> -<text x="15" y="0" font-size="12" font-family="Arial" fill="#333" text-anchor="start" dominant-baseline="central"> - row = 1 -</text> -</g> +<!-- Last unnamed child node of A --> +<path d="M 80,170 L 110,170 L 110,200 L 80,200 Z" class="line-style" /> +<text x="117" y="189" class="text-style">row = 2</text> -<!-- Node F --> -<g transform="translate(30, 185)"> -<path fill="none" stroke="#333" d="M -10 -10 l 20 0 l 0 20 l -20 0 z"/> -<text x="15" y="0" font-size="12" font-family="Arial" fill="#333" text-anchor="start" dominant-baseline="central"> - row = 2 -</text> -</g> +<!-- Node C --> +<path d="M 45,210 L 75,210 L 75,240 L 45,240 Z" class="line-style" /> +<text x="55" y="230" class="item-style">C</text> +<text x="82" y="229" class="text-style">row = 1</text> +<!-- Last visible child node of the root node --> +<path d="M 45,250 L 75,250 L 75,280 L 45,280 Z" class="line-style" /> +<text x="82" y="269" class="text-style">row = 2</text> </svg> diff --git a/licenseRule.json b/licenseRule.json index 3bc58bddca9..8c4029f91e1 100644 --- a/licenseRule.json +++ b/licenseRule.json @@ -377,6 +377,11 @@ "file type": "util", "spdx": ["LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0"] }, + "util/sbom/cyclonedx/pyproject.toml": { + "comment": "Default", + "file type": "util", + "spdx": ["LicenseRef-Qt-Commercial OR BSD-3-Clause"] + }, "util/unicode/data/.*\\.txt": { "comment": "Exception.", "file type": "3rd party", diff --git a/src/3rdparty/double-conversion/0001-fix-decimal_point-initialization-to-suppress-warning.patch b/src/3rdparty/double-conversion/0001-fix-decimal_point-initialization-to-suppress-warning.patch new file mode 100644 index 00000000000..2dd58d4b818 --- /dev/null +++ b/src/3rdparty/double-conversion/0001-fix-decimal_point-initialization-to-suppress-warning.patch @@ -0,0 +1,40 @@ +From c75f7f48c8a3d9c6aaaeb13a48fb3c051b46ccab Mon Sep 17 00:00:00 2001 +From: Ivan Solovev <[email protected]> +Date: Mon, 3 Nov 2025 12:33:26 +0100 +Subject: [PATCH] fix decimal_point initialization to suppress warnings in GCC + 14 + +This patch was submitted upstream as [0], but there was no new release +yet. + +[0]: https://github.com/google/double-conversion/commit/4aecc844c566d84a939fc35f4e62d58bd693f18d + +Change-Id: I97cc3103ff758f4c65b23d2f4c0fd82932e36df7 +--- + .../double-conversion/double-conversion/double-to-string.cc | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +diff --git a/src/3rdparty/double-conversion/double-conversion/double-to-string.cc b/src/3rdparty/double-conversion/double-conversion/double-to-string.cc +index 215eaa96d47..9ea3d18d5f7 100644 +--- a/src/3rdparty/double-conversion/double-conversion/double-to-string.cc ++++ b/src/3rdparty/double-conversion/double-conversion/double-to-string.cc +@@ -180,7 +180,7 @@ bool DoubleToStringConverter::ToShortestIeeeNumber( + return HandleSpecialValues(value, result_builder); + } + +- int decimal_point; ++ int decimal_point = 0; + bool sign; + const int kDecimalRepCapacity = kBase10MaximalLength + 1; + char decimal_rep[kDecimalRepCapacity]; +@@ -405,6 +405,7 @@ void DoubleToStringConverter::DoubleToAscii(double v, + if (mode == PRECISION && requested_digits == 0) { + vector[0] = '\0'; + *length = 0; ++ *point = 0; + return; + } + +-- +2.44.0 + diff --git a/src/3rdparty/double-conversion/REUSE.toml b/src/3rdparty/double-conversion/REUSE.toml index a7ebffce4f0..14e6e9b6706 100644 --- a/src/3rdparty/double-conversion/REUSE.toml +++ b/src/3rdparty/double-conversion/REUSE.toml @@ -1,7 +1,7 @@ version = 1 [[annotations]] -path = ["double-conversion/*"] +path = ["double-conversion/*", "0001-fix-decimal_point-initialization-to-suppress-warning.patch"] precedence = "closest" SPDX-FileCopyrightText = "Copyright 2006-2012, the V8 project authors" SPDX-License-Identifier = "BSD-3-Clause" diff --git a/src/3rdparty/double-conversion/double-conversion/double-to-string.cc b/src/3rdparty/double-conversion/double-conversion/double-to-string.cc index 215eaa96d47..9ea3d18d5f7 100644 --- a/src/3rdparty/double-conversion/double-conversion/double-to-string.cc +++ b/src/3rdparty/double-conversion/double-conversion/double-to-string.cc @@ -180,7 +180,7 @@ bool DoubleToStringConverter::ToShortestIeeeNumber( return HandleSpecialValues(value, result_builder); } - int decimal_point; + int decimal_point = 0; bool sign; const int kDecimalRepCapacity = kBase10MaximalLength + 1; char decimal_rep[kDecimalRepCapacity]; @@ -405,6 +405,7 @@ void DoubleToStringConverter::DoubleToAscii(double v, if (mode == PRECISION && requested_digits == 0) { vector[0] = '\0'; *length = 0; + *point = 0; return; } diff --git a/src/3rdparty/pcre2/qt_attribution.json b/src/3rdparty/pcre2/qt_attribution.json index ef29ad7d951..0468bca1b3e 100644 --- a/src/3rdparty/pcre2/qt_attribution.json +++ b/src/3rdparty/pcre2/qt_attribution.json @@ -30,7 +30,7 @@ "Homepage": "http://www.pcre.org/", "Version": "10.47", "DownloadLocation": "https://github.com/PCRE2Project/pcre2/releases/download/pcre2-10.47/pcre2-10.47.tar.bz2", - "PURL": "pkg:github/PCRE2Project/pcre2@$<VERSION>", + "PURL": "pkg:github/PCRE2Project/pcre2@pcre2-$<VERSION>", "CPE": "cpe:2.3:a:pcre:pcre2:$<VERSION>:*:*:*:*:*:*:*", "License": "BSD 2-clause \"Simplified\" License", "LicenseId": "BSD-2-Clause", diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index babf5bc31d2..2920c743243 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -71,7 +71,6 @@ add_subdirectory(tools) if(QT_FEATURE_gui) add_subdirectory(gui) - add_subdirectory(assets) if(QT_FEATURE_opengl) add_subdirectory(opengl) diff --git a/src/assets/CMakeLists.txt b/src/assets/CMakeLists.txt deleted file mode 100644 index ab07d27696e..00000000000 --- a/src/assets/CMakeLists.txt +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (C) 2024 The Qt Company Ltd. -# SPDX-License-Identifier: BSD-3-Clause - -if (NOT INTEGRITY AND TARGET Qt6::Network AND TARGET Qt6::Concurrent) - add_subdirectory(downloader) -endif() diff --git a/src/assets/downloader/CMakeLists.txt b/src/assets/downloader/CMakeLists.txt deleted file mode 100644 index 6b0564e72af..00000000000 --- a/src/assets/downloader/CMakeLists.txt +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright (C) 2024 The Qt Company Ltd. -# SPDX-License-Identifier: BSD-3-Clause - -qt_internal_add_module(ExamplesAssetDownloaderPrivate - CONFIG_MODULE_NAME examples_asset_downloader - STATIC - INTERNAL_MODULE - SOURCES - assetdownloader.cpp assetdownloader.h - tasking/barrier.cpp tasking/barrier.h - tasking/concurrentcall.h - tasking/conditional.cpp tasking/conditional.h - tasking/networkquery.cpp tasking/networkquery.h - tasking/qprocesstask.cpp tasking/qprocesstask.h - tasking/tasking_global.h - tasking/tasktree.cpp tasking/tasktree.h - tasking/tasktreerunner.cpp tasking/tasktreerunner.h - tasking/tcpsocket.cpp tasking/tcpsocket.h - DEFINES - QT_NO_CAST_FROM_ASCII - LIBRARIES - Qt6::CorePrivate - PUBLIC_LIBRARIES - Qt6::Concurrent - Qt6::Core - Qt6::Network - NO_GENERATE_CPP_EXPORTS -) - -if (NOT QT_FEATURE_process) - set_source_files_properties(tasking/qprocesstask.h PROPERTIES SKIP_AUTOMOC TRUE) -endif() diff --git a/src/assets/downloader/assetdownloader.cpp b/src/assets/downloader/assetdownloader.cpp deleted file mode 100644 index 7c2f525a66d..00000000000 --- a/src/assets/downloader/assetdownloader.cpp +++ /dev/null @@ -1,592 +0,0 @@ -// Copyright (C) 2024 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 "assetdownloader.h" - -#include "tasking/concurrentcall.h" -#include "tasking/networkquery.h" -#include "tasking/tasktreerunner.h" - -#include <QtCore/private/qzipreader_p.h> - -#include <QtCore/QDir> -#include <QtCore/QFile> -#include <QtCore/QJsonArray> -#include <QtCore/QJsonDocument> -#include <QtCore/QJsonObject> -#include <QtCore/QStandardPaths> -#include <QtCore/QTemporaryDir> -#include <QtCore/QTemporaryFile> - -using namespace Tasking; - -QT_BEGIN_NAMESPACE - -namespace Assets::Downloader { - -struct DownloadableAssets -{ - QUrl remoteUrl; - QList<QUrl> files; -}; - -class AssetDownloaderPrivate -{ -public: - AssetDownloaderPrivate(AssetDownloader *q) : m_q(q) {} - AssetDownloader *m_q = nullptr; - - std::unique_ptr<QNetworkAccessManager> m_manager; - std::unique_ptr<QTemporaryDir> m_temporaryDir; - TaskTreeRunner m_taskTreeRunner; - QString m_lastProgressText; - QDir m_localDownloadDir; - - QString m_jsonFileName; - QString m_zipFileName; - QDir m_preferredLocalDownloadDir = - QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation); - QUrl m_offlineAssetsFilePath; - QUrl m_downloadBase; - QStringList m_networkErrors; - QStringList m_sslErrors; - - void setLocalDownloadDir(const QDir &dir) - { - if (m_localDownloadDir != dir) { - m_localDownloadDir = dir; - emit m_q->localDownloadDirChanged(QUrl::fromLocalFile(m_localDownloadDir.absolutePath())); - } - } - void setProgress(int progressValue, int progressMaximum, const QString &progressText) - { - m_lastProgressText = progressText; - emit m_q->progressChanged(progressValue, progressMaximum, progressText); - } - void updateProgress(int progressValue, int progressMaximum) - { - setProgress(progressValue, progressMaximum, m_lastProgressText); - } - void clearProgress(const QString &progressText) - { - setProgress(0, 0, progressText); - } - - void setupDownload(NetworkQuery *query, const QString &progressText) - { - query->setNetworkAccessManager(m_manager.get()); - clearProgress(progressText); - QObject::connect(query, &NetworkQuery::started, query, [this, query] { - QNetworkReply *reply = query->reply(); - QObject::connect(reply, &QNetworkReply::downloadProgress, - query, [this](qint64 bytesReceived, qint64 totalBytes) { - updateProgress((totalBytes > 0) ? 100.0 * bytesReceived / totalBytes : 0, 100); - }); - QObject::connect(reply, &QNetworkReply::errorOccurred, query, [this, reply] { - m_networkErrors << reply->errorString(); - }); -#if QT_CONFIG(ssl) - QObject::connect(reply, &QNetworkReply::sslErrors, - query, [this](const QList<QSslError> &sslErrors) { - for (const QSslError &sslError : sslErrors) - m_sslErrors << sslError.errorString(); - }); -#endif - }); - } -}; - -static bool isWritableDir(const QDir &dir) -{ - if (dir.exists()) { - QTemporaryFile file(dir.filePath(QString::fromLatin1("tmp"))); - return file.open(); - } - return false; -} - -static bool sameFileContent(const QFileInfo &first, const QFileInfo &second) -{ - if (first.exists() ^ second.exists()) - return false; - - if (first.size() != second.size()) - return false; - - QFile firstFile(first.absoluteFilePath()); - QFile secondFile(second.absoluteFilePath()); - - if (firstFile.open(QFile::ReadOnly) && secondFile.open(QFile::ReadOnly)) { - char char1; - char char2; - int readBytes1 = 0; - int readBytes2 = 0; - while (!firstFile.atEnd()) { - readBytes1 = firstFile.read(&char1, 1); - readBytes2 = secondFile.read(&char2, 1); - if (readBytes1 != readBytes2 || readBytes1 != 1) - return false; - if (char1 != char2) - return false; - } - return true; - } - - return false; -} - -static bool createDirectory(const QDir &dir) -{ - if (dir.exists()) - return true; - - if (!createDirectory(dir.absoluteFilePath(QString::fromUtf8("..")))) - return false; - - return dir.mkpath(QString::fromUtf8(".")); -} - -static bool canBeALocalBaseDir(const QDir &dir) -{ - if (dir.exists()) - return !dir.isEmpty() || isWritableDir(dir); - return createDirectory(dir) && isWritableDir(dir); -} - -static QDir baseLocalDir(const QDir &preferredLocalDir) -{ - if (canBeALocalBaseDir(preferredLocalDir)) - return preferredLocalDir; - - qWarning().noquote() << "AssetDownloader: Cannot set \"" << preferredLocalDir - << "\" as a local download directory!"; - return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation); -} - -static QString pathFromUrl(const QUrl &url) -{ - if (url.isLocalFile()) - return url.toLocalFile(); - - if (url.scheme() == u"qrc") - return u":" + url.path(); - - return url.toString(); -} - -static QList<QUrl> filterDownloadableAssets(const QList<QUrl> &assetFiles, const QDir &expectedDir) -{ - QList<QUrl> downloadList; - std::copy_if(assetFiles.begin(), assetFiles.end(), std::back_inserter(downloadList), - [&](const QUrl &assetPath) { - return !QFileInfo::exists(expectedDir.absoluteFilePath(assetPath.toString())); - }); - return downloadList; -} - -static bool allAssetsPresent(const QList<QUrl> &assetFiles, const QDir &expectedDir) -{ - return std::all_of(assetFiles.begin(), assetFiles.end(), [&](const QUrl &assetPath) { - return QFileInfo::exists(expectedDir.absoluteFilePath(assetPath.toString())); - }); -} - -AssetDownloader::AssetDownloader(QObject *parent) - : QObject(parent) - , d(new AssetDownloaderPrivate(this)) -{} - -AssetDownloader::~AssetDownloader() = default; - -QUrl AssetDownloader::downloadBase() const -{ - return d->m_downloadBase; -} - -void AssetDownloader::setDownloadBase(const QUrl &downloadBase) -{ - if (d->m_downloadBase != downloadBase) { - d->m_downloadBase = downloadBase; - emit downloadBaseChanged(d->m_downloadBase); - } -} - -QUrl AssetDownloader::preferredLocalDownloadDir() const -{ - return QUrl::fromLocalFile(d->m_preferredLocalDownloadDir.absolutePath()); -} - -void AssetDownloader::setPreferredLocalDownloadDir(const QUrl &localDir) -{ - if (localDir.scheme() == u"qrc") { - qWarning() << "Cannot set a qrc as preferredLocalDownloadDir"; - return; - } - - const QString path = pathFromUrl(localDir); - if (d->m_preferredLocalDownloadDir != path) { - d->m_preferredLocalDownloadDir.setPath(path); - emit preferredLocalDownloadDirChanged(preferredLocalDownloadDir()); - } -} - -QUrl AssetDownloader::offlineAssetsFilePath() const -{ - return d->m_offlineAssetsFilePath; -} - -void AssetDownloader::setOfflineAssetsFilePath(const QUrl &offlineAssetsFilePath) -{ - if (d->m_offlineAssetsFilePath != offlineAssetsFilePath) { - d->m_offlineAssetsFilePath = offlineAssetsFilePath; - emit offlineAssetsFilePathChanged(d->m_offlineAssetsFilePath); - } -} - -QString AssetDownloader::jsonFileName() const -{ - return d->m_jsonFileName; -} - -void AssetDownloader::setJsonFileName(const QString &jsonFileName) -{ - if (d->m_jsonFileName != jsonFileName) { - d->m_jsonFileName = jsonFileName; - emit jsonFileNameChanged(d->m_jsonFileName); - } -} - -QString AssetDownloader::zipFileName() const -{ - return d->m_zipFileName; -} - -void AssetDownloader::setZipFileName(const QString &zipFileName) -{ - if (d->m_zipFileName != zipFileName) { - d->m_zipFileName = zipFileName; - emit zipFileNameChanged(d->m_zipFileName); - } -} - -QUrl AssetDownloader::localDownloadDir() const -{ - return QUrl::fromLocalFile(d->m_localDownloadDir.absolutePath()); -} - -QStringList AssetDownloader::networkErrors() const -{ - return d->m_networkErrors; -} - -QStringList AssetDownloader::sslErrors() const -{ - return d->m_sslErrors; -} - -static void precheckLocalFile(const QUrl &url) -{ - if (url.isEmpty()) - return; - QFile file(pathFromUrl(url)); - if (!file.open(QIODevice::ReadOnly)) - qWarning() << "Cannot open local file" << url; -} - -static void readAssetsFileContent(QPromise<DownloadableAssets> &promise, const QByteArray &content) -{ - const QJsonObject json = QJsonDocument::fromJson(content).object(); - const QJsonArray assetsArray = json[u"assets"].toArray(); - DownloadableAssets result; - result.remoteUrl = json[u"url"].toString(); - for (const QJsonValue &asset : assetsArray) { - if (promise.isCanceled()) - return; - result.files.append(asset.toString()); - } - - if (result.files.isEmpty() || result.remoteUrl.isEmpty()) - promise.future().cancel(); - else - promise.addResult(result); -} - -static void unzip(QPromise<void> &promise, const QByteArray &content, const QDir &directory, - const QString &fileName) -{ - const QString zipFilePath = directory.absoluteFilePath(fileName); - QFile zipFile(zipFilePath); - if (!zipFile.open(QIODevice::WriteOnly)) { - promise.future().cancel(); - return; - } - zipFile.write(content); - zipFile.close(); - - if (promise.isCanceled()) - return; - - QZipReader reader(zipFilePath); - const bool extracted = reader.extractAll(directory.absolutePath()); - reader.close(); - if (extracted) - QFile::remove(zipFilePath); - else - promise.future().cancel(); -} - -static void writeAsset(QPromise<void> &promise, const QByteArray &content, const QString &filePath) -{ - const QFileInfo fileInfo(filePath); - QFile file(fileInfo.absoluteFilePath()); - if (!createDirectory(fileInfo.dir()) || !file.open(QFile::WriteOnly)) { - promise.future().cancel(); - return; - } - - if (promise.isCanceled()) - return; - - file.write(content); - file.close(); -} - -static void copyAndCheck(QPromise<void> &promise, const QString &sourcePath, const QString &destPath) -{ - QFile sourceFile(sourcePath); - QFile destFile(destPath); - const QFileInfo sourceFileInfo(sourceFile.fileName()); - const QFileInfo destFileInfo(destFile.fileName()); - - if (destFile.exists() && !destFile.remove()) { - qWarning().noquote() << QString::fromLatin1("Unable to remove file \"%1\".") - .arg(QFileInfo(destFile.fileName()).absoluteFilePath()); - promise.future().cancel(); - return; - } - - if (!createDirectory(destFileInfo.absolutePath())) { - qWarning().noquote() << QString::fromLatin1("Cannot create directory \"%1\".") - .arg(destFileInfo.absolutePath()); - promise.future().cancel(); - return; - } - - if (promise.isCanceled()) - return; - - if (!sourceFile.copy(destFile.fileName()) && !sameFileContent(sourceFileInfo, destFileInfo)) - promise.future().cancel(); -} - -void AssetDownloader::start() -{ - if (d->m_taskTreeRunner.isRunning()) - return; - - struct StorageData - { - QDir tempDir; - QByteArray jsonContent; - DownloadableAssets assets; - QList<QUrl> assetsToDownload; - QByteArray zipContent; - int doneCount = 0; - }; - - const Storage<StorageData> storage; - - const auto onSetup = [this, storage] { - if (!d->m_manager) - d->m_manager = std::make_unique<QNetworkAccessManager>(); - if (!d->m_temporaryDir) - d->m_temporaryDir = std::make_unique<QTemporaryDir>(); - if (!d->m_temporaryDir->isValid()) { - qWarning() << "Cannot create a temporary directory."; - return SetupResult::StopWithError; - } - storage->tempDir = d->m_temporaryDir->path(); - d->setLocalDownloadDir(baseLocalDir(d->m_preferredLocalDownloadDir)); - d->m_networkErrors.clear(); - d->m_sslErrors.clear(); - precheckLocalFile(resolvedUrl(d->m_offlineAssetsFilePath)); - return SetupResult::Continue; - }; - - const auto onJsonDownloadSetup = [this](NetworkQuery &query) { - query.setRequest(QNetworkRequest(d->m_downloadBase.resolved(d->m_jsonFileName))); - d->setupDownload(&query, tr("Downloading JSON file...")); - }; - const auto onJsonDownloadDone = [this, storage](const NetworkQuery &query, DoneWith result) { - if (result == DoneWith::Success) { - storage->jsonContent = query.reply()->readAll(); - return DoneResult::Success; - } - qWarning() << "Cannot download" << d->m_downloadBase.resolved(d->m_jsonFileName) - << query.reply()->errorString(); - if (d->m_offlineAssetsFilePath.isEmpty()) { - qWarning() << "Also there is no local file as a replacement"; - return DoneResult::Error; - } - - QFile file(pathFromUrl(resolvedUrl(d->m_offlineAssetsFilePath))); - if (!file.open(QIODevice::ReadOnly)) { - qWarning() << "Also failed to open" << d->m_offlineAssetsFilePath; - return DoneResult::Error; - } - - storage->jsonContent = file.readAll(); - return DoneResult::Success; - }; - - const auto onReadAssetsFileSetup = [storage](ConcurrentCall<DownloadableAssets> &async) { - async.setConcurrentCallData(readAssetsFileContent, storage->jsonContent); - }; - const auto onReadAssetsFileDone = [storage](const ConcurrentCall<DownloadableAssets> &async) { - storage->assets = async.result(); - storage->assetsToDownload = storage->assets.files; - }; - - const auto onSkipIfAllAssetsPresent = [this, storage] { - return allAssetsPresent(storage->assets.files, d->m_localDownloadDir) - ? SetupResult::StopWithSuccess : SetupResult::Continue; - }; - - const auto onZipDownloadSetup = [this, storage](NetworkQuery &query) { - if (d->m_zipFileName.isEmpty()) - return SetupResult::StopWithSuccess; - - query.setRequest(QNetworkRequest(d->m_downloadBase.resolved(d->m_zipFileName))); - d->setupDownload(&query, tr("Downloading zip file...")); - return SetupResult::Continue; - }; - const auto onZipDownloadDone = [storage](const NetworkQuery &query, DoneWith result) { - if (result == DoneWith::Success) - storage->zipContent = query.reply()->readAll(); - return DoneResult::Success; // Ignore zip download failure - }; - - const auto onUnzipSetup = [this, storage](ConcurrentCall<void> &async) { - if (storage->zipContent.isEmpty()) - return SetupResult::StopWithSuccess; - - async.setConcurrentCallData(unzip, storage->zipContent, storage->tempDir, d->m_zipFileName); - d->clearProgress(tr("Unzipping...")); - return SetupResult::Continue; - }; - const auto onUnzipDone = [storage](DoneWith result) { - if (result == DoneWith::Success) { - // Avoid downloading assets that are present in unzipped tree - StorageData &storageData = *storage; - storageData.assetsToDownload = - filterDownloadableAssets(storageData.assets.files, storageData.tempDir); - } else { - qWarning() << "ZipFile failed"; - } - return DoneResult::Success; // Ignore unzip failure - }; - - const LoopUntil downloadIterator([storage](int iteration) { - return iteration < storage->assetsToDownload.count(); - }); - - const Storage<QByteArray> assetStorage; - - const auto onAssetsDownloadGroupSetup = [this, storage] { - d->setProgress(0, storage->assetsToDownload.size(), tr("Downloading assets...")); - }; - - const auto onAssetDownloadSetup = [this, storage, downloadIterator](NetworkQuery &query) { - query.setNetworkAccessManager(d->m_manager.get()); - query.setRequest(QNetworkRequest(storage->assets.remoteUrl.resolved( - storage->assetsToDownload.at(downloadIterator.iteration())))); - }; - const auto onAssetDownloadDone = [assetStorage](const NetworkQuery &query, DoneWith result) { - if (result == DoneWith::Success) - *assetStorage = query.reply()->readAll(); - }; - - const auto onAssetWriteSetup = [storage, downloadIterator, assetStorage]( - ConcurrentCall<void> &async) { - const QString filePath = storage->tempDir.absoluteFilePath( - storage->assetsToDownload.at(downloadIterator.iteration()).toString()); - async.setConcurrentCallData(writeAsset, *assetStorage, filePath); - }; - const auto onAssetWriteDone = [this, storage](DoneWith result) { - if (result != DoneWith::Success) { - qWarning() << "Asset write failed"; - return; - } - StorageData &storageData = *storage; - ++storageData.doneCount; - d->updateProgress(storageData.doneCount, storageData.assetsToDownload.size()); - }; - - const LoopUntil copyIterator([storage](int iteration) { - return iteration < storage->assets.files.count(); - }); - - const auto onAssetsCopyGroupSetup = [this, storage] { - storage->doneCount = 0; - d->setProgress(0, storage->assets.files.size(), tr("Copying assets...")); - }; - - const auto onAssetCopySetup = [this, storage, copyIterator](ConcurrentCall<void> &async) { - const QString fileName = storage->assets.files.at(copyIterator.iteration()).toString(); - const QString sourcePath = storage->tempDir.absoluteFilePath(fileName); - const QString destPath = d->m_localDownloadDir.absoluteFilePath(fileName); - async.setConcurrentCallData(copyAndCheck, sourcePath, destPath); - }; - const auto onAssetCopyDone = [this, storage] { - StorageData &storageData = *storage; - ++storageData.doneCount; - d->updateProgress(storageData.doneCount, storageData.assets.files.size()); - }; - - const auto onAssetsCopyGroupDone = [this, storage](DoneWith result) { - if (result != DoneWith::Success) { - d->setLocalDownloadDir(storage->tempDir); - qWarning() << "Asset copy failed"; - return; - } - d->m_temporaryDir.reset(); - }; - - const Group recipe { - storage, - onGroupSetup(onSetup), - NetworkQueryTask(onJsonDownloadSetup, onJsonDownloadDone), - ConcurrentCallTask<DownloadableAssets>(onReadAssetsFileSetup, onReadAssetsFileDone, CallDoneIf::Success), - Group { - onGroupSetup(onSkipIfAllAssetsPresent), - NetworkQueryTask(onZipDownloadSetup, onZipDownloadDone), - ConcurrentCallTask<void>(onUnzipSetup, onUnzipDone), - For (downloadIterator) >> Do { - parallelIdealThreadCountLimit, - onGroupSetup(onAssetsDownloadGroupSetup), - Group { - assetStorage, - NetworkQueryTask(onAssetDownloadSetup, onAssetDownloadDone), - ConcurrentCallTask<void>(onAssetWriteSetup, onAssetWriteDone) - } - }, - For (copyIterator) >> Do { - parallelIdealThreadCountLimit, - onGroupSetup(onAssetsCopyGroupSetup), - ConcurrentCallTask<void>(onAssetCopySetup, onAssetCopyDone, CallDoneIf::Success), - onGroupDone(onAssetsCopyGroupDone) - } - } - }; - d->m_taskTreeRunner.start(recipe, [this](TaskTree *) { emit started(); }, - [this](DoneWith result) { emit finished(result == DoneWith::Success); }); -} - -QUrl AssetDownloader::resolvedUrl(const QUrl &url) const -{ - return url; -} - -} // namespace Assets::Downloader - -QT_END_NAMESPACE diff --git a/src/assets/downloader/assetdownloader.h b/src/assets/downloader/assetdownloader.h deleted file mode 100644 index 3c9351ceafe..00000000000 --- a/src/assets/downloader/assetdownloader.h +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright (C) 2024 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 ASSETDOWNLOADER_H -#define ASSETDOWNLOADER_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/QObject> -#include <QtCore/QUrl> - -#include <memory> - -QT_BEGIN_NAMESPACE - -namespace Assets::Downloader { - -class AssetDownloaderPrivate; - -class AssetDownloader : public QObject -{ - Q_OBJECT - - Q_PROPERTY( - QUrl downloadBase - READ downloadBase - WRITE setDownloadBase - NOTIFY downloadBaseChanged) - - Q_PROPERTY( - QUrl preferredLocalDownloadDir - READ preferredLocalDownloadDir - WRITE setPreferredLocalDownloadDir - NOTIFY preferredLocalDownloadDirChanged) - - Q_PROPERTY( - QUrl offlineAssetsFilePath - READ offlineAssetsFilePath - WRITE setOfflineAssetsFilePath - NOTIFY offlineAssetsFilePathChanged) - - Q_PROPERTY( - QString jsonFileName - READ jsonFileName - WRITE setJsonFileName - NOTIFY jsonFileNameChanged) - - Q_PROPERTY( - QString zipFileName - READ zipFileName - WRITE setZipFileName - NOTIFY zipFileNameChanged) - - Q_PROPERTY( - QUrl localDownloadDir - READ localDownloadDir - NOTIFY localDownloadDirChanged) - -public: - AssetDownloader(QObject *parent = nullptr); - ~AssetDownloader(); - - QUrl downloadBase() const; - void setDownloadBase(const QUrl &downloadBase); - - QUrl preferredLocalDownloadDir() const; - void setPreferredLocalDownloadDir(const QUrl &localDir); - - QUrl offlineAssetsFilePath() const; - void setOfflineAssetsFilePath(const QUrl &offlineAssetsFilePath); - - QString jsonFileName() const; - void setJsonFileName(const QString &jsonFileName); - - QString zipFileName() const; - void setZipFileName(const QString &zipFileName); - - QUrl localDownloadDir() const; - - Q_INVOKABLE QStringList networkErrors() const; - Q_INVOKABLE QStringList sslErrors() const; - -public Q_SLOTS: - void start(); - -protected: - virtual QUrl resolvedUrl(const QUrl &url) const; - -Q_SIGNALS: - void started(); - void finished(bool success); - void progressChanged(int progressValue, int progressMaximum, const QString &progressText); - void localDownloadDirChanged(const QUrl &url); - - void downloadBaseChanged(const QUrl &); - void preferredLocalDownloadDirChanged(const QUrl &url); - void offlineAssetsFilePathChanged(const QUrl &); - void jsonFileNameChanged(const QString &); - void zipFileNameChanged(const QString &); - -private: - std::unique_ptr<AssetDownloaderPrivate> d; -}; - -} // namespace Assets::Downloader - -QT_END_NAMESPACE - -#endif // ASSETDOWNLOADER_H diff --git a/src/assets/downloader/tasking/barrier.cpp b/src/assets/downloader/tasking/barrier.cpp deleted file mode 100644 index c9e5992bc78..00000000000 --- a/src/assets/downloader/tasking/barrier.cpp +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (C) 2024 Jarek Kobus -// Copyright (C) 2024 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 "barrier.h" - -QT_BEGIN_NAMESPACE - -namespace Tasking { - -// That's cut down qtcassert.{c,h} to avoid the dependency. -#define QT_STRING(cond) qDebug("SOFT ASSERT: \"%s\" in %s: %s", cond, __FILE__, QT_STRINGIFY(__LINE__)) -#define QT_ASSERT(cond, action) if (Q_LIKELY(cond)) {} else { QT_STRING(#cond); action; } do {} while (0) - -void Barrier::setLimit(int value) -{ - QT_ASSERT(!isRunning(), return); - QT_ASSERT(value > 0, return); - - m_limit = value; -} - -void Barrier::start() -{ - QT_ASSERT(!isRunning(), return); - m_current = 0; - m_result.reset(); -} - -void Barrier::advance() -{ - // Calling advance on finished is OK - QT_ASSERT(isRunning() || m_result, return); - if (!isRunning()) // no-op - return; - ++m_current; - if (m_current == m_limit) - stopWithResult(DoneResult::Success); -} - -void Barrier::stopWithResult(DoneResult result) -{ - // Calling stopWithResult on finished is OK when the same success is passed - QT_ASSERT(isRunning() || (m_result && *m_result == result), return); - if (!isRunning()) // no-op - return; - m_current = -1; - m_result = result; - emit done(result); -} - -} // namespace Tasking - -QT_END_NAMESPACE diff --git a/src/assets/downloader/tasking/barrier.h b/src/assets/downloader/tasking/barrier.h deleted file mode 100644 index d489d2722a4..00000000000 --- a/src/assets/downloader/tasking/barrier.h +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright (C) 2024 Jarek Kobus -// Copyright (C) 2024 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 TASKING_BARRIER_H -#define TASKING_BARRIER_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 "tasking_global.h" - -#include "tasktree.h" - -QT_BEGIN_NAMESPACE - -namespace Tasking { - -class TASKING_EXPORT Barrier final : public QObject -{ - Q_OBJECT - -public: - void setLimit(int value); - int limit() const { return m_limit; } - - void start(); - void advance(); // If limit reached, stops with true - void stopWithResult(DoneResult result); // Ignores limit - - bool isRunning() const { return m_current >= 0; } - int current() const { return m_current; } - std::optional<DoneResult> result() const { return m_result; } - -Q_SIGNALS: - void done(DoneResult success); - -private: - std::optional<DoneResult> m_result = {}; - int m_limit = 1; - int m_current = -1; -}; - -using BarrierTask = SimpleCustomTask<Barrier>; - -template <int Limit = 1> -class SharedBarrier -{ -public: - static_assert(Limit > 0, "SharedBarrier's limit should be 1 or more."); - SharedBarrier() : m_barrier(new Barrier) { - m_barrier->setLimit(Limit); - m_barrier->start(); - } - Barrier *barrier() const { return m_barrier.get(); } - -private: - std::shared_ptr<Barrier> m_barrier; -}; - -template <int Limit = 1> -using MultiBarrier = Storage<SharedBarrier<Limit>>; - -// Can't write: "MultiBarrier barrier;". Only "MultiBarrier<> barrier;" would work. -// Can't have one alias with default type in C++17, getting the following error: -// alias template deduction only available with C++20. -using SingleBarrier = MultiBarrier<1>; - -template <int Limit> -ExecutableItem waitForBarrierTask(const MultiBarrier<Limit> &sharedBarrier) -{ - return BarrierTask([sharedBarrier](Barrier &barrier) { - SharedBarrier<Limit> *activeBarrier = sharedBarrier.activeStorage(); - if (!activeBarrier) { - qWarning("The barrier referenced from WaitForBarrier element " - "is not reachable in the running tree. " - "It is possible that no barrier was added to the tree, " - "or the barrier is not reachable from where it is referenced. " - "The WaitForBarrier task finishes with an error. "); - return SetupResult::StopWithError; - } - Barrier *activeSharedBarrier = activeBarrier->barrier(); - const std::optional<DoneResult> result = activeSharedBarrier->result(); - if (result.has_value()) { - return *result == DoneResult::Success ? SetupResult::StopWithSuccess - : SetupResult::StopWithError; - } - QObject::connect(activeSharedBarrier, &Barrier::done, &barrier, &Barrier::stopWithResult); - return SetupResult::Continue; - }); -} - -template <typename Signal> -ExecutableItem signalAwaiter(const typename QtPrivate::FunctionPointer<Signal>::Object *sender, Signal signal) -{ - return BarrierTask([sender, signal](Barrier &barrier) { - QObject::connect(sender, signal, &barrier, &Barrier::advance, Qt::SingleShotConnection); - }); -} - -using BarrierKickerGetter = std::function<ExecutableItem(const SingleBarrier &)>; - -class TASKING_EXPORT When final -{ -public: - explicit When(const BarrierKickerGetter &kicker) : m_barrierKicker(kicker) {} - -private: - TASKING_EXPORT friend Group operator>>(const When &whenItem, const Do &doItem); - - BarrierKickerGetter m_barrierKicker; -}; - -} // namespace Tasking - -QT_END_NAMESPACE - -#endif // TASKING_BARRIER_H diff --git a/src/assets/downloader/tasking/concurrentcall.h b/src/assets/downloader/tasking/concurrentcall.h deleted file mode 100644 index cc701297d13..00000000000 --- a/src/assets/downloader/tasking/concurrentcall.h +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright (C) 2024 Jarek Kobus -// Copyright (C) 2024 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 TASKING_CONCURRENTCALL_H -#define TASKING_CONCURRENTCALL_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 "tasktree.h" - -#include <QtConcurrent/QtConcurrent> - -QT_BEGIN_NAMESPACE - -namespace Tasking { - -// This class introduces the dependency to Qt::Concurrent, otherwise Tasking namespace -// is independent on Qt::Concurrent. -// Possibly, it could be placed inside Qt::Concurrent library, as a wrapper around -// QtConcurrent::run() call. - -template <typename ResultType> -class ConcurrentCall -{ - Q_DISABLE_COPY_MOVE(ConcurrentCall) - -public: - ConcurrentCall() = default; - template <typename Function, typename ...Args> - void setConcurrentCallData(Function &&function, Args &&...args) - { - wrapConcurrent(std::forward<Function>(function), std::forward<Args>(args)...); - } - void setThreadPool(QThreadPool *pool) { m_threadPool = pool; } - ResultType result() const - { - return m_future.resultCount() ? m_future.result() : ResultType(); - } - QList<ResultType> results() const - { - return m_future.results(); - } - QFuture<ResultType> future() const { return m_future; } - -private: - template <typename Function, typename ...Args> - void wrapConcurrent(Function &&function, Args &&...args) - { - m_startHandler = [this, function = std::forward<Function>(function), args...] { - QThreadPool *threadPool = m_threadPool ? m_threadPool : QThreadPool::globalInstance(); - return QtConcurrent::run(threadPool, function, args...); - }; - } - - template <typename Function, typename ...Args> - void wrapConcurrent(std::reference_wrapper<const Function> &&wrapper, Args &&...args) - { - m_startHandler = [this, wrapper = std::forward<std::reference_wrapper<const Function>>(wrapper), args...] { - QThreadPool *threadPool = m_threadPool ? m_threadPool : QThreadPool::globalInstance(); - return QtConcurrent::run(threadPool, std::forward<const Function>(wrapper.get()), - args...); - }; - } - - template <typename T> - friend class ConcurrentCallTaskAdapter; - - std::function<QFuture<ResultType>()> m_startHandler; - QThreadPool *m_threadPool = nullptr; - QFuture<ResultType> m_future; -}; - -template <typename ResultType> -class ConcurrentCallTaskAdapter : public TaskAdapter<ConcurrentCall<ResultType>> -{ -public: - ~ConcurrentCallTaskAdapter() { - if (m_watcher) { - m_watcher->cancel(); - m_watcher->waitForFinished(); - } - } - - void start() final { - if (!this->task()->m_startHandler) { - emit this->done(DoneResult::Error); // TODO: Add runtime assert - return; - } - m_watcher.reset(new QFutureWatcher<ResultType>); - this->connect(m_watcher.get(), &QFutureWatcherBase::finished, this, [this] { - emit this->done(toDoneResult(!m_watcher->isCanceled())); - m_watcher.release()->deleteLater(); - }); - this->task()->m_future = this->task()->m_startHandler(); - m_watcher->setFuture(this->task()->m_future); - } - -private: - std::unique_ptr<QFutureWatcher<ResultType>> m_watcher; -}; - -template <typename T> -using ConcurrentCallTask = CustomTask<ConcurrentCallTaskAdapter<T>>; - -} // namespace Tasking - -QT_END_NAMESPACE - -#endif // TASKING_CONCURRENTCALL_H diff --git a/src/assets/downloader/tasking/conditional.cpp b/src/assets/downloader/tasking/conditional.cpp deleted file mode 100644 index 24a03fb703e..00000000000 --- a/src/assets/downloader/tasking/conditional.cpp +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (C) 2024 Jarek Kobus -// Copyright (C) 2024 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 "conditional.h" - -QT_BEGIN_NAMESPACE - -namespace Tasking { - -static Group conditionRecipe(const Storage<bool> &bodyExecutedStorage, const ConditionData &condition) -{ - const auto onSetup = [bodyExecutedStorage] { - return *bodyExecutedStorage ? SetupResult::StopWithSuccess : SetupResult::Continue; - }; - - const auto onBodyDone = [bodyExecutedStorage] { *bodyExecutedStorage = true; }; - - const Group bodyTask { condition.m_body, onGroupDone(onBodyDone) }; - - return { - onGroupSetup(onSetup), - condition.m_condition ? Group{ !*condition.m_condition || bodyTask } : bodyTask - }; -} - -static ExecutableItem conditionsRecipe(const QList<ConditionData> &conditions) -{ - Storage<bool> bodyExecutedStorage; - - GroupItems recipes; - for (const ConditionData &condition : conditions) - recipes << conditionRecipe(bodyExecutedStorage, condition); - - return Group { bodyExecutedStorage, recipes }; -} - -ThenItem::operator ExecutableItem() const -{ - return conditionsRecipe(m_conditions); -} - -ThenItem::ThenItem(const If &ifItem, const Then &thenItem) - : m_conditions{{ifItem.m_condition, thenItem.m_body}} -{} - -ThenItem::ThenItem(const ElseIfItem &elseIfItem, const Then &thenItem) - : m_conditions(elseIfItem.m_conditions) -{ - m_conditions.append({elseIfItem.m_nextCondition, thenItem.m_body}); -} - -ElseItem::operator ExecutableItem() const -{ - return conditionsRecipe(m_conditions); -} - -ElseItem::ElseItem(const ThenItem &thenItem, const Else &elseItem) - : m_conditions(thenItem.m_conditions) -{ - m_conditions.append({{}, elseItem.m_body}); -} - -ElseIfItem::ElseIfItem(const ThenItem &thenItem, const ElseIf &elseIfItem) - : m_conditions(thenItem.m_conditions) - , m_nextCondition(elseIfItem.m_condition) -{} - -ThenItem operator>>(const If &ifItem, const Then &thenItem) -{ - return {ifItem, thenItem}; -} - -ThenItem operator>>(const ElseIfItem &elseIfItem, const Then &thenItem) -{ - return {elseIfItem, thenItem}; -} - -ElseIfItem operator>>(const ThenItem &thenItem, const ElseIf &elseIfItem) -{ - return {thenItem, elseIfItem}; -} - -ElseItem operator>>(const ThenItem &thenItem, const Else &elseItem) -{ - return {thenItem, elseItem}; -} - -} // namespace Tasking - -QT_END_NAMESPACE diff --git a/src/assets/downloader/tasking/conditional.h b/src/assets/downloader/tasking/conditional.h deleted file mode 100644 index 52fdfd64cb7..00000000000 --- a/src/assets/downloader/tasking/conditional.h +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright (C) 2024 Jarek Kobus -// Copyright (C) 2024 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 TASKING_CONDITIONAL_H -#define TASKING_CONDITIONAL_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 "tasking_global.h" - -#include "tasktree.h" - -QT_BEGIN_NAMESPACE - -namespace Tasking { - -class Then; -class ThenItem; -class ElseItem; -class ElseIfItem; - -class TASKING_EXPORT If -{ -public: - explicit If(const ExecutableItem &condition) : m_condition(condition) {} - - template <typename Handler, - std::enable_if_t<!std::is_base_of_v<ExecutableItem, std::decay_t<Handler>>, bool> = true> - explicit If(Handler &&handler) : m_condition(Sync(std::forward<Handler>(handler))) {} - -private: - TASKING_EXPORT friend ThenItem operator>>(const If &ifItem, const Then &thenItem); - - friend class ThenItem; - ExecutableItem m_condition; -}; - -class TASKING_EXPORT ElseIf -{ -public: - explicit ElseIf(const ExecutableItem &condition) : m_condition(condition) {} - - template <typename Handler, - std::enable_if_t<!std::is_base_of_v<ExecutableItem, std::decay_t<Handler>>, bool> = true> - explicit ElseIf(Handler &&handler) : m_condition(Sync(std::forward<Handler>(handler))) {} - -private: - friend class ElseIfItem; - ExecutableItem m_condition; -}; - -class TASKING_EXPORT Else -{ -public: - explicit Else(const GroupItems &children) : m_body({children}) {} - explicit Else(std::initializer_list<GroupItem> children) : m_body({children}) {} - -private: - friend class ElseItem; - Group m_body; -}; - -class TASKING_EXPORT Then -{ -public: - explicit Then(const GroupItems &children) : m_body({children}) {} - explicit Then(std::initializer_list<GroupItem> children) : m_body({children}) {} - -private: - friend class ThenItem; - Group m_body; -}; - -class ConditionData -{ -public: - std::optional<ExecutableItem> m_condition; - Group m_body; -}; - -class ElseIfItem; - -class TASKING_EXPORT ThenItem -{ -public: - operator ExecutableItem() const; - -private: - ThenItem(const If &ifItem, const Then &thenItem); - ThenItem(const ElseIfItem &elseIfItem, const Then &thenItem); - - TASKING_EXPORT friend ElseItem operator>>(const ThenItem &thenItem, const Else &elseItem); - TASKING_EXPORT friend ElseIfItem operator>>(const ThenItem &thenItem, const ElseIf &elseIfItem); - TASKING_EXPORT friend ThenItem operator>>(const If &ifItem, const Then &thenItem); - TASKING_EXPORT friend ThenItem operator>>(const ElseIfItem &elseIfItem, const Then &thenItem); - - friend class ElseItem; - friend class ElseIfItem; - QList<ConditionData> m_conditions; -}; - -class TASKING_EXPORT ElseItem -{ -public: - operator ExecutableItem() const; - -private: - ElseItem(const ThenItem &thenItem, const Else &elseItem); - - TASKING_EXPORT friend ElseItem operator>>(const ThenItem &thenItem, const Else &elseItem); - - QList<ConditionData> m_conditions; -}; - -class TASKING_EXPORT ElseIfItem -{ -private: - ElseIfItem(const ThenItem &thenItem, const ElseIf &elseIfItem); - - TASKING_EXPORT friend ThenItem operator>>(const ElseIfItem &elseIfItem, const Then &thenItem); - TASKING_EXPORT friend ElseIfItem operator>>(const ThenItem &thenItem, const ElseIf &elseIfItem); - - friend class ThenItem; - QList<ConditionData> m_conditions; - ExecutableItem m_nextCondition; -}; - -} // namespace Tasking - -QT_END_NAMESPACE - -#endif // TASKING_CONDITIONAL_H diff --git a/src/assets/downloader/tasking/networkquery.cpp b/src/assets/downloader/tasking/networkquery.cpp deleted file mode 100644 index 3f15fed90e0..00000000000 --- a/src/assets/downloader/tasking/networkquery.cpp +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (C) 2024 Jarek Kobus -// Copyright (C) 2024 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 "networkquery.h" - -#include <QtNetwork/QNetworkAccessManager> - -QT_BEGIN_NAMESPACE - -namespace Tasking { - -void NetworkQuery::start() -{ - if (m_reply) { - qWarning("The NetworkQuery is already running. Ignoring the call to start()."); - return; - } - if (!m_manager) { - qWarning("Can't start the NetworkQuery without the QNetworkAccessManager. " - "Stopping with an error."); - emit done(DoneResult::Error); - return; - } - switch (m_operation) { - case NetworkOperation::Get: - m_reply.reset(m_manager->get(m_request)); - break; - case NetworkOperation::Put: - m_reply.reset(m_manager->put(m_request, m_writeData)); - break; - case NetworkOperation::Post: - m_reply.reset(m_manager->post(m_request, m_writeData)); - break; - case NetworkOperation::Delete: - m_reply.reset(m_manager->deleteResource(m_request)); - break; - } - - connect(m_reply.get(), &QNetworkReply::downloadProgress, this, &NetworkQuery::downloadProgress); -#if QT_CONFIG(ssl) - connect(m_reply.get(), &QNetworkReply::sslErrors, this, &NetworkQuery::sslErrors); -#endif - connect(m_reply.get(), &QNetworkReply::finished, this, [this] { - disconnect(m_reply.get(), &QNetworkReply::finished, this, nullptr); - emit done(toDoneResult(m_reply->error() == QNetworkReply::NoError)); - m_reply.release()->deleteLater(); - }); - if (m_reply->isRunning()) - emit started(); -} - -NetworkQuery::~NetworkQuery() -{ - if (m_reply) { - disconnect(m_reply.get(), nullptr, this, nullptr); - m_reply->abort(); - } -} - -} // namespace Tasking - -QT_END_NAMESPACE diff --git a/src/assets/downloader/tasking/networkquery.h b/src/assets/downloader/tasking/networkquery.h deleted file mode 100644 index 2733fef8352..00000000000 --- a/src/assets/downloader/tasking/networkquery.h +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (C) 2024 Jarek Kobus -// Copyright (C) 2024 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 TASKING_NETWORKQUERY_H -#define TASKING_NETWORKQUERY_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 "tasking_global.h" - -#include "tasktree.h" - -#include <QtNetwork/QNetworkReply> -#include <QtNetwork/QNetworkRequest> - -#include <memory> - -QT_BEGIN_NAMESPACE -class QNetworkAccessManager; - -namespace Tasking { - -// This class introduces the dependency to Qt::Network, otherwise Tasking namespace -// is independent on Qt::Network. -// Possibly, it could be placed inside Qt::Network library, as a wrapper around QNetworkReply. - -enum class NetworkOperation { Get, Put, Post, Delete }; - -class TASKING_EXPORT NetworkQuery final : public QObject -{ - Q_OBJECT - -public: - ~NetworkQuery(); - void setRequest(const QNetworkRequest &request) { m_request = request; } - void setOperation(NetworkOperation operation) { m_operation = operation; } - void setWriteData(const QByteArray &data) { m_writeData = data; } - void setNetworkAccessManager(QNetworkAccessManager *manager) { m_manager = manager; } - QNetworkReply *reply() const { return m_reply.get(); } - void start(); - -Q_SIGNALS: - void started(); - void downloadProgress(qint64 bytesReceived, qint64 bytesTotal); -#if QT_CONFIG(ssl) - void sslErrors(const QList<QSslError> &errors); -#endif - void done(DoneResult result); - -private: - QNetworkRequest m_request; - NetworkOperation m_operation = NetworkOperation::Get; - QByteArray m_writeData; // Used by Put and Post - QNetworkAccessManager *m_manager = nullptr; - std::unique_ptr<QNetworkReply> m_reply; -}; - -using NetworkQueryTask = SimpleCustomTask<NetworkQuery>; - -} // namespace Tasking - -QT_END_NAMESPACE - -#endif // TASKING_NETWORKQUERY_H diff --git a/src/assets/downloader/tasking/qprocesstask.cpp b/src/assets/downloader/tasking/qprocesstask.cpp deleted file mode 100644 index a4696ebbbfb..00000000000 --- a/src/assets/downloader/tasking/qprocesstask.cpp +++ /dev/null @@ -1,280 +0,0 @@ -// Copyright (C) 2024 Jarek Kobus -// Copyright (C) 2024 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 "qprocesstask.h" - -#include <QtCore/QCoreApplication> -#include <QtCore/QDebug> -#include <QtCore/QElapsedTimer> -#include <QtCore/QMutex> -#include <QtCore/QThread> -#include <QtCore/QTimer> -#include <QtCore/QWaitCondition> - -QT_BEGIN_NAMESPACE - -#if QT_CONFIG(process) - -namespace Tasking { - -class ProcessReaperPrivate; - -class ProcessReaper final -{ -public: - static void reap(QProcess *process, int timeoutMs = 500); - ProcessReaper(); - ~ProcessReaper(); - -private: - static ProcessReaper *instance(); - - QThread m_thread; - ProcessReaperPrivate *m_private; -}; - -static const int s_timeoutThreshold = 10000; // 10 seconds - -static QString execWithArguments(QProcess *process) -{ - QStringList commandLine; - commandLine.append(process->program()); - commandLine.append(process->arguments()); - return commandLine.join(QChar::Space); -} - -struct ReaperSetup -{ - QProcess *m_process = nullptr; - int m_timeoutMs; -}; - -class Reaper : public QObject -{ - Q_OBJECT - -public: - Reaper(const ReaperSetup &reaperSetup) : m_reaperSetup(reaperSetup) {} - - void reap() - { - m_timer.start(); - connect(m_reaperSetup.m_process, &QProcess::finished, this, &Reaper::handleFinished); - if (emitFinished()) - return; - terminate(); - } - -Q_SIGNALS: - void finished(); - -private: - void terminate() - { - m_reaperSetup.m_process->terminate(); - QTimer::singleShot(m_reaperSetup.m_timeoutMs, this, &Reaper::handleTerminateTimeout); - } - - void kill() { m_reaperSetup.m_process->kill(); } - - bool emitFinished() - { - if (m_reaperSetup.m_process->state() != QProcess::NotRunning) - return false; - - if (!m_finished) { - const int timeout = m_timer.elapsed(); - if (timeout > s_timeoutThreshold) { - qWarning() << "Finished parallel reaping of" << execWithArguments(m_reaperSetup.m_process) - << "in" << (timeout / 1000.0) << "seconds."; - } - - m_finished = true; - emit finished(); - } - return true; - } - - void handleFinished() - { - if (emitFinished()) - return; - qWarning() << "Finished process still running..."; - // In case the process is still running - wait until it has finished - QTimer::singleShot(m_reaperSetup.m_timeoutMs, this, &Reaper::handleFinished); - } - - void handleTerminateTimeout() - { - if (emitFinished()) - return; - kill(); - } - - bool m_finished = false; - QElapsedTimer m_timer; - const ReaperSetup m_reaperSetup; -}; - -class ProcessReaperPrivate : public QObject -{ - Q_OBJECT - -public: - // Called from non-reaper's thread - void scheduleReap(const ReaperSetup &reaperSetup) - { - if (QThread::currentThread() == thread()) - qWarning() << "Can't schedule reap from the reaper internal thread."; - - QMutexLocker locker(&m_mutex); - m_reaperSetupList.append(reaperSetup); - QMetaObject::invokeMethod(this, &ProcessReaperPrivate::flush); - } - - // Called from non-reaper's thread - void waitForFinished() - { - if (QThread::currentThread() == thread()) - qWarning() << "Can't wait for finished from the reaper internal thread."; - - QMetaObject::invokeMethod(this, &ProcessReaperPrivate::flush, - Qt::BlockingQueuedConnection); - QMutexLocker locker(&m_mutex); - if (m_reaperList.isEmpty()) - return; - - m_waitCondition.wait(&m_mutex); - } - -private: - // All the private methods are called from the reaper's thread - QList<ReaperSetup> takeReaperSetupList() - { - QMutexLocker locker(&m_mutex); - return std::exchange(m_reaperSetupList, {}); - } - - void flush() - { - while (true) { - const QList<ReaperSetup> reaperSetupList = takeReaperSetupList(); - if (reaperSetupList.isEmpty()) - return; - for (const ReaperSetup &reaperSetup : reaperSetupList) - reap(reaperSetup); - } - } - - void reap(const ReaperSetup &reaperSetup) - { - Reaper *reaper = new Reaper(reaperSetup); - connect(reaper, &Reaper::finished, this, [this, reaper, process = reaperSetup.m_process] { - QMutexLocker locker(&m_mutex); - const bool isRemoved = m_reaperList.removeOne(reaper); - if (!isRemoved) - qWarning() << "Reaper list doesn't contain the finished process."; - - delete reaper; - delete process; - if (m_reaperList.isEmpty()) - m_waitCondition.wakeOne(); - }, Qt::QueuedConnection); - - { - QMutexLocker locker(&m_mutex); - m_reaperList.append(reaper); - } - - reaper->reap(); - } - - QMutex m_mutex; - QWaitCondition m_waitCondition; - QList<ReaperSetup> m_reaperSetupList; - QList<Reaper *> m_reaperList; -}; - -static ProcessReaper *s_instance = nullptr; -static QMutex s_instanceMutex; - -// Call me with s_instanceMutex locked. -ProcessReaper *ProcessReaper::instance() -{ - if (!s_instance) - s_instance = new ProcessReaper; - return s_instance; -} - -ProcessReaper::ProcessReaper() - : m_private(new ProcessReaperPrivate) -{ - m_private->moveToThread(&m_thread); - QObject::connect(&m_thread, &QThread::finished, m_private, &QObject::deleteLater); - m_thread.start(); - m_thread.moveToThread(qApp->thread()); -} - -ProcessReaper::~ProcessReaper() -{ - if (!QThread::isMainThread()) - qWarning() << "Destructing process reaper from non-main thread."; - - instance()->m_private->waitForFinished(); - m_thread.quit(); - m_thread.wait(); -} - -void ProcessReaper::reap(QProcess *process, int timeoutMs) -{ - if (!process) - return; - - if (QThread::currentThread() != process->thread()) { - qWarning() << "Can't reap process from non-process's thread."; - return; - } - - process->disconnect(); - if (process->state() == QProcess::NotRunning) { - delete process; - return; - } - - // Neither can move object with a parent into a different thread - // nor reaping the process with a parent makes any sense. - process->setParent(nullptr); - - QMutexLocker locker(&s_instanceMutex); - ProcessReaperPrivate *priv = instance()->m_private; - - process->moveToThread(priv->thread()); - ReaperSetup reaperSetup {process, timeoutMs}; - priv->scheduleReap(reaperSetup); -} - -void QProcessDeleter::deleteAll() -{ - QMutexLocker locker(&s_instanceMutex); - delete s_instance; - s_instance = nullptr; -} - -void QProcessDeleter::operator()(QProcess *process) -{ - ProcessReaper::reap(process); -} - -} // namespace Tasking - -#endif // QT_CONFIG(process) - -QT_END_NAMESPACE - -#if QT_CONFIG(process) - -#include "qprocesstask.moc" - -#endif // QT_CONFIG(process) - diff --git a/src/assets/downloader/tasking/qprocesstask.h b/src/assets/downloader/tasking/qprocesstask.h deleted file mode 100644 index 6f2ca4a18e2..00000000000 --- a/src/assets/downloader/tasking/qprocesstask.h +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (C) 2024 Jarek Kobus -// Copyright (C) 2024 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 TASKING_QPROCESSTASK_H -#define TASKING_QPROCESSTASK_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 "tasking_global.h" - -#include "tasktree.h" - -#include <QtCore/QProcess> - -QT_BEGIN_NAMESPACE - -#if QT_CONFIG(process) - -namespace Tasking { - -// Deleting a running QProcess may block the caller thread up to 30 seconds and issue warnings. -// To avoid these issues we move the running QProcess into a separate thread -// managed by the internal ProcessReaper, instead of deleting it immediately. -// Inside the ProcessReaper's thread we try to finish the process in a most gentle way: -// we call QProcess::terminate() with 500 ms timeout, and if the process is still running -// after this timeout passed, we call QProcess::kill() and wait for the process to finish. -// All these handlings are done is a separate thread, so the main thread doesn't block at all -// when the QProcessTask is destructed. -// Finally, on application quit, QProcessDeleter::deleteAll() should be called in order -// to synchronize all the processes being still potentially reaped in a separate thread. -// The call to QProcessDeleter::deleteAll() is blocking in case some processes -// are still being reaped. -// This strategy seems most sensible, since when passing the running QProcess into the -// ProcessReaper we don't block immediately, but postpone the possible (not certain) block -// until the end of an application. -// In this way we terminate the running processes in the most safe way and keep the main thread -// responsive. That's a common case when the running application wants to terminate the QProcess -// immediately (e.g. on Cancel button pressed), without keeping and managing the handle -// to the still running QProcess. - -// The implementation of the internal reaper is inspired by the Utils::ProcessReaper taken -// from the QtCreator codebase. - -class TASKING_EXPORT QProcessDeleter -{ -public: - // Blocking, should be called after all QProcessAdapter instances are deleted. - static void deleteAll(); - void operator()(QProcess *process); -}; - -class TASKING_EXPORT QProcessAdapter : public TaskAdapter<QProcess, QProcessDeleter> -{ -private: - void start() final { - connect(task(), &QProcess::finished, this, [this] { - const bool success = task()->exitStatus() == QProcess::NormalExit - && task()->error() == QProcess::UnknownError - && task()->exitCode() == 0; - Q_EMIT done(toDoneResult(success)); - }); - connect(task(), &QProcess::errorOccurred, this, [this](QProcess::ProcessError error) { - if (error != QProcess::FailedToStart) - return; - Q_EMIT done(DoneResult::Error); - }); - task()->start(); - } -}; - -using QProcessTask = CustomTask<QProcessAdapter>; - -} // namespace Tasking - -#endif // QT_CONFIG(process) - -QT_END_NAMESPACE - -#endif // TASKING_QPROCESSTASK_H diff --git a/src/assets/downloader/tasking/tasking_global.h b/src/assets/downloader/tasking/tasking_global.h deleted file mode 100644 index 57f0b7fefef..00000000000 --- a/src/assets/downloader/tasking/tasking_global.h +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (C) 2024 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 TASKING_GLOBAL_H -#define TASKING_GLOBAL_H - -#include <QtCore/qglobal.h> - -QT_BEGIN_NAMESPACE - -// #if defined(QT_SHARED) || !defined(QT_STATIC) -// # if defined(TASKING_LIBRARY) -// # define TASKING_EXPORT Q_DECL_EXPORT -// # else -// # define TASKING_EXPORT Q_DECL_IMPORT -// # endif -// #else -// # define TASKING_EXPORT -// #endif - -#define TASKING_EXPORT - -QT_END_NAMESPACE - -#endif // TASKING_GLOBAL_H diff --git a/src/assets/downloader/tasking/tasktree.cpp b/src/assets/downloader/tasking/tasktree.cpp deleted file mode 100644 index 37064a3e714..00000000000 --- a/src/assets/downloader/tasking/tasktree.cpp +++ /dev/null @@ -1,3701 +0,0 @@ -// Copyright (C) 2024 Jarek Kobus -// Copyright (C) 2024 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 "tasktree.h" - -#include "barrier.h" -#include "conditional.h" - -#include <QtCore/QDebug> -#include <QtCore/QEventLoop> -#include <QtCore/QFutureWatcher> -#include <QtCore/QHash> -#include <QtCore/QMetaEnum> -#include <QtCore/QMutex> -#include <QtCore/QPointer> -#include <QtCore/QPromise> -#include <QtCore/QSet> -#include <QtCore/QTime> -#include <QtCore/QTimer> - -using namespace Qt::StringLiterals; -using namespace std::chrono; - -QT_BEGIN_NAMESPACE - -namespace Tasking { - -// That's cut down qtcassert.{c,h} to avoid the dependency. -#define QT_STRING(cond) qDebug("SOFT ASSERT: \"%s\" in %s: %s", cond, __FILE__, QT_STRINGIFY(__LINE__)) -#define QT_ASSERT(cond, action) if (Q_LIKELY(cond)) {} else { QT_STRING(#cond); action; } do {} while (0) -#define QT_CHECK(cond) if (cond) {} else { QT_STRING(#cond); } do {} while (0) - -class Guard -{ - Q_DISABLE_COPY(Guard) -public: - Guard() = default; - ~Guard() { QT_CHECK(m_lockCount == 0); } - bool isLocked() const { return m_lockCount; } -private: - int m_lockCount = 0; - friend class GuardLocker; -}; - -class GuardLocker -{ - Q_DISABLE_COPY(GuardLocker) -public: - GuardLocker(Guard &guard) : m_guard(guard) { ++m_guard.m_lockCount; } - ~GuardLocker() { --m_guard.m_lockCount; } -private: - Guard &m_guard; -}; - -/*! - \module TaskingSolution - \title Tasking Solution - \ingroup solutions-modules - \brief Contains a general purpose Tasking solution. - - The Tasking solution depends on Qt only, and doesn't depend on any \QC specific code. -*/ - -/*! - \namespace Tasking - \inmodule TaskingSolution - \brief The Tasking namespace encloses all classes and global functions of the Tasking solution. -*/ - -/*! - \class Tasking::TaskInterface - \inheaderfile solutions/tasking/tasktree.h - \inmodule TaskingSolution - \brief TaskInterface is the abstract base class for implementing custom task adapters. - \reentrant - - To implement a custom task adapter, derive your adapter from the - \c TaskAdapter<Task> class template. TaskAdapter automatically creates and destroys - the custom task instance and associates the adapter with a given \c Task type. -*/ - -/*! - \fn virtual void TaskInterface::start() - - This method is called by the running TaskTree for starting the \c Task instance. - Reimplement this method in \c TaskAdapter<Task>'s subclass in order to start the - associated task. - - Use TaskAdapter::task() to access the associated \c Task instance. - - \sa done(), TaskAdapter::task() -*/ - -/*! - \fn void TaskInterface::done(DoneResult result) - - Emit this signal from the \c TaskAdapter<Task>'s subclass, when the \c Task is finished. - Pass DoneResult::Success as a \a result argument when the task finishes with success; - otherwise, when an error occurs, pass DoneResult::Error. -*/ - -/*! - \class Tasking::TaskAdapter - \inheaderfile solutions/tasking/tasktree.h - \inmodule TaskingSolution - \brief A class template for implementing custom task adapters. - \reentrant - - The TaskAdapter class template is responsible for creating a task of the \c Task type, - starting it, and reporting success or an error when the task is finished. - It also associates the adapter with a given \c Task type. - - Reimplement this class with the actual \c Task type to adapt the task's interface - into the general TaskTree's interface for managing the \c Task instances. - - Each subclass needs to provide a public default constructor, - implement the start() method, and emit the done() signal when the task is finished. - Use task() to access the associated \c Task instance. - - To use your task adapter inside the task tree, create an alias to the - Tasking::CustomTask template passing your task adapter as a template parameter: - \code - // Defines actual worker - class Worker {...}; - - // Adapts Worker's interface to work with task tree - class WorkerTaskAdapter : public TaskAdapter<Worker> {...}; - - // Defines WorkerTask as a new custom task type to be placed inside Group items - using WorkerTask = CustomTask<WorkerTaskAdapter>; - \endcode - - Optionally, you may pass a custom \c Deleter for the associated \c Task - as a second template parameter of your \c TaskAdapter subclass. - When the \c Deleter parameter is omitted, the \c std::default_delete<Task> is used by default. - The custom \c Deleter is useful when the destructor of the running \c Task - may potentially block the caller thread. Instead of blocking, the custom deleter may move - the running task into a separate thread and implement the blocking destruction there. - In this way, the fast destruction (seen from the caller thread) of the running task - with a blocking destructor may be achieved. - - For more information on implementing the custom task adapters, refer to \l {Task Adapters}. - - \sa start(), done(), task() -*/ - -/*! - \fn template <typename Task, typename Deleter = std::default_delete<Task>> TaskAdapter<Task, Deleter>::TaskAdapter<Task, Deleter>() - - Creates a task adapter for the given \c Task type. - - Internally, it creates an instance of \c Task, which is accessible via the task() method. - The optionally provided \c Deleter is used instead of the \c Task destructor. - When \c Deleter is omitted, the \c std::default_delete<Task> is used by default. - - \sa task() -*/ - -/*! - \fn template <typename Task, typename Deleter = std::default_delete<Task>> Task *TaskAdapter<Task, Deleter>::task() - - Returns the pointer to the associated \c Task instance. -*/ - -/*! - \fn template <typename Task, typename Deleter = std::default_delete<Task>> Task *TaskAdapter<Task, Deleter>::task() const - \overload - - Returns the \c const pointer to the associated \c Task instance. -*/ - -/*! - \class Tasking::Storage - \inheaderfile solutions/tasking/tasktree.h - \inmodule TaskingSolution - \brief A class template for custom data exchange in the running task tree. - \reentrant - - The Storage class template is responsible for dynamically creating and destructing objects - of the custom \c StorageStruct type. The creation and destruction are managed by the - running task tree. If a Storage object is placed inside a \l {Tasking::Group} {Group} element, - the running task tree creates the \c StorageStruct object when the group is started and before - the group's setup handler is called. Later, whenever any handler inside this group is called, - the task tree activates the previously created instance of the \c StorageStruct object. - This includes all tasks' and groups' setup and done handlers inside the group where the - Storage object was placed, also within the nested groups. - When a copy of the Storage object is passed to the handler via the lambda capture, - the handler may access the instance activated by the running task tree via the - \l {Tasking::Storage::operator->()} {operator->()}, - \l {Tasking::Storage::operator*()} {operator*()}, or activeStorage() method. - If two handlers capture the same Storage object, one of them may store a custom data there, - and the other may read it afterwards. - When the group is finished, the previously created instance of the \c StorageStruct - object is destroyed after the group's done handler is called. - - An example of data exchange between tasks: - - \code - const Storage<QString> storage; - - const auto onFirstDone = [storage](const Task &task) { - // Assings QString, taken from the first task result, to the active QString instance - // of the Storage object. - *storage = task.getResultAsString(); - }; - - const auto onSecondSetup = [storage](Task &task) { - // Reads QString from the active QString instance of the Storage object and use it to - // configure the second task before start. - task.configureWithString(*storage); - }; - - const Group root { - // The running task tree creates QString instance when root in entered - storage, - // The done handler of the first task stores the QString in the storage - TaskItem(..., onFirstDone), - // The setup handler of the second task reads the QString from the storage - TaskItem(onSecondSetup, ...) - }; - \endcode - - Since the root group executes its tasks sequentially, the \c onFirstDone handler - is always called before the \c onSecondSetup handler. This means that the QString data, - read from the \c storage inside the \c onSecondSetup handler's body, - has already been set by the \c onFirstDone handler. - You can always rely on it in \l {Tasking::sequential} {sequential} execution mode. - - The Storage internals are shared between all of its copies. That is why the copies of the - Storage object inside the handlers' lambda captures still refer to the same Storage instance. - You may place multiple Storage objects inside one \l {Tasking::Group} {Group} element, - provided that they do not include copies of the same Storage object. - Otherwise, an assert is triggered at runtime that includes an error message. - However, you can place copies of the same Storage object in different - \l {Tasking::Group} {Group} elements of the same recipe. In this case, the running task - tree will create multiple instances of the \c StorageStruct objects (one for each copy) - and storage shadowing will take place. Storage shadowing works in a similar way - to C++ variable shadowing inside the nested blocks of code: - - \code - Storage<QString> storage; - - const Group root { - storage, // Top copy, 1st instance of StorageStruct - onGroupSetup([storage] { ... }), // Top copy is active - Group { - storage, // Nested copy, 2nd instance of StorageStruct, - // shadows Top copy - onGroupSetup([storage] { ... }), // Nested copy is active - }, - Group { - onGroupSetup([storage] { ... }), // Top copy is active - } - }; - \endcode - - The Storage objects may also be used for passing the initial data to the executed task tree, - and for reading the final data out of the task tree before it finishes. - To do this, use \l {TaskTree::onStorageSetup()} {onStorageSetup()} or - \l {TaskTree::onStorageDone()} {onStorageDone()}, respectively. - - \note If you use an unreachable Storage object inside the handler, - because you forgot to place the storage in the recipe, - or placed it, but not in any handler's ancestor group, - you may expect a crash, preceded by the following message: - \e {The referenced storage is not reachable in the running tree. - A nullptr will be returned which might lead to a crash in the calling code. - It is possible that no storage was added to the tree, - or the storage is not reachable from where it is referenced.} -*/ - -/*! - \fn template <typename StorageStruct> Storage<StorageStruct>::Storage<StorageStruct>() - - Creates a storage for the given \c StorageStruct type. - - \note All copies of \c this object are considered to be the same Storage instance. -*/ - -/*! - \fn template <typename StorageStruct> StorageStruct &Storage<StorageStruct>::operator*() const noexcept - - Returns a \e reference to the active \c StorageStruct object, created by the running task tree. - Use this function only from inside the handler body of any GroupItem element placed - in the recipe, otherwise you may expect a crash. - Make sure that Storage is placed in any group ancestor of the handler's group item. - - \note The returned reference is valid as long as the group that created this instance - is still running. - - \sa activeStorage(), operator->() -*/ - -/*! - \fn template <typename StorageStruct> StorageStruct *Storage<StorageStruct>::operator->() const noexcept - - Returns a \e pointer to the active \c StorageStruct object, created by the running task tree. - Use this function only from inside the handler body of any GroupItem element placed - in the recipe, otherwise you may expect a crash. - Make sure that Storage is placed in any group ancestor of the handler's group item. - - \note The returned pointer is valid as long as the group that created this instance - is still running. - - \sa activeStorage(), operator*() -*/ - -/*! - \fn template <typename StorageStruct> StorageStruct *Storage<StorageStruct>::activeStorage() const - - Returns a \e pointer to the active \c StorageStruct object, created by the running task tree. - Use this function only from inside the handler body of any GroupItem element placed - in the recipe, otherwise you may expect a crash. - Make sure that Storage is placed in any group ancestor of the handler's group item. - - \note The returned pointer is valid as long as the group that created this instance - is still running. - - \sa operator->(), operator*() -*/ - -/*! - \typealias Tasking::GroupItems - - Type alias for QList<GroupItem>. -*/ - -/*! - \class Tasking::GroupItem - \inheaderfile solutions/tasking/tasktree.h - \inmodule TaskingSolution - \brief GroupItem represents the basic element that may be a part of any Group. - \reentrant - - GroupItem is a basic element that may be a part of any \l {Tasking::Group} {Group}. - It encapsulates the functionality provided by any GroupItem's subclass. - It is a value type and it is safe to copy the GroupItem instance, - even when it is originally created via the subclass' constructor. - - There are four main kinds of GroupItem: - \table - \header - \li GroupItem Kind - \li Brief Description - \row - \li \l CustomTask - \li Defines asynchronous task type and task's start, done, and error handlers. - Aliased with a unique task name, such as, \c ConcurrentCallTask<ResultType> - or \c NetworkQueryTask. Asynchronous tasks are the main reason for using a task tree. - \row - \li \l {Tasking::Group} {Group} - \li A container for other group items. Since the group is of the GroupItem type, - it's possible to nest it inside another group. The group is seen by its parent - as a single asynchronous task. - \row - \li GroupItem containing \l {Tasking::Storage} {Storage} - \li Enables the child tasks of a group to exchange data. When GroupItem containing - \l {Tasking::Storage} {Storage} is placed inside a group, the task tree instantiates - the storage's data object just before the group is entered, - and destroys it just after the group is left. - \row - \li Other group control items - \li The items returned by \l {Tasking::parallelLimit()} {parallelLimit()} or - \l {Tasking::workflowPolicy()} {workflowPolicy()} influence the group's behavior. - The items returned by \l {Tasking::onGroupSetup()} {onGroupSetup()} or - \l {Tasking::onGroupDone()} {onGroupDone()} define custom handlers called when - the group starts or ends execution. - \endtable -*/ - -/*! - \fn template <typename StorageStruct> GroupItem::GroupItem(const Storage<StorageStruct> &storage) - - Constructs a \c GroupItem element holding the \a storage object. - - When the \l {Tasking::Group} {Group} element containing \e this GroupItem is entered - by the running task tree, an instance of the \c StorageStruct is created dynamically. - - When that group is about to be left after its execution, the previously instantiated - \c StorageStruct is deleted. - - The dynamically created instance of \c StorageStruct is accessible from inside any - handler body of the parent \l {Tasking::Group} {Group} element, - including nested groups and its tasks, via the - \l {Tasking::Storage::operator->()} {Storage::operator->()}, - \l {Tasking::Storage::operator*()} {Storage::operator*()}, or Storage::activeStorage() method. - - \sa {Tasking::Storage} {Storage} -*/ - -/*! - \fn GroupItem::GroupItem(const GroupItems &items) - - Constructs a \c GroupItem element with a given list of \a items. - - When this \c GroupItem element is parsed by the TaskTree, it is simply replaced with - its \a items. - - This constructor is useful when constructing a \l {Tasking::Group} {Group} element with - lists of \c GroupItem elements: - - \code - static QList<GroupItems> getItems(); - - ... - - const Group root { - parallel, - finishAllAndSuccess, - getItems(), // OK, getItems() list is wrapped into a single GroupItem element - onGroupSetup(...), - onGroupDone(...) - }; - \endcode - - If you want to create a subtree, use \l {Tasking::Group} {Group} instead. - - \note Don't confuse this \c GroupItem with the \l {Tasking::Group} {Group} element, as - \l {Tasking::Group} {Group} keeps its children nested - after being parsed by the task tree, while this \c GroupItem does not. - - \sa {Tasking::Group} {Group} -*/ - -/*! - \fn Tasking::GroupItem(std::initializer_list<GroupItem> items) - \overload - \sa GroupItem(const GroupItems &items) -*/ - -/*! - \class Tasking::Group - \inheaderfile solutions/tasking/tasktree.h - \inmodule TaskingSolution - \brief Group represents the basic element for composing declarative recipes describing - how to execute and handle a nested tree of asynchronous tasks. - \reentrant - - Group is a container for other group items. It encloses child tasks into one unit, - which is seen by the group's parent as a single, asynchronous task. - Since Group is of the GroupItem type, it may also be a child of Group. - - Insert child tasks into the group by using aliased custom task names, such as, - \c ConcurrentCallTask<ResultType> or \c NetworkQueryTask: - - \code - const Group group { - NetworkQueryTask(...), - ConcurrentCallTask<int>(...) - }; - \endcode - - The group's behavior may be customized by inserting the items returned by - \l {Tasking::parallelLimit()} {parallelLimit()} or - \l {Tasking::workflowPolicy()} {workflowPolicy()} functions: - - \code - const Group group { - parallel, - continueOnError, - NetworkQueryTask(...), - NetworkQueryTask(...) - }; - \endcode - - The group may contain nested groups: - - \code - const Group group { - finishAllAndSuccess, - NetworkQueryTask(...), - Group { - NetworkQueryTask(...), - Group { - parallel, - NetworkQueryTask(...), - NetworkQueryTask(...), - } - ConcurrentCallTask<QString>(...) - } - }; - \endcode - - The group may dynamically instantiate a custom storage structure, which may be used for - inter-task data exchange: - - \code - struct MyCustomStruct { QByteArray data; }; - - Storage<MyCustomStruct> storage; - - const auto onFirstSetup = [](NetworkQuery &task) { ... }; - const auto onFirstDone = [storage](const NetworkQuery &task) { - // storage-> gives a pointer to MyCustomStruct instance, - // created dynamically by the running task tree. - storage->data = task.reply()->readAll(); - }; - const auto onSecondSetup = [storage](ConcurrentCall<QImage> &task) { - // storage-> gives a pointer to MyCustomStruct. Since the group is sequential, - // the stored MyCustomStruct was already updated inside the onFirstDone handler. - const QByteArray storedData = storage->data; - }; - - const Group group { - // When the group is entered by a running task tree, it creates MyCustomStruct - // instance dynamically. It is later accessible from all handlers via - // the *storage or storage-> operators. - sequential, - storage, - NetworkQueryTask(onFirstSetup, onFirstDone, CallDoneIf::Success), - ConcurrentCallTask<QImage>(onSecondSetup) - }; - \endcode -*/ - -/*! - \fn Group::Group(const GroupItems &children) - - Constructs a group with a given list of \a children. - - This constructor is useful when the child items of the group are not known at compile time, - but later, at runtime: - - \code - const QStringList sourceList = ...; - - GroupItems groupItems { parallel }; - - for (const QString &source : sourceList) { - const NetworkQueryTask task(...); // use source for setup handler - groupItems << task; - } - - const Group group(groupItems); - \endcode -*/ - -/*! - \fn Group::Group(std::initializer_list<GroupItem> children) - - Constructs a group from \c std::initializer_list given by \a children. - - This constructor is useful when all child items of the group are known at compile time: - - \code - const Group group { - finishAllAndSuccess, - NetworkQueryTask(...), - Group { - NetworkQueryTask(...), - Group { - parallel, - NetworkQueryTask(...), - NetworkQueryTask(...), - } - ConcurrentCallTask<QString>(...) - } - }; - \endcode -*/ - -/*! - \class Tasking::Sync - \inheaderfile solutions/tasking/tasktree.h - \inmodule TaskingSolution - \brief Synchronously executes a custom handler between other tasks. - \reentrant - - \c Sync is useful when you want to execute an additional handler between other tasks. - \c Sync is seen by its parent \l {Tasking::Group} {Group} as any other task. - Avoid long-running execution of the \c Sync's handler body, since it is executed - synchronously from the caller thread. If that is unavoidable, consider using - \c ConcurrentCallTask instead. -*/ - -/*! - \fn template <typename Handler> Sync::Sync(Handler &&handler) - - Constructs an element that executes a passed \a handler synchronously. - The \c Handler is of the \c std::function<DoneResult()> type. - The DoneResult value, returned by the \a handler, is considered during parent group's - \l {workflowPolicy} {workflow policy} resolution. - Optionally, the shortened form of \c std::function<void()> is also accepted. - In this case, it's assumed that the return value is DoneResult::Success. - - The passed \a handler executes synchronously from the caller thread, so avoid a long-running - execution of the handler body. Otherwise, consider using \c ConcurrentCallTask. - - \note The \c Sync element is not counted as a task when reporting task tree progress, - and is not included in TaskTree::taskCount() or TaskTree::progressMaximum(). -*/ - -/*! - \class Tasking::CustomTask - \inheaderfile solutions/tasking/tasktree.h - \inmodule TaskingSolution - \brief A class template used for declaring custom task items and defining their setup - and done handlers. - \reentrant - - Describes custom task items within task tree recipes. - - Custom task names are aliased with unique names using the \c CustomTask template - with a given TaskAdapter subclass as a template parameter. - For example, \c ConcurrentCallTask<T> is an alias to the \c CustomTask that is defined - to work with \c ConcurrentCall<T> as an associated task class. - The following table contains example custom tasks and their associated task classes: - - \table - \header - \li Aliased Task Name (Tasking Namespace) - \li Associated Task Class - \li Brief Description - \row - \li ConcurrentCallTask<ReturnType> - \li ConcurrentCall<ReturnType> - \li Starts an asynchronous task. Runs in a separate thread. - \row - \li NetworkQueryTask - \li NetworkQuery - \li Sends a network query. - \row - \li TaskTreeTask - \li TaskTree - \li Starts a nested task tree. - \row - \li TimeoutTask - \li \c std::chrono::milliseconds - \li Starts a timer. - \row - \li WaitForBarrierTask - \li MultiBarrier<Limit> - \li Starts an asynchronous task waiting for the barrier to pass. - \endtable -*/ - -/*! - \typealias Tasking::CustomTask::Task - - Type alias for the task type associated with the custom task's \c Adapter. -*/ - -/*! - \typealias Tasking::CustomTask::Deleter - - Type alias for the task's type deleter associated with the custom task's \c Adapter. -*/ - -/*! - \typealias Tasking::CustomTask::TaskSetupHandler - - Type alias for \c std::function<SetupResult(Task &)>. - - The \c TaskSetupHandler is an optional argument of a custom task element's constructor. - Any function with the above signature, when passed as a task setup handler, - will be called by the running task tree after the task is created and before it is started. - - Inside the body of the handler, you may configure the task according to your needs. - The additional parameters, including storages, may be passed to the handler - via the lambda capture. - You can decide dynamically whether the task should be started or skipped with - success or an error. - - \note Do not start the task inside the start handler by yourself. Leave it for TaskTree, - otherwise the behavior is undefined. - - The return value of the handler instructs the running task tree on how to proceed - after the handler's invocation is finished. The return value of SetupResult::Continue - instructs the task tree to continue running, that is, to execute the associated \c Task. - The return value of SetupResult::StopWithSuccess or SetupResult::StopWithError - instructs the task tree to skip the task's execution and finish it immediately with - success or an error, respectively. - - When the return type is either SetupResult::StopWithSuccess or SetupResult::StopWithError, - the task's done handler (if provided) isn't called afterwards. - - The constructor of a custom task accepts also functions in the shortened form of - \c std::function<void(Task &)>, that is, the return value is \c void. - In this case, it's assumed that the return value is SetupResult::Continue. - - \sa CustomTask(), TaskDoneHandler, GroupSetupHandler -*/ - -/*! - \typealias Tasking::CustomTask::TaskDoneHandler - - Type alias for \c std::function<DoneResult(const Task &, DoneWith)> or DoneResult. - - The \c TaskDoneHandler is an optional argument of a custom task element's constructor. - Any function with the above signature, when passed as a task done handler, - will be called by the running task tree after the task execution finished and before - the final result of the execution is reported back to the parent group. - - Inside the body of the handler you may retrieve the final data from the finished task. - The additional parameters, including storages, may be passed to the handler - via the lambda capture. - It is also possible to decide dynamically whether the task should finish with its return - value, or the final result should be tweaked. - - The DoneWith argument is optional and your done handler may omit it. - When provided, it holds the info about the final result of a task that will be - reported to its parent. - - If you do not plan to read any data from the finished task, - you may omit the \c {const Task &} argument. - - The returned DoneResult value is optional and your handler may return \c void instead. - In this case, the final result of the task will be equal to the value indicated by - the DoneWith argument. When the handler returns the DoneResult value, - the task's final result may be tweaked inside the done handler's body by the returned value. - - For a \c TaskDoneHandler of the DoneResult type, no additional handling is executed, - and the task finishes unconditionally with the passed value of DoneResult. - - \sa CustomTask(), TaskSetupHandler, GroupDoneHandler -*/ - -/*! - \fn template <typename Adapter> template <typename SetupHandler = TaskSetupHandler, typename DoneHandler = TaskDoneHandler> CustomTask<Adapter>::CustomTask(SetupHandler &&setup = TaskSetupHandler(), DoneHandler &&done = TaskDoneHandler(), CallDoneIf callDoneIf = CallDoneIf::SuccessOrError) - - Constructs a \c CustomTask instance and attaches the \a setup and \a done handlers to the task. - When the running task tree is about to start the task, - it instantiates the associated \l Task object, invokes \a setup handler with a \e reference - to the created task, and starts it. When the running task finishes, - the task tree invokes a \a done handler, with a \c const \e reference to the created task. - - The passed \a setup handler is of the \l TaskSetupHandler type. For example: - - \code - static void parseAndLog(const QString &input); - - ... - - const QString input = ...; - - const auto onFirstSetup = [input](ConcurrentCall<void> &task) { - if (input == "Skip") - return SetupResult::StopWithSuccess; // This task won't start, the next one will - if (input == "Error") - return SetupResult::StopWithError; // This task and the next one won't start - task.setConcurrentCallData(parseAndLog, input); - // This task will start, and the next one will start after this one finished with success - return SetupResult::Continue; - }; - - const auto onSecondSetup = [input](ConcurrentCall<void> &task) { - task.setConcurrentCallData(parseAndLog, input); - }; - - const Group group { - ConcurrentCallTask<void>(onFirstSetup), - ConcurrentCallTask<void>(onSecondSetup) - }; - \endcode - - The \a done handler is of the \l TaskDoneHandler type. - By default, the \a done handler is invoked whenever the task finishes. - Pass a non-default value for the \a callDoneIf argument when you want the handler to be called - only on a successful or failed execution. - - \sa TaskSetupHandler, TaskDoneHandler -*/ - -/*! - \enum Tasking::WorkflowPolicy - - This enum describes the possible behavior of the Group element when any group's child task - finishes its execution. It's also used when the running Group is canceled. - - \value StopOnError - Default. Corresponds to the stopOnError global element. - If any child task finishes with an error, the group stops and finishes with an error. - If all child tasks finished with success, the group finishes with success. - If a group is empty, it finishes with success. - \value ContinueOnError - Corresponds to the continueOnError global element. - Similar to stopOnError, but in case any child finishes with an error, - the execution continues until all tasks finish, and the group reports an error - afterwards, even when some other tasks in the group finished with success. - If all child tasks finish successfully, the group finishes with success. - If a group is empty, it finishes with success. - \value StopOnSuccess - Corresponds to the stopOnSuccess global element. - If any child task finishes with success, the group stops and finishes with success. - If all child tasks finished with an error, the group finishes with an error. - If a group is empty, it finishes with an error. - \value ContinueOnSuccess - Corresponds to the continueOnSuccess global element. - Similar to stopOnSuccess, but in case any child finishes successfully, - the execution continues until all tasks finish, and the group reports success - afterwards, even when some other tasks in the group finished with an error. - If all child tasks finish with an error, the group finishes with an error. - If a group is empty, it finishes with an error. - \value StopOnSuccessOrError - Corresponds to the stopOnSuccessOrError global element. - The group starts as many tasks as it can. When any task finishes, - the group stops and reports the task's result. - Useful only in parallel mode. - In sequential mode, only the first task is started, and when finished, - the group finishes too, so the other tasks are always skipped. - If a group is empty, it finishes with an error. - \value FinishAllAndSuccess - Corresponds to the finishAllAndSuccess global element. - The group executes all tasks and ignores their return results. When all - tasks finished, the group finishes with success. - If a group is empty, it finishes with success. - \value FinishAllAndError - Corresponds to the finishAllAndError global element. - The group executes all tasks and ignores their return results. When all - tasks finished, the group finishes with an error. - If a group is empty, it finishes with an error. - - Whenever a child task's result causes the Group to stop, that is, - in case of StopOnError, StopOnSuccess, or StopOnSuccessOrError policies, - the Group cancels the other running child tasks (if any - for example in parallel mode), - and skips executing tasks it has not started yet (for example, in the sequential mode - - those, that are placed after the failed task). Both canceling and skipping child tasks - may happen when parallelLimit() is used. - - The table below summarizes the differences between various workflow policies: - - \table - \header - \li \l WorkflowPolicy - \li Executes all child tasks - \li Result - \li Result when the group is empty - \row - \li StopOnError - \li Stops when any child task finished with an error and reports an error - \li An error when at least one child task failed, success otherwise - \li Success - \row - \li ContinueOnError - \li Yes - \li An error when at least one child task failed, success otherwise - \li Success - \row - \li StopOnSuccess - \li Stops when any child task finished with success and reports success - \li Success when at least one child task succeeded, an error otherwise - \li An error - \row - \li ContinueOnSuccess - \li Yes - \li Success when at least one child task succeeded, an error otherwise - \li An error - \row - \li StopOnSuccessOrError - \li Stops when any child task finished and reports child task's result - \li Success or an error, depending on the finished child task's result - \li An error - \row - \li FinishAllAndSuccess - \li Yes - \li Success - \li Success - \row - \li FinishAllAndError - \li Yes - \li An error - \li An error - \endtable - - If a child of a group is also a group, the child group runs its tasks according to its own - workflow policy. When a parent group stops the running child group because - of parent group's workflow policy, that is, when the StopOnError, StopOnSuccess, - or StopOnSuccessOrError policy was used for the parent, - the child group's result is reported according to the - \b Result column and to the \b {child group's workflow policy} row in the table above. -*/ - -/*! - \variable Tasking::nullItem - - A convenient global group's element indicating a no-op item. - - This is useful in conditional expressions to indicate the absence of an optional element: - - \code - const ExecutableItem task = ...; - const std::optional<ExecutableItem> optionalTask = ...; - - Group group { - task, - optionalTask ? *optionalTask : nullItem - }; - \endcode -*/ - -/*! - \variable Tasking::successItem - - A convenient global executable element containing an empty, successful, synchronous task. - - This is useful in if-statements to indicate that a branch ends with success: - - \code - const ExecutableItem conditionalTask = ...; - - Group group { - stopOnDone, - If (conditionalTask) >> Then { - ... - } >> Else { - successItem - }, - nextTask - }; - \endcode - - In the above example, if the \c conditionalTask finishes with an error, the \c Else branch - is chosen, which finishes immediately with success. This causes the \c nextTask to be skipped - (because of the stopOnDone workflow policy of the \c group) - and the \c group finishes with success. - - \sa errorItem -*/ - -/*! - \variable Tasking::errorItem - - A convenient global executable element containing an empty, erroneous, synchronous task. - - This is useful in if-statements to indicate that a branch ends with an error: - - \code - const ExecutableItem conditionalTask = ...; - - Group group { - stopOnError, - If (conditionalTask) >> Then { - ... - } >> Else { - errorItem - }, - nextTask - }; - \endcode - - In the above example, if the \c conditionalTask finishes with an error, the \c Else branch - is chosen, which finishes immediately with an error. This causes the \c nextTask to be skipped - (because of the stopOnError workflow policy of the \c group) - and the \c group finishes with an error. - - \sa successItem -*/ - -/*! - \variable Tasking::sequential - A convenient global group's element describing the sequential execution mode. - - This is the default execution mode of the Group element. - - When a Group has no execution mode, it runs in the sequential mode. - All the direct child tasks of a group are started in a chain, so that when one task finishes, - the next one starts. This enables you to pass the results from the previous task - as input to the next task before it starts. This mode guarantees that the next task - is started only after the previous task finishes. - - \sa parallel, parallelLimit() -*/ - -/*! - \variable Tasking::parallel - A convenient global group's element describing the parallel execution mode. - - All the direct child tasks of a group are started after the group is started, - without waiting for the previous child tasks to finish. - In this mode, all child tasks run simultaneously. - - \sa sequential, parallelLimit() -*/ - -/*! - \variable Tasking::parallelIdealThreadCountLimit - A convenient global group's element describing the parallel execution mode with a limited - number of tasks running simultanously. The limit is equal to the ideal number of threads - excluding the calling thread. - - This is a shortcut to: - \code - parallelLimit(qMax(QThread::idealThreadCount() - 1, 1)) - \endcode - - \sa parallel, parallelLimit() -*/ - -/*! - \variable Tasking::stopOnError - A convenient global group's element describing the StopOnError workflow policy. - - This is the default workflow policy of the Group element. -*/ - -/*! - \variable Tasking::continueOnError - A convenient global group's element describing the ContinueOnError workflow policy. -*/ - -/*! - \variable Tasking::stopOnSuccess - A convenient global group's element describing the StopOnSuccess workflow policy. -*/ - -/*! - \variable Tasking::continueOnSuccess - A convenient global group's element describing the ContinueOnSuccess workflow policy. -*/ - -/*! - \variable Tasking::stopOnSuccessOrError - A convenient global group's element describing the StopOnSuccessOrError workflow policy. -*/ - -/*! - \variable Tasking::finishAllAndSuccess - A convenient global group's element describing the FinishAllAndSuccess workflow policy. -*/ - -/*! - \variable Tasking::finishAllAndError - A convenient global group's element describing the FinishAllAndError workflow policy. -*/ - -/*! - \enum Tasking::SetupResult - - This enum is optionally returned from the group's or task's setup handler function. - It instructs the running task tree on how to proceed after the setup handler's execution - finished. - \value Continue - Default. The group's or task's execution continues normally. - When a group's or task's setup handler returns void, it's assumed that - it returned Continue. - \value StopWithSuccess - The group's or task's execution stops immediately with success. - When returned from the group's setup handler, all child tasks are skipped, - and the group's onGroupDone() handler is invoked with DoneWith::Success. - The group reports success to its parent. The group's workflow policy is ignored. - When returned from the task's setup handler, the task isn't started, - its done handler isn't invoked, and the task reports success to its parent. - \value StopWithError - The group's or task's execution stops immediately with an error. - When returned from the group's setup handler, all child tasks are skipped, - and the group's onGroupDone() handler is invoked with DoneWith::Error. - The group reports an error to its parent. The group's workflow policy is ignored. - When returned from the task's setup handler, the task isn't started, - its error handler isn't invoked, and the task reports an error to its parent. -*/ - -/*! - \enum Tasking::DoneResult - - This enum is optionally returned from the group's or task's done handler function. - When the done handler doesn't return any value, that is, its return type is \c void, - its final return value is automatically deduced by the running task tree and reported - to its parent group. - - When the done handler returns the DoneResult, you can tweak the final return value - inside the handler. - - When the DoneResult is returned by the group's done handler, the group's workflow policy - is ignored. - - This enum is also used inside the TaskInterface::done() signal and it indicates whether - the task finished with success or an error. - - \value Success - The group's or task's execution ends with success. - \value Error - The group's or task's execution ends with an error. -*/ - -/*! - \enum Tasking::DoneWith - - This enum is an optional argument for the group's or task's done handler. - It indicates whether the group or task finished with success or an error, or it was canceled. - - It is also used as an argument inside the TaskTree::done() signal, - indicating the final result of the TaskTree execution. - - \value Success - The group's or task's execution ended with success. - \value Error - The group's or task's execution ended with an error. - \value Cancel - The group's or task's execution was canceled. This happens when the user calls - TaskTree::cancel() for the running task tree or when the group's workflow policy - results in canceling some of its running children. - Tweaking the done handler's final result by returning Tasking::DoneResult from - the handler is no-op when the group's or task's execution was canceled. -*/ - -/*! - \enum Tasking::CallDoneIf - - This enum is an optional argument for the \l onGroupDone() element or custom task's constructor. - It instructs the task tree on when the group's or task's done handler should be invoked. - - \value SuccessOrError - The done handler is always invoked. - \value Success - The done handler is invoked only after successful execution, - that is, when DoneWith::Success. - \value Error - The done handler is invoked only after failed execution, - that is, when DoneWith::Error or when DoneWith::Cancel. -*/ - -/*! - \typealias Tasking::GroupItem::GroupSetupHandler - - Type alias for \c std::function<SetupResult()>. - - The \c GroupSetupHandler is an argument of the onGroupSetup() element. - Any function with the above signature, when passed as a group setup handler, - will be called by the running task tree when the group execution starts. - - The return value of the handler instructs the running group on how to proceed - after the handler's invocation is finished. The default return value of SetupResult::Continue - instructs the group to continue running, that is, to start executing its child tasks. - The return value of SetupResult::StopWithSuccess or SetupResult::StopWithError - instructs the group to skip the child tasks' execution and finish immediately with - success or an error, respectively. - - When the return type is either SetupResult::StopWithSuccess or SetupResult::StopWithError, - the group's done handler (if provided) is called synchronously immediately afterwards. - - \note Even if the group setup handler returns StopWithSuccess or StopWithError, - the group's done handler is invoked. This behavior differs from that of task done handler - and might change in the future. - - The onGroupSetup() element accepts also functions in the shortened form of - \c std::function<void()>, that is, the return value is \c void. - In this case, it's assumed that the return value is SetupResult::Continue. - - \sa onGroupSetup(), GroupDoneHandler, CustomTask::TaskSetupHandler -*/ - -/*! - \typealias Tasking::GroupItem::GroupDoneHandler - - Type alias for \c std::function<DoneResult(DoneWith)> or DoneResult. - - The \c GroupDoneHandler is an argument of the onGroupDone() element. - Any function with the above signature, when passed as a group done handler, - will be called by the running task tree when the group execution ends. - - The DoneWith argument is optional and your done handler may omit it. - When provided, it holds the info about the final result of a group that will be - reported to its parent. - - The returned DoneResult value is optional and your handler may return \c void instead. - In this case, the final result of the group will be equal to the value indicated by - the DoneWith argument. When the handler returns the DoneResult value, - the group's final result may be tweaked inside the done handler's body by the returned value. - - For a \c GroupDoneHandler of the DoneResult type, no additional handling is executed, - and the group finishes unconditionally with the passed value of DoneResult, - ignoring the group's workflow policy. - - \sa onGroupDone(), GroupSetupHandler, CustomTask::TaskDoneHandler -*/ - -/*! - \fn template <typename Handler> GroupItem onGroupSetup(Handler &&handler) - - Constructs a group's element holding the group setup handler. - The \a handler is invoked whenever the group starts. - - The passed \a handler is either of the \c std::function<SetupResult()> or the - \c std::function<void()> type. For more information on a possible handler type, refer to - \l {GroupItem::GroupSetupHandler}. - - When the \a handler is invoked, none of the group's child tasks are running yet. - - If a group contains the Storage elements, the \a handler is invoked - after the storages are constructed, so that the \a handler may already - perform some initial modifications to the active storages. - - \sa GroupItem::GroupSetupHandler, onGroupDone() -*/ - -/*! - \fn template <typename Handler> GroupItem onGroupDone(Handler &&handler, CallDoneIf callDoneIf = CallDoneIf::SuccessOrError) - - Constructs a group's element holding the group done handler. - By default, the \a handler is invoked whenever the group finishes. - Pass a non-default value for the \a callDoneIf argument when you want the handler to be called - only on a successful or failed execution. - Depending on the group's workflow policy, this handler may also be called - when the running group is canceled (e.g. when stopOnError element was used). - - The passed \a handler is of the \c std::function<DoneResult(DoneWith)> type. - Optionally, each of the return DoneResult type or the argument DoneWith type may be omitted - (that is, its return type may be \c void). For more information on a possible handler type, - refer to \l {GroupItem::GroupDoneHandler}. - - When the \a handler is invoked, all of the group's child tasks are already finished. - - If a group contains the Storage elements, the \a handler is invoked - before the storages are destructed, so that the \a handler may still - perform a last read of the active storages' data. - - \sa GroupItem::GroupDoneHandler, onGroupSetup() -*/ - -/*! - Constructs a group's element describing the \l{Execution Mode}{execution mode}. - - The execution mode element in a Group specifies how the direct child tasks of - the Group are started. - - For convenience, when appropriate, the \l sequential or \l parallel global elements - may be used instead. - - The \a limit defines the maximum number of direct child tasks running in parallel: - - \list - \li When \a limit equals to 0, there is no limit, and all direct child tasks are started - together, in the oder in which they appear in a group. This means the fully parallel - execution, and the \l parallel element may be used instead. - - \li When \a limit equals to 1, it means that only one child task may run at the time. - This means the sequential execution, and the \l sequential element may be used instead. - In this case, child tasks run in chain, so the next child task starts after - the previous child task has finished. - - \li When other positive number is passed as \a limit, the group's child tasks run - in parallel, but with a limited number of tasks running simultanously. - The \e limit defines the maximum number of tasks running in parallel in a group. - When the group is started, the first batch of tasks is started - (the number of tasks in a batch equals to the passed \a limit, at most), - while the others are kept waiting. When any running task finishes, - the group starts the next remaining one, so that the \e limit of simultaneously - running tasks inside a group isn't exceeded. This repeats on every child task's - finish until all child tasks are started. This enables you to limit the maximum - number of tasks that run simultaneously, for example if running too many processes might - block the machine for a long time. - \endlist - - In all execution modes, a group starts tasks in the oder in which they appear. - - If a child of a group is also a group, the child group runs its tasks according - to its own execution mode. - - \sa sequential, parallel -*/ - -GroupItem parallelLimit(int limit) -{ - struct ParallelLimit : GroupItem { - ParallelLimit(int limit) : GroupItem({{}, limit}) {} - }; - return ParallelLimit(limit); -} - -/*! - Constructs a group's \l {Workflow Policy} {workflow policy} element for a given \a policy. - - For convenience, global elements may be used instead. - - \sa stopOnError, continueOnError, stopOnSuccess, continueOnSuccess, stopOnSuccessOrError, - finishAllAndSuccess, finishAllAndError, WorkflowPolicy -*/ -GroupItem workflowPolicy(WorkflowPolicy policy) -{ - struct WorkflowPolicyItem : GroupItem { - WorkflowPolicyItem(WorkflowPolicy policy) : GroupItem({{}, {}, policy}) {} - }; - return WorkflowPolicyItem(policy); -} - -const GroupItem sequential = parallelLimit(1); -const GroupItem parallel = parallelLimit(0); -const GroupItem parallelIdealThreadCountLimit = parallelLimit(qMax(QThread::idealThreadCount() - 1, 1)); - -const GroupItem stopOnError = workflowPolicy(WorkflowPolicy::StopOnError); -const GroupItem continueOnError = workflowPolicy(WorkflowPolicy::ContinueOnError); -const GroupItem stopOnSuccess = workflowPolicy(WorkflowPolicy::StopOnSuccess); -const GroupItem continueOnSuccess = workflowPolicy(WorkflowPolicy::ContinueOnSuccess); -const GroupItem stopOnSuccessOrError = workflowPolicy(WorkflowPolicy::StopOnSuccessOrError); -const GroupItem finishAllAndSuccess = workflowPolicy(WorkflowPolicy::FinishAllAndSuccess); -const GroupItem finishAllAndError = workflowPolicy(WorkflowPolicy::FinishAllAndError); - -// Keep below the above in order to avoid static initialization fiasco. -const GroupItem nullItem = Group {}; -const ExecutableItem successItem = Group { finishAllAndSuccess }; -const ExecutableItem errorItem = Group { finishAllAndError }; - -Group operator>>(const For &forItem, const Do &doItem) -{ - return {forItem.m_loop, doItem.m_children}; -} - -Group operator>>(const When &whenItem, const Do &doItem) -{ - const SingleBarrier barrier; - - return { - barrier, - parallel, - whenItem.m_barrierKicker(barrier), - Group { - waitForBarrierTask(barrier), - doItem.m_children - } - }; -} - -// Please note the thread_local keyword below guarantees a separate instance per thread. -// The s_activeTaskTrees is currently used internally only and is not exposed in the public API. -// It serves for withLog() implementation now. Add a note here when a new usage is introduced. -static thread_local QList<TaskTree *> s_activeTaskTrees = {}; - -static TaskTree *activeTaskTree() -{ - QT_ASSERT(s_activeTaskTrees.size(), return nullptr); - return s_activeTaskTrees.back(); -} - -DoneResult toDoneResult(bool success) -{ - return success ? DoneResult::Success : DoneResult::Error; -} - -static SetupResult toSetupResult(bool success) -{ - return success ? SetupResult::StopWithSuccess : SetupResult::StopWithError; -} - -static DoneResult toDoneResult(DoneWith doneWith) -{ - return doneWith == DoneWith::Success ? DoneResult::Success : DoneResult::Error; -} - -static DoneWith toDoneWith(DoneResult result) -{ - return result == DoneResult::Success ? DoneWith::Success : DoneWith::Error; -} - -class LoopThreadData -{ - Q_DISABLE_COPY_MOVE(LoopThreadData) - -public: - LoopThreadData() = default; - void pushIteration(int index) - { - m_activeLoopStack.push_back(index); - } - void popIteration() - { - QT_ASSERT(m_activeLoopStack.size(), return); - m_activeLoopStack.pop_back(); - } - int iteration() const - { - QT_ASSERT(m_activeLoopStack.size(), qWarning( - "The referenced loop is not reachable in the running tree. " - "A -1 will be returned which might lead to a crash in the calling code. " - "It is possible that no loop was added to the tree, " - "or the loop is not reachable from where it is referenced."); return -1); - return m_activeLoopStack.last(); - } - -private: - QList<int> m_activeLoopStack; -}; - -class LoopData -{ -public: - LoopThreadData &threadData() { - QMutexLocker lock(&m_threadDataMutex); - return m_threadDataMap.try_emplace(QThread::currentThread()).first->second; - } - - const std::optional<int> m_loopCount = {}; - const Loop::ValueGetter m_valueGetter = {}; - const Loop::Condition m_condition = {}; - QMutex m_threadDataMutex = {}; - // Use std::map on purpose, so that it doesn't invalidate references on modifications. - // Don't optimize it by using std::unordered_map. - std::map<QThread *, LoopThreadData> m_threadDataMap = {}; -}; - -Loop::Loop() - : m_loopData(new LoopData) -{} - -Loop::Loop(int count, const ValueGetter &valueGetter) - : m_loopData(new LoopData{count, valueGetter}) -{} - -Loop::Loop(const Condition &condition) - : m_loopData(new LoopData{{}, {}, condition}) -{} - -int Loop::iteration() const -{ - return m_loopData->threadData().iteration(); -} - -const void *Loop::valuePtr() const -{ - return m_loopData->m_valueGetter(iteration()); -} - -using StoragePtr = void *; - -static constexpr QLatin1StringView s_activeStorageWarning = - "The referenced storage is not reachable in the running tree. " - "A nullptr will be returned which might lead to a crash in the calling code. " - "It is possible that no storage was added to the tree, " - "or the storage is not reachable from where it is referenced."_L1; - -class StorageThreadData -{ - Q_DISABLE_COPY_MOVE(StorageThreadData) - -public: - StorageThreadData() = default; - void pushStorage(StoragePtr storagePtr) - { - m_activeStorageStack.push_back({storagePtr, activeTaskTree()}); - } - void popStorage() - { - QT_ASSERT(m_activeStorageStack.size(), return); - m_activeStorageStack.pop_back(); - } - StoragePtr activeStorage() const - { - QT_ASSERT(m_activeStorageStack.size(), - qWarning().noquote() << s_activeStorageWarning; return nullptr); - const QPair<StoragePtr, TaskTree *> &top = m_activeStorageStack.last(); - QT_ASSERT(top.second == activeTaskTree(), - qWarning().noquote() << s_activeStorageWarning; return nullptr); - return top.first; - } - -private: - QList<QPair<StoragePtr, TaskTree *>> m_activeStorageStack; -}; - -class StorageData -{ -public: - StorageThreadData &threadData() { - QMutexLocker lock(&m_threadDataMutex); - return m_threadDataMap.try_emplace(QThread::currentThread()).first->second; - } - - const StorageBase::StorageConstructor m_constructor = {}; - const StorageBase::StorageDestructor m_destructor = {}; - QMutex m_threadDataMutex = {}; - // Use std::map on purpose, so that it doesn't invalidate references on modifications. - // Don't optimize it by using std::unordered_map. - std::map<QThread *, StorageThreadData> m_threadDataMap = {}; -}; - -StorageBase::StorageBase(const StorageConstructor &ctor, const StorageDestructor &dtor) - : m_storageData(new StorageData{ctor, dtor}) -{} - -void *StorageBase::activeStorageVoid() const -{ - return m_storageData->threadData().activeStorage(); -} - -void GroupItem::addChildren(const GroupItems &children) -{ - QT_ASSERT(m_type == Type::Group || m_type == Type::List, - qWarning("Only Group or List may have children, skipping..."); return); - if (m_type == Type::List) { - m_children.append(children); - return; - } - for (const GroupItem &child : children) { - switch (child.m_type) { - case Type::List: - addChildren(child.m_children); - break; - case Type::Group: - m_children.append(child); - break; - case Type::GroupData: - if (child.m_groupData.m_groupHandler.m_setupHandler) { - QT_ASSERT(!m_groupData.m_groupHandler.m_setupHandler, - qWarning("Group setup handler redefinition, overriding...")); - m_groupData.m_groupHandler.m_setupHandler - = child.m_groupData.m_groupHandler.m_setupHandler; - } - if (child.m_groupData.m_groupHandler.m_doneHandler) { - QT_ASSERT(!m_groupData.m_groupHandler.m_doneHandler, - qWarning("Group done handler redefinition, overriding...")); - m_groupData.m_groupHandler.m_doneHandler - = child.m_groupData.m_groupHandler.m_doneHandler; - m_groupData.m_groupHandler.m_callDoneIf - = child.m_groupData.m_groupHandler.m_callDoneIf; - } - if (child.m_groupData.m_parallelLimit) { - QT_ASSERT(!m_groupData.m_parallelLimit, - qWarning("Group execution mode redefinition, overriding...")); - m_groupData.m_parallelLimit = child.m_groupData.m_parallelLimit; - } - if (child.m_groupData.m_workflowPolicy) { - QT_ASSERT(!m_groupData.m_workflowPolicy, - qWarning("Group workflow policy redefinition, overriding...")); - m_groupData.m_workflowPolicy = child.m_groupData.m_workflowPolicy; - } - if (child.m_groupData.m_loop) { - QT_ASSERT(!m_groupData.m_loop, - qWarning("Group loop redefinition, overriding...")); - m_groupData.m_loop = child.m_groupData.m_loop; - } - break; - case Type::TaskHandler: - QT_ASSERT(child.m_taskHandler.m_createHandler, - qWarning("Task create handler can't be null, skipping..."); return); - m_children.append(child); - break; - case Type::Storage: - // Check for duplicates, as can't have the same storage twice on the same level. - for (const StorageBase &storage : child.m_storageList) { - if (m_storageList.contains(storage)) { - QT_ASSERT(false, qWarning("Can't add the same storage into one Group twice, " - "skipping...")); - continue; - } - m_storageList.append(storage); - } - break; - } - } -} - -/*! - \class Tasking::ExecutableItem - \inheaderfile solutions/tasking/tasktree.h - \inmodule TaskingSolution - \brief Base class for executable task items. - \reentrant - - \c ExecutableItem provides an additional interface for items containing executable tasks. - Use withTimeout() to attach a timeout to a task. - Use withLog() to include debugging information about the task startup and the execution result. -*/ - -/*! - Attaches \c TimeoutTask to a copy of \c this ExecutableItem, elapsing after \a timeout - in milliseconds, with an optionally provided timeout \a handler, and returns the coupled item. - - When the ExecutableItem finishes before \a timeout passes, the returned item finishes - immediately with the task's result. Otherwise, \a handler is invoked (if provided), - the task is canceled, and the returned item finishes with an error. -*/ -Group ExecutableItem::withTimeout(milliseconds timeout, - const std::function<void()> &handler) const -{ - const auto onSetup = [timeout](milliseconds &timeoutData) { timeoutData = timeout; }; - return Group { - parallel, - stopOnSuccessOrError, - Group { - finishAllAndError, - handler ? TimeoutTask(onSetup, [handler] { handler(); }, CallDoneIf::Success) - : TimeoutTask(onSetup) - }, - *this - }; -} - -static QString currentTime() { return QTime::currentTime().toString(Qt::ISODateWithMs); } - -static QString logHeader(const QString &logName) -{ - return QString::fromLatin1("TASK TREE LOG [%1] \"%2\"").arg(currentTime(), logName); -}; - -/*! - Attaches a custom debug printout to a copy of \c this ExecutableItem, - issued on task startup and after the task is finished, and returns the coupled item. - - The debug printout includes a timestamp of the event (start or finish) - and \a logName to identify the specific task in the debug log. - - The finish printout contains the additional information whether the execution was - synchronous or asynchronous, its result (the value described by the DoneWith enum), - and the total execution time in milliseconds. -*/ -Group ExecutableItem::withLog(const QString &logName) const -{ - struct LogStorage - { - time_point<system_clock, nanoseconds> start; - int asyncCount = 0; - }; - const Storage<LogStorage> storage; - return Group { - storage, - onGroupSetup([storage, logName] { - storage->start = system_clock::now(); - storage->asyncCount = activeTaskTree()->asyncCount(); - qDebug().noquote().nospace() << logHeader(logName) << " started."; - }), - *this, - onGroupDone([storage, logName](DoneWith result) { - const auto elapsed = duration_cast<milliseconds>(system_clock::now() - storage->start); - const int asyncCountDiff = activeTaskTree()->asyncCount() - storage->asyncCount; - QT_CHECK(asyncCountDiff >= 0); - const QMetaEnum doneWithEnum = QMetaEnum::fromType<DoneWith>(); - const QString syncType = asyncCountDiff ? QString::fromLatin1("asynchronously") - : QString::fromLatin1("synchronously"); - qDebug().noquote().nospace() << logHeader(logName) << " finished " << syncType - << " with " << doneWithEnum.valueToKey(int(result)) - << " within " << elapsed.count() << "ms."; - }) - }; -} - -/*! - \fn Group ExecutableItem::operator!(const ExecutableItem &item) - - Returns a Group with the DoneResult of \a item negated. - - If \a item reports DoneResult::Success, the returned item reports DoneResult::Error. - If \a item reports DoneResult::Error, the returned item reports DoneResult::Success. - - The returned item is equivalent to: - \code - Group { - item, - onGroupDone([](DoneWith doneWith) { return toDoneResult(doneWith == DoneWith::Error); }) - } - \endcode - - \sa operator&&(), operator||() -*/ -Group operator!(const ExecutableItem &item) -{ - return { - item, - onGroupDone([](DoneWith doneWith) { return toDoneResult(doneWith == DoneWith::Error); }) - }; -} - -/*! - \fn Group ExecutableItem::operator&&(const ExecutableItem &first, const ExecutableItem &second) - - Returns a Group with \a first and \a second tasks merged with conjunction. - - Both \a first and \a second tasks execute in sequence. - If both tasks report DoneResult::Success, the returned item reports DoneResult::Success. - Otherwise, the returned item reports DoneResult::Error. - - The returned item is - \l {https://en.wikipedia.org/wiki/Short-circuit_evaluation}{short-circuiting}: - if the \a first task reports DoneResult::Error, the \a second task is skipped, - and the returned item reports DoneResult::Error immediately. - - The returned item is equivalent to: - \code - Group { stopOnError, first, second } - \endcode - - \note Parallel execution of conjunction in a short-circuit manner can be achieved with the - following code: \c {Group { parallel, stopOnError, first, second }}. In this case: - if the \e {first finished} task reports DoneResult::Error, - the \e other task is canceled, and the group reports DoneResult::Error immediately. - - \sa operator||(), operator!() -*/ -Group operator&&(const ExecutableItem &first, const ExecutableItem &second) -{ - return { stopOnError, first, second }; -} - -/*! - \fn Group ExecutableItem::operator||(const ExecutableItem &first, const ExecutableItem &second) - - Returns a Group with \a first and \a second tasks merged with disjunction. - - Both \a first and \a second tasks execute in sequence. - If both tasks report DoneResult::Error, the returned item reports DoneResult::Error. - Otherwise, the returned item reports DoneResult::Success. - - The returned item is - \l {https://en.wikipedia.org/wiki/Short-circuit_evaluation}{short-circuiting}: - if the \a first task reports DoneResult::Success, the \a second task is skipped, - and the returned item reports DoneResult::Success immediately. - - The returned item is equivalent to: - \code - Group { stopOnSuccess, first, second } - \endcode - - \note Parallel execution of disjunction in a short-circuit manner can be achieved with the - following code: \c {Group { parallel, stopOnSuccess, first, second }}. In this case: - if the \e {first finished} task reports DoneResult::Success, - the \e other task is canceled, and the group reports DoneResult::Success immediately. - - \sa operator&&(), operator!() -*/ -Group operator||(const ExecutableItem &first, const ExecutableItem &second) -{ - return { stopOnSuccess, first, second }; -} - -/*! - \fn Group ExecutableItem::operator&&(const ExecutableItem &item, DoneResult result) - \overload ExecutableItem::operator&&() - - Returns the \a item task if the \a result is DoneResult::Success; otherwise returns - the \a item task with its done result tweaked to DoneResult::Error. - - The \c {task && DoneResult::Error} is an eqivalent to tweaking the task's done result - into DoneResult::Error unconditionally. -*/ -Group operator&&(const ExecutableItem &item, DoneResult result) -{ - return { result == DoneResult::Success ? stopOnError : finishAllAndError, item }; -} - -/*! - \fn Group ExecutableItem::operator||(const ExecutableItem &item, DoneResult result) - \overload ExecutableItem::operator||() - - Returns the \a item task if the \a result is DoneResult::Error; otherwise returns - the \a item task with its done result tweaked to DoneResult::Success. - - The \c {task || DoneResult::Success} is an eqivalent to tweaking the task's done result - into DoneResult::Success unconditionally. -*/ -Group operator||(const ExecutableItem &item, DoneResult result) -{ - return { result == DoneResult::Error ? stopOnError : finishAllAndSuccess, item }; -} - -Group ExecutableItem::withCancelImpl( - const std::function<void(QObject *, const std::function<void()> &)> &connectWrapper, - const GroupItems &postCancelRecipe) const -{ - const Storage<bool> canceledStorage(false); - - const auto onSetup = [connectWrapper, canceledStorage](Barrier &barrier) { - connectWrapper(&barrier, [barrierPtr = &barrier, canceled = canceledStorage.activeStorage()] { - *canceled = true; - barrierPtr->advance(); - }); - }; - - const auto wasCanceled = [canceledStorage] { return *canceledStorage; }; - - return { - continueOnError, - canceledStorage, - Group { - parallel, - stopOnSuccessOrError, - BarrierTask(onSetup) && errorItem, - *this - }, - If (wasCanceled) >> Then { - postCancelRecipe - } - }; -} - -Group ExecutableItem::withAcceptImpl( - const std::function<void(QObject *, const std::function<void()> &)> &connectWrapper) const -{ - const auto onSetup = [connectWrapper](Barrier &barrier) { - connectWrapper(&barrier, [barrierPtr = &barrier] { barrierPtr->advance(); }); - }; - return Group { - parallel, - BarrierTask(onSetup), - *this - }; -} - -class TaskTreePrivate; -class TaskNode; -class RuntimeContainer; -class RuntimeIteration; -class RuntimeTask; - -class ExecutionContextActivator -{ -public: - ExecutionContextActivator(RuntimeIteration *iteration) { - activateTaskTree(iteration); - activateContext(iteration); - } - ExecutionContextActivator(RuntimeContainer *container) { - activateTaskTree(container); - activateContext(container); - } - ~ExecutionContextActivator() { - for (int i = m_activeStorages.size() - 1; i >= 0; --i) // iterate in reverse order - m_activeStorages[i].m_storageData->threadData().popStorage(); - for (int i = m_activeLoops.size() - 1; i >= 0; --i) // iterate in reverse order - m_activeLoops[i].m_loopData->threadData().popIteration(); - QT_ASSERT(s_activeTaskTrees.size(), return); - s_activeTaskTrees.pop_back(); - } - -private: - void activateTaskTree(RuntimeIteration *iteration); - void activateTaskTree(RuntimeContainer *container); - void activateContext(RuntimeIteration *iteration); - void activateContext(RuntimeContainer *container); - QList<Loop> m_activeLoops; - QList<StorageBase> m_activeStorages; -}; - -class ContainerNode -{ - Q_DISABLE_COPY(ContainerNode) - -public: - ContainerNode(ContainerNode &&other) = default; - ContainerNode(TaskTreePrivate *taskTreePrivate, const GroupItem &task); - - TaskTreePrivate *const m_taskTreePrivate = nullptr; - - const GroupItem::GroupHandler m_groupHandler; - const int m_parallelLimit = 1; - const WorkflowPolicy m_workflowPolicy = WorkflowPolicy::StopOnError; - const std::optional<Loop> m_loop; - const QList<StorageBase> m_storageList; - std::vector<TaskNode> m_children; - const int m_taskCount = 0; -}; - -class TaskNode -{ - Q_DISABLE_COPY(TaskNode) - -public: - TaskNode(TaskNode &&other) = default; - TaskNode(TaskTreePrivate *taskTreePrivate, const GroupItem &task) - : m_taskHandler(task.m_taskHandler) - , m_container(taskTreePrivate, task) - {} - - bool isTask() const { return bool(m_taskHandler.m_createHandler); } - int taskCount() const { return isTask() ? 1 : m_container.m_taskCount; } - - const GroupItem::TaskHandler m_taskHandler; - ContainerNode m_container; -}; - -class TaskTreePrivate -{ - Q_DISABLE_COPY_MOVE(TaskTreePrivate) - -public: - explicit TaskTreePrivate(TaskTree *taskTree); - ~TaskTreePrivate(); - - void start(); - void stop(); - void bumpAsyncCount(); - void advanceProgress(int byValue); - void emitDone(DoneWith result); - void callSetupHandler(const StorageBase &storage, StoragePtr storagePtr) { - callStorageHandler(storage, storagePtr, &StorageHandler::m_setupHandler); - } - void callDoneHandler(const StorageBase &storage, StoragePtr storagePtr) { - callStorageHandler(storage, storagePtr, &StorageHandler::m_doneHandler); - } - struct StorageHandler { - StorageBase::StorageHandler m_setupHandler = {}; - StorageBase::StorageHandler m_doneHandler = {}; - }; - typedef StorageBase::StorageHandler StorageHandler::*HandlerPtr; // ptr to class member - void callStorageHandler(const StorageBase &storage, StoragePtr storagePtr, HandlerPtr ptr) - { - const auto it = m_storageHandlers.constFind(storage); - if (it == m_storageHandlers.constEnd()) - return; - const StorageHandler storageHandler = *it; - if (storageHandler.*ptr) { - GuardLocker locker(m_guard); - (storageHandler.*ptr)(storagePtr); - } - } - - // Node related methods - - // If returned value != Continue, childDone() needs to be called in parent container (in caller) - // in order to unwind properly. - void startTask(const std::shared_ptr<RuntimeTask> &node); - void stopTask(RuntimeTask *node); - bool invokeTaskDoneHandler(RuntimeTask *node, DoneWith doneWith); - - // Container related methods - - void continueContainer(RuntimeContainer *container); - void startChildren(RuntimeContainer *container); - void childDone(RuntimeIteration *iteration, bool success); - void stopContainer(RuntimeContainer *container); - bool invokeDoneHandler(RuntimeContainer *container, DoneWith doneWith); - bool invokeLoopHandler(RuntimeContainer *container); - - template <typename Container, typename Handler, typename ...Args, - typename ReturnType = std::invoke_result_t<Handler, Args...>> - ReturnType invokeHandler(Container *container, Handler &&handler, Args &&...args) - { - QT_ASSERT(!m_guard.isLocked(), qWarning("Nested execution of handlers detected." - "This may happen when one task's handler has entered a nested event loop," - "and other task finished during nested event loop's processing, " - "causing stopping (canceling) the task executing the nested event loop. " - "This includes the case when QCoreApplication::processEvents() was called from " - "the handler. It may also happen when the Barrier task is advanced directly " - "from some other task handler. This will lead to a crash. " - "Avoid event processing during handlers' execution. " - "If it can't be avoided, make sure no other tasks are run in parallel when " - "processing events from the handler.")); - ExecutionContextActivator activator(container); - GuardLocker locker(m_guard); - return std::invoke(std::forward<Handler>(handler), std::forward<Args>(args)...); - } - - static int effectiveLoopCount(const std::optional<Loop> &loop) - { - return loop && loop->m_loopData->m_loopCount ? *loop->m_loopData->m_loopCount : 1; - } - - TaskTree *q = nullptr; - Guard m_guard; - int m_progressValue = 0; - int m_asyncCount = 0; - QSet<StorageBase> m_storages; - QHash<StorageBase, StorageHandler> m_storageHandlers; - std::optional<TaskNode> m_root; - std::shared_ptr<RuntimeTask> m_runtimeRoot; // Keep me last in order to destruct first -}; - -static bool initialSuccessBit(WorkflowPolicy workflowPolicy) -{ - switch (workflowPolicy) { - case WorkflowPolicy::StopOnError: - case WorkflowPolicy::ContinueOnError: - case WorkflowPolicy::FinishAllAndSuccess: - return true; - case WorkflowPolicy::StopOnSuccess: - case WorkflowPolicy::ContinueOnSuccess: - case WorkflowPolicy::StopOnSuccessOrError: - case WorkflowPolicy::FinishAllAndError: - return false; - } - QT_CHECK(false); - return false; -} - -static bool isProgressive(RuntimeContainer *container); - -class RuntimeIteration -{ - Q_DISABLE_COPY(RuntimeIteration) - -public: - RuntimeIteration(int index, RuntimeContainer *container); - ~RuntimeIteration(); - std::optional<Loop> loop() const; - void removeChild(RuntimeTask *node); - - const int m_iterationIndex = 0; - const bool m_isProgressive = true; - RuntimeContainer *m_container = nullptr; - int m_doneCount = 0; - std::vector<std::shared_ptr<RuntimeTask>> m_children = {}; // Owning. -}; - -class RuntimeContainer -{ - Q_DISABLE_COPY(RuntimeContainer) - -public: - RuntimeContainer(const ContainerNode &taskContainer, RuntimeTask *parentTask) - : m_containerNode(taskContainer) - , m_parentTask(parentTask) - , m_storages(createStorages(taskContainer)) - , m_successBit(initialSuccessBit(taskContainer.m_workflowPolicy)) - , m_shouldIterate(taskContainer.m_loop) - {} - - ~RuntimeContainer() - { - for (int i = m_containerNode.m_storageList.size() - 1; i >= 0; --i) { // iterate in reverse order - const StorageBase storage = m_containerNode.m_storageList[i]; - StoragePtr storagePtr = m_storages.value(i); - if (m_callStorageDoneHandlersOnDestruction) - m_containerNode.m_taskTreePrivate->callDoneHandler(storage, storagePtr); - storage.m_storageData->m_destructor(storagePtr); - } - } - - static QList<StoragePtr> createStorages(const ContainerNode &container); - bool isStarting() const { return m_startGuard.isLocked(); } - RuntimeIteration *parentIteration() const; - bool updateSuccessBit(bool success); - void deleteFinishedIterations(); - int progressiveLoopCount() const - { - return m_containerNode.m_taskTreePrivate->effectiveLoopCount(m_containerNode.m_loop); - } - - const ContainerNode &m_containerNode; // Not owning. - RuntimeTask *m_parentTask = nullptr; // Not owning. - const QList<StoragePtr> m_storages; // Owning. - - bool m_successBit = true; - bool m_callStorageDoneHandlersOnDestruction = false; - Guard m_startGuard; - - int m_iterationCount = 0; - int m_nextToStart = 0; - int m_runningChildren = 0; - bool m_shouldIterate = true; - std::vector<std::unique_ptr<RuntimeIteration>> m_iterations; // Owning. -}; - -class RuntimeTask -{ -public: - ~RuntimeTask() - { - if (m_task) { - // Ensures the running task's d'tor doesn't emit done() signal. QTCREATORBUG-30204. - QObject::disconnect(m_task.get(), &TaskInterface::done, nullptr, nullptr); - } - } - - const TaskNode &m_taskNode; // Not owning. - RuntimeIteration *m_parentIteration = nullptr; // Not owning. - std::optional<RuntimeContainer> m_container = {}; // Owning. - std::unique_ptr<TaskInterface> m_task = {}; // Owning. - SetupResult m_setupResult = SetupResult::Continue; -}; - -RuntimeIteration::~RuntimeIteration() = default; - -TaskTreePrivate::TaskTreePrivate(TaskTree *taskTree) - : q(taskTree) {} - -TaskTreePrivate::~TaskTreePrivate() = default; - -static bool isProgressive(RuntimeContainer *container) -{ - RuntimeIteration *iteration = container->m_parentTask->m_parentIteration; - return iteration ? iteration->m_isProgressive : true; -} - -void ExecutionContextActivator::activateTaskTree(RuntimeIteration *iteration) -{ - activateTaskTree(iteration->m_container); -} - -void ExecutionContextActivator::activateTaskTree(RuntimeContainer *container) -{ - s_activeTaskTrees.push_back(container->m_containerNode.m_taskTreePrivate->q); -} - -void ExecutionContextActivator::activateContext(RuntimeIteration *iteration) -{ - std::optional<Loop> loop = iteration->loop(); - if (loop) { - loop->m_loopData->threadData().pushIteration(iteration->m_iterationIndex); - m_activeLoops.append(*loop); - } - activateContext(iteration->m_container); -} - -void ExecutionContextActivator::activateContext(RuntimeContainer *container) -{ - const ContainerNode &containerNode = container->m_containerNode; - for (int i = 0; i < containerNode.m_storageList.size(); ++i) { - const StorageBase &storage = containerNode.m_storageList[i]; - if (m_activeStorages.contains(storage)) - continue; // Storage shadowing: The storage is already active, skipping it... - m_activeStorages.append(storage); - storage.m_storageData->threadData().pushStorage(container->m_storages.value(i)); - } - // Go to the parent after activating this storages so that storage shadowing works - // in the direction from child to parent root. - if (container->parentIteration()) - activateContext(container->parentIteration()); -} - -void TaskTreePrivate::start() -{ - QT_ASSERT(m_root, return); - QT_ASSERT(!m_runtimeRoot, return); - m_asyncCount = 0; - m_progressValue = 0; - { - GuardLocker locker(m_guard); - emit q->started(); - emit q->asyncCountChanged(m_asyncCount); - emit q->progressValueChanged(m_progressValue); - } - // TODO: check storage handlers for not existing storages in tree - for (auto it = m_storageHandlers.cbegin(); it != m_storageHandlers.cend(); ++it) { - QT_ASSERT(m_storages.contains(it.key()), qWarning("The registered storage doesn't " - "exist in task tree. Its handlers will never be called.")); - } - m_runtimeRoot.reset(new RuntimeTask{*m_root}); - startTask(m_runtimeRoot); - bumpAsyncCount(); -} - -void TaskTreePrivate::stop() -{ - QT_ASSERT(m_root, return); - if (!m_runtimeRoot) - return; - stopTask(m_runtimeRoot.get()); - m_runtimeRoot.reset(); - emitDone(DoneWith::Cancel); -} - -void TaskTreePrivate::bumpAsyncCount() -{ - if (!m_runtimeRoot) - return; - ++m_asyncCount; - GuardLocker locker(m_guard); - emit q->asyncCountChanged(m_asyncCount); -} - -void TaskTreePrivate::advanceProgress(int byValue) -{ - if (byValue == 0) - return; - QT_CHECK(byValue > 0); - QT_CHECK(m_progressValue + byValue <= m_root->taskCount()); - m_progressValue += byValue; - GuardLocker locker(m_guard); - emit q->progressValueChanged(m_progressValue); -} - -void TaskTreePrivate::emitDone(DoneWith result) -{ - QT_CHECK(m_progressValue == m_root->taskCount()); - GuardLocker locker(m_guard); - emit q->done(result); -} - -RuntimeIteration::RuntimeIteration(int index, RuntimeContainer *container) - : m_iterationIndex(index) - , m_isProgressive(index < container->progressiveLoopCount() && isProgressive(container)) - , m_container(container) -{} - -std::optional<Loop> RuntimeIteration::loop() const -{ - return m_container->m_containerNode.m_loop; -} - -void RuntimeIteration::removeChild(RuntimeTask *task) -{ - const auto it = std::find_if(m_children.cbegin(), m_children.cend(), [task](const auto &ptr) { - return ptr.get() == task; - }); - if (it != m_children.cend()) - m_children.erase(it); -} - -static std::vector<TaskNode> createChildren(TaskTreePrivate *taskTreePrivate, - const GroupItems &children) -{ - std::vector<TaskNode> result; - result.reserve(children.size()); - for (const GroupItem &child : children) - result.emplace_back(taskTreePrivate, child); - return result; -} - -ContainerNode::ContainerNode(TaskTreePrivate *taskTreePrivate, const GroupItem &task) - : m_taskTreePrivate(taskTreePrivate) - , m_groupHandler(task.m_groupData.m_groupHandler) - , m_parallelLimit(task.m_groupData.m_parallelLimit.value_or(1)) - , m_workflowPolicy(task.m_groupData.m_workflowPolicy.value_or(WorkflowPolicy::StopOnError)) - , m_loop(task.m_groupData.m_loop) - , m_storageList(task.m_storageList) - , m_children(createChildren(taskTreePrivate, task.m_children)) - , m_taskCount(std::accumulate(m_children.cbegin(), m_children.cend(), 0, - [](int r, const TaskNode &n) { return r + n.taskCount(); }) - * taskTreePrivate->effectiveLoopCount(m_loop)) -{ - for (const StorageBase &storage : m_storageList) - m_taskTreePrivate->m_storages << storage; -} - -QList<StoragePtr> RuntimeContainer::createStorages(const ContainerNode &container) -{ - QList<StoragePtr> storages; - for (const StorageBase &storage : container.m_storageList) { - StoragePtr storagePtr = storage.m_storageData->m_constructor(); - storages.append(storagePtr); - container.m_taskTreePrivate->callSetupHandler(storage, storagePtr); - } - return storages; -} - -RuntimeIteration *RuntimeContainer::parentIteration() const -{ - return m_parentTask->m_parentIteration; -} - -bool RuntimeContainer::updateSuccessBit(bool success) -{ - if (m_containerNode.m_workflowPolicy == WorkflowPolicy::FinishAllAndSuccess - || m_containerNode.m_workflowPolicy == WorkflowPolicy::FinishAllAndError - || m_containerNode.m_workflowPolicy == WorkflowPolicy::StopOnSuccessOrError) { - if (m_containerNode.m_workflowPolicy == WorkflowPolicy::StopOnSuccessOrError) - m_successBit = success; - return m_successBit; - } - - const bool donePolicy = m_containerNode.m_workflowPolicy == WorkflowPolicy::StopOnSuccess - || m_containerNode.m_workflowPolicy == WorkflowPolicy::ContinueOnSuccess; - m_successBit = donePolicy ? (m_successBit || success) : (m_successBit && success); - return m_successBit; -} - -void RuntimeContainer::deleteFinishedIterations() -{ - for (auto it = m_iterations.cbegin(); it != m_iterations.cend(); ) { - if (it->get()->m_doneCount == int(m_containerNode.m_children.size())) - it = m_iterations.erase(it); - else - ++it; - } -} - -void TaskTreePrivate::continueContainer(RuntimeContainer *container) -{ - RuntimeTask *parentTask = container->m_parentTask; - if (parentTask->m_setupResult == SetupResult::Continue) - startChildren(container); - if (parentTask->m_setupResult == SetupResult::Continue) - return; - - const bool bit = container->updateSuccessBit(parentTask->m_setupResult == SetupResult::StopWithSuccess); - RuntimeIteration *parentIteration = container->parentIteration(); - QT_CHECK(parentTask); - const bool result = invokeDoneHandler(container, bit ? DoneWith::Success : DoneWith::Error); - parentTask->m_setupResult = toSetupResult(result); - if (parentIteration) { - parentIteration->removeChild(parentTask); - if (!parentIteration->m_container->isStarting()) - childDone(parentIteration, result); - } else { - QT_CHECK(m_runtimeRoot.get() == parentTask); - m_runtimeRoot.reset(); - emitDone(result ? DoneWith::Success : DoneWith::Error); - } -} - -void TaskTreePrivate::startChildren(RuntimeContainer *container) -{ - const ContainerNode &containerNode = container->m_containerNode; - const int childCount = int(containerNode.m_children.size()); - - if (container->m_iterationCount == 0) { - if (container->m_shouldIterate && !invokeLoopHandler(container)) { - if (isProgressive(container)) - advanceProgress(containerNode.m_taskCount); - container->m_parentTask->m_setupResult = toSetupResult(container->m_successBit); - return; - } - container->m_iterations.emplace_back( - std::make_unique<RuntimeIteration>(container->m_iterationCount, container)); - ++container->m_iterationCount; - } - - GuardLocker locker(container->m_startGuard); - - while (containerNode.m_parallelLimit == 0 - || container->m_runningChildren < containerNode.m_parallelLimit) { - container->deleteFinishedIterations(); - if (container->m_nextToStart == childCount) { - if (invokeLoopHandler(container)) { - container->m_nextToStart = 0; - container->m_iterations.emplace_back( - std::make_unique<RuntimeIteration>(container->m_iterationCount, container)); - ++container->m_iterationCount; - } else if (container->m_iterations.empty()) { - container->m_parentTask->m_setupResult = toSetupResult(container->m_successBit); - return; - } else { - return; - } - } - if (containerNode.m_children.size() == 0) // Empty loop body. - continue; - - RuntimeIteration *iteration = container->m_iterations.back().get(); - const std::shared_ptr<RuntimeTask> task( - new RuntimeTask{containerNode.m_children.at(container->m_nextToStart), iteration}); - iteration->m_children.emplace_back(task); - ++container->m_runningChildren; - ++container->m_nextToStart; - - startTask(task); - if (task->m_setupResult == SetupResult::Continue) - continue; - - task->m_parentIteration->removeChild(task.get()); - childDone(iteration, task->m_setupResult == SetupResult::StopWithSuccess); - if (container->m_parentTask->m_setupResult != SetupResult::Continue) - return; - } -} - -void TaskTreePrivate::childDone(RuntimeIteration *iteration, bool success) -{ - RuntimeContainer *container = iteration->m_container; - const WorkflowPolicy &workflowPolicy = container->m_containerNode.m_workflowPolicy; - const bool shouldStop = workflowPolicy == WorkflowPolicy::StopOnSuccessOrError - || (workflowPolicy == WorkflowPolicy::StopOnSuccess && success) - || (workflowPolicy == WorkflowPolicy::StopOnError && !success); - ++iteration->m_doneCount; - --container->m_runningChildren; - const bool updatedSuccess = container->updateSuccessBit(success); - container->m_parentTask->m_setupResult = shouldStop ? toSetupResult(updatedSuccess) : SetupResult::Continue; - if (shouldStop) - stopContainer(container); - - if (container->isStarting()) - return; - continueContainer(container); -} - -void TaskTreePrivate::stopContainer(RuntimeContainer *container) -{ - const ContainerNode &containerNode = container->m_containerNode; - for (auto &iteration : container->m_iterations) { - for (auto &child : iteration->m_children) { - ++iteration->m_doneCount; - stopTask(child.get()); - } - - if (iteration->m_isProgressive) { - int skippedTaskCount = 0; - for (int i = iteration->m_doneCount; i < int(containerNode.m_children.size()); ++i) - skippedTaskCount += containerNode.m_children.at(i).taskCount(); - advanceProgress(skippedTaskCount); - } - } - const int skippedIterations = container->progressiveLoopCount() - container->m_iterationCount; - if (skippedIterations > 0) { - advanceProgress(container->m_containerNode.m_taskCount / container->progressiveLoopCount() - * skippedIterations); - } -} - -static bool shouldCall(CallDoneIf callDoneIf, DoneWith result) -{ - if (result == DoneWith::Success) - return callDoneIf != CallDoneIf::Error; - return callDoneIf != CallDoneIf::Success; -} - -bool TaskTreePrivate::invokeDoneHandler(RuntimeContainer *container, DoneWith doneWith) -{ - DoneResult result = toDoneResult(doneWith); - const GroupItem::GroupHandler &groupHandler = container->m_containerNode.m_groupHandler; - if (groupHandler.m_doneHandler && shouldCall(groupHandler.m_callDoneIf, doneWith)) - result = invokeHandler(container, groupHandler.m_doneHandler, doneWith); - container->m_callStorageDoneHandlersOnDestruction = true; - return result == DoneResult::Success; -} - -bool TaskTreePrivate::invokeLoopHandler(RuntimeContainer *container) -{ - if (container->m_shouldIterate) { - const LoopData *loopData = container->m_containerNode.m_loop->m_loopData.get(); - if (loopData->m_loopCount) { - container->m_shouldIterate = container->m_iterationCount < loopData->m_loopCount; - } else if (loopData->m_condition) { - container->m_shouldIterate = invokeHandler(container, loopData->m_condition, - container->m_iterationCount); - } - } - return container->m_shouldIterate; -} - -void TaskTreePrivate::startTask(const std::shared_ptr<RuntimeTask> &node) -{ - if (!node->m_taskNode.isTask()) { - const ContainerNode &containerNode = node->m_taskNode.m_container; - node->m_container.emplace(containerNode, node.get()); - RuntimeContainer *container = &*node->m_container; - if (containerNode.m_groupHandler.m_setupHandler) { - container->m_parentTask->m_setupResult = invokeHandler(container, containerNode.m_groupHandler.m_setupHandler); - if (container->m_parentTask->m_setupResult != SetupResult::Continue) { - if (isProgressive(container)) - advanceProgress(containerNode.m_taskCount); - // Non-Continue SetupResult takes precedence over the workflow policy. - container->m_successBit = container->m_parentTask->m_setupResult == SetupResult::StopWithSuccess; - } - } - continueContainer(container); - return; - } - - const GroupItem::TaskHandler &handler = node->m_taskNode.m_taskHandler; - node->m_task.reset(handler.m_createHandler()); - node->m_setupResult = handler.m_setupHandler - ? invokeHandler(node->m_parentIteration, handler.m_setupHandler, *node->m_task.get()) - : SetupResult::Continue; - if (node->m_setupResult != SetupResult::Continue) { - if (node->m_parentIteration->m_isProgressive) - advanceProgress(1); - node->m_parentIteration->removeChild(node.get()); - return; - } - QObject::connect(node->m_task.get(), &TaskInterface::done, - q, [this, node](DoneResult doneResult) { - const bool result = invokeTaskDoneHandler(node.get(), toDoneWith(doneResult)); - node->m_setupResult = toSetupResult(result); - QObject::disconnect(node->m_task.get(), &TaskInterface::done, q, nullptr); - node->m_task.release()->deleteLater(); - RuntimeIteration *parentIteration = node->m_parentIteration; - if (parentIteration->m_container->isStarting()) - return; - - parentIteration->removeChild(node.get()); - childDone(parentIteration, result); - bumpAsyncCount(); - }); - node->m_task->start(); -} - -void TaskTreePrivate::stopTask(RuntimeTask *node) -{ - if (!node->m_task) { - if (!node->m_container) - return; - stopContainer(&*node->m_container); - node->m_container->updateSuccessBit(false); - invokeDoneHandler(&*node->m_container, DoneWith::Cancel); - return; - } - - invokeTaskDoneHandler(node, DoneWith::Cancel); - node->m_task.reset(); -} - -bool TaskTreePrivate::invokeTaskDoneHandler(RuntimeTask *node, DoneWith doneWith) -{ - DoneResult result = toDoneResult(doneWith); - const GroupItem::TaskHandler &handler = node->m_taskNode.m_taskHandler; - if (handler.m_doneHandler && shouldCall(handler.m_callDoneIf, doneWith)) { - result = invokeHandler(node->m_parentIteration, - handler.m_doneHandler, *node->m_task.get(), doneWith); - } - if (node->m_parentIteration->m_isProgressive) - advanceProgress(1); - return result == DoneResult::Success; -} - -/*! - \class Tasking::TaskTree - \inheaderfile solutions/tasking/tasktree.h - \inmodule TaskingSolution - \brief The TaskTree class runs an async task tree structure defined in a declarative way. - \reentrant - - Use the Tasking namespace to build extensible, declarative task tree - structures that contain possibly asynchronous tasks, such as QProcess, - NetworkQuery, or ConcurrentCall<ReturnType>. TaskTree structures enable you - to create a sophisticated mixture of a parallel or sequential flow of tasks - in the form of a tree and to run it any time later. - - \section1 Root Element and Tasks - - The TaskTree has a mandatory Group root element, which may contain - any number of tasks of various types, such as QProcessTask, NetworkQueryTask, - or ConcurrentCallTask<ReturnType>: - - \code - using namespace Tasking; - - const Group root { - QProcessTask(...), - NetworkQueryTask(...), - ConcurrentCallTask<int>(...) - }; - - TaskTree *taskTree = new TaskTree(root); - connect(taskTree, &TaskTree::done, ...); // finish handler - taskTree->start(); - \endcode - - The task tree above has a top level element of the Group type that contains - tasks of the QProcessTask, NetworkQueryTask, and ConcurrentCallTask<int> type. - After taskTree->start() is called, the tasks are run in a chain, starting - with QProcessTask. When the QProcessTask finishes successfully, the NetworkQueryTask - task is started. Finally, when the network task finishes successfully, the - ConcurrentCallTask<int> task is started. - - When the last running task finishes with success, the task tree is considered - to have run successfully and the done() signal is emitted with DoneWith::Success. - When a task finishes with an error, the execution of the task tree is stopped - and the remaining tasks are skipped. The task tree finishes with an error and - sends the TaskTree::done() signal with DoneWith::Error. - - \section1 Groups - - The parent of the Group sees it as a single task. Like other tasks, - the group can be started and it can finish with success or an error. - The Group elements can be nested to create a tree structure: - - \code - const Group root { - Group { - parallel, - QProcessTask(...), - ConcurrentCallTask<int>(...) - }, - NetworkQueryTask(...) - }; - \endcode - - The example above differs from the first example in that the root element has - a subgroup that contains the QProcessTask and ConcurrentCallTask<int>. The subgroup is a - sibling element of the NetworkQueryTask in the root. The subgroup contains an - additional \e parallel element that instructs its Group to execute its tasks - in parallel. - - So, when the tree above is started, the QProcessTask and ConcurrentCallTask<int> start - immediately and run in parallel. Since the root group doesn't contain a - \e parallel element, its direct child tasks are run in sequence. Thus, the - NetworkQueryTask starts when the whole subgroup finishes. The group is - considered as finished when all its tasks have finished. The order in which - the tasks finish is not relevant. - - So, depending on which task lasts longer (QProcessTask or ConcurrentCallTask<int>), the - following scenarios can take place: - - \table - \header - \li Scenario 1 - \li Scenario 2 - \row - \li Root Group starts - \li Root Group starts - \row - \li Sub Group starts - \li Sub Group starts - \row - \li QProcessTask starts - \li QProcessTask starts - \row - \li ConcurrentCallTask<int> starts - \li ConcurrentCallTask<int> starts - \row - \li ... - \li ... - \row - \li \b {QProcessTask finishes} - \li \b {ConcurrentCallTask<int> finishes} - \row - \li ... - \li ... - \row - \li \b {ConcurrentCallTask<int> finishes} - \li \b {QProcessTask finishes} - \row - \li Sub Group finishes - \li Sub Group finishes - \row - \li NetworkQueryTask starts - \li NetworkQueryTask starts - \row - \li ... - \li ... - \row - \li NetworkQueryTask finishes - \li NetworkQueryTask finishes - \row - \li Root Group finishes - \li Root Group finishes - \endtable - - The differences between the scenarios are marked with bold. Three dots mean - that an unspecified amount of time passes between previous and next events - (a task or tasks continue to run). No dots between events - means that they occur synchronously. - - The presented scenarios assume that all tasks run successfully. If a task - fails during execution, the task tree finishes with an error. In particular, - when QProcessTask finishes with an error while ConcurrentCallTask<int> is still being executed, - the ConcurrentCallTask<int> is automatically canceled, the subgroup finishes with an error, - the NetworkQueryTask is skipped, and the tree finishes with an error. - - \section1 Task Types - - Each task type is associated with its corresponding task class that executes - the task. For example, a QProcessTask inside a task tree is associated with - the QProcess class that executes the process. The associated objects are - automatically created, started, and destructed exclusively by the task tree - at the appropriate time. - - If a root group consists of five sequential QProcessTask tasks, and the task tree - executes the group, it creates an instance of QProcess for the first - QProcessTask and starts it. If the QProcess instance finishes successfully, - the task tree destructs it and creates a new QProcess instance for the - second QProcessTask, and so on. If the first task finishes with an error, the task - tree stops creating QProcess instances, and the root group finishes with an - error. - - The following table shows examples of task types and their corresponding task - classes: - - \table - \header - \li Task Type (Tasking Namespace) - \li Associated Task Class - \li Brief Description - \row - \li QProcessTask - \li QProcess - \li Starts process. - \row - \li ConcurrentCallTask<ReturnType> - \li Tasking::ConcurrentCall<ReturnType> - \li Starts asynchronous task, runs in separate thread. - \row - \li TaskTreeTask - \li Tasking::TaskTree - \li Starts nested task tree. - \row - \li NetworkQueryTask - \li NetworkQuery - \li Starts network download. - \endtable - - \section1 Task Handlers - - Use Task handlers to set up a task for execution and to enable reading - the output data from the task when it finishes with success or an error. - - \section2 Task's Start Handler - - When a corresponding task class object is created and before it's started, - the task tree invokes an optionally user-provided setup handler. The setup - handler should always take a \e reference to the associated task class object: - - \code - const auto onSetup = [](QProcess &process) { - process.setProgram("sleep"); - process.setArguments({"3"}); - }; - const Group root { - QProcessTask(onSetup) - }; - \endcode - - You can modify the passed QProcess in the setup handler, so that the task - tree can start the process according to your configuration. - You should not call \c {process.start();} in the setup handler, - as the task tree calls it when needed. The setup handler is optional. When used, - it must be the first argument of the task's constructor. - - Optionally, the setup handler may return a SetupResult. The returned - SetupResult influences the further start behavior of a given task. The - possible values are: - - \table - \header - \li SetupResult Value - \li Brief Description - \row - \li Continue - \li The task will be started normally. This is the default behavior when the - setup handler doesn't return SetupResult (that is, its return type is - void). - \row - \li StopWithSuccess - \li The task won't be started and it will report success to its parent. - \row - \li StopWithError - \li The task won't be started and it will report an error to its parent. - \endtable - - This is useful for running a task only when a condition is met and the data - needed to evaluate this condition is not known until previously started tasks - finish. In this way, the setup handler dynamically decides whether to start the - corresponding task normally or skip it and report success or an error. - For more information about inter-task data exchange, see \l Storage. - - \section2 Task's Done Handler - - When a running task finishes, the task tree invokes an optionally provided done handler. - The handler should take a \c const \e reference to the associated task class object: - - \code - const auto onSetup = [](QProcess &process) { - process.setProgram("sleep"); - process.setArguments({"3"}); - }; - const auto onDone = [](const QProcess &process, DoneWith result) { - if (result == DoneWith::Success) - qDebug() << "Success" << process.cleanedStdOut(); - else - qDebug() << "Failure" << process.cleanedStdErr(); - }; - const Group root { - QProcessTask(onSetup, onDone) - }; - \endcode - - The done handler may collect output data from QProcess, and store it - for further processing or perform additional actions. - - \note If the task setup handler returns StopWithSuccess or StopWithError, - the done handler is not invoked. - - \section1 Group Handlers - - Similarly to task handlers, group handlers enable you to set up a group to - execute and to apply more actions when the whole group finishes with - success or an error. - - \section2 Group's Start Handler - - The task tree invokes the group start handler before it starts the child - tasks. The group handler doesn't take any arguments: - - \code - const auto onSetup = [] { - qDebug() << "Entering the group"; - }; - const Group root { - onGroupSetup(onSetup), - QProcessTask(...) - }; - \endcode - - The group setup handler is optional. To define a group setup handler, add an - onGroupSetup() element to a group. The argument of onGroupSetup() is a user - handler. If you add more than one onGroupSetup() element to a group, an assert - is triggered at runtime that includes an error message. - - Like the task's start handler, the group start handler may return SetupResult. - The returned SetupResult value affects the start behavior of the - whole group. If you do not specify a group start handler or its return type - is void, the default group's action is SetupResult::Continue, so that all - tasks are started normally. Otherwise, when the start handler returns - SetupResult::StopWithSuccess or SetupResult::StopWithError, the tasks are not - started (they are skipped) and the group itself reports success or failure, - depending on the returned value, respectively. - - \code - const Group root { - onGroupSetup([] { qDebug() << "Root setup"; }), - Group { - onGroupSetup([] { qDebug() << "Group 1 setup"; return SetupResult::Continue; }), - QProcessTask(...) // Process 1 - }, - Group { - onGroupSetup([] { qDebug() << "Group 2 setup"; return SetupResult::StopWithSuccess; }), - QProcessTask(...) // Process 2 - }, - Group { - onGroupSetup([] { qDebug() << "Group 3 setup"; return SetupResult::StopWithError; }), - QProcessTask(...) // Process 3 - }, - QProcessTask(...) // Process 4 - }; - \endcode - - In the above example, all subgroups of a root group define their setup handlers. - The following scenario assumes that all started processes finish with success: - - \table - \header - \li Scenario - \li Comment - \row - \li Root Group starts - \li Doesn't return SetupResult, so its tasks are executed. - \row - \li Group 1 starts - \li Returns Continue, so its tasks are executed. - \row - \li Process 1 starts - \li - \row - \li ... - \li ... - \row - \li Process 1 finishes (success) - \li - \row - \li Group 1 finishes (success) - \li - \row - \li Group 2 starts - \li Returns StopWithSuccess, so Process 2 is skipped and Group 2 reports - success. - \row - \li Group 2 finishes (success) - \li - \row - \li Group 3 starts - \li Returns StopWithError, so Process 3 is skipped and Group 3 reports - an error. - \row - \li Group 3 finishes (error) - \li - \row - \li Root Group finishes (error) - \li Group 3, which is a direct child of the root group, finished with an - error, so the root group stops executing, skips Process 4, which has - not started yet, and reports an error. - \endtable - - \section2 Groups's Done Handler - - A Group's done handler is executed after the successful or failed execution of its tasks. - The final value reported by the group depends on its \l {Workflow Policy}. - The handler can apply other necessary actions. - The done handler is defined inside the onGroupDone() element of a group. - It may take the optional DoneWith argument, indicating the successful or failed execution: - - \code - const Group root { - onGroupSetup([] { qDebug() << "Root setup"; }), - QProcessTask(...), - onGroupDone([](DoneWith result) { - if (result == DoneWith::Success) - qDebug() << "Root finished with success"; - else - qDebug() << "Root finished with an error"; - }) - }; - \endcode - - The group done handler is optional. If you add more than one onGroupDone() to a group, - an assert is triggered at runtime that includes an error message. - - \note Even if the group setup handler returns StopWithSuccess or StopWithError, - the group's done handler is invoked. This behavior differs from that of task done handler - and might change in the future. - - \section1 Other Group Elements - - A group can contain other elements that describe the processing flow, such as - the execution mode or workflow policy. It can also contain storage elements - that are responsible for collecting and sharing custom common data gathered - during group execution. - - \section2 Execution Mode - - The execution mode element in a Group specifies how the direct child tasks of - the Group are started. The most common execution modes are \l sequential and - \l parallel. It's also possible to specify the limit of tasks running - in parallel by using the parallelLimit() function. - - In all execution modes, a group starts tasks in the oder in which they appear. - - If a child of a group is also a group, the child group runs its tasks - according to its own execution mode. - - \section2 Workflow Policy - - The workflow policy element in a Group specifies how the group should behave - when any of its \e direct child's tasks finish. For a detailed description of possible - policies, refer to WorkflowPolicy. - - If a child of a group is also a group, the child group runs its tasks - according to its own workflow policy. - - \section2 Storage - - Use the \l {Tasking::Storage} {Storage} element to exchange information between tasks. - Especially, in the sequential execution mode, when a task needs data from another, - already finished task, before it can start. For example, a task tree that copies data by reading - it from a source and writing it to a destination might look as follows: - - \code - static QByteArray load(const QString &fileName) { ... } - static void save(const QString &fileName, const QByteArray &array) { ... } - - static Group copyRecipe(const QString &source, const QString &destination) - { - struct CopyStorage { // [1] custom inter-task struct - QByteArray content; // [2] custom inter-task data - }; - - // [3] instance of custom inter-task struct manageable by task tree - const Storage<CopyStorage> storage; - - const auto onLoaderSetup = [source](ConcurrentCall<QByteArray> &async) { - async.setConcurrentCallData(&load, source); - }; - // [4] runtime: task tree activates the instance from [7] before invoking handler - const auto onLoaderDone = [storage](const ConcurrentCall<QByteArray> &async) { - storage->content = async.result(); // [5] loader stores the result in storage - }; - - // [4] runtime: task tree activates the instance from [7] before invoking handler - const auto onSaverSetup = [storage, destination](ConcurrentCall<void> &async) { - const QByteArray content = storage->content; // [6] saver takes data from storage - async.setConcurrentCallData(&save, destination, content); - }; - const auto onSaverDone = [](const ConcurrentCall<void> &async) { - qDebug() << "Save done successfully"; - }; - - const Group root { - // [7] runtime: task tree creates an instance of CopyStorage when root is entered - storage, - ConcurrentCallTask<QByteArray>(onLoaderSetup, onLoaderDone, CallDoneIf::Success), - ConcurrentCallTask<void>(onSaverSetup, onSaverDone, CallDoneIf::Success) - }; - return root; - } - - const QString source = ...; - const QString destination = ...; - TaskTree taskTree(copyRecipe(source, destination)); - connect(&taskTree, &TaskTree::done, - &taskTree, [](DoneWith result) { - if (result == DoneWith::Success) - qDebug() << "The copying finished successfully."; - }); - tasktree.start(); - \endcode - - In the example above, the inter-task data consists of a QByteArray content - variable [2] enclosed in a \c CopyStorage custom struct [1]. If the loader - finishes successfully, it stores the data in a \c CopyStorage::content - variable [5]. The saver then uses the variable to configure the saving task [6]. - - To enable a task tree to manage the \c CopyStorage struct, an instance of - \l {Tasking::Storage} {Storage}<\c CopyStorage> is created [3]. If a copy of this object is - inserted as the group's child item [7], an instance of the \c CopyStorage struct is - created dynamically when the task tree enters this group. When the task - tree leaves this group, the existing instance of the \c CopyStorage struct is - destructed as it's no longer needed. - - If several task trees holding a copy of the common - \l {Tasking::Storage} {Storage}<\c CopyStorage> instance run simultaneously - (including the case when the task trees are run in different threads), - each task tree contains its own copy of the \c CopyStorage struct. - - You can access \c CopyStorage from any handler in the group with a storage object. - This includes all handlers of all descendant tasks of the group with - a storage object. To access the custom struct in a handler, pass the - copy of the \l {Tasking::Storage} {Storage}<\c CopyStorage> object to the handler - (for example, in a lambda capture) [4]. - - When the task tree invokes a handler in a subtree containing the storage [7], - the task tree activates its own \c CopyStorage instance inside the - \l {Tasking::Storage} {Storage}<\c CopyStorage> object. Therefore, the \c CopyStorage struct - may be accessed only from within the handler body. To access the currently active - \c CopyStorage from within \l {Tasking::Storage} {Storage}<\c CopyStorage>, use the - \l {Tasking::Storage::operator->()} {Storage::operator->()}, - \l {Tasking::Storage::operator*()} {Storage::operator*()}, or Storage::activeStorage() method. - - The following list summarizes how to employ a Storage object into the task - tree: - \list 1 - \li Define the custom structure \c MyStorage with custom data [1], [2] - \li Create an instance of the \l {Tasking::Storage} {Storage}<\c MyStorage> storage [3] - \li Pass the \l {Tasking::Storage} {Storage}<\c MyStorage> instance to handlers [4] - \li Access the \c MyStorage instance in handlers [5], [6] - \li Insert the \l {Tasking::Storage} {Storage}<\c MyStorage> instance into a group [7] - \endlist - - \section1 TaskTree class - - TaskTree executes the tree structure of asynchronous tasks according to the - recipe described by the Group root element. - - As TaskTree is also an asynchronous task, it can be a part of another TaskTree. - To place a nested TaskTree inside another TaskTree, insert the TaskTreeTask - element into another Group element. - - TaskTree reports progress of completed tasks when running. The progress value - is increased when a task finishes or is skipped or canceled. - When TaskTree is finished and the TaskTree::done() signal is emitted, - the current value of the progress equals the maximum progress value. - Maximum progress equals the total number of asynchronous tasks in a tree. - A nested TaskTree is counted as a single task, and its child tasks are not - counted in the top level tree. Groups themselves are not counted as tasks, - but their tasks are counted. \l {Tasking::Sync} {Sync} tasks are not asynchronous, - so they are not counted as tasks. - - To set additional initial data for the running tree, modify the storage - instances in a tree when it creates them by installing a storage setup - handler: - - \code - Storage<CopyStorage> storage; - const Group root = ...; // storage placed inside root's group and inside handlers - TaskTree taskTree(root); - auto initStorage = [](CopyStorage &storage) { - storage.content = "initial content"; - }; - taskTree.onStorageSetup(storage, initStorage); - taskTree.start(); - \endcode - - When the running task tree creates a \c CopyStorage instance, and before any - handler inside a tree is called, the task tree calls the initStorage handler, - to enable setting up initial data of the storage, unique to this particular - run of taskTree. - - Similarly, to collect some additional result data from the running tree, - read it from storage instances in the tree when they are about to be - destroyed. To do this, install a storage done handler: - - \code - Storage<CopyStorage> storage; - const Group root = ...; // storage placed inside root's group and inside handlers - TaskTree taskTree(root); - auto collectStorage = [](const CopyStorage &storage) { - qDebug() << "final content" << storage.content; - }; - taskTree.onStorageDone(storage, collectStorage); - taskTree.start(); - \endcode - - When the running task tree is about to destroy a \c CopyStorage instance, the - task tree calls the collectStorage handler, to enable reading the final data - from the storage, unique to this particular run of taskTree. - - \section1 Task Adapters - - To extend a TaskTree with a new task type, implement a simple adapter class - derived from the TaskAdapter class template. The following class is an - adapter for a single shot timer, which may be considered as a new asynchronous task: - - \code - class TimerTaskAdapter : public TaskAdapter<QTimer> - { - public: - TimerTaskAdapter() { - task()->setSingleShot(true); - task()->setInterval(1000); - connect(task(), &QTimer::timeout, this, [this] { emit done(DoneResult::Success); }); - } - private: - void start() final { task()->start(); } - }; - - using TimerTask = CustomTask<TimerTaskAdapter>; - \endcode - - You must derive the custom adapter from the TaskAdapter class template - instantiated with a template parameter of the class implementing a running - task. The code above uses QTimer to run the task. This class appears - later as an argument to the task's handlers. The instance of this class - parameter automatically becomes a member of the TaskAdapter template, and is - accessible through the TaskAdapter::task() method. The constructor - of \c TimerTaskAdapter initially configures the QTimer object and connects - to the QTimer::timeout() signal. When the signal is triggered, \c TimerTaskAdapter - emits the TaskInterface::done(DoneResult::Success) signal to inform the task tree that - the task finished successfully. If it emits TaskInterface::done(DoneResult::Error), - the task finished with an error. - The TaskAdapter::start() method starts the timer. - - To make QTimer accessible inside TaskTree under the \c TimerTask name, - define \c TimerTask to be an alias to the CustomTask<\c TimerTaskAdapter>. - \c TimerTask becomes a new custom task type, using \c TimerTaskAdapter. - - The new task type is now registered, and you can use it in TaskTree: - - \code - const auto onSetup = [](QTimer &task) { task.setInterval(2000); }; - const auto onDone = [] { qDebug() << "timer triggered"; }; - const Group root { - TimerTask(onSetup, onDone) - }; - \endcode - - When a task tree containing the root from the above example is started, it - prints a debug message within two seconds and then finishes successfully. - - \note The class implementing the running task should have a default constructor, - and objects of this class should be freely destructible. It should be allowed - to destroy a running object, preferably without waiting for the running task - to finish (that is, safe non-blocking destructor of a running task). - To achieve a non-blocking destruction of a task that has a blocking destructor, - consider using the optional \c Deleter template parameter of the TaskAdapter. -*/ - -/*! - Constructs an empty task tree. Use setRecipe() to pass a declarative description - on how the task tree should execute the tasks and how it should handle the finished tasks. - - Starting an empty task tree is no-op and the relevant warning message is issued. - - \sa setRecipe(), start() -*/ -TaskTree::TaskTree() - : d(new TaskTreePrivate(this)) -{} - -/*! - \overload - - Constructs a task tree with a given \a recipe. After the task tree is started, - it executes the tasks contained inside the \a recipe and - handles finished tasks according to the passed description. - - \sa setRecipe(), start() -*/ -TaskTree::TaskTree(const Group &recipe) : TaskTree() -{ - setRecipe(recipe); -} - -/*! - Destroys the task tree. - - When the task tree is running while being destructed, it cancels all the running tasks - immediately. In this case, no handlers are called, not even the groups' and - tasks' done handlers or onStorageDone() handlers. The task tree also doesn't emit any - signals from the destructor, not even done() or progressValueChanged() signals. - This behavior may always be relied on. - It is completely safe to destruct the running task tree. - - It's a usual pattern to destruct the running task tree. - It's guaranteed that the destruction will run quickly, without having to wait for - the currently running tasks to finish, provided that the used tasks implement - their destructors in a non-blocking way. - - \note Do not call the destructor directly from any of the running task's handlers - or task tree's signals. In these cases, use \l deleteLater() instead. - - \sa cancel() -*/ -TaskTree::~TaskTree() -{ - QT_ASSERT(!d->m_guard.isLocked(), qWarning("Deleting TaskTree instance directly from " - "one of its handlers will lead to a crash!")); - // TODO: delete storages explicitly here? - delete d; -} - -/*! - Sets a given \a recipe for the task tree. After the task tree is started, - it executes the tasks contained inside the \a recipe and - handles finished tasks according to the passed description. - - \note When called for a running task tree, the call is ignored. - - \sa TaskTree(const Tasking::Group &recipe), start() -*/ -void TaskTree::setRecipe(const Group &recipe) -{ - QT_ASSERT(!isRunning(), qWarning("The TaskTree is already running, ignoring..."); return); - QT_ASSERT(!d->m_guard.isLocked(), qWarning("The setRecipe() is called from one of the" - "TaskTree handlers, ignoring..."); return); - // TODO: Should we clear the m_storageHandlers, too? - d->m_storages.clear(); - d->m_root.emplace(d, recipe); -} - -/*! - Starts the task tree. - - Use setRecipe() or the constructor to set the declarative description according to which - the task tree will execute the contained tasks and handle finished tasks. - - When the task tree is empty, that is, constructed with a default constructor, - a call to \c start() is no-op and the relevant warning message is issued. - - Otherwise, when the task tree is already running, a call to \e start() is ignored and the - relevant warning message is issued. - - Otherwise, the task tree is started. - - The started task tree may finish synchronously, - for example when the main group's start handler returns SetupResult::StopWithError. - For this reason, the connection to the done signal should be established before calling - \c start(). Use isRunning() in order to detect whether the task tree is still running - after a call to \c start(). - - The task tree implementation relies on the running event loop. - Make sure you have a QEventLoop or QCoreApplication or one of its - subclasses running (or about to be run) when calling this method. - - \sa TaskTree(const Tasking::Group &), setRecipe(), isRunning(), cancel() -*/ -void TaskTree::start() -{ - QT_ASSERT(!isRunning(), qWarning("The TaskTree is already running, ignoring..."); return); - QT_ASSERT(!d->m_guard.isLocked(), qWarning("The start() is called from one of the" - "TaskTree handlers, ignoring..."); return); - d->start(); -} - -/*! - \fn void TaskTree::started() - - This signal is emitted when the task tree is started. The emission of this signal is - followed synchronously by the progressValueChanged() signal with an initial \c 0 value. - - \sa start(), done() -*/ - -/*! - \fn void TaskTree::done(DoneWith result) - - This signal is emitted when the task tree finished, passing the final \a result - of the execution. The task tree neither calls any handler, - nor emits any signal anymore after this signal was emitted. - - \note Do not delete the task tree directly from this signal's handler. - Use deleteLater() instead. - - \sa started() -*/ - -/*! - Cancels the execution of the running task tree. - - Cancels all the running tasks immediately. - All running tasks finish with an error, invoking their error handlers. - All running groups dispatch their handlers according to their workflow policies, - invoking their done handlers. The storages' onStorageDone() handlers are invoked, too. - The progressValueChanged() signals are also being sent. - This behavior may always be relied on. - - The \c cancel() function is executed synchronously, so that after a call to \c cancel() - all running tasks are finished and the tree is already canceled. - It's guaranteed that \c cancel() will run quickly, without any blocking wait for - the currently running tasks to finish, provided the used tasks implement their destructors - in a non-blocking way. - - When the task tree is empty, that is, constructed with a default constructor, - a call to \c cancel() is no-op and the relevant warning message is issued. - - Otherwise, when the task tree wasn't started, a call to \c cancel() is ignored. - - \note Do not call this function directly from any of the running task's handlers - or task tree's signals. - - \sa ~TaskTree() -*/ -void TaskTree::cancel() -{ - QT_ASSERT(!d->m_guard.isLocked(), qWarning("The cancel() is called from one of the" - "TaskTree handlers, ignoring..."); return); - d->stop(); -} - -/*! - Returns \c true if the task tree is currently running; otherwise returns \c false. - - \sa start(), cancel() -*/ -bool TaskTree::isRunning() const -{ - return bool(d->m_runtimeRoot); -} - -/*! - Executes a local event loop with QEventLoop::ExcludeUserInputEvents and starts the task tree. - - Returns DoneWith::Success if the task tree finished successfully; - otherwise returns DoneWith::Error. - - \note Avoid using this method from the main thread. Use asynchronous start() instead. - This method is to be used in non-main threads or in auto tests. - - \sa start() -*/ -DoneWith TaskTree::runBlocking() -{ - QPromise<void> dummy; - dummy.start(); - return runBlocking(dummy.future()); -} - -/*! - \overload runBlocking() - - The passed \a future is used for listening to the cancel event. - When the task tree is canceled, this method cancels the passed \a future. -*/ -DoneWith TaskTree::runBlocking(const QFuture<void> &future) -{ - if (future.isCanceled()) - return DoneWith::Cancel; - - DoneWith doneWith = DoneWith::Cancel; - QEventLoop loop; - connect(this, &TaskTree::done, &loop, [&loop, &doneWith](DoneWith result) { - doneWith = result; - // Otherwise, the tasks from inside the running tree that were deleteLater() - // will be leaked. Refer to the QObject::deleteLater() docs. - QMetaObject::invokeMethod(&loop, [&loop] { loop.quit(); }, Qt::QueuedConnection); - }); - QFutureWatcher<void> watcher; - connect(&watcher, &QFutureWatcherBase::canceled, this, &TaskTree::cancel); - watcher.setFuture(future); - - QTimer::singleShot(0, this, &TaskTree::start); - - loop.exec(QEventLoop::ExcludeUserInputEvents); - if (doneWith == DoneWith::Cancel) { - auto nonConstFuture = future; - nonConstFuture.cancel(); - } - return doneWith; -} - -/*! - Constructs a temporary task tree using the passed \a recipe and runs it blocking. - - Returns DoneWith::Success if the task tree finished successfully; - otherwise returns DoneWith::Error. - - \note Avoid using this method from the main thread. Use asynchronous start() instead. - This method is to be used in non-main threads or in auto tests. - - \sa start() -*/ -DoneWith TaskTree::runBlocking(const Group &recipe) -{ - QPromise<void> dummy; - dummy.start(); - return TaskTree::runBlocking(recipe, dummy.future()); -} - -/*! - \overload runBlocking(const Group &recipe) - - The passed \a future is used for listening to the cancel event. - When the task tree is canceled, this method cancels the passed \a future. -*/ -DoneWith TaskTree::runBlocking(const Group &recipe, const QFuture<void> &future) -{ - TaskTree taskTree(recipe); - return taskTree.runBlocking(future); -} - -/*! - Returns the current real count of asynchronous chains of invocations. - - The returned value indicates how many times the control returns to the caller's - event loop while the task tree is running. Initially, this value is 0. - If the execution of the task tree finishes fully synchronously, this value remains 0. - If the task tree contains any asynchronous tasks that are successfully started during - a call to start(), this value is bumped to 1 just before the call to start() finishes. - Later, when any asynchronous task finishes and any possible continuations are started, - this value is bumped again. The bumping continues until the task tree finishes. - When the task tree emits the done() signal, the bumping stops. - The asyncCountChanged() signal is emitted on every bump of this value. - - \sa asyncCountChanged() -*/ -int TaskTree::asyncCount() const -{ - return d->m_asyncCount; -} - -/*! - \fn void TaskTree::asyncCountChanged(int count) - - This signal is emitted when the running task tree is about to return control to the caller's - event loop. When the task tree is started, this signal is emitted with \a count value of 0, - and emitted later on every asyncCount() value bump with an updated \a count value. - Every signal sent (except the initial one with the value of 0) guarantees that the task tree - is still running asynchronously after the emission. - - \sa asyncCount() -*/ - -/*! - Returns the number of asynchronous tasks contained in the stored recipe. - - \note The returned number doesn't include \l {Tasking::Sync} {Sync} tasks. - \note Any task or group that was set up using withTimeout() increases the total number of - tasks by \c 1. - - \sa setRecipe(), progressMaximum() -*/ -int TaskTree::taskCount() const -{ - return d->m_root ? d->m_root->taskCount() : 0; -} - -/*! - \fn void TaskTree::progressValueChanged(int value) - - This signal is emitted when the running task tree finished, canceled, or skipped some tasks. - The \a value gives the current total number of finished, canceled or skipped tasks. - When the task tree is started, and after the started() signal was emitted, - this signal is emitted with an initial \a value of \c 0. - When the task tree is about to finish, and before the done() signal is emitted, - this signal is emitted with the final \a value of progressMaximum(). - - \sa progressValue(), progressMaximum() -*/ - -/*! - \fn int TaskTree::progressMaximum() const - - Returns the maximum progressValue(). - - \note Currently, it's the same as taskCount(). This might change in the future. - - \sa progressValue() -*/ - -/*! - Returns the current progress value, which is between the \c 0 and progressMaximum(). - - The returned number indicates how many tasks have been already finished, canceled, or skipped - while the task tree is running. - When the task tree is started, this number is set to \c 0. - When the task tree is finished, this number always equals progressMaximum(). - - \sa progressMaximum(), progressValueChanged() -*/ -int TaskTree::progressValue() const -{ - return d->m_progressValue; -} - -/*! - \fn template <typename StorageStruct, typename Handler> void TaskTree::onStorageSetup(const Storage<StorageStruct> &storage, Handler &&handler) - - Installs a storage setup \a handler for the \a storage to pass the initial data - dynamically to the running task tree. - - The \c StorageHandler takes a \e reference to the \c StorageStruct instance: - - \code - static void save(const QString &fileName, const QByteArray &array) { ... } - - Storage<QByteArray> storage; - - const auto onSaverSetup = [storage](ConcurrentCall<QByteArray> &concurrent) { - concurrent.setConcurrentCallData(&save, "foo.txt", *storage); - }; - - const Group root { - storage, - ConcurrentCallTask(onSaverSetup) - }; - - TaskTree taskTree(root); - auto initStorage = [](QByteArray &storage){ - storage = "initial content"; - }; - taskTree.onStorageSetup(storage, initStorage); - taskTree.start(); - \endcode - - When the running task tree enters a Group where the \a storage is placed in, - it creates a \c StorageStruct instance, ready to be used inside this group. - Just after the \c StorageStruct instance is created, and before any handler of this group - is called, the task tree invokes the passed \a handler. This enables setting up - initial content for the given storage dynamically. Later, when any group's handler is invoked, - the task tree activates the created and initialized storage, so that it's available inside - any group's handler. - - \sa onStorageDone() -*/ - -/*! - \fn template <typename StorageStruct, typename Handler> void TaskTree::onStorageDone(const Storage<StorageStruct> &storage, Handler &&handler) - - Installs a storage done \a handler for the \a storage to retrieve the final data - dynamically from the running task tree. - - The \c StorageHandler takes a \c const \e reference to the \c StorageStruct instance: - - \code - static QByteArray load(const QString &fileName) { ... } - - Storage<QByteArray> storage; - - const auto onLoaderSetup = [](ConcurrentCall<QByteArray> &concurrent) { - concurrent.setConcurrentCallData(&load, "foo.txt"); - }; - const auto onLoaderDone = [storage](const ConcurrentCall<QByteArray> &concurrent) { - *storage = concurrent.result(); - }; - - const Group root { - storage, - ConcurrentCallTask(onLoaderSetup, onLoaderDone, CallDoneIf::Success) - }; - - TaskTree taskTree(root); - auto collectStorage = [](const QByteArray &storage){ - qDebug() << "final content" << storage; - }; - taskTree.onStorageDone(storage, collectStorage); - taskTree.start(); - \endcode - - When the running task tree is about to leave a Group where the \a storage is placed in, - it destructs a \c StorageStruct instance. - Just before the \c StorageStruct instance is destructed, and after all possible handlers from - this group were called, the task tree invokes the passed \a handler. This enables reading - the final content of the given storage dynamically and processing it further outside of - the task tree. - - This handler is called also when the running tree is canceled. However, it's not called - when the running tree is destructed. - - \sa onStorageSetup() -*/ - -void TaskTree::setupStorageHandler(const StorageBase &storage, - const StorageBase::StorageHandler &setupHandler, - const StorageBase::StorageHandler &doneHandler) -{ - auto it = d->m_storageHandlers.find(storage); - if (it == d->m_storageHandlers.end()) { - d->m_storageHandlers.insert(storage, {setupHandler, doneHandler}); - return; - } - if (setupHandler) { - QT_ASSERT(!it->m_setupHandler, - qWarning("The storage has its setup handler defined, overriding...")); - it->m_setupHandler = setupHandler; - } - if (doneHandler) { - QT_ASSERT(!it->m_doneHandler, - qWarning("The storage has its done handler defined, overriding...")); - it->m_doneHandler = doneHandler; - } -} - -TaskTreeTaskAdapter::TaskTreeTaskAdapter() -{ - connect(task(), &TaskTree::done, this, - [this](DoneWith result) { emit done(toDoneResult(result)); }); -} - -void TaskTreeTaskAdapter::start() -{ - task()->start(); -} - -using TimeoutCallback = std::function<void()>; - -struct TimerData -{ - system_clock::time_point m_deadline; - QPointer<QObject> m_context; - TimeoutCallback m_callback; -}; - -struct TimerThreadData -{ - Q_DISABLE_COPY_MOVE(TimerThreadData) - - TimerThreadData() = default; // defult constructor is required for initializing with {} since C++20 by Mingw 11.20 - QHash<int, TimerData> m_timerIdToTimerData = {}; - QMap<system_clock::time_point, QList<int>> m_deadlineToTimerId = {}; - int m_timerIdCounter = 0; -}; - -// Please note the thread_local keyword below guarantees a separate instance per thread. -static thread_local TimerThreadData s_threadTimerData = {}; - -static void removeTimerId(int timerId) -{ - const auto it = s_threadTimerData.m_timerIdToTimerData.constFind(timerId); - QT_ASSERT(it != s_threadTimerData.m_timerIdToTimerData.cend(), - qWarning("Removing active timerId failed."); return); - - const system_clock::time_point deadline = it->m_deadline; - s_threadTimerData.m_timerIdToTimerData.erase(it); - - QList<int> &ids = s_threadTimerData.m_deadlineToTimerId[deadline]; - const int removedCount = ids.removeAll(timerId); - QT_ASSERT(removedCount == 1, qWarning("Removing active timerId failed."); return); - if (ids.isEmpty()) - s_threadTimerData.m_deadlineToTimerId.remove(deadline); -} - -static void handleTimeout(int timerId) -{ - const auto itData = s_threadTimerData.m_timerIdToTimerData.constFind(timerId); - if (itData == s_threadTimerData.m_timerIdToTimerData.cend()) - return; // The timer was already activated. - - const auto deadline = itData->m_deadline; - while (true) { - auto itMap = s_threadTimerData.m_deadlineToTimerId.begin(); - if (itMap == s_threadTimerData.m_deadlineToTimerId.end()) - return; - - if (itMap.key() > deadline) - return; - - std::optional<TimerData> timerData; - QList<int> &idList = *itMap; - if (!idList.isEmpty()) { - const int first = idList.first(); - idList.removeFirst(); - - const auto it = s_threadTimerData.m_timerIdToTimerData.constFind(first); - if (it != s_threadTimerData.m_timerIdToTimerData.cend()) { - timerData = it.value(); - s_threadTimerData.m_timerIdToTimerData.erase(it); - } else { - QT_CHECK(false); - } - } else { - QT_CHECK(false); - } - - if (idList.isEmpty()) - s_threadTimerData.m_deadlineToTimerId.erase(itMap); - if (timerData && timerData->m_context) - timerData->m_callback(); - } -} - -static int scheduleTimeout(milliseconds timeout, QObject *context, const TimeoutCallback &callback) -{ - const int timerId = ++s_threadTimerData.m_timerIdCounter; - const system_clock::time_point deadline = system_clock::now() + timeout; - QTimer::singleShot(timeout, context, [timerId] { handleTimeout(timerId); }); - s_threadTimerData.m_timerIdToTimerData.emplace(timerId, TimerData{deadline, context, callback}); - s_threadTimerData.m_deadlineToTimerId[deadline].append(timerId); - return timerId; -} - -TimeoutTaskAdapter::TimeoutTaskAdapter() -{ - *task() = milliseconds::zero(); -} - -TimeoutTaskAdapter::~TimeoutTaskAdapter() -{ - if (m_timerId) - removeTimerId(*m_timerId); -} - -void TimeoutTaskAdapter::start() -{ - m_timerId = scheduleTimeout(*task(), this, [this] { - m_timerId.reset(); - emit done(DoneResult::Success); - }); -} - -ExecutableItem timeoutTask(const std::chrono::milliseconds &timeout, DoneResult result) -{ - return TimeoutTask([timeout](std::chrono::milliseconds &t) { t = timeout; }, result); -} - -/*! - \typealias Tasking::TaskTreeTask - - Type alias for the CustomTask, to be used inside recipes, associated with the TaskTree task. -*/ - -/*! - \typealias Tasking::TimeoutTask - - Type alias for the CustomTask, to be used inside recipes, associated with the - \c std::chrono::milliseconds type. \c std::chrono::milliseconds is used to set up the - timeout duration. The default timeout is \c std::chrono::milliseconds::zero(), that is, - the TimeoutTask finishes as soon as the control returns to the running event loop. - - Example usage: - - \code - using namespace std::chrono; - using namespace std::chrono_literals; - - const auto onSetup = [](milliseconds &timeout) { timeout = 1000ms; } - const auto onDone = [] { qDebug() << "Timed out."; } - - const Group root { - Timeout(onSetup, onDone) - }; - \endcode -*/ - -} // namespace Tasking - -QT_END_NAMESPACE diff --git a/src/assets/downloader/tasking/tasktree.h b/src/assets/downloader/tasking/tasktree.h deleted file mode 100644 index d46bd8eee3e..00000000000 --- a/src/assets/downloader/tasking/tasktree.h +++ /dev/null @@ -1,757 +0,0 @@ -// Copyright (C) 2024 Jarek Kobus -// Copyright (C) 2024 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 TASKING_TASKTREE_H -#define TASKING_TASKTREE_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 "tasking_global.h" - -#include <QtCore/QList> -#include <QtCore/QObject> - -#include <memory> - -QT_BEGIN_NAMESPACE -template <class T> -class QFuture; - -namespace Tasking { - -class Do; -class For; -class Group; -class GroupItem; -using GroupItems = QList<GroupItem>; - -Q_NAMESPACE_EXPORT(TASKING_EXPORT) - -// WorkflowPolicy: -// 1. When all children finished with success -> report success, otherwise: -// a) Report error on first error and stop executing other children (including their subtree). -// b) On first error - continue executing all children and report error afterwards. -// 2. When all children finished with error -> report error, otherwise: -// a) Report success on first success and stop executing other children (including their subtree). -// b) On first success - continue executing all children and report success afterwards. -// 3. Stops on first finished child. In sequential mode it will never run other children then the first one. -// Useful only in parallel mode. -// 4. Always run all children, let them finish, ignore their results and report success afterwards. -// 5. Always run all children, let them finish, ignore their results and report error afterwards. - -enum class WorkflowPolicy -{ - StopOnError, // 1a - Reports error on first child error, otherwise success (if all children were success). - ContinueOnError, // 1b - The same, but children execution continues. Reports success when no children. - StopOnSuccess, // 2a - Reports success on first child success, otherwise error (if all children were error). - ContinueOnSuccess, // 2b - The same, but children execution continues. Reports error when no children. - StopOnSuccessOrError, // 3 - Stops on first finished child and report its result. - FinishAllAndSuccess, // 4 - Reports success after all children finished. - FinishAllAndError // 5 - Reports error after all children finished. -}; -Q_ENUM_NS(WorkflowPolicy) - -enum class SetupResult -{ - Continue, - StopWithSuccess, - StopWithError -}; -Q_ENUM_NS(SetupResult) - -enum class DoneResult -{ - Success, - Error -}; -Q_ENUM_NS(DoneResult) - -enum class DoneWith -{ - Success, - Error, - Cancel -}; -Q_ENUM_NS(DoneWith) - -enum class CallDoneIf -{ - SuccessOrError, - Success, - Error -}; -Q_ENUM_NS(CallDoneIf) - -TASKING_EXPORT DoneResult toDoneResult(bool success); - -class LoopData; -class StorageData; -class TaskTreePrivate; - -class TASKING_EXPORT TaskInterface : public QObject -{ - Q_OBJECT - -Q_SIGNALS: - void done(DoneResult result); - -private: - template <typename Task, typename Deleter> friend class TaskAdapter; - friend class TaskTreePrivate; - TaskInterface() = default; -#ifdef Q_QDOC -protected: -#endif - virtual void start() = 0; -}; - -class TASKING_EXPORT Loop -{ -public: - using Condition = std::function<bool(int)>; // Takes iteration, called prior to each iteration. - using ValueGetter = std::function<const void *(int)>; // Takes iteration, returns ptr to ref. - - int iteration() const; - -protected: - Loop(); // LoopForever - Loop(int count, const ValueGetter &valueGetter = {}); // LoopRepeat, LoopList - Loop(const Condition &condition); // LoopUntil - - const void *valuePtr() const; - -private: - friend class ExecutionContextActivator; - friend class TaskTreePrivate; - std::shared_ptr<LoopData> m_loopData; -}; - -class TASKING_EXPORT LoopForever final : public Loop -{ -public: - LoopForever() : Loop() {} -}; - -class TASKING_EXPORT LoopRepeat final : public Loop -{ -public: - LoopRepeat(int count) : Loop(count) {} -}; - -class TASKING_EXPORT LoopUntil final : public Loop -{ -public: - LoopUntil(const Condition &condition) : Loop(condition) {} -}; - -template <typename T> -class LoopList final : public Loop -{ -public: - LoopList(const QList<T> &list) : Loop(list.size(), [list](int i) { return &list.at(i); }) {} - const T *operator->() const { return static_cast<const T *>(valuePtr()); } - const T &operator*() const { return *static_cast<const T *>(valuePtr()); } -}; - -class TASKING_EXPORT StorageBase -{ -private: - using StorageConstructor = std::function<void *(void)>; - using StorageDestructor = std::function<void(void *)>; - using StorageHandler = std::function<void(void *)>; - - StorageBase(const StorageConstructor &ctor, const StorageDestructor &dtor); - - void *activeStorageVoid() const; - - friend bool operator==(const StorageBase &first, const StorageBase &second) - { return first.m_storageData == second.m_storageData; } - - friend bool operator!=(const StorageBase &first, const StorageBase &second) - { return first.m_storageData != second.m_storageData; } - - friend size_t qHash(const StorageBase &storage, uint seed = 0) - { return size_t(storage.m_storageData.get()) ^ seed; } - - std::shared_ptr<StorageData> m_storageData; - - template <typename StorageStruct> friend class Storage; - friend class ExecutionContextActivator; - friend class StorageData; - friend class RuntimeContainer; - friend class TaskTree; - friend class TaskTreePrivate; -}; - -template <typename StorageStruct> -class Storage final : public StorageBase -{ -public: - Storage() : StorageBase(Storage::ctor(), Storage::dtor()) {} -#if __cplusplus >= 201803L // C++20: Allow pack expansion in lambda init-capture. - template <typename ...Args> - Storage(const Args &...args) - : StorageBase([...args = args] { return new StorageStruct(args...); }, Storage::dtor()) {} -#else // C++17 - template <typename ...Args> - Storage(const Args &...args) - : StorageBase([argsTuple = std::tuple(args...)] { - return std::apply([](const Args &...arguments) { return new StorageStruct(arguments...); }, argsTuple); - }, Storage::dtor()) {} -#endif - StorageStruct &operator*() const noexcept { return *activeStorage(); } - StorageStruct *operator->() const noexcept { return activeStorage(); } - StorageStruct *activeStorage() const { - return static_cast<StorageStruct *>(activeStorageVoid()); - } - -private: - static StorageConstructor ctor() { return [] { return new StorageStruct(); }; } - static StorageDestructor dtor() { - return [](void *storage) { delete static_cast<StorageStruct *>(storage); }; - } -}; - -class TASKING_EXPORT GroupItem -{ -public: - // Called when group entered, after group's storages are created - using GroupSetupHandler = std::function<SetupResult()>; - // Called when group done, before group's storages are deleted - using GroupDoneHandler = std::function<DoneResult(DoneWith)>; - - template <typename StorageStruct> - GroupItem(const Storage<StorageStruct> &storage) - : m_type(Type::Storage) - , m_storageList{storage} {} - - // TODO: Add tests. - GroupItem(const GroupItems &children) : m_type(Type::List) { addChildren(children); } - GroupItem(std::initializer_list<GroupItem> children) : m_type(Type::List) { addChildren(children); } - -protected: - GroupItem(const Loop &loop) : GroupItem(GroupData{{}, {}, {}, loop}) {} - // Internal, provided by CustomTask - using InterfaceCreateHandler = std::function<TaskInterface *(void)>; - // Called prior to task start, just after createHandler - using InterfaceSetupHandler = std::function<SetupResult(TaskInterface &)>; - // Called on task done, just before deleteLater - using InterfaceDoneHandler = std::function<DoneResult(const TaskInterface &, DoneWith)>; - - struct TaskHandler { - InterfaceCreateHandler m_createHandler; - InterfaceSetupHandler m_setupHandler = {}; - InterfaceDoneHandler m_doneHandler = {}; - CallDoneIf m_callDoneIf = CallDoneIf::SuccessOrError; - }; - - struct GroupHandler { - GroupSetupHandler m_setupHandler; - GroupDoneHandler m_doneHandler = {}; - CallDoneIf m_callDoneIf = CallDoneIf::SuccessOrError; - }; - - struct GroupData { - GroupHandler m_groupHandler = {}; - std::optional<int> m_parallelLimit = {}; - std::optional<WorkflowPolicy> m_workflowPolicy = {}; - std::optional<Loop> m_loop = {}; - }; - - enum class Type { - List, - Group, - GroupData, - Storage, - TaskHandler - }; - - GroupItem() = default; - GroupItem(Type type) : m_type(type) { } - GroupItem(const GroupData &data) - : m_type(Type::GroupData) - , m_groupData(data) {} - GroupItem(const TaskHandler &handler) - : m_type(Type::TaskHandler) - , m_taskHandler(handler) {} - void addChildren(const GroupItems &children); - - static GroupItem groupHandler(const GroupHandler &handler) { return GroupItem({handler}); } - - // Checks if Function may be invoked with Args and if Function's return type is Result. - template <typename Result, typename Function, typename ...Args, - typename DecayedFunction = std::decay_t<Function>> - static constexpr bool isInvocable() - { - // Note, that std::is_invocable_r_v doesn't check Result type properly. - if constexpr (std::is_invocable_r_v<Result, DecayedFunction, Args...>) - return std::is_same_v<Result, std::invoke_result_t<DecayedFunction, Args...>>; - return false; - } - -private: - TASKING_EXPORT friend Group operator>>(const For &forItem, const Do &doItem); - friend class ContainerNode; - friend class TaskNode; - friend class TaskTreePrivate; - friend class ParallelLimitFunctor; - friend class WorkflowPolicyFunctor; - Type m_type = Type::Group; - GroupItems m_children; - GroupData m_groupData; - QList<StorageBase> m_storageList; - TaskHandler m_taskHandler; -}; - -class TASKING_EXPORT ExecutableItem : public GroupItem -{ -public: - Group withTimeout(std::chrono::milliseconds timeout, - const std::function<void()> &handler = {}) const; - Group withLog(const QString &logName) const; - template <typename SenderSignalPairGetter> - Group withCancel(SenderSignalPairGetter &&getter, std::initializer_list<GroupItem> postCancelRecipe = {}) const; - template <typename SenderSignalPairGetter> - Group withAccept(SenderSignalPairGetter &&getter) const; - -protected: - ExecutableItem() = default; - ExecutableItem(const TaskHandler &handler) : GroupItem(handler) {} - -private: - TASKING_EXPORT friend Group operator!(const ExecutableItem &item); - TASKING_EXPORT friend Group operator&&(const ExecutableItem &first, - const ExecutableItem &second); - TASKING_EXPORT friend Group operator||(const ExecutableItem &first, - const ExecutableItem &second); - TASKING_EXPORT friend Group operator&&(const ExecutableItem &item, DoneResult result); - TASKING_EXPORT friend Group operator||(const ExecutableItem &item, DoneResult result); - - Group withCancelImpl( - const std::function<void(QObject *, const std::function<void()> &)> &connectWrapper, - const GroupItems &postCancelRecipe) const; - Group withAcceptImpl( - const std::function<void(QObject *, const std::function<void()> &)> &connectWrapper) const; -}; - -class TASKING_EXPORT Group : public ExecutableItem -{ -public: - Group(const GroupItems &children) { addChildren(children); } - Group(std::initializer_list<GroupItem> children) { addChildren(children); } - - // GroupData related: - template <typename Handler> - static GroupItem onGroupSetup(Handler &&handler) { - return groupHandler({wrapGroupSetup(std::forward<Handler>(handler))}); - } - template <typename Handler> - static GroupItem onGroupDone(Handler &&handler, CallDoneIf callDoneIf = CallDoneIf::SuccessOrError) { - return groupHandler({{}, wrapGroupDone(std::forward<Handler>(handler)), callDoneIf}); - } - -private: - template <typename Handler> - static GroupSetupHandler wrapGroupSetup(Handler &&handler) - { - // R, V stands for: Setup[R]esult, [V]oid - static constexpr bool isR = isInvocable<SetupResult, Handler>(); - static constexpr bool isV = isInvocable<void, Handler>(); - static_assert(isR || isV, - "Group setup handler needs to take no arguments and has to return void or SetupResult. " - "The passed handler doesn't fulfill these requirements."); - return [handler = std::move(handler)] { - if constexpr (isR) - return std::invoke(handler); - std::invoke(handler); - return SetupResult::Continue; - }; - } - template <typename Handler> - static GroupDoneHandler wrapGroupDone(Handler &&handler) - { - static constexpr bool isDoneResultType = std::is_same_v<std::decay_t<Handler>, DoneResult>; - // R, B, V, D stands for: Done[R]esult, [B]ool, [V]oid, [D]oneWith - static constexpr bool isRD = isInvocable<DoneResult, Handler, DoneWith>(); - static constexpr bool isR = isInvocable<DoneResult, Handler>(); - static constexpr bool isBD = isInvocable<bool, Handler, DoneWith>(); - static constexpr bool isB = isInvocable<bool, Handler>(); - static constexpr bool isVD = isInvocable<void, Handler, DoneWith>(); - static constexpr bool isV = isInvocable<void, Handler>(); - static_assert(isDoneResultType || isRD || isR || isBD || isB || isVD || isV, - "Group done handler needs to take (DoneWith) or (void) as an argument and has to " - "return void, bool or DoneResult. Alternatively, it may be of DoneResult type. " - "The passed handler doesn't fulfill these requirements."); - return [handler = std::move(handler)](DoneWith result) { - if constexpr (isDoneResultType) - return handler; - if constexpr (isRD) - return std::invoke(handler, result); - if constexpr (isR) - return std::invoke(handler); - if constexpr (isBD) - return toDoneResult(std::invoke(handler, result)); - if constexpr (isB) - return toDoneResult(std::invoke(handler)); - if constexpr (isVD) - std::invoke(handler, result); - else if constexpr (isV) - std::invoke(handler); - return toDoneResult(result == DoneWith::Success); - }; - } -}; - -template <typename SenderSignalPairGetter> -Group ExecutableItem::withCancel(SenderSignalPairGetter &&getter, - std::initializer_list<GroupItem> postCancelRecipe) const -{ - const auto connectWrapper = [getter](QObject *guard, const std::function<void()> &trigger) { - const auto senderSignalPair = getter(); - QObject::connect(senderSignalPair.first, senderSignalPair.second, guard, [trigger] { - trigger(); - }, static_cast<Qt::ConnectionType>(Qt::QueuedConnection | Qt::SingleShotConnection)); - }; - return withCancelImpl(connectWrapper, postCancelRecipe); -} - -template <typename SenderSignalPairGetter> -Group ExecutableItem::withAccept(SenderSignalPairGetter &&getter) const -{ - const auto connectWrapper = [getter](QObject *guard, const std::function<void()> &trigger) { - const auto senderSignalPair = getter(); - QObject::connect(senderSignalPair.first, senderSignalPair.second, guard, [trigger] { - trigger(); - }, static_cast<Qt::ConnectionType>(Qt::QueuedConnection | Qt::SingleShotConnection)); - }; - return withAcceptImpl(connectWrapper); -} - -template <typename Handler> -static GroupItem onGroupSetup(Handler &&handler) -{ - return Group::onGroupSetup(std::forward<Handler>(handler)); -} - -template <typename Handler> -static GroupItem onGroupDone(Handler &&handler, CallDoneIf callDoneIf = CallDoneIf::SuccessOrError) -{ - return Group::onGroupDone(std::forward<Handler>(handler), callDoneIf); -} - -// Default: 1 (sequential). 0 means unlimited (parallel). -TASKING_EXPORT GroupItem parallelLimit(int limit); - -// Default: WorkflowPolicy::StopOnError. -TASKING_EXPORT GroupItem workflowPolicy(WorkflowPolicy policy); - -TASKING_EXPORT extern const GroupItem sequential; -TASKING_EXPORT extern const GroupItem parallel; -TASKING_EXPORT extern const GroupItem parallelIdealThreadCountLimit; - -TASKING_EXPORT extern const GroupItem stopOnError; -TASKING_EXPORT extern const GroupItem continueOnError; -TASKING_EXPORT extern const GroupItem stopOnSuccess; -TASKING_EXPORT extern const GroupItem continueOnSuccess; -TASKING_EXPORT extern const GroupItem stopOnSuccessOrError; -TASKING_EXPORT extern const GroupItem finishAllAndSuccess; -TASKING_EXPORT extern const GroupItem finishAllAndError; - -TASKING_EXPORT extern const GroupItem nullItem; -TASKING_EXPORT extern const ExecutableItem successItem; -TASKING_EXPORT extern const ExecutableItem errorItem; - -class TASKING_EXPORT For final -{ -public: - explicit For(const Loop &loop) : m_loop(loop) {} - -private: - TASKING_EXPORT friend Group operator>>(const For &forItem, const Do &doItem); - - Loop m_loop; -}; - -class When; - -class TASKING_EXPORT Do final -{ -public: - explicit Do(const GroupItems &children) : m_children(children) {} - explicit Do(std::initializer_list<GroupItem> children) : m_children(children) {} - -private: - TASKING_EXPORT friend Group operator>>(const For &forItem, const Do &doItem); - TASKING_EXPORT friend Group operator>>(const When &whenItem, const Do &doItem); - - GroupItem m_children; -}; - -class TASKING_EXPORT Forever final : public ExecutableItem -{ -public: - explicit Forever(const GroupItems &children) - { addChildren({ For (LoopForever()) >> Do { children } } ); } - explicit Forever(std::initializer_list<GroupItem> children) - { addChildren({ For (LoopForever()) >> Do { children } } ); } -}; - -// Synchronous invocation. Similarly to Group - isn't counted as a task inside taskCount() -class TASKING_EXPORT Sync final : public ExecutableItem -{ -public: - template <typename Handler> - Sync(Handler &&handler) { - addChildren({ onGroupDone(wrapHandler(std::forward<Handler>(handler))) }); - } - -private: - template <typename Handler> - static auto wrapHandler(Handler &&handler) { - // R, B, V stands for: Done[R]esult, [B]ool, [V]oid - static constexpr bool isR = isInvocable<DoneResult, Handler>(); - static constexpr bool isB = isInvocable<bool, Handler>(); - static constexpr bool isV = isInvocable<void, Handler>(); - static_assert(isR || isB || isV, - "Sync handler needs to take no arguments and has to return void, bool or DoneResult. " - "The passed handler doesn't fulfill these requirements."); - return handler; - } -}; - -template <typename Task, typename Deleter = std::default_delete<Task>> -class TaskAdapter : public TaskInterface -{ -protected: - TaskAdapter() : m_task(new Task) {} - Task *task() { return m_task.get(); } - const Task *task() const { return m_task.get(); } - -private: - using TaskType = Task; - using DeleterType = Deleter; - template <typename Adapter> friend class CustomTask; - std::unique_ptr<Task, Deleter> m_task; -}; - -template <typename Adapter> -class CustomTask final : public ExecutableItem -{ -public: - using Task = typename Adapter::TaskType; - using Deleter = typename Adapter::DeleterType; - static_assert(std::is_base_of_v<TaskAdapter<Task, Deleter>, Adapter>, - "The Adapter type for the CustomTask<Adapter> needs to be derived from " - "TaskAdapter<Task>."); - using TaskSetupHandler = std::function<SetupResult(Task &)>; - using TaskDoneHandler = std::function<DoneResult(const Task &, DoneWith)>; - - template <typename SetupHandler = TaskSetupHandler, typename DoneHandler = TaskDoneHandler> - CustomTask(SetupHandler &&setup = TaskSetupHandler(), DoneHandler &&done = TaskDoneHandler(), - CallDoneIf callDoneIf = CallDoneIf::SuccessOrError) - : ExecutableItem({&createAdapter, wrapSetup(std::forward<SetupHandler>(setup)), - wrapDone(std::forward<DoneHandler>(done)), callDoneIf}) - {} - -private: - static Adapter *createAdapter() { return new Adapter; } - - template <typename Handler> - static InterfaceSetupHandler wrapSetup(Handler &&handler) { - if constexpr (std::is_same_v<Handler, TaskSetupHandler>) - return {}; // When user passed {} for the setup handler. - // R, V stands for: Setup[R]esult, [V]oid - static constexpr bool isR = isInvocable<SetupResult, Handler, Task &>(); - static constexpr bool isV = isInvocable<void, Handler, Task &>(); - static_assert(isR || isV, - "Task setup handler needs to take (Task &) as an argument and has to return void or " - "SetupResult. The passed handler doesn't fulfill these requirements."); - return [handler = std::move(handler)](TaskInterface &taskInterface) { - Adapter &adapter = static_cast<Adapter &>(taskInterface); - if constexpr (isR) - return std::invoke(handler, *adapter.task()); - std::invoke(handler, *adapter.task()); - return SetupResult::Continue; - }; - } - - template <typename Handler> - static InterfaceDoneHandler wrapDone(Handler &&handler) { - if constexpr (std::is_same_v<Handler, TaskDoneHandler>) - return {}; // User passed {} for the done handler. - static constexpr bool isDoneResultType = std::is_same_v<std::decay_t<Handler>, DoneResult>; - // R, B, V, T, D stands for: Done[R]esult, [B]ool, [V]oid, [T]ask, [D]oneWith - static constexpr bool isRTD = isInvocable<DoneResult, Handler, const Task &, DoneWith>(); - static constexpr bool isRT = isInvocable<DoneResult, Handler, const Task &>(); - static constexpr bool isRD = isInvocable<DoneResult, Handler, DoneWith>(); - static constexpr bool isR = isInvocable<DoneResult, Handler>(); - static constexpr bool isBTD = isInvocable<bool, Handler, const Task &, DoneWith>(); - static constexpr bool isBT = isInvocable<bool, Handler, const Task &>(); - static constexpr bool isBD = isInvocable<bool, Handler, DoneWith>(); - static constexpr bool isB = isInvocable<bool, Handler>(); - static constexpr bool isVTD = isInvocable<void, Handler, const Task &, DoneWith>(); - static constexpr bool isVT = isInvocable<void, Handler, const Task &>(); - static constexpr bool isVD = isInvocable<void, Handler, DoneWith>(); - static constexpr bool isV = isInvocable<void, Handler>(); - static_assert(isDoneResultType || isRTD || isRT || isRD || isR - || isBTD || isBT || isBD || isB - || isVTD || isVT || isVD || isV, - "Task done handler needs to take (const Task &, DoneWith), (const Task &), " - "(DoneWith) or (void) as arguments and has to return void, bool or DoneResult. " - "Alternatively, it may be of DoneResult type. " - "The passed handler doesn't fulfill these requirements."); - return [handler = std::move(handler)](const TaskInterface &taskInterface, DoneWith result) { - if constexpr (isDoneResultType) - return handler; - const Adapter &adapter = static_cast<const Adapter &>(taskInterface); - if constexpr (isRTD) - return std::invoke(handler, *adapter.task(), result); - if constexpr (isRT) - return std::invoke(handler, *adapter.task()); - if constexpr (isRD) - return std::invoke(handler, result); - if constexpr (isR) - return std::invoke(handler); - if constexpr (isBTD) - return toDoneResult(std::invoke(handler, *adapter.task(), result)); - if constexpr (isBT) - return toDoneResult(std::invoke(handler, *adapter.task())); - if constexpr (isBD) - return toDoneResult(std::invoke(handler, result)); - if constexpr (isB) - return toDoneResult(std::invoke(handler)); - if constexpr (isVTD) - std::invoke(handler, *adapter.task(), result); - else if constexpr (isVT) - std::invoke(handler, *adapter.task()); - else if constexpr (isVD) - std::invoke(handler, result); - else if constexpr (isV) - std::invoke(handler); - return toDoneResult(result == DoneWith::Success); - }; - } -}; - -template <typename Task> -class SimpleTaskAdapter final : public TaskAdapter<Task> -{ -public: - SimpleTaskAdapter() { this->connect(this->task(), &Task::done, this, &TaskInterface::done); } - void start() final { this->task()->start(); } -}; - -// A convenient helper, when: -// 1. Task is derived from QObject. -// 2. Task::start() method starts the task. -// 3. Task::done(DoneResult) signal is emitted when the task is finished. -template <typename Task> -using SimpleCustomTask = CustomTask<SimpleTaskAdapter<Task>>; - -class TASKING_EXPORT TaskTree final : public QObject -{ - Q_OBJECT - -public: - TaskTree(); - TaskTree(const Group &recipe); - ~TaskTree(); - - void setRecipe(const Group &recipe); - - void start(); - void cancel(); - bool isRunning() const; - - // Helper methods. They execute a local event loop with ExcludeUserInputEvents. - // The passed future is used for listening to the cancel event. - // Don't use it in main thread. To be used in non-main threads or in auto tests. - DoneWith runBlocking(); - DoneWith runBlocking(const QFuture<void> &future); - static DoneWith runBlocking(const Group &recipe); - static DoneWith runBlocking(const Group &recipe, const QFuture<void> &future); - - int asyncCount() const; - int taskCount() const; - int progressMaximum() const { return taskCount(); } - int progressValue() const; // all finished / skipped / stopped tasks, groups itself excluded - - template <typename StorageStruct, typename Handler> - void onStorageSetup(const Storage<StorageStruct> &storage, Handler &&handler) { - static_assert(std::is_invocable_v<std::decay_t<Handler>, StorageStruct &>, - "Storage setup handler needs to take (Storage &) as an argument. " - "The passed handler doesn't fulfill this requirement."); - setupStorageHandler(storage, - wrapHandler<StorageStruct>(std::forward<Handler>(handler)), {}); - } - template <typename StorageStruct, typename Handler> - void onStorageDone(const Storage<StorageStruct> &storage, Handler &&handler) { - static_assert(std::is_invocable_v<std::decay_t<Handler>, const StorageStruct &>, - "Storage done handler needs to take (const Storage &) as an argument. " - "The passed handler doesn't fulfill this requirement."); - setupStorageHandler(storage, {}, - wrapHandler<const StorageStruct>(std::forward<Handler>(handler))); - } - -Q_SIGNALS: - void started(); - void done(DoneWith result); - void asyncCountChanged(int count); - void progressValueChanged(int value); // updated whenever task finished / skipped / stopped - -private: - void setupStorageHandler(const StorageBase &storage, - const StorageBase::StorageHandler &setupHandler, - const StorageBase::StorageHandler &doneHandler); - template <typename StorageStruct, typename Handler> - StorageBase::StorageHandler wrapHandler(Handler &&handler) { - return [handler](void *voidStruct) { - auto *storageStruct = static_cast<StorageStruct *>(voidStruct); - std::invoke(handler, *storageStruct); - }; - } - - TaskTreePrivate *d; -}; - -class TASKING_EXPORT TaskTreeTaskAdapter : public TaskAdapter<TaskTree> -{ -public: - TaskTreeTaskAdapter(); - -private: - void start() final; -}; - -class TASKING_EXPORT TimeoutTaskAdapter : public TaskAdapter<std::chrono::milliseconds> -{ -public: - TimeoutTaskAdapter(); - ~TimeoutTaskAdapter(); - -private: - void start() final; - std::optional<int> m_timerId; -}; - -using TaskTreeTask = CustomTask<TaskTreeTaskAdapter>; -using TimeoutTask = CustomTask<TimeoutTaskAdapter>; - -TASKING_EXPORT ExecutableItem timeoutTask(const std::chrono::milliseconds &timeout, - DoneResult result = DoneResult::Error); - -} // namespace Tasking - -QT_END_NAMESPACE - -#endif // TASKING_TASKTREE_H diff --git a/src/assets/downloader/tasking/tasktreerunner.cpp b/src/assets/downloader/tasking/tasktreerunner.cpp deleted file mode 100644 index 6ed642b1bfd..00000000000 --- a/src/assets/downloader/tasking/tasktreerunner.cpp +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (C) 2024 Jarek Kobus -// Copyright (C) 2024 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 "tasktreerunner.h" - -#include "tasktree.h" - -QT_BEGIN_NAMESPACE - -namespace Tasking { - -TaskTreeRunner::~TaskTreeRunner() = default; - -void TaskTreeRunner::start(const Group &recipe, - const SetupHandler &setupHandler, - const DoneHandler &doneHandler) -{ - m_taskTree.reset(new TaskTree(recipe)); - connect(m_taskTree.get(), &TaskTree::done, this, [this, doneHandler](DoneWith result) { - m_taskTree.release()->deleteLater(); - if (doneHandler) - doneHandler(result); - emit done(result); - }); - if (setupHandler) - setupHandler(m_taskTree.get()); - emit aboutToStart(m_taskTree.get()); - m_taskTree->start(); -} - -void TaskTreeRunner::cancel() -{ - if (m_taskTree) - m_taskTree->cancel(); -} - -void TaskTreeRunner::reset() -{ - m_taskTree.reset(); -} - -} // namespace Tasking - -QT_END_NAMESPACE diff --git a/src/assets/downloader/tasking/tasktreerunner.h b/src/assets/downloader/tasking/tasktreerunner.h deleted file mode 100644 index f91e7608113..00000000000 --- a/src/assets/downloader/tasking/tasktreerunner.h +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (C) 2024 Jarek Kobus -// Copyright (C) 2024 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 TASKING_TASKTREERUNNER_H -#define TASKING_TASKTREERUNNER_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 "tasking_global.h" -#include "tasktree.h" - -#include <QtCore/QObject> - -QT_BEGIN_NAMESPACE - -namespace Tasking { - -class TASKING_EXPORT TaskTreeRunner : public QObject -{ - Q_OBJECT - -public: - using SetupHandler = std::function<void(TaskTree *)>; - using DoneHandler = std::function<void(DoneWith)>; - - ~TaskTreeRunner(); - - bool isRunning() const { return bool(m_taskTree); } - - // When task tree is running it resets the old task tree. - void start(const Group &recipe, - const SetupHandler &setupHandler = {}, - const DoneHandler &doneHandler = {}); - - // When task tree is running it emits done(DoneWith::Cancel) synchronously. - void cancel(); - - // No done() signal is emitted. - void reset(); - -Q_SIGNALS: - void aboutToStart(TaskTree *taskTree); - void done(DoneWith result); - -private: - std::unique_ptr<TaskTree> m_taskTree; -}; - -} // namespace Tasking - -QT_END_NAMESPACE - -#endif // TASKING_TASKTREERUNNER_H diff --git a/src/assets/downloader/tasking/tcpsocket.cpp b/src/assets/downloader/tasking/tcpsocket.cpp deleted file mode 100644 index 19aab8fc714..00000000000 --- a/src/assets/downloader/tasking/tcpsocket.cpp +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (C) 2024 Jarek Kobus -// Copyright (C) 2024 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 "tcpsocket.h" - -QT_BEGIN_NAMESPACE - -namespace Tasking { - -void TcpSocket::start() -{ - if (m_socket) { - qWarning("The TcpSocket is already running. Ignoring the call to start()."); - return; - } - if (m_address.isNull()) { - qWarning("Can't start the TcpSocket with invalid address. " - "Stopping with an error."); - m_error = QAbstractSocket::HostNotFoundError; - emit done(DoneResult::Error); - return; - } - - m_socket.reset(new QTcpSocket); - connect(m_socket.get(), &QAbstractSocket::errorOccurred, this, - [this](QAbstractSocket::SocketError error) { - m_error = error; - m_socket->disconnect(); - emit done(DoneResult::Error); - m_socket.release()->deleteLater(); - }); - connect(m_socket.get(), &QAbstractSocket::connected, this, [this] { - if (!m_writeData.isEmpty()) - m_socket->write(m_writeData); - emit started(); - }); - connect(m_socket.get(), &QAbstractSocket::disconnected, this, [this] { - m_socket->disconnect(); - emit done(DoneResult::Success); - m_socket.release()->deleteLater(); - }); - - m_socket->connectToHost(m_address, m_port); -} - -TcpSocket::~TcpSocket() -{ - if (m_socket) { - m_socket->disconnect(); - m_socket->abort(); - } -} - -} // namespace Tasking - -QT_END_NAMESPACE diff --git a/src/assets/downloader/tasking/tcpsocket.h b/src/assets/downloader/tasking/tcpsocket.h deleted file mode 100644 index ec0069bf130..00000000000 --- a/src/assets/downloader/tasking/tcpsocket.h +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (C) 2024 Jarek Kobus -// Copyright (C) 2024 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 TASKING_TCPSOCKET_H -#define TASKING_TCPSOCKET_H - -#include "tasking_global.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 "tasktree.h" - -#include <QtNetwork/QTcpSocket> - -#include <memory> - -QT_BEGIN_NAMESPACE - -namespace Tasking { - -// This class introduces the dependency to Qt::Network, otherwise Tasking namespace -// is independent on Qt::Network. -// Possibly, it could be placed inside Qt::Network library, as a wrapper around QTcpSocket. - -class TASKING_EXPORT TcpSocket final : public QObject -{ - Q_OBJECT - -public: - ~TcpSocket(); - void setAddress(const QHostAddress &address) { m_address = address; } - void setPort(quint16 port) { m_port = port; } - void setWriteData(const QByteArray &data) { m_writeData = data; } - QTcpSocket *socket() const { return m_socket.get(); } - void start(); - -Q_SIGNALS: - void started(); - void done(DoneResult result); - -private: - QHostAddress m_address; - quint16 m_port = 0; - QByteArray m_writeData; - std::unique_ptr<QTcpSocket> m_socket; - QAbstractSocket::SocketError m_error = QAbstractSocket::UnknownSocketError; -}; - -using TcpSocketTask = SimpleCustomTask<TcpSocket>; - -} // namespace Tasking - -QT_END_NAMESPACE - -#endif // TASKING_TCPSOCKET_H diff --git a/src/corelib/CMakeLists.txt b/src/corelib/CMakeLists.txt index 1147205b79f..c4a3480b2f9 100644 --- a/src/corelib/CMakeLists.txt +++ b/src/corelib/CMakeLists.txt @@ -1028,7 +1028,7 @@ qt_internal_extend_target(Core qt_internal_extend_target(Core CONDITION - QT_FEATURE_timezone AND UNIX + QT_FEATURE_timezone AND UNIX AND NOT VXWORKS AND NOT ANDROID AND NOT APPLE AND NOT QT_FEATURE_timezone_tzdb SOURCES time/qtimezoneprivate_tz.cpp @@ -1037,7 +1037,7 @@ qt_internal_extend_target(Core qt_internal_extend_target(Core CONDITION QT_FEATURE_icu AND QT_FEATURE_timezone - AND NOT UNIX AND NOT QT_FEATURE_timezone_tzdb + AND (VXWORKS OR NOT UNIX) AND NOT QT_FEATURE_timezone_tzdb SOURCES time/qtimezoneprivate_icu.cpp ) diff --git a/src/corelib/configure.cmake b/src/corelib/configure.cmake index d951b85c147..edcfba0f6ce 100644 --- a/src/corelib/configure.cmake +++ b/src/corelib/configure.cmake @@ -1150,7 +1150,7 @@ qt_feature("timezone" PUBLIC SECTION "Utilities" LABEL "QTimeZone" PURPOSE "Provides support for time-zone handling." - CONDITION NOT WASM AND NOT VXWORKS + CONDITION NOT WASM ) qt_feature("timezone_locale" PRIVATE SECTION "Utilities" diff --git a/src/corelib/doc/images/javaiterators1.svg b/src/corelib/doc/images/javaiterators1.svg new file mode 100644 index 00000000000..468dbe5371c --- /dev/null +++ b/src/corelib/doc/images/javaiterators1.svg @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + version="1.1" + width="340" + height="110" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + +<style> + svg .box-style { stroke: black; fill: white } + svg .line-style { stroke: red; fill: none } + svg .text-style { font: 20px arial; fill: black } + svg .fill-style { stroke: none; fill: red } + + [data-theme="dark"] svg .box-style { stroke: #f2f2f2; fill: black } + [data-theme="dark"] svg .line-style { stroke: red; fill: none } + [data-theme="dark"] svg .text-style { font: 20px arial; fill: #f2f2f2 } + [data-theme="dark"] svg .fill-style { stroke: none; fill: red } + + [data-theme="light"] svg .box-style { stroke: black; fill: white } + [data-theme="light"] svg .line-style { stroke: red; fill: none } + [data-theme="light"] svg .text-style { font: 20px arial; fill: black } + [data-theme="light"] svg .fill-style { stroke: none; fill: red } +</style> + +<g transform="translate(10.5, 10.5)"> +<path d="m 0,0 h 80 v 60 h -80 z" class="box-style" /> +<text x="35" y="36" class="text-style">A</text> +<path d="M 0,60 v 30" class="line-style" /> +<path d="M 0,60 l -5,10 l 10,0 z" class="fill-style" /> +</g> + +<g transform="translate(90.5, 10.5)"> +<path d="m 0,0 h 80 v 60 h -80 z" class="box-style" /> +<text x="35" y="36" class="text-style">B</text> +<path d="M 0,60 v 30" class="line-style" /> +<path d="M 0,60 l -5,10 l 10,0 z" class="fill-style" /> +</g> + +<g transform="translate(170.5, 10.5)"> +<path d="m 0,0 h 80 v 60 h -80 z" class="box-style" /> +<text x="35" y="36" class="text-style">C</text> +<path d="M 0,60 v 30" class="line-style" /> +<path d="M 0,60 l -5,10 l 10,0 z" class="fill-style" /> +</g> + +<g transform="translate(250.5, 10.5)"> +<path d="m 0,0 h 80 v 60 h -80 z" class="box-style" /> +<text x="35" y="36" class="text-style">D</text> +<path d="M 0,60 v 30" class="line-style" /> +<path d="M 0,60 l -5,10 l 10,0 z" class="fill-style" /> +</g> + +<g transform="translate(330.5, 10.5)"> +<path d="M 0,60 v 30" class="line-style" /> +<path d="M 0,60 l -5,10 l 10,0 z" class="fill-style" /> +</g> + +</svg> diff --git a/src/corelib/doc/images/javaiterators2.svg b/src/corelib/doc/images/javaiterators2.svg new file mode 100644 index 00000000000..df4c6b352a6 --- /dev/null +++ b/src/corelib/doc/images/javaiterators2.svg @@ -0,0 +1,63 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + version="1.1" + width="340" + height="130" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + +<style> + svg .box-style { stroke: black; fill: white } + svg .line-style { stroke: red; fill: none } + svg .fill-style { stroke: none; fill: red } + svg .np-line-style { stroke: blue; fill: none } + svg .np-fill-style { stroke: none; fill: blue } + svg .text-style { font: 20px arial; fill: black } + svg .small-text-style { font: 12px monospace; fill: black } + + [data-theme="dark"] svg .box-style { stroke: #f2f2f2; fill: black } + [data-theme="dark"] svg .line-style { stroke: red; fill: none } + [data-theme="dark"] svg .fill-style { stroke: none; fill: red } + [data-theme="dark"] svg .np-line-style { stroke: #4080ff; fill: none; stroke-width: 1.5 } + [data-theme="dark"] svg .np-fill-style { stroke: none; fill: #4080ff } + [data-theme="dark"] svg .text-style { font: 20px arial; fill: #f2f2f2 } + [data-theme="dark"] svg .small-text-style { font: 12px monospace; fill: #f2f2f2 } + + [data-theme="light"] svg .box-style { stroke: black; fill: white } + [data-theme="light"] svg .line-style { stroke: red; fill: none } + [data-theme="light"] svg .fill-style { stroke: none; fill: red } + [data-theme="light"] svg .np-line-style { stroke: blue; fill: none } + [data-theme="light"] svg .np-fill-style { stroke: none; fill: blue } + [data-theme="light"] svg .text-style { font: 20px arial; fill: black } + [data-theme="light"] svg .small-text-style { font: 12px monospace; fill: black } +</style> + +<g transform="translate(10.5, 10.5)"> +<path d="m 0,0 h 80 v 60 h -80 z" class="box-style" /> +<text x="35" y="36" class="text-style">A</text> +</g> + +<g transform="translate(90.5, 10.5)"> +<path d="m 0,0 h 80 v 60 h -80 z" class="box-style" /> +<text x="35" y="36" class="text-style">B</text> +<path d="M 0,60 c 0,30 50,40 80,30" class="np-line-style" /> +<path d="M 0,60 l -2,14 l 11,-4 z" class="np-fill-style" /> +<text x="-15" y="110" class="small-text-style">previous()</text> +</g> + +<g transform="translate(170.5, 10.5)"> +<path d="m 0,0 h 80 v 60 h -80 z" class="box-style" /> +<text x="35" y="36" class="text-style">C</text> +<path d="M 0,60 v 50" class="line-style" /> +<path d="M 0,60 l -5,10 l 10,0 z" class="fill-style" /> +<text x="30" y="110" class="small-text-style">next()</text> +</g> + +<g transform="translate(250.5, 10.5)"> +<path d="m 0,0 h 80 v 60 h -80 z" class="box-style" /> +<text x="35" y="36" class="text-style">D</text> +<path d="M 0,60 c 0,30 -50,40 -80,30" class="np-line-style" /> +<path d="M 0,60 l 2,14 l -11,-4 z" class="np-fill-style" /> +</g> + +</svg> diff --git a/src/corelib/doc/src/java-style-iterators.qdoc b/src/corelib/doc/src/java-style-iterators.qdoc index 856005cb66c..a0e5c53d633 100644 --- a/src/corelib/doc/src/java-style-iterators.qdoc +++ b/src/corelib/doc/src/java-style-iterators.qdoc @@ -42,7 +42,7 @@ diagram below shows the valid iterator positions as red arrows for a list containing four items: - \image javaiterators1.png + \image javaiterators1.svg Java-style iterators point between items Here's a typical loop for iterating through all the elements of a QList<QString> in order: @@ -70,7 +70,7 @@ \l{QListIterator::next()}{next()} and \l{QListIterator::previous()}{previous()} on an iterator: - \image javaiterators2.png + \image javaiterators2.png Iterating to the next and previous items The following table summarizes the QListIterator API: diff --git a/src/corelib/doc/src/objectmodel/bindableproperties.qdoc b/src/corelib/doc/src/objectmodel/bindableproperties.qdoc index 9b3ea6ae660..5dc2e6dad12 100644 --- a/src/corelib/doc/src/objectmodel/bindableproperties.qdoc +++ b/src/corelib/doc/src/objectmodel/bindableproperties.qdoc @@ -172,7 +172,7 @@ The following illustrates this approach. - \badcode + \code void DerivedClass::setValue(int val) { // do something @@ -247,7 +247,7 @@ be called for the current value of the property, register your callback using subscribe() instead. - \section1 Interaction with Q_PROPERTYs + \section1 Interaction with Q_PROPERTY A \l {The Property System}{Q_PROPERTY} that defines \c BINDABLE can be bound and used in binding expressions. You can implement such properties using \l {QProperty}, diff --git a/src/corelib/io/qlockfile.cpp b/src/corelib/io/qlockfile.cpp index 075eb144e51..4d431b46213 100644 --- a/src/corelib/io/qlockfile.cpp +++ b/src/corelib/io/qlockfile.cpp @@ -379,8 +379,9 @@ QLockFilePrivate::~QLockFilePrivate() QByteArray QLockFilePrivate::lockFileContents() const { // Use operator% from the fast builder to avoid multiple memory allocations. - return QByteArray::number(QCoreApplication::applicationPid()) % '\n' - % processNameByPid(QCoreApplication::applicationPid()).toUtf8() % '\n' + qint64 pid = QCoreApplication::applicationPid(); + return QByteArray::number(pid) % '\n' + % processNameByPid(pid).toUtf8() % '\n' % machineName().toUtf8() % '\n' % QSysInfo::machineUniqueId() % '\n' % QSysInfo::bootUniqueId() % '\n'; diff --git a/src/corelib/itemmodels/qrangemodel.cpp b/src/corelib/itemmodels/qrangemodel.cpp index 05e3a39e589..b0c6c46b125 100644 --- a/src/corelib/itemmodels/qrangemodel.cpp +++ b/src/corelib/itemmodels/qrangemodel.cpp @@ -20,6 +20,8 @@ public: private: std::unique_ptr<QRangeModelImplBase, QRangeModelImplBase::Deleter> impl; + friend class QRangeModelImplBase; + mutable QHash<int, QByteArray> m_roleNames; }; @@ -28,6 +30,16 @@ QRangeModel::QRangeModel(QRangeModelImplBase *impl, QObject *parent) { } +QRangeModelImplBase *QRangeModelImplBase::getImplementation(QRangeModel *model) +{ + return model->d_func()->impl.get(); +} + +const QRangeModelImplBase *QRangeModelImplBase::getImplementation(const QRangeModel *model) +{ + return model->d_func()->impl.get(); +} + /*! \class QRangeModel \inmodule QtCore diff --git a/src/corelib/itemmodels/qrangemodel_impl.h b/src/corelib/itemmodels/qrangemodel_impl.h index f38dc88c0d9..c2f473542d7 100644 --- a/src/corelib/itemmodels/qrangemodel_impl.h +++ b/src/corelib/itemmodels/qrangemodel_impl.h @@ -842,6 +842,11 @@ namespace QRangeModelDetails { using protocol = wrapped_t<Protocol>; using row = typename range_traits<wrapped_t<Range>>::value_type; + static constexpr bool is_tree = std::conjunction_v<protocol_parentRow<protocol, row>, + protocol_childRows<protocol, row>>; + static constexpr bool is_list = static_size_v<row> == 0 + && (!has_metaobject_v<row> || row_category<row>::isMultiRole); + static constexpr bool is_table = !is_list && !is_tree; static constexpr bool has_newRow = protocol_newRow<protocol>(); static constexpr bool has_deleteRow = protocol_deleteRow<protocol, row>(); @@ -1046,6 +1051,9 @@ public: typename C::MultiData >; + static Q_CORE_EXPORT QRangeModelImplBase *getImplementation(QRangeModel *model); + static Q_CORE_EXPORT const QRangeModelImplBase *getImplementation(const QRangeModel *model); + private: friend class QRangeModelPrivate; QRangeModel *m_rangeModel; @@ -1147,13 +1155,6 @@ protected: } } - static constexpr bool isMutable() - { - return range_features::is_mutable && row_features::is_mutable - && std::is_reference_v<row_reference> - && Structure::is_mutable_impl; - } - static constexpr int static_row_count = QRangeModelDetails::static_size_v<range_type>; static constexpr bool rows_are_raw_pointers = std::is_pointer_v<row_type>; static constexpr bool rows_are_owning_or_raw_pointers = @@ -1161,9 +1162,6 @@ protected: static constexpr int static_column_count = QRangeModelDetails::static_size_v<row_type>; static constexpr bool one_dimensional_range = static_column_count == 0; - static constexpr bool dynamicRows() { return isMutable() && static_row_count < 0; } - static constexpr bool dynamicColumns() { return static_column_count < 0; } - // A row might be a value (or range of values), or a pointer. // row_ptr is always a pointer, and const_row_ptr is a pointer to const. using row_ptr = wrapped_row_type *; @@ -1205,6 +1203,15 @@ protected: "The range holding a move-only row-type must support insert(pos, start, end)"); public: + static constexpr bool isMutable() + { + return range_features::is_mutable && row_features::is_mutable + && std::is_reference_v<row_reference> + && Structure::is_mutable_impl; + } + static constexpr bool dynamicRows() { return isMutable() && static_row_count < 0; } + static constexpr bool dynamicColumns() { return static_column_count < 0; } + explicit QRangeModelImpl(Range &&model, Protocol&& protocol, QRangeModel *itemModel) : Ancestor(itemModel) , ProtocolStorage{std::forward<Protocol>(protocol)} @@ -1236,7 +1243,7 @@ public: if (row == index.row() && column == index.column()) return index; - if (column < 0 || column >= this->itemModel().columnCount()) + if (column < 0 || column >= this->columnCount({})) return {}; if (row == index.row()) @@ -1974,8 +1981,8 @@ public: } if (sourceRow == destRow || sourceRow == destRow - 1 || count <= 0 - || sourceRow < 0 || sourceRow + count - 1 >= this->itemModel().rowCount(sourceParent) - || destRow < 0 || destRow > this->itemModel().rowCount(destParent)) { + || sourceRow < 0 || sourceRow + count - 1 >= this->rowCount(sourceParent) + || destRow < 0 || destRow > this->rowCount(destParent)) { return false; } @@ -1995,11 +2002,14 @@ public: } } - QModelIndex parent(const QModelIndex &child) const { return that().parent(child); } + const protocol_type& protocol() const { return QRangeModelDetails::refTo(ProtocolStorage::object()); } + protocol_type& protocol() { return QRangeModelDetails::refTo(ProtocolStorage::object()); } + + QModelIndex parent(const QModelIndex &child) const { return that().parentImpl(child); } - int rowCount(const QModelIndex &parent) const { return that().rowCount(parent); } + int rowCount(const QModelIndex &parent) const { return that().rowCountImpl(parent); } - int columnCount(const QModelIndex &parent) const { return that().columnCount(parent); } + int columnCount(const QModelIndex &parent) const { return that().columnCountImpl(parent); } void destroy() { delete std::addressof(that()); } @@ -2035,6 +2045,11 @@ public: protected: ~QRangeModelImpl() { + deleteOwnedRows(); + } + + void deleteOwnedRows() + { // We delete row objects if we are not operating on a reference or pointer // to a range, as in that case, the owner of the referenced/pointed to // range also owns the row entries. @@ -2335,9 +2350,6 @@ protected: } - const protocol_type& protocol() const { return QRangeModelDetails::refTo(ProtocolStorage::object()); } - protocol_type& protocol() { return QRangeModelDetails::refTo(ProtocolStorage::object()); } - ModelData m_data; }; @@ -2385,7 +2397,7 @@ protected: return this->createIndex(row, column, QRangeModelDetails::pointerTo(*it)); } - QModelIndex parent(const QModelIndex &child) const + QModelIndex parentImpl(const QModelIndex &child) const { if (!child.isValid()) return {}; @@ -2410,12 +2422,12 @@ protected: return {}; } - int rowCount(const QModelIndex &parent) const + int rowCountImpl(const QModelIndex &parent) const { return Base::size(this->childRange(parent)); } - int columnCount(const QModelIndex &) const + int columnCountImpl(const QModelIndex &) const { // all levels of a tree have to have the same, static, column count if constexpr (Base::one_dimensional_range) @@ -2662,6 +2674,9 @@ class QGenericTableItemModelImpl using Base = QRangeModelImpl<QGenericTableItemModelImpl<Range>, Range>; friend class QRangeModelImpl<QGenericTableItemModelImpl<Range>, Range>; + static constexpr bool is_mutable_impl = true; + +public: using range_type = typename Base::range_type; using range_features = typename Base::range_features; using row_type = typename Base::row_type; @@ -2669,9 +2684,6 @@ class QGenericTableItemModelImpl using row_traits = typename Base::row_traits; using row_features = typename Base::row_features; - static constexpr bool is_mutable_impl = true; - -public: explicit QGenericTableItemModelImpl(Range &&model, QRangeModel *itemModel) : Base(std::forward<Range>(model), {}, itemModel) {} @@ -2692,19 +2704,19 @@ protected: } } - QModelIndex parent(const QModelIndex &) const + QModelIndex parentImpl(const QModelIndex &) const { return {}; } - int rowCount(const QModelIndex &parent) const + int rowCountImpl(const QModelIndex &parent) const { if (parent.isValid()) return 0; return int(Base::size(*this->m_data.model())); } - int columnCount(const QModelIndex &parent) const + int columnCountImpl(const QModelIndex &parent) const { if (parent.isValid()) return 0; @@ -2760,7 +2772,7 @@ protected: // dynamically sized rows all have to have the same column count if constexpr (Base::dynamicColumns() && row_features::has_resize) { if (QRangeModelDetails::isValid(empty_row)) - QRangeModelDetails::refTo(empty_row).resize(this->itemModel().columnCount()); + QRangeModelDetails::refTo(empty_row).resize(this->columnCount({})); } return empty_row; diff --git a/src/corelib/kernel/qcoreapplication.cpp b/src/corelib/kernel/qcoreapplication.cpp index 0c7e7db3188..de7c4df6d1d 100644 --- a/src/corelib/kernel/qcoreapplication.cpp +++ b/src/corelib/kernel/qcoreapplication.cpp @@ -2512,7 +2512,7 @@ QString QCoreApplication::applicationFilePath() Returns the current process ID for the application. */ -qint64 QCoreApplication::applicationPid() +qint64 QCoreApplication::applicationPid() noexcept { #if defined(Q_OS_WIN) return GetCurrentProcessId(); diff --git a/src/corelib/kernel/qcoreapplication.h b/src/corelib/kernel/qcoreapplication.h index 55c8097adc5..054e53a4a41 100644 --- a/src/corelib/kernel/qcoreapplication.h +++ b/src/corelib/kernel/qcoreapplication.h @@ -119,7 +119,7 @@ public: static QString applicationDirPath(); static QString applicationFilePath(); - Q_DECL_CONST_FUNCTION static qint64 applicationPid(); + Q_DECL_CONST_FUNCTION static qint64 applicationPid() noexcept; #if QT_CONFIG(permissions) || defined(Q_QDOC) Qt::PermissionStatus checkPermission(const QPermission &permission); diff --git a/src/corelib/kernel/qpermissions.cpp b/src/corelib/kernel/qpermissions.cpp index 8c95431d01f..bbcea8338ca 100644 --- a/src/corelib/kernel/qpermissions.cpp +++ b/src/corelib/kernel/qpermissions.cpp @@ -132,6 +132,10 @@ Q_LOGGING_CATEGORY(lcPermissions, "qt.permissions", QtWarningMsg); \annotatedlist permissions + \note The available permission types cover core functionality of Qt modules + like Qt Multimedia and Qt Positioning, but do not encompass all platform-specific + permissions. Custom permission types are not currently supported. + \section1 Best Practices To ensure the best possible user experience for the end user we recommend diff --git a/src/corelib/kernel/qvariant.cpp b/src/corelib/kernel/qvariant.cpp index 8366787ea66..57089f164b2 100644 --- a/src/corelib/kernel/qvariant.cpp +++ b/src/corelib/kernel/qvariant.cpp @@ -2974,4 +2974,156 @@ const QVariant *QVariantConstPointer::operator->() const implement operator->(). */ +/*! + \class QVariant::ConstReference + \since 6.11 + \inmodule QtCore + \brief The QVariant::ConstReference acts as a const reference to a QVariant. + + As the generic iterators don't actually instantiate a QVariant on each + step, they cannot return a reference to one from operator*(). + QVariant::ConstReference provides the same functionality as an actual + reference to a QVariant would, but is backed a referred-to value given as + template parameter. The template is implemented for + QMetaSequence::ConstIterator, QMetaSequence::Iterator, + QMetaAssociation::ConstIterator, and QMetaAssociation::Iterator. +*/ + +/*! + \fn template<typename Referred> QVariant::ConstReference<Referred>::ConstReference(const Referred &referred) + + Creates a QVariant::ConstReference from a \a referred. + */ + +/*! + \fn template<typename Referred> QVariant::ConstReference<Referred>::ConstReference(Referred &&referred) + + Creates a QVariant::ConstReference from a \a referred. + */ + +/*! + \fn template<typename Referred> QVariant::ConstReference<Referred>::operator QVariant() const + + Dereferences the reference to a QVariant. + This method needs to be specialized for each Referred type. It is + pre-defined for QMetaSequence::ConstIterator, QMetaSequence::Iterator, + QMetaAssociation::ConstIterator, and QMetaAssociation::Iterator. + */ + + +/*! + \class QVariant::Reference + \since 6.11 + \inmodule QtCore + \brief The QVariant::Reference acts as a non-const reference to a QVariant. + + As the generic iterators don't actually instantiate a QVariant on each + step, they cannot return a reference to one from operator*(). + QVariant::Reference provides the same functionality as an actual reference + to a QVariant would, but is backed a referred-to value given as template + parameter. The template is implemented for QMetaSequence::Iterator and + QMetaAssociation::Iterator. +*/ + +/*! + \fn template<typename Referred> QVariant::Reference<Referred>::Reference(const Referred &referred) + + Creates a QVariant::Reference from a \a referred. + */ + +/*! + \fn template<typename Referred> QVariant::Reference<Referred>::Reference(Referred &&referred) + + Creates a QVariant::Reference from a \a referred. + */ + +/*! + \fn template<typename Referred> QVariant::Reference<Referred> &QVariant::Reference<Referred>::operator=(const Reference<Referred> &value) + + Assigns a new \a value to the value referred to by this QVariant::Reference. + */ + +/*! + \fn template<typename Referred> QVariant::Reference<Referred> &QVariant::Reference<Referred>::operator=(Reference<Referred> &&value) + + Assigns a new \a value to the value referred to by this QVariant::Reference. +*/ + +/*! + \fn template<typename Referred> QVariant::Reference<Referred> &QVariant::Reference<Referred>::operator=(const QVariant &value) + + Assigns a new \a value to the value referred to by this QVariant::Reference. + This method needs to be specialized for each Referred type. It is + pre-defined for QMetaSequence::Iterator and QMetaAssociation::Iterator. + */ + +/*! + \fn template<typename Referred> QVariant::Reference<Referred>::operator QVariant() const + + Dereferences the reference to a QVariant. By default this instantiates a + temporary QVariant::ConstReference and calls dereferences that. In cases + where instantiating a temporary ConstReference is expensive, this method + should be specialized. + */ + +/*! + \class QVariant::ConstPointer + \since 6.11 + \inmodule QtCore + \brief QVariant::ConstPointer is a template class that emulates a const pointer to QVariant. + + QVariant::ConstPointer wraps pointed-to value and returns a + QVariant::ConstReference to it from its operator*(). This makes it suitable + as replacement for an actual pointer. We cannot return an actual pointer + from generic iterators as the iterators don't hold an actual QVariant. +*/ + +/*! + \fn template<typename Pointed> QVariant::ConstPointer<Pointed>::ConstPointer(const Pointed &pointed) + + Constructs a QVariant::ConstPointer from the value \a pointed to. + */ + +/*! + \fn template<typename Pointed> QVariant::ConstPointer<Pointed>::ConstPointer(Pointed &&pointed) + + Constructs a QVariant::ConstPointer from the value \a pointed to. + */ + +/*! + \fn template<typename Pointed> QVariant::ConstReference<Pointer> QVariant::ConstPointer<Pointed>::operator*() const + + Dereferences the QVariant::ConstPointer to a QVariant::ConstReference. + */ + +/*! + \class QVariant::Pointer + \since 6.11 + \inmodule QtCore + \brief QVariant::Pointer is a template class that emulates a non-const pointer to QVariant. + + QVariant::Pointer wraps pointed-to value and returns a QVariant::Reference + to it from its operator*(). This makes it suitable as replacement for an + actual pointer. We cannot return an actual pointer from generic iterators as + the iterators don't hold an actual QVariant. +*/ + +/*! + \fn template<typename Pointed> QVariant::Pointer<Pointed>::Pointer(const Pointed &pointed) + + Constructs a QVariant::Pointer from the value \a pointed to. + */ + +/*! + \fn template<typename Pointed> QVariant::Pointer<Pointed>::Pointer(Pointed &&pointed) + + Constructs a QVariant::Pointer from the value \a pointed to. + */ + +/*! + \fn template<typename Pointed> QVariant::Reference<Pointer> QVariant::Pointer<Pointed>::operator*() const + + Dereferences the QVariant::Pointer to a QVariant::Reference. + */ + QT_END_NAMESPACE diff --git a/src/corelib/kernel/qvariant.h b/src/corelib/kernel/qvariant.h index 542b1d9b709..9b219d089b5 100644 --- a/src/corelib/kernel/qvariant.h +++ b/src/corelib/kernel/qvariant.h @@ -61,7 +61,20 @@ inline T qvariant_cast(const QVariant &); namespace QtPrivate { template<> constexpr inline bool qIsRelocatable<QVariant> = true; -} + +template<typename Referred> +class ConstReference; + +template<typename Referred> +class Reference; + +template<typename Pointed> +class ConstPointer; + +template<typename Pointed> +class Pointer; +} // namespace QtPrivate + class Q_CORE_EXPORT QVariant { template <typename T, typename... Args> @@ -228,6 +241,123 @@ private: >; public: + template<typename Referred> + class ConstReference + { + private: + const Referred m_referred; + + public: + // You can initialize a const reference from another one, but you can't assign to it. + + explicit ConstReference(const Referred &referred) + noexcept(std::is_nothrow_copy_constructible_v<Referred>) + : m_referred(referred) {} + explicit ConstReference(Referred &&referred) + noexcept(std::is_nothrow_move_constructible_v<Referred>) + : m_referred(std::move(referred)) {} + ConstReference(const ConstReference &) = default; + ConstReference(ConstReference &&) = default; + ~ConstReference() = default; + ConstReference &operator=(const ConstReference &value) = delete; + ConstReference &operator=(ConstReference &&value) = delete; + + // To be specialized for each Referred + operator QVariant() const noexcept(Referred::canNoexceptConvertToQVariant); + }; + + template<typename Referred> + class Reference + { + private: + Referred m_referred; + + friend void swap(Reference a, Reference b) { return a.swap(std::move(b)); } + + public: + // Assigning and initializing are different operations for references. + + explicit Reference(const Referred &referred) + noexcept(std::is_nothrow_copy_constructible_v<Referred>) + : m_referred(referred) {} + explicit Reference(Referred &&referred) + noexcept(std::is_nothrow_move_constructible_v<Referred>) + : m_referred(std::move(referred)) {} + Reference(const Reference &) = default; + Reference(Reference &&) = default; + ~Reference() = default; + + Reference &operator=(const Reference &value) + noexcept(Referred::canNoexceptAssignQVariant) + { + return operator=(QVariant(value)); + } + + Reference &operator=(Reference &&value) + noexcept(Referred::canNoexceptAssignQVariant) + { + return operator=(QVariant(value)); + } + + operator QVariant() const noexcept(Referred::canNoexceptConvertToQVariant) + { + return ConstReference(m_referred); + } + + void swap(Reference b) + { + // swapping a reference is not swapping the referred item, but swapping its contents. + QVariant tmp = *this; + *this = std::move(b); + b = std::move(tmp); + } + + // To be specialized for each Referred + Reference &operator=(const QVariant &value) noexcept(Referred::canNoexceptAssignQVariant); + }; + + template<typename Pointed> + class ConstPointer + { + private: + Pointed m_pointed; + + public: + explicit ConstPointer(const Pointed &pointed) + noexcept(std::is_nothrow_copy_constructible_v<Pointed>) + : m_pointed(pointed) {} + explicit ConstPointer(Pointed &&pointed) + noexcept(std::is_nothrow_move_constructible_v<Pointed>) + : m_pointed(std::move(pointed)) {} + + ConstReference<Pointed> operator*() + const noexcept(std::is_nothrow_copy_constructible_v<Pointed>) + { + return ConstReference<Pointed>(m_pointed); + } + }; + + template<typename Pointed> + class Pointer + { + private: + Pointed m_pointed; + + public: + explicit Pointer(const Pointed &pointed) + noexcept(std::is_nothrow_copy_constructible_v<Pointed>) + : m_pointed(pointed) {} + explicit Pointer(Pointed &&pointed) + noexcept(std::is_nothrow_move_constructible_v<Pointed>) + : m_pointed(std::move(pointed)) {} + + Reference<Pointed> operator*() + const noexcept(std::is_nothrow_copy_constructible_v<Pointed>) + { + return Reference<Pointed>(m_pointed); + } + }; + template <typename T, typename... Args, if_constructible<T, Args...> = true> explicit QVariant(std::in_place_type_t<T>, Args&&... args) diff --git a/src/corelib/mimetypes/qmimedatabase.cpp b/src/corelib/mimetypes/qmimedatabase.cpp index 06dc614c352..93f905afc48 100644 --- a/src/corelib/mimetypes/qmimedatabase.cpp +++ b/src/corelib/mimetypes/qmimedatabase.cpp @@ -158,7 +158,7 @@ void QMimeDatabasePrivate::loadProviders() const QMimeDatabasePrivate::Providers &QMimeDatabasePrivate::providers() { -#ifndef Q_OS_WASM // stub implementation always returns true +#if QT_CONFIG(thread) // stub implementation always returns true Q_ASSERT(!mutex.tryLock()); // caller should have locked mutex #endif if (m_providers.empty()) { @@ -289,7 +289,9 @@ QStringList QMimeDatabasePrivate::mimeParents(const QString &mimeName) QStringList QMimeDatabasePrivate::parents(const QString &mimeName) { +#if QT_CONFIG(thread) // stub implementation always returns true Q_ASSERT(!mutex.tryLock()); +#endif QStringList result; for (const auto &provider : providers()) provider->addParents(mimeName, result); diff --git a/src/corelib/mimetypes/qmimeprovider.cpp b/src/corelib/mimetypes/qmimeprovider.cpp index 4ccc58b3f39..de7043e8c1d 100644 --- a/src/corelib/mimetypes/qmimeprovider.cpp +++ b/src/corelib/mimetypes/qmimeprovider.cpp @@ -377,7 +377,6 @@ void QMimeBinaryProvider::findByMagic(const QByteArray &data, QMimeMagicResult & void QMimeBinaryProvider::addParents(const QString &mime, QStringList &result) { - const QByteArray mimeStr = mime.toLatin1(); const int parentListOffset = m_cacheFile->getUint32(PosParentListOffset); const int numEntries = m_cacheFile->getUint32(parentListOffset); @@ -388,7 +387,7 @@ void QMimeBinaryProvider::addParents(const QString &mime, QStringList &result) const int off = parentListOffset + 4 + 8 * medium; const int mimeOffset = m_cacheFile->getUint32(off); const char *aMime = m_cacheFile->getCharStar(mimeOffset); - const int cmp = qstrcmp(aMime, mimeStr); + const int cmp = QLatin1StringView(aMime).compare(mime); if (cmp < 0) { begin = medium + 1; } else if (cmp > 0) { @@ -409,7 +408,6 @@ void QMimeBinaryProvider::addParents(const QString &mime, QStringList &result) QString QMimeBinaryProvider::resolveAlias(const QString &name) { - const QByteArray input = name.toLatin1(); const int aliasListOffset = m_cacheFile->getUint32(PosAliasListOffset); const int numEntries = m_cacheFile->getUint32(aliasListOffset); int begin = 0; @@ -419,7 +417,7 @@ QString QMimeBinaryProvider::resolveAlias(const QString &name) const int off = aliasListOffset + 4 + 8 * medium; const int aliasOffset = m_cacheFile->getUint32(off); const char *alias = m_cacheFile->getCharStar(aliasOffset); - const int cmp = qstrcmp(alias, input); + const int cmp = QLatin1StringView(alias).compare(name); if (cmp < 0) { begin = medium + 1; } else if (cmp > 0) { @@ -435,7 +433,6 @@ QString QMimeBinaryProvider::resolveAlias(const QString &name) void QMimeBinaryProvider::addAliases(const QString &name, QStringList &result) { - const QByteArray input = name.toLatin1(); const int aliasListOffset = m_cacheFile->getUint32(PosAliasListOffset); const int numEntries = m_cacheFile->getUint32(aliasListOffset); for (int pos = 0; pos < numEntries; ++pos) { @@ -443,7 +440,7 @@ void QMimeBinaryProvider::addAliases(const QString &name, QStringList &result) const int mimeOffset = m_cacheFile->getUint32(off + 4); const char *mimeType = m_cacheFile->getCharStar(mimeOffset); - if (input == mimeType) { + if (name == QLatin1StringView(mimeType)) { const int aliasOffset = m_cacheFile->getUint32(off); const char *alias = m_cacheFile->getCharStar(aliasOffset); const QString strAlias = QString::fromLatin1(alias); @@ -586,7 +583,7 @@ QMimeBinaryProvider::loadMimeTypeExtra(const QString &mimeName) // Binary search in the icons or generic-icons list QLatin1StringView QMimeBinaryProvider::iconForMime(CacheFile *cacheFile, int posListOffset, - const QByteArray &inputMime) + QStringView inputMime) { const int iconsListOffset = cacheFile->getUint32(posListOffset); const int numIcons = cacheFile->getUint32(iconsListOffset); @@ -597,7 +594,7 @@ QLatin1StringView QMimeBinaryProvider::iconForMime(CacheFile *cacheFile, int pos const int off = iconsListOffset + 4 + 8 * medium; const int mimeOffset = cacheFile->getUint32(off); const char *mime = cacheFile->getCharStar(mimeOffset); - const int cmp = qstrcmp(mime, inputMime); + const int cmp = QLatin1StringView(mime).compare(inputMime); if (cmp < 0) begin = medium + 1; else if (cmp > 0) @@ -612,14 +609,12 @@ QLatin1StringView QMimeBinaryProvider::iconForMime(CacheFile *cacheFile, int pos QString QMimeBinaryProvider::icon(const QString &name) { - const QByteArray inputMime = name.toLatin1(); - return iconForMime(m_cacheFile.get(), PosIconsListOffset, inputMime); + return iconForMime(m_cacheFile.get(), PosIconsListOffset, name); } QString QMimeBinaryProvider::genericIcon(const QString &name) { - const QByteArray inputMime = name.toLatin1(); - return iconForMime(m_cacheFile.get(), PosGenericIconsListOffset, inputMime); + return iconForMime(m_cacheFile.get(), PosGenericIconsListOffset, name); } //// diff --git a/src/corelib/mimetypes/qmimeprovider_p.h b/src/corelib/mimetypes/qmimeprovider_p.h index aede727d710..d597651b362 100644 --- a/src/corelib/mimetypes/qmimeprovider_p.h +++ b/src/corelib/mimetypes/qmimeprovider_p.h @@ -111,7 +111,7 @@ private: bool caseSensitiveCheck); bool matchMagicRule(CacheFile *cacheFile, int numMatchlets, int firstOffset, const QByteArray &data); - QLatin1StringView iconForMime(CacheFile *cacheFile, int posListOffset, const QByteArray &inputMime); + QLatin1StringView iconForMime(CacheFile *cacheFile, int posListOffset, QStringView inputMime); void loadMimeTypeList(); bool checkCacheChanged(); diff --git a/src/corelib/platform/wasm/qstdweb.cpp b/src/corelib/platform/wasm/qstdweb.cpp index d1fff5388c6..287138bb915 100644 --- a/src/corelib/platform/wasm/qstdweb.cpp +++ b/src/corelib/platform/wasm/qstdweb.cpp @@ -177,15 +177,13 @@ Blob Blob::slice(uint32_t begin, uint32_t end) const ArrayBuffer Blob::arrayBuffer_sync() const { - QEventLoop loop; emscripten::val buffer; - qstdweb::Promise::make(m_blob, QStringLiteral("arrayBuffer"), { - .thenFunc = [&loop, &buffer](emscripten::val arrayBuffer) { + uint32_t handlerIndex = qstdweb::Promise::make(m_blob, QStringLiteral("arrayBuffer"), { + .thenFunc = [&buffer](emscripten::val arrayBuffer) { buffer = arrayBuffer; - loop.quit(); } }); - loop.exec(); + Promise::suspendExclusive(handlerIndex); return ArrayBuffer(buffer); } @@ -443,7 +441,7 @@ EventCallback::EventCallback(emscripten::val element, const std::string &name, } -void Promise::adoptPromise(emscripten::val promise, PromiseCallbacks callbacks) +uint32_t Promise::adoptPromise(emscripten::val promise, PromiseCallbacks callbacks) { Q_ASSERT_X(!!callbacks.catchFunc || !!callbacks.finallyFunc || !!callbacks.thenFunc, "Promise::adoptPromise", "must provide at least one callback function"); @@ -499,13 +497,23 @@ void Promise::adoptPromise(emscripten::val promise, PromiseCallbacks callbacks) promise = promise.call<emscripten::val>("finally", suspendResume->jsEventHandlerAt(*finallyIndex)); + + return *finallyIndex; +} + +void Promise::suspendExclusive(uint32_t handlerIndex) +{ + QWasmSuspendResumeControl *suspendResume = QWasmSuspendResumeControl::get(); + Q_ASSERT(suspendResume); + suspendResume->suspendExclusive(handlerIndex); + suspendResume->sendPendingEvents(); } void Promise::all(std::vector<emscripten::val> promises, PromiseCallbacks callbacks) { auto arr = emscripten::val::array(promises); auto all = val::global("Promise").call<emscripten::val>("all", arr); - return adoptPromise(all, callbacks); + adoptPromise(all, callbacks); } // Asyncify and thread blocking: Normally, it's not possible to block the main @@ -630,6 +638,249 @@ qint64 Uint8ArrayIODevice::writeData(const char *data, qint64 maxSize) return size; } +FileSystemWritableFileStreamIODevice::FileSystemWritableFileStreamIODevice(FileSystemWritableFileStream stream) + : m_stream(std::move(stream)) +{ +} + +bool FileSystemWritableFileStreamIODevice::open(QIODevice::OpenMode mode) +{ + if (mode.testFlag(QIODevice::ReadOnly)) + return false; + return QIODevice::open(mode); +} + +void FileSystemWritableFileStreamIODevice::close() +{ + if (!isOpen()) { + QIODevice::close(); + return; + } + + uint32_t handlerIndex = Promise::make(m_stream.val(), QStringLiteral("close"), { + .thenFunc = [](emscripten::val) { + } + }); + Promise::suspendExclusive(handlerIndex); + + QIODevice::close(); +} + +bool FileSystemWritableFileStreamIODevice::isSequential() const +{ + return false; +} + +qint64 FileSystemWritableFileStreamIODevice::size() const +{ + return m_size; +} + +bool FileSystemWritableFileStreamIODevice::seek(qint64 pos) +{ + bool success = false; + + 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"), { + .thenFunc = [&success](emscripten::val) { + success = true; + }, + .catchFunc = [](emscripten::val) { + } + }, seekParams); + Promise::suspendExclusive(handlerIndex); + + if (!success) + return false; + + return QIODevice::seek(pos); +} + +qint64 FileSystemWritableFileStreamIODevice::readData(char *, qint64) +{ + Q_UNREACHABLE(); +} + +qint64 FileSystemWritableFileStreamIODevice::writeData(const char *data, qint64 size) +{ + bool success = false; + + Uint8Array array = Uint8Array::copyFrom(data, size); + uint32_t handlerIndex = Promise::make(m_stream.val(), QStringLiteral("write"), { + .thenFunc = [&success](emscripten::val) { + success = true; + }, + .catchFunc = [](emscripten::val) { + } + }, array.val()); + Promise::suspendExclusive(handlerIndex); + + if (success) { + qint64 newPos = pos() + size; + m_size = std::max(m_size, newPos); + return size; + } + return -1; +} + +FileSystemWritableFileStream::FileSystemWritableFileStream(const emscripten::val &writableStream) + : m_writableStream(writableStream) +{ +} + +emscripten::val FileSystemWritableFileStream::val() const +{ + return m_writableStream; +} + +FileSystemFileHandle::FileSystemFileHandle(const emscripten::val &fileHandle) + : m_fileHandle(fileHandle) +{ +} + +std::string FileSystemFileHandle::name() const +{ + return m_fileHandle["name"].as<std::string>(); +} + +std::string FileSystemFileHandle::kind() const +{ + return m_fileHandle["kind"].as<std::string>(); +} + +emscripten::val FileSystemFileHandle::val() const +{ + return m_fileHandle; +} + +FileSystemFileIODevice::FileSystemFileIODevice(FileSystemFileHandle fileHandle) + : m_fileHandle(fileHandle) +{ +} + +bool FileSystemFileIODevice::open(QIODevice::OpenMode mode) +{ + if (isOpen()) + return false; + + // Read mode: get the File and create a BlobIODevice + if (mode & QIODevice::ReadOnly) { + File file; + bool success = false; + + uint32_t handlerIndex = Promise::make(m_fileHandle.val(), QStringLiteral("getFile"), { + .thenFunc = [&file, &success](emscripten::val fileVal) { + file = File(fileVal); + success = true; + }, + .catchFunc = [](emscripten::val) { + } + }); + Promise::suspendExclusive(handlerIndex); + + if (success) { + m_blobDevice = std::make_unique<BlobIODevice>(file.slice(0, file.size())); + m_size = file.size(); + + if (!m_blobDevice->open(mode)) + return false; + } else { + return false; + } + } + + // Write mode: create a writable stream + if (mode & QIODevice::WriteOnly) { + FileSystemWritableFileStream writableStream; + bool success = false; + + uint32_t handlerIndex = Promise::make(m_fileHandle.val(), QStringLiteral("createWritable"), { + .thenFunc = [&writableStream, &success](emscripten::val writable) { + writableStream = FileSystemWritableFileStream(writable); + success = true; + }, + .catchFunc = [](emscripten::val) { + } + }); + Promise::suspendExclusive(handlerIndex); + + if (success) { + m_writableDevice = std::make_unique<FileSystemWritableFileStreamIODevice>(writableStream); + if (!m_writableDevice->open(mode)) + return false; + } else { + return false; + } + } + + return QIODevice::open(mode); +} + +void FileSystemFileIODevice::close() +{ + if (!isOpen()) { + QIODevice::close(); + return; + } + + if (m_writableDevice) { + m_writableDevice->close(); + m_writableDevice.reset(); + } + if (m_blobDevice) { + m_blobDevice->close(); + m_blobDevice.reset(); + } + + QIODevice::close(); +} + +bool FileSystemFileIODevice::isSequential() const +{ + return false; +} + +qint64 FileSystemFileIODevice::size() const +{ + return m_size; +} + +bool FileSystemFileIODevice::seek(qint64 pos) +{ + if (m_blobDevice) { + if (!m_blobDevice->seek(pos)) + return false; + } + if (m_writableDevice) { + if (!m_writableDevice->seek(pos)) + return false; + } + return QIODevice::seek(pos); +} + +qint64 FileSystemFileIODevice::readData(char *data, qint64 maxSize) +{ + if (!m_blobDevice) + return -1; + + return m_blobDevice->read(data, maxSize); +} + +qint64 FileSystemFileIODevice::writeData(const char *data, qint64 size) +{ + if (!m_writableDevice) + return -1; + + qint64 written = m_writableDevice->write(data, size); + if (written > 0) { + qint64 newPos = pos() + written; + m_size = std::max(m_size, newPos); + } + return written; +} + } // namespace qstdweb QT_END_NAMESPACE diff --git a/src/corelib/platform/wasm/qstdweb_p.h b/src/corelib/platform/wasm/qstdweb_p.h index 711751f65ab..9a97370448e 100644 --- a/src/corelib/platform/wasm/qstdweb_p.h +++ b/src/corelib/platform/wasm/qstdweb_p.h @@ -195,6 +195,30 @@ namespace qstdweb { emscripten::val m_uint8Array = emscripten::val::undefined(); }; + class Q_CORE_EXPORT FileSystemWritableFileStream { + public: + FileSystemWritableFileStream() = default; + explicit FileSystemWritableFileStream(const emscripten::val &writableStream); + emscripten::val val() const; + + private: + emscripten::val m_writableStream = emscripten::val::undefined(); + }; + + class Q_CORE_EXPORT FileSystemFileHandle { + public: + FileSystemFileHandle() = default; + explicit FileSystemFileHandle(const emscripten::val &fileHandle); + + std::string name() const; + std::string kind() const; + + emscripten::val val() const; + + private: + emscripten::val m_fileHandle = emscripten::val::undefined(); + }; + // EventCallback here for source compatibility; prefer using QWasmEventHandler directly class Q_CORE_EXPORT EventCallback : public QWasmEventHandler { @@ -214,13 +238,13 @@ namespace qstdweb { }; namespace Promise { - void Q_CORE_EXPORT adoptPromise(emscripten::val promise, PromiseCallbacks callbacks); + uint32_t Q_CORE_EXPORT adoptPromise(emscripten::val promise, PromiseCallbacks callbacks); template<typename... Args> - void make(emscripten::val target, - QString methodName, - PromiseCallbacks callbacks, - Args... args) + uint32_t make(emscripten::val target, + QString methodName, + PromiseCallbacks callbacks, + Args... args) { emscripten::val promiseObject = target.call<emscripten::val>( methodName.toStdString().c_str(), std::forward<Args>(args)...); @@ -228,9 +252,10 @@ namespace qstdweb { qFatal("This function did not return a promise"); } - adoptPromise(std::move(promiseObject), std::move(callbacks)); + return adoptPromise(std::move(promiseObject), std::move(callbacks)); } + void Q_CORE_EXPORT suspendExclusive(uint32_t handlerIndex); void Q_CORE_EXPORT all(std::vector<emscripten::val> promises, PromiseCallbacks callbacks); }; @@ -274,6 +299,46 @@ namespace qstdweb { Uint8Array m_array; }; + class Q_CORE_EXPORT FileSystemWritableFileStreamIODevice: public QIODevice + { + public: + FileSystemWritableFileStreamIODevice(FileSystemWritableFileStream stream); + bool open(QIODevice::OpenMode mode) override; + void close() override; + bool isSequential() const override; + qint64 size() const override; + bool seek(qint64 pos) override; + + protected: + qint64 readData(char *data, qint64 maxSize) override; + qint64 writeData(const char *data, qint64 size) override; + + private: + FileSystemWritableFileStream m_stream; + qint64 m_size = 0; + }; + + class Q_CORE_EXPORT FileSystemFileIODevice: public QIODevice + { + public: + FileSystemFileIODevice(FileSystemFileHandle fileHandle); + bool open(QIODevice::OpenMode mode) override; + void close() override; + bool isSequential() const override; + qint64 size() const override; + bool seek(qint64 pos) override; + + protected: + qint64 readData(char *data, qint64 maxSize) override; + qint64 writeData(const char *data, qint64 size) override; + + private: + FileSystemFileHandle m_fileHandle; + std::unique_ptr<BlobIODevice> m_blobDevice; + std::unique_ptr<FileSystemWritableFileStreamIODevice> m_writableDevice; + qint64 m_size = 0; + }; + inline emscripten::val window() { static emscripten::val savedWindow = emscripten::val::global("window"); diff --git a/src/corelib/platform/wasm/qwasmsuspendresumecontrol.cpp b/src/corelib/platform/wasm/qwasmsuspendresumecontrol.cpp index 961bbb53e54..093898c520a 100644 --- a/src/corelib/platform/wasm/qwasmsuspendresumecontrol.cpp +++ b/src/corelib/platform/wasm/qwasmsuspendresumecontrol.cpp @@ -49,6 +49,7 @@ void qtSuspendResumeControlClearJs() { asyncifyEnabled: false, // asyncify 1 or JSPI enabled eventHandlers: {}, pendingEvents: [], + exclusiveEventHandler: 0, }); }); } @@ -118,7 +119,15 @@ void qtRegisterEventHandlerJs(int index) { }); // Handle the event based on instance state and asyncify flag - if (control.resume) { + if (control.exclusiveEventHandler > 0) { + // In exclusive mode, resume on exclusive event handler match only + if (index != control.exclusiveEventHandler) + return; + + const resume = control.resume; + control.resume = null; + resume(); + } else if (control.resume) { // The instance is suspended in processEvents(), resume and process the event const resume = control.resume; control.resume = null; @@ -148,14 +157,12 @@ QWasmSuspendResumeControl::QWasmSuspendResumeControl() #endif qtSuspendResumeControlClearJs(); suspendResumeControlJs().set("asyncifyEnabled", qstdweb::haveAsyncify()); - Q_ASSERT(!QWasmSuspendResumeControl::s_suspendResumeControl); QWasmSuspendResumeControl::s_suspendResumeControl = this; } QWasmSuspendResumeControl::~QWasmSuspendResumeControl() { qtSuspendResumeControlClearJs(); - Q_ASSERT(QWasmSuspendResumeControl::s_suspendResumeControl); QWasmSuspendResumeControl::s_suspendResumeControl = nullptr; } @@ -199,13 +206,24 @@ void QWasmSuspendResumeControl::suspend() qtSuspendJs(); } -// Sends any pending events. Returns true if an event was sent, false otherwise. +void QWasmSuspendResumeControl::suspendExclusive(uint32_t eventHandlerIndex) +{ + suspendResumeControlJs().set("exclusiveEventHandler", eventHandlerIndex); + qtSuspendJs(); +} + +// Sends any pending events. Returns the number of sent events. int QWasmSuspendResumeControl::sendPendingEvents() { #if QT_CONFIG(thread) Q_ASSERT(emscripten_is_main_runtime_thread()); #endif - emscripten::val pendingEvents = suspendResumeControlJs()["pendingEvents"]; + 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; @@ -214,13 +232,28 @@ int QWasmSuspendResumeControl::sendPendingEvents() // 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>()); - Q_ASSERT(it != m_eventHandlers.end()); - it->second(event["arg"]); + 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; +} + void qtSendPendingEvents() { if (QWasmSuspendResumeControl::s_suspendResumeControl) diff --git a/src/corelib/platform/wasm/qwasmsuspendresumecontrol_p.h b/src/corelib/platform/wasm/qwasmsuspendresumecontrol_p.h index 2b9962e4be1..37c71ed8123 100644 --- a/src/corelib/platform/wasm/qwasmsuspendresumecontrol_p.h +++ b/src/corelib/platform/wasm/qwasmsuspendresumecontrol_p.h @@ -38,7 +38,9 @@ public: static emscripten::val suspendResumeControlJs(); void suspend(); + void suspendExclusive(uint32_t eventHandlerIndex); int sendPendingEvents(); + int sendPendingExclusiveEvent(); private: friend void qtSendPendingEvents(); diff --git a/src/corelib/plugin/qlibrary.h b/src/corelib/plugin/qlibrary.h index f31047b214a..95c14178b22 100644 --- a/src/corelib/plugin/qlibrary.h +++ b/src/corelib/plugin/qlibrary.h @@ -63,6 +63,7 @@ private: Loaded }; + friend class QLibraryPrivate; QTaggedPointer<QLibraryPrivate, LoadStatusTag> d = nullptr; Q_DISABLE_COPY(QLibrary) }; diff --git a/src/corelib/plugin/qlibrary_p.h b/src/corelib/plugin/qlibrary_p.h index b4e29a79e28..2331c86d844 100644 --- a/src/corelib/plugin/qlibrary_p.h +++ b/src/corelib/plugin/qlibrary_p.h @@ -67,7 +67,7 @@ public: bool load(); QtPluginInstanceFunction loadPlugin(); // loads and resolves instance - bool unload(UnloadFlag flag = UnloadSys); + Q_AUTOTEST_EXPORT bool unload(UnloadFlag flag = UnloadSys); void release(); QFunctionPointer resolve(const char *); @@ -103,6 +103,11 @@ public: void updatePluginState(); bool isPlugin(); + static QLibraryPrivate* get(QLibrary* lib) + { + return lib->d.data(); + } + private: explicit QLibraryPrivate(const QString &canonicalFileName, const QString &version, QLibrary::LoadHints loadHints); ~QLibraryPrivate(); diff --git a/src/corelib/text/qstring.cpp b/src/corelib/text/qstring.cpp index 711b70ebf8f..c01c1a9999d 100644 --- a/src/corelib/text/qstring.cpp +++ b/src/corelib/text/qstring.cpp @@ -3681,95 +3681,24 @@ QString &QString::remove(QChar ch, Qt::CaseSensitivity cs) \sa remove() */ - -/*! \internal - Instead of detaching, or reallocating if "before" is shorter than "after" - and there isn't enough capacity, create a new string, copy characters to it - as needed, then swap it with "str". -*/ -static void replace_with_copy(QString &str, QSpan<size_t> indices, qsizetype blen, - QStringView after) -{ - const qsizetype alen = after.size(); - const char16_t *after_b = after.utf16(); - - const QString::DataPointer &str_d = str.data_ptr(); - auto src_start = str_d.begin(); - const qsizetype newSize = str_d.size + indices.size() * (alen - blen); - QString copy{ newSize, Qt::Uninitialized }; - QString::DataPointer ©_d = copy.data_ptr(); - auto dst = copy_d.begin(); - for (size_t index : indices) { - auto hit = str_d.begin() + index; - dst = std::copy(src_start, hit, dst); - dst = std::copy_n(after_b, alen, dst); - src_start = hit + blen; - } - dst = std::copy(src_start, str_d.end(), dst); - str.swap(copy); -} - -// No detaching or reallocation is needed -static void replace_in_place(QString &str, QSpan<size_t> indices, - qsizetype blen, QStringView after) -{ - const qsizetype alen = after.size(); - const char16_t *after_b = after.utf16(); - const char16_t *after_e = after.utf16() + after.size(); - - if (blen == alen) { // Replace in place - for (size_t index : indices) - std::copy_n(after_b, alen, str.data_ptr().begin() + index); - } else if (blen > alen) { // Replace from front - char16_t *begin = str.data_ptr().begin(); - char16_t *hit = begin + indices.front(); - char16_t *to = hit; - to = std::copy_n(after_b, alen, to); - char16_t *movestart = hit + blen; - for (size_t index : indices.sliced(1)) { - hit = begin + index; - to = std::move(movestart, hit, to); - to = std::copy_n(after_b, alen, to); - movestart = hit + blen; - } - to = std::move(movestart, str.data_ptr().end(), to); - str.resize(std::distance(begin, to)); - } else { // blen < alen, Replace from back - const qsizetype oldSize = str.data_ptr().size; - const qsizetype adjust = indices.size() * (alen - blen); - const qsizetype newSize = oldSize + adjust; - - str.resize(newSize); - char16_t *begin = str.data_ptr().begin(); - char16_t *moveend = begin + oldSize; - char16_t *to = str.data_ptr().end(); - - for (auto it = indices.rbegin(), end = indices.rend(); it != end; ++it) { - char16_t *hit = begin + *it; - char16_t *movestart = hit + blen; - to = std::move_backward(movestart, moveend, to); - to = std::copy_backward(after_b, after_e, to); - moveend = hit; - } - } -} - -static void replace_helper(QString &str, QSpan<size_t> indices, qsizetype blen, QStringView after) +static void replace_helper(QString &str, QSpan<qsizetype> indices, qsizetype blen, QStringView after) { const qsizetype oldSize = str.data_ptr().size; const qsizetype adjust = indices.size() * (after.size() - blen); const qsizetype newSize = oldSize + adjust; + using A = QStringAlgorithms<QString>; if (str.data_ptr().needsDetach() || needsReallocate(str, newSize)) { - replace_with_copy(str, indices, blen, after); + A::replace_helper(str, blen, after, indices); return; } - if (QtPrivate::q_points_into_range(after.begin(), str)) + if (QtPrivate::q_points_into_range(after.begin(), str)) { // Copy after if it lies inside our own d.b area (which we could // possibly invalidate via a realloc or modify by replacement) - replace_in_place(str, indices, blen, QVarLengthArray(after.begin(), after.end())); - else - replace_in_place(str, indices, blen, after); + A::replace_helper(str, blen, QVarLengthArray(after.begin(), after.end()), indices); + } else { + A::replace_helper(str, blen, after, indices); + } } /*! @@ -3811,8 +3740,8 @@ QString &QString::replace(qsizetype pos, qsizetype len, const QChar *after, qsiz if (len > this->size() - pos) len = this->size() - pos; - size_t index = pos; - replace_helper(*this, QSpan(&index, 1), len, QStringView{after, alen}); + qsizetype indices[] = {pos}; + replace_helper(*this, indices, len, QStringView{after, alen}); return *this; } @@ -3890,7 +3819,7 @@ QString &QString::replace(const QChar *before, qsizetype blen, qsizetype index = 0; - QVarLengthArray<size_t> indices; + QVarLengthArray<qsizetype> indices; while ((index = matcher.indexIn(*this, index)) != -1) { indices.push_back(index); if (blen) // Step over before: @@ -3925,7 +3854,7 @@ QString& QString::replace(QChar ch, const QString &after, Qt::CaseSensitivity cs const char16_t cc = (cs == Qt::CaseSensitive ? ch.unicode() : ch.toCaseFolded().unicode()); - QVarLengthArray<size_t> indices; + QVarLengthArray<qsizetype> indices; if (cs == Qt::CaseSensitive) { const char16_t *begin = d.begin(); const char16_t *end = d.end(); diff --git a/src/corelib/thread/qthread.h b/src/corelib/thread/qthread.h index bb70678a958..6e4e532fd7d 100644 --- a/src/corelib/thread/qthread.h +++ b/src/corelib/thread/qthread.h @@ -172,27 +172,15 @@ inline Qt::HANDLE QThread::currentThreadId() noexcept #elif defined(Q_PROCESSOR_X86_64) && ((defined(Q_OS_LINUX) && defined(__GLIBC__)) || defined(Q_OS_FREEBSD)) // x86_64 Linux, BSD uses FS __asm__("mov %%fs:%c1, %0" : "=r" (tid) : "i" (2 * sizeof(void*)) : ); -#elif defined(Q_PROCESSOR_X86_64) && defined(Q_OS_WIN) +#elif defined(Q_PROCESSOR_X86_64) && defined(Q_OS_WIN) && defined(Q_CC_MSVC) // See https://en.wikipedia.org/wiki/Win32_Thread_Information_Block - // First get the pointer to the TIB - quint8 *tib; -# if defined(Q_CC_MINGW) // internal compiler error when using the intrinsics - __asm__("movq %%gs:0x30, %0" : "=r" (tib) : :); -# else - tib = reinterpret_cast<quint8 *>(__readgsqword(0x30)); -# endif - // Then read the thread ID - tid = *reinterpret_cast<Qt::HANDLE *>(tib + 0x48); -#elif defined(Q_PROCESSOR_X86_32) && defined(Q_OS_WIN) - // First get the pointer to the TIB - quint8 *tib; -# if defined(Q_CC_MINGW) // internal compiler error when using the intrinsics - __asm__("movl %%fs:0x18, %0" : "=r" (tib) : :); -# else - tib = reinterpret_cast<quint8 *>(__readfsdword(0x18)); -# endif - // Then read the thread ID - tid = *reinterpret_cast<Qt::HANDLE *>(tib + 0x24); + tid = reinterpret_cast<Qt::HANDLE>(__readgsqword(0x48)); +#elif defined(Q_PROCESSOR_X86_64) && defined(Q_OS_WIN) // !Q_CC_MSVC + __asm__("mov %%gs:0x48, %0" : "=r" (tid)); +#elif defined(Q_PROCESSOR_X86_32) && defined(Q_OS_WIN) && defined(Q_CC_MSVC) + tid = reinterpret_cast<Qt::HANDLE>(__readfsdword(0x24)); +#elif defined(Q_PROCESSOR_X86_32) && defined(Q_OS_WIN) // !Q_CC_MSVC + __asm__("mov %%fs:0x24, %0" : "=r" (tid)); #else #undef QT_HAS_FAST_CURRENT_THREAD_ID tid = currentThreadIdImpl(); diff --git a/src/corelib/time/qdatetime.cpp b/src/corelib/time/qdatetime.cpp index 03eeed84465..974c486b915 100644 --- a/src/corelib/time/qdatetime.cpp +++ b/src/corelib/time/qdatetime.cpp @@ -517,6 +517,7 @@ QDate::QDate(int y, int m, int d, QCalendar cal) */ /*! + \overload primary \fn bool QDate::isValid() const Returns \c true if this date is valid; otherwise returns \c false. @@ -525,6 +526,8 @@ QDate::QDate(int y, int m, int d, QCalendar cal) */ /*! + \overload primary + Returns the year of this date. Uses \a cal as calendar, if supplied, else the Gregorian calendar. @@ -557,8 +560,8 @@ int QDate::year(QCalendar cal) const } /*! - \overload - */ + \overload year() +*/ int QDate::year() const { @@ -571,6 +574,8 @@ int QDate::year() const } /*! + \overload primary + Returns the month-number for the date. Numbers the months of the year starting with 1 for the first. Uses \a cal @@ -609,8 +614,8 @@ int QDate::month(QCalendar cal) const } /*! - \overload - */ + \overload month() +*/ int QDate::month() const { @@ -623,6 +628,8 @@ int QDate::month() const } /*! + \overload primary + Returns the day of the month for this date. Uses \a cal as calendar if supplied, else the Gregorian calendar (for which @@ -642,8 +649,8 @@ int QDate::day(QCalendar cal) const } /*! - \overload - */ + \overload day() +*/ int QDate::day() const { @@ -656,6 +663,8 @@ int QDate::day() const } /*! + \overload primary + Returns the weekday (1 = Monday to 7 = Sunday) for this date. Uses \a cal as calendar if supplied, else the Gregorian calendar. Returns 0 @@ -674,8 +683,8 @@ int QDate::dayOfWeek(QCalendar cal) const } /*! - \overload - */ + \overload dayOfWeek() +*/ int QDate::dayOfWeek() const { @@ -683,6 +692,8 @@ int QDate::dayOfWeek() const } /*! + \overload primary + Returns the day of the year (1 for the first day) for this date. Uses \a cal as calendar if supplied, else the Gregorian calendar. @@ -702,8 +713,8 @@ int QDate::dayOfYear(QCalendar cal) const } /*! - \overload - */ + \overload dayOfYear() +*/ int QDate::dayOfYear() const { @@ -715,6 +726,8 @@ int QDate::dayOfYear() const } /*! + \overload primary + Returns the number of days in the month for this date. Uses \a cal as calendar if supplied, else the Gregorian calendar (for which @@ -735,8 +748,8 @@ int QDate::daysInMonth(QCalendar cal) const } /*! - \overload - */ + \overload daysInMonth() +*/ int QDate::daysInMonth() const { @@ -749,6 +762,8 @@ int QDate::daysInMonth() const } /*! + \overload primary + Returns the number of days in the year for this date. Uses \a cal as calendar if supplied, else the Gregorian calendar (for which @@ -766,8 +781,8 @@ int QDate::daysInYear(QCalendar cal) const } /*! - \overload - */ + \overload daysInYear() +*/ int QDate::daysInYear() const { @@ -918,6 +933,7 @@ static QDateTime toEarliest(QDate day, const QTimeZone &zone) /*! \since 5.14 + \overload primary Returns the start-moment of the day. @@ -972,8 +988,8 @@ QDateTime QDate::startOfDay(const QTimeZone &zone) const } /*! - \overload \since 6.5 + \overload startOfDay() */ QDateTime QDate::startOfDay() const { @@ -982,8 +998,8 @@ QDateTime QDate::startOfDay() const #if QT_DEPRECATED_SINCE(6, 9) /*! - \overload \since 5.14 + \overload startOfDay() \deprecated [6.9] Use \c{startOfDay(const QTimeZone &)} instead. Returns the start-moment of the day. @@ -1073,6 +1089,7 @@ static QDateTime toLatest(QDate day, const QTimeZone &zone) /*! \since 5.14 + \overload primary Returns the end-moment of the day. @@ -1127,8 +1144,8 @@ QDateTime QDate::endOfDay(const QTimeZone &zone) const } /*! - \overload \since 6.5 + \overload endOfDay() */ QDateTime QDate::endOfDay() const { @@ -1137,8 +1154,8 @@ QDateTime QDate::endOfDay() const #if QT_DEPRECATED_SINCE(6, 9) /*! - \overload \since 5.14 + \overload endOfDay() \deprecated [6.9] Use \c{endOfDay(const QTimeZone &) instead. Returns the end-moment of the day. @@ -1199,7 +1216,7 @@ static QString toStringIsoDate(QDate date) } /*! - \overload + \overload toString() Returns the date as a string. The \a format parameter determines the format of the string. @@ -1245,9 +1262,10 @@ QString QDate::toString(Qt::DateFormat format) const } /*! + \since 5.14 + \overload primary \fn QString QDate::toString(const QString &format, QCalendar cal) const \fn QString QDate::toString(QStringView format, QCalendar cal) const - \since 5.14 Returns the date as a string. The \a format parameter determines the format of the result string. If \a cal is supplied, it determines the calendar used @@ -1313,8 +1331,8 @@ QString QDate::toString(QStringView format, QCalendar cal) const // Out-of-line no-calendar overloads, since QCalendar is a non-trivial type /*! - \overload \since 5.10 + \overload toString() */ QString QDate::toString(QStringView format) const { @@ -1322,8 +1340,8 @@ QString QDate::toString(QStringView format) const } /*! - \overload \since 4.6 + \overload toString() */ QString QDate::toString(const QString &format) const { @@ -1414,9 +1432,8 @@ QDate QDate::addDays(qint64 ndays) const } /*! - \fn QDate QDate::addDuration(std::chrono::days ndays) const - \since 6.4 + \fn QDate QDate::addDuration(std::chrono::days ndays) const Returns a QDate object containing a date \a ndays later than the date of this object (or earlier if \a ndays is negative). @@ -1436,6 +1453,8 @@ QDate QDate::addDays(qint64 ndays) const */ /*! + \overload primary + Returns a QDate object containing a date \a nmonths later than the date of this object (or earlier if \a nmonths is negative). @@ -1477,7 +1496,7 @@ QDate QDate::addMonths(int nmonths, QCalendar cal) const } /*! - \overload + \overload addMonths() */ QDate QDate::addMonths(int nmonths) const @@ -1509,6 +1528,8 @@ QDate QDate::addMonths(int nmonths) const } /*! + \overload primary + Returns a QDate object containing a date \a nyears later than the date of this object (or earlier if \a nyears is negative). @@ -1542,7 +1563,7 @@ QDate QDate::addYears(int nyears, QCalendar cal) const } /*! - \overload + \overload addYears() */ QDate QDate::addYears(int nyears) const @@ -1638,6 +1659,7 @@ qint64 QDate::daysTo(QDate d) const #if QT_CONFIG(datestring) // depends on, so implies, textdate /*! + \overload \fn QDate QDate::fromString(const QString &string, Qt::DateFormat format) Returns the QDate represented by the \a string, using the @@ -1651,8 +1673,8 @@ qint64 QDate::daysTo(QDate d) const */ /*! - \overload \since 6.0 + \overload fromString() */ QDate QDate::fromString(QStringView string, Qt::DateFormat format) { @@ -1702,6 +1724,7 @@ QDate QDate::fromString(QStringView string, Qt::DateFormat format) } /*! + \overload primary \fn QDate QDate::fromString(const QString &string, const QString &format, int baseYear, QCalendar cal) Returns the QDate represented by the \a string, using the \a @@ -1833,14 +1856,14 @@ QDate QDate::fromString(QStringView string, Qt::DateFormat format) */ /*! - \fn QDate QDate::fromString(QStringView string, QStringView format, QCalendar cal) - \overload \since 6.0 + \overload fromString() + \fn QDate QDate::fromString(QStringView string, QStringView format, QCalendar cal) */ /*! - \overload \since 6.0 + \overload fromString() */ QDate QDate::fromString(const QString &string, QStringView format, int baseYear, QCalendar cal) { @@ -1860,34 +1883,34 @@ QDate QDate::fromString(const QString &string, QStringView format, int baseYear, } /*! - \fn QDate QDate::fromString(const QString &string, const QString &format, QCalendar cal) - \overload \since 5.14 + \overload fromString() + \fn QDate QDate::fromString(const QString &string, const QString &format, QCalendar cal) */ /*! - \fn QDate QDate::fromString(const QString &string, QStringView format, QCalendar cal) - \overload \since 6.0 + \overload fromString() + \fn QDate QDate::fromString(const QString &string, QStringView format, QCalendar cal) */ /*! - \fn QDate QDate::fromString(QStringView string, QStringView format, int baseYear, QCalendar cal) - \overload \since 6.7 + \overload fromString() + \fn QDate QDate::fromString(QStringView string, QStringView format, int baseYear, QCalendar cal) */ /*! - \fn QDate QDate::fromString(QStringView string, QStringView format, int baseYear) - \overload \since 6.7 + \overload fromString() + \fn QDate QDate::fromString(QStringView string, QStringView format, int baseYear) Uses a default-constructed QCalendar. */ /*! - \overload \since 6.7 + \overload fromString() Uses a default-constructed QCalendar. */ @@ -1897,16 +1920,16 @@ QDate QDate::fromString(const QString &string, QStringView format, int baseYear) } /*! - \fn QDate QDate::fromString(const QString &string, const QString &format, int baseYear) - \overload \since 6.7 + \overload fromString() + \fn QDate QDate::fromString(const QString &string, const QString &format, int baseYear) Uses a default-constructed QCalendar. */ #endif // datestring /*! - \overload + \overload isValid() Returns \c true if the specified date (\a year, \a month, and \a day) is valid in the Gregorian calendar; otherwise returns \c false. @@ -2037,6 +2060,8 @@ QTime::QTime(int h, int m, int s, int ms) */ /*! + \overload primary + Returns \c true if the time is valid; otherwise returns \c false. For example, the time 23:30:55.746 is valid, but 24:12:30 is invalid. @@ -2115,7 +2140,7 @@ int QTime::msec() const #if QT_CONFIG(datestring) // depends on, so implies, textdate /*! - \overload + \overload toString() Returns the time as a string. The \a format parameter determines the format of the string. @@ -2155,6 +2180,7 @@ QString QTime::toString(Qt::DateFormat format) const } /*! + \overload primary \fn QString QTime::toString(const QString &format) const \fn QString QTime::toString(QStringView format) const @@ -2261,11 +2287,11 @@ QString QTime::toString(Qt::DateFormat format) const \sa fromString(), QDate::toString(), QDateTime::toString(), QLocale::toString() */ -// ### Qt 7 The 't' format specifiers should be specific to QDateTime (compare fromString). QString QTime::toString(QStringView format) const { return QLocale::c().toString(*this, format); } +// ### Qt 7 The 't' format specifiers should be specific to QDateTime (compare fromString). #endif // datestring /*! @@ -2557,6 +2583,7 @@ static QTime fromIsoTimeString(QStringView string, Qt::DateFormat format, bool * } /*! + \overload \fn QTime QTime::fromString(const QString &string, Qt::DateFormat format) Returns the time represented in the \a string as a QTime using the @@ -2566,8 +2593,8 @@ static QTime fromIsoTimeString(QStringView string, Qt::DateFormat format, bool * */ /*! - \overload \since 6.0 + \overload fromString() */ QTime QTime::fromString(QStringView string, Qt::DateFormat format) { @@ -2586,6 +2613,7 @@ QTime QTime::fromString(QStringView string, Qt::DateFormat format) } /*! + \overload primary \fn QTime QTime::fromString(const QString &string, const QString &format) Returns the QTime represented by the \a string, using the \a @@ -2664,14 +2692,14 @@ QTime QTime::fromString(QStringView string, Qt::DateFormat format) */ /*! - \fn QTime QTime::fromString(QStringView string, QStringView format) - \overload \since 6.0 + \overload fromString() + \fn QTime QTime::fromString(QStringView string, QStringView format) */ /*! - \overload \since 6.0 + \overload fromString() */ QTime QTime::fromString(const QString &string, QStringView format) { @@ -2691,7 +2719,7 @@ QTime QTime::fromString(const QString &string, QStringView format) /*! - \overload + \overload isValid() Returns \c true if the specified time is valid; otherwise returns false. @@ -4012,6 +4040,7 @@ QDateTime::QDateTime(QDate date, QTime time, Qt::TimeSpec spec, int offsetSecond /*! \since 5.2 + \overload primary Constructs a datetime with the given \a date and \a time, using the time representation described by \a timeZone. @@ -4620,7 +4649,7 @@ void QDateTime::setSecsSinceEpoch(qint64 secs) #if QT_CONFIG(datestring) // depends on, so implies, textdate /*! - \overload + \overload toString() Returns the datetime as a string in the \a format given. @@ -4712,9 +4741,10 @@ QString QDateTime::toString(Qt::DateFormat format) const } /*! + \since 5.14 + \overload primary \fn QString QDateTime::toString(const QString &format, QCalendar cal) const \fn QString QDateTime::toString(QStringView format, QCalendar cal) const - \since 5.14 Returns the datetime as a string. The \a format parameter determines the format of the result string. If \a cal is supplied, it determines the @@ -4761,8 +4791,8 @@ QString QDateTime::toString(QStringView format, QCalendar cal) const // Out-of-line no-calendar overloads, since QCalendar is a non-trivial type /*! - \overload \since 5.10 + \overload toString() */ QString QDateTime::toString(QStringView format) const { @@ -4770,8 +4800,8 @@ QString QDateTime::toString(QStringView format) const } /*! - \overload \since 4.6 + \overload toString() */ QString QDateTime::toString(const QString &format) const { @@ -5370,6 +5400,7 @@ Qt::weak_ordering compareThreeWay(const QDateTime &lhs, const QDateTime &rhs) /*! \since 6.5 + \overload primary \fn QDateTime QDateTime::currentDateTime(const QTimeZone &zone) Returns the system clock's current datetime, using the time representation @@ -5379,8 +5410,8 @@ Qt::weak_ordering compareThreeWay(const QDateTime &lhs, const QDateTime &rhs) */ /*! - \overload \since 0.90 + \overload currentDateTime() */ QDateTime QDateTime::currentDateTime() { @@ -5426,8 +5457,9 @@ QDateTime QDateTime::currentDateTimeUtc() */ /*! - \fn template <typename Clock, typename Duration> QDateTime QDateTime::fromStdTimePoint(const std::chrono::time_point<Clock, Duration> &time) \since 6.4 + \overload primary + \fn template <typename Clock, typename Duration> QDateTime QDateTime::fromStdTimePoint(const std::chrono::time_point<Clock, Duration> &time) Constructs a datetime representing the same point in time as \a time, using Qt::UTC as its time representation. @@ -5451,7 +5483,7 @@ QDateTime QDateTime::currentDateTimeUtc() /*! \since 6.4 - \overload + \overload fromStdTimePoint() Constructs a datetime representing the same point in time as \a time, using Qt::UTC as its time representation. @@ -5627,7 +5659,7 @@ qint64 QDateTime::currentSecsSinceEpoch() noexcept #if QT_DEPRECATED_SINCE(6, 9) /*! \since 5.2 - \overload + \overload fromMSecsSinceEpoch() \deprecated [6.9] Pass a \l QTimeZone instead, or omit \a spec and \a offsetSeconds. Returns a datetime representing a moment the given number \a msecs of @@ -5656,7 +5688,7 @@ QDateTime QDateTime::fromMSecsSinceEpoch(qint64 msecs, Qt::TimeSpec spec, int of /*! \since 5.8 - \overload + \overload fromSecsSinceEpoch \deprecated [6.9] Pass a \l QTimeZone instead, or omit \a spec and \a offsetSeconds. Returns a datetime representing a moment the given number \a secs of seconds @@ -5686,6 +5718,7 @@ QDateTime QDateTime::fromSecsSinceEpoch(qint64 secs, Qt::TimeSpec spec, int offs /*! \since 5.2 + \overload primary Returns a datetime representing a moment the given number \a msecs of milliseconds after the start, in UTC, of the year 1970, described as @@ -5707,7 +5740,7 @@ QDateTime QDateTime::fromMSecsSinceEpoch(qint64 msecs, const QTimeZone &timeZone } /*! - \overload + \overload fromMSecsSinceEpoch() */ QDateTime QDateTime::fromMSecsSinceEpoch(qint64 msecs) { @@ -5716,6 +5749,7 @@ QDateTime QDateTime::fromMSecsSinceEpoch(qint64 msecs) /*! \since 5.8 + \overload primary Returns a datetime representing a moment the given number \a secs of seconds after the start, in UTC, of the year 1970, described as specified by \a @@ -5737,7 +5771,7 @@ QDateTime QDateTime::fromSecsSinceEpoch(qint64 secs, const QTimeZone &timeZone) } /*! - \overload + \overload fromSecsSinceEpoch() */ QDateTime QDateTime::fromSecsSinceEpoch(qint64 secs) { @@ -5747,6 +5781,7 @@ QDateTime QDateTime::fromSecsSinceEpoch(qint64 secs) #if QT_CONFIG(datestring) // depends on, so implies, textdate /*! + \overload \fn QDateTime QDateTime::fromString(const QString &string, Qt::DateFormat format) Returns the QDateTime represented by the \a string, using the @@ -5759,8 +5794,8 @@ QDateTime QDateTime::fromSecsSinceEpoch(qint64 secs) */ /*! - \overload \since 6.0 + \overload fromString() */ QDateTime QDateTime::fromString(QStringView string, Qt::DateFormat format) { @@ -5901,6 +5936,7 @@ QDateTime QDateTime::fromString(QStringView string, Qt::DateFormat format) } /*! + \overload primary \fn QDateTime QDateTime::fromString(const QString &string, const QString &format, int baseYear, QCalendar cal) Returns the QDateTime represented by the \a string, using the \a @@ -5984,14 +6020,14 @@ QDateTime QDateTime::fromString(QStringView string, Qt::DateFormat format) */ /*! - \fn QDateTime QDateTime::fromString(QStringView string, QStringView format, QCalendar cal) - \overload \since 6.0 + \overload fromString() + \fn QDateTime QDateTime::fromString(QStringView string, QStringView format, QCalendar cal) */ /*! - \overload \since 6.0 + \overload fromString() */ QDateTime QDateTime::fromString(const QString &string, QStringView format, int baseYear, QCalendar cal) @@ -6015,34 +6051,34 @@ QDateTime QDateTime::fromString(const QString &string, QStringView format, int b } /*! - \fn QDateTime QDateTime::fromString(const QString &string, const QString &format, QCalendar cal) - \overload \since 5.14 + \overload fromString() + \fn QDateTime QDateTime::fromString(const QString &string, const QString &format, QCalendar cal) */ /*! - \fn QDateTime QDateTime::fromString(const QString &string, QStringView format, QCalendar cal) - \overload \since 6.0 + \overload fromString() + \fn QDateTime QDateTime::fromString(const QString &string, QStringView format, QCalendar cal) */ /*! - \fn QDateTime QDateTime::fromString(QStringView string, QStringView format, int baseYear, QCalendar cal) - \overload \since 6.7 + \overload fromString() + \fn QDateTime QDateTime::fromString(QStringView string, QStringView format, int baseYear, QCalendar cal) */ /*! - \fn QDateTime QDateTime::fromString(QStringView string, QStringView format, int baseYear) - \overload \since 6.7 + \overload fromString() + \fn QDateTime QDateTime::fromString(QStringView string, QStringView format, int baseYear) Uses a default-constructed QCalendar. */ /*! - \overload \since 6.7 + \overload fromString() Uses a default-constructed QCalendar. */ @@ -6052,9 +6088,9 @@ QDateTime QDateTime::fromString(const QString &string, QStringView format, int b } /*! - \fn QDateTime QDateTime::fromString(const QString &string, const QString &format, int baseYear) - \overload \since 6.7 + \overload fromString() + \fn QDateTime QDateTime::fromString(const QString &string, const QString &format, int baseYear) Uses a default-constructed QCalendar. */ diff --git a/src/corelib/time/qgregoriancalendar.cpp b/src/corelib/time/qgregoriancalendar.cpp index d46d24ac30d..dfb99c5073d 100644 --- a/src/corelib/time/qgregoriancalendar.cpp +++ b/src/corelib/time/qgregoriancalendar.cpp @@ -31,6 +31,7 @@ static_assert(qDivMod<86400>(-172800).remainder == 0); /*! \since 5.14 + \internal \class QGregorianCalendar \inmodule QtCore diff --git a/src/corelib/time/qjalalicalendar.cpp b/src/corelib/time/qjalalicalendar.cpp index 8bc9fe125e7..683ce6e7712 100644 --- a/src/corelib/time/qjalalicalendar.cpp +++ b/src/corelib/time/qjalalicalendar.cpp @@ -39,6 +39,7 @@ qint64 firstDayOfYear(int year, int cycleNo) /*! \since 5.14 + \internal \class QJalaliCalendar \inmodule QtCore diff --git a/src/corelib/time/qjuliancalendar.cpp b/src/corelib/time/qjuliancalendar.cpp index 47da952b84a..cf3718f471d 100644 --- a/src/corelib/time/qjuliancalendar.cpp +++ b/src/corelib/time/qjuliancalendar.cpp @@ -13,6 +13,7 @@ using namespace QRoundingDown; /*! \since 5.14 + \internal \class QJulianCalendar \inmodule QtCore diff --git a/src/corelib/time/qmilankoviccalendar.cpp b/src/corelib/time/qmilankoviccalendar.cpp index a3ffa2a3053..14aef83afe3 100644 --- a/src/corelib/time/qmilankoviccalendar.cpp +++ b/src/corelib/time/qmilankoviccalendar.cpp @@ -13,6 +13,7 @@ using namespace QRoundingDown; /*! \since 5.14 + \internal \class QMilankovicCalendar \inmodule QtCore diff --git a/src/corelib/time/qromancalendar.cpp b/src/corelib/time/qromancalendar.cpp index ae113cf8323..a8ce027275a 100644 --- a/src/corelib/time/qromancalendar.cpp +++ b/src/corelib/time/qromancalendar.cpp @@ -9,6 +9,7 @@ QT_BEGIN_NAMESPACE /*! \since 5.14 + \internal \class QRomanCalendar \inmodule QtCore diff --git a/src/corelib/time/qtimezone.cpp b/src/corelib/time/qtimezone.cpp index f44c681ea80..7b43aab22d1 100644 --- a/src/corelib/time/qtimezone.cpp +++ b/src/corelib/time/qtimezone.cpp @@ -29,7 +29,7 @@ static QTimeZonePrivate *newBackendTimeZone() return new QMacTimeZonePrivate(); #elif defined(Q_OS_ANDROID) return new QAndroidTimeZonePrivate(); -#elif defined(Q_OS_UNIX) +#elif defined(Q_OS_UNIX) && !defined(Q_OS_VXWORKS) return new QTzTimeZonePrivate(); #elif QT_CONFIG(icu) return new QIcuTimeZonePrivate(); @@ -50,7 +50,7 @@ static QTimeZonePrivate *newBackendTimeZone(const QByteArray &ianaId) return new QMacTimeZonePrivate(ianaId); #elif defined(Q_OS_ANDROID) return new QAndroidTimeZonePrivate(ianaId); -#elif defined(Q_OS_UNIX) +#elif defined(Q_OS_UNIX) && !defined(Q_OS_VXWORKS) return new QTzTimeZonePrivate(ianaId); #elif QT_CONFIG(icu) return new QIcuTimeZonePrivate(ianaId); @@ -1482,7 +1482,8 @@ QTimeZone QTimeZone::utc() bool QTimeZone::isTimeZoneIdAvailable(const QByteArray &ianaId) { -#if defined(Q_OS_UNIX) && !(defined(Q_OS_ANDROID) || defined(Q_OS_DARWIN)) +#if defined(Q_OS_UNIX) && !(QT_CONFIG(timezone_tzdb) || defined(Q_OS_DARWIN) \ + || defined(Q_OS_ANDROID) || defined(Q_OS_VXWORKS)) // Keep #if-ery consistent with selection of QTzTimeZonePrivate in // newBackendTimeZone(). Skip the pre-check, as the TZ backend accepts POSIX // zone IDs, which need not be valid IANA IDs. See also QTBUG-112006. diff --git a/src/corelib/time/qtimezonelocale.cpp b/src/corelib/time/qtimezonelocale.cpp index a794682fe0b..6ad4aa1479c 100644 --- a/src/corelib/time/qtimezonelocale.cpp +++ b/src/corelib/time/qtimezonelocale.cpp @@ -611,6 +611,27 @@ QString QTimeZonePrivate::localeName(qint64 atMSecsSinceEpoch, int offsetFromUtc // Custom zone with perverse m_id ? return; } + const auto isMixedCaseAbbrev = [tail](char ch) { + // cv-RU and en-GU abbreviate Chamorro as ChST + // scn-IT abbreviates Cuba as CuT/CuST/CuDT + // blo-BJ abbreviates GMT as Gk + switch (tail.size()) { + case 2: return tail == "Gk"; + case 3: return tail == "CuT"; + case 4: + if (tail[0] == 'C' && tail[1] == ch && tail[3] == 'T') { + switch (ch) { + case 'h': return tail[2] == 'S'; + case 'u': return tail[2] == 'S' || tail[2] == 'D'; + default: break; + } + } + return false; + default: + break; + } + return false; + }; // Even if it is abbr or city name, we don't care if we've found one before. bool maybeAbbr = ianaAbbrev.isEmpty(), maybeCityName = ianaTail.isEmpty(), inword = false; @@ -632,7 +653,7 @@ QString QTimeZonePrivate::localeName(qint64 atMSecsSinceEpoch, int offsetFromUtc maybeCityName = false; inword = false; } else if (QChar::isLower(ch)) { - maybeAbbr = false; + maybeAbbr = isMixedCaseAbbrev(ch); // Dar_es_Salaam shows both cases as word starts inword = true; } else if (QChar::isUpper(ch)) { @@ -891,8 +912,11 @@ QTimeZonePrivate::findLongNamePrefix(QStringView text, const QLocale &locale, if (best.ianaIdIndex != invalidIanaId) return { QByteArray(ianaIdData + best.ianaIdIndex), best.nameLength, best.timeType }; - // Now try for a region format: - best = {}; + // Now try for a region format. + // Since we may get the IANA ID directly from a zone, we may not need an + // ianaIdIndex from CLDR-derived tables: and the active backend may know + // some zones newer than our latest CLDR. + NamePrefixMatch found; for (const qsizetype locInd : indices) { const LocaleZoneData &locData = localeZoneData[locInd]; const LocaleZoneData &nextData = localeZoneData[locInd + 1]; @@ -928,11 +952,11 @@ QTimeZonePrivate::findLongNamePrefix(QStringView text, const QLocale &locale, QStringView city = row.exemplarCity().viewData(exemplarCityTable); if (textMatches(city)) { qsizetype length = cut + city.size() + suffix.size(); - if (length > best.nameLength) { - bool gotZone = row.ianaIdIndex == best.ianaIdIndex + if (length > found.nameLength) { + bool gotZone = row.ianaId() == found.ianaId // (cheap pre-test) || QTimeZone::isTimeZoneIdAvailable(row.ianaId().toByteArray()); if (gotZone) - best = { length, timeType, row.ianaIdIndex }; + found = { row.ianaId().toByteArray(), length, timeType }; } } } @@ -945,38 +969,16 @@ QTimeZonePrivate::findLongNamePrefix(QStringView text, const QLocale &locale, QString city = QString::fromLatin1(local.replace('_', ' ')); if (textMatches(city)) { qsizetype length = cut + city.size() + suffix.size(); - if (length > best.nameLength) { - // Have to find iana in ianaIdData. Although its entries - // from locale-independent data are nicely sorted, the - // rest are (sadly) not. - QByteArrayView run(ianaIdData, qstrlen(ianaIdData)); - // std::size includes the trailing '\0', so subtract one: - const char *stop = ianaIdData + std::size(ianaIdData) - 1; - while (run != iana) { - if (run.end() < stop) { // Step to the next: - run = QByteArrayView(run.end() + 1); - } else { - run = QByteArrayView(); - break; - } - } - if (!run.isEmpty()) { - Q_ASSERT(run == iana); - const auto ianaIdIndex = run.begin() - ianaIdData; - Q_ASSERT(ianaIdIndex <= (std::numeric_limits<quint16>::max)()); - best = { length, timeType, quint16(ianaIdIndex) }; - } - } + if (length > found.nameLength) + found = { iana, length, timeType }; } } // TODO: similar for territories, at least once localeName() does so. } } - if (best.ianaIdIndex != invalidIanaId) - return { QByteArray(ianaIdData + best.ianaIdIndex), best.nameLength, best.timeType }; #undef localeRows - return {}; // No match found. + return found; } QTimeZonePrivate::NamePrefixMatch diff --git a/src/corelib/time/qtimezoneprivate.cpp b/src/corelib/time/qtimezoneprivate.cpp index 556f7e21402..3f0254b1c5f 100644 --- a/src/corelib/time/qtimezoneprivate.cpp +++ b/src/corelib/time/qtimezoneprivate.cpp @@ -1177,7 +1177,7 @@ QUtcTimeZonePrivate::QUtcTimeZonePrivate(qint32 offsetSeconds) } Q_ASSERT(!name.isEmpty()); } else { // Fall back to a UTC-offset name: - name = isoOffsetFormat(offsetSeconds, QTimeZone::ShortName); + name = isoOffsetFormat(offsetSeconds, QTimeZone::OffsetName); id = name.toUtf8(); } init(id, offsetSeconds, name, name, QLocale::AnyTerritory, name); @@ -1292,12 +1292,16 @@ QString QUtcTimeZonePrivate::displayName(QTimeZone::TimeType timeType, if (!(name.startsWith("GMT"_L1) || name.startsWith("UTC"_L1)) || name.size() < 5) return false; // Fallback drops trailing ":00" minute: - QStringView tail{avoid}; + QStringView tail{avoid}; // TODO: deal with sign earlier ! Also: invisible Unicode ! tail = tail.sliced(3); - if (tail.endsWith(":00"_L1)) - tail = tail.chopped(3); if (name.sliced(3) == tail) return true; + while (tail.endsWith(":00"_L1)) + tail = tail.chopped(3); + while (name.endsWith(":00"_L1)) + name = name.chopped(3); + if (name == tail) + return true; // Accept U+2212 as minus sign: const QChar sign = name[3] == u'\u2212' ? u'-' : name[3]; // Fallback doesn't zero-pad hour: diff --git a/src/corelib/time/qtimezoneprivate_p.h b/src/corelib/time/qtimezoneprivate_p.h index 804c28af372..9a86ded6efb 100644 --- a/src/corelib/time/qtimezoneprivate_p.h +++ b/src/corelib/time/qtimezoneprivate_p.h @@ -50,7 +50,7 @@ class Q_AUTOTEST_EXPORT QTimeZonePrivate : public QSharedData { // Nothing should be copy-assigning instances of either this or its derived // classes (only clone() should copy, using the copy-constructor): - bool operator=(const QTimeZonePrivate &) const = delete; + QTimeZonePrivate &operator=(const QTimeZonePrivate &) const = delete; protected: QTimeZonePrivate(const QTimeZonePrivate &other) = default; public: @@ -210,7 +210,7 @@ Q_DECLARE_TYPEINFO(QTimeZonePrivate::Data, Q_RELOCATABLE_TYPE); class Q_AUTOTEST_EXPORT QUtcTimeZonePrivate final : public QTimeZonePrivate { - bool operator=(const QUtcTimeZonePrivate &) const = delete; + QUtcTimeZonePrivate &operator=(const QUtcTimeZonePrivate &) const = delete; QUtcTimeZonePrivate(const QUtcTimeZonePrivate &other); public: // Create default UTC time zone @@ -273,7 +273,7 @@ private: #if QT_CONFIG(timezone_tzdb) class QChronoTimeZonePrivate final : public QTimeZonePrivate { - bool operator=(const QChronoTimeZonePrivate &) const = delete; + QChronoTimeZonePrivate &operator=(const QChronoTimeZonePrivate &) const = delete; QChronoTimeZonePrivate(const QChronoTimeZonePrivate &) = default; public: QChronoTimeZonePrivate(); @@ -307,7 +307,7 @@ private: #elif defined(Q_OS_DARWIN) class Q_AUTOTEST_EXPORT QMacTimeZonePrivate final : public QTimeZonePrivate { - bool operator=(const QMacTimeZonePrivate &) const = delete; + QMacTimeZonePrivate &operator=(const QMacTimeZonePrivate &) const = delete; QMacTimeZonePrivate(const QMacTimeZonePrivate &other); public: // Create default time zone @@ -353,7 +353,7 @@ private: #elif defined(Q_OS_ANDROID) class QAndroidTimeZonePrivate final : public QTimeZonePrivate { - bool operator=(const QAndroidTimeZonePrivate &) const = delete; + QAndroidTimeZonePrivate &operator=(const QAndroidTimeZonePrivate &) const = delete; QAndroidTimeZonePrivate(const QAndroidTimeZonePrivate &) = default; public: // Create default time zone @@ -388,7 +388,7 @@ private: QJniObject androidTimeZone; }; -#elif defined(Q_OS_UNIX) +#elif defined(Q_OS_UNIX) && !defined(Q_OS_VXWORKS) struct QTzTransitionTime { qint64 atMSecsSinceEpoch; @@ -421,7 +421,7 @@ struct QTzTimeZoneCacheEntry class Q_AUTOTEST_EXPORT QTzTimeZonePrivate final : public QTimeZonePrivate { - bool operator=(const QTzTimeZonePrivate &) const = delete; + QTzTimeZonePrivate &operator=(const QTzTimeZonePrivate &) const = delete; QTzTimeZonePrivate(const QTzTimeZonePrivate &) = default; public: // Create default time zone @@ -474,7 +474,7 @@ private: #elif QT_CONFIG(icu) class Q_AUTOTEST_EXPORT QIcuTimeZonePrivate final : public QTimeZonePrivate { - bool operator=(const QIcuTimeZonePrivate &) const = delete; + QIcuTimeZonePrivate &operator=(const QIcuTimeZonePrivate &) const = delete; QIcuTimeZonePrivate(const QIcuTimeZonePrivate &other); public: // Create default time zone @@ -518,7 +518,7 @@ private: #elif defined(Q_OS_WIN) class Q_AUTOTEST_EXPORT QWinTimeZonePrivate final : public QTimeZonePrivate { - bool operator=(const QWinTimeZonePrivate &) const = delete; + QWinTimeZonePrivate &operator=(const QWinTimeZonePrivate &) const = delete; QWinTimeZonePrivate(const QWinTimeZonePrivate &) = default; public: struct QWinTransitionRule { diff --git a/src/corelib/tools/qiterator.qdoc b/src/corelib/tools/qiterator.qdoc index 3d8ea595167..d517457027a 100644 --- a/src/corelib/tools/qiterator.qdoc +++ b/src/corelib/tools/qiterator.qdoc @@ -165,7 +165,7 @@ position between the second and third item, and returns the second item; and so on. - \image javaiterators1.png + \image javaiterators1.svg Java-style iterators point between items Here's how to iterate over the elements in reverse order: @@ -211,7 +211,7 @@ position between the second and third item, returning the second item; and so on. - \image javaiterators1.png + \image javaiterators1.svg Java-style iterators point between items If you want to find all occurrences of a particular value, use findNext() in a loop. @@ -260,7 +260,7 @@ position between the second and third item, returning the second item; and so on. - \image javaiterators1.png + \image javaiterators1.svg Java-style iterators point between items Here's how to iterate over the elements in reverse order: @@ -321,7 +321,7 @@ position between the second and third item, returning the second item; and so on. - \image javaiterators1.png + \image javaiterators1.svg Java-style iterators point between items If you want to remove items as you iterate over the set, use remove(). @@ -718,7 +718,7 @@ next() advances the iterator to the position between the second and third item; and so on. - \image javaiterators1.png + \image javaiterators1.svg Java-style iterators point between items Here's how to iterate over the elements in reverse order: @@ -768,7 +768,7 @@ next() advances the iterator to the position between the second and third item; and so on. - \image javaiterators1.png + \image javaiterators1.svg Java-style iterators point between items Here's how to iterate over the elements in reverse order: @@ -819,7 +819,7 @@ next() advances the iterator to the position between the second and third item; and so on. - \image javaiterators1.png + \image javaiterators1.svg Java-style iterators point between items If you want to find all occurrences of a particular value, use findNext() in a loop. For example: @@ -867,7 +867,7 @@ next() advances the iterator to the position between the second and third item; and so on. - \image javaiterators1.png + \image javaiterators1.svg Java-style iterators point between items Here's how to iterate over the elements in reverse order: @@ -931,7 +931,7 @@ next() advances the iterator to the position between the second and third item; and so on. - \image javaiterators1.png + \image javaiterators1.svg Java-style iterators point between items Here's how to iterate over the elements in reverse order: @@ -994,7 +994,7 @@ next() advances the iterator to the position between the second and third item; and so on. - \image javaiterators1.png + \image javaiterators1.svg Java-style iterators point between items If you want to find all occurrences of a particular value, use findNext() in a loop. For example: diff --git a/src/corelib/tools/quniquehandle_p.h b/src/corelib/tools/quniquehandle_p.h index 3ba557e838d..fd6ab693912 100644 --- a/src/corelib/tools/quniquehandle_p.h +++ b/src/corelib/tools/quniquehandle_p.h @@ -18,6 +18,7 @@ #include <QtCore/qtconfigmacros.h> #include <QtCore/qassert.h> #include <QtCore/qcompare.h> +#include <QtCore/qfunctionaltools_impl.h> #include <QtCore/qswap.h> #include <QtCore/qtclasshelpermacros.h> @@ -99,6 +100,42 @@ QT_BEGIN_NAMESPACE ... + Example 3: + + struct TempFileTraits { + using Type = FILE*; + + static Type invalidValue() { + return nullptr; + } + + static bool close(Type handle) { + return fclose(handle) == 0; + } + }; + + struct TempFileDeleter { + using Type = TempFileTraits::Type; + + void operator()(Type handle) { + if (handle != TempFileTraits::invalidValue()) { + TempFileTraits::close(handle); + if (path) + remove(path); + } + } + + const char* path{ nullptr }; + }; + + using TempFileHandle = QUniqueHandle<TempFileTraits, TempFileDeleter>; + + Usage: + + TempFileHandle tempFile(fopen("temp.bin", "wb"), TempFileDeleter{ "my_temp.bin" }); + + ... + NOTE: The QUniqueHandle assumes that closing a resource is guaranteed to succeed, and provides no support for handling failure to close a resource. It is therefore only recommended for use cases @@ -108,9 +145,32 @@ QT_BEGIN_NAMESPACE // clang-format off +namespace QtUniqueHandleTraits { + template <typename HandleTraits> -class QUniqueHandle +struct DefaultDeleter { + using Type = typename HandleTraits::Type; + + void operator()(Type handle) const noexcept + { + if (handle != HandleTraits::invalidValue()) { + const bool success = HandleTraits::close(handle); + Q_ASSERT(success); + } + } +}; + +} // namespace QtUniqueHandleTraits + +template <typename HandleTraits, typename Deleter = QtUniqueHandleTraits::DefaultDeleter<HandleTraits>> +class QUniqueHandle : private QtPrivate::CompactStorage<Deleter> +{ + using Storage = QtPrivate::CompactStorage<Deleter>; + + template <typename D> + using if_default_constructible = std::enable_if_t<std::is_default_constructible_v<D>, bool>; + public: using Type = typename HandleTraits::Type; static_assert(std::is_nothrow_default_constructible_v<Type>); @@ -120,6 +180,11 @@ public: static_assert(std::is_nothrow_copy_assignable_v<Type>); static_assert(std::is_nothrow_move_assignable_v<Type>); static_assert(std::is_nothrow_destructible_v<Type>); + static_assert(std::is_nothrow_copy_constructible_v<Deleter>); + static_assert(std::is_nothrow_move_constructible_v<Deleter>); + static_assert(std::is_nothrow_copy_assignable_v<Deleter>); + static_assert(std::is_nothrow_move_assignable_v<Deleter>); + static_assert(std::is_nothrow_destructible_v<Deleter>); static_assert(noexcept(std::declval<Type>() == std::declval<Type>())); static_assert(noexcept(std::declval<Type>() != std::declval<Type>())); static_assert(noexcept(std::declval<Type>() < std::declval<Type>())); @@ -127,16 +192,24 @@ public: static_assert(noexcept(std::declval<Type>() > std::declval<Type>())); static_assert(noexcept(std::declval<Type>() >= std::declval<Type>())); - QUniqueHandle() = default; - - explicit QUniqueHandle(const Type &handle) noexcept + template <if_default_constructible<Deleter> = true> + explicit QUniqueHandle(const Type& handle = HandleTraits::invalidValue()) noexcept : m_handle{ handle } {} - QUniqueHandle(QUniqueHandle &&other) noexcept - : m_handle{ other.release() } + QUniqueHandle(const Type &handle, const Deleter &deleter) noexcept + : Storage{ deleter }, m_handle{ handle } + {} + + QUniqueHandle(const Type &handle, Deleter &&deleter) noexcept + : Storage{ std::move(deleter) }, m_handle{ handle } {} + QUniqueHandle(QUniqueHandle &&other) noexcept + : Storage{ std::move(other.deleter()) }, m_handle{ other.release() } + { + } + ~QUniqueHandle() noexcept { close(); @@ -145,6 +218,7 @@ public: void swap(QUniqueHandle &other) noexcept { qSwap(m_handle, other.m_handle); + qSwap(deleter(), other.deleter()); } QT_MOVE_ASSIGNMENT_OPERATOR_IMPL_VIA_MOVE_AND_SWAP(QUniqueHandle) @@ -168,6 +242,16 @@ public: return m_handle; } + [[nodiscard]] Deleter& deleter() noexcept + { + return Storage::object(); + } + + [[nodiscard]] const Deleter& deleter() const noexcept + { + return Storage::object(); + } + void reset(const Type& handle = HandleTraits::invalidValue()) noexcept { if (handle == m_handle) @@ -193,8 +277,7 @@ public: if (!isValid()) return; - const bool success = HandleTraits::close(m_handle); - Q_ASSERT(success); + deleter()(m_handle); m_handle = HandleTraits::invalidValue(); } @@ -222,8 +305,8 @@ private: // clang-format on -template <typename Trait> -void swap(QUniqueHandle<Trait> &lhs, QUniqueHandle<Trait> &rhs) noexcept +template <typename Trait, typename Deleter> +void swap(QUniqueHandle<Trait, Deleter> &lhs, QUniqueHandle<Trait, Deleter> &rhs) noexcept { lhs.swap(rhs); } diff --git a/src/gui/configure.cmake b/src/gui/configure.cmake index ad76bb095a0..5001f2deeec 100644 --- a/src/gui/configure.cmake +++ b/src/gui/configure.cmake @@ -1217,17 +1217,6 @@ qt_feature("xcb-egl-plugin" PRIVATE CONDITION QT_FEATURE_egl AND QT_FEATURE_opengl EMIT_IF QT_FEATURE_xcb ) -qt_feature("xcb-native-painting" PRIVATE - LABEL "Native painting (experimental)" - AUTODETECT OFF - CONDITION QT_FEATURE_xcb_xlib AND QT_FEATURE_fontconfig AND XRender_FOUND - EMIT_IF QT_FEATURE_xcb -) -qt_feature("xrender" PRIVATE - LABEL "XRender for native painting" - CONDITION QT_FEATURE_xcb_native_painting - EMIT_IF QT_FEATURE_xcb AND QT_FEATURE_xcb_native_painting -) qt_feature("xcb-xlib" PRIVATE LABEL "XCB Xlib" CONDITION QT_FEATURE_xlib AND X11_XCB_FOUND diff --git a/src/gui/doc/src/richtext.qdoc b/src/gui/doc/src/richtext.qdoc index 2fa49a31e03..f94c436bd80 100644 --- a/src/gui/doc/src/richtext.qdoc +++ b/src/gui/doc/src/richtext.qdoc @@ -1047,6 +1047,12 @@ \li \c type (\c 1, \c a, \c A, \c square, \c disc, \c circle) \endlist + Additionally, the following attribute is supported by the \c ol tag: + + \list + \li \c start + \endlist + \section1 Table Cell Attributes The following attributes are supported by the \c td and \c th diff --git a/src/gui/image/qimage.cpp b/src/gui/image/qimage.cpp index 6ba113ef8fb..4a8b409379c 100644 --- a/src/gui/image/qimage.cpp +++ b/src/gui/image/qimage.cpp @@ -47,6 +47,9 @@ #include <memory> +#define QT_XFORM_TYPE_MSBFIRST 0 +#define QT_XFORM_TYPE_LSBFIRST 1 + QT_BEGIN_NAMESPACE class QCmyk32; @@ -4447,6 +4450,8 @@ int QImage::metric(PaintDeviceMetric metric) const trigx += m11; \ trigy += m12; // END OF MACRO + +static bool qt_xForm_helper(const QTransform &trueMat, int xoffset, int type, int depth, uchar *dptr, qsizetype dbpl, int p_inc, int dHeight, const uchar *sptr, qsizetype sbpl, int sWidth, int sHeight) diff --git a/src/gui/image/qplatformpixmap.h b/src/gui/image/qplatformpixmap.h index 5621afa4da5..3346ca85375 100644 --- a/src/gui/image/qplatformpixmap.h +++ b/src/gui/image/qplatformpixmap.h @@ -33,7 +33,7 @@ public: enum ClassId { RasterClass, DirectFBClass, BlitterClass, Direct2DClass, - X11Class, CustomClass = 1024 }; + CustomClass = 1024 }; QPlatformPixmap(PixelType pixelType, int classId); virtual ~QPlatformPixmap(); @@ -111,7 +111,6 @@ protected: private: friend class QPixmap; - friend class QX11PlatformPixmap; friend class QImagePixmapCleanupHooks; // Needs to set is_cached int detach_no; @@ -122,10 +121,6 @@ private: uint is_cached; }; -# define QT_XFORM_TYPE_MSBFIRST 0 -# define QT_XFORM_TYPE_LSBFIRST 1 -Q_GUI_EXPORT bool qt_xForm_helper(const QTransform&, int, int, int, uchar*, qsizetype, int, int, const uchar*, qsizetype, int, int); - QT_END_NAMESPACE #endif // QPLATFORMPIXMAP_H diff --git a/src/gui/itemmodels/qfilesystemmodel.cpp b/src/gui/itemmodels/qfilesystemmodel.cpp index 622c2e5bc92..9eb31f1b6d4 100644 --- a/src/gui/itemmodels/qfilesystemmodel.cpp +++ b/src/gui/itemmodels/qfilesystemmodel.cpp @@ -1787,14 +1787,17 @@ bool QFileSystemModel::event(QEvent *event) bool QFileSystemModel::rmdir(const QModelIndex &aindex) { + Q_D(QFileSystemModel); + QString path = filePath(aindex); const bool success = QDir().rmdir(path); -#if QT_CONFIG(filesystemwatcher) if (success) { - QFileSystemModelPrivate * d = const_cast<QFileSystemModelPrivate*>(d_func()); +#if QT_CONFIG(filesystemwatcher) d->fileInfoGatherer->removePath(path); - } #endif + QFileSystemModelPrivate::QFileSystemNode *parentNode = d->node(aindex.parent()); + d->removeNode(parentNode, fileName(aindex)); + } return success; } diff --git a/src/gui/kernel/qevent.cpp b/src/gui/kernel/qevent.cpp index ae730a9b3af..7b1e76cc78f 100644 --- a/src/gui/kernel/qevent.cpp +++ b/src/gui/kernel/qevent.cpp @@ -1146,6 +1146,33 @@ Q_IMPL_POINTER_EVENT(QHoverEvent) */ /*! + \property QWheelEvent::device + \brief the device from which the wheel event originated + + \sa pointingDevice() +*/ + +/*! + \property QWheelEvent::inverted + \since 5.7 + \brief whether the delta values delivered with the event are inverted + + Normally, a vertical wheel will produce a QWheelEvent with positive delta + values if the top of the wheel is rotating away from the hand operating it. + Similarly, a horizontal wheel movement will produce a QWheelEvent with + positive delta values if the top of the wheel is moved to the left. + + However, on some platforms this is configurable, so that the same + operations described above will produce negative delta values (but with the + same magnitude). With the inverted property a wheel event consumer can + choose to always follow the direction of the wheel, regardless of the + system settings, but only for specific widgets. + + \note Many platforms provide no such information. On such platforms + \l inverted always returns false. +*/ + +/*! \fn bool QWheelEvent::inverted() const \since 5.7 @@ -1236,6 +1263,24 @@ bool QWheelEvent::isEndEvent() const #endif // QT_CONFIG(wheelevent) /*! + \property QWheelEvent::pixelDelta + \brief the scrolling distance in pixels on screen + + This value is provided on platforms that support high-resolution + pixel-based delta values, such as \macos. The value should be used + directly to scroll content on screen. + + \note On platforms that support scrolling \l{phase()}{phases}, the delta + may be null when scrolling is about to begin (Qt::ScrollBegin) or has + ended (Qt::ScrollEnd). + + \note On X11 this value is driver-specific and unreliable, use + angleDelta() instead. + + \sa angleDelta() +*/ + +/*! \fn QPoint QWheelEvent::pixelDelta() const Returns the scrolling distance in pixels on screen. This value is @@ -1256,6 +1301,27 @@ bool QWheelEvent::isEndEvent() const */ /*! + \property QWheelEvent::angleDelta + \brief the relative amount that the wheel was rotated, in eighths of a degree + + A positive value indicates that the wheel was rotated forwards away from the + user; a negative value indicates that the wheel was rotated backwards toward + the user. \c angleDelta().y() provides the angle through which the common + vertical mouse wheel was rotated since the previous event. \c angleDelta().x() + provides the angle through which the horizontal mouse wheel was rotated, if + the mouse has a horizontal wheel; otherwise it stays at zero. + + Most mouse types work in steps of 15 degrees, in which case the delta value + is a multiple of 120; i.e., 120 units * 1/8 = 15 degrees. + + \note On platforms that support scrolling \l{phase()}{phases}, the delta + may be null when scrolling is about to begin (Qt::ScrollBegin) or has + ended (Qt::ScrollEnd). + + \sa pixelDelta() +*/ + +/*! \fn QPoint QWheelEvent::angleDelta() const Returns the relative amount that the wheel was rotated, in eighths of a @@ -1294,6 +1360,15 @@ bool QWheelEvent::isEndEvent() const */ /*! + \property QWheelEvent::phase + \since 5.2 + \brief the scrolling phase of this wheel event + + \note The Qt::ScrollBegin and Qt::ScrollEnd phases are currently + supported only on \macos. +*/ + +/*! \fn Qt::ScrollPhase QWheelEvent::phase() const \since 5.2 diff --git a/src/gui/kernel/qinputdevice.cpp b/src/gui/kernel/qinputdevice.cpp index 8e5c38922e0..caed0fc6135 100644 --- a/src/gui/kernel/qinputdevice.cpp +++ b/src/gui/kernel/qinputdevice.cpp @@ -187,6 +187,11 @@ QString QInputDevice::name() const } /*! + \property QInputDevice::type + \brief the device type +*/ + +/*! Returns the device type. */ QInputDevice::DeviceType QInputDevice::type() const diff --git a/src/gui/kernel/qpointingdevice.cpp b/src/gui/kernel/qpointingdevice.cpp index dcce354688a..7062340b287 100644 --- a/src/gui/kernel/qpointingdevice.cpp +++ b/src/gui/kernel/qpointingdevice.cpp @@ -241,6 +241,11 @@ void QPointingDevice::setMaximumTouchPoints(int c) #endif // QT_DEPRECATED_SINCE(6, 0) /*! + \property QPointingDevice::pointerType + \brief the pointer type +*/ + +/*! Returns the pointer type. */ QPointingDevice::PointerType QPointingDevice::pointerType() const @@ -250,6 +255,12 @@ QPointingDevice::PointerType QPointingDevice::pointerType() const } /*! + \property QPointingDevice::maximumPoints + \brief the maximum number of simultaneous touch points (fingers) that + can be detected +*/ + +/*! Returns the maximum number of simultaneous touch points (fingers) that can be detected. */ @@ -260,6 +271,11 @@ int QPointingDevice::maximumPoints() const } /*! + \property QPointingDevice::buttonCount + \brief the maximum number of on-device buttons that can be detected +*/ + +/*! Returns the maximum number of on-device buttons that can be detected. */ int QPointingDevice::buttonCount() const @@ -269,6 +285,13 @@ int QPointingDevice::buttonCount() const } /*! + \property QPointingDevice::uniqueId + \brief a unique ID (of dubious utility) for the device + + You probably should rather be concerned with QPointerEventPoint::uniqueId(). +*/ + +/*! Returns a unique ID (of dubious utility) for the device. You probably should rather be concerned with QPointerEventPoint::uniqueId(). diff --git a/src/gui/painting/qtextureglyphcache.cpp b/src/gui/painting/qtextureglyphcache.cpp index d27539b2419..c9df5d28a45 100644 --- a/src/gui/painting/qtextureglyphcache.cpp +++ b/src/gui/painting/qtextureglyphcache.cpp @@ -301,6 +301,8 @@ void QImageTextureGlyphCache::fillTexture(const Coord &c, const QFixedPoint &subPixelPosition) { QImage mask = textureMapForGlyph(g, subPixelPosition); + if (mask.isNull()) + return; #ifdef CACHE_DEBUG printf("fillTexture of %dx%d at %d,%d in the cache of %dx%d\n", c.w, c.h, c.x, c.y, m_image.width(), m_image.height()); diff --git a/src/gui/rhi/qrhi.cpp b/src/gui/rhi/qrhi.cpp index 62b7ab6efb0..5590f2fb431 100644 --- a/src/gui/rhi/qrhi.cpp +++ b/src/gui/rhi/qrhi.cpp @@ -7186,6 +7186,26 @@ QRhiResource::Type QRhiGraphicsPipeline::resourceType() const */ /*! + \fn bool QRhiGraphicsPipeline::hasDepthClamp() const + \return true if depth clamp is enabled. + + \since 6.11 + */ + +/*! + \fn void QRhiGraphicsPipeline::setDepthClamp(bool enable) + + Enables depth clamping when \a enable is true. When depth clamping is + enabled, primitives that would otherwise be clipped by the near or far + clip plane are rasterized and their depth values are clamped to the + depth range. When disabled (the default), such primitives are clipped. + + \note This setting is ignored on OpenGL ES. + + \since 6.11 + */ + +/*! \fn QRhiGraphicsPipeline::CompareOp QRhiGraphicsPipeline::depthOp() const \return the depth comparison function. */ diff --git a/src/gui/rhi/qrhi.h b/src/gui/rhi/qrhi.h index b5a3f7b43be..5ee2a9acd13 100644 --- a/src/gui/rhi/qrhi.h +++ b/src/gui/rhi/qrhi.h @@ -1453,6 +1453,9 @@ public: bool hasDepthWrite() const { return m_depthWrite; } void setDepthWrite(bool enable) { m_depthWrite = enable; } + bool hasDepthClamp() const { return m_depthClamp; } + void setDepthClamp(bool enable) { m_depthClamp = enable; } + CompareOp depthOp() const { return m_depthOp; } void setDepthOp(CompareOp op) { m_depthOp = op; } @@ -1524,6 +1527,7 @@ protected: QVarLengthArray<TargetBlend, 8> m_targetBlends; bool m_depthTest = false; bool m_depthWrite = false; + bool m_depthClamp = false; CompareOp m_depthOp = Less; bool m_stencilTest = false; StencilOpState m_stencilFront; diff --git a/src/gui/rhi/qrhid3d11.cpp b/src/gui/rhi/qrhid3d11.cpp index a5f860e7724..1441be24043 100644 --- a/src/gui/rhi/qrhid3d11.cpp +++ b/src/gui/rhi/qrhid3d11.cpp @@ -4673,7 +4673,7 @@ bool QD3D11GraphicsPipeline::create() rastDesc.FrontCounterClockwise = m_frontFace == CCW; rastDesc.DepthBias = m_depthBias; rastDesc.SlopeScaledDepthBias = m_slopeScaledDepthBias; - rastDesc.DepthClipEnable = true; + rastDesc.DepthClipEnable = m_depthClamp ? FALSE : TRUE; rastDesc.ScissorEnable = m_flags.testFlag(UsesScissor); rastDesc.MultisampleEnable = rhiD->effectiveSampleDesc(m_sampleCount).Count > 1; HRESULT hr = rhiD->dev->CreateRasterizerState(&rastDesc, &rastState); diff --git a/src/gui/rhi/qrhid3d12.cpp b/src/gui/rhi/qrhid3d12.cpp index 4f09b3c136b..b68b65b1063 100644 --- a/src/gui/rhi/qrhid3d12.cpp +++ b/src/gui/rhi/qrhid3d12.cpp @@ -6263,7 +6263,7 @@ bool QD3D12GraphicsPipeline::create() stream.rasterizerState.object.FrontCounterClockwise = m_frontFace == CCW; stream.rasterizerState.object.DepthBias = m_depthBias; stream.rasterizerState.object.SlopeScaledDepthBias = m_slopeScaledDepthBias; - stream.rasterizerState.object.DepthClipEnable = TRUE; + stream.rasterizerState.object.DepthClipEnable = m_depthClamp ? FALSE : TRUE; stream.rasterizerState.object.MultisampleEnable = sampleDesc.Count > 1; stream.depthStencilState.object.DepthEnable = m_depthTest; diff --git a/src/gui/rhi/qrhigles2.cpp b/src/gui/rhi/qrhigles2.cpp index 15f5cd8f7e8..1308d4362e5 100644 --- a/src/gui/rhi/qrhigles2.cpp +++ b/src/gui/rhi/qrhigles2.cpp @@ -585,6 +585,10 @@ QT_BEGIN_NAMESPACE #define GL_PROGRAM 0x82E2 #endif +#ifndef GL_DEPTH_CLAMP +#define GL_DEPTH_CLAMP 0x864F +#endif + /*! Constructs a new QRhiGles2InitParams. @@ -998,6 +1002,13 @@ bool QRhiGles2::create(QRhi::Flags flags) } if (caps.gles) + caps.depthClamp = false; + else + caps.depthClamp = caps.ctxMajor > 3 || (caps.ctxMajor == 3 && caps.ctxMinor >= 2); // Desktop 3.2 + if (!caps.depthClamp) + caps.depthClamp = ctx->hasExtension("GL_EXT_depth_clamp") || ctx->hasExtension("GL_ARB_depth_clamp"); + + if (caps.gles) caps.textureCompareMode = caps.ctxMajor >= 3; // ES 3.0 else caps.textureCompareMode = true; @@ -3959,6 +3970,7 @@ void QRhiGles2::executeCommandBuffer(QRhiCommandBuffer *cb) break; case QGles2CommandBuffer::Command::InvalidateFramebuffer: if (caps.gles && caps.ctxMajor >= 3) { + f->glBindFramebuffer(GL_FRAMEBUFFER, cmd.args.invalidateFramebuffer.fbo); f->glInvalidateFramebuffer(GL_DRAW_FRAMEBUFFER, cmd.args.invalidateFramebuffer.attCount, cmd.args.invalidateFramebuffer.att); @@ -4095,6 +4107,15 @@ void QRhiGles2::executeBindGraphicsPipeline(QGles2CommandBuffer *cbD, QGles2Grap f->glDepthMask(depthWrite); } + const bool depthClamp = psD->m_depthClamp; + if (caps.depthClamp && (forceUpdate || depthClamp != state.depthClamp)) { + state.depthClamp = depthClamp; + if (depthClamp) + f->glEnable(GL_DEPTH_CLAMP); + else + f->glDisable(GL_DEPTH_CLAMP); + } + const GLenum depthFunc = toGlCompareOp(psD->m_depthOp); if (forceUpdate || depthFunc != state.depthFunc) { state.depthFunc = depthFunc; @@ -4881,6 +4902,7 @@ void QRhiGles2::endPass(QRhiCommandBuffer *cb, QRhiResourceUpdateBatch *resource if (mayDiscardDepthStencil) { QGles2CommandBuffer::Command &cmd(cbD->commands.get()); cmd.cmd = QGles2CommandBuffer::Command::InvalidateFramebuffer; + cmd.args.invalidateFramebuffer.fbo = rtTex->framebuffer; if (caps.needsDepthStencilCombinedAttach) { cmd.args.invalidateFramebuffer.attCount = 1; cmd.args.invalidateFramebuffer.att[0] = GL_DEPTH_STENCIL_ATTACHMENT; diff --git a/src/gui/rhi/qrhigles2_p.h b/src/gui/rhi/qrhigles2_p.h index 725e71a3665..70dd96a8dc7 100644 --- a/src/gui/rhi/qrhigles2_p.h +++ b/src/gui/rhi/qrhigles2_p.h @@ -547,6 +547,7 @@ struct QGles2CommandBuffer : public QRhiCommandBuffer GLbitfield barriers; } barrier; struct { + GLuint fbo; int attCount; GLenum att[3]; } invalidateFramebuffer; @@ -592,6 +593,7 @@ struct QGles2CommandBuffer : public QRhiCommandBuffer } blend[16]; bool depthTest; bool depthWrite; + bool depthClamp; GLenum depthFunc; bool stencilTest; GLuint stencilReadMask; @@ -1074,6 +1076,7 @@ public: uint baseVertex : 1; uint compute : 1; uint textureCompareMode : 1; + uint depthClamp : 1; uint properMapBuffer : 1; uint nonBaseLevelFramebufferTexture : 1; uint texelFetch : 1; diff --git a/src/gui/rhi/qrhimetal.mm b/src/gui/rhi/qrhimetal.mm index c3f1031b44b..7fa05f69232 100644 --- a/src/gui/rhi/qrhimetal.mm +++ b/src/gui/rhi/qrhimetal.mm @@ -403,6 +403,7 @@ struct QMetalGraphicsPipelineData MTLWinding winding; MTLCullMode cullMode; MTLTriangleFillMode triangleFillMode; + MTLDepthClipMode depthClipMode; float depthBias; float slopeScaledDepthBias; QMetalShader vs; @@ -1477,7 +1478,6 @@ void QMetalGraphicsPipeline::makeActiveForCurrentRenderPassEncoder(QMetalCommand [cbD->d->currentRenderPassEncoder setDepthStencilState: d->ds]; cbD->d->currentDepthStencilState = d->ds; } - if (cbD->currentCullMode == -1 || d->cullMode != uint(cbD->currentCullMode)) { [cbD->d->currentRenderPassEncoder setCullMode: d->cullMode]; cbD->currentCullMode = int(d->cullMode); @@ -1486,6 +1486,10 @@ void QMetalGraphicsPipeline::makeActiveForCurrentRenderPassEncoder(QMetalCommand [cbD->d->currentRenderPassEncoder setTriangleFillMode: d->triangleFillMode]; cbD->currentTriangleFillMode = int(d->triangleFillMode); } + if (cbD->currentDepthClipMode == -1 || d->depthClipMode != uint(cbD->currentDepthClipMode)) { + [cbD->d->currentRenderPassEncoder setDepthClipMode: d->depthClipMode]; + cbD->currentDepthClipMode = int(d->depthClipMode); + } if (cbD->currentFrontFaceWinding == -1 || d->winding != uint(cbD->currentFrontFaceWinding)) { [cbD->d->currentRenderPassEncoder setFrontFacingWinding: d->winding]; cbD->currentFrontFaceWinding = int(d->winding); @@ -5035,6 +5039,7 @@ void QMetalGraphicsPipeline::mapStates() d->winding = m_frontFace == CCW ? MTLWindingCounterClockwise : MTLWindingClockwise; d->cullMode = toMetalCullMode(m_cullMode); d->triangleFillMode = toMetalTriangleFillMode(m_polygonMode); + d->depthClipMode = m_depthClamp ? MTLDepthClipModeClamp : MTLDepthClipModeClip; d->depthBias = float(m_depthBias); d->slopeScaledDepthBias = m_slopeScaledDepthBias; } @@ -6257,6 +6262,7 @@ void QMetalCommandBuffer::resetPerPassCachedState() currentIndexFormat = QRhiCommandBuffer::IndexUInt16; currentCullMode = -1; currentTriangleFillMode = -1; + currentDepthClipMode = -1; currentFrontFaceWinding = -1; currentDepthBiasValues = { 0.0f, 0.0f }; diff --git a/src/gui/rhi/qrhimetal_p.h b/src/gui/rhi/qrhimetal_p.h index 7c19ae9e767..6649a6cd304 100644 --- a/src/gui/rhi/qrhimetal_p.h +++ b/src/gui/rhi/qrhimetal_p.h @@ -299,6 +299,7 @@ struct QMetalCommandBuffer : public QRhiCommandBuffer QRhiCommandBuffer::IndexFormat currentIndexFormat; int currentCullMode; int currentTriangleFillMode; + int currentDepthClipMode; int currentFrontFaceWinding; std::pair<float, float> currentDepthBiasValues; diff --git a/src/gui/rhi/qrhivulkan.cpp b/src/gui/rhi/qrhivulkan.cpp index c5167a6e7de..481ffd57b5d 100644 --- a/src/gui/rhi/qrhivulkan.cpp +++ b/src/gui/rhi/qrhivulkan.cpp @@ -949,6 +949,8 @@ bool QRhiVulkan::create(QRhi::Flags flags) // elsewhere states that the minimum bufferOffset is 4... texbufAlign = qMax<VkDeviceSize>(4, physDevProperties.limits.optimalBufferCopyOffsetAlignment); + caps.depthClamp = physDevFeatures.depthClamp; + caps.wideLines = physDevFeatures.wideLines; caps.texture3DSliceAs2D = caps.apiVersion >= QVersionNumber(1, 1); @@ -8402,6 +8404,8 @@ bool QVkGraphicsPipeline::create() VkPipelineRasterizationStateCreateInfo rastInfo = {}; rastInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; + if (m_depthClamp && rhiD->caps.depthClamp) + rastInfo.depthClampEnable = m_depthClamp; rastInfo.cullMode = toVkCullMode(m_cullMode); rastInfo.frontFace = toVkFrontFace(m_frontFace); if (m_depthBias != 0 || !qFuzzyIsNull(m_slopeScaledDepthBias)) { diff --git a/src/gui/rhi/qrhivulkan_p.h b/src/gui/rhi/qrhivulkan_p.h index d141a84c5fb..1e9318513fd 100644 --- a/src/gui/rhi/qrhivulkan_p.h +++ b/src/gui/rhi/qrhivulkan_p.h @@ -936,6 +936,7 @@ public: struct { bool compute = false; + bool depthClamp = false; bool wideLines = false; bool debugUtils = false; bool vertexAttribDivisor = false; diff --git a/src/gui/text/qfont_p.h b/src/gui/text/qfont_p.h index d1509e4a251..75550439521 100644 --- a/src/gui/text/qfont_p.h +++ b/src/gui/text/qfont_p.h @@ -166,6 +166,7 @@ public: QFontPrivate(); QFontPrivate(const QFontPrivate &other); + QFontPrivate &operator=(const QFontPrivate &) = delete; ~QFontPrivate(); QFontEngine *engineForScript(int script) const; @@ -206,9 +207,6 @@ public: void setVariableAxis(QFont::Tag tag, float value); void unsetVariableAxis(QFont::Tag tag); bool hasVariableAxis(QFont::Tag tag, float value) const; - -private: - QFontPrivate &operator=(const QFontPrivate &) { return *this; } }; diff --git a/src/gui/text/qfontvariableaxis.cpp b/src/gui/text/qfontvariableaxis.cpp index be83a3e02ce..bbc14f4cd11 100644 --- a/src/gui/text/qfontvariableaxis.cpp +++ b/src/gui/text/qfontvariableaxis.cpp @@ -60,6 +60,18 @@ QT_DEFINE_QESDP_SPECIALIZATION_DTOR(QFontVariableAxisPrivate) QFontVariableAxis::QFontVariableAxis(const QFontVariableAxis &axis) = default; /*! + \property QFontVariableAxis::tag + \brief the tag of the axis + + This is a four-character sequence which identifies the axis. Certain tags + have standardized meanings, such as "wght" (weight) and "wdth" (width), + but any sequence of four latin-1 characters is a valid tag. By convention, + non-standard/custom axes are denoted by tags in all uppercase. + + \sa QFont::setVariableAxis(), name() +*/ + +/*! Returns the tag of the axis. This is a four-character sequence which identifies the axis. Certain tags have standardized meanings, such as "wght" (weight) and "wdth" (width), but any sequence of four latin-1 characters is a valid tag. By convention, non-standard/custom axes @@ -91,6 +103,13 @@ void QFontVariableAxis::setTag(QFont::Tag tag) } /*! + \property QFontVariableAxis::name + \brief the name of the axis, if provided by the font + + \sa tag() +*/ + +/*! Returns the name of the axis, if provided by the font. \sa tag() @@ -153,6 +172,15 @@ void QFontVariableAxis::setMinimumValue(qreal minimumValue) } /*! + \property QFontVariableAxis::maximumValue + \brief the maximum value of the axis + + Setting the axis to a value which is higher than this is not supported. + + \sa minimumValue(), defaultValue() +*/ + +/*! Returns the maximum value of the axis. Setting the axis to a value which is higher than this is not supported. @@ -182,6 +210,16 @@ void QFontVariableAxis::setMaximumValue(qreal maximumValue) } /*! + \property QFontVariableAxis::defaultValue + \brief the default value of the axis + + This is the value the axis will have if none has been provided in the + QFont query. + + \sa minimumValue(), maximumValue() +*/ + +/*! Returns the default value of the axis. This is the value the axis will have if none has been provided in the QFont query. diff --git a/src/gui/text/qtextengine.cpp b/src/gui/text/qtextengine.cpp index 11c79f23d25..29fda652ef6 100644 --- a/src/gui/text/qtextengine.cpp +++ b/src/gui/text/qtextengine.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only // Qt-Security score:critical reason:data-parser +#include <QtCore/private/qflatmap_p.h> #include <QtGui/private/qtguiglobal_p.h> #include "qdebug.h" #include "qtextformat.h" @@ -1613,11 +1614,15 @@ int QTextEngine::shapeTextWithHarfbuzzNG(const QScriptItem &si, const ushort *st { uint glyphs_shaped = 0; - hb_buffer_t *buffer = hb_buffer_create(); - hb_buffer_set_unicode_funcs(buffer, hb_qt_get_unicode_funcs()); + if (!buffer) { + buffer = hb_buffer_create(); + hb_buffer_set_unicode_funcs(buffer, hb_qt_get_unicode_funcs()); + } + hb_buffer_pre_allocate(buffer, itemLength); if (Q_UNLIKELY(!hb_buffer_allocation_successful(buffer))) { hb_buffer_destroy(buffer); + buffer = nullptr; return 0; } @@ -1671,26 +1676,26 @@ int QTextEngine::shapeTextWithHarfbuzzNG(const QScriptItem &si, const ushort *st bool dontLigate = hasLetterSpacing && !scriptRequiresOpenType; - QHash<QFont::Tag, quint32> features; - features.insert(QFont::Tag("kern"), !!kerningEnabled); + QVarLengthFlatMap<QFont::Tag, hb_feature_t, 16> features; + auto insertFeature = [&features](QFont::Tag tag, quint32 value) { + features.insert(tag, { tag.value(), + value, + HB_FEATURE_GLOBAL_START, + HB_FEATURE_GLOBAL_END }); + }; + // fontFeatures have precedence + for (const auto &[tag, value]: fontFeatures.asKeyValueRange()) + insertFeature(tag, value); + insertFeature(QFont::Tag("kern"), !!kerningEnabled); if (dontLigate) { - features.insert(QFont::Tag("liga"), false); - features.insert(QFont::Tag("clig"), false); - features.insert(QFont::Tag("dlig"), false); - features.insert(QFont::Tag("hlig"), false); - } - features.insert(fontFeatures); - - QVarLengthArray<hb_feature_t, 16> featureArray; - for (auto it = features.constBegin(); it != features.constEnd(); ++it) { - featureArray.append({ it.key().value(), - it.value(), - HB_FEATURE_GLOBAL_START, - HB_FEATURE_GLOBAL_END }); + insertFeature(QFont::Tag("liga"), false); + insertFeature(QFont::Tag("clig"), false); + insertFeature(QFont::Tag("dlig"), false); + insertFeature(QFont::Tag("hlig"), false); } // whitelist cross-platforms shapers only - static const char *shaper_list[] = { + constexpr const char *shaper_list[] = { "graphite2", "ot", "fallback", @@ -1699,13 +1704,11 @@ int QTextEngine::shapeTextWithHarfbuzzNG(const QScriptItem &si, const ushort *st bool shapedOk = hb_shape_full(hb_font, buffer, - featureArray.constData(), - features.size(), + features.values().constData(), + features.values().size(), shaper_list); - if (Q_UNLIKELY(!shapedOk)) { - hb_buffer_destroy(buffer); + if (Q_UNLIKELY(!shapedOk)) return 0; - } if (Q_UNLIKELY(HB_DIRECTION_IS_BACKWARD(props.direction))) hb_buffer_reverse(buffer); @@ -1718,10 +1721,8 @@ int QTextEngine::shapeTextWithHarfbuzzNG(const QScriptItem &si, const ushort *st num_glyphs = 1; // ensure we have enough space for shaped glyphs and metrics - if (Q_UNLIKELY(!ensureSpace(glyphs_shaped + num_glyphs))) { - hb_buffer_destroy(buffer); + if (Q_UNLIKELY(!ensureSpace(glyphs_shaped + num_glyphs))) return 0; - } // fetch the shaped glyphs and metrics QGlyphLayout g = availableGlyphs(&si).mid(glyphs_shaped, num_glyphs); @@ -1781,8 +1782,6 @@ int QTextEngine::shapeTextWithHarfbuzzNG(const QScriptItem &si, const ushort *st glyphs_shaped += num_glyphs; } - hb_buffer_destroy(buffer); - return glyphs_shaped; } @@ -1826,6 +1825,12 @@ QTextEngine::~QTextEngine() delete layoutData; delete specialData; resetFontEngineCache(); +#if QT_CONFIG(harfbuzz) + if (buffer) { + hb_buffer_destroy(buffer); + buffer = nullptr; + } +#endif } const QCharAttributes *QTextEngine::attributes() const diff --git a/src/gui/text/qtextengine_p.h b/src/gui/text/qtextengine_p.h index dffbc129b3a..e513fd598ba 100644 --- a/src/gui/text/qtextengine_p.h +++ b/src/gui/text/qtextengine_p.h @@ -40,6 +40,8 @@ #include <stdlib.h> #include <vector> +struct hb_buffer_t; + QT_BEGIN_NAMESPACE class QFontPrivate; @@ -583,6 +585,8 @@ private: void indexFormats(); void resolveFormats() const; + mutable hb_buffer_t *buffer = nullptr; + public: bool atWordSeparator(int position) const; diff --git a/src/network/access/qnetworkreplyhttpimpl.cpp b/src/network/access/qnetworkreplyhttpimpl.cpp index f680950701c..f76d79571c3 100644 --- a/src/network/access/qnetworkreplyhttpimpl.cpp +++ b/src/network/access/qnetworkreplyhttpimpl.cpp @@ -272,6 +272,11 @@ void QNetworkReplyHttpImpl::close() void QNetworkReplyHttpImpl::abort() { + abortImpl(QNetworkReply::OperationCanceledError); +} + +void QNetworkReplyHttpImpl::abortImpl(QNetworkReply::NetworkError error) +{ Q_D(QNetworkReplyHttpImpl); // FIXME if (d->state == QNetworkReplyPrivate::Finished || d->state == QNetworkReplyPrivate::Aborted) @@ -282,7 +287,8 @@ void QNetworkReplyHttpImpl::abort() if (d->state != QNetworkReplyPrivate::Finished) { // call finished which will emit signals // FIXME shouldn't this be emitted Queued? - d->error(OperationCanceledError, tr("Operation canceled")); + d->error(error, + error == TimeoutError ? tr("Operation timed out") : tr("Operation canceled")); d->finished(); } @@ -2120,7 +2126,7 @@ void QNetworkReplyHttpImplPrivate::_q_bufferOutgoingData() void QNetworkReplyHttpImplPrivate::_q_transferTimedOut() { Q_Q(QNetworkReplyHttpImpl); - q->abort(); + q->abortImpl(QNetworkReply::TimeoutError); } void QNetworkReplyHttpImplPrivate::setupTransferTimeout() @@ -2242,8 +2248,10 @@ void QNetworkReplyHttpImplPrivate::error(QNetworkReplyImpl::NetworkError code, c // Can't set and emit multiple errors. if (errorCode != QNetworkReply::NoError) { // But somewhat unavoidable if we have cancelled the request: - if (errorCode != QNetworkReply::OperationCanceledError) + if (errorCode != QNetworkReply::OperationCanceledError + && errorCode != QNetworkReply::TimeoutError) { qWarning("QNetworkReplyImplPrivate::error: Internal problem, this method must only be called once."); + } return; } diff --git a/src/network/access/qnetworkreplyhttpimpl_p.h b/src/network/access/qnetworkreplyhttpimpl_p.h index 0d16d02ff53..a354b388ad6 100644 --- a/src/network/access/qnetworkreplyhttpimpl_p.h +++ b/src/network/access/qnetworkreplyhttpimpl_p.h @@ -59,6 +59,7 @@ public: void close() override; void abort() override; + void abortImpl(QNetworkReply::NetworkError error); qint64 bytesAvailable() const override; bool isSequential () const override; qint64 size() const override; diff --git a/src/plugins/networkinformation/glib/qglibnetworkinformationbackend.cpp b/src/plugins/networkinformation/glib/qglibnetworkinformationbackend.cpp index 62d24eefd33..e5ada714ba5 100644 --- a/src/plugins/networkinformation/glib/qglibnetworkinformationbackend.cpp +++ b/src/plugins/networkinformation/glib/qglibnetworkinformationbackend.cpp @@ -110,7 +110,8 @@ QGlibNetworkInformationBackend::QGlibNetworkInformationBackend() connectivityHandlerId = g_signal_connect_swapped(networkMonitor, "notify::connectivity", G_CALLBACK(updateConnectivity), this); - networkHandlerId = g_signal_connect_swapped(networkMonitor, "network-changed", + // needed until GLib 2.86 for netlink support + networkHandlerId = g_signal_connect_swapped(networkMonitor, "notify::network-available", G_CALLBACK(updateConnectivity), this); meteredHandlerId = g_signal_connect_swapped(networkMonitor, "notify::network-metered", diff --git a/src/plugins/platforms/cocoa/qcocoaaccessibility.mm b/src/plugins/platforms/cocoa/qcocoaaccessibility.mm index 08c9f5d5ba2..2989b4d6df3 100644 --- a/src/plugins/platforms/cocoa/qcocoaaccessibility.mm +++ b/src/plugins/platforms/cocoa/qcocoaaccessibility.mm @@ -161,6 +161,7 @@ static void populateRoleMap() roleMap[QAccessible::Graphic] = NSAccessibilityImageRole; roleMap[QAccessible::Tree] = NSAccessibilityOutlineRole; roleMap[QAccessible::BlockQuote] = NSAccessibilityGroupRole; + roleMap[QAccessible::LayeredPane] = NSAccessibilityGroupRole; } /* diff --git a/src/plugins/platforms/cocoa/qcocoafiledialoghelper.mm b/src/plugins/platforms/cocoa/qcocoafiledialoghelper.mm index e0ef6cec794..4c4e5fac962 100644 --- a/src/plugins/platforms/cocoa/qcocoafiledialoghelper.mm +++ b/src/plugins/platforms/cocoa/qcocoafiledialoghelper.mm @@ -491,7 +491,7 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions; return; if (m_panel.visible) { - const QString selection = QString::fromNSString(m_panel.URL.path); + const QString selection = QString::fromNSString(m_panel.URL.path).normalized(QString::NormalizationForm_C); if (selection != m_currentSelection) { m_currentSelection = selection; emit m_helper->currentChanged(QUrl::fromLocalFile(selection)); diff --git a/src/plugins/platforms/cocoa/qcocoamessagedialog.mm b/src/plugins/platforms/cocoa/qcocoamessagedialog.mm index dab348beaa4..7a6f010ba8f 100644 --- a/src/plugins/platforms/cocoa/qcocoamessagedialog.mm +++ b/src/plugins/platforms/cocoa/qcocoamessagedialog.mm @@ -88,6 +88,11 @@ bool QCocoaMessageDialog::show(Qt::WindowFlags windowFlags, Qt::WindowModality w return false; } + // Tahoe has issues with window-modal alert buttons not responding to mouse + if (windowModality == Qt::WindowModal + && QOperatingSystemVersion::current() >= QOperatingSystemVersion::MacOSTahoe) + return false; + // And without options we don't know what to show if (!options()) return false; diff --git a/src/plugins/platforms/ios/qiostheme.h b/src/plugins/platforms/ios/qiostheme.h index b7ff3bb2a58..7af9cf80355 100644 --- a/src/plugins/platforms/ios/qiostheme.h +++ b/src/plugins/platforms/ios/qiostheme.h @@ -26,6 +26,7 @@ public: Qt::ColorScheme colorScheme() const override; void requestColorScheme(Qt::ColorScheme scheme) override; + Qt::ContrastPreference contrastPreference() const override; #if !defined(Q_OS_TVOS) && !defined(Q_OS_VISIONOS) QPlatformMenuItem* createPlatformMenuItem() const override; diff --git a/src/plugins/platforms/ios/qiostheme.mm b/src/plugins/platforms/ios/qiostheme.mm index cdf669f921c..435c21bf579 100644 --- a/src/plugins/platforms/ios/qiostheme.mm +++ b/src/plugins/platforms/ios/qiostheme.mm @@ -202,6 +202,12 @@ void QIOSTheme::requestColorScheme(Qt::ColorScheme scheme) #endif } +Qt::ContrastPreference QIOSTheme::contrastPreference() const +{ + return UIAccessibilityDarkerSystemColorsEnabled() ? Qt::ContrastPreference::HighContrast : Qt::ContrastPreference::NoPreference; +} + + void QIOSTheme::applyTheme(UIWindow *window) { const UIUserInterfaceStyle style = []{ diff --git a/src/plugins/platforms/ios/quiwindow.mm b/src/plugins/platforms/ios/quiwindow.mm index bb4268e1c88..62fd9c5d770 100644 --- a/src/plugins/platforms/ios/quiwindow.mm +++ b/src/plugins/platforms/ios/quiwindow.mm @@ -54,7 +54,9 @@ if (self.screen == UIScreen.mainScreen) { // Check if the current userInterfaceStyle reports a different appearance than // the platformTheme's appearance. We might have set that one based on the UIScreen + // Check for changes in the "Increase contrast" setting too. if (previousTraitCollection.userInterfaceStyle != self.traitCollection.userInterfaceStyle + || previousTraitCollection.accessibilityContrast != self.traitCollection.accessibilityContrast || QGuiApplicationPrivate::platformTheme()->colorScheme() != colorScheme) { QIOSTheme::initializeSystemPalette(); QWindowSystemInterface::handleThemeChange<QWindowSystemInterface::SynchronousDelivery>(); diff --git a/src/plugins/platforms/wasm/qwasmaccessibility.cpp b/src/plugins/platforms/wasm/qwasmaccessibility.cpp index a87c33c8346..5807d157636 100644 --- a/src/plugins/platforms/wasm/qwasmaccessibility.cpp +++ b/src/plugins/platforms/wasm/qwasmaccessibility.cpp @@ -586,14 +586,7 @@ void QWasmAccessibility::linkToParent(QAccessibleInterface *iface) void QWasmAccessibility::setHtmlElementVisibility(QAccessibleInterface *iface, bool visible) { emscripten::val element = getHtmlElement(iface); - - if (visible) { - setAttribute(element, "aria-hidden", false); - setAttribute(element, "tabindex", ""); - } else { - setAttribute(element, "aria-hidden", true); // aria-hidden mean completely hidden; maybe some sort of soft-hidden should be used. - setAttribute(element, "tabindex", "-1"); - } + setAttribute(element, "aria-hidden", !visible); } void QWasmAccessibility::setHtmlElementGeometry(QAccessibleInterface *iface) diff --git a/src/plugins/platforms/wasm/qwasminputcontext.cpp b/src/plugins/platforms/wasm/qwasminputcontext.cpp index 18a457198f1..a0546fdc215 100644 --- a/src/plugins/platforms/wasm/qwasminputcontext.cpp +++ b/src/plugins/platforms/wasm/qwasminputcontext.cpp @@ -40,8 +40,23 @@ void QWasmInputContext::inputCallback(emscripten::val event) // Some of them should be implemented here later. qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << "inputType : " << inputTypeString; if (!inputTypeString.compare("deleteContentBackward")) { - QWindowSystemInterface::handleKeyEvent(0, QEvent::KeyPress, Qt::Key_Backspace, Qt::NoModifier); - QWindowSystemInterface::handleKeyEvent(0, QEvent::KeyRelease, Qt::Key_Backspace, Qt::NoModifier); + + 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) { + deleteFrom = -(cursorPosition - rangesPair.first); + } + QInputMethodEvent e; + e.setCommitString(QString(), deleteFrom, deleteLength); + QCoreApplication::sendEvent(m_focusObject, &e); + + rangesPair.first = 0; + rangesPair.second = 0; + event.call<void>("stopImmediatePropagation"); return; } else if (!inputTypeString.compare("deleteContentForward")) { @@ -138,41 +153,23 @@ void QWasmInputContext::compositionUpdateCallback(emscripten::val event) const auto compositionStr = QString::fromEcmaString(event["data"]); qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << compositionStr; - // WA for IOS. - // Not sure now because I cannot test it anymore. -// int replaceSize = 0; -// emscripten::val win = emscripten::val::global("window"); -// emscripten::val sel = win.call<emscripten::val>("getSelection"); -// if (!sel.isNull() && !sel.isUndefined() -// && sel["rangeCount"].as<int>() > 0) { -// QInputMethodQueryEvent queryEvent(Qt::ImQueryAll); -// QCoreApplication::sendEvent(QGuiApplication::focusObject(), &queryEvent); -// qCDebug(qLcQpaWasmInputContext) << "Qt surrounding text: " << queryEvent.value(Qt::ImSurroundingText).toString(); -// qCDebug(qLcQpaWasmInputContext) << "Qt current selection: " << queryEvent.value(Qt::ImCurrentSelection).toString(); -// qCDebug(qLcQpaWasmInputContext) << "Qt text before cursor: " << queryEvent.value(Qt::ImTextBeforeCursor).toString(); -// qCDebug(qLcQpaWasmInputContext) << "Qt text after cursor: " << queryEvent.value(Qt::ImTextAfterCursor).toString(); -// -// const QString &selectedStr = QString::fromEcmaString(sel.call<emscripten::val>("toString")); -// const auto &preeditStr = preeditString(); -// qCDebug(qLcQpaWasmInputContext) << "Selection.type : " << sel["type"].as<std::string>(); -// qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << "Selected: " << selectedStr; -// qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << "PreeditString: " << preeditStr; -// if (!sel["type"].as<std::string>().compare("Range")) { -// QString surroundingTextBeforeCursor = queryEvent.value(Qt::ImTextBeforeCursor).toString(); -// if (surroundingTextBeforeCursor.endsWith(selectedStr)) { -// replaceSize = selectedStr.size(); -// qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << "Current Preedit: " << preeditStr << replaceSize; -// } -// } -// emscripten::val range = sel.call<emscripten::val>("getRangeAt", 0); -// qCDebug(qLcQpaWasmInputContext) << "Range.startOffset : " << range["startOffset"].as<int>(); -// qCDebug(qLcQpaWasmInputContext) << "Range.endOffset : " << range["endOffset"].as<int>(); -// } -// -// setPreeditString(compositionStr, replaceSize); setPreeditString(compositionStr, 0); } +void QWasmInputContext::beforeInputCallback(emscripten::val event) +{ + emscripten::val ranges = event.call<emscripten::val>("getTargetRanges"); + + auto length = ranges["length"].as<int>(); + for (auto i = 0; i < length; i++) { + emscripten::val range = ranges[i]; + qCDebug(qLcQpaWasmInputContext) << "startOffset" << range["startOffset"].as<int>(); + qCDebug(qLcQpaWasmInputContext) << "endOffset" << range["endOffset"].as<int>(); + rangesPair.first = range["startOffset"].as<int>(); + rangesPair.second = range["endOffset"].as<int>(); + } +} + QWasmInputContext::QWasmInputContext() { qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO; diff --git a/src/plugins/platforms/wasm/qwasminputcontext.h b/src/plugins/platforms/wasm/qwasminputcontext.h index 6d24c7fea0d..97415451b2a 100644 --- a/src/plugins/platforms/wasm/qwasminputcontext.h +++ b/src/plugins/platforms/wasm/qwasminputcontext.h @@ -45,6 +45,7 @@ public: void compositionEndCallback(emscripten::val event); void compositionStartCallback(emscripten::val event); void compositionUpdateCallback(emscripten::val event); + void beforeInputCallback(emscripten::val event); void updateGeometry(); @@ -62,6 +63,7 @@ private: bool m_inputMethodAccepted = false; QObject *m_focusObject = nullptr; emscripten::val m_inputElement = emscripten::val::null(); + QPair<int, int> rangesPair; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmwindow.cpp b/src/plugins/platforms/wasm/qwasmwindow.cpp index 04f52ac9b1b..e49c1dc49f1 100644 --- a/src/plugins/platforms/wasm/qwasmwindow.cpp +++ b/src/plugins/platforms/wasm/qwasmwindow.cpp @@ -110,6 +110,7 @@ QWasmWindow::QWasmWindow(QWindow *w, QWasmDeadKeySupport *deadKeySupport, // Set up m_inputElement, which takes focus whenever a Qt text input UI element has // foucus. m_inputElement["classList"].call<void>("add", emscripten::val("qt-window-input-element")); + m_inputElement.call<void>("setAttribute", std::string("contenteditable"), std::string("true")); m_inputElement.set("type", "text"); m_inputElement["style"].set("position", "absolute"); m_inputElement["style"].set("left", 0); @@ -227,6 +228,8 @@ void QWasmWindow::registerEventHandlers() [this](emscripten::val event){ handleCompositionStartEvent(event); }); m_compositionEndCallback = QWasmEventHandler(m_window, "compositionend", [this](emscripten::val event){ handleCompositionEndEvent(event); }); + m_beforeInputCallback = QWasmEventHandler(m_window, "beforeinput", + [this](emscripten::val event){ handleBeforeInputEvent(event); }); } QWasmWindow::~QWasmWindow() @@ -789,6 +792,16 @@ void QWasmWindow::handleCompositionEndEvent(emscripten::val event) m_focusHelper.set("innerHTML", std::string()); } +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()); +} + void QWasmWindow::handlePointerEnterLeaveEvent(const PointerEvent &event) { if (processPointerEnterLeave(event)) diff --git a/src/plugins/platforms/wasm/qwasmwindow.h b/src/plugins/platforms/wasm/qwasmwindow.h index cbe930dce89..8e6e5021dcf 100644 --- a/src/plugins/platforms/wasm/qwasmwindow.h +++ b/src/plugins/platforms/wasm/qwasmwindow.h @@ -145,6 +145,7 @@ private: void handleCompositionStartEvent(emscripten::val event); void handleCompositionUpdateEvent(emscripten::val event); void handleCompositionEndEvent(emscripten::val event); + void handleBeforeInputEvent(emscripten::val event); void handlePointerEnterLeaveEvent(const PointerEvent &event); bool processPointerEnterLeave(const PointerEvent &event); @@ -183,6 +184,7 @@ private: QWasmEventHandler m_compositionStartCallback; QWasmEventHandler m_compositionUpdateCallback; QWasmEventHandler m_compositionEndCallback; + QWasmEventHandler m_beforeInputCallback; QWasmEventHandler m_pointerDownCallback; QWasmEventHandler m_pointerMoveCallback; diff --git a/src/plugins/platforms/wayland/CMakeLists.txt b/src/plugins/platforms/wayland/CMakeLists.txt index 7e3589def6b..0ce9a4b091c 100644 --- a/src/plugins/platforms/wayland/CMakeLists.txt +++ b/src/plugins/platforms/wayland/CMakeLists.txt @@ -17,14 +17,6 @@ qt_internal_add_module(WaylandGlobalPrivate NO_GENERATE_CPP_EXPORTS ) -# Work around 115101. -# If nothing depends on the WaylandGlobalPrivate target it doesn't run custom commands that the -# target depends on. WaylandGlobalPrivate_ensure_sync_headers makes sure that 'all' depends on -# WaylandGlobalPrivate_sync_headers. -# TODO: This needs to be removed once the fix for QTBUG-115101 is merged in qtbase. -add_custom_target(WaylandGlobalPrivate_ensure_sync_headers ALL) -add_dependencies(WaylandGlobalPrivate_ensure_sync_headers WaylandGlobalPrivate_sync_headers) - # special case begin # TODO: Ideally these macros would be part of the qtwaylandscanner tool, and not the compositor/client include(../../../../src/tools/qtwaylandscanner/Qt6WaylandClientMacros.cmake) diff --git a/src/plugins/platforms/wayland/qwaylandinputcontext.cpp b/src/plugins/platforms/wayland/qwaylandinputcontext.cpp index 0ccc4dba57a..5ab285ad97d 100644 --- a/src/plugins/platforms/wayland/qwaylandinputcontext.cpp +++ b/src/plugins/platforms/wayland/qwaylandinputcontext.cpp @@ -192,10 +192,12 @@ void QWaylandInputContext::setFocusObject(QObject *object) if (window && window->handle()) { if (mCurrentWindow.data() != window) { if (!inputMethodAccepted()) { - auto *surface = static_cast<QWaylandWindow *>(window->handle())->wlSurface(); - if (surface) - inputInterface->disableSurface(surface); - mCurrentWindow.clear(); + if (mCurrentWindow) { + auto *surface = static_cast<QWaylandWindow *>(mCurrentWindow->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/qwindowsiconengine.cpp b/src/plugins/platforms/windows/qwindowsiconengine.cpp index 71103183183..edc1ea3a9dc 100644 --- a/src/plugins/platforms/windows/qwindowsiconengine.cpp +++ b/src/plugins/platforms/windows/qwindowsiconengine.cpp @@ -63,8 +63,8 @@ static QString getGlyphs(QStringView iconName) {"go-home"_L1, u"\ue80f"}, // {"go-jump"_L1, u"\uf719"}, //{"go-last"_L1, u"\ue5dd"}, - {"go-next"_L1, u"\ue893"}, - {"go-previous"_L1, u"\ue892"}, + {"go-next"_L1, u"\ue72a"}, + {"go-previous"_L1, u"\ue72b"}, //{"go-top"_L1, u"\ue25a"}, {"go-up"_L1, u"\ue74a"}, {"help-about"_L1, u"\ue946"}, @@ -100,7 +100,7 @@ static QString getGlyphs(QStringView iconName) //{"object-flip-vertical"_L1, u"\u"}, {"object-rotate-left"_L1, u"\ue80c"}, {"object-rotate-right"_L1, u"\ue80d"}, - //{"process-stop"_L1, u"\ue5c9"}, + {"process-stop"_L1, u"\uf140"}, {"system-lock-screen"_L1, u"\uee3f"}, {"system-log-out"_L1, u"\uf3b1"}, //{"system-run"_L1, u"\u"}, diff --git a/src/plugins/platforms/windows/qwindowsscreen.cpp b/src/plugins/platforms/windows/qwindowsscreen.cpp index 482810d5b7e..7908f22d99d 100644 --- a/src/plugins/platforms/windows/qwindowsscreen.cpp +++ b/src/plugins/platforms/windows/qwindowsscreen.cpp @@ -874,9 +874,8 @@ const QWindowsScreen *QWindowsScreenManager::screenAtDp(const QPoint &p) const return nullptr; } -const QWindowsScreen *QWindowsScreenManager::screenForHwnd(HWND hwnd) const +const QWindowsScreen *QWindowsScreenManager::screenForMonitor(HMONITOR hMonitor) const { - HMONITOR hMonitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONULL); if (hMonitor == nullptr) return nullptr; const auto it = @@ -889,4 +888,18 @@ const QWindowsScreen *QWindowsScreenManager::screenForHwnd(HWND hwnd) const return it != m_screens.cend() ? *it : nullptr; } +const QWindowsScreen *QWindowsScreenManager::screenForHwnd(HWND hwnd) const +{ + HMONITOR hMonitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONULL); + return screenForMonitor(hMonitor); +} + +const QWindowsScreen *QWindowsScreenManager::screenForRect(const RECT *rect) const +{ + if (rect == nullptr) + return nullptr; + HMONITOR hMonitor = MonitorFromRect(rect, MONITOR_DEFAULTTONULL); + return screenForMonitor(hMonitor); +} + QT_END_NAMESPACE diff --git a/src/plugins/platforms/windows/qwindowsscreen.h b/src/plugins/platforms/windows/qwindowsscreen.h index 8d555998388..0032fd462cb 100644 --- a/src/plugins/platforms/windows/qwindowsscreen.h +++ b/src/plugins/platforms/windows/qwindowsscreen.h @@ -115,12 +115,14 @@ public: const QWindowsScreen *screenAtDp(const QPoint &p) const; const QWindowsScreen *screenForHwnd(HWND hwnd) const; + const QWindowsScreen *screenForRect(const RECT *rect) const; static bool isSingleScreen(); private: void addScreen(const QWindowsScreenData &screenData); void removeScreen(int index); + const QWindowsScreen *screenForMonitor(HMONITOR monitor) const; HWND m_displayChangeObserver = nullptr; WindowsScreenList m_screens; diff --git a/src/plugins/platforms/windows/qwindowswindow.cpp b/src/plugins/platforms/windows/qwindowswindow.cpp index 01716fba60c..72daffb56b1 100644 --- a/src/plugins/platforms/windows/qwindowswindow.cpp +++ b/src/plugins/platforms/windows/qwindowswindow.cpp @@ -466,15 +466,17 @@ static bool applyBlurBehindWindow(HWND hwnd) return result; } +static bool shouldShowTitlebarButton(Qt::WindowFlags flags, Qt::WindowFlags button) +{ + return !flags.testFlag(Qt::CustomizeWindowHint) || flags.testFlags(Qt::CustomizeWindowHint | button); +} + // from qwidget_win.cpp, pass flags separately in case they have been "autofixed". static bool shouldShowMaximizeButton(const QWindow *w, Qt::WindowFlags flags) { - if ((flags & Qt::MSWindowsFixedSizeDialogHint) || !(flags & Qt::WindowMaximizeButtonHint)) - return false; - // if the user explicitly asked for the maximize button, we try to add - // it even if the window has fixed size. - return (flags & Qt::CustomizeWindowHint) || - w->maximumSize() == QSize(QWINDOWSIZE_MAX, QWINDOWSIZE_MAX); + return !flags.testFlag(Qt::MSWindowsFixedSizeDialogHint) && + (shouldShowTitlebarButton(flags, Qt::WindowMaximizeButtonHint) || + w->maximumSize() == QSize(QWINDOWSIZE_MAX, QWINDOWSIZE_MAX)); } bool QWindowsWindow::hasNoNativeFrame(HWND hwnd, Qt::WindowFlags flags) @@ -805,6 +807,7 @@ void WindowCreationData::fromWindow(const QWindow *w, const Qt::WindowFlags flag if (topLevel) { if ((type == Qt::Window || dialog || tool)) { + const bool defaultTitlebar = !flags.testFlag(Qt::CustomizeWindowHint); if (!(flags & Qt::FramelessWindowHint)) { style |= WS_POPUP; if (flags & Qt::MSWindowsFixedSizeDialogHint) { @@ -812,16 +815,16 @@ void WindowCreationData::fromWindow(const QWindow *w, const Qt::WindowFlags flag } else { style |= WS_THICKFRAME; } - if (flags & Qt::WindowTitleHint) + if (defaultTitlebar || flags.testFlags(Qt::CustomizeWindowHint | Qt::WindowTitleHint)) style |= WS_CAPTION; // Contains WS_DLGFRAME } - if (flags & Qt::WindowSystemMenuHint) + if (defaultTitlebar || flags.testFlags(Qt::CustomizeWindowHint | Qt::WindowSystemMenuHint)) style |= WS_SYSMENU; - else if (dialog && (flags & Qt::WindowCloseButtonHint) && !(flags & Qt::FramelessWindowHint)) { + else if (dialog && (defaultTitlebar || flags.testFlags(Qt::CustomizeWindowHint | Qt::WindowCloseButtonHint)) && !(flags & Qt::FramelessWindowHint)) { style |= WS_SYSMENU | WS_BORDER; // QTBUG-2027, dialogs without system menu. exStyle |= WS_EX_DLGMODALFRAME; } - const bool showMinimizeButton = flags & Qt::WindowMinimizeButtonHint; + const bool showMinimizeButton = shouldShowTitlebarButton(flags, Qt::WindowMinimizeButtonHint); if (showMinimizeButton) style |= WS_MINIMIZEBOX; const bool showMaximizeButton = shouldShowMaximizeButton(w, flags); @@ -2113,7 +2116,8 @@ void QWindowsWindow::handleDpiChanged(HWND hwnd, WPARAM wParam, LPARAM lParam) QWindowsThemeCache::clearThemeCache(hwnd); // Send screen change first, so that the new screen is set during any following resize - checkForScreenChanged(QWindowsWindow::FromDpiChange); + const auto prcNewWindow = reinterpret_cast<const RECT *>(lParam); + checkForScreenChanged(QWindowsWindow::FromDpiChange, !m_inSetgeometry ? prcNewWindow : nullptr); if (!IsZoomed(hwnd)) m_data.restoreGeometry.setSize(m_data.restoreGeometry.size() * scale); @@ -2136,7 +2140,6 @@ void QWindowsWindow::handleDpiChanged(HWND hwnd, WPARAM wParam, LPARAM lParam) // making the SetWindowPos() call. if (!m_inSetgeometry) { updateFullFrameMargins(); - const auto prcNewWindow = reinterpret_cast<RECT *>(lParam); SetWindowPos(hwnd, nullptr, prcNewWindow->left, prcNewWindow->top, prcNewWindow->right - prcNewWindow->left, prcNewWindow->bottom - prcNewWindow->top, SWP_NOZORDER | SWP_NOACTIVATE); @@ -2351,14 +2354,15 @@ static inline bool equalDpi(const QDpi &d1, const QDpi &d2) return qFuzzyCompare(d1.first, d2.first) && qFuzzyCompare(d1.second, d2.second); } -void QWindowsWindow::checkForScreenChanged(ScreenChangeMode mode) +void QWindowsWindow::checkForScreenChanged(ScreenChangeMode mode, const RECT *suggestedRect) { if ((parent() && !parent()->isForeignWindow()) || QWindowsScreenManager::isSingleScreen()) return; QPlatformScreen *currentScreen = screen(); auto topLevel = isTopLevel_sys() ? m_data.hwnd : GetAncestor(m_data.hwnd, GA_ROOT); - const QWindowsScreen *newScreen = + const QWindowsScreen *newScreen = suggestedRect ? + QWindowsContext::instance()->screenManager().screenForRect(suggestedRect) : QWindowsContext::instance()->screenManager().screenForHwnd(topLevel); if (newScreen == nullptr || newScreen == currentScreen) diff --git a/src/plugins/platforms/windows/qwindowswindow.h b/src/plugins/platforms/windows/qwindowswindow.h index 499b2a9ca86..ee348f1ac16 100644 --- a/src/plugins/platforms/windows/qwindowswindow.h +++ b/src/plugins/platforms/windows/qwindowswindow.h @@ -342,7 +342,7 @@ public: void stopAlertWindow(); enum ScreenChangeMode { FromGeometryChange, FromDpiChange, FromScreenAdded }; - void checkForScreenChanged(ScreenChangeMode mode = FromGeometryChange); + void checkForScreenChanged(ScreenChangeMode mode = FromGeometryChange, const RECT *suggestedRect = nullptr); void registerTouchWindow(); static void setHasBorderInFullScreenStatic(QWindow *window, bool border); diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiautils.cpp b/src/plugins/platforms/windows/uiautomation/qwindowsuiautils.cpp index b2675d5b884..835499d3554 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiautils.cpp +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiautils.cpp @@ -154,6 +154,7 @@ long roleToControlTypeId(QAccessible::Role role) {QAccessible::WebDocument, UIA_DocumentControlTypeId}, {QAccessible::Heading, UIA_TextControlTypeId}, {QAccessible::BlockQuote, UIA_GroupControlTypeId}, + {QAccessible::LayeredPane, UIA_PaneControlTypeId}, }; long controlType = mapping.value(role, UIA_CustomControlTypeId); diff --git a/src/plugins/platforms/xcb/CMakeLists.txt b/src/plugins/platforms/xcb/CMakeLists.txt index 492d274c2fd..204acd72ba0 100644 --- a/src/plugins/platforms/xcb/CMakeLists.txt +++ b/src/plugins/platforms/xcb/CMakeLists.txt @@ -120,30 +120,6 @@ qt_internal_extend_target(XcbQpaPrivate CONDITION CLANG -ftemplate-depth=1024 ) -qt_internal_extend_target(XcbQpaPrivate CONDITION QT_FEATURE_xcb_native_painting - SOURCES - nativepainting/qbackingstore_x11.cpp nativepainting/qbackingstore_x11_p.h - nativepainting/qcolormap_x11.cpp nativepainting/qcolormap_x11_p.h - nativepainting/qpaintengine_x11.cpp nativepainting/qpaintengine_x11_p.h - nativepainting/qpixmap_x11.cpp nativepainting/qpixmap_x11_p.h - nativepainting/qpolygonclipper_p.h - nativepainting/qt_x11_p.h - nativepainting/qtessellator.cpp nativepainting/qtessellator_p.h - nativepainting/qxcbnativepainting.cpp nativepainting/qxcbnativepainting.h - INCLUDE_DIRECTORIES - nativepainting -) - -qt_internal_extend_target(XcbQpaPrivate CONDITION QT_FEATURE_xcb_native_painting AND QT_FEATURE_xrender - PUBLIC_LIBRARIES - PkgConfig::XRender -) - -qt_internal_extend_target(XcbQpaPrivate CONDITION QT_FEATURE_fontconfig AND QT_FEATURE_xcb_native_painting - LIBRARIES - WrapFreetype::WrapFreetype -) - if(QT_FEATURE_system_xcb_xinput) qt_internal_extend_target(XcbQpaPrivate LIBRARIES XCB::XINPUT) else() diff --git a/src/plugins/platforms/xcb/nativepainting/qbackingstore_x11.cpp b/src/plugins/platforms/xcb/nativepainting/qbackingstore_x11.cpp deleted file mode 100644 index 1ac5f6a64bc..00000000000 --- a/src/plugins/platforms/xcb/nativepainting/qbackingstore_x11.cpp +++ /dev/null @@ -1,175 +0,0 @@ -// Copyright (C) 2018 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 "qbackingstore_x11_p.h" -#include "qxcbwindow.h" -#include "qpixmap_x11_p.h" - -#include <private/qhighdpiscaling_p.h> -#include <QPainter> - -#if QT_CONFIG(xrender) -# include <X11/extensions/Xrender.h> -#endif - -#define register /* C++17 deprecated register */ -#include <X11/Xlib.h> -#undef register - -QT_BEGIN_NAMESPACE - -QXcbNativeBackingStore::QXcbNativeBackingStore(QWindow *window) - : QPlatformBackingStore(window) - , m_translucentBackground(false) -{ - if (QXcbWindow *w = static_cast<QXcbWindow *>(window->handle())) { - m_translucentBackground = w->connection()->hasXRender() && - QImage::toPixelFormat(w->imageFormat()).alphaUsage() == QPixelFormat::UsesAlpha; - } -} - -QXcbNativeBackingStore::~QXcbNativeBackingStore() -{} - -QPaintDevice *QXcbNativeBackingStore::paintDevice() -{ - return &m_pixmap; -} - -void QXcbNativeBackingStore::flush(QWindow *window, const QRegion ®ion, const QPoint &offset) -{ - if (m_pixmap.isNull()) - return; - - QSize pixmapSize = m_pixmap.size(); - - QRegion clipped = region; - clipped &= QRect(QPoint(), QHighDpi::toNativePixels(window->size(), window)); - clipped &= QRect(0, 0, pixmapSize.width(), pixmapSize.height()).translated(-offset); - - QRect br = clipped.boundingRect(); - if (br.isNull()) - return; - - QXcbWindow *platformWindow = static_cast<QXcbWindow *>(window->handle()); - if (!platformWindow) { - qWarning("QXcbBackingStore::flush: QWindow has no platform window (QTBUG-32681)"); - return; - } - - Window wid = platformWindow->xcb_window(); - Pixmap pid = qt_x11PixmapHandle(m_pixmap); - - QList<XRectangle> clipRects = qt_region_to_xrectangles(clipped); - -#if QT_CONFIG(xrender) - if (m_translucentBackground) - { - XWindowAttributes attrib; - XGetWindowAttributes(display(), wid, &attrib); - XRenderPictFormat *format = XRenderFindVisualFormat(display(), attrib.visual); - - Picture srcPic = qt_x11PictureHandle(m_pixmap); - Picture dstPic = XRenderCreatePicture(display(), wid, format, 0, 0); - - XRenderSetPictureClipRectangles(display(), dstPic, 0, 0, clipRects.constData(), clipRects.size()); - - XRenderComposite(display(), PictOpSrc, srcPic, 0L /*None*/, dstPic, br.x() + offset.x(), - br.y() + offset.y(), 0, 0, br.x(), br.y(), br.width(), br.height()); - - XRenderFreePicture(display(), dstPic); - } - else -#endif - { - GC gc = XCreateGC(display(), wid, 0, nullptr); - - if (clipRects.size() != 1) - XSetClipRectangles(display(), gc, 0, 0, clipRects.data(), clipRects.size(), YXBanded); - - XCopyArea(display(), pid, wid, gc, br.x() + offset.x(), br.y() + offset.y(), br.width(), br.height(), br.x(), br.y()); - XFreeGC(display(), gc); - } - - - if (platformWindow->needsSync()) { - platformWindow->updateSyncRequestCounter(); - } else { - XFlush(display()); - } -} - -QImage QXcbNativeBackingStore::toImage() const -{ - return m_pixmap.toImage(); -} - -void QXcbNativeBackingStore::resize(const QSize &size, const QRegion &staticContents) -{ - if (size == m_pixmap.size()) - return; - - QPixmap newPixmap(size); - -#if QT_CONFIG(xrender) - if (m_translucentBackground && newPixmap.depth() != 32) - qt_x11Pixmap(newPixmap)->convertToARGB32(); -#endif - - if (!m_pixmap.isNull()) { - Pixmap from = qt_x11PixmapHandle(m_pixmap); - Pixmap to = qt_x11PixmapHandle(newPixmap); - QRect br = staticContents.boundingRect().intersected(QRect(QPoint(0, 0), size)); - - if (!br.isEmpty()) { - GC gc = XCreateGC(display(), to, 0, nullptr); - XCopyArea(display(), from, to, gc, br.x(), br.y(), br.width(), br.height(), br.x(), br.y()); - XFreeGC(display(), gc); - } - } - - m_pixmap = newPixmap; -} - -bool QXcbNativeBackingStore::scroll(const QRegion &area, int dx, int dy) -{ - if (m_pixmap.isNull()) - return false; - - QRect rect = area.boundingRect(); - Pixmap pix = qt_x11PixmapHandle(m_pixmap); - - GC gc = XCreateGC(display(), pix, 0, nullptr); - XCopyArea(display(), pix, pix, gc, - rect.x(), rect.y(), rect.width(), rect.height(), - rect.x()+dx, rect.y()+dy); - XFreeGC(display(), gc); - return true; -} - -void QXcbNativeBackingStore::beginPaint(const QRegion ®ion) -{ - QX11PlatformPixmap *x11pm = qt_x11Pixmap(m_pixmap); - if (x11pm) - x11pm->setIsBackingStore(true); - -#if QT_CONFIG(xrender) - if (m_translucentBackground) { - const QList<XRectangle> xrects = qt_region_to_xrectangles(region); - const XRenderColor color = { 0, 0, 0, 0 }; - XRenderFillRectangles(display(), PictOpSrc, - qt_x11PictureHandle(m_pixmap), &color, - xrects.constData(), xrects.size()); - } -#else - Q_UNUSED(region); -#endif -} - -Display *QXcbNativeBackingStore::display() const -{ - return static_cast<Display *>(static_cast<QXcbWindow *>(window()->handle())->connection()->xlib_display()); -} - -QT_END_NAMESPACE diff --git a/src/plugins/platforms/xcb/nativepainting/qbackingstore_x11_p.h b/src/plugins/platforms/xcb/nativepainting/qbackingstore_x11_p.h deleted file mode 100644 index b730b076d3f..00000000000 --- a/src/plugins/platforms/xcb/nativepainting/qbackingstore_x11_p.h +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (C) 2018 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 - -#pragma once - -#include <qpa/qplatformbackingstore.h> - -typedef struct _XDisplay Display; - -QT_BEGIN_NAMESPACE - -class QXcbWindow; - -class QXcbNativeBackingStore : public QPlatformBackingStore -{ -public: - QXcbNativeBackingStore(QWindow *window); - ~QXcbNativeBackingStore(); - - QPaintDevice *paintDevice() override; - void flush(QWindow *window, const QRegion ®ion, const QPoint &offset) override; - - QImage toImage() const override; - - void resize(const QSize &size, const QRegion &staticContents) override; - bool scroll(const QRegion &area, int dx, int dy) override; - - void beginPaint(const QRegion ®ion) override; - -private: - Display *display() const; - - QPixmap m_pixmap; - bool m_translucentBackground; -}; - -QT_END_NAMESPACE diff --git a/src/plugins/platforms/xcb/nativepainting/qcolormap_x11.cpp b/src/plugins/platforms/xcb/nativepainting/qcolormap_x11.cpp deleted file mode 100644 index fddd3e03989..00000000000 --- a/src/plugins/platforms/xcb/nativepainting/qcolormap_x11.cpp +++ /dev/null @@ -1,615 +0,0 @@ -// Copyright (C) 2018 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 <QVarLengthArray> - -#include <private/qguiapplication_p.h> - -#include "qcolormap_x11_p.h" -#include "qxcbnativepainting.h" -#include "qt_x11_p.h" - -QT_BEGIN_NAMESPACE - -class QXcbColormapPrivate -{ -public: - QXcbColormapPrivate() - : ref(1), mode(QXcbColormap::Direct), depth(0), - colormap(0), defaultColormap(true), - visual(0), defaultVisual(true), - r_max(0), g_max(0), b_max(0), - r_shift(0), g_shift(0), b_shift(0) - {} - - QAtomicInt ref; - - QXcbColormap::Mode mode; - int depth; - - Colormap colormap; - bool defaultColormap; - - Visual *visual; - bool defaultVisual; - - int r_max; - int g_max; - int b_max; - - uint r_shift; - uint g_shift; - uint b_shift; - - QList<QColor> colors; - QList<int> pixels; -}; - -static uint right_align(uint v) -{ - while (!(v & 0x1)) - v >>= 1; - return v; -} - -static int cube_root(int v) -{ - if (v == 1) - return 1; - // brute force algorithm - int i = 1; - for (;;) { - const int b = i * i * i; - if (b <= v) { - ++i; - } else { - --i; - break; - } - } - return i; -} - -static Visual *find_visual(Display *display, - int screen, - int visual_class, - int visual_id, - int *depth, - bool *defaultVisual) -{ - XVisualInfo *vi, rvi; - int count; - - uint mask = VisualScreenMask; - rvi.screen = screen; - - if (visual_class != -1) { - rvi.c_class = visual_class; - mask |= VisualClassMask; - } - if (visual_id != -1) { - rvi.visualid = visual_id; - mask |= VisualIDMask; - } - - Visual *visual = DefaultVisual(display, screen); - *defaultVisual = true; - *depth = DefaultDepth(display, screen); - - vi = XGetVisualInfo(display, mask, &rvi, &count); - if (vi) { - int best = 0; - for (int x = 0; x < count; ++x) { - if (vi[x].depth > vi[best].depth) - best = x; - } - if (best >= 0 && best <= count && vi[best].visualid != XVisualIDFromVisual(visual)) { - visual = vi[best].visual; - *defaultVisual = (visual == DefaultVisual(display, screen)); - *depth = vi[best].depth; - } - } - if (vi) - XFree((char *)vi); - return visual; -} - -static void query_colormap(QXcbColormapPrivate *d, int screen) -{ - Display *display = X11->display; - - // query existing colormap - int q_colors = (((1u << d->depth) > 256u) ? 256u : (1u << d->depth)); - XColor queried[256]; - memset(queried, 0, sizeof(queried)); - for (int x = 0; x < q_colors; ++x) - queried[x].pixel = x; - XQueryColors(display, d->colormap, queried, q_colors); - - d->colors.resize(q_colors); - for (int x = 0; x < q_colors; ++x) { - if (queried[x].red == 0 - && queried[x].green == 0 - && queried[x].blue == 0 - && queried[x].pixel != BlackPixel(display, screen)) { - // unallocated color cell, skip it - continue; - } - - d->colors[x] = QColor::fromRgbF(queried[x].red / float(USHRT_MAX), - queried[x].green / float(USHRT_MAX), - queried[x].blue / float(USHRT_MAX)); - } - - // for missing colors, find the closest color in the existing colormap - Q_ASSERT(d->pixels.size()); - for (int x = 0; x < d->pixels.size(); ++x) { - if (d->pixels.at(x) != -1) - continue; - - QRgb rgb; - if (d->mode == QXcbColormap::Indexed) { - const int r = (x / (d->g_max * d->b_max)) % d->r_max; - const int g = (x / d->b_max) % d->g_max; - const int b = x % d->b_max; - rgb = qRgb((r * 0xff + (d->r_max - 1) / 2) / (d->r_max - 1), - (g * 0xff + (d->g_max - 1) / 2) / (d->g_max - 1), - (b * 0xff + (d->b_max - 1) / 2) / (d->b_max - 1)); - } else { - rgb = qRgb(x, x, x); - } - - // find closest color - int mindist = INT_MAX, best = -1; - for (int y = 0; y < q_colors; ++y) { - int r = qRed(rgb) - (queried[y].red >> 8); - int g = qGreen(rgb) - (queried[y].green >> 8); - int b = qBlue(rgb) - (queried[y].blue >> 8); - int dist = (r * r) + (g * g) + (b * b); - if (dist < mindist) { - mindist = dist; - best = y; - } - } - - Q_ASSERT(best >= 0 && best < q_colors); - if (d->visual->c_class & 1) { - XColor xcolor; - xcolor.red = queried[best].red; - xcolor.green = queried[best].green; - xcolor.blue = queried[best].blue; - xcolor.pixel = queried[best].pixel; - - if (XAllocColor(display, d->colormap, &xcolor)) { - d->pixels[x] = xcolor.pixel; - } else { - // some weird stuff is going on... - d->pixels[x] = (qGray(rgb) < 127 - ? BlackPixel(display, screen) - : WhitePixel(display, screen)); - } - } else { - d->pixels[x] = best; - } - } -} - -static void init_gray(QXcbColormapPrivate *d, int screen) -{ - d->pixels.resize(d->r_max); - - for (int g = 0; g < d->g_max; ++g) { - const int gray = (g * 0xff + (d->r_max - 1) / 2) / (d->r_max - 1); - const QRgb rgb = qRgb(gray, gray, gray); - - d->pixels[g] = -1; - - if (d->visual->c_class & 1) { - XColor xcolor; - xcolor.red = qRed(rgb) * 0x101; - xcolor.green = qGreen(rgb) * 0x101; - xcolor.blue = qBlue(rgb) * 0x101; - xcolor.pixel = 0ul; - - if (XAllocColor(X11->display, d->colormap, &xcolor)) - d->pixels[g] = xcolor.pixel; - } - } - - query_colormap(d, screen); -} - -static void init_indexed(QXcbColormapPrivate *d, int screen) -{ - d->pixels.resize(d->r_max * d->g_max * d->b_max); - - // create color cube - for (int x = 0, r = 0; r < d->r_max; ++r) { - for (int g = 0; g < d->g_max; ++g) { - for (int b = 0; b < d->b_max; ++b, ++x) { - const QRgb rgb = qRgb((r * 0xff + (d->r_max - 1) / 2) / (d->r_max - 1), - (g * 0xff + (d->g_max - 1) / 2) / (d->g_max - 1), - (b * 0xff + (d->b_max - 1) / 2) / (d->b_max - 1)); - - d->pixels[x] = -1; - - if (d->visual->c_class & 1) { - XColor xcolor; - xcolor.red = qRed(rgb) * 0x101; - xcolor.green = qGreen(rgb) * 0x101; - xcolor.blue = qBlue(rgb) * 0x101; - xcolor.pixel = 0ul; - - if (XAllocColor(X11->display, d->colormap, &xcolor)) - d->pixels[x] = xcolor.pixel; - } - } - } - } - - query_colormap(d, screen); -} - -static void init_direct(QXcbColormapPrivate *d, bool ownColormap) -{ - if (d->visual->c_class != DirectColor || !ownColormap) - return; - - // preallocate 768 on the stack, so that we don't have to malloc - // for the common case (<= 24 bpp) - QVarLengthArray<XColor, 768> colorTable(d->r_max + d->g_max + d->b_max); - int i = 0; - - for (int r = 0; r < d->r_max; ++r) { - colorTable[i].red = r << 8 | r; - colorTable[i].pixel = r << d->r_shift; - colorTable[i].flags = DoRed; - ++i; - } - - for (int g = 0; g < d->g_max; ++g) { - colorTable[i].green = g << 8 | g; - colorTable[i].pixel = g << d->g_shift; - colorTable[i].flags = DoGreen; - ++i; - } - - for (int b = 0; b < d->b_max; ++b) { - colorTable[i].blue = (b << 8 | b); - colorTable[i].pixel = b << d->b_shift; - colorTable[i].flags = DoBlue; - ++i; - } - - XStoreColors(X11->display, d->colormap, colorTable.data(), colorTable.count()); -} - -static QXcbColormap **cmaps = nullptr; - -void QXcbColormap::initialize() -{ - Display *display = X11->display; - const int screens = ScreenCount(display); - - cmaps = new QXcbColormap*[screens]; - - for (int i = 0; i < screens; ++i) { - cmaps[i] = new QXcbColormap; - QXcbColormapPrivate * const d = cmaps[i]->d; - - bool use_stdcmap = false; - int color_count = X11->color_count; - - // defaults - d->depth = DefaultDepth(display, i); - d->colormap = DefaultColormap(display, i); - d->defaultColormap = true; - d->visual = DefaultVisual(display, i); - d->defaultVisual = true; - - Visual *argbVisual = nullptr; - - if (X11->visual && i == DefaultScreen(display)) { - // only use the outside colormap on the default screen - d->visual = find_visual(display, i, X11->visual->c_class, - XVisualIDFromVisual(X11->visual), - &d->depth, &d->defaultVisual); - } else if ((X11->visual_class != -1 && X11->visual_class >= 0 && X11->visual_class < 6) - || (X11->visual_id != -1)) { - // look for a specific visual or type of visual - d->visual = find_visual(display, i, X11->visual_class, X11->visual_id, - &d->depth, &d->defaultVisual); - } else if (!X11->custom_cmap) { - XStandardColormap *stdcmap = nullptr; - int ncmaps = 0; - -#if QT_CONFIG(xrender) - if (X11->use_xrender) { - int nvi; - XVisualInfo templ; - templ.screen = i; - templ.depth = 32; - templ.c_class = TrueColor; - XVisualInfo *xvi = XGetVisualInfo(X11->display, VisualScreenMask | - VisualDepthMask | - VisualClassMask, &templ, &nvi); - for (int idx = 0; idx < nvi; ++idx) { - XRenderPictFormat *format = XRenderFindVisualFormat(X11->display, - xvi[idx].visual); - if (format->type == PictTypeDirect && format->direct.alphaMask) { - argbVisual = xvi[idx].visual; - break; - } - } - XFree(xvi); - } -#endif - if (XGetRGBColormaps(display, RootWindow(display, i), - &stdcmap, &ncmaps, XA_RGB_DEFAULT_MAP)) { - if (stdcmap) { - for (int c = 0; c < ncmaps; ++c) { - if (!stdcmap[c].red_max || - !stdcmap[c].green_max || - !stdcmap[c].blue_max || - !stdcmap[c].red_mult || - !stdcmap[c].green_mult || - !stdcmap[c].blue_mult) - continue; // invalid stdcmap - - XVisualInfo proto; - proto.visualid = stdcmap[c].visualid; - proto.screen = i; - - int nvisuals = 0; - XVisualInfo *vi = XGetVisualInfo(display, VisualIDMask | VisualScreenMask, - &proto, &nvisuals); - if (vi) { - if (nvisuals > 0) { - use_stdcmap = true; - - d->mode = ((vi[0].visual->c_class < StaticColor) - ? Gray - : ((vi[0].visual->c_class < TrueColor) - ? Indexed - : Direct)); - - d->depth = vi[0].depth; - d->colormap = stdcmap[c].colormap; - d->defaultColormap = true; - d->visual = vi[0].visual; - d->defaultVisual = (d->visual == DefaultVisual(display, i)); - - d->r_max = stdcmap[c].red_max + 1; - d->g_max = stdcmap[c].green_max + 1; - d->b_max = stdcmap[c].blue_max + 1; - - if (d->mode == Direct) { - // calculate offsets - d->r_shift = lowest_bit(d->visual->red_mask); - d->g_shift = lowest_bit(d->visual->green_mask); - d->b_shift = lowest_bit(d->visual->blue_mask); - } else { - d->r_shift = 0; - d->g_shift = 0; - d->b_shift = 0; - } - } - XFree(vi); - } - break; - } - XFree(stdcmap); - } - } - } - if (!use_stdcmap) { - switch (d->visual->c_class) { - case StaticGray: - d->mode = Gray; - - d->r_max = d->g_max = d->b_max = d->visual->map_entries; - break; - - case XGrayScale: - d->mode = Gray; - - // follow precedent set in libXmu... - if (color_count != 0) - d->r_max = d->g_max = d->b_max = color_count; - else if (d->visual->map_entries > 65000) - d->r_max = d->g_max = d->b_max = 4096; - else if (d->visual->map_entries > 4000) - d->r_max = d->g_max = d->b_max = 512; - else if (d->visual->map_entries > 250) - d->r_max = d->g_max = d->b_max = 12; - else - d->r_max = d->g_max = d->b_max = 4; - break; - - case StaticColor: - d->mode = Indexed; - - d->r_max = right_align(d->visual->red_mask) + 1; - d->g_max = right_align(d->visual->green_mask) + 1; - d->b_max = right_align(d->visual->blue_mask) + 1; - break; - - case PseudoColor: - d->mode = Indexed; - - // follow precedent set in libXmu... - if (color_count != 0) - d->r_max = d->g_max = d->b_max = cube_root(color_count); - else if (d->visual->map_entries > 65000) - d->r_max = d->g_max = d->b_max = 27; - else if (d->visual->map_entries > 4000) - d->r_max = d->g_max = d->b_max = 12; - else if (d->visual->map_entries > 250) - d->r_max = d->g_max = d->b_max = cube_root(d->visual->map_entries - 125); - else - d->r_max = d->g_max = d->b_max = cube_root(d->visual->map_entries); - break; - - case TrueColor: - case DirectColor: - d->mode = Direct; - - d->r_max = right_align(d->visual->red_mask) + 1; - d->g_max = right_align(d->visual->green_mask) + 1; - d->b_max = right_align(d->visual->blue_mask) + 1; - - d->r_shift = lowest_bit(d->visual->red_mask); - d->g_shift = lowest_bit(d->visual->green_mask); - d->b_shift = lowest_bit(d->visual->blue_mask); - break; - } - } - - bool ownColormap = false; - if (X11->colormap && i == DefaultScreen(display)) { - // only use the outside colormap on the default screen - d->colormap = X11->colormap; - d->defaultColormap = (d->colormap == DefaultColormap(display, i)); - } else if ((!use_stdcmap - && (((d->visual->c_class & 1) && X11->custom_cmap) - || d->visual != DefaultVisual(display, i))) - || d->visual->c_class == DirectColor) { - // allocate custom colormap (we always do this when using DirectColor visuals) - d->colormap = - XCreateColormap(display, RootWindow(display, i), d->visual, - d->visual->c_class == DirectColor ? AllocAll : AllocNone); - d->defaultColormap = false; - ownColormap = true; - } - - switch (d->mode) { - case Gray: - init_gray(d, i); - break; - case Indexed: - init_indexed(d, i); - break; - case Direct: - init_direct(d, ownColormap); - break; - } - - QX11InfoData *screen = X11->screens + i; - screen->depth = d->depth; - screen->visual = d->visual; - screen->defaultVisual = d->defaultVisual; - screen->colormap = d->colormap; - screen->defaultColormap = d->defaultColormap; - screen->cells = screen->visual->map_entries; - - if (argbVisual) { - X11->argbVisuals[i] = argbVisual; - X11->argbColormaps[i] = XCreateColormap(display, RootWindow(display, i), argbVisual, AllocNone); - } - - // ### - // We assume that 8bpp == pseudocolor, but this is not - // always the case (according to the X server), so we need - // to make sure that our internal data is setup in a way - // that is compatible with our assumptions - if (screen->visual->c_class == TrueColor && screen->depth == 8 && screen->cells == 8) - screen->cells = 256; - } -} - -void QXcbColormap::cleanup() -{ - Display *display = X11->display; - const int screens = ScreenCount(display); - - for (int i = 0; i < screens; ++i) - delete cmaps[i]; - - delete [] cmaps; - cmaps = 0; -} - - -QXcbColormap QXcbColormap::instance(int screen) -{ - if (screen == -1) - screen = QXcbX11Info::appScreen(); - return *cmaps[screen]; -} - -/*! \internal - Constructs a new colormap. -*/ -QXcbColormap::QXcbColormap() - : d(new QXcbColormapPrivate) -{} - -QXcbColormap::QXcbColormap(const QXcbColormap &colormap) - :d (colormap.d) -{ d->ref.ref(); } - -QXcbColormap::~QXcbColormap() -{ - if (!d->ref.deref()) { - if (!d->defaultColormap) - XFreeColormap(X11->display, d->colormap); - delete d; - } -} - -QXcbColormap::Mode QXcbColormap::mode() const -{ return d->mode; } - -int QXcbColormap::depth() const -{ return d->depth; } - -int QXcbColormap::size() const -{ - return (d->mode == Gray - ? d->r_max - : (d->mode == Indexed - ? d->r_max * d->g_max * d->b_max - : -1)); -} - -uint QXcbColormap::pixel(const QColor &color) const -{ - const QRgba64 rgba64 = color.rgba64(); - // XXX We emulate the raster engine here by getting the - // 8-bit values, but we could instead use the 16-bit - // values for slightly better color accuracy - const uint r = (rgba64.red8() * d->r_max) >> 8; - const uint g = (rgba64.green8() * d->g_max) >> 8; - const uint b = (rgba64.blue8() * d->b_max) >> 8; - if (d->mode != Direct) { - if (d->mode == Gray) - return d->pixels.at((r * 30 + g * 59 + b * 11) / 100); - return d->pixels.at(r * d->g_max * d->b_max + g * d->b_max + b); - } - return (r << d->r_shift) + (g << d->g_shift) + (b << d->b_shift); -} - -const QColor QXcbColormap::colorAt(uint pixel) const -{ - if (d->mode != Direct) { - Q_ASSERT(pixel <= (uint)d->colors.size()); - return d->colors.at(pixel); - } - - const int r = (((pixel & d->visual->red_mask) >> d->r_shift) << 8) / d->r_max; - const int g = (((pixel & d->visual->green_mask) >> d->g_shift) << 8) / d->g_max; - const int b = (((pixel & d->visual->blue_mask) >> d->b_shift) << 8) / d->b_max; - return QColor(r, g, b); -} - -const QList<QColor> QXcbColormap::colormap() const -{ return d->colors; } - -QXcbColormap &QXcbColormap::operator=(const QXcbColormap &colormap) -{ - qAtomicAssign(d, colormap.d); - return *this; -} - -QT_END_NAMESPACE diff --git a/src/plugins/platforms/xcb/nativepainting/qcolormap_x11_p.h b/src/plugins/platforms/xcb/nativepainting/qcolormap_x11_p.h deleted file mode 100644 index 9c9cce424c9..00000000000 --- a/src/plugins/platforms/xcb/nativepainting/qcolormap_x11_p.h +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (C) 2018 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 - -#pragma once - -#include <QColor> -#include <QList> - -QT_BEGIN_NAMESPACE - -class QXcbColormapPrivate; -class QXcbColormap -{ -public: - enum Mode { Direct, Indexed, Gray }; - - static void initialize(); - static void cleanup(); - - static QXcbColormap instance(int screen = -1); - - QXcbColormap(const QXcbColormap &colormap); - ~QXcbColormap(); - - QXcbColormap &operator=(const QXcbColormap &colormap); - - Mode mode() const; - - int depth() const; - int size() const; - - uint pixel(const QColor &color) const; - const QColor colorAt(uint pixel) const; - - const QList<QColor> colormap() const; - -private: - QXcbColormap(); - QXcbColormapPrivate *d; -}; - -QT_END_NAMESPACE diff --git a/src/plugins/platforms/xcb/nativepainting/qpaintengine_x11.cpp b/src/plugins/platforms/xcb/nativepainting/qpaintengine_x11.cpp deleted file mode 100644 index 2f3827025f3..00000000000 --- a/src/plugins/platforms/xcb/nativepainting/qpaintengine_x11.cpp +++ /dev/null @@ -1,2807 +0,0 @@ -// Copyright (C) 2018 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 <QtCore/qrandom.h> - -#include <private/qpixmapcache_p.h> -#include <private/qpaintengine_p.h> -#include <private/qpainterpath_p.h> -#include <private/qdrawhelper_p.h> -#include <private/qfontengineglyphcache_p.h> - -#if QT_CONFIG(fontconfig) -#include <private/qfontengine_ft_p.h> -#endif - -#include "qpaintengine_x11_p.h" -#include "qpolygonclipper_p.h" -#include "qtessellator_p.h" -#include "qpixmap_x11_p.h" -#include "qcolormap_x11_p.h" -#include "qt_x11_p.h" -#include "qxcbexport.h" -#include "qxcbnativepainting.h" - -QT_BEGIN_NAMESPACE - -using namespace Qt::StringLiterals; - -#if QT_CONFIG(xrender) - -class QXRenderTessellator : public QTessellator -{ -public: - QXRenderTessellator() : traps(0), allocated(0), size(0) {} - ~QXRenderTessellator() { free(traps); } - XTrapezoid *traps; - int allocated; - int size; - void addTrap(const Trapezoid &trap) override; - QRect tessellate(const QPointF *points, int nPoints, bool winding) { - size = 0; - setWinding(winding); - return QTessellator::tessellate(points, nPoints).toRect(); - } - void done() { - if (allocated > 64) { - free(traps); - traps = 0; - allocated = 0; - } - } -}; - -void QXRenderTessellator::addTrap(const Trapezoid &trap) -{ - if (size == allocated) { - allocated = qMax(2*allocated, 64); - traps = q_check_ptr((XTrapezoid *)realloc(traps, allocated * sizeof(XTrapezoid))); - } - traps[size].top = Q27Dot5ToXFixed(trap.top); - traps[size].bottom = Q27Dot5ToXFixed(trap.bottom); - traps[size].left.p1.x = Q27Dot5ToXFixed(trap.topLeft->x); - traps[size].left.p1.y = Q27Dot5ToXFixed(trap.topLeft->y); - traps[size].left.p2.x = Q27Dot5ToXFixed(trap.bottomLeft->x); - traps[size].left.p2.y = Q27Dot5ToXFixed(trap.bottomLeft->y); - traps[size].right.p1.x = Q27Dot5ToXFixed(trap.topRight->x); - traps[size].right.p1.y = Q27Dot5ToXFixed(trap.topRight->y); - traps[size].right.p2.x = Q27Dot5ToXFixed(trap.bottomRight->x); - traps[size].right.p2.y = Q27Dot5ToXFixed(trap.bottomRight->y); - ++size; -} - -#endif // QT_CONFIG(xrender) - -class QX11PaintEnginePrivate : public QPaintEnginePrivate -{ - Q_DECLARE_PUBLIC(QX11PaintEngine) -public: - QX11PaintEnginePrivate() - { - opacity = 1.0; - scrn = -1; - hd = 0; - picture = 0; - gc = gc_brush = 0; - dpy = 0; - xinfo = 0; - txop = QTransform::TxNone; - has_clipping = false; - render_hints = 0; - xform_scale = 1; -#if QT_CONFIG(xrender) - tessellator = 0; -#endif - } - enum GCMode { - PenGC, - BrushGC - }; - - void init(); - void fillPolygon_translated(const QPointF *points, int pointCount, GCMode gcMode, - QPaintEngine::PolygonDrawMode mode); - void fillPolygon_dev(const QPointF *points, int pointCount, GCMode gcMode, - QPaintEngine::PolygonDrawMode mode); - void fillPath(const QPainterPath &path, GCMode gcmode, bool transform); - void strokePolygon_dev(const QPointF *points, int pointCount, bool close); - void strokePolygon_translated(const QPointF *points, int pointCount, bool close); - void setupAdaptedOrigin(const QPoint &p); - void resetAdaptedOrigin(); - void decidePathFallback() { - use_path_fallback = has_alpha_brush - || has_alpha_pen - || has_custom_pen - || has_complex_xform - || (render_hints & QPainter::Antialiasing); - } - void decideCoordAdjust() { - adjust_coords = false; - } - void clipPolygon_dev(const QPolygonF &poly, QPolygonF *clipped_poly); - void systemStateChanged() override; - inline bool isCosmeticPen() const { - return cpen.isCosmetic(); - } - - Display *dpy; - int scrn; - int pdev_depth; - unsigned long hd; - QPixmap brush_pm; -#if QT_CONFIG(xrender) - unsigned long picture; - unsigned long current_brush; - QPixmap bitmap_texture; - int composition_mode; -#else - unsigned long picture; -#endif - GC gc; - GC gc_brush; - - QPen cpen; - QBrush cbrush; - QRegion crgn; - QTransform matrix; - qreal opacity; - - uint has_complex_xform : 1; - uint has_scaling_xform : 1; - uint has_non_scaling_xform : 1; - uint has_custom_pen : 1; - uint use_path_fallback : 1; - uint adjust_coords : 1; - uint has_clipping : 1; - uint adapted_brush_origin : 1; - uint adapted_pen_origin : 1; - uint has_pen : 1; - uint has_brush : 1; - uint has_texture : 1; - uint has_alpha_texture : 1; - uint has_pattern : 1; - uint has_alpha_pen : 1; - uint has_alpha_brush : 1; - uint use_sysclip : 1; - uint render_hints; - - const QXcbX11Info *xinfo; - QPointF bg_origin; - QTransform::TransformationType txop; - qreal xform_scale; - - struct qt_float_point - { - qreal x, y; - }; - QPolygonClipper<qt_float_point, qt_float_point, float> polygonClipper; - - int xlibMaxLinePoints; -#if QT_CONFIG(xrender) - QXRenderTessellator *tessellator; -#endif -}; - -#if QT_CONFIG(xrender) -class QXRenderGlyphCache : public QFontEngineGlyphCache -{ -public: - QXRenderGlyphCache(QXcbX11Info x, QFontEngine::GlyphFormat format, const QTransform &matrix); - ~QXRenderGlyphCache(); - - bool addGlyphs(const QTextItemInt &ti, - const QVarLengthArray<glyph_t> &glyphs, - const QVarLengthArray<QFixedPoint> &positions); - bool draw(Drawable src, Drawable dst, const QTransform &matrix, const QTextItemInt &ti); - - inline GlyphSet glyphSet(); - inline int glyphBufferSize(const QFontEngineFT::Glyph &glyph) const; - - inline QImage::Format imageFormat() const; - inline const XRenderPictFormat *renderPictFormat() const; - - static inline QFontEngine::GlyphFormat glyphFormatForDepth(QFontEngine *fontEngine, int depth); - static inline Glyph glyphId(glyph_t glyph, QFixed subPixelPosition); - - static inline bool isValidCoordinate(const QFixedPoint &fp); - -private: - QXcbX11Info xinfo; - GlyphSet gset; - QSet<Glyph> cachedGlyphs; -}; -#endif // QT_CONFIG(xrender) - -extern QPixmap qt_pixmapForBrush(int brushStyle, bool invert); //in qbrush.cpp -extern QPixmap qt_toX11Pixmap(const QPixmap &pixmap); - -extern "C" { -Q_XCB_EXPORT Drawable qt_x11Handle(const QPaintDevice *pd) -{ - if (!pd) - return 0; - -// if (pd->devType() == QInternal::Widget) -// return static_cast<const QWidget *>(pd)->handle(); - - if (pd->devType() == QInternal::Pixmap) - return qt_x11PixmapHandle(*static_cast<const QPixmap *>(pd)); - - return 0; -} -} - -static const QXcbX11Info *qt_x11Info(const QPaintDevice *pd) -{ - if (!pd) - return 0; - -// if (pd->devType() == QInternal::Widget) -// return &static_cast<const QWidget *>(pd)->x11Info(); - - if (pd->devType() == QInternal::Pixmap) - return &qt_x11Info(*static_cast<const QPixmap *>(pd)); - - return 0; -} - -// use the same rounding as in qrasterizer.cpp (6 bit fixed point) -static const qreal aliasedCoordinateDelta = 0.5 - 0.015625; - -#undef X11 // defined in qt_x11_p.h -extern "C" { -/*! - Returns the X11 specific pen GC for the painter \a p. Note that - QPainter::begin() must be called before this function returns a - valid GC. -*/ -GC Q_XCB_EXPORT qt_x11_get_pen_gc(QPainter *p) -{ - if (p && p->paintEngine() - && p->paintEngine()->isActive() - && p->paintEngine()->type() == QPaintEngine::X11) { - return static_cast<QX11PaintEngine *>(p->paintEngine())->d_func()->gc; - } - return 0; -} - -/*! - Returns the X11 specific brush GC for the painter \a p. Note that - QPainter::begin() must be called before this function returns a - valid GC. -*/ -GC Q_XCB_EXPORT qt_x11_get_brush_gc(QPainter *p) -{ - if (p && p->paintEngine() - && p->paintEngine()->isActive() - && p->paintEngine()->type() == QPaintEngine::X11) { - return static_cast<QX11PaintEngine *>(p->paintEngine())->d_func()->gc_brush; - } - return 0; -} -} -#define X11 qt_x11Data - -// internal helper. Converts an integer value to an unique string token -template <typename T> - struct HexString -{ - inline HexString(const T t) - : val(t) - {} - - inline void write(QChar *&dest) const - { - const ushort hexChars[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; - const char *c = reinterpret_cast<const char *>(&val); - for (uint i = 0; i < sizeof(T); ++i) { - *dest++ = hexChars[*c & 0xf]; - *dest++ = hexChars[(*c & 0xf0) >> 4]; - ++c; - } - } - const T val; -}; - -// specialization to enable fast concatenating of our string tokens to a string -template <typename T> - struct QConcatenable<HexString<T> > -{ - typedef HexString<T> type; - enum { ExactSize = true }; - static int size(const HexString<T> &) { return sizeof(T) * 2; } - static inline void appendTo(const HexString<T> &str, QChar *&out) { str.write(out); } - typedef QString ConvertTo; -}; - -#if QT_CONFIG(xrender) -static const int compositionModeToRenderOp[QPainter::CompositionMode_Xor + 1] = { - PictOpOver, //CompositionMode_SourceOver, - PictOpOverReverse, //CompositionMode_DestinationOver, - PictOpClear, //CompositionMode_Clear, - PictOpSrc, //CompositionMode_Source, - PictOpDst, //CompositionMode_Destination, - PictOpIn, //CompositionMode_SourceIn, - PictOpInReverse, //CompositionMode_DestinationIn, - PictOpOut, //CompositionMode_SourceOut, - PictOpOutReverse, //CompositionMode_DestinationOut, - PictOpAtop, //CompositionMode_SourceAtop, - PictOpAtopReverse, //CompositionMode_DestinationAtop, - PictOpXor //CompositionMode_Xor -}; - -static inline int qpainterOpToXrender(QPainter::CompositionMode mode) -{ - Q_ASSERT(mode <= QPainter::CompositionMode_Xor); - return compositionModeToRenderOp[mode]; -} - -static inline bool complexPictOp(int op) -{ - return op != PictOpOver && op != PictOpSrc; -} -#endif - -static inline void x11SetClipRegion(Display *dpy, GC gc, GC gc2, -#if QT_CONFIG(xrender) - Picture picture, -#else - Qt::HANDLE picture, -#endif - const QRegion &r) -{ -// int num; -// XRectangle *rects = (XRectangle *)qt_getClipRects(r, num); - QList<XRectangle> rects = qt_region_to_xrectangles(r); - int num = rects.size(); - - if (gc) - XSetClipRectangles( dpy, gc, 0, 0, rects.data(), num, Unsorted ); - if (gc2) - XSetClipRectangles( dpy, gc2, 0, 0, rects.data(), num, Unsorted ); - -#if QT_CONFIG(xrender) - if (picture) - XRenderSetPictureClipRectangles(dpy, picture, 0, 0, rects.data(), num); -#else - Q_UNUSED(picture); -#endif // QT_CONFIG(xrender) -} - - -static inline void x11ClearClipRegion(Display *dpy, GC gc, GC gc2, -#if QT_CONFIG(xrender) - Picture picture -#else - Qt::HANDLE picture -#endif - ) -{ - if (gc) - XSetClipMask(dpy, gc, XNone); - if (gc2) - XSetClipMask(dpy, gc2, XNone); - -#if QT_CONFIG(xrender) - if (picture) { - XRenderPictureAttributes attrs; - attrs.clip_mask = XNone; - XRenderChangePicture (dpy, picture, CPClipMask, &attrs); - } -#else - Q_UNUSED(picture); -#endif // QT_CONFIG(xrender) -} - - -#define DITHER_SIZE 16 -static const uchar base_dither_matrix[DITHER_SIZE][DITHER_SIZE] = { - { 0,192, 48,240, 12,204, 60,252, 3,195, 51,243, 15,207, 63,255 }, - { 128, 64,176,112,140, 76,188,124,131, 67,179,115,143, 79,191,127 }, - { 32,224, 16,208, 44,236, 28,220, 35,227, 19,211, 47,239, 31,223 }, - { 160, 96,144, 80,172,108,156, 92,163, 99,147, 83,175,111,159, 95 }, - { 8,200, 56,248, 4,196, 52,244, 11,203, 59,251, 7,199, 55,247 }, - { 136, 72,184,120,132, 68,180,116,139, 75,187,123,135, 71,183,119 }, - { 40,232, 24,216, 36,228, 20,212, 43,235, 27,219, 39,231, 23,215 }, - { 168,104,152, 88,164,100,148, 84,171,107,155, 91,167,103,151, 87 }, - { 2,194, 50,242, 14,206, 62,254, 1,193, 49,241, 13,205, 61,253 }, - { 130, 66,178,114,142, 78,190,126,129, 65,177,113,141, 77,189,125 }, - { 34,226, 18,210, 46,238, 30,222, 33,225, 17,209, 45,237, 29,221 }, - { 162, 98,146, 82,174,110,158, 94,161, 97,145, 81,173,109,157, 93 }, - { 10,202, 58,250, 6,198, 54,246, 9,201, 57,249, 5,197, 53,245 }, - { 138, 74,186,122,134, 70,182,118,137, 73,185,121,133, 69,181,117 }, - { 42,234, 26,218, 38,230, 22,214, 41,233, 25,217, 37,229, 21,213 }, - { 170,106,154, 90,166,102,150, 86,169,105,153, 89,165,101,149, 85 } -}; - -static QPixmap qt_patternForAlpha(uchar alpha, int screen) -{ - QPixmap pm; - QString key = "$qt-alpha-brush$"_L1 - % HexString<uchar>(alpha) - % HexString<int>(screen); - - if (!QPixmapCache::find(key, &pm)) { - // #### why not use a mono image here???? - QImage pattern(DITHER_SIZE, DITHER_SIZE, QImage::Format_ARGB32); - pattern.fill(0xffffffff); - for (int y = 0; y < DITHER_SIZE; ++y) { - for (int x = 0; x < DITHER_SIZE; ++x) { - if (base_dither_matrix[x][y] <= alpha) - pattern.setPixel(x, y, 0x00000000); - } - } - pm = QBitmap::fromImage(pattern); - qt_x11SetScreen(pm, screen); - //pm.x11SetScreen(screen); - QPixmapCache::insert(key, pm); - } - return pm; -} - - -#if QT_CONFIG(xrender) -static Picture getPatternFill(int screen, const QBrush &b) -{ - if (!X11->use_xrender) - return XNone; - - XRenderColor color = X11->preMultiply(b.color()); - XRenderColor bg_color; - - bg_color = X11->preMultiply(QColor(0, 0, 0, 0)); - - for (int i = 0; i < X11->pattern_fill_count; ++i) { - if (X11->pattern_fills[i].screen == screen - && X11->pattern_fills[i].opaque == false - && X11->pattern_fills[i].style == b.style() - && X11->pattern_fills[i].color.alpha == color.alpha - && X11->pattern_fills[i].color.red == color.red - && X11->pattern_fills[i].color.green == color.green - && X11->pattern_fills[i].color.blue == color.blue - && X11->pattern_fills[i].bg_color.alpha == bg_color.alpha - && X11->pattern_fills[i].bg_color.red == bg_color.red - && X11->pattern_fills[i].bg_color.green == bg_color.green - && X11->pattern_fills[i].bg_color.blue == bg_color.blue) - return X11->pattern_fills[i].picture; - } - // none found, replace one - int i = QRandomGenerator::global()->generate() % 16; - - if (X11->pattern_fills[i].screen != screen && X11->pattern_fills[i].picture) { - XRenderFreePicture (QXcbX11Info::display(), X11->pattern_fills[i].picture); - X11->pattern_fills[i].picture = 0; - } - - if (!X11->pattern_fills[i].picture) { - Pixmap pixmap = XCreatePixmap (QXcbX11Info::display(), RootWindow (QXcbX11Info::display(), screen), 8, 8, 32); - XRenderPictureAttributes attrs; - attrs.repeat = True; - X11->pattern_fills[i].picture = XRenderCreatePicture (QXcbX11Info::display(), pixmap, - XRenderFindStandardFormat(QXcbX11Info::display(), PictStandardARGB32), - CPRepeat, &attrs); - XFreePixmap (QXcbX11Info::display(), pixmap); - } - - X11->pattern_fills[i].screen = screen; - X11->pattern_fills[i].color = color; - X11->pattern_fills[i].bg_color = bg_color; - X11->pattern_fills[i].opaque = false; - X11->pattern_fills[i].style = b.style(); - - XRenderFillRectangle(QXcbX11Info::display(), PictOpSrc, X11->pattern_fills[i].picture, &bg_color, 0, 0, 8, 8); - - QPixmap pattern(qt_pixmapForBrush(b.style(), true)); - XRenderPictureAttributes attrs; - attrs.repeat = true; - XRenderChangePicture(QXcbX11Info::display(), qt_x11PictureHandle(pattern), CPRepeat, &attrs); - - Picture fill_fg = X11->getSolidFill(screen, b.color()); - XRenderComposite(QXcbX11Info::display(), PictOpOver, fill_fg, qt_x11PictureHandle(pattern), - X11->pattern_fills[i].picture, - 0, 0, 0, 0, 0, 0, 8, 8); - - return X11->pattern_fills[i].picture; -} - -static void qt_render_bitmap(Display *dpy, int scrn, Picture src, Picture dst, - int sx, int sy, int x, int y, int sw, int sh, - const QPen &pen) -{ - Picture fill_fg = X11->getSolidFill(scrn, pen.color()); - XRenderComposite(dpy, PictOpOver, - fill_fg, src, dst, sx, sy, sx, sy, x, y, sw, sh); -} -#endif - -void QX11PaintEnginePrivate::init() -{ - dpy = 0; - scrn = 0; - hd = 0; - picture = 0; - xinfo = 0; -#if QT_CONFIG(xrender) - current_brush = 0; - composition_mode = PictOpOver; - tessellator = new QXRenderTessellator; -#endif -} - -void QX11PaintEnginePrivate::setupAdaptedOrigin(const QPoint &p) -{ - if (adapted_pen_origin) - XSetTSOrigin(dpy, gc, p.x(), p.y()); - if (adapted_brush_origin) - XSetTSOrigin(dpy, gc_brush, p.x(), p.y()); -} - -void QX11PaintEnginePrivate::resetAdaptedOrigin() -{ - if (adapted_pen_origin) - XSetTSOrigin(dpy, gc, 0, 0); - if (adapted_brush_origin) - XSetTSOrigin(dpy, gc_brush, 0, 0); -} - -void QX11PaintEnginePrivate::clipPolygon_dev(const QPolygonF &poly, QPolygonF *clipped_poly) -{ - int clipped_count = 0; - qt_float_point *clipped_points = 0; - polygonClipper.clipPolygon((qt_float_point *) poly.data(), poly.size(), - &clipped_points, &clipped_count); - clipped_poly->resize(clipped_count); - for (int i=0; i<clipped_count; ++i) - (*clipped_poly)[i] = *((QPointF *)(&clipped_points[i])); -} - -void QX11PaintEnginePrivate::systemStateChanged() -{ - Q_Q(QX11PaintEngine); - QPainter *painter = q->state ? q->state->painter() : nullptr; - if (painter && painter->hasClipping()) { - if (q->testDirty(QPaintEngine::DirtyTransform)) - q->updateMatrix(q->state->transform()); - QPolygonF clip_poly_dev(matrix.map(painter->clipPath().toFillPolygon())); - QPolygonF clipped_poly_dev; - clipPolygon_dev(clip_poly_dev, &clipped_poly_dev); - q->updateClipRegion_dev(QRegion(clipped_poly_dev.toPolygon()), Qt::ReplaceClip); - } else { - q->updateClipRegion_dev(QRegion(), Qt::NoClip); - } -} - -static QPaintEngine::PaintEngineFeatures qt_decide_features() -{ - QPaintEngine::PaintEngineFeatures features = - QPaintEngine::PrimitiveTransform - | QPaintEngine::PatternBrush - | QPaintEngine::AlphaBlend - | QPaintEngine::PainterPaths - | QPaintEngine::RasterOpModes; - - if (X11->use_xrender) { - features |= QPaintEngine::Antialiasing; - features |= QPaintEngine::PorterDuff; - features |= QPaintEngine::MaskedBrush; -#if 0 - if (X11->xrender_version > 10) { - features |= QPaintEngine::LinearGradientFill; - // ### - } -#endif - } - - return features; -} - -/* - * QX11PaintEngine members - */ - -QX11PaintEngine::QX11PaintEngine() - : QPaintEngine(*(new QX11PaintEnginePrivate), qt_decide_features()) -{ - Q_D(QX11PaintEngine); - d->init(); -} - -QX11PaintEngine::QX11PaintEngine(QX11PaintEnginePrivate &dptr) - : QPaintEngine(dptr, qt_decide_features()) -{ - Q_D(QX11PaintEngine); - d->init(); -} - -QX11PaintEngine::~QX11PaintEngine() -{ -#if QT_CONFIG(xrender) - Q_D(QX11PaintEngine); - delete d->tessellator; -#endif -} - -bool QX11PaintEngine::begin(QPaintDevice *pdev) -{ - Q_D(QX11PaintEngine); - d->xinfo = qt_x11Info(pdev); -#if QT_CONFIG(xrender) - if (pdev->devType() == QInternal::Pixmap) { - const QPixmap *pm = static_cast<const QPixmap *>(pdev); - QX11PlatformPixmap *data = static_cast<QX11PlatformPixmap*>(pm->handle()); - if (X11->use_xrender && data->depth() != 32 && data->x11_mask) - data->convertToARGB32(); - d->picture = qt_x11PictureHandle(*static_cast<const QPixmap *>(pdev)); - } -#else - d->picture = 0; -#endif - d->hd = qt_x11Handle(pdev); - - Q_ASSERT(d->xinfo != 0); - d->dpy = d->xinfo->display(); // get display variable - d->scrn = d->xinfo->screen(); // get screen variable - - d->crgn = QRegion(); - d->gc = XCreateGC(d->dpy, d->hd, 0, 0); - d->gc_brush = XCreateGC(d->dpy, d->hd, 0, 0); - d->has_alpha_brush = false; - d->has_alpha_pen = false; - d->has_clipping = false; - d->has_complex_xform = false; - d->has_scaling_xform = false; - d->has_non_scaling_xform = true; - d->xform_scale = 1; - d->has_custom_pen = false; - d->matrix = QTransform(); - d->pdev_depth = d->pdev->depth(); - d->render_hints = 0; - d->txop = QTransform::TxNone; - d->use_path_fallback = false; -#if QT_CONFIG(xrender) - d->composition_mode = PictOpOver; -#endif - d->xlibMaxLinePoints = 32762; // a safe number used to avoid, call to XMaxRequestSize(d->dpy) - 3; - d->opacity = 1; - - QX11PlatformPixmap *x11pm = paintDevice()->devType() == QInternal::Pixmap ? qt_x11Pixmap(*static_cast<QPixmap *>(paintDevice())) : nullptr; - d->use_sysclip = paintDevice()->devType() == QInternal::Widget || (x11pm ? x11pm->isBackingStore() : false); - - // Set up the polygon clipper. Note: This will only work in - // polyline mode as long as we have a buffer zone, since a - // polyline may be clipped into several non-connected polylines. - const int BUFFERZONE = 1000; - QRect devClipRect(-BUFFERZONE, -BUFFERZONE, - pdev->width() + 2*BUFFERZONE, pdev->height() + 2*BUFFERZONE); - d->polygonClipper.setBoundingRect(devClipRect); - - setSystemClip(systemClip()); - d->systemStateChanged(); - - qt_x11SetDefaultScreen(d->xinfo->screen()); - - updatePen(QPen(Qt::black)); - updateBrush(QBrush(Qt::white), QPoint()); - - setDirty(QPaintEngine::DirtyClipRegion); - setDirty(QPaintEngine::DirtyPen); - setDirty(QPaintEngine::DirtyBrush); - setDirty(QPaintEngine::DirtyBackground); - - setActive(true); - return true; -} - -bool QX11PaintEngine::end() -{ - Q_D(QX11PaintEngine); - -#if QT_CONFIG(xrender) - if (d->picture) { - // reset clipping/subwindow mode on our render picture - XRenderPictureAttributes attrs; - attrs.subwindow_mode = ClipByChildren; - attrs.clip_mask = XNone; - XRenderChangePicture(d->dpy, d->picture, CPClipMask|CPSubwindowMode, &attrs); - } -#endif - - if (d->gc_brush && d->pdev->painters < 2) { - XFreeGC(d->dpy, d->gc_brush); - d->gc_brush = 0; - } - - if (d->gc && d->pdev->painters < 2) { - XFreeGC(d->dpy, d->gc); - d->gc = 0; - } - - // Restore system clip for alien widgets painting outside the paint event. -// if (d->pdev->devType() == QInternal::Widget && !static_cast<QWidget *>(d->pdev)->internalWinId()) - d->currentClipDevice = nullptr; - setSystemClip(QRegion()); - - setActive(false); - return true; -} - -static bool clipLine(QLineF *line, const QRect &rect) -{ - qreal x1 = line->x1(); - qreal x2 = line->x2(); - qreal y1 = line->y1(); - qreal y2 = line->y2(); - - qreal left = rect.x(); - qreal right = rect.x() + rect.width() - 1; - qreal top = rect.y(); - qreal bottom = rect.y() + rect.height() - 1; - - enum { Left, Right, Top, Bottom }; - // clip the lines, after cohen-sutherland, see e.g. http://www.nondot.org/~sabre/graphpro/line6.html - int p1 = ((x1 < left) << Left) - | ((x1 > right) << Right) - | ((y1 < top) << Top) - | ((y1 > bottom) << Bottom); - int p2 = ((x2 < left) << Left) - | ((x2 > right) << Right) - | ((y2 < top) << Top) - | ((y2 > bottom) << Bottom); - - if (p1 & p2) - // completely outside - return false; - - if (p1 | p2) { - qreal dx = x2 - x1; - qreal dy = y2 - y1; - - // clip x coordinates - if (x1 < left) { - y1 += dy/dx * (left - x1); - x1 = left; - } else if (x1 > right) { - y1 -= dy/dx * (x1 - right); - x1 = right; - } - if (x2 < left) { - y2 += dy/dx * (left - x2); - x2 = left; - } else if (x2 > right) { - y2 -= dy/dx * (x2 - right); - x2 = right; - } - p1 = ((y1 < top) << Top) - | ((y1 > bottom) << Bottom); - p2 = ((y2 < top) << Top) - | ((y2 > bottom) << Bottom); - if (p1 & p2) - return false; - // clip y coordinates - if (y1 < top) { - x1 += dx/dy * (top - y1); - y1 = top; - } else if (y1 > bottom) { - x1 -= dx/dy * (y1 - bottom); - y1 = bottom; - } - if (y2 < top) { - x2 += dx/dy * (top - y2); - y2 = top; - } else if (y2 > bottom) { - x2 -= dx/dy * (y2 - bottom); - y2 = bottom; - } - *line = QLineF(QPointF(x1, y1), QPointF(x2, y2)); - } - return true; -} - -void QX11PaintEngine::drawLines(const QLine *lines, int lineCount) -{ - Q_ASSERT(lines); - Q_ASSERT(lineCount); - Q_D(QX11PaintEngine); - - if (d->has_alpha_brush - || d->has_alpha_pen - || d->has_custom_pen - || (d->cpen.widthF() > 0 && d->has_complex_xform - && !d->has_non_scaling_xform) - || (d->render_hints & QPainter::Antialiasing)) { - for (int i = 0; i < lineCount; ++i) { - QPainterPath path(lines[i].p1()); - path.lineTo(lines[i].p2()); - drawPath(path); - } - return; - } - - if (d->has_pen) { - for (int i = 0; i < lineCount; ++i) { - QLineF linef; - if (d->txop == QTransform::TxNone) { - linef = lines[i]; - } else { - linef = d->matrix.map(QLineF(lines[i])); - } - if (clipLine(&linef, d->polygonClipper.boundingRect())) { - int x1 = qRound(linef.x1() + aliasedCoordinateDelta); - int y1 = qRound(linef.y1() + aliasedCoordinateDelta); - int x2 = qRound(linef.x2() + aliasedCoordinateDelta); - int y2 = qRound(linef.y2() + aliasedCoordinateDelta); - - XDrawLine(d->dpy, d->hd, d->gc, x1, y1, x2, y2); - } - } - } -} - -void QX11PaintEngine::drawLines(const QLineF *lines, int lineCount) -{ - Q_ASSERT(lines); - Q_ASSERT(lineCount); - Q_D(QX11PaintEngine); - - if (d->has_alpha_brush - || d->has_alpha_pen - || d->has_custom_pen - || (d->cpen.widthF() > 0 && d->has_complex_xform - && !d->has_non_scaling_xform) - || (d->render_hints & QPainter::Antialiasing)) { - for (int i = 0; i < lineCount; ++i) { - QPainterPath path(lines[i].p1()); - path.lineTo(lines[i].p2()); - drawPath(path); - } - return; - } - - if (d->has_pen) { - for (int i = 0; i < lineCount; ++i) { - QLineF linef = d->matrix.map(lines[i]); - if (clipLine(&linef, d->polygonClipper.boundingRect())) { - int x1 = qRound(linef.x1() + aliasedCoordinateDelta); - int y1 = qRound(linef.y1() + aliasedCoordinateDelta); - int x2 = qRound(linef.x2() + aliasedCoordinateDelta); - int y2 = qRound(linef.y2() + aliasedCoordinateDelta); - - XDrawLine(d->dpy, d->hd, d->gc, x1, y1, x2, y2); - } - } - } -} - -static inline QLine clipStraightLine(const QRect &clip, const QLine &l) -{ - if (l.p1().x() == l.p2().x()) { - int x = qBound(clip.left(), l.p1().x(), clip.right()); - int y1 = qBound(clip.top(), l.p1().y(), clip.bottom()); - int y2 = qBound(clip.top(), l.p2().y(), clip.bottom()); - - return QLine(x, y1, x, y2); - } else { - Q_ASSERT(l.p1().y() == l.p2().y()); - - int x1 = qBound(clip.left(), l.p1().x(), clip.right()); - int x2 = qBound(clip.left(), l.p2().x(), clip.right()); - int y = qBound(clip.top(), l.p1().y(), clip.bottom()); - - return QLine(x1, y, x2, y); - } -} - -void QX11PaintEngine::drawRects(const QRectF *rects, int rectCount) -{ - Q_D(QX11PaintEngine); - Q_ASSERT(rects); - Q_ASSERT(rectCount); - - if (rectCount != 1 - || d->has_pen - || d->has_alpha_brush - || d->has_complex_xform - || d->has_custom_pen - || d->cbrush.style() != Qt::SolidPattern -#if QT_CONFIG(xrender) - || complexPictOp(d->composition_mode) -#endif - ) - { - QPaintEngine::drawRects(rects, rectCount); - return; - } - - QPoint alignedOffset; - if (d->txop == QTransform::TxTranslate) { - QPointF offset(d->matrix.dx(), d->matrix.dy()); - alignedOffset = offset.toPoint(); - if (offset != QPointF(alignedOffset)) { - QPaintEngine::drawRects(rects, rectCount); - return; - } - } - - const QRectF& r = rects[0]; - QRect alignedRect = r.toAlignedRect(); - if (r != QRectF(alignedRect)) { - QPaintEngine::drawRects(rects, rectCount); - return; - } - alignedRect.translate(alignedOffset); - - QRect clip(d->polygonClipper.boundingRect()); - alignedRect = alignedRect.intersected(clip); - if (alignedRect.isEmpty()) - return; - - // simple-case: - // the rectangle is pixel-aligned - // the fill brush is just a solid non-alpha color - // the painter transform is only integer translation - // ignore: antialiasing and just XFillRectangles directly - XRectangle xrect; - xrect.x = short(alignedRect.x()); - xrect.y = short(alignedRect.y()); - xrect.width = ushort(alignedRect.width()); - xrect.height = ushort(alignedRect.height()); - XFillRectangles(d->dpy, d->hd, d->gc_brush, &xrect, 1); -} - -void QX11PaintEngine::drawRects(const QRect *rects, int rectCount) -{ - Q_D(QX11PaintEngine); - Q_ASSERT(rects); - Q_ASSERT(rectCount); - - if (d->has_alpha_pen - || d->has_complex_xform - || d->has_custom_pen - || (d->render_hints & QPainter::Antialiasing)) - { - for (int i = 0; i < rectCount; ++i) { - QPainterPath path; - path.addRect(rects[i]); - drawPath(path); - } - return; - } - - QRect clip(d->polygonClipper.boundingRect()); - QPoint offset(qRound(d->matrix.dx()), qRound(d->matrix.dy())); -#if QT_CONFIG(xrender) - ::Picture pict = d->picture; - - if (X11->use_xrender && pict && d->has_brush && d->pdev_depth != 1 - && (d->has_texture || d->has_alpha_brush || complexPictOp(d->composition_mode))) - { - XRenderColor xc; - if (!d->has_texture && !d->has_pattern) - xc = X11->preMultiply(d->cbrush.color()); - - for (int i = 0; i < rectCount; ++i) { - QRect r(rects[i]); - if (d->txop == QTransform::TxTranslate) - r.translate(offset); - - if (r.width() == 0 || r.height() == 0) { - if (d->has_pen) { - const QLine l = clipStraightLine(clip, QLine(r.left(), r.top(), r.left() + r.width(), r.top() + r.height())); - XDrawLine(d->dpy, d->hd, d->gc, l.p1().x(), l.p1().y(), l.p2().x(), l.p2().y()); - } - continue; - } - - r = r.intersected(clip); - if (r.isEmpty()) - continue; - if (d->has_texture || d->has_pattern) { - XRenderComposite(d->dpy, d->composition_mode, d->current_brush, 0, pict, - qRound(r.x() - d->bg_origin.x()), qRound(r.y() - d->bg_origin.y()), - 0, 0, r.x(), r.y(), r.width(), r.height()); - } else { - XRenderFillRectangle(d->dpy, d->composition_mode, pict, &xc, r.x(), r.y(), r.width(), r.height()); - } - if (d->has_pen) - XDrawRectangle(d->dpy, d->hd, d->gc, r.x(), r.y(), r.width(), r.height()); - } - } else -#endif // QT_CONFIG(xrender) - { - if (d->has_brush && d->has_pen) { - for (int i = 0; i < rectCount; ++i) { - QRect r(rects[i]); - if (d->txop == QTransform::TxTranslate) - r.translate(offset); - - if (r.width() == 0 || r.height() == 0) { - const QLine l = clipStraightLine(clip, QLine(r.left(), r.top(), r.left() + r.width(), r.top() + r.height())); - XDrawLine(d->dpy, d->hd, d->gc, l.p1().x(), l.p1().y(), l.p2().x(), l.p2().y()); - continue; - } - - r = r.intersected(clip); - if (r.isEmpty()) - continue; - d->setupAdaptedOrigin(r.topLeft()); - XFillRectangle(d->dpy, d->hd, d->gc_brush, r.x(), r.y(), r.width(), r.height()); - XDrawRectangle(d->dpy, d->hd, d->gc, r.x(), r.y(), r.width(), r.height()); - } - d->resetAdaptedOrigin(); - } else { - QVarLengthArray<XRectangle> xrects(rectCount); - int numClipped = rectCount; - for (int i = 0; i < rectCount; ++i) { - QRect r(rects[i]); - if (d->txop == QTransform::TxTranslate) - r.translate(offset); - - if (r.width() == 0 || r.height() == 0) { - --numClipped; - if (d->has_pen) { - const QLine l = clipStraightLine(clip, QLine(r.left(), r.top(), r.left() + r.width(), r.top() + r.height())); - XDrawLine(d->dpy, d->hd, d->gc, l.p1().x(), l.p1().y(), l.p2().x(), l.p2().y()); - } - continue; - } - - r = r.intersected(clip); - if (r.isEmpty()) { - --numClipped; - continue; - } - xrects[i].x = short(r.x()); - xrects[i].y = short(r.y()); - xrects[i].width = ushort(r.width()); - xrects[i].height = ushort(r.height()); - } - if (numClipped) { - d->setupAdaptedOrigin(rects[0].topLeft()); - if (d->has_brush) - XFillRectangles(d->dpy, d->hd, d->gc_brush, xrects.data(), numClipped); - else if (d->has_pen) - XDrawRectangles(d->dpy, d->hd, d->gc, xrects.data(), numClipped); - d->resetAdaptedOrigin(); - } - } - } -} - -static inline void setCapStyle(int cap_style, GC gc) -{ - ulong mask = GCCapStyle; - XGCValues vals; - vals.cap_style = cap_style; - XChangeGC(QXcbX11Info::display(), gc, mask, &vals); -} - -void QX11PaintEngine::drawPoints(const QPoint *points, int pointCount) -{ - Q_ASSERT(points); - Q_ASSERT(pointCount); - Q_D(QX11PaintEngine); - - if (!d->has_pen) - return; - - // use the same test here as in drawPath to ensure that we don't use the path fallback - // and end up in XDrawLines for pens with width <= 1 - if (d->cpen.widthF() > 1.0f - || (X11->use_xrender && (d->has_alpha_pen || (d->render_hints & QPainter::Antialiasing))) - || (!d->isCosmeticPen() && d->txop > QTransform::TxTranslate)) - { - Qt::PenCapStyle capStyle = d->cpen.capStyle(); - if (capStyle == Qt::FlatCap) { - setCapStyle(CapProjecting, d->gc); - d->cpen.setCapStyle(Qt::SquareCap); - } - const QPoint *end = points + pointCount; - while (points < end) { - QPainterPath path; - path.moveTo(*points); - path.lineTo(points->x()+.005, points->y()); - drawPath(path); - ++points; - } - - if (capStyle == Qt::FlatCap) { - setCapStyle(CapButt, d->gc); - d->cpen.setCapStyle(capStyle); - } - return; - } - - static const int BUF_SIZE = 1024; - XPoint xPoints[BUF_SIZE]; - int i = 0, j = 0; - while (i < pointCount) { - while (i < pointCount && j < BUF_SIZE) { - const QPoint &xformed = d->matrix.map(points[i]); - int x = xformed.x(); - int y = xformed.y(); - if (x >= SHRT_MIN && y >= SHRT_MIN && x < SHRT_MAX && y < SHRT_MAX) { - xPoints[j].x = x; - xPoints[j].y = y; - ++j; - } - ++i; - } - if (j) - XDrawPoints(d->dpy, d->hd, d->gc, xPoints, j, CoordModeOrigin); - - j = 0; - } -} - -void QX11PaintEngine::drawPoints(const QPointF *points, int pointCount) -{ - Q_ASSERT(points); - Q_ASSERT(pointCount); - Q_D(QX11PaintEngine); - - if (!d->has_pen) - return; - - // use the same test here as in drawPath to ensure that we don't use the path fallback - // and end up in XDrawLines for pens with width <= 1 - if (d->cpen.widthF() > 1.0f - || (X11->use_xrender && (d->has_alpha_pen || (d->render_hints & QPainter::Antialiasing))) - || (!d->isCosmeticPen() && d->txop > QTransform::TxTranslate)) - { - Qt::PenCapStyle capStyle = d->cpen.capStyle(); - if (capStyle == Qt::FlatCap) { - setCapStyle(CapProjecting, d->gc); - d->cpen.setCapStyle(Qt::SquareCap); - } - - const QPointF *end = points + pointCount; - while (points < end) { - QPainterPath path; - path.moveTo(*points); - path.lineTo(points->x() + 0.005, points->y()); - drawPath(path); - ++points; - } - if (capStyle == Qt::FlatCap) { - setCapStyle(CapButt, d->gc); - d->cpen.setCapStyle(capStyle); - } - return; - } - - static const int BUF_SIZE = 1024; - XPoint xPoints[BUF_SIZE]; - int i = 0, j = 0; - while (i < pointCount) { - while (i < pointCount && j < BUF_SIZE) { - const QPointF &xformed = d->matrix.map(points[i]); - int x = qFloor(xformed.x()); - int y = qFloor(xformed.y()); - - if (x >= SHRT_MIN && y >= SHRT_MIN && x < SHRT_MAX && y < SHRT_MAX) { - xPoints[j].x = x; - xPoints[j].y = y; - ++j; - } - ++i; - } - if (j) - XDrawPoints(d->dpy, d->hd, d->gc, xPoints, j, CoordModeOrigin); - - j = 0; - } -} - -QPainter::RenderHints QX11PaintEngine::supportedRenderHints() const -{ -#if QT_CONFIG(xrender) - if (X11->use_xrender) - return QPainter::Antialiasing; -#endif - return QFlag(0); -} - -void QX11PaintEngine::updateState(const QPaintEngineState &state) -{ - Q_D(QX11PaintEngine); - QPaintEngine::DirtyFlags flags = state.state(); - - - if (flags & DirtyOpacity) { - d->opacity = state.opacity(); - // Force update pen/brush as to get proper alpha colors propagated - flags |= DirtyPen; - flags |= DirtyBrush; - } - - if (flags & DirtyTransform) updateMatrix(state.transform()); - if (flags & DirtyPen) updatePen(state.pen()); - if (flags & (DirtyBrush | DirtyBrushOrigin)) updateBrush(state.brush(), state.brushOrigin()); - if (flags & DirtyFont) updateFont(state.font()); - - if (state.state() & DirtyClipEnabled) { - if (state.isClipEnabled()) { - QPolygonF clip_poly_dev(d->matrix.map(painter()->clipPath().toFillPolygon())); - QPolygonF clipped_poly_dev; - d->clipPolygon_dev(clip_poly_dev, &clipped_poly_dev); - updateClipRegion_dev(QRegion(clipped_poly_dev.toPolygon()), Qt::ReplaceClip); - } else { - updateClipRegion_dev(QRegion(), Qt::NoClip); - } - } - - if (flags & DirtyClipPath) { - QPolygonF clip_poly_dev(d->matrix.map(state.clipPath().toFillPolygon())); - QPolygonF clipped_poly_dev; - d->clipPolygon_dev(clip_poly_dev, &clipped_poly_dev); - updateClipRegion_dev(QRegion(clipped_poly_dev.toPolygon(), state.clipPath().fillRule()), - state.clipOperation()); - } else if (flags & DirtyClipRegion) { - extern QPainterPath qt_regionToPath(const QRegion ®ion); - QPainterPath clip_path = qt_regionToPath(state.clipRegion()); - QPolygonF clip_poly_dev(d->matrix.map(clip_path.toFillPolygon())); - QPolygonF clipped_poly_dev; - d->clipPolygon_dev(clip_poly_dev, &clipped_poly_dev); - updateClipRegion_dev(QRegion(clipped_poly_dev.toPolygon()), state.clipOperation()); - } - if (flags & DirtyHints) updateRenderHints(state.renderHints()); - if (flags & DirtyCompositionMode) { - int function = GXcopy; - if (state.compositionMode() >= QPainter::RasterOp_SourceOrDestination) { - switch (state.compositionMode()) { - case QPainter::RasterOp_SourceOrDestination: - function = GXor; - break; - case QPainter::RasterOp_SourceAndDestination: - function = GXand; - break; - case QPainter::RasterOp_SourceXorDestination: - function = GXxor; - break; - case QPainter::RasterOp_NotSourceAndNotDestination: - function = GXnor; - break; - case QPainter::RasterOp_NotSourceOrNotDestination: - function = GXnand; - break; - case QPainter::RasterOp_NotSourceXorDestination: - function = GXequiv; - break; - case QPainter::RasterOp_NotSource: - function = GXcopyInverted; - break; - case QPainter::RasterOp_SourceAndNotDestination: - function = GXandReverse; - break; - case QPainter::RasterOp_NotSourceAndDestination: - function = GXandInverted; - break; - default: - function = GXcopy; - } - } -#if QT_CONFIG(xrender) - else { - d->composition_mode = - qpainterOpToXrender(state.compositionMode()); - } -#endif - XSetFunction(X11->display, d->gc, function); - XSetFunction(X11->display, d->gc_brush, function); - } - d->decidePathFallback(); - d->decideCoordAdjust(); -} - -void QX11PaintEngine::updateRenderHints(QPainter::RenderHints hints) -{ - Q_D(QX11PaintEngine); - d->render_hints = hints; - -#if QT_CONFIG(xrender) - if (X11->use_xrender && d->picture) { - XRenderPictureAttributes attrs; - attrs.poly_edge = (hints & QPainter::Antialiasing) ? PolyEdgeSmooth : PolyEdgeSharp; - XRenderChangePicture(d->dpy, d->picture, CPPolyEdge, &attrs); - } -#endif -} - -void QX11PaintEngine::updatePen(const QPen &pen) -{ - Q_D(QX11PaintEngine); - d->cpen = pen; - int cp = CapButt; - int jn = JoinMiter; - int ps = pen.style(); - - if (d->opacity < 1.0) { - QColor c = d->cpen.color(); - c.setAlpha(qRound(c.alpha()*d->opacity)); - d->cpen.setColor(c); - } - - d->has_pen = (ps != Qt::NoPen); - d->has_alpha_pen = (pen.color().alpha() != 255); - - switch (pen.capStyle()) { - case Qt::SquareCap: - cp = CapProjecting; - break; - case Qt::RoundCap: - cp = CapRound; - break; - case Qt::FlatCap: - default: - cp = CapButt; - break; - } - switch (pen.joinStyle()) { - case Qt::BevelJoin: - jn = JoinBevel; - break; - case Qt::RoundJoin: - jn = JoinRound; - break; - case Qt::MiterJoin: - default: - jn = JoinMiter; - break; - } - - d->adapted_pen_origin = false; - - char dashes[10]; // custom pen dashes - int dash_len = 0; // length of dash list - int xStyle = LineSolid; - - /* - We are emulating Windows here. Windows treats cpen.width() == 1 - (or 0) as a very special case. The fudge variable unifies this - case with the general case. - */ - qreal pen_width = pen.widthF(); - int scale = qRound(pen_width < 1 ? 1 : pen_width); - int space = (pen_width < 1 && pen_width > 0 ? 1 : (2 * scale)); - int dot = 1 * scale; - int dash = 4 * scale; - - d->has_custom_pen = false; - - switch (ps) { - case Qt::NoPen: - case Qt::SolidLine: - xStyle = LineSolid; - break; - case Qt::DashLine: - dashes[0] = dash; - dashes[1] = space; - dash_len = 2; - xStyle = LineOnOffDash; - break; - case Qt::DotLine: - dashes[0] = dot; - dashes[1] = space; - dash_len = 2; - xStyle = LineOnOffDash; - break; - case Qt::DashDotLine: - dashes[0] = dash; - dashes[1] = space; - dashes[2] = dot; - dashes[3] = space; - dash_len = 4; - xStyle = LineOnOffDash; - break; - case Qt::DashDotDotLine: - dashes[0] = dash; - dashes[1] = space; - dashes[2] = dot; - dashes[3] = space; - dashes[4] = dot; - dashes[5] = space; - dash_len = 6; - xStyle = LineOnOffDash; - break; - case Qt::CustomDashLine: - d->has_custom_pen = true; - break; - } - - ulong mask = GCForeground | GCBackground | GCGraphicsExposures | GCLineWidth - | GCCapStyle | GCJoinStyle | GCLineStyle; - XGCValues vals; - vals.graphics_exposures = false; - if (d->pdev_depth == 1) { - vals.foreground = qGray(pen.color().rgb()) > 127 ? 0 : 1; - vals.background = qGray(QColor(Qt::transparent).rgb()) > 127 ? 0 : 1; - } else if (d->pdev->devType() == QInternal::Pixmap && d->pdev_depth == 32 - && X11->use_xrender) { - vals.foreground = pen.color().rgba(); - vals.background = QColor(Qt::transparent).rgba(); - } else { - QXcbColormap cmap = QXcbColormap::instance(d->scrn); - vals.foreground = cmap.pixel(pen.color()); - vals.background = cmap.pixel(QColor(Qt::transparent)); - } - - - vals.line_width = qRound(pen.widthF()); - vals.cap_style = cp; - vals.join_style = jn; - vals.line_style = xStyle; - - XChangeGC(d->dpy, d->gc, mask, &vals); - - if (dash_len) { // make dash list - XSetDashes(d->dpy, d->gc, 0, dashes, dash_len); - } - - if (!d->has_clipping) { // if clipping is set the paintevent clip region is merged with the clip region - QRegion sysClip = d->use_sysclip ? systemClip() : QRegion(); - if (!sysClip.isEmpty()) - x11SetClipRegion(d->dpy, d->gc, 0, d->picture, sysClip); - else - x11ClearClipRegion(d->dpy, d->gc, 0, d->picture); - } -} - -void QX11PaintEngine::updateBrush(const QBrush &brush, const QPointF &origin) -{ - Q_D(QX11PaintEngine); - d->cbrush = brush; - d->bg_origin = origin; - d->adapted_brush_origin = false; -#if QT_CONFIG(xrender) - d->current_brush = 0; -#endif - if (d->opacity < 1.0) { - QColor c = d->cbrush.color(); - c.setAlpha(qRound(c.alpha()*d->opacity)); - d->cbrush.setColor(c); - } - - int s = FillSolid; - int bs = d->cbrush.style(); - d->has_brush = (bs != Qt::NoBrush); - d->has_pattern = bs >= Qt::Dense1Pattern && bs <= Qt::DiagCrossPattern; - d->has_texture = bs == Qt::TexturePattern; - d->has_alpha_brush = brush.color().alpha() != 255; - d->has_alpha_texture = d->has_texture && d->cbrush.texture().hasAlphaChannel(); - - ulong mask = GCForeground | GCBackground | GCGraphicsExposures - | GCLineStyle | GCCapStyle | GCJoinStyle | GCFillStyle; - XGCValues vals; - vals.graphics_exposures = false; - if (d->pdev_depth == 1) { - vals.foreground = qGray(d->cbrush.color().rgb()) > 127 ? 0 : 1; - vals.background = qGray(QColor(Qt::transparent).rgb()) > 127 ? 0 : 1; - } else if (X11->use_xrender && d->pdev->devType() == QInternal::Pixmap - && d->pdev_depth == 32) { - vals.foreground = d->cbrush.color().rgba(); - vals.background = QColor(Qt::transparent).rgba(); - } else { - QXcbColormap cmap = QXcbColormap::instance(d->scrn); - vals.foreground = cmap.pixel(d->cbrush.color()); - vals.background = cmap.pixel(QColor(Qt::transparent)); - - if (!X11->use_xrender && d->has_brush && !d->has_pattern && !brush.isOpaque()) { - QPixmap pattern = qt_patternForAlpha(brush.color().alpha(), d->scrn); - mask |= GCStipple; - vals.stipple = qt_x11PixmapHandle(pattern); - s = FillStippled; - d->adapted_brush_origin = true; - } - } - vals.cap_style = CapButt; - vals.join_style = JoinMiter; - vals.line_style = LineSolid; - - if (d->has_pattern || d->has_texture) { - if (bs == Qt::TexturePattern) { - d->brush_pm = qt_toX11Pixmap(d->cbrush.texture()); -#if QT_CONFIG(xrender) - if (X11->use_xrender) { - XRenderPictureAttributes attrs; - attrs.repeat = true; - XRenderChangePicture(d->dpy, qt_x11PictureHandle(d->brush_pm), CPRepeat, &attrs); - QX11PlatformPixmap *data = static_cast<QX11PlatformPixmap*>(d->brush_pm.handle()); - if (data->mask_picture) - XRenderChangePicture(d->dpy, data->mask_picture, CPRepeat, &attrs); - } -#endif - } else { - d->brush_pm = qt_toX11Pixmap(qt_pixmapForBrush(bs, true)); - } - qt_x11SetScreen(d->brush_pm, d->scrn); - if (d->brush_pm.depth() == 1) { - mask |= GCStipple; - vals.stipple = qt_x11PixmapHandle(d->brush_pm); - s = FillStippled; -#if QT_CONFIG(xrender) - if (X11->use_xrender) { - d->bitmap_texture = QPixmap(d->brush_pm.size()); - d->bitmap_texture.fill(Qt::transparent); - d->bitmap_texture = qt_toX11Pixmap(d->bitmap_texture); - qt_x11SetScreen(d->bitmap_texture, d->scrn); - - ::Picture src = X11->getSolidFill(d->scrn, d->cbrush.color()); - XRenderComposite(d->dpy, PictOpSrc, src, qt_x11PictureHandle(d->brush_pm), - qt_x11PictureHandle(d->bitmap_texture), - 0, 0, d->brush_pm.width(), d->brush_pm.height(), - 0, 0, d->brush_pm.width(), d->brush_pm.height()); - - XRenderPictureAttributes attrs; - attrs.repeat = true; - XRenderChangePicture(d->dpy, qt_x11PictureHandle(d->bitmap_texture), CPRepeat, &attrs); - - d->current_brush = qt_x11PictureHandle(d->bitmap_texture); - } -#endif - } else { - mask |= GCTile; -#if QT_CONFIG(xrender) - if (d->pdev_depth == 32 && d->brush_pm.depth() != 32) { - d->brush_pm.detach(); - QX11PlatformPixmap *brushData = static_cast<QX11PlatformPixmap*>(d->brush_pm.handle()); - brushData->convertToARGB32(); - } -#endif - vals.tile = (d->brush_pm.depth() == d->pdev_depth - ? qt_x11PixmapHandle(d->brush_pm) - : static_cast<QX11PlatformPixmap*>(d->brush_pm.handle())->x11ConvertToDefaultDepth()); - s = FillTiled; -#if QT_CONFIG(xrender) - d->current_brush = qt_x11PictureHandle(d->cbrush.texture()); -#endif - } - - mask |= GCTileStipXOrigin | GCTileStipYOrigin; - vals.ts_x_origin = qRound(origin.x()); - vals.ts_y_origin = qRound(origin.y()); - } -#if QT_CONFIG(xrender) - else if (d->has_alpha_brush) { - d->current_brush = X11->getSolidFill(d->scrn, d->cbrush.color()); - } -#endif - - vals.fill_style = s; - XChangeGC(d->dpy, d->gc_brush, mask, &vals); - if (!d->has_clipping) { - QRegion sysClip = d->use_sysclip ? systemClip() : QRegion(); - if (!sysClip.isEmpty()) - x11SetClipRegion(d->dpy, d->gc_brush, 0, d->picture, sysClip); - else - x11ClearClipRegion(d->dpy, d->gc_brush, 0, d->picture); - } -} - -void QX11PaintEngine::drawEllipse(const QRectF &rect) -{ - QRect aligned = rect.toAlignedRect(); - if (aligned == rect) - drawEllipse(aligned); - else - QPaintEngine::drawEllipse(rect); -} - -void QX11PaintEngine::drawEllipse(const QRect &rect) -{ - if (rect.isEmpty()) { - drawRects(&rect, 1); - return; - } - - Q_D(QX11PaintEngine); - QRect devclip(SHRT_MIN, SHRT_MIN, SHRT_MAX*2 - 1, SHRT_MAX*2 - 1); - QRect r(rect); - if (d->txop < QTransform::TxRotate) { - r = d->matrix.mapRect(rect); - } else if (d->txop == QTransform::TxRotate && rect.width() == rect.height()) { - QPainterPath path; - path.addEllipse(rect); - r = d->matrix.map(path).boundingRect().toRect(); - } - - if (d->has_alpha_brush || d->has_alpha_pen || d->has_custom_pen || (d->render_hints & QPainter::Antialiasing) - || d->has_alpha_texture || devclip.intersected(r) != r - || (d->has_complex_xform - && !(d->has_non_scaling_xform && rect.width() == rect.height()))) - { - QPainterPath path; - path.addEllipse(rect); - drawPath(path); - return; - } - - int x = r.x(); - int y = r.y(); - int w = r.width(); - int h = r.height(); - if (w < 1 || h < 1) - return; - if (w == 1 && h == 1) { - XDrawPoint(d->dpy, d->hd, d->has_pen ? d->gc : d->gc_brush, x, y); - return; - } - d->setupAdaptedOrigin(rect.topLeft()); - if (d->has_brush) { // draw filled ellipse - XFillArc(d->dpy, d->hd, d->gc_brush, x, y, w, h, 0, 360*64); - if (!d->has_pen) // make smoother outline - XDrawArc(d->dpy, d->hd, d->gc_brush, x, y, w-1, h-1, 0, 360*64); - } - if (d->has_pen) // draw outline - XDrawArc(d->dpy, d->hd, d->gc, x, y, w, h, 0, 360*64); - d->resetAdaptedOrigin(); -} - - - -void QX11PaintEnginePrivate::fillPolygon_translated(const QPointF *polygonPoints, int pointCount, - QX11PaintEnginePrivate::GCMode gcMode, - QPaintEngine::PolygonDrawMode mode) -{ - - QVarLengthArray<QPointF> translated_points(pointCount); - QPointF offset(matrix.dx(), matrix.dy()); - - qreal offs = adjust_coords ? aliasedCoordinateDelta : 0.0; - if (!X11->use_xrender || !(render_hints & QPainter::Antialiasing)) - offset += QPointF(aliasedCoordinateDelta, aliasedCoordinateDelta); - - for (int i = 0; i < pointCount; ++i) { - translated_points[i] = polygonPoints[i] + offset; - - translated_points[i].rx() = qRound(translated_points[i].x()) + offs; - translated_points[i].ry() = qRound(translated_points[i].y()) + offs; - } - - fillPolygon_dev(translated_points.data(), pointCount, gcMode, mode); -} - -#if QT_CONFIG(xrender) -static void qt_XRenderCompositeTrapezoids(Display *dpy, - int op, - Picture src, - Picture dst, - _Xconst XRenderPictFormat *maskFormat, - int xSrc, - int ySrc, - const XTrapezoid *traps, int size) -{ - const int MAX_TRAPS = 50000; - while (size) { - int to_draw = size; - if (to_draw > MAX_TRAPS) - to_draw = MAX_TRAPS; - XRenderCompositeTrapezoids(dpy, op, src, dst, - maskFormat, - xSrc, ySrc, - traps, to_draw); - size -= to_draw; - traps += to_draw; - } -} -#endif - -void QX11PaintEnginePrivate::fillPolygon_dev(const QPointF *polygonPoints, int pointCount, - QX11PaintEnginePrivate::GCMode gcMode, - QPaintEngine::PolygonDrawMode mode) -{ - Q_Q(QX11PaintEngine); - - int clippedCount = 0; - qt_float_point *clippedPoints = 0; - -#if QT_CONFIG(xrender) - //can change if we switch to pen if gcMode != BrushGC - bool has_fill_texture = has_texture; - bool has_fill_pattern = has_pattern; - ::Picture src; -#endif - QBrush fill; - GC fill_gc; - if (gcMode == BrushGC) { - fill = cbrush; - fill_gc = gc_brush; -#if QT_CONFIG(xrender) - if (current_brush) - src = current_brush; - else - src = X11->getSolidFill(scrn, fill.color()); -#endif - } else { - fill = QBrush(cpen.brush()); - fill_gc = gc; -#if QT_CONFIG(xrender) - //we use the pens brush - has_fill_texture = (fill.style() == Qt::TexturePattern); - has_fill_pattern = (fill.style() >= Qt::Dense1Pattern && fill.style() <= Qt::DiagCrossPattern); - if (has_fill_texture) - src = qt_x11PictureHandle(fill.texture()); - else if (has_fill_pattern) - src = getPatternFill(scrn, fill); - else - src = X11->getSolidFill(scrn, fill.color()); -#endif - } - - polygonClipper.clipPolygon((qt_float_point *) polygonPoints, pointCount, - &clippedPoints, &clippedCount); - -#if QT_CONFIG(xrender) - bool solid_fill = fill.color().alpha() == 255; - if (has_fill_texture && fill.texture().depth() == 1 && solid_fill) { - has_fill_texture = false; - has_fill_pattern = true; - } - - bool antialias = render_hints & QPainter::Antialiasing; - - if (X11->use_xrender - && picture - && !has_fill_pattern - && (clippedCount > 0) - && (fill.style() != Qt::NoBrush) - && ((has_fill_texture && fill.texture().hasAlpha()) || antialias || !solid_fill || has_alpha_pen != has_alpha_brush)) - { - tessellator->tessellate((QPointF *)clippedPoints, clippedCount, - mode == QPaintEngine::WindingMode); - if (tessellator->size > 0) { - XRenderPictureAttributes attrs; - attrs.poly_edge = antialias ? PolyEdgeSmooth : PolyEdgeSharp; - XRenderChangePicture(dpy, picture, CPPolyEdge, &attrs); - int x_offset = int(XFixedToDouble(tessellator->traps[0].left.p1.x) - bg_origin.x()); - int y_offset = int(XFixedToDouble(tessellator->traps[0].left.p1.y) - bg_origin.y()); - qt_XRenderCompositeTrapezoids(dpy, composition_mode, src, picture, - antialias - ? XRenderFindStandardFormat(dpy, PictStandardA8) - : XRenderFindStandardFormat(dpy, PictStandardA1), - x_offset, y_offset, - tessellator->traps, tessellator->size); - tessellator->done(); - } - } else -#endif - if (fill.style() != Qt::NoBrush) { - if (clippedCount > 200000) { - QPolygon poly; - for (int i = 0; i < clippedCount; ++i) - poly << QPoint(qFloor(clippedPoints[i].x), qFloor(clippedPoints[i].y)); - - const QRect bounds = poly.boundingRect(); - const QRect aligned = bounds - & QRect(QPoint(), QSize(pdev->width(), pdev->height())); - - QImage img(aligned.size(), QImage::Format_ARGB32_Premultiplied); - img.fill(0); - - QPainter painter(&img); - painter.translate(-aligned.x(), -aligned.y()); - painter.setPen(Qt::NoPen); - painter.setBrush(fill); - if (gcMode == BrushGC) - painter.setBrushOrigin(q->painter()->brushOriginF()); - painter.drawPolygon(poly); - painter.end(); - - q->drawImage(aligned, img, img.rect(), Qt::AutoColor); - } else if (clippedCount > 0) { - QVarLengthArray<XPoint> xpoints(clippedCount); - for (int i = 0; i < clippedCount; ++i) { - xpoints[i].x = qFloor(clippedPoints[i].x); - xpoints[i].y = qFloor(clippedPoints[i].y); - } - if (mode == QPaintEngine::WindingMode) - XSetFillRule(dpy, fill_gc, WindingRule); - setupAdaptedOrigin(QPoint(xpoints[0].x, xpoints[0].y)); - XFillPolygon(dpy, hd, fill_gc, - xpoints.data(), clippedCount, - mode == QPaintEngine::ConvexMode ? Convex : Complex, CoordModeOrigin); - resetAdaptedOrigin(); - if (mode == QPaintEngine::WindingMode) - XSetFillRule(dpy, fill_gc, EvenOddRule); - } - } -} - -void QX11PaintEnginePrivate::strokePolygon_translated(const QPointF *polygonPoints, int pointCount, bool close) -{ - QVarLengthArray<QPointF> translated_points(pointCount); - QPointF offset(matrix.dx(), matrix.dy()); - for (int i = 0; i < pointCount; ++i) - translated_points[i] = polygonPoints[i] + offset; - strokePolygon_dev(translated_points.data(), pointCount, close); -} - -void QX11PaintEnginePrivate::strokePolygon_dev(const QPointF *polygonPoints, int pointCount, bool close) -{ - int clippedCount = 0; - qt_float_point *clippedPoints = 0; - polygonClipper.clipPolygon((qt_float_point *) polygonPoints, pointCount, - &clippedPoints, &clippedCount, close); - - if (clippedCount > 0) { - QVarLengthArray<XPoint> xpoints(clippedCount); - for (int i = 0; i < clippedCount; ++i) { - xpoints[i].x = qRound(clippedPoints[i].x + aliasedCoordinateDelta); - xpoints[i].y = qRound(clippedPoints[i].y + aliasedCoordinateDelta); - } - uint numberPoints = qMin(clippedCount, xlibMaxLinePoints); - XPoint *pts = xpoints.data(); - XDrawLines(dpy, hd, gc, pts, numberPoints, CoordModeOrigin); - pts += numberPoints; - clippedCount -= numberPoints; - numberPoints = qMin(clippedCount, xlibMaxLinePoints-1); - while (clippedCount) { - XDrawLines(dpy, hd, gc, pts-1, numberPoints+1, CoordModeOrigin); - pts += numberPoints; - clippedCount -= numberPoints; - numberPoints = qMin(clippedCount, xlibMaxLinePoints-1); - } - } -} - -void QX11PaintEngine::drawPolygon(const QPointF *polygonPoints, int pointCount, PolygonDrawMode mode) -{ - Q_D(QX11PaintEngine); - - if (d->use_path_fallback) { - QPainterPath path(polygonPoints[0]); - for (int i = 1; i < pointCount; ++i) - path.lineTo(polygonPoints[i]); - if (mode == PolylineMode) { - QBrush oldBrush = d->cbrush; - d->cbrush = QBrush(Qt::NoBrush); - path.setFillRule(Qt::WindingFill); - drawPath(path); - d->cbrush = oldBrush; - } else { - path.setFillRule(mode == OddEvenMode ? Qt::OddEvenFill : Qt::WindingFill); - path.closeSubpath(); - drawPath(path); - } - return; - } - if (mode != PolylineMode && d->has_brush) - d->fillPolygon_translated(polygonPoints, pointCount, QX11PaintEnginePrivate::BrushGC, mode); - - if (d->has_pen) - d->strokePolygon_translated(polygonPoints, pointCount, mode != PolylineMode); -} - - -void QX11PaintEnginePrivate::fillPath(const QPainterPath &path, QX11PaintEnginePrivate::GCMode gc_mode, bool transform) -{ - qreal offs = adjust_coords ? aliasedCoordinateDelta : 0.0; - - QPainterPath clippedPath; - QPainterPath clipPath; - clipPath.addRect(polygonClipper.boundingRect()); - - if (transform) - clippedPath = (path*matrix).intersected(clipPath); - else - clippedPath = path.intersected(clipPath); - - QList<QPolygonF> polys = clippedPath.toFillPolygons(); - for (int i = 0; i < polys.size(); ++i) { - QVarLengthArray<QPointF> translated_points(polys.at(i).size()); - - for (int j = 0; j < polys.at(i).size(); ++j) { - translated_points[j] = polys.at(i).at(j); - if (!X11->use_xrender || !(render_hints & QPainter::Antialiasing)) { - translated_points[j].rx() = qRound(translated_points[j].rx() + aliasedCoordinateDelta) + offs; - translated_points[j].ry() = qRound(translated_points[j].ry() + aliasedCoordinateDelta) + offs; - } - } - - fillPolygon_dev(translated_points.data(), polys.at(i).size(), gc_mode, - path.fillRule() == Qt::OddEvenFill ? QPaintEngine::OddEvenMode : QPaintEngine::WindingMode); - } -} - -void QX11PaintEngine::drawPath(const QPainterPath &path) -{ - Q_D(QX11PaintEngine); - if (path.isEmpty()) - return; - - if (d->has_brush) - d->fillPath(path, QX11PaintEnginePrivate::BrushGC, true); - if (d->has_pen - && ((X11->use_xrender && (d->has_alpha_pen || (d->render_hints & QPainter::Antialiasing))) - || (!d->isCosmeticPen() && d->txop > QTransform::TxTranslate - && !d->has_non_scaling_xform) - || (d->cpen.style() == Qt::CustomDashLine))) { - QPainterPathStroker stroker; - if (d->cpen.style() == Qt::CustomDashLine) { - stroker.setDashPattern(d->cpen.dashPattern()); - stroker.setDashOffset(d->cpen.dashOffset()); - } else { - stroker.setDashPattern(d->cpen.style()); - } - stroker.setCapStyle(d->cpen.capStyle()); - stroker.setJoinStyle(d->cpen.joinStyle()); - QPainterPath stroke; - qreal width = d->cpen.widthF(); - QPolygonF poly; - QRectF deviceRect(0, 0, d->pdev->width(), d->pdev->height()); - // necessary to get aliased alphablended primitives to be drawn correctly - if (d->isCosmeticPen() || d->has_scaling_xform) { - if (d->isCosmeticPen()) - stroker.setWidth(width == 0 ? 1 : width); - else - stroker.setWidth(width * d->xform_scale); - stroker.d_ptr->stroker.setClipRect(deviceRect); - stroke = stroker.createStroke(path * d->matrix); - if (stroke.isEmpty()) - return; - stroke.setFillRule(Qt::WindingFill); - d->fillPath(stroke, QX11PaintEnginePrivate::PenGC, false); - } else { - stroker.setWidth(width); - stroker.d_ptr->stroker.setClipRect(d->matrix.inverted().mapRect(deviceRect)); - stroke = stroker.createStroke(path); - if (stroke.isEmpty()) - return; - stroke.setFillRule(Qt::WindingFill); - d->fillPath(stroke, QX11PaintEnginePrivate::PenGC, true); - } - } else if (d->has_pen) { - // if we have a cosmetic pen - use XDrawLine() for speed - QList<QPolygonF> polys = path.toSubpathPolygons(d->matrix); - for (int i = 0; i < polys.size(); ++i) - d->strokePolygon_dev(polys.at(i).data(), polys.at(i).size(), false); - } -} - -Q_GUI_EXPORT void qt_x11_drawImage(const QRect &rect, const QPoint &pos, const QImage &image, - Drawable hd, GC gc, Display *dpy, Visual *visual, int depth) -{ - Q_ASSERT(image.format() == QImage::Format_RGB32); - Q_ASSERT(image.depth() == 32); - - XImage *xi; - // Note: this code assumes either RGB or BGR, 8 bpc server layouts - const uint red_mask = (uint) visual->red_mask; - bool bgr_layout = (red_mask == 0xff); - - const int w = rect.width(); - const int h = rect.height(); - - QImage im; - int image_byte_order = ImageByteOrder(QXcbX11Info::display()); - if ((QSysInfo::ByteOrder == QSysInfo::BigEndian && ((image_byte_order == LSBFirst) || bgr_layout)) - || (image_byte_order == MSBFirst && QSysInfo::ByteOrder == QSysInfo::LittleEndian) - || (image_byte_order == LSBFirst && bgr_layout)) - { - im = image.copy(rect); - const qsizetype iw = im.bytesPerLine() / 4; - uint *data = (uint *)im.bits(); - for (int i=0; i < h; i++) { - uint *p = data; - uint *end = p + w; - if (bgr_layout && image_byte_order == MSBFirst && QSysInfo::ByteOrder == QSysInfo::LittleEndian) { - while (p < end) { - *p = ((*p << 8) & 0xffffff00) | ((*p >> 24) & 0x000000ff); - p++; - } - } else if ((image_byte_order == LSBFirst && QSysInfo::ByteOrder == QSysInfo::BigEndian) - || (image_byte_order == MSBFirst && QSysInfo::ByteOrder == QSysInfo::LittleEndian)) { - while (p < end) { - *p = ((*p << 24) & 0xff000000) | ((*p << 8) & 0x00ff0000) - | ((*p >> 8) & 0x0000ff00) | ((*p >> 24) & 0x000000ff); - p++; - } - } else if ((image_byte_order == MSBFirst && QSysInfo::ByteOrder == QSysInfo::BigEndian) - || (image_byte_order == LSBFirst && bgr_layout)) - { - while (p < end) { - *p = ((*p << 16) & 0x00ff0000) | ((*p >> 16) & 0x000000ff) - | ((*p ) & 0xff00ff00); - p++; - } - } - data += iw; - } - xi = XCreateImage(dpy, visual, depth, ZPixmap, - 0, (char *) im.bits(), w, h, 32, im.bytesPerLine()); - } else { - xi = XCreateImage(dpy, visual, depth, ZPixmap, - 0, (char *) image.scanLine(rect.y())+rect.x()*sizeof(uint), w, h, 32, image.bytesPerLine()); - } - XPutImage(dpy, hd, gc, xi, 0, 0, pos.x(), pos.y(), w, h); - xi->data = 0; // QImage owns these bits - XDestroyImage(xi); -} - -void QX11PaintEngine::drawImage(const QRectF &r, const QImage &image, const QRectF &sr, Qt::ImageConversionFlags flags) -{ - Q_D(QX11PaintEngine); - - if (image.format() == QImage::Format_RGB32 - && d->pdev_depth >= 24 && image.depth() == 32 - && r.size() == sr.size()) - { - int sx = qRound(sr.x()); - int sy = qRound(sr.y()); - int x = qRound(r.x()); - int y = qRound(r.y()); - int w = qRound(r.width()); - int h = qRound(r.height()); - - qt_x11_drawImage(QRect(sx, sy, w, h), QPoint(x, y), image, d->hd, d->gc, d->dpy, - (Visual *)d->xinfo->visual(), d->pdev_depth); - } else { - QPaintEngine::drawImage(r, image, sr, flags); - } -} - -void QX11PaintEngine::drawPixmap(const QRectF &r, const QPixmap &px, const QRectF &_sr) -{ - Q_D(QX11PaintEngine); - QRectF sr = _sr; - int x = qRound(r.x()); - int y = qRound(r.y()); - int sx = qRound(sr.x()); - int sy = qRound(sr.y()); - int sw = qRound(sr.width()); - int sh = qRound(sr.height()); - - QPixmap pixmap = qt_toX11Pixmap(px); - if (pixmap.isNull()) - return; - - if ((d->xinfo && d->xinfo->screen() != qt_x11Info(pixmap).screen()) - || (qt_x11Info(pixmap).screen() != DefaultScreen(QXcbX11Info::display()))) { - qt_x11SetScreen(pixmap, d->xinfo ? d->xinfo->screen() : DefaultScreen(X11->display)); - } - - qt_x11SetDefaultScreen(qt_x11Info(pixmap).screen()); - -#if QT_CONFIG(xrender) - ::Picture src_pict = qt_x11PictureHandle(pixmap); - if (src_pict && d->picture) { - const int pDepth = pixmap.depth(); - if (pDepth == 1 && (d->has_alpha_pen)) { - qt_render_bitmap(d->dpy, d->scrn, src_pict, d->picture, - sx, sy, x, y, sw, sh, d->cpen); - return; - } else if (pDepth != 1 && (pDepth == 32 || pDepth != d->pdev_depth)) { - XRenderComposite(d->dpy, d->composition_mode, - src_pict, 0, d->picture, sx, sy, 0, 0, x, y, sw, sh); - return; - } - } -#endif - - bool mono_src = pixmap.depth() == 1; - bool mono_dst = d->pdev_depth == 1; - bool restore_clip = false; - - if (static_cast<QX11PlatformPixmap*>(pixmap.handle())->x11_mask) { // pixmap has a mask - QBitmap comb(sw, sh); - GC cgc = XCreateGC(d->dpy, qt_x11PixmapHandle(comb), 0, 0); - XSetForeground(d->dpy, cgc, 0); - XFillRectangle(d->dpy, qt_x11PixmapHandle(comb), cgc, 0, 0, sw, sh); - XSetBackground(d->dpy, cgc, 0); - XSetForeground(d->dpy, cgc, 1); - if (!d->crgn.isEmpty()) { - QList<XRectangle> rects = qt_region_to_xrectangles(d->crgn); - XSetClipRectangles(d->dpy, cgc, -x, -y, rects.data(), rects.size(), Unsorted); - } else if (d->has_clipping) { - XSetClipRectangles(d->dpy, cgc, 0, 0, 0, 0, Unsorted); - } - XSetFillStyle(d->dpy, cgc, FillOpaqueStippled); - XSetTSOrigin(d->dpy, cgc, -sx, -sy); - XSetStipple(d->dpy, cgc, - static_cast<QX11PlatformPixmap*>(pixmap.handle())->x11_mask); - XFillRectangle(d->dpy, qt_x11PixmapHandle(comb), cgc, 0, 0, sw, sh); - XFreeGC(d->dpy, cgc); - - XSetClipOrigin(d->dpy, d->gc, x, y); - XSetClipMask(d->dpy, d->gc, qt_x11PixmapHandle(comb)); - restore_clip = true; - } - - if (mono_src) { - if (!d->crgn.isEmpty()) { - Pixmap comb = XCreatePixmap(d->dpy, d->hd, sw, sh, 1); - GC cgc = XCreateGC(d->dpy, comb, 0, 0); - XSetForeground(d->dpy, cgc, 0); - XFillRectangle(d->dpy, comb, cgc, 0, 0, sw, sh); - QList<XRectangle> rects = qt_region_to_xrectangles(d->crgn); - XSetClipRectangles(d->dpy, cgc, -x, -y, rects.data(), rects.size(), Unsorted); - XCopyArea(d->dpy, qt_x11PixmapHandle(pixmap), comb, cgc, sx, sy, sw, sh, 0, 0); - XFreeGC(d->dpy, cgc); - - XSetClipMask(d->dpy, d->gc, comb); - XSetClipOrigin(d->dpy, d->gc, x, y); - XFreePixmap(d->dpy, comb); - } else { - XSetClipMask(d->dpy, d->gc, qt_x11PixmapHandle(pixmap)); - XSetClipOrigin(d->dpy, d->gc, x - sx, y - sy); - } - - if (mono_dst) { - XSetForeground(d->dpy, d->gc, qGray(d->cpen.color().rgb()) > 127 ? 0 : 1); - } else { - QXcbColormap cmap = QXcbColormap::instance(d->scrn); - XSetForeground(d->dpy, d->gc, cmap.pixel(d->cpen.color())); - } - XFillRectangle(d->dpy, d->hd, d->gc, x, y, sw, sh); - restore_clip = true; - } else if (mono_dst && !mono_src) { - QBitmap bitmap = QBitmap::fromPixmap(pixmap); - XCopyArea(d->dpy, qt_x11PixmapHandle(bitmap), d->hd, d->gc, sx, sy, sw, sh, x, y); - } else { - XCopyArea(d->dpy, qt_x11PixmapHandle(pixmap), d->hd, d->gc, sx, sy, sw, sh, x, y); - } - - if (d->pdev->devType() == QInternal::Pixmap) { - const QPixmap *px = static_cast<const QPixmap*>(d->pdev); - Pixmap src_mask = static_cast<QX11PlatformPixmap*>(pixmap.handle())->x11_mask; - Pixmap dst_mask = static_cast<QX11PlatformPixmap*>(px->handle())->x11_mask; - if (dst_mask) { - GC cgc = XCreateGC(d->dpy, dst_mask, 0, 0); - XSetClipOrigin(d->dpy, cgc, x, y); - XSetClipMask(d->dpy, cgc, src_mask); - if (src_mask) { // copy src mask into dst mask - XCopyArea(d->dpy, src_mask, dst_mask, cgc, sx, sy, sw, sh, x, y); - } else { // no src mask, but make sure the area copied is opaque in dest - XSetBackground(d->dpy, cgc, 0); - XSetForeground(d->dpy, cgc, 1); - XFillRectangle(d->dpy, dst_mask, cgc, x, y, sw, sh); - } - XFreeGC(d->dpy, cgc); - } - } - - if (restore_clip) { - XSetClipOrigin(d->dpy, d->gc, 0, 0); - QList<XRectangle> rects = qt_region_to_xrectangles(d->crgn); - if (rects.isEmpty()) - XSetClipMask(d->dpy, d->gc, XNone); - else - XSetClipRectangles(d->dpy, d->gc, 0, 0, rects.data(), rects.size(), Unsorted); - } -} - -void QX11PaintEngine::updateMatrix(const QTransform &mtx) -{ - Q_D(QX11PaintEngine); - d->txop = mtx.type(); - d->matrix = mtx; - - d->has_complex_xform = (d->txop > QTransform::TxTranslate); - - extern bool qt_scaleForTransform(const QTransform &transform, qreal *scale); - bool scaling = qt_scaleForTransform(d->matrix, &d->xform_scale); - d->has_scaling_xform = scaling && d->xform_scale != 1.0; - d->has_non_scaling_xform = scaling && d->xform_scale == 1.0; -} - -/* - NB! the clip region is expected to be in dev coordinates -*/ -void QX11PaintEngine::updateClipRegion_dev(const QRegion &clipRegion, Qt::ClipOperation op) -{ - Q_D(QX11PaintEngine); - QRegion sysClip = d->use_sysclip ? systemClip() : QRegion(); - if (op == Qt::NoClip) { - d->has_clipping = false; - d->crgn = sysClip; - if (!sysClip.isEmpty()) { - x11SetClipRegion(d->dpy, d->gc, d->gc_brush, d->picture, sysClip); - } else { - x11ClearClipRegion(d->dpy, d->gc, d->gc_brush, d->picture); - } - return; - } - - switch (op) { - case Qt::IntersectClip: - if (d->has_clipping) { - d->crgn &= clipRegion; - break; - } - // fall through - case Qt::ReplaceClip: - if (!sysClip.isEmpty()) - d->crgn = clipRegion.intersected(sysClip); - else - d->crgn = clipRegion; - break; -// case Qt::UniteClip: -// d->crgn |= clipRegion; -// if (!sysClip.isEmpty()) -// d->crgn = d->crgn.intersected(sysClip); -// break; - default: - break; - } - d->has_clipping = true; - x11SetClipRegion(d->dpy, d->gc, d->gc_brush, d->picture, d->crgn); -} - -void QX11PaintEngine::updateFont(const QFont &) -{ -} - -Drawable QX11PaintEngine::handle() const -{ - Q_D(const QX11PaintEngine); - Q_ASSERT(isActive()); - Q_ASSERT(d->hd); - return d->hd; -} - -extern void qt_draw_tile(QPaintEngine *, qreal, qreal, qreal, qreal, const QPixmap &, - qreal, qreal); - -void QX11PaintEngine::drawTiledPixmap(const QRectF &r, const QPixmap &pixmap, const QPointF &p) -{ - int x = qRound(r.x()); - int y = qRound(r.y()); - int w = qRound(r.width()); - int h = qRound(r.height()); - int sx = qRound(p.x()); - int sy = qRound(p.y()); - - bool mono_src = pixmap.depth() == 1; - Q_D(QX11PaintEngine); - - if ((d->xinfo && d->xinfo->screen() != qt_x11Info(pixmap).screen()) - || (qt_x11Info(pixmap).screen() != DefaultScreen(QXcbX11Info::display()))) { - QPixmap* p = const_cast<QPixmap *>(&pixmap); - qt_x11SetScreen(*p, d->xinfo ? d->xinfo->screen() : DefaultScreen(QXcbX11Info::display())); - } - - qt_x11SetDefaultScreen(qt_x11Info(pixmap).screen()); - -#if QT_CONFIG(xrender) - if (X11->use_xrender && d->picture && qt_x11PictureHandle(pixmap)) { - const int numTiles = (w / pixmap.width()) * (h / pixmap.height()); - if (numTiles < 100) { - // this is essentially qt_draw_tile(), inlined for - // the XRenderComposite call - int yPos, xPos, drawH, drawW, yOff, xOff; - yPos = y; - yOff = sy; - while (yPos < y + h) { - drawH = pixmap.height() - yOff; // Cropping first row - if (yPos + drawH > y + h) // Cropping last row - drawH = y + h - yPos; - xPos = x; - xOff = sx; - while (xPos < x + w) { - drawW = pixmap.width() - xOff; // Cropping first column - if (xPos + drawW > x + w) // Cropping last column - drawW = x + w - xPos; - if (mono_src) { - qt_render_bitmap(d->dpy, d->scrn, qt_x11PictureHandle(pixmap), d->picture, - xOff, yOff, xPos, yPos, drawW, drawH, d->cpen); - } else { - XRenderComposite(d->dpy, d->composition_mode, - qt_x11PictureHandle(pixmap), XNone, d->picture, - xOff, yOff, 0, 0, xPos, yPos, drawW, drawH); - } - xPos += drawW; - xOff = 0; - } - yPos += drawH; - yOff = 0; - } - } else { - w = qMin(w, d->pdev->width() - x); - h = qMin(h, d->pdev->height() - y); - if (w <= 0 || h <= 0) - return; - - const int pw = w + sx; - const int ph = h + sy; - QPixmap pm(pw, ph); - if (pixmap.hasAlpha() || mono_src) - pm.fill(Qt::transparent); - - const int mode = pixmap.hasAlpha() ? PictOpOver : PictOpSrc; - const ::Picture pmPicture = qt_x11PictureHandle(pm); - - // first tile - XRenderComposite(d->dpy, mode, - qt_x11PictureHandle(pixmap), XNone, pmPicture, - 0, 0, 0, 0, 0, 0, qMin(pw, pixmap.width()), qMin(ph, pixmap.height())); - - // first row of tiles - int xPos = pixmap.width(); - const int sh = qMin(ph, pixmap.height()); - while (xPos < pw) { - const int sw = qMin(xPos, pw - xPos); - XRenderComposite(d->dpy, mode, - pmPicture, XNone, pmPicture, - 0, 0, 0, 0, xPos, 0, sw, sh); - xPos *= 2; - } - - // remaining rows - int yPos = pixmap.height(); - const int sw = pw; - while (yPos < ph) { - const int sh = qMin(yPos, ph - yPos); - XRenderComposite(d->dpy, mode, - pmPicture, XNone, pmPicture, - 0, 0, 0, 0, 0, yPos, sw, sh); - yPos *= 2; - } - - // composite - if (mono_src) - qt_render_bitmap(d->dpy, d->scrn, pmPicture, d->picture, - sx, sy, x, y, w, h, d->cpen); - else - XRenderComposite(d->dpy, d->composition_mode, - pmPicture, XNone, d->picture, - sx, sy, 0, 0, x, y, w, h); - } - } else -#endif // QT_CONFIG(xrender) - if (pixmap.depth() > 1 && !static_cast<QX11PlatformPixmap*>(pixmap.handle())->x11_mask) { - XSetTile(d->dpy, d->gc, qt_x11PixmapHandle(pixmap)); - XSetFillStyle(d->dpy, d->gc, FillTiled); - XSetTSOrigin(d->dpy, d->gc, x-sx, y-sy); - XFillRectangle(d->dpy, d->hd, d->gc, x, y, w, h); - XSetTSOrigin(d->dpy, d->gc, 0, 0); - XSetFillStyle(d->dpy, d->gc, FillSolid); - } else { - qt_draw_tile(this, x, y, w, h, pixmap, sx, sy); - } -} - -bool QX11PaintEngine::drawCachedGlyphs(const QTransform &transform, const QTextItemInt &ti) -{ -#if QT_CONFIG(xrender) - Q_D(QX11PaintEngine); - Q_ASSERT(ti.fontEngine->type() == QFontEngine::Freetype); - - if (!X11->use_xrender) - return false; - - QFontEngineFT *ft = static_cast<QFontEngineFT *>(ti.fontEngine); - QFontEngineFT::QGlyphSet *set = ft->loadGlyphSet(transform); - - if (!set || set->outline_drawing) - return false; - - QFontEngine::GlyphFormat glyphFormat = QXRenderGlyphCache::glyphFormatForDepth(ft, d->pdev_depth); - - QXRenderGlyphCache *cache = static_cast<QXRenderGlyphCache *>(ft->glyphCache(set, glyphFormat, transform)); - if (!cache) { - cache = new QXRenderGlyphCache(QXcbX11Info(), glyphFormat, transform); - ft->setGlyphCache(set, cache); - } - - return cache->draw(X11->getSolidFill(d->scrn, d->cpen.color()), d->picture, transform, ti); -#else // !QT_CONFIG(xrender) - return false; -#endif // QT_CONFIG(xrender) -} - -void QX11PaintEngine::drawTextItem(const QPointF &p, const QTextItem &textItem) -{ - Q_D(QX11PaintEngine); - const QTextItemInt &ti = static_cast<const QTextItemInt &>(textItem); - - switch (ti.fontEngine->type()) { - case QFontEngine::TestFontEngine: - case QFontEngine::Box: - d->drawBoxTextItem(p, ti); - break; -#if QT_CONFIG(fontconfig) - case QFontEngine::Freetype: - drawFreetype(p, ti); - break; -#endif - default: - Q_ASSERT(false); - } -} - -#if QT_CONFIG(fontconfig) -static bool path_for_glyphs(QPainterPath *path, - const QVarLengthArray<glyph_t> &glyphs, - const QVarLengthArray<QFixedPoint> &positions, - const QFontEngineFT *ft) -{ - bool result = true; - *path = QPainterPath(); - path->setFillRule(Qt::WindingFill); - ft->lockFace(); - int i = 0; - while (i < glyphs.size()) { - QFontEngineFT::Glyph *glyph = ft->loadGlyph(glyphs[i], QFixedPoint(), QFontEngineFT::Format_Mono); - // #### fix case where we don't get a glyph - if (!glyph || glyph->format != QFontEngineFT::Format_Mono) { - result = false; - break; - } - - int n = 0; - int h = glyph->height; - int xp = qRound(positions[i].x); - int yp = qRound(positions[i].y); - - xp += glyph->x; - yp += -glyph->y + glyph->height; - int pitch = ((glyph->width + 31) & ~31) >> 3; - - uchar *src = glyph->data; - while (h--) { - for (int x = 0; x < glyph->width; ++x) { - bool set = src[x >> 3] & (0x80 >> (x & 7)); - if (set) { - QRect r(xp + x, yp - h, 1, 1); - while (x+1 < glyph->width && src[(x+1) >> 3] & (0x80 >> ((x+1) & 7))) { - ++x; - r.setRight(r.right()+1); - } - - path->addRect(r); - ++n; - } - } - src += pitch; - } - ++i; - } - ft->unlockFace(); - return result; -} - -void QX11PaintEngine::drawFreetype(const QPointF &p, const QTextItemInt &ti) -{ - Q_D(QX11PaintEngine); - - if (!ti.glyphs.numGlyphs) - return; - - if (!d->cpen.isSolid()) { - QPaintEngine::drawTextItem(p, ti); - return; - } - - const bool xrenderPath = (X11->use_xrender - && !(d->pdev->devType() == QInternal::Pixmap - && static_cast<const QPixmap *>(d->pdev)->handle()->pixelType() == QPlatformPixmap::BitmapType)); - - if (xrenderPath) { - QTransform transform = d->matrix; - transform.translate(p.x(), p.y()); - - if (drawCachedGlyphs(transform, ti)) - return; - } - - QTransform transform; - transform.translate(p.x(), p.y()); - - QVarLengthArray<QFixedPoint> positions; - QVarLengthArray<glyph_t> glyphs; - ti.fontEngine->getGlyphPositions(ti.glyphs, transform, ti.flags, glyphs, positions); - - if (glyphs.count() == 0) - return; - - QFontEngineFT *ft = static_cast<QFontEngineFT *>(ti.fontEngine); - QFontEngineFT::QGlyphSet *set = ft->loadGlyphSet(transform); - QPainterPath path; - - if (!set || set->outline_drawing || !path_for_glyphs(&path, glyphs, positions, ft)) { - QPaintEngine::drawTextItem(p, ti); - return; - } - - if (path.elementCount() <= 1) - return; - - Q_ASSERT((path.elementCount() % 5) == 0); - if (d->txop >= QTransform::TxScale) { - painter()->save(); - painter()->setBrush(d->cpen.brush()); - painter()->setPen(Qt::NoPen); - painter()->drawPath(path); - painter()->restore(); - return; - } - - const int rectcount = 256; - XRectangle rects[rectcount]; - int num_rects = 0; - - QPoint delta(qRound(d->matrix.dx()), qRound(d->matrix.dy())); - QRect clip(d->polygonClipper.boundingRect()); - for (int i=0; i < path.elementCount(); i+=5) { - int x = qRound(path.elementAt(i).x); - int y = qRound(path.elementAt(i).y); - int w = qRound(path.elementAt(i+1).x) - x; - int h = qRound(path.elementAt(i+2).y) - y; - - QRect rect = QRect(x + delta.x(), y + delta.y(), w, h); - rect = rect.intersected(clip); - if (rect.isEmpty()) - continue; - - rects[num_rects].x = short(rect.x()); - rects[num_rects].y = short(rect.y()); - rects[num_rects].width = ushort(rect.width()); - rects[num_rects].height = ushort(rect.height()); - ++num_rects; - if (num_rects == rectcount) { - XFillRectangles(d->dpy, d->hd, d->gc, rects, num_rects); - num_rects = 0; - } - } - if (num_rects > 0) - XFillRectangles(d->dpy, d->hd, d->gc, rects, num_rects); -} -#endif // QT_CONFIG(fontconfig) - -#if QT_CONFIG(xrender) -QXRenderGlyphCache::QXRenderGlyphCache(QXcbX11Info x, QFontEngine::GlyphFormat format, const QTransform &matrix) - : QFontEngineGlyphCache(format, matrix) - , xinfo(x) - , gset(XNone) -{} - -QXRenderGlyphCache::~QXRenderGlyphCache() -{ - if (gset != XNone) - XRenderFreeGlyphSet(xinfo.display(), gset); -} - -bool QXRenderGlyphCache::addGlyphs(const QTextItemInt &ti, - const QVarLengthArray<glyph_t> &glyphs, - const QVarLengthArray<QFixedPoint> &positions) -{ - Q_ASSERT(ti.fontEngine->type() == QFontEngine::Freetype); - - QFontEngineFT *ft = static_cast<QFontEngineFT *>(ti.fontEngine); - QFontEngineFT::QGlyphSet *set = ft->loadGlyphSet(transform()); - - XGlyphInfo xglyphinfo; - - for (int i = 0; i < glyphs.size(); ++i) { - const QFixed sppx = ft->subPixelPositionForX(positions[i].x); - const QFixedPoint spp(sppx, 0); - QFontEngineFT::Glyph *glyph = set->getGlyph(glyphs[i], spp); - Glyph xglyphid = qHash(QFontEngineFT::GlyphAndSubPixelPosition(glyphs[i], spp)); - - if (glyph && glyph->format == glyphFormat()) { - if (cachedGlyphs.contains(xglyphid)) { - continue; - } else { - set->setGlyph(glyphs[i], spp, nullptr); - delete glyph; - glyph = 0; - } - } - - glyph = ft->loadGlyphFor(glyphs[i], spp, glyphFormat(), transform(), QColor()); - - if (glyph == 0 || glyph->format != glyphFormat()) - return false; - - if (glyph->format == QFontEngine::Format_Mono) { - // Must convert bitmap from msb to lsb bit order - QImage img(glyph->data, glyph->width, glyph->height, QImage::Format_Mono); - img = img.convertToFormat(QImage::Format_MonoLSB); - memcpy(glyph->data, img.constBits(), static_cast<size_t>(img.sizeInBytes())); - } - - set->setGlyph(glyphs[i], spp, glyph); - Q_ASSERT(glyph->data || glyph->width == 0 || glyph->height == 0); - - xglyphinfo.width = glyph->width; - xglyphinfo.height = glyph->height; - xglyphinfo.x = -glyph->x; - xglyphinfo.y = glyph->y; - xglyphinfo.xOff = glyph->advance; - xglyphinfo.yOff = 0; - - XRenderAddGlyphs(xinfo.display(), glyphSet(), &xglyphid, &xglyphinfo, 1, (const char *) glyph->data, glyphBufferSize(*glyph)); - cachedGlyphs.insert(xglyphid); - } - - return true; -} - -bool QXRenderGlyphCache::draw(Drawable src, Drawable dst, const QTransform &matrix, const QTextItemInt &ti) -{ - Q_ASSERT(ti.fontEngine->type() == QFontEngine::Freetype); - - if (ti.glyphs.numGlyphs == 0) - return true; - - QFontEngineFT *ft = static_cast<QFontEngineFT *>(ti.fontEngine); - QFontEngineFT::QGlyphSet *set = ft->loadGlyphSet(matrix); - - QVarLengthArray<glyph_t> glyphs; - QVarLengthArray<QFixedPoint> positions; - ti.fontEngine->getGlyphPositions(ti.glyphs, matrix, ti.flags, glyphs, positions); - - if (glyphs.isEmpty()) - return true; - - if (!addGlyphs(ti, glyphs, positions)) - return false; - - QVarLengthArray<unsigned int> chars(glyphs.size()); - - for (int i = 0; i < glyphs.size(); ++i) - chars[i] = glyphId(glyphs[i], ft->subPixelPositionForX(positions[i].x)); - - int i = 0; - while (i < glyphs.size() && !isValidCoordinate(positions[i])) - ++i; - - if (i >= glyphs.size()) - return true; - - QFixed xp = positions[i].x; - QFixed yp = positions[i].y; - QFixed offs = QFixed::fromReal(aliasedCoordinateDelta); - - XGlyphElt32 elt; - elt.glyphset = gset; - elt.chars = &chars[i]; - elt.nchars = 1; - elt.xOff = qRound(xp + offs); - elt.yOff = qRound(yp + offs); - - ++i; - - for (; i < glyphs.size(); ++i) { - if (!isValidCoordinate(positions[i])) - break; - - const QFixed sppx = ft->subPixelPositionForX(positions[i].x); - const QFixedPoint spp(sppx, 0); - QFontEngineFT::Glyph *g = set->getGlyph(glyphs[i], spp); - - if (g - && positions[i].x == xp + g->advance - && positions[i].y == yp - && elt.nchars < 253 // don't draw more than 253 characters as some X servers - // hang with it - ) { - elt.nchars++; - xp += g->advance; - } else { - xp = positions[i].x; - yp = positions[i].y; - - XRenderCompositeText32(xinfo.display(), PictOpOver, src, dst, - renderPictFormat(), 0, 0, 0, 0, - &elt, 1); - elt.chars = &chars[i]; - elt.nchars = 1; - elt.xOff = qRound(xp + offs); - elt.yOff = qRound(yp + offs); - } - } - - XRenderCompositeText32(xinfo.display(), PictOpOver, src, dst, - renderPictFormat(), 0, 0, 0, 0, &elt, 1); - - return true; -} - -GlyphSet QXRenderGlyphCache::glyphSet() -{ - if (gset == XNone) - gset = XRenderCreateGlyphSet(xinfo.display(), renderPictFormat()); - - Q_ASSERT(gset != XNone); - return gset; -} - -int QXRenderGlyphCache::glyphBufferSize(const QFontEngineFT::Glyph &glyph) const -{ - int pitch = 0; - - switch (glyphFormat()) { - case QFontEngine::Format_Mono: - pitch = ((glyph.width + 31) & ~31) >> 3; - break; - case QFontEngine::Format_A8: - pitch = (glyph.width + 3) & ~3; - break; - default: - pitch = glyph.width * 4; - break; - } - - return pitch * glyph.height; -} - -QImage::Format QXRenderGlyphCache::imageFormat() const -{ - switch (glyphFormat()) { - case QFontEngine::Format_None: - Q_UNREACHABLE(); - break; - case QFontEngine::Format_Mono: - return QImage::Format_Mono; - break; - case QFontEngine::Format_A8: - return QImage::Format_Alpha8; - break; - case QFontEngine::Format_A32: - case QFontEngine::Format_ARGB: - return QImage::Format_ARGB32_Premultiplied; - break; - } - - Q_UNREACHABLE(); -} - -const XRenderPictFormat *QXRenderGlyphCache::renderPictFormat() const -{ - switch (glyphFormat()) { - case QFontEngine::Format_None: - Q_UNREACHABLE(); - break; - case QFontEngine::Format_Mono: - return XRenderFindStandardFormat(xinfo.display(), PictStandardA1); - break; - case QFontEngine::Format_A8: - return XRenderFindStandardFormat(xinfo.display(), PictStandardA8); - break; - case QFontEngine::Format_A32: - case QFontEngine::Format_ARGB: - return XRenderFindStandardFormat(xinfo.display(), PictStandardARGB32); - break; - } - - Q_UNREACHABLE(); -} - -QFontEngine::GlyphFormat QXRenderGlyphCache::glyphFormatForDepth(QFontEngine *fontEngine, int depth) -{ - QFontEngine::GlyphFormat glyphFormat = fontEngine->glyphFormat; - - if (glyphFormat == QFontEngine::Format_None) { - switch (depth) { - case 32: - glyphFormat = QFontEngine::Format_ARGB; - break; - case 24: - glyphFormat = QFontEngine::Format_A32; - break; - case 1: - glyphFormat = QFontEngine::Format_Mono; - break; - default: - glyphFormat = QFontEngine::Format_A8; - break; - } - } - - return glyphFormat; -} - -Glyph QXRenderGlyphCache::glyphId(glyph_t glyph, QFixed subPixelPosition) -{ - return qHash(QFontEngineFT::GlyphAndSubPixelPosition(glyph, QFixedPoint(subPixelPosition, 0))); -} - -bool QXRenderGlyphCache::isValidCoordinate(const QFixedPoint &fp) -{ - enum { t_min = SHRT_MIN, t_max = SHRT_MAX }; - return (fp.x < t_min || fp.x > t_max || fp.y < t_min || fp.y > t_max) ? false : true; -} -#endif // QT_CONFIG(xrender) - -QT_END_NAMESPACE diff --git a/src/plugins/platforms/xcb/nativepainting/qpaintengine_x11_p.h b/src/plugins/platforms/xcb/nativepainting/qpaintengine_x11_p.h deleted file mode 100644 index bcbf84682c6..00000000000 --- a/src/plugins/platforms/xcb/nativepainting/qpaintengine_x11_p.h +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (C) 2018 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 - -#pragma once - -#include <QtGui/QPaintEngine> - -typedef unsigned long XID; -typedef XID Drawable; -typedef struct _XGC *GC; - -QT_BEGIN_NAMESPACE - -extern "C" { -Drawable qt_x11Handle(const QPaintDevice *pd); -GC qt_x11_get_pen_gc(QPainter *); -GC qt_x11_get_brush_gc(QPainter *); -} - -class QX11PaintEnginePrivate; -class QX11PaintEngine : public QPaintEngine -{ - Q_DECLARE_PRIVATE(QX11PaintEngine) -public: - QX11PaintEngine(); - ~QX11PaintEngine(); - - bool begin(QPaintDevice *pdev) override; - bool end() override; - - void updateState(const QPaintEngineState &state) override; - - void updatePen(const QPen &pen); - void updateBrush(const QBrush &brush, const QPointF &pt); - void updateRenderHints(QPainter::RenderHints hints); - void updateFont(const QFont &font); - void updateMatrix(const QTransform &matrix); - void updateClipRegion_dev(const QRegion ®ion, Qt::ClipOperation op); - - void drawLines(const QLine *lines, int lineCount) override; - void drawLines(const QLineF *lines, int lineCount) override; - - void drawRects(const QRect *rects, int rectCount) override; - void drawRects(const QRectF *rects, int rectCount) override; - - void drawPoints(const QPoint *points, int pointCount) override; - void drawPoints(const QPointF *points, int pointCount) override; - - void drawEllipse(const QRect &r) override; - void drawEllipse(const QRectF &r) override; - - virtual void drawPolygon(const QPointF *points, int pointCount, PolygonDrawMode mode) override; - inline void drawPolygon(const QPoint *points, int pointCount, PolygonDrawMode mode) override - { QPaintEngine::drawPolygon(points, pointCount, mode); } - - void drawPixmap(const QRectF &r, const QPixmap &pm, const QRectF &sr) override; - void drawTiledPixmap(const QRectF &r, const QPixmap &pixmap, const QPointF &s) override; - void drawPath(const QPainterPath &path) override; - void drawTextItem(const QPointF &p, const QTextItem &textItem) override; - void drawImage(const QRectF &r, const QImage &img, const QRectF &sr, - Qt::ImageConversionFlags flags = Qt::AutoColor) override; - - virtual Drawable handle() const; - inline Type type() const override { return QPaintEngine::X11; } - - QPainter::RenderHints supportedRenderHints() const; - -protected: - QX11PaintEngine(QX11PaintEnginePrivate &dptr); - -#if QT_CONFIG(fontconfig) - void drawFreetype(const QPointF &p, const QTextItemInt &ti); - bool drawCachedGlyphs(const QTransform &transform, const QTextItemInt &ti); -#endif // QT_CONFIG(fontconfig) - - friend class QPixmap; - friend class QFontEngineBox; - friend GC qt_x11_get_pen_gc(QPainter *); - friend GC qt_x11_get_brush_gc(QPainter *); - -private: - Q_DISABLE_COPY_MOVE(QX11PaintEngine) -}; - -QT_END_NAMESPACE diff --git a/src/plugins/platforms/xcb/nativepainting/qpixmap_x11.cpp b/src/plugins/platforms/xcb/nativepainting/qpixmap_x11.cpp deleted file mode 100644 index b47bd3f5dcc..00000000000 --- a/src/plugins/platforms/xcb/nativepainting/qpixmap_x11.cpp +++ /dev/null @@ -1,2087 +0,0 @@ -// Copyright (C) 2018 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 <QGuiApplication> - -#include <private/qdrawhelper_p.h> -#include <private/qimage_p.h> -#include <private/qimagepixmapcleanuphooks_p.h> - -#include "qxcbnativepainting.h" -#include "qpixmap_x11_p.h" -#include "qcolormap_x11_p.h" -#include "qpaintengine_x11_p.h" - -QT_BEGIN_NAMESPACE - -#if QT_POINTER_SIZE == 8 // 64-bit versions - -Q_ALWAYS_INLINE uint PREMUL(uint x) { - uint a = x >> 24; - quint64 t = (((quint64(x)) | ((quint64(x)) << 24)) & 0x00ff00ff00ff00ff) * a; - t = (t + ((t >> 8) & 0xff00ff00ff00ff) + 0x80008000800080) >> 8; - t &= 0x000000ff00ff00ff; - return (uint(t)) | (uint(t >> 24)) | (a << 24); -} - -#else // 32-bit versions - -Q_ALWAYS_INLINE uint PREMUL(uint x) { - uint a = x >> 24; - uint t = (x & 0xff00ff) * a; - t = (t + ((t >> 8) & 0xff00ff) + 0x800080) >> 8; - t &= 0xff00ff; - - x = ((x >> 8) & 0xff) * a; - x = (x + ((x >> 8) & 0xff) + 0x80); - x &= 0xff00; - x |= t | (a << 24); - return x; -} -#endif - - - -struct QXImageWrapper -{ - XImage *xi; -}; - -QPixmap qt_toX11Pixmap(const QImage &image) -{ - QPlatformPixmap *data = - new QX11PlatformPixmap(image.depth() == 1 - ? QPlatformPixmap::BitmapType - : QPlatformPixmap::PixmapType); - - data->fromImage(image, Qt::AutoColor); - - return QPixmap(data); -} - -QPixmap qt_toX11Pixmap(const QPixmap &pixmap) -{ - if (pixmap.isNull()) - return QPixmap(); - - if (QPixmap(pixmap).data_ptr()->classId() == QPlatformPixmap::X11Class) - return pixmap; - - return qt_toX11Pixmap(pixmap.toImage()); -} - -// For thread-safety: -// image->data does not belong to X11, so we must free it ourselves. - -inline static void qSafeXDestroyImage(XImage *x) -{ - if (x->data) { - free(x->data); - x->data = 0; - } - XDestroyImage(x); -} - -QBitmap QX11PlatformPixmap::mask_to_bitmap(int screen) const -{ - if (!x11_mask) - return QBitmap(); - qt_x11SetDefaultScreen(screen); - QBitmap bm(w, h); - QX11PlatformPixmap *that = qt_x11Pixmap(bm); - const QXcbX11Info *x = that->x11_info(); - GC gc = XCreateGC(x->display(), that->handle(), 0, 0); - XCopyArea(x->display(), x11_mask, that->handle(), gc, 0, 0, - that->width(), that->height(), 0, 0); - XFreeGC(x->display(), gc); - return bm; -} - -void QX11PlatformPixmap::bitmapFromImage(const QImage &image) -{ - w = image.width(); - h = image.height(); - d = 1; - is_null = (w <= 0 || h <= 0); - hd = createBitmapFromImage(image); -#if QT_CONFIG(xrender) - if (X11->use_xrender) - picture = XRenderCreatePicture(xinfo.display(), hd, - XRenderFindStandardFormat(xinfo.display(), PictStandardA1), 0, 0); -#endif // QT_CONFIG(xrender) -} - -bool QX11PlatformPixmap::canTakeQImageFromXImage(const QXImageWrapper &xiWrapper) const -{ - XImage *xi = xiWrapper.xi; - - if (xi->format != ZPixmap) - return false; - - // ARGB32_Premultiplied - if (picture && depth() == 32) - return true; - - // RGB32 - if (depth() == 24 && xi->bits_per_pixel == 32 && xi->red_mask == 0xff0000 - && xi->green_mask == 0xff00 && xi->blue_mask == 0xff) - return true; - - // RGB16 - if (depth() == 16 && xi->bits_per_pixel == 16 && xi->red_mask == 0xf800 - && xi->green_mask == 0x7e0 && xi->blue_mask == 0x1f) - return true; - - return false; -} - -QImage QX11PlatformPixmap::takeQImageFromXImage(const QXImageWrapper &xiWrapper) const -{ - XImage *xi = xiWrapper.xi; - - QImage::Format format = QImage::Format_ARGB32_Premultiplied; - if (depth() == 24) - format = QImage::Format_RGB32; - else if (depth() == 16) - format = QImage::Format_RGB16; - - QImage image((uchar *)xi->data, xi->width, xi->height, xi->bytes_per_line, format); - image.setDevicePixelRatio(devicePixelRatio()); - // take ownership - image.data_ptr()->own_data = true; - xi->data = 0; - - // we may have to swap the byte order - if ((QSysInfo::ByteOrder == QSysInfo::LittleEndian && xi->byte_order == MSBFirst) - || (QSysInfo::ByteOrder == QSysInfo::BigEndian && xi->byte_order == LSBFirst)) - { - for (int i=0; i < image.height(); i++) { - if (depth() == 16) { - ushort *p = (ushort*)image.scanLine(i); - ushort *end = p + image.width(); - while (p < end) { - *p = ((*p << 8) & 0xff00) | ((*p >> 8) & 0x00ff); - p++; - } - } else { - uint *p = (uint*)image.scanLine(i); - uint *end = p + image.width(); - while (p < end) { - *p = ((*p << 24) & 0xff000000) | ((*p << 8) & 0x00ff0000) - | ((*p >> 8) & 0x0000ff00) | ((*p >> 24) & 0x000000ff); - p++; - } - } - } - } - - // fix-up alpha channel - if (format == QImage::Format_RGB32) { - QRgb *p = (QRgb *)image.bits(); - for (int y = 0; y < xi->height; ++y) { - for (int x = 0; x < xi->width; ++x) - p[x] |= 0xff000000; - p += xi->bytes_per_line / 4; - } - } - - XDestroyImage(xi); - return image; -} - -XID QX11PlatformPixmap::bitmap_to_mask(const QBitmap &bitmap, int screen) -{ - if (bitmap.isNull()) - return 0; - QBitmap bm = bitmap; - qt_x11SetScreen(bm, screen); - - QX11PlatformPixmap *that = qt_x11Pixmap(bm); - const QXcbX11Info *x = that->x11_info(); - Pixmap mask = XCreatePixmap(x->display(), RootWindow(x->display(), screen), - that->width(), that->height(), 1); - GC gc = XCreateGC(x->display(), mask, 0, 0); - XCopyArea(x->display(), that->handle(), mask, gc, 0, 0, - that->width(), that->height(), 0, 0); - XFreeGC(x->display(), gc); - return mask; -} - -Drawable qt_x11Handle(const QPixmap &pixmap) -{ - if (pixmap.isNull()) - return XNone; - - if (pixmap.handle()->classId() != QPlatformPixmap::X11Class) - return XNone; - - return static_cast<const QX11PlatformPixmap *>(pixmap.handle())->handle(); -} - - -/***************************************************************************** - Internal functions - *****************************************************************************/ - -//extern const uchar *qt_get_bitflip_array(); // defined in qimage.cpp - -// Returns position of highest bit set or -1 if none -static int highest_bit(uint v) -{ - int i; - uint b = (uint)1 << 31; - for (i=31; ((b & v) == 0) && i>=0; i--) - b >>= 1; - return i; -} - -// Counts the number of bits set in 'v' -static uint n_bits(uint v) -{ - int i = 0; - while (v) { - v = v & (v - 1); - i++; - } - return i; -} - -static uint *red_scale_table = nullptr; -static uint *green_scale_table = nullptr; -static uint *blue_scale_table = nullptr; - -static void cleanup_scale_tables() -{ - delete[] red_scale_table; - delete[] green_scale_table; - delete[] blue_scale_table; -} - -/* - Could do smart bitshifting, but the "obvious" algorithm only works for - nBits >= 4. This is more robust. -*/ -static void build_scale_table(uint **table, uint nBits) -{ - if (nBits > 7) { - qWarning("build_scale_table: internal error, nBits = %i", nBits); - return; - } - if (!*table) { - static bool firstTable = true; - if (firstTable) { - qAddPostRoutine(cleanup_scale_tables); - firstTable = false; - } - *table = new uint[256]; - } - int maxVal = (1 << nBits) - 1; - int valShift = 8 - nBits; - int i; - for (i = 0 ; i < maxVal + 1 ; i++) - (*table)[i << valShift] = i*255/maxVal; -} - -static int defaultScreen = -1; - -int qt_x11SetDefaultScreen(int screen) -{ - int old = defaultScreen; - defaultScreen = screen; - return old; -} - -void qt_x11SetScreen(QPixmap &pixmap, int screen) -{ - if (pixmap.paintingActive()) { - qWarning("qt_x11SetScreen(): Cannot change screens during painting"); - return; - } - - if (pixmap.isNull()) - return; - - if (pixmap.handle()->classId() != QPlatformPixmap::X11Class) - return; - - if (screen < 0) - screen = QXcbX11Info::appScreen(); - - QX11PlatformPixmap *pm = static_cast<QX11PlatformPixmap *>(pixmap.handle()); - if (screen == pm->xinfo.screen()) - return; // nothing to do - - if (pixmap.isNull()) { - pm->xinfo = QXcbX11Info::fromScreen(screen); - return; - } - -#if 0 - qDebug("qt_x11SetScreen for %p from %d to %d. Size is %d/%d", pm, pm->xinfo.screen(), screen, pm->width(), pm->height()); -#endif - - qt_x11SetDefaultScreen(screen); - pixmap = qt_toX11Pixmap(pixmap.toImage()); -} - -/***************************************************************************** - QPixmap member functions - *****************************************************************************/ - -QBasicAtomicInt qt_pixmap_serial = Q_BASIC_ATOMIC_INITIALIZER(0); -int Q_GUI_EXPORT qt_x11_preferred_pixmap_depth = 0; - -QX11PlatformPixmap::QX11PlatformPixmap(PixelType pixelType) - : QPlatformPixmap(pixelType, X11Class), hd(0), - flags(Uninitialized), x11_mask(0), picture(0), mask_picture(0), hd2(0), - dpr(1.0), pengine(0) -{} - -QX11PlatformPixmap::~QX11PlatformPixmap() -{ - // Cleanup hooks have to be called before the handles are freed - if (is_cached) { - QImagePixmapCleanupHooks::executePlatformPixmapDestructionHooks(this); - is_cached = false; - } - - release(); -} - -QPlatformPixmap *QX11PlatformPixmap::createCompatiblePlatformPixmap() const -{ - QX11PlatformPixmap *p = new QX11PlatformPixmap(pixelType()); - p->setDevicePixelRatio(devicePixelRatio()); - return p; -} - -void QX11PlatformPixmap::resize(int width, int height) -{ - setSerialNumber(qt_pixmap_serial.fetchAndAddRelaxed(1)); - - w = width; - h = height; - is_null = (w <= 0 || h <= 0); - - if (defaultScreen >= 0 && defaultScreen != xinfo.screen()) { - xinfo = QXcbX11Info::fromScreen(defaultScreen); - } - - int dd = xinfo.depth(); - - if (qt_x11_preferred_pixmap_depth) - dd = qt_x11_preferred_pixmap_depth; - - bool make_null = w <= 0 || h <= 0; // create null pixmap - d = (pixelType() == BitmapType ? 1 : dd); - if (make_null || d == 0) { - w = 0; - h = 0; - is_null = true; - hd = 0; - picture = 0; - d = 0; - if (!make_null) - qWarning("QPixmap: Invalid pixmap parameters"); - return; - } - hd = XCreatePixmap(xinfo.display(), - RootWindow(xinfo.display(), xinfo.screen()), - w, h, d); -#if QT_CONFIG(xrender) - if (X11->use_xrender) { - XRenderPictFormat *format = d == 1 - ? XRenderFindStandardFormat(xinfo.display(), PictStandardA1) - : XRenderFindVisualFormat(xinfo.display(), (Visual *) xinfo.visual()); - picture = XRenderCreatePicture(xinfo.display(), hd, format, 0, 0); - } -#endif // QT_CONFIG(xrender) -} - -struct QX11AlphaDetector -{ - bool hasAlpha() const { - if (checked) - return has; - // Will implicitly also check format and return quickly for opaque types... - checked = true; - has = image->isNull() ? false : const_cast<QImage *>(image)->data_ptr()->checkForAlphaPixels(); - return has; - } - - bool hasXRenderAndAlpha() const { - if (!X11->use_xrender) - return false; - return hasAlpha(); - } - - QX11AlphaDetector(const QImage *i, Qt::ImageConversionFlags flags) - : image(i), checked(false), has(false) - { - if (flags & Qt::NoOpaqueDetection) { - checked = true; - has = image->hasAlphaChannel(); - } - } - - const QImage *image; - mutable bool checked; - mutable bool has; -}; - -void QX11PlatformPixmap::fromImage(const QImage &img, Qt::ImageConversionFlags flags) -{ - setSerialNumber(qt_pixmap_serial.fetchAndAddRelaxed(1)); - - w = img.width(); - h = img.height(); - d = img.depth(); - is_null = (w <= 0 || h <= 0); - setDevicePixelRatio(img.devicePixelRatio()); - - if (is_null) { - w = h = 0; - return; - } - - if (defaultScreen >= 0 && defaultScreen != xinfo.screen()) { - xinfo = QXcbX11Info::fromScreen(defaultScreen); - } - - if (pixelType() == BitmapType) { - bitmapFromImage(img); - return; - } - - if (uint(w) >= 32768 || uint(h) >= 32768) { - w = h = 0; - is_null = true; - return; - } - - QX11AlphaDetector alphaCheck(&img, flags); - int dd = alphaCheck.hasXRenderAndAlpha() ? 32 : xinfo.depth(); - - if (qt_x11_preferred_pixmap_depth) - dd = qt_x11_preferred_pixmap_depth; - - QImage image = img; - - // must be monochrome - if (dd == 1 || (flags & Qt::ColorMode_Mask) == Qt::MonoOnly) { - if (d != 1) { - // dither - image = image.convertToFormat(QImage::Format_MonoLSB, flags); - d = 1; - } - } else { // can be both - bool conv8 = false; - if (d > 8 && dd <= 8) { // convert to 8 bit - if ((flags & Qt::DitherMode_Mask) == Qt::AutoDither) - flags = (flags & ~Qt::DitherMode_Mask) - | Qt::PreferDither; - conv8 = true; - } else if ((flags & Qt::ColorMode_Mask) == Qt::ColorOnly) { - conv8 = (d == 1); // native depth wanted - } else if (d == 1) { - if (image.colorCount() == 2) { - QRgb c0 = image.color(0); // Auto: convert to best - QRgb c1 = image.color(1); - conv8 = qMin(c0,c1) != qRgb(0,0,0) || qMax(c0,c1) != qRgb(255,255,255); - } else { - // eg. 1-color monochrome images (they do exist). - conv8 = true; - } - } - if (conv8) { - image = image.convertToFormat(QImage::Format_Indexed8, flags); - d = 8; - } - } - - if (d == 1 || image.format() > QImage::Format_ARGB32_Premultiplied) { - QImage::Format fmt = QImage::Format_RGB32; - if (alphaCheck.hasXRenderAndAlpha() && d > 1) - fmt = QImage::Format_ARGB32_Premultiplied; - image = image.convertToFormat(fmt, flags); - fromImage(image, Qt::AutoColor); - return; - } - - Display *dpy = xinfo.display(); - Visual *visual = (Visual *)xinfo.visual(); - XImage *xi = nullptr; - bool trucol = (visual->c_class >= TrueColor); - size_t nbytes = image.sizeInBytes(); - uchar *newbits= nullptr; - -#if QT_CONFIG(xrender) - if (alphaCheck.hasXRenderAndAlpha()) { - const QImage &cimage = image; - - d = 32; - - if (QXcbX11Info::appDepth() != d) { - xinfo.setDepth(d); - } - - hd = XCreatePixmap(dpy, RootWindow(dpy, xinfo.screen()), w, h, d); - picture = XRenderCreatePicture(dpy, hd, - XRenderFindStandardFormat(dpy, PictStandardARGB32), 0, 0); - - xi = XCreateImage(dpy, visual, d, ZPixmap, 0, 0, w, h, 32, 0); - Q_CHECK_PTR(xi); - newbits = (uchar *)malloc(xi->bytes_per_line*h); - Q_CHECK_PTR(newbits); - xi->data = (char *)newbits; - - switch (cimage.format()) { - case QImage::Format_Indexed8: { - QList<QRgb> colorTable = cimage.colorTable(); - uint *xidata = (uint *)xi->data; - for (int y = 0; y < h; ++y) { - const uchar *p = cimage.scanLine(y); - for (int x = 0; x < w; ++x) { - const QRgb rgb = colorTable[p[x]]; - const int a = qAlpha(rgb); - if (a == 0xff) - *xidata = rgb; - else - // RENDER expects premultiplied alpha - *xidata = qRgba(qt_div_255(qRed(rgb) * a), - qt_div_255(qGreen(rgb) * a), - qt_div_255(qBlue(rgb) * a), - a); - ++xidata; - } - } - } - break; - case QImage::Format_RGB32: { - uint *xidata = (uint *)xi->data; - for (int y = 0; y < h; ++y) { - const QRgb *p = (const QRgb *) cimage.scanLine(y); - for (int x = 0; x < w; ++x) - *xidata++ = p[x] | 0xff000000; - } - } - break; - case QImage::Format_ARGB32: { - uint *xidata = (uint *)xi->data; - for (int y = 0; y < h; ++y) { - const QRgb *p = (const QRgb *) cimage.scanLine(y); - for (int x = 0; x < w; ++x) { - const QRgb rgb = p[x]; - const int a = qAlpha(rgb); - if (a == 0xff) - *xidata = rgb; - else - // RENDER expects premultiplied alpha - *xidata = qRgba(qt_div_255(qRed(rgb) * a), - qt_div_255(qGreen(rgb) * a), - qt_div_255(qBlue(rgb) * a), - a); - ++xidata; - } - } - - } - break; - case QImage::Format_ARGB32_Premultiplied: { - uint *xidata = (uint *)xi->data; - for (int y = 0; y < h; ++y) { - const QRgb *p = (const QRgb *) cimage.scanLine(y); - memcpy(xidata, p, w*sizeof(QRgb)); - xidata += w; - } - } - break; - default: - Q_ASSERT(false); - } - - if ((xi->byte_order == MSBFirst) != (QSysInfo::ByteOrder == QSysInfo::BigEndian)) { - uint *xidata = (uint *)xi->data; - uint *xiend = xidata + w*h; - while (xidata < xiend) { - *xidata = (*xidata >> 24) - | ((*xidata >> 8) & 0xff00) - | ((*xidata << 8) & 0xff0000) - | (*xidata << 24); - ++xidata; - } - } - - GC gc = XCreateGC(dpy, hd, 0, 0); - XPutImage(dpy, hd, gc, xi, 0, 0, 0, 0, w, h); - XFreeGC(dpy, gc); - - qSafeXDestroyImage(xi); - - return; - } -#endif // QT_CONFIG(xrender) - - if (trucol) { // truecolor display - if (image.format() == QImage::Format_ARGB32_Premultiplied) - image = image.convertToFormat(QImage::Format_ARGB32); - - const QImage &cimage = image; - QRgb pix[256]; // pixel translation table - const bool d8 = (d == 8); - const uint red_mask = (uint)visual->red_mask; - const uint green_mask = (uint)visual->green_mask; - const uint blue_mask = (uint)visual->blue_mask; - const int red_shift = highest_bit(red_mask) - 7; - const int green_shift = highest_bit(green_mask) - 7; - const int blue_shift = highest_bit(blue_mask) - 7; - const uint rbits = highest_bit(red_mask) - lowest_bit(red_mask) + 1; - const uint gbits = highest_bit(green_mask) - lowest_bit(green_mask) + 1; - const uint bbits = highest_bit(blue_mask) - lowest_bit(blue_mask) + 1; - - if (d8) { // setup pixel translation - QList<QRgb> ctable = cimage.colorTable(); - for (int i=0; i < cimage.colorCount(); i++) { - int r = qRed (ctable[i]); - int g = qGreen(ctable[i]); - int b = qBlue (ctable[i]); - r = red_shift > 0 ? r << red_shift : r >> -red_shift; - g = green_shift > 0 ? g << green_shift : g >> -green_shift; - b = blue_shift > 0 ? b << blue_shift : b >> -blue_shift; - pix[i] = (b & blue_mask) | (g & green_mask) | (r & red_mask) - | ~(blue_mask | green_mask | red_mask); - } - } - - xi = XCreateImage(dpy, visual, dd, ZPixmap, 0, 0, w, h, 32, 0); - Q_CHECK_PTR(xi); - newbits = (uchar *)malloc(xi->bytes_per_line*h); - Q_CHECK_PTR(newbits); - if (!newbits) // no memory - return; - int bppc = xi->bits_per_pixel; - - bool contig_bits = n_bits(red_mask) == rbits && - n_bits(green_mask) == gbits && - n_bits(blue_mask) == bbits; - bool dither_tc = - // Want it? - (flags & Qt::Dither_Mask) != Qt::ThresholdDither && - (flags & Qt::DitherMode_Mask) != Qt::AvoidDither && - // Need it? - bppc < 24 && !d8 && - // Can do it? (Contiguous bits?) - contig_bits; - - static bool init=false; - static int D[16][16]; - if (dither_tc && !init) { - // I also contributed this code to XV - WWA. - /* - The dither matrix, D, is obtained with this formula: - - D2 = [0 2] - [3 1] - - - D2*n = [4*Dn 4*Dn+2*Un] - [4*Dn+3*Un 4*Dn+1*Un] - */ - int n,i,j; - init=1; - - /* Set D2 */ - D[0][0]=0; - D[1][0]=2; - D[0][1]=3; - D[1][1]=1; - - /* Expand using recursive definition given above */ - for (n=2; n<16; n*=2) { - for (i=0; i<n; i++) { - for (j=0; j<n; j++) { - D[i][j]*=4; - D[i+n][j]=D[i][j]+2; - D[i][j+n]=D[i][j]+3; - D[i+n][j+n]=D[i][j]+1; - } - } - } - init=true; - } - - enum { BPP8, - BPP16_565, BPP16_555, - BPP16_MSB, BPP16_LSB, - BPP24_888, - BPP24_MSB, BPP24_LSB, - BPP32_8888, - BPP32_MSB, BPP32_LSB - } mode = BPP8; - - bool same_msb_lsb = (xi->byte_order == MSBFirst) == (QSysInfo::ByteOrder == QSysInfo::BigEndian); - - if (bppc == 8) // 8 bit - mode = BPP8; - else if (bppc == 16) { // 16 bit MSB/LSB - if (red_shift == 8 && green_shift == 3 && blue_shift == -3 && !d8 && same_msb_lsb) - mode = BPP16_565; - else if (red_shift == 7 && green_shift == 2 && blue_shift == -3 && !d8 && same_msb_lsb) - mode = BPP16_555; - else - mode = (xi->byte_order == LSBFirst) ? BPP16_LSB : BPP16_MSB; - } else if (bppc == 24) { // 24 bit MSB/LSB - if (red_shift == 16 && green_shift == 8 && blue_shift == 0 && !d8 && same_msb_lsb) - mode = BPP24_888; - else - mode = (xi->byte_order == LSBFirst) ? BPP24_LSB : BPP24_MSB; - } else if (bppc == 32) { // 32 bit MSB/LSB - if (red_shift == 16 && green_shift == 8 && blue_shift == 0 && !d8 && same_msb_lsb) - mode = BPP32_8888; - else - mode = (xi->byte_order == LSBFirst) ? BPP32_LSB : BPP32_MSB; - } else - qFatal("Logic error 3"); - -#define GET_PIXEL \ - uint pixel; \ - if (d8) pixel = pix[*src++]; \ - else { \ - int r = qRed (*p); \ - int g = qGreen(*p); \ - int b = qBlue (*p++); \ - r = red_shift > 0 \ - ? r << red_shift : r >> -red_shift; \ - g = green_shift > 0 \ - ? g << green_shift : g >> -green_shift; \ - b = blue_shift > 0 \ - ? b << blue_shift : b >> -blue_shift; \ - pixel = (r & red_mask)|(g & green_mask) | (b & blue_mask) \ - | ~(blue_mask | green_mask | red_mask); \ - } - -#define GET_PIXEL_DITHER_TC \ - int r = qRed (*p); \ - int g = qGreen(*p); \ - int b = qBlue (*p++); \ - const int thres = D[x%16][y%16]; \ - if (r <= (255-(1<<(8-rbits))) && ((r<<rbits) & 255) \ - > thres) \ - r += (1<<(8-rbits)); \ - if (g <= (255-(1<<(8-gbits))) && ((g<<gbits) & 255) \ - > thres) \ - g += (1<<(8-gbits)); \ - if (b <= (255-(1<<(8-bbits))) && ((b<<bbits) & 255) \ - > thres) \ - b += (1<<(8-bbits)); \ - r = red_shift > 0 \ - ? r << red_shift : r >> -red_shift; \ - g = green_shift > 0 \ - ? g << green_shift : g >> -green_shift; \ - b = blue_shift > 0 \ - ? b << blue_shift : b >> -blue_shift; \ - uint pixel = (r & red_mask)|(g & green_mask) | (b & blue_mask); - -// again, optimized case -// can't be optimized that much :( -#define GET_PIXEL_DITHER_TC_OPT(red_shift,green_shift,blue_shift,red_mask,green_mask,blue_mask, \ - rbits,gbits,bbits) \ - const int thres = D[x%16][y%16]; \ - int r = qRed (*p); \ - if (r <= (255-(1<<(8-rbits))) && ((r<<rbits) & 255) \ - > thres) \ - r += (1<<(8-rbits)); \ - int g = qGreen(*p); \ - if (g <= (255-(1<<(8-gbits))) && ((g<<gbits) & 255) \ - > thres) \ - g += (1<<(8-gbits)); \ - int b = qBlue (*p++); \ - if (b <= (255-(1<<(8-bbits))) && ((b<<bbits) & 255) \ - > thres) \ - b += (1<<(8-bbits)); \ - uint pixel = ((r red_shift) & red_mask) \ - | ((g green_shift) & green_mask) \ - | ((b blue_shift) & blue_mask); - -#define CYCLE(body) \ - for (int y=0; y<h; y++) { \ - const uchar* src = cimage.scanLine(y); \ - uchar* dst = newbits + xi->bytes_per_line*y; \ - const QRgb* p = (const QRgb *)src; \ - body \ - } - - if (dither_tc) { - switch (mode) { - case BPP16_565: - CYCLE( - quint16* dst16 = (quint16*)dst; - for (int x=0; x<w; x++) { - GET_PIXEL_DITHER_TC_OPT(<<8,<<3,>>3,0xf800,0x7e0,0x1f,5,6,5) - *dst16++ = pixel; - } - ) - break; - case BPP16_555: - CYCLE( - quint16* dst16 = (quint16*)dst; - for (int x=0; x<w; x++) { - GET_PIXEL_DITHER_TC_OPT(<<7,<<2,>>3,0x7c00,0x3e0,0x1f,5,5,5) - *dst16++ = pixel; - } - ) - break; - case BPP16_MSB: // 16 bit MSB - CYCLE( - for (int x=0; x<w; x++) { - GET_PIXEL_DITHER_TC - *dst++ = (pixel >> 8); - *dst++ = pixel; - } - ) - break; - case BPP16_LSB: // 16 bit LSB - CYCLE( - for (int x=0; x<w; x++) { - GET_PIXEL_DITHER_TC - *dst++ = pixel; - *dst++ = pixel >> 8; - } - ) - break; - default: - qFatal("Logic error"); - } - } else { - switch (mode) { - case BPP8: // 8 bit - CYCLE( - Q_UNUSED(p); - for (int x=0; x<w; x++) - *dst++ = pix[*src++]; - ) - break; - case BPP16_565: - CYCLE( - quint16* dst16 = (quint16*)dst; - for (int x = 0; x < w; x++) { - *dst16++ = ((*p >> 8) & 0xf800) - | ((*p >> 5) & 0x7e0) - | ((*p >> 3) & 0x1f); - ++p; - } - ) - break; - case BPP16_555: - CYCLE( - quint16* dst16 = (quint16*)dst; - for (int x=0; x<w; x++) { - *dst16++ = ((*p >> 9) & 0x7c00) - | ((*p >> 6) & 0x3e0) - | ((*p >> 3) & 0x1f); - ++p; - } - ) - break; - case BPP16_MSB: // 16 bit MSB - CYCLE( - for (int x=0; x<w; x++) { - GET_PIXEL - *dst++ = (pixel >> 8); - *dst++ = pixel; - } - ) - break; - case BPP16_LSB: // 16 bit LSB - CYCLE( - for (int x=0; x<w; x++) { - GET_PIXEL - *dst++ = pixel; - *dst++ = pixel >> 8; - } - ) - break; - case BPP24_888: - CYCLE( - if (QSysInfo::ByteOrder == QSysInfo::BigEndian) { - for (int x=0; x<w; x++) { - *dst++ = qRed (*p); - *dst++ = qGreen(*p); - *dst++ = qBlue (*p++); - } - } else { - for (int x=0; x<w; x++) { - *dst++ = qBlue (*p); - *dst++ = qGreen(*p); - *dst++ = qRed (*p++); - } - } - ) - break; - case BPP24_MSB: // 24 bit MSB - CYCLE( - for (int x=0; x<w; x++) { - GET_PIXEL - *dst++ = pixel >> 16; - *dst++ = pixel >> 8; - *dst++ = pixel; - } - ) - break; - case BPP24_LSB: // 24 bit LSB - CYCLE( - for (int x=0; x<w; x++) { - GET_PIXEL - *dst++ = pixel; - *dst++ = pixel >> 8; - *dst++ = pixel >> 16; - } - ) - break; - case BPP32_8888: - CYCLE( - memcpy(dst, p, w * 4); - ) - break; - case BPP32_MSB: // 32 bit MSB - CYCLE( - for (int x=0; x<w; x++) { - GET_PIXEL - *dst++ = pixel >> 24; - *dst++ = pixel >> 16; - *dst++ = pixel >> 8; - *dst++ = pixel; - } - ) - break; - case BPP32_LSB: // 32 bit LSB - CYCLE( - for (int x=0; x<w; x++) { - GET_PIXEL - *dst++ = pixel; - *dst++ = pixel >> 8; - *dst++ = pixel >> 16; - *dst++ = pixel >> 24; - } - ) - break; - default: - qFatal("Logic error 2"); - } - } - xi->data = (char *)newbits; - } - - if (d == 8 && !trucol) { // 8 bit pixmap - int pop[256]; // pixel popularity - - if (image.colorCount() == 0) - image.setColorCount(1); - - const QImage &cimage = image; - memset(pop, 0, sizeof(int)*256); // reset popularity array - for (int i = 0; i < h; i++) { // for each scanline... - const uchar* p = cimage.scanLine(i); - const uchar *end = p + w; - while (p < end) // compute popularity - pop[*p++]++; - } - - newbits = (uchar *)malloc(nbytes); // copy image into newbits - Q_CHECK_PTR(newbits); - if (!newbits) // no memory - return; - uchar* p = newbits; - memcpy(p, cimage.bits(), nbytes); // copy image data into newbits - - /* - * The code below picks the most important colors. It is based on the - * diversity algorithm, implemented in XV 3.10. XV is (C) by John Bradley. - */ - - struct PIX { // pixel sort element - uchar r,g,b,n; // color + pad - int use; // popularity - int index; // index in colormap - int mindist; - }; - int ncols = 0; - for (int i=0; i< cimage.colorCount(); i++) { // compute number of colors - if (pop[i] > 0) - ncols++; - } - for (int i = cimage.colorCount(); i < 256; i++) // ignore out-of-range pixels - pop[i] = 0; - - // works since we make sure above to have at least - // one color in the image - if (ncols == 0) - ncols = 1; - - PIX pixarr[256]; // pixel array - PIX pixarr_sorted[256]; // pixel array (sorted) - memset(pixarr, 0, ncols*sizeof(PIX)); - PIX *px = &pixarr[0]; - int maxpop = 0; - int maxpix = 0; - uint j = 0; - QList<QRgb> ctable = cimage.colorTable(); - for (int i = 0; i < 256; i++) { // init pixel array - if (pop[i] > 0) { - px->r = qRed (ctable[i]); - px->g = qGreen(ctable[i]); - px->b = qBlue (ctable[i]); - px->n = 0; - px->use = pop[i]; - if (pop[i] > maxpop) { // select most popular entry - maxpop = pop[i]; - maxpix = j; - } - px->index = i; - px->mindist = 1000000; - px++; - j++; - } - } - pixarr_sorted[0] = pixarr[maxpix]; - pixarr[maxpix].use = 0; - - for (int i = 1; i < ncols; i++) { // sort pixels - int minpix = -1, mindist = -1; - px = &pixarr_sorted[i-1]; - int r = px->r; - int g = px->g; - int b = px->b; - int dist; - if ((i & 1) || i<10) { // sort on max distance - for (int j=0; j<ncols; j++) { - px = &pixarr[j]; - if (px->use) { - dist = (px->r - r)*(px->r - r) + - (px->g - g)*(px->g - g) + - (px->b - b)*(px->b - b); - if (px->mindist > dist) - px->mindist = dist; - if (px->mindist > mindist) { - mindist = px->mindist; - minpix = j; - } - } - } - } else { // sort on max popularity - for (int j=0; j<ncols; j++) { - px = &pixarr[j]; - if (px->use) { - dist = (px->r - r)*(px->r - r) + - (px->g - g)*(px->g - g) + - (px->b - b)*(px->b - b); - if (px->mindist > dist) - px->mindist = dist; - if (px->use > mindist) { - mindist = px->use; - minpix = j; - } - } - } - } - pixarr_sorted[i] = pixarr[minpix]; - pixarr[minpix].use = 0; - } - - QXcbColormap cmap = QXcbColormap::instance(xinfo.screen()); - uint pix[256]; // pixel translation table - px = &pixarr_sorted[0]; - for (int i = 0; i < ncols; i++) { // allocate colors - QColor c(px->r, px->g, px->b); - pix[px->index] = cmap.pixel(c); - px++; - } - - p = newbits; - for (size_t i = 0; i < nbytes; i++) { // translate pixels - *p = pix[*p]; - p++; - } - } - - if (!xi) { // X image not created - xi = XCreateImage(dpy, visual, dd, ZPixmap, 0, 0, w, h, 32, 0); - if (xi->bits_per_pixel == 16) { // convert 8 bpp ==> 16 bpp - ushort *p2; - int p2inc = xi->bytes_per_line/sizeof(ushort); - ushort *newerbits = (ushort *)malloc(xi->bytes_per_line * h); - Q_CHECK_PTR(newerbits); - if (!newerbits) // no memory - return; - uchar* p = newbits; - for (int y = 0; y < h; y++) { // OOPS: Do right byte order!! - p2 = newerbits + p2inc*y; - for (int x = 0; x < w; x++) - *p2++ = *p++; - } - free(newbits); - newbits = (uchar *)newerbits; - } else if (xi->bits_per_pixel != 8) { - qWarning("QPixmap::fromImage: Display not supported " - "(bpp=%d)", xi->bits_per_pixel); - } - xi->data = (char *)newbits; - } - - hd = XCreatePixmap(dpy, - RootWindow(dpy, xinfo.screen()), - w, h, dd); - - GC gc = XCreateGC(dpy, hd, 0, 0); - XPutImage(dpy, hd, gc, xi, 0, 0, 0, 0, w, h); - XFreeGC(dpy, gc); - - qSafeXDestroyImage(xi); - d = dd; - -#if QT_CONFIG(xrender) - if (X11->use_xrender) { - XRenderPictFormat *format = d == 1 - ? XRenderFindStandardFormat(dpy, PictStandardA1) - : XRenderFindVisualFormat(dpy, (Visual *)xinfo.visual()); - picture = XRenderCreatePicture(dpy, hd, format, 0, 0); - } -#endif - - if (alphaCheck.hasAlpha()) { - QBitmap m = QBitmap::fromImage(image.createAlphaMask(flags)); - setMask(m); - } -} - -void QX11PlatformPixmap::copy(const QPlatformPixmap *data, const QRect &rect) -{ - if (data->pixelType() == BitmapType) { - fromImage(data->toImage().copy(rect), Qt::AutoColor); - return; - } - - const QX11PlatformPixmap *x11Data = static_cast<const QX11PlatformPixmap*>(data); - - setSerialNumber(qt_pixmap_serial.fetchAndAddRelaxed(1)); - - flags &= ~Uninitialized; - xinfo = x11Data->xinfo; - d = x11Data->d; - w = rect.width(); - h = rect.height(); - is_null = (w <= 0 || h <= 0); - hd = XCreatePixmap(xinfo.display(), - RootWindow(xinfo.display(), x11Data->xinfo.screen()), - w, h, d); -#if QT_CONFIG(xrender) - if (X11->use_xrender) { - XRenderPictFormat *format = d == 32 - ? XRenderFindStandardFormat(xinfo.display(), PictStandardARGB32) - : XRenderFindVisualFormat(xinfo.display(), (Visual *)xinfo.visual()); - picture = XRenderCreatePicture(xinfo.display(), hd, format, 0, 0); - } -#endif // QT_CONFIG(xrender) - if (x11Data->x11_mask) { - x11_mask = XCreatePixmap(xinfo.display(), hd, w, h, 1); -#if QT_CONFIG(xrender) - if (X11->use_xrender) { - mask_picture = XRenderCreatePicture(xinfo.display(), x11_mask, - XRenderFindStandardFormat(xinfo.display(), PictStandardA1), 0, 0); - XRenderPictureAttributes attrs; - attrs.alpha_map = x11Data->mask_picture; - XRenderChangePicture(xinfo.display(), x11Data->picture, CPAlphaMap, &attrs); - } -#endif - } - -#if QT_CONFIG(xrender) - if (x11Data->picture && x11Data->d == 32) { - XRenderComposite(xinfo.display(), PictOpSrc, - x11Data->picture, 0, picture, - rect.x(), rect.y(), 0, 0, 0, 0, w, h); - } else -#endif - { - GC gc = XCreateGC(xinfo.display(), hd, 0, 0); - XCopyArea(xinfo.display(), x11Data->hd, hd, gc, - rect.x(), rect.y(), w, h, 0, 0); - if (x11Data->x11_mask) { - GC monogc = XCreateGC(xinfo.display(), x11_mask, 0, 0); - XCopyArea(xinfo.display(), x11Data->x11_mask, x11_mask, monogc, - rect.x(), rect.y(), w, h, 0, 0); - XFreeGC(xinfo.display(), monogc); - } - XFreeGC(xinfo.display(), gc); - } -} - -bool QX11PlatformPixmap::scroll(int dx, int dy, const QRect &rect) -{ - GC gc = XCreateGC(xinfo.display(), hd, 0, 0); - XCopyArea(xinfo.display(), hd, hd, gc, - rect.left(), rect.top(), rect.width(), rect.height(), - rect.left() + dx, rect.top() + dy); - XFreeGC(xinfo.display(), gc); - return true; -} - -int QX11PlatformPixmap::metric(QPaintDevice::PaintDeviceMetric metric) const -{ - switch (metric) { - case QPaintDevice::PdmDevicePixelRatio: - return devicePixelRatio(); - break; - case QPaintDevice::PdmDevicePixelRatioScaled: - return devicePixelRatio() * QPaintDevice::devicePixelRatioFScale(); - break; - case QPaintDevice::PdmWidth: - return w; - case QPaintDevice::PdmHeight: - return h; - case QPaintDevice::PdmNumColors: - return 1 << d; - case QPaintDevice::PdmDepth: - return d; - case QPaintDevice::PdmWidthMM: { - const int screen = xinfo.screen(); - const int mm = DisplayWidthMM(xinfo.display(), screen) * w - / DisplayWidth(xinfo.display(), screen); - return mm; - } - case QPaintDevice::PdmHeightMM: { - const int screen = xinfo.screen(); - const int mm = (DisplayHeightMM(xinfo.display(), screen) * h) - / DisplayHeight(xinfo.display(), screen); - return mm; - } - case QPaintDevice::PdmDpiX: - case QPaintDevice::PdmPhysicalDpiX: - return QXcbX11Info::appDpiX(xinfo.screen()); - case QPaintDevice::PdmDpiY: - case QPaintDevice::PdmPhysicalDpiY: - return QXcbX11Info::appDpiY(xinfo.screen()); - default: - qWarning("QX11PlatformPixmap::metric(): Invalid metric"); - return 0; - } -} - -void QX11PlatformPixmap::fill(const QColor &fillColor) -{ - if (fillColor.alpha() != 255) { -#if QT_CONFIG(xrender) - if (X11->use_xrender) { - if (!picture || d != 32) - convertToARGB32(/*preserveContents = */false); - - ::Picture src = X11->getSolidFill(xinfo.screen(), fillColor); - XRenderComposite(xinfo.display(), PictOpSrc, src, 0, picture, - 0, 0, width(), height(), - 0, 0, width(), height()); - } else -#endif - { - QImage im(width(), height(), QImage::Format_ARGB32_Premultiplied); - im.fill(PREMUL(fillColor.rgba())); - release(); - fromImage(im, Qt::AutoColor | Qt::OrderedAlphaDither); - } - return; - } - - GC gc = XCreateGC(xinfo.display(), hd, 0, 0); - if (depth() == 1) { - XSetForeground(xinfo.display(), gc, qGray(fillColor.rgb()) > 127 ? 0 : 1); - } else if (X11->use_xrender && d >= 24) { - XSetForeground(xinfo.display(), gc, fillColor.rgba()); - } else { - XSetForeground(xinfo.display(), gc, - QXcbColormap::instance(xinfo.screen()).pixel(fillColor)); - } - XFillRectangle(xinfo.display(), hd, gc, 0, 0, width(), height()); - XFreeGC(xinfo.display(), gc); -} - -QBitmap QX11PlatformPixmap::mask() const -{ - QBitmap mask; -#if QT_CONFIG(xrender) - if (picture && d == 32) { - // #### slow - there must be a better way.. - mask = QBitmap::fromImage(toImage().createAlphaMask()); - } else -#endif - if (d == 1) { - QX11PlatformPixmap *that = const_cast<QX11PlatformPixmap*>(this); - mask = QBitmap::fromPixmap(QPixmap(that)); - } else { - mask = mask_to_bitmap(xinfo.screen()); - } - return mask; -} - -void QX11PlatformPixmap::setMask(const QBitmap &newmask) -{ - if (newmask.isNull()) { // clear mask -#if QT_CONFIG(xrender) - if (picture && d == 32) { - QX11PlatformPixmap newData(pixelType()); - newData.resize(w, h); - newData.fill(Qt::black); - XRenderComposite(xinfo.display(), PictOpOver, - picture, 0, newData.picture, - 0, 0, 0, 0, 0, 0, w, h); - release(); - *this = newData; - // the new QX11PlatformPixmap object isn't referenced yet, so - // ref it - ref.ref(); - - // the below is to make sure the QX11PlatformPixmap destructor - // doesn't delete our newly created render picture - newData.hd = 0; - newData.x11_mask = 0; - newData.picture = 0; - newData.mask_picture = 0; - newData.hd2 = 0; - } else -#endif - if (x11_mask) { -#if QT_CONFIG(xrender) - if (picture) { - XRenderPictureAttributes attrs; - attrs.alpha_map = 0; - XRenderChangePicture(xinfo.display(), picture, CPAlphaMap, - &attrs); - } - if (mask_picture) - XRenderFreePicture(xinfo.display(), mask_picture); - mask_picture = 0; -#endif - XFreePixmap(xinfo.display(), x11_mask); - x11_mask = 0; - } - return; - } - -#if QT_CONFIG(xrender) - if (picture && d == 32) { - XRenderComposite(xinfo.display(), PictOpSrc, - picture, qt_x11Pixmap(newmask)->x11PictureHandle(), - picture, 0, 0, 0, 0, 0, 0, w, h); - } else -#endif - if (depth() == 1) { - XGCValues vals; - vals.function = GXand; - GC gc = XCreateGC(xinfo.display(), hd, GCFunction, &vals); - XCopyArea(xinfo.display(), qt_x11Pixmap(newmask)->handle(), hd, gc, 0, 0, - width(), height(), 0, 0); - XFreeGC(xinfo.display(), gc); - } else { - // ##### should or the masks together - if (x11_mask) { - XFreePixmap(xinfo.display(), x11_mask); -#if QT_CONFIG(xrender) - if (mask_picture) - XRenderFreePicture(xinfo.display(), mask_picture); -#endif - } - x11_mask = QX11PlatformPixmap::bitmap_to_mask(newmask, xinfo.screen()); -#if QT_CONFIG(xrender) - if (picture) { - mask_picture = XRenderCreatePicture(xinfo.display(), x11_mask, - XRenderFindStandardFormat(xinfo.display(), PictStandardA1), 0, 0); - XRenderPictureAttributes attrs; - attrs.alpha_map = mask_picture; - XRenderChangePicture(xinfo.display(), picture, CPAlphaMap, &attrs); - } -#endif - } -} - -bool QX11PlatformPixmap::hasAlphaChannel() const -{ - if (picture && d == 32) - return true; - - if (x11_mask && d == 1) - return true; - - return false; -} - -QPixmap QX11PlatformPixmap::transformed(const QTransform &transform, Qt::TransformationMode mode) const -{ - if (mode == Qt::SmoothTransformation || transform.type() >= QTransform::TxProject) { - QImage image = toImage(); - return QPixmap::fromImage(image.transformed(transform, mode)); - } - - uint w = 0; - uint h = 0; // size of target pixmap - uint ws, hs; // size of source pixmap - uchar *dptr; // data in target pixmap - uint dbpl, dbytes; // bytes per line/bytes total - uchar *sptr; // data in original pixmap - int sbpl; // bytes per line in original - int bpp; // bits per pixel - bool depth1 = depth() == 1; - Display *dpy = xinfo.display(); - - ws = width(); - hs = height(); - - QTransform mat(transform.m11(), transform.m12(), transform.m13(), - transform.m21(), transform.m22(), transform.m23(), - 0., 0., 1); - bool complex_xform = false; - - if (mat.type() <= QTransform::TxScale) { - h = qRound(qAbs(mat.m22()) * hs); - w = qRound(qAbs(mat.m11()) * ws); - } else { // rotation or shearing - QPolygonF a(QRectF(0, 0, ws, hs)); - a = mat.map(a); - QRect r = a.boundingRect().toAlignedRect(); - w = r.width(); - h = r.height(); - complex_xform = true; - } - mat = QPixmap::trueMatrix(mat, ws, hs); // true matrix - - bool invertible; - mat = mat.inverted(&invertible); // invert matrix - - if (h == 0 || w == 0 || !invertible - || qAbs(h) >= 32768 || qAbs(w) >= 32768 ) - // error, return null pixmap - return QPixmap(); - - XImage *xi = XGetImage(xinfo.display(), handle(), 0, 0, ws, hs, AllPlanes, - depth1 ? XYPixmap : ZPixmap); - - if (!xi) - return QPixmap(); - - sbpl = xi->bytes_per_line; - sptr = (uchar *)xi->data; - bpp = xi->bits_per_pixel; - - if (depth1) - dbpl = (w+7)/8; - else - dbpl = ((w*bpp+31)/32)*4; - dbytes = dbpl*h; - - dptr = (uchar *)malloc(dbytes); // create buffer for bits - Q_CHECK_PTR(dptr); - if (depth1) // fill with zeros - memset(dptr, 0, dbytes); - else if (bpp == 8) // fill with background color - memset(dptr, WhitePixel(xinfo.display(), xinfo.screen()), dbytes); - else - memset(dptr, 0, dbytes); - - // #define QT_DEBUG_XIMAGE -#if defined(QT_DEBUG_XIMAGE) - qDebug("----IMAGE--INFO--------------"); - qDebug("width............. %d", xi->width); - qDebug("height............ %d", xi->height); - qDebug("xoffset........... %d", xi->xoffset); - qDebug("format............ %d", xi->format); - qDebug("byte order........ %d", xi->byte_order); - qDebug("bitmap unit....... %d", xi->bitmap_unit); - qDebug("bitmap bit order.. %d", xi->bitmap_bit_order); - qDebug("depth............. %d", xi->depth); - qDebug("bytes per line.... %d", xi->bytes_per_line); - qDebug("bits per pixel.... %d", xi->bits_per_pixel); -#endif - - int type; - if (xi->bitmap_bit_order == MSBFirst) - type = QT_XFORM_TYPE_MSBFIRST; - else - type = QT_XFORM_TYPE_LSBFIRST; - int xbpl, p_inc; - if (depth1) { - xbpl = (w+7)/8; - p_inc = dbpl - xbpl; - } else { - xbpl = (w*bpp)/8; - p_inc = dbpl - xbpl; - } - - if (!qt_xForm_helper(mat, xi->xoffset, type, bpp, dptr, xbpl, p_inc, h, sptr, sbpl, ws, hs)){ - qWarning("QPixmap::transform: display not supported (bpp=%d)",bpp); - QPixmap pm; - free(dptr); - return pm; - } - - qSafeXDestroyImage(xi); - - if (depth1) { // mono bitmap - QBitmap bm = QBitmap::fromData(QSize(w, h), dptr, - BitmapBitOrder(xinfo.display()) == MSBFirst - ? QImage::Format_Mono - : QImage::Format_MonoLSB); - free(dptr); - return bm; - } else { // color pixmap - QX11PlatformPixmap *x11Data = new QX11PlatformPixmap(QPlatformPixmap::PixmapType); - QPixmap pm(x11Data); - x11Data->flags &= ~QX11PlatformPixmap::Uninitialized; - x11Data->xinfo = xinfo; - x11Data->d = d; - x11Data->w = w; - x11Data->h = h; - x11Data->is_null = (w <= 0 || h <= 0); - x11Data->hd = XCreatePixmap(xinfo.display(), - RootWindow(xinfo.display(), xinfo.screen()), - w, h, d); - x11Data->setSerialNumber(qt_pixmap_serial.fetchAndAddRelaxed(1)); - -#if QT_CONFIG(xrender) - if (X11->use_xrender) { - XRenderPictFormat *format = x11Data->d == 32 - ? XRenderFindStandardFormat(xinfo.display(), PictStandardARGB32) - : XRenderFindVisualFormat(xinfo.display(), (Visual *) x11Data->xinfo.visual()); - x11Data->picture = XRenderCreatePicture(xinfo.display(), x11Data->hd, format, 0, 0); - } -#endif // QT_CONFIG(xrender) - - GC gc = XCreateGC(xinfo.display(), x11Data->hd, 0, 0); - xi = XCreateImage(dpy, (Visual*)x11Data->xinfo.visual(), - x11Data->d, - ZPixmap, 0, (char *)dptr, w, h, 32, 0); - XPutImage(dpy, qt_x11Pixmap(pm)->handle(), gc, xi, 0, 0, 0, 0, w, h); - qSafeXDestroyImage(xi); - XFreeGC(xinfo.display(), gc); - - if (x11_mask) { // xform mask, too - pm.setMask(mask_to_bitmap(xinfo.screen()).transformed(transform)); - } else if (d != 32 && complex_xform) { // need a mask! - QBitmap mask(ws, hs); - mask.fill(Qt::color1); - pm.setMask(mask.transformed(transform)); - } - return pm; - } -} - -QImage QX11PlatformPixmap::toImage() const -{ - return toImage(QRect(0, 0, w, h)); -} - -QImage QX11PlatformPixmap::toImage(const QRect &rect) const -{ - Window root_return; - int x_return; - int y_return; - unsigned int width_return; - unsigned int height_return; - unsigned int border_width_return; - unsigned int depth_return; - - XGetGeometry(xinfo.display(), hd, &root_return, &x_return, &y_return, &width_return, &height_return, &border_width_return, &depth_return); - - QXImageWrapper xiWrapper; - xiWrapper.xi = XGetImage(xinfo.display(), hd, rect.x(), rect.y(), rect.width(), rect.height(), - AllPlanes, (depth() == 1) ? XYPixmap : ZPixmap); - - Q_CHECK_PTR(xiWrapper.xi); - if (!xiWrapper.xi) - return QImage(); - - if (!x11_mask && canTakeQImageFromXImage(xiWrapper)) - return takeQImageFromXImage(xiWrapper); - - QImage image = toImage(xiWrapper, rect); - qSafeXDestroyImage(xiWrapper.xi); - return image; -} - -#if QT_CONFIG(xrender) -static XRenderPictFormat *qt_renderformat_for_depth(const QXcbX11Info &xinfo, int depth) -{ - if (depth == 1) - return XRenderFindStandardFormat(xinfo.display(), PictStandardA1); - else if (depth == 32) - return XRenderFindStandardFormat(xinfo.display(), PictStandardARGB32); - else - return XRenderFindVisualFormat(xinfo.display(), (Visual *)xinfo.visual()); -} -#endif - -Q_GLOBAL_STATIC(QX11PaintEngine, qt_x11_paintengine) - -QPaintEngine *QX11PlatformPixmap::paintEngine() const -{ - QX11PlatformPixmap *that = const_cast<QX11PlatformPixmap*>(this); - - if ((flags & Readonly)/* && share_mode == QPixmap::ImplicitlyShared*/) { - // if someone wants to draw onto us, copy the shared contents - // and turn it into a fully fledged QPixmap - ::Pixmap hd_copy = XCreatePixmap(xinfo.display(), RootWindow(xinfo.display(), xinfo.screen()), - w, h, d); -#if QT_CONFIG(xrender) - if (picture && d == 32) { - XRenderPictFormat *format = qt_renderformat_for_depth(xinfo, d); - ::Picture picture_copy = XRenderCreatePicture(xinfo.display(), - hd_copy, format, - 0, 0); - - XRenderComposite(xinfo.display(), PictOpSrc, picture, 0, picture_copy, - 0, 0, 0, 0, 0, 0, w, h); - XRenderFreePicture(xinfo.display(), picture); - that->picture = picture_copy; - } else -#endif - { - GC gc = XCreateGC(xinfo.display(), hd_copy, 0, 0); - XCopyArea(xinfo.display(), hd, hd_copy, gc, 0, 0, w, h, 0, 0); - XFreeGC(xinfo.display(), gc); - } - that->hd = hd_copy; - that->flags &= ~QX11PlatformPixmap::Readonly; - } - - if (qt_x11_paintengine->isActive()) { - if (!that->pengine) - that->pengine = new QX11PaintEngine; - - return that->pengine; - } - - return qt_x11_paintengine(); -} - -qreal QX11PlatformPixmap::devicePixelRatio() const -{ - return dpr; -} - -void QX11PlatformPixmap::setDevicePixelRatio(qreal scaleFactor) -{ - dpr = scaleFactor; -} - -Pixmap QX11PlatformPixmap::x11ConvertToDefaultDepth() -{ -#if QT_CONFIG(xrender) - if (d == xinfo.appDepth() || !X11->use_xrender) - return hd; - if (!hd2) { - hd2 = XCreatePixmap(xinfo.display(), hd, w, h, xinfo.appDepth()); - XRenderPictFormat *format = XRenderFindVisualFormat(xinfo.display(), - (Visual*) xinfo.visual()); - Picture pic = XRenderCreatePicture(xinfo.display(), hd2, format, 0, 0); - XRenderComposite(xinfo.display(), PictOpSrc, picture, - XNone, pic, 0, 0, 0, 0, 0, 0, w, h); - XRenderFreePicture(xinfo.display(), pic); - } - return hd2; -#else - return hd; -#endif -} - -XID QX11PlatformPixmap::createBitmapFromImage(const QImage &image) -{ - QImage img = image.convertToFormat(QImage::Format_MonoLSB); - const QRgb c0 = QColor(Qt::black).rgb(); - const QRgb c1 = QColor(Qt::white).rgb(); - if (img.color(0) == c0 && img.color(1) == c1) { - img.invertPixels(); - img.setColor(0, c1); - img.setColor(1, c0); - } - - char *bits; - uchar *tmp_bits; - int w = img.width(); - int h = img.height(); - int bpl = (w + 7) / 8; - qsizetype ibpl = img.bytesPerLine(); - if (bpl != ibpl) { - tmp_bits = new uchar[bpl*h]; - bits = (char *)tmp_bits; - uchar *p, *b; - int y; - b = tmp_bits; - p = img.scanLine(0); - for (y = 0; y < h; y++) { - memcpy(b, p, bpl); - b += bpl; - p += ibpl; - } - } else { - bits = (char *)img.bits(); - tmp_bits = 0; - } - XID hd = XCreateBitmapFromData(QXcbX11Info::display(), - QXcbX11Info::appRootWindow(), - bits, w, h); - if (tmp_bits) // Avoid purify complaint - delete [] tmp_bits; - return hd; -} - -bool QX11PlatformPixmap::isBackingStore() const -{ - return (flags & IsBackingStore); -} - -void QX11PlatformPixmap::setIsBackingStore(bool on) -{ - if (on) - flags |= IsBackingStore; - else { - flags &= ~IsBackingStore; - } -} - -#if QT_CONFIG(xrender) -void QX11PlatformPixmap::convertToARGB32(bool preserveContents) -{ - if (!X11->use_xrender) - return; - - // Q_ASSERT(count == 1); - if ((flags & Readonly)/* && share_mode == QPixmap::ExplicitlyShared*/) - return; - - Pixmap pm = XCreatePixmap(xinfo.display(), RootWindow(xinfo.display(), xinfo.screen()), - w, h, 32); - Picture p = XRenderCreatePicture(xinfo.display(), pm, - XRenderFindStandardFormat(xinfo.display(), PictStandardARGB32), 0, 0); - if (picture) { - if (preserveContents) - XRenderComposite(xinfo.display(), PictOpSrc, picture, 0, p, 0, 0, 0, 0, 0, 0, w, h); - if (!(flags & Readonly)) - XRenderFreePicture(xinfo.display(), picture); - } - if (hd && !(flags & Readonly)) - XFreePixmap(xinfo.display(), hd); - if (x11_mask) { - XFreePixmap(xinfo.display(), x11_mask); - if (mask_picture) - XRenderFreePicture(xinfo.display(), mask_picture); - x11_mask = 0; - mask_picture = 0; - } - hd = pm; - picture = p; - - d = 32; - xinfo.setDepth(32); - - XVisualInfo visinfo; - if (XMatchVisualInfo(xinfo.display(), xinfo.screen(), 32, TrueColor, &visinfo)) - xinfo.setVisual(visinfo.visual); -} -#endif - -void QX11PlatformPixmap::release() -{ - delete pengine; - pengine = 0; - - if (/*!X11*/ QCoreApplication::closingDown()) { - // At this point, the X server will already have freed our resources, - // so there is nothing to do. - return; - } - - if (x11_mask) { -#if QT_CONFIG(xrender) - if (mask_picture) - XRenderFreePicture(xinfo.display(), mask_picture); - mask_picture = 0; -#endif - XFreePixmap(xinfo.display(), x11_mask); - x11_mask = 0; - } - - if (hd) { -#if QT_CONFIG(xrender) - if (picture) { - XRenderFreePicture(xinfo.display(), picture); - picture = 0; - } -#endif // QT_CONFIG(xrender) - - if (hd2) { - XFreePixmap(xinfo.display(), hd2); - hd2 = 0; - } - if (!(flags & Readonly)) - XFreePixmap(xinfo.display(), hd); - hd = 0; - } -} - -QImage QX11PlatformPixmap::toImage(const QXImageWrapper &xiWrapper, const QRect &rect) const -{ - XImage *xi = xiWrapper.xi; - - int d = depth(); - Visual *visual = (Visual *)xinfo.visual(); - bool trucol = (visual->c_class >= TrueColor) && d > 1; - - QImage::Format format = QImage::Format_Mono; - if (d > 1 && d <= 8) { - d = 8; - format = QImage::Format_Indexed8; - } - // we could run into the situation where d == 8 AND trucol is true, which can - // cause problems when converting to and from images. in this case, always treat - // the depth as 32... - if (d > 8 || trucol) { - d = 32; - format = QImage::Format_RGB32; - } - - if (d == 1 && xi->bitmap_bit_order == LSBFirst) - format = QImage::Format_MonoLSB; - if (x11_mask && format == QImage::Format_RGB32) - format = QImage::Format_ARGB32; - - QImage image(xi->width, xi->height, format); - image.setDevicePixelRatio(devicePixelRatio()); - if (image.isNull()) // could not create image - return image; - - QImage alpha; - if (x11_mask) { - if (rect.contains(QRect(0, 0, w, h))) - alpha = mask().toImage(); - else - alpha = mask().toImage().copy(rect); - } - bool ale = alpha.format() == QImage::Format_MonoLSB; - - if (trucol) { // truecolor - const uint red_mask = (uint)visual->red_mask; - const uint green_mask = (uint)visual->green_mask; - const uint blue_mask = (uint)visual->blue_mask; - const int red_shift = highest_bit(red_mask) - 7; - const int green_shift = highest_bit(green_mask) - 7; - const int blue_shift = highest_bit(blue_mask) - 7; - - const uint red_bits = n_bits(red_mask); - const uint green_bits = n_bits(green_mask); - const uint blue_bits = n_bits(blue_mask); - - static uint red_table_bits = 0; - static uint green_table_bits = 0; - static uint blue_table_bits = 0; - - if (red_bits < 8 && red_table_bits != red_bits) { - build_scale_table(&red_scale_table, red_bits); - red_table_bits = red_bits; - } - if (blue_bits < 8 && blue_table_bits != blue_bits) { - build_scale_table(&blue_scale_table, blue_bits); - blue_table_bits = blue_bits; - } - if (green_bits < 8 && green_table_bits != green_bits) { - build_scale_table(&green_scale_table, green_bits); - green_table_bits = green_bits; - } - - int r, g, b; - - QRgb *dst; - uchar *src; - uint pixel; - int bppc = xi->bits_per_pixel; - - if (bppc > 8 && xi->byte_order == LSBFirst) - bppc++; - - for (int y = 0; y < xi->height; ++y) { - uchar* asrc = x11_mask ? alpha.scanLine(y) : 0; - dst = (QRgb *)image.scanLine(y); - src = (uchar *)xi->data + xi->bytes_per_line*y; - for (int x = 0; x < xi->width; x++) { - switch (bppc) { - case 8: - pixel = *src++; - break; - case 16: // 16 bit MSB - pixel = src[1] | (uint)src[0] << 8; - src += 2; - break; - case 17: // 16 bit LSB - pixel = src[0] | (uint)src[1] << 8; - src += 2; - break; - case 24: // 24 bit MSB - pixel = src[2] | (uint)src[1] << 8 | (uint)src[0] << 16; - src += 3; - break; - case 25: // 24 bit LSB - pixel = src[0] | (uint)src[1] << 8 | (uint)src[2] << 16; - src += 3; - break; - case 32: // 32 bit MSB - pixel = src[3] | (uint)src[2] << 8 | (uint)src[1] << 16 | (uint)src[0] << 24; - src += 4; - break; - case 33: // 32 bit LSB - pixel = src[0] | (uint)src[1] << 8 | (uint)src[2] << 16 | (uint)src[3] << 24; - src += 4; - break; - default: // should not really happen - x = xi->width; // leave loop - y = xi->height; - pixel = 0; // eliminate compiler warning - qWarning("QPixmap::convertToImage: Invalid depth %d", bppc); - } - if (red_shift > 0) - r = (pixel & red_mask) >> red_shift; - else - r = (pixel & red_mask) << -red_shift; - if (green_shift > 0) - g = (pixel & green_mask) >> green_shift; - else - g = (pixel & green_mask) << -green_shift; - if (blue_shift > 0) - b = (pixel & blue_mask) >> blue_shift; - else - b = (pixel & blue_mask) << -blue_shift; - - if (red_bits < 8) - r = red_scale_table[r]; - if (green_bits < 8) - g = green_scale_table[g]; - if (blue_bits < 8) - b = blue_scale_table[b]; - - if (x11_mask) { - if (ale) { - *dst++ = (asrc[x >> 3] & (1 << (x & 7))) ? qRgba(r, g, b, 0xff) : 0; - } else { - *dst++ = (asrc[x >> 3] & (0x80 >> (x & 7))) ? qRgba(r, g, b, 0xff) : 0; - } - } else { - *dst++ = qRgb(r, g, b); - } - } - } - } else if (xi->bits_per_pixel == d) { // compatible depth - char *xidata = xi->data; // copy each scanline - qsizetype bpl = qMin(image.bytesPerLine(),xi->bytes_per_line); - for (int y=0; y<xi->height; y++) { - memcpy(image.scanLine(y), xidata, bpl); - xidata += xi->bytes_per_line; - } - } else { - /* Typically 2 or 4 bits display depth */ - qWarning("QPixmap::convertToImage: Display not supported (bpp=%d)", - xi->bits_per_pixel); - return QImage(); - } - - if (d == 1) { // bitmap - image.setColorCount(2); - image.setColor(0, qRgb(255,255,255)); - image.setColor(1, qRgb(0,0,0)); - } else if (!trucol) { // pixmap with colormap - uchar *p; - uchar *end; - uchar use[256]; // pixel-in-use table - uchar pix[256]; // pixel translation table - int ncols; - memset(use, 0, 256); - memset(pix, 0, 256); - qsizetype bpl = image.bytesPerLine(); - - if (x11_mask) { // which pixels are used? - for (int i = 0; i < xi->height; i++) { - uchar* asrc = alpha.scanLine(i); - p = image.scanLine(i); - if (ale) { - for (int x = 0; x < xi->width; x++) { - if (asrc[x >> 3] & (1 << (x & 7))) - use[*p] = 1; - ++p; - } - } else { - for (int x = 0; x < xi->width; x++) { - if (asrc[x >> 3] & (0x80 >> (x & 7))) - use[*p] = 1; - ++p; - } - } - } - } else { - for (int i = 0; i < xi->height; i++) { - p = image.scanLine(i); - end = p + bpl; - while (p < end) - use[*p++] = 1; - } - } - ncols = 0; - for (int i = 0; i < 256; i++) { // build translation table - if (use[i]) - pix[i] = ncols++; - } - for (int i = 0; i < xi->height; i++) { // translate pixels - p = image.scanLine(i); - end = p + bpl; - while (p < end) { - *p = pix[*p]; - p++; - } - } - if (x11_mask) { - int trans; - if (ncols < 256) { - trans = ncols++; - image.setColorCount(ncols); // create color table - image.setColor(trans, 0x00000000); - } else { - image.setColorCount(ncols); // create color table - // oh dear... no spare "transparent" pixel. - // use first pixel in image (as good as any). - trans = image.scanLine(0)[0]; - } - for (int i = 0; i < xi->height; i++) { - uchar* asrc = alpha.scanLine(i); - p = image.scanLine(i); - if (ale) { - for (int x = 0; x < xi->width; x++) { - if (!(asrc[x >> 3] & (1 << (x & 7)))) - *p = trans; - ++p; - } - } else { - for (int x = 0; x < xi->width; x++) { - if (!(asrc[x >> 3] & (1 << (7 -(x & 7))))) - *p = trans; - ++p; - } - } - } - } else { - image.setColorCount(ncols); // create color table - } - QList<QColor> colors = QXcbColormap::instance(xinfo.screen()).colormap(); - int j = 0; - for (int i=0; i<colors.size(); i++) { // translate pixels - if (use[i]) - image.setColor(j++, 0xff000000 | colors.at(i).rgb()); - } - } - - return image; -} - -QT_END_NAMESPACE diff --git a/src/plugins/platforms/xcb/nativepainting/qpixmap_x11_p.h b/src/plugins/platforms/xcb/nativepainting/qpixmap_x11_p.h deleted file mode 100644 index 0755a34b4a8..00000000000 --- a/src/plugins/platforms/xcb/nativepainting/qpixmap_x11_p.h +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright (C) 2018 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 - -#pragma once - -#include <QBitmap> -#include <QPixmap> - -#include <qpa/qplatformpixmap.h> -#include "qxcbnativepainting.h" - -typedef unsigned long XID; -typedef XID Drawable; -typedef XID Picture; -typedef XID Pixmap; - -QT_BEGIN_NAMESPACE - -class QX11PaintEngine; -struct QXImageWrapper; - -class QX11PlatformPixmap : public QPlatformPixmap -{ -public: - QX11PlatformPixmap(PixelType pixelType); - ~QX11PlatformPixmap(); - - QPlatformPixmap *createCompatiblePlatformPixmap() const override; - void resize(int width, int height) override; - void fromImage(const QImage &img, Qt::ImageConversionFlags flags) override; - void copy(const QPlatformPixmap *data, const QRect &rect) override; - bool scroll(int dx, int dy, const QRect &rect) override; - int metric(QPaintDevice::PaintDeviceMetric metric) const override; - void fill(const QColor &fillColor) override; - QBitmap mask() const override; - void setMask(const QBitmap &mask) override; - bool hasAlphaChannel() const override; - QPixmap transformed(const QTransform &matrix, Qt::TransformationMode mode) const override; - QImage toImage() const override; - QImage toImage(const QRect &rect) const override; - QPaintEngine *paintEngine() const override; - qreal devicePixelRatio() const override; - void setDevicePixelRatio(qreal scaleFactor) override; - - inline Drawable handle() const { return hd; } - inline Picture x11PictureHandle() const { return picture; } - inline const QXcbX11Info *x11_info() const { return &xinfo; } - - Pixmap x11ConvertToDefaultDepth(); - static XID createBitmapFromImage(const QImage &image); - -#if QT_CONFIG(xrender) - void convertToARGB32(bool preserveContents = true); -#endif - - bool isBackingStore() const; - void setIsBackingStore(bool on); -private: - friend class QX11PaintEngine; - friend const QXcbX11Info &qt_x11Info(const QPixmap &pixmap); - friend void qt_x11SetScreen(QPixmap &pixmap, int screen); - - void release(); - QImage toImage(const QXImageWrapper &xi, const QRect &rect) const; - QBitmap mask_to_bitmap(int screen) const; - static Pixmap bitmap_to_mask(const QBitmap &, int screen); - void bitmapFromImage(const QImage &image); - bool canTakeQImageFromXImage(const QXImageWrapper &xi) const; - QImage takeQImageFromXImage(const QXImageWrapper &xi) const; - - Pixmap hd = 0; - - enum Flag { - NoFlags = 0x0, - Uninitialized = 0x1, - Readonly = 0x2, - InvertedWhenBoundToTexture = 0x4, - GlSurfaceCreatedWithAlpha = 0x8, - IsBackingStore = 0x10 - }; - uint flags; - - QXcbX11Info xinfo; - Pixmap x11_mask; - Picture picture; - Picture mask_picture; - Pixmap hd2; // sorted in the default display depth - //QPixmap::ShareMode share_mode; - qreal dpr; - - QX11PaintEngine *pengine; -}; - -inline QX11PlatformPixmap *qt_x11Pixmap(const QPixmap &pixmap) -{ - return (pixmap.handle() && pixmap.handle()->classId() == QPlatformPixmap::X11Class) - ? static_cast<QX11PlatformPixmap *>(pixmap.handle()) - : nullptr; -} - -inline Picture qt_x11PictureHandle(const QPixmap &pixmap) -{ - if (QX11PlatformPixmap *pm = qt_x11Pixmap(pixmap)) - return pm->x11PictureHandle(); - - return 0; -} - -inline Pixmap qt_x11PixmapHandle(const QPixmap &pixmap) -{ - if (QX11PlatformPixmap *pm = qt_x11Pixmap(pixmap)) - return pm->handle(); - - return 0; -} - -inline const QXcbX11Info &qt_x11Info(const QPixmap &pixmap) -{ - if (QX11PlatformPixmap *pm = qt_x11Pixmap(pixmap)) { - return pm->xinfo; - } else { - static QXcbX11Info nullX11Info; - return nullX11Info; - } -} - -int qt_x11SetDefaultScreen(int screen); -void qt_x11SetScreen(QPixmap &pixmap, int screen); - -QT_END_NAMESPACE diff --git a/src/plugins/platforms/xcb/nativepainting/qpolygonclipper_p.h b/src/plugins/platforms/xcb/nativepainting/qpolygonclipper_p.h deleted file mode 100644 index e1e31722d76..00000000000 --- a/src/plugins/platforms/xcb/nativepainting/qpolygonclipper_p.h +++ /dev/null @@ -1,278 +0,0 @@ -// 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 -// Qt-Security score:significant reason:default - -#pragma once - -// -// W A R N I N G -// ------------- -// -// This file is not part of the Qt API. It exists for the convenience -// of other Qt classes. This header file may change from version to -// version without notice, or even be removed. -// -// We mean it. -// - -#include <QtCore/qrect.h> -#include <QtGui/private/qdatabuffer_p.h> - -QT_BEGIN_NAMESPACE - -/* based on sutherland-hodgman line-by-line clipping, as described in - Computer Graphics and Principles */ -template <typename InType, typename OutType, typename CastType> class QPolygonClipper -{ -public: - QPolygonClipper() : - buffer1(0), buffer2(0) - { - x1 = y1 = x2 = y2 = 0; - } - - ~QPolygonClipper() - { - } - - void setBoundingRect(const QRect bounds) - { - x1 = bounds.x(); - x2 = bounds.x() + bounds.width(); - y1 = bounds.y(); - y2 = bounds.y() + bounds.height(); - } - - QRect boundingRect() - { - return QRect(QPoint(x1, y1), QPoint(x2, y2)); - } - - inline OutType intersectLeft(const OutType &p1, const OutType &p2) - { - OutType t; - qreal dy = (p1.y - p2.y) / qreal(p1.x - p2.x); - t.x = x1; - t.y = static_cast<CastType>(p2.y + (x1 - p2.x) * dy); - return t; - } - - - inline OutType intersectRight(const OutType &p1, const OutType &p2) - { - OutType t; - qreal dy = (p1.y - p2.y) / qreal(p1.x - p2.x); - t.x = x2; - t.y = static_cast<CastType>(p2.y + (x2 - p2.x) * dy); - return t; - } - - - inline OutType intersectTop(const OutType &p1, const OutType &p2) - { - OutType t; - qreal dx = (p1.x - p2.x) / qreal(p1.y - p2.y); - t.x = static_cast<CastType>(p2.x + (y1 - p2.y) * dx); - t.y = y1; - return t; - } - - - inline OutType intersectBottom(const OutType &p1, const OutType &p2) - { - OutType t; - qreal dx = (p1.x - p2.x) / qreal(p1.y - p2.y); - t.x = static_cast<CastType>(p2.x + (y2 - p2.y) * dx); - t.y = y2; - return t; - } - - - void clipPolygon(const InType *inPoints, int inCount, OutType **outPoints, int *outCount, - bool closePolygon = true) - { - Q_ASSERT(outPoints); - Q_ASSERT(outCount); - - if (inCount < 2) { - *outCount = 0; - return; - } - - buffer1.reset(); - buffer2.reset(); - - QDataBuffer<OutType> *source = &buffer1; - QDataBuffer<OutType> *clipped = &buffer2; - - // Gather some info since we are iterating through the points anyway.. - bool doLeft = false, doRight = false, doTop = false, doBottom = false; - OutType ot; - for (int i=0; i<inCount; ++i) { - ot = inPoints[i]; - clipped->add(ot); - - if (ot.x < x1) - doLeft = true; - else if (ot.x > x2) - doRight = true; - if (ot.y < y1) - doTop = true; - else if (ot.y > y2) - doBottom = true; - } - - if (doLeft && clipped->size() > 1) { - QDataBuffer<OutType> *tmp = source; - source = clipped; - clipped = tmp; - clipped->reset(); - int lastPos, start; - if (closePolygon) { - lastPos = source->size() - 1; - start = 0; - } else { - lastPos = 0; - start = 1; - if (source->at(0).x >= x1) - clipped->add(source->at(0)); - } - for (int i=start; i<inCount; ++i) { - const OutType &cpt = source->at(i); - const OutType &ppt = source->at(lastPos); - - if (cpt.x >= x1) { - if (ppt.x >= x1) { - clipped->add(cpt); - } else { - clipped->add(intersectLeft(cpt, ppt)); - clipped->add(cpt); - } - } else if (ppt.x >= x1) { - clipped->add(intersectLeft(cpt, ppt)); - } - lastPos = i; - } - } - - if (doRight && clipped->size() > 1) { - QDataBuffer<OutType> *tmp = source; - source = clipped; - clipped = tmp; - clipped->reset(); - int lastPos, start; - if (closePolygon) { - lastPos = source->size() - 1; - start = 0; - } else { - lastPos = 0; - start = 1; - if (source->at(0).x <= x2) - clipped->add(source->at(0)); - } - for (int i=start; i<source->size(); ++i) { - const OutType &cpt = source->at(i); - const OutType &ppt = source->at(lastPos); - - if (cpt.x <= x2) { - if (ppt.x <= x2) { - clipped->add(cpt); - } else { - clipped->add(intersectRight(cpt, ppt)); - clipped->add(cpt); - } - } else if (ppt.x <= x2) { - clipped->add(intersectRight(cpt, ppt)); - } - - lastPos = i; - } - - } - - if (doTop && clipped->size() > 1) { - QDataBuffer<OutType> *tmp = source; - source = clipped; - clipped = tmp; - clipped->reset(); - int lastPos, start; - if (closePolygon) { - lastPos = source->size() - 1; - start = 0; - } else { - lastPos = 0; - start = 1; - if (source->at(0).y >= y1) - clipped->add(source->at(0)); - } - for (int i=start; i<source->size(); ++i) { - const OutType &cpt = source->at(i); - const OutType &ppt = source->at(lastPos); - - if (cpt.y >= y1) { - if (ppt.y >= y1) { - clipped->add(cpt); - } else { - clipped->add(intersectTop(cpt, ppt)); - clipped->add(cpt); - } - } else if (ppt.y >= y1) { - clipped->add(intersectTop(cpt, ppt)); - } - - lastPos = i; - } - } - - if (doBottom && clipped->size() > 1) { - QDataBuffer<OutType> *tmp = source; - source = clipped; - clipped = tmp; - clipped->reset(); - int lastPos, start; - if (closePolygon) { - lastPos = source->size() - 1; - start = 0; - } else { - lastPos = 0; - start = 1; - if (source->at(0).y <= y2) - clipped->add(source->at(0)); - } - for (int i=start; i<source->size(); ++i) { - const OutType &cpt = source->at(i); - const OutType &ppt = source->at(lastPos); - - if (cpt.y <= y2) { - if (ppt.y <= y2) { - clipped->add(cpt); - } else { - clipped->add(intersectBottom(cpt, ppt)); - clipped->add(cpt); - } - } else if (ppt.y <= y2) { - clipped->add(intersectBottom(cpt, ppt)); - } - lastPos = i; - } - } - - if (closePolygon && clipped->size() > 0) { - // close clipped polygon - if (clipped->at(0).x != clipped->at(clipped->size()-1).x || - clipped->at(0).y != clipped->at(clipped->size()-1).y) { - OutType ot = clipped->at(0); - clipped->add(ot); - } - } - *outCount = clipped->size(); - *outPoints = clipped->data(); - } - -private: - int x1, x2, y1, y2; - QDataBuffer<OutType> buffer1; - QDataBuffer<OutType> buffer2; -}; - -QT_END_NAMESPACE diff --git a/src/plugins/platforms/xcb/nativepainting/qt_x11_p.h b/src/plugins/platforms/xcb/nativepainting/qt_x11_p.h deleted file mode 100644 index 2986b8f1453..00000000000 --- a/src/plugins/platforms/xcb/nativepainting/qt_x11_p.h +++ /dev/null @@ -1,163 +0,0 @@ -// Copyright (C) 2018 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 - -#pragma once - -#define register /* C++17 deprecated register */ -#include <X11/Xlib.h> -#include <X11/Xatom.h> -#undef register - -#if QT_CONFIG(xrender) -# include "qtessellator_p.h" -# include <X11/extensions/Xrender.h> -#endif - -#if QT_CONFIG(fontconfig) -#include <fontconfig/fontconfig.h> -#endif - -#if defined(FT_LCD_FILTER_H) -#include FT_LCD_FILTER_H -#endif - -#if defined(FC_LCD_FILTER) - -#ifndef FC_LCD_FILTER_NONE -#define FC_LCD_FILTER_NONE FC_LCD_NONE -#endif - -#ifndef FC_LCD_FILTER_DEFAULT -#define FC_LCD_FILTER_DEFAULT FC_LCD_DEFAULT -#endif - -#ifndef FC_LCD_FILTER_LIGHT -#define FC_LCD_FILTER_LIGHT FC_LCD_LIGHT -#endif - -#ifndef FC_LCD_FILTER_LEGACY -#define FC_LCD_FILTER_LEGACY FC_LCD_LEGACY -#endif - -#endif - -QT_BEGIN_NAMESPACE - -// rename a couple of X defines to get rid of name clashes -// resolve the conflict between X11's FocusIn and QEvent::FocusIn -enum { - XFocusOut = FocusOut, - XFocusIn = FocusIn, - XKeyPress = KeyPress, - XKeyRelease = KeyRelease, - XNone = None, - XRevertToParent = RevertToParent, - XGrayScale = GrayScale, - XCursorShape = CursorShape, -}; -#undef FocusOut -#undef FocusIn -#undef KeyPress -#undef KeyRelease -#undef None -#undef RevertToParent -#undef GrayScale -#undef CursorShape - -#ifdef FontChange -#undef FontChange -#endif - -Q_DECLARE_TYPEINFO(XPoint, Q_PRIMITIVE_TYPE); -Q_DECLARE_TYPEINFO(XRectangle, Q_PRIMITIVE_TYPE); -Q_DECLARE_TYPEINFO(XChar2b, Q_PRIMITIVE_TYPE); -#if QT_CONFIG(xrender) -Q_DECLARE_TYPEINFO(XGlyphElt32, Q_PRIMITIVE_TYPE); -#endif - -struct QX11InfoData; - -enum DesktopEnvironment { - DE_UNKNOWN, - DE_KDE, - DE_GNOME, - DE_CDE, - DE_MEEGO_COMPOSITOR, - DE_4DWM -}; - -struct QXcbX11Data { - Display *display = nullptr; - - // true if Qt is compiled w/ RENDER support and RENDER is supported on the connected Display - bool use_xrender = false; - int xrender_major = 0; - int xrender_version = 0; - - QX11InfoData *screens = nullptr; - Visual **argbVisuals = nullptr; - Colormap *argbColormaps = nullptr; - int screenCount = 0; - int defaultScreen = 0; - - // options - int visual_class = 0; - int visual_id = 0; - int color_count = 0; - bool custom_cmap = false; - - // outside visual/colormap - Visual *visual = nullptr; - Colormap colormap = 0; - -#if QT_CONFIG(xrender) - enum { solid_fill_count = 16 }; - struct SolidFills { - XRenderColor color; - int screen; - Picture picture; - } solid_fills[solid_fill_count]; - enum { pattern_fill_count = 16 }; - struct PatternFills { - XRenderColor color; - XRenderColor bg_color; - int screen; - int style; - bool opaque; - Picture picture; - } pattern_fills[pattern_fill_count]; - Picture getSolidFill(int screen, const QColor &c); - XRenderColor preMultiply(const QColor &c); -#endif - - bool fc_antialias = true; - int fc_hint_style = 0; - - DesktopEnvironment desktopEnvironment = DE_GNOME; -}; - -extern QXcbX11Data *qt_x11Data; -#define X11 qt_x11Data - -struct QX11InfoData { - int screen; - int dpiX; - int dpiY; - int depth; - int cells; - Colormap colormap; - Visual *visual; - bool defaultColormap; - bool defaultVisual; - int subpixel = 0; -}; - -template <class T> -constexpr inline int lowest_bit(T v) noexcept -{ - int result = qCountTrailingZeroBits(v); - return ((result >> 3) == sizeof(T)) ? -1 : result; -} - -QT_END_NAMESPACE diff --git a/src/plugins/platforms/xcb/nativepainting/qtessellator.cpp b/src/plugins/platforms/xcb/nativepainting/qtessellator.cpp deleted file mode 100644 index dd83f8852b7..00000000000 --- a/src/plugins/platforms/xcb/nativepainting/qtessellator.cpp +++ /dev/null @@ -1,1466 +0,0 @@ -// Copyright (C) 2018 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 "qtessellator_p.h" - -#include <QRect> -#include <QList> -#include <QMap> -#include <QDebug> - -#include <qmath.h> -#include <limits.h> -#include <algorithm> - -QT_BEGIN_NAMESPACE - -//#define DEBUG -#ifdef DEBUG -#define QDEBUG qDebug -#else -#define QDEBUG if (1){} else qDebug -#endif - -static const bool emit_clever = true; -static const bool mark_clever = false; - -enum VertexFlags { - LineBeforeStarts = 0x1, - LineBeforeEnds = 0x2, - LineBeforeHorizontal = 0x4, - LineAfterStarts = 0x8, - LineAfterEnds = 0x10, - LineAfterHorizontal = 0x20 -}; - - - -class QTessellatorPrivate { -public: - struct Vertices; - - QTessellatorPrivate() {} - - QRectF collectAndSortVertices(const QPointF *points, int *maxActiveEdges); - void cancelCoincidingEdges(); - - void emitEdges(QTessellator *tessellator); - void processIntersections(); - void removeEdges(); - void addEdges(); - void addIntersections(); - - struct Vertex : public QTessellator::Vertex - { - int flags; - }; - - struct Intersection - { - Q27Dot5 y; - int edge; - bool operator <(const Intersection &other) const { - if (y != other.y) - return y < other.y; - return edge < other.edge; - } - }; - struct IntersectionLink - { - int next; - int prev; - }; - typedef QMap<Intersection, IntersectionLink> Intersections; - - struct Edge { - Edge(const Vertices &v, int _edge); - int edge; - const Vertex *v0; - const Vertex *v1; - Q27Dot5 y_left; - Q27Dot5 y_right; - signed int winding : 8; - bool mark; - bool free; - bool intersect_left; - bool intersect_right; - bool isLeftOf(const Edge &other, Q27Dot5 y) const; - Q27Dot5 positionAt(Q27Dot5 y) const; - bool intersect(const Edge &other, Q27Dot5 *y, bool *det_positive) const; - - }; - - class EdgeSorter - { - public: - EdgeSorter(int _y) : y(_y) {} - bool operator() (const Edge *e1, const Edge *e2); - int y; - }; - - class Scanline { - public: - Scanline(); - ~Scanline(); - - void init(int maxActiveEdges); - void done(); - - int findEdgePosition(Q27Dot5 x, Q27Dot5 y) const; - int findEdgePosition(const Edge &e) const; - int findEdge(int edge) const; - void clearMarks(); - - void swap(int p1, int p2) { - Edge *tmp = edges[p1]; - edges[p1] = edges[p2]; - edges[p2] = tmp; - } - void insert(int pos, const Edge &e); - void removeAt(int pos); - void markEdges(int pos1, int pos2); - - void prepareLine(); - void lineDone(); - - Edge **old; - int old_size; - - Edge **edges; - int size; - - private: - Edge *edge_table; - int first_unused; - int max_edges; - enum { default_alloc = 32 }; - }; - - struct Vertices { - enum { default_alloc = 128 }; - Vertices(); - ~Vertices(); - void init(int maxVertices); - void done(); - Vertex *storage; - Vertex **sorted; - - Vertex *operator[] (int i) { return storage + i; } - const Vertex *operator[] (int i) const { return storage + i; } - int position(const Vertex *v) const { - return v - storage; - } - Vertex *next(Vertex *v) { - ++v; - if (v == storage + nPoints) - v = storage; - return v; - } - const Vertex *next(const Vertex *v) const { - ++v; - if (v == storage + nPoints) - v = storage; - return v; - } - int nextPos(const Vertex *v) const { - ++v; - if (v == storage + nPoints) - return 0; - return v - storage; - } - Vertex *prev(Vertex *v) { - if (v == storage) - v = storage + nPoints; - --v; - return v; - } - const Vertex *prev(const Vertex *v) const { - if (v == storage) - v = storage + nPoints; - --v; - return v; - } - int prevPos(const Vertex *v) const { - if (v == storage) - v = storage + nPoints; - --v; - return v - storage; - } - int nPoints; - int allocated; - }; - Vertices vertices; - Intersections intersections; - Scanline scanline; - bool winding; - Q27Dot5 y; - int currentVertex; - -private: - void addIntersection(const Edge *e1, const Edge *e2); - bool edgeInChain(Intersection i, int edge); -}; - - -QTessellatorPrivate::Edge::Edge(const QTessellatorPrivate::Vertices &vertices, int edge) -{ - this->edge = edge; - intersect_left = intersect_right = true; - mark = false; - free = false; - - v0 = vertices[edge]; - v1 = vertices.next(v0); - - Q_ASSERT(v0->y != v1->y); - - if (v0->y > v1->y) { - qSwap(v0, v1); - winding = -1; - } else { - winding = 1; - } - y_left = y_right = v0->y; -} - -// This is basically the algorithm from graphics gems. The algorithm -// is cubic in the coordinates at one place. Since we use 64bit -// integers, this implies, that the allowed range for our coordinates -// is limited to 21 bits. With 5 bits behind the decimal, this -// implies that differences in coordaintes can range from 2*SHORT_MIN -// to 2*SHORT_MAX, giving us efficiently a coordinate system from -// SHORT_MIN to SHORT_MAX. -// - -// WARNING: It's absolutely critical that the intersect() and isLeftOf() methods use -// exactly the same algorithm to calculate yi. It's also important to be sure the algorithms -// are transitive (ie. the conditions below are true for all input data): -// -// a.intersect(b) == b.intersect(a) -// a.isLeftOf(b) != b.isLeftOf(a) -// -// This is tricky to get right, so be very careful when changing anything in here! - -static inline bool sameSign(qint64 a, qint64 b) { - return (((qint64) ((quint64) a ^ (quint64) b)) >= 0 ); -} - -bool QTessellatorPrivate::Edge::intersect(const Edge &other, Q27Dot5 *y, bool *det_positive) const -{ - qint64 a1 = v1->y - v0->y; - qint64 b1 = v0->x - v1->x; - - qint64 a2 = other.v1->y - other.v0->y; - qint64 b2 = other.v0->x - other.v1->x; - - qint64 det = a1 * b2 - a2 * b1; - if (det == 0) - return false; - - qint64 c1 = qint64(v1->x) * v0->y - qint64(v0->x) * v1->y; - - qint64 r3 = a1 * other.v0->x + b1 * other.v0->y + c1; - qint64 r4 = a1 * other.v1->x + b1 * other.v1->y + c1; - - // Check signs of r3 and r4. If both point 3 and point 4 lie on - // same side of line 1, the line segments do not intersect. - QDEBUG() << " " << r3 << r4; - if (r3 != 0 && r4 != 0 && sameSign( r3, r4 )) - return false; - - qint64 c2 = qint64(other.v1->x) * other.v0->y - qint64(other.v0->x) * other.v1->y; - - qint64 r1 = a2 * v0->x + b2 * v0->y + c2; - qint64 r2 = a2 * v1->x + b2 * v1->y + c2; - - // Check signs of r1 and r2. If both point 1 and point 2 lie - // on same side of second line segment, the line segments do not intersect. - QDEBUG() << " " << r1 << r2; - if (r1 != 0 && r2 != 0 && sameSign( r1, r2 )) - return false; - - // The det/2 is to get rounding instead of truncating. It - // is added or subtracted to the numerator, depending upon the - // sign of the numerator. - qint64 offset = det < 0 ? -det : det; - offset >>= 1; - - qint64 num = a2 * c1 - a1 * c2; - *y = ( num < 0 ? num - offset : num + offset ) / det; - - *det_positive = (det > 0); - - return true; -} - -#undef SAME_SIGNS - -bool QTessellatorPrivate::Edge::isLeftOf(const Edge &other, Q27Dot5 y) const -{ -// QDEBUG() << "isLeftOf" << edge << other.edge << y; - qint64 a1 = v1->y - v0->y; - qint64 b1 = v0->x - v1->x; - qint64 a2 = other.v1->y - other.v0->y; - qint64 b2 = other.v0->x - other.v1->x; - - qint64 c2 = qint64(other.v1->x) * other.v0->y - qint64(other.v0->x) * other.v1->y; - - qint64 det = a1 * b2 - a2 * b1; - if (det == 0) { - // lines are parallel. Only need to check side of one point - // fixed ordering for coincident edges - qint64 r1 = a2 * v0->x + b2 * v0->y + c2; -// QDEBUG() << "det = 0" << r1; - if (r1 == 0) - return edge < other.edge; - return (r1 < 0); - } - - // not parallel, need to find the y coordinate of the intersection point - qint64 c1 = qint64(v1->x) * v0->y - qint64(v0->x) * v1->y; - - qint64 offset = det < 0 ? -det : det; - offset >>= 1; - - qint64 num = a2 * c1 - a1 * c2; - qint64 yi = ( num < 0 ? num - offset : num + offset ) / det; -// QDEBUG() << " num=" << num << "offset=" << offset << "det=" << det; - - return ((yi > y) ^ (det < 0)); -} - -static inline bool compareVertex(const QTessellatorPrivate::Vertex *p1, - const QTessellatorPrivate::Vertex *p2) -{ - if (p1->y == p2->y) { - if (p1->x == p2->x) - return p1 < p2; - return p1->x < p2->x; - } - return p1->y < p2->y; -} - -Q27Dot5 QTessellatorPrivate::Edge::positionAt(Q27Dot5 y) const -{ - if (y == v0->y) - return v0->x; - else if (y == v1->y) - return v1->x; - - qint64 d = v1->x - v0->x; - return (v0->x + d*(y - v0->y)/(v1->y-v0->y)); -} - -bool QTessellatorPrivate::EdgeSorter::operator() (const Edge *e1, const Edge *e2) -{ - return e1->isLeftOf(*e2, y); -} - - -QTessellatorPrivate::Scanline::Scanline() -{ - edges = 0; - edge_table = 0; - old = 0; -} - -void QTessellatorPrivate::Scanline::init(int maxActiveEdges) -{ - maxActiveEdges *= 2; - if (!edges || maxActiveEdges > default_alloc) { - max_edges = maxActiveEdges; - int s = qMax(maxActiveEdges + 1, default_alloc + 1); - edges = q_check_ptr((Edge **)realloc(edges, s*sizeof(Edge *))); - edge_table = q_check_ptr((Edge *)realloc(edge_table, s*sizeof(Edge))); - old = q_check_ptr((Edge **)realloc(old, s*sizeof(Edge *))); - } - size = 0; - old_size = 0; - first_unused = 0; - for (int i = 0; i < maxActiveEdges; ++i) - edge_table[i].edge = i+1; - edge_table[maxActiveEdges].edge = -1; -} - -void QTessellatorPrivate::Scanline::done() -{ - if (max_edges > default_alloc) { - free(edges); - free(old); - free(edge_table); - edges = 0; - old = 0; - edge_table = 0; - } -} - -QTessellatorPrivate::Scanline::~Scanline() -{ - free(edges); - free(old); - free(edge_table); -} - -int QTessellatorPrivate::Scanline::findEdgePosition(Q27Dot5 x, Q27Dot5 y) const -{ - int min = 0; - int max = size - 1; - while (min < max) { - int pos = min + ((max - min + 1) >> 1); - Q27Dot5 ax = edges[pos]->positionAt(y); - if (ax > x) { - max = pos - 1; - } else { - min = pos; - } - } - return min; -} - -int QTessellatorPrivate::Scanline::findEdgePosition(const Edge &e) const -{ -// qDebug() << ">> findEdgePosition"; - int min = 0; - int max = size; - while (min < max) { - int pos = min + ((max - min) >> 1); -// qDebug() << " " << min << max << pos << edges[pos]->isLeftOf(e, e.y0); - if (edges[pos]->isLeftOf(e, e.v0->y)) { - min = pos + 1; - } else { - max = pos; - } - } -// qDebug() << "<< findEdgePosition got" << min; - return min; -} - -int QTessellatorPrivate::Scanline::findEdge(int edge) const -{ - for (int i = 0; i < size; ++i) { - int item_edge = edges[i]->edge; - if (item_edge == edge) - return i; - } - //Q_ASSERT(false); - return -1; -} - -void QTessellatorPrivate::Scanline::clearMarks() -{ - for (int i = 0; i < size; ++i) { - edges[i]->mark = false; - edges[i]->intersect_left = false; - edges[i]->intersect_right = false; - } -} - -void QTessellatorPrivate::Scanline::prepareLine() -{ - Edge **end = edges + size; - Edge **e = edges; - Edge **o = old; - while (e < end) { - *o = *e; - ++o; - ++e; - } - old_size = size; -} - -void QTessellatorPrivate::Scanline::lineDone() -{ - Edge **end = old + old_size; - Edge **e = old; - while (e < end) { - if ((*e)->free) { - (*e)->edge = first_unused; - first_unused = (*e - edge_table); - } - ++e; - } -} - -void QTessellatorPrivate::Scanline::insert(int pos, const Edge &e) -{ - Edge *edge = edge_table + first_unused; - first_unused = edge->edge; - Q_ASSERT(first_unused != -1); - *edge = e; - memmove(edges + pos + 1, edges + pos, (size - pos)*sizeof(Edge *)); - edges[pos] = edge; - ++size; -} - -void QTessellatorPrivate::Scanline::removeAt(int pos) -{ - Edge *e = edges[pos]; - e->free = true; - --size; - memmove(edges + pos, edges + pos + 1, (size - pos)*sizeof(Edge *)); -} - -void QTessellatorPrivate::Scanline::markEdges(int pos1, int pos2) -{ - if (pos2 < pos1) - return; - - for (int i = pos1; i <= pos2; ++i) - edges[i]->mark = true; -} - - -QTessellatorPrivate::Vertices::Vertices() -{ - storage = 0; - sorted = 0; - allocated = 0; - nPoints = 0; -} - -QTessellatorPrivate::Vertices::~Vertices() -{ - if (storage) { - free(storage); - free(sorted); - } -} - -void QTessellatorPrivate::Vertices::init(int maxVertices) -{ - if (!storage || maxVertices > allocated) { - int size = qMax((int)default_alloc, maxVertices); - storage = q_check_ptr((Vertex *)realloc(storage, size*sizeof(Vertex))); - sorted = q_check_ptr((Vertex **)realloc(sorted, size*sizeof(Vertex *))); - allocated = maxVertices; - } -} - -void QTessellatorPrivate::Vertices::done() -{ - if (allocated > default_alloc) { - free(storage); - free(sorted); - storage = 0; - sorted = 0; - allocated = 0; - } -} - - - -static inline void fillTrapezoid(Q27Dot5 y1, Q27Dot5 y2, int left, int right, - const QTessellatorPrivate::Vertices &vertices, - QTessellator::Trapezoid *trap) -{ - trap->top = y1; - trap->bottom = y2; - const QTessellatorPrivate::Vertex *v = vertices[left]; - trap->topLeft = v; - trap->bottomLeft = vertices.next(v); - if (trap->topLeft->y > trap->bottomLeft->y) - qSwap(trap->topLeft,trap->bottomLeft); - v = vertices[right]; - trap->topRight = v; - trap->bottomRight = vertices.next(v); - if (trap->topRight->y > trap->bottomRight->y) - qSwap(trap->topRight, trap->bottomRight); -} - -QRectF QTessellatorPrivate::collectAndSortVertices(const QPointF *points, int *maxActiveEdges) -{ - *maxActiveEdges = 0; - Vertex *v = vertices.storage; - Vertex **vv = vertices.sorted; - - qreal xmin(points[0].x()); - qreal xmax(points[0].x()); - qreal ymin(points[0].y()); - qreal ymax(points[0].y()); - - // collect vertex data - Q27Dot5 y_prev = FloatToQ27Dot5(points[vertices.nPoints-1].y()); - Q27Dot5 x_next = FloatToQ27Dot5(points[0].x()); - Q27Dot5 y_next = FloatToQ27Dot5(points[0].y()); - int j = 0; - int i = 0; - while (i < vertices.nPoints) { - Q27Dot5 y_curr = y_next; - - *vv = v; - - v->x = x_next; - v->y = y_next; - v->flags = 0; - - next_point: - - xmin = qMin(xmin, points[i+1].x()); - xmax = qMax(xmax, points[i+1].x()); - ymin = qMin(ymin, points[i+1].y()); - ymax = qMax(ymax, points[i+1].y()); - - y_next = FloatToQ27Dot5(points[i+1].y()); - x_next = FloatToQ27Dot5(points[i+1].x()); - - // skip vertices on top of each other - if (v->x == x_next && v->y == y_next) { - ++i; - if (i < vertices.nPoints) - goto next_point; - Vertex *v0 = vertices.storage; - v0->flags &= ~(LineBeforeStarts|LineBeforeEnds|LineBeforeHorizontal); - if (y_prev < y_curr) - v0->flags |= LineBeforeEnds; - else if (y_prev > y_curr) - v0->flags |= LineBeforeStarts; - else - v0->flags |= LineBeforeHorizontal; - if ((v0->flags & (LineBeforeStarts|LineAfterStarts)) - && !(v0->flags & (LineAfterEnds|LineBeforeEnds))) - *maxActiveEdges += 2; - break; - } - - if (y_prev < y_curr) - v->flags |= LineBeforeEnds; - else if (y_prev > y_curr) - v->flags |= LineBeforeStarts; - else - v->flags |= LineBeforeHorizontal; - - - if (y_curr < y_next) - v->flags |= LineAfterStarts; - else if (y_curr > y_next) - v->flags |= LineAfterEnds; - else - v->flags |= LineAfterHorizontal; - // ### could probably get better limit by looping over sorted list and counting down on ending edges - if ((v->flags & (LineBeforeStarts|LineAfterStarts)) - && !(v->flags & (LineAfterEnds|LineBeforeEnds))) - *maxActiveEdges += 2; - y_prev = y_curr; - ++v; - ++vv; - ++j; - ++i; - } - vertices.nPoints = j; - - QDEBUG() << "maxActiveEdges=" << *maxActiveEdges; - vv = vertices.sorted; - std::sort(vv, vv + vertices.nPoints, compareVertex); - - return QRectF(xmin, ymin, xmax-xmin, ymax-ymin); -} - -struct QCoincidingEdge { - QTessellatorPrivate::Vertex *start; - QTessellatorPrivate::Vertex *end; - bool used; - bool before; - - inline bool operator<(const QCoincidingEdge &e2) const - { - return end->y == e2.end->y ? end->x < e2.end->x : end->y < e2.end->y; - } -}; - -static void cancelEdges(QCoincidingEdge &e1, QCoincidingEdge &e2) -{ - if (e1.before) { - e1.start->flags &= ~(LineBeforeStarts|LineBeforeHorizontal); - e1.end->flags &= ~(LineAfterEnds|LineAfterHorizontal); - } else { - e1.start->flags &= ~(LineAfterStarts|LineAfterHorizontal); - e1.end->flags &= ~(LineBeforeEnds|LineBeforeHorizontal); - } - if (e2.before) { - e2.start->flags &= ~(LineBeforeStarts|LineBeforeHorizontal); - e2.end->flags &= ~(LineAfterEnds|LineAfterHorizontal); - } else { - e2.start->flags &= ~(LineAfterStarts|LineAfterHorizontal); - e2.end->flags &= ~(LineBeforeEnds|LineBeforeHorizontal); - } - e1.used = e2.used = true; -} - -void QTessellatorPrivate::cancelCoincidingEdges() -{ - Vertex **vv = vertices.sorted; - - QCoincidingEdge *tl = nullptr; - int tlSize = 0; - - for (int i = 0; i < vertices.nPoints - 1; ++i) { - Vertex *v = vv[i]; - int testListSize = 0; - while (i < vertices.nPoints - 1) { - Vertex *n = vv[i]; - if (v->x != n->x || v->y != n->y) - break; - - if (testListSize > tlSize - 2) { - tlSize = qMax(tlSize*2, 16); - tl = q_check_ptr((QCoincidingEdge *)realloc(tl, tlSize*sizeof(QCoincidingEdge))); - } - if (n->flags & (LineBeforeStarts|LineBeforeHorizontal)) { - tl[testListSize].start = n; - tl[testListSize].end = vertices.prev(n); - tl[testListSize].used = false; - tl[testListSize].before = true; - ++testListSize; - } - if (n->flags & (LineAfterStarts|LineAfterHorizontal)) { - tl[testListSize].start = n; - tl[testListSize].end = vertices.next(n); - tl[testListSize].used = false; - tl[testListSize].before = false; - ++testListSize; - } - ++i; - } - if (!testListSize) - continue; - - std::sort(tl, tl + testListSize); - -QT_WARNING_PUSH -QT_WARNING_DISABLE_CLANG("-Wfor-loop-analysis") - for (int j = 0; j < testListSize; ++j) { - if (tl[j].used) - continue; - - for (int k = j + 1; k < testListSize; ++k) { - if (tl[j].end->x != tl[k].end->x - || tl[j].end->y != tl[k].end->y - || tl[k].used) - break; - - if (!winding || tl[j].before != tl[k].before) { - cancelEdges(tl[j], tl[k]); - break; - } - ++k; - } - ++j; - } -QT_WARNING_POP - } - free(tl); -} - - -void QTessellatorPrivate::emitEdges(QTessellator *tessellator) -{ - //QDEBUG() << "TRAPS:"; - if (!scanline.old_size) - return; - - // emit edges - if (winding) { - // winding fill rule - int w = 0; - - scanline.old[0]->y_left = y; - - for (int i = 0; i < scanline.old_size - 1; ++i) { - Edge *left = scanline.old[i]; - Edge *right = scanline.old[i+1]; - w += left->winding; -// qDebug() << "i=" << i << "edge->winding=" << left->winding << "winding=" << winding; - if (w == 0) { - left->y_right = y; - right->y_left = y; - } else if (!emit_clever || left->mark || right->mark) { - Q27Dot5 top = qMax(left->y_right, right->y_left); - if (top != y) { - QTessellator::Trapezoid trap; - fillTrapezoid(top, y, left->edge, right->edge, vertices, &trap); - tessellator->addTrap(trap); -// QDEBUG() << " top=" << Q27Dot5ToDouble(top) << "left=" << left->edge << "right=" << right->edge; - } - right->y_left = y; - left->y_right = y; - } - left->mark = false; - } - if (scanline.old[scanline.old_size - 1]->mark) { - scanline.old[scanline.old_size - 1]->y_right = y; - scanline.old[scanline.old_size - 1]->mark = false; - } - } else { - // odd-even fill rule - for (int i = 0; i < scanline.old_size; i += 2) { - Edge *left = scanline.old[i]; - Edge *right = scanline.old[i+1]; - if (!emit_clever || left->mark || right->mark) { - Q27Dot5 top = qMax(left->y_right, right->y_left); - if (top != y) { - QTessellator::Trapezoid trap; - fillTrapezoid(top, y, left->edge, right->edge, vertices, &trap); - tessellator->addTrap(trap); - } -// QDEBUG() << " top=" << Q27Dot5ToDouble(top) << "left=" << left->edge << "right=" << right->edge; - left->y_left = y; - left->y_right = y; - right->y_left = y; - right->y_right = y; - left->mark = right->mark = false; - } - } - } -} - - -void QTessellatorPrivate::processIntersections() -{ - QDEBUG() << "PROCESS INTERSECTIONS"; - // process intersections - while (!intersections.isEmpty()) { - Intersections::iterator it = intersections.begin(); - if (it.key().y != y) - break; - - // swap edges - QDEBUG() << " swapping intersecting edges "; - int min = scanline.size; - int max = 0; - Q27Dot5 xmin = INT_MAX; - Q27Dot5 xmax = INT_MIN; - int num = 0; - while (1) { - const Intersection i = it.key(); - int next = it->next; - - int edgePos = scanline.findEdge(i.edge); - if (edgePos >= 0) { - ++num; - min = qMin(edgePos, min); - max = qMax(edgePos, max); - Edge *edge = scanline.edges[edgePos]; - xmin = qMin(xmin, edge->positionAt(y)); - xmax = qMax(xmax, edge->positionAt(y)); - } - Intersection key; - key.y = y; - key.edge = next; - it = intersections.find(key); - intersections.remove(i); - if (it == intersections.end()) - break; - } - if (num < 2) - continue; - - Q_ASSERT(min != max); - QDEBUG() << "sorting between" << min << "and" << max << "xpos=" << xmin << xmax; - while (min > 0 && scanline.edges[min - 1]->positionAt(y) >= xmin) { - QDEBUG() << " adding edge on left"; - --min; - } - while (max < scanline.size - 1 && scanline.edges[max + 1]->positionAt(y) <= xmax) { - QDEBUG() << " adding edge on right"; - ++max; - } - - std::sort(scanline.edges + min, scanline.edges + max + 1, EdgeSorter(y)); -#ifdef DEBUG - for (int i = min; i <= max; ++i) - QDEBUG() << " " << scanline.edges[i]->edge << "at pos" << i; -#endif - for (int i = min; i <= max; ++i) { - Edge *edge = scanline.edges[i]; - edge->intersect_left = true; - edge->intersect_right = true; - edge->mark = true; - } - } -} - -void QTessellatorPrivate::removeEdges() -{ - int cv = currentVertex; - while (cv < vertices.nPoints) { - const Vertex *v = vertices.sorted[cv]; - if (v->y > y) - break; - if (v->flags & LineBeforeEnds) { - QDEBUG() << " removing edge" << vertices.prevPos(v); - int pos = scanline.findEdge(vertices.prevPos(v)); - if (pos == -1) - continue; - scanline.edges[pos]->mark = true; - if (pos > 0) - scanline.edges[pos - 1]->intersect_right = true; - if (pos < scanline.size - 1) - scanline.edges[pos + 1]->intersect_left = true; - scanline.removeAt(pos); - } - if (v->flags & LineAfterEnds) { - QDEBUG() << " removing edge" << vertices.position(v); - int pos = scanline.findEdge(vertices.position(v)); - if (pos == -1) - continue; - scanline.edges[pos]->mark = true; - if (pos > 0) - scanline.edges[pos - 1]->intersect_right = true; - if (pos < scanline.size - 1) - scanline.edges[pos + 1]->intersect_left = true; - scanline.removeAt(pos); - } - ++cv; - } -} - -void QTessellatorPrivate::addEdges() -{ - while (currentVertex < vertices.nPoints) { - const Vertex *v = vertices.sorted[currentVertex]; - if (v->y > y) - break; - if (v->flags & LineBeforeStarts) { - // add new edge - int start = vertices.prevPos(v); - Edge e(vertices, start); - int pos = scanline.findEdgePosition(e); - QDEBUG() << " adding edge" << start << "at position" << pos; - scanline.insert(pos, e); - if (!mark_clever || !(v->flags & LineAfterEnds)) { - if (pos > 0) - scanline.edges[pos - 1]->mark = true; - if (pos < scanline.size - 1) - scanline.edges[pos + 1]->mark = true; - } - } - if (v->flags & LineAfterStarts) { - Edge e(vertices, vertices.position(v)); - int pos = scanline.findEdgePosition(e); - QDEBUG() << " adding edge" << vertices.position(v) << "at position" << pos; - scanline.insert(pos, e); - if (!mark_clever || !(v->flags & LineBeforeEnds)) { - if (pos > 0) - scanline.edges[pos - 1]->mark = true; - if (pos < scanline.size - 1) - scanline.edges[pos + 1]->mark = true; - } - } - if (v->flags & LineAfterHorizontal) { - int pos1 = scanline.findEdgePosition(v->x, v->y); - const Vertex *next = vertices.next(v); - Q_ASSERT(v->y == next->y); - int pos2 = scanline.findEdgePosition(next->x, next->y); - if (pos2 < pos1) - qSwap(pos1, pos2); - if (pos1 > 0) - --pos1; - if (pos2 == scanline.size) - --pos2; - //QDEBUG() << "marking horizontal edge from " << pos1 << "to" << pos2; - scanline.markEdges(pos1, pos2); - } - ++currentVertex; - } -} - -#ifdef DEBUG -static void checkLinkChain(const QTessellatorPrivate::Intersections &intersections, - QTessellatorPrivate::Intersection i) -{ -// qDebug() << " Link chain: "; - int end = i.edge; - while (1) { - QTessellatorPrivate::IntersectionLink l = intersections.value(i); -// qDebug() << " " << i.edge << "next=" << l.next << "prev=" << l.prev; - if (l.next == end) - break; - Q_ASSERT(l.next != -1); - Q_ASSERT(l.prev != -1); - - QTessellatorPrivate::Intersection i2 = i; - i2.edge = l.next; - QTessellatorPrivate::IntersectionLink l2 = intersections.value(i2); - - Q_ASSERT(l2.next != -1); - Q_ASSERT(l2.prev != -1); - Q_ASSERT(l.next == i2.edge); - Q_ASSERT(l2.prev == i.edge); - i = i2; - } -} -#endif - -bool QTessellatorPrivate::edgeInChain(Intersection i, int edge) -{ - int end = i.edge; - while (1) { - if (i.edge == edge) - return true; - IntersectionLink l = intersections.value(i); - if (l.next == end) - break; - Q_ASSERT(l.next != -1); - Q_ASSERT(l.prev != -1); - - Intersection i2 = i; - i2.edge = l.next; - -#ifndef QT_NO_DEBUG - IntersectionLink l2 = intersections.value(i2); - Q_ASSERT(l2.next != -1); - Q_ASSERT(l2.prev != -1); - Q_ASSERT(l.next == i2.edge); - Q_ASSERT(l2.prev == i.edge); -#endif - i = i2; - } - return false; -} - - -void QTessellatorPrivate::addIntersection(const Edge *e1, const Edge *e2) -{ - const IntersectionLink emptyLink = {-1, -1}; - - int next = vertices.nextPos(vertices[e1->edge]); - if (e2->edge == next) - return; - int prev = vertices.prevPos(vertices[e1->edge]); - if (e2->edge == prev) - return; - - Q27Dot5 yi; - bool det_positive; - bool isect = e1->intersect(*e2, &yi, &det_positive); - QDEBUG("checking edges %d and %d", e1->edge, e2->edge); - if (!isect) { - QDEBUG() << " no intersection"; - return; - } - - // don't emit an intersection if it's at the start of a line segment or above us - if (yi <= y) { - if (!det_positive) - return; - QDEBUG() << " ----->>>>>> WRONG ORDER!"; - yi = y; - } - QDEBUG() << " between edges " << e1->edge << "and" << e2->edge << "at point (" - << Q27Dot5ToDouble(yi) << ')'; - - Intersection i1; - i1.y = yi; - i1.edge = e1->edge; - IntersectionLink link1 = intersections.value(i1, emptyLink); - Intersection i2; - i2.y = yi; - i2.edge = e2->edge; - IntersectionLink link2 = intersections.value(i2, emptyLink); - - // new pair of edges - if (link1.next == -1 && link2.next == -1) { - link1.next = link1.prev = i2.edge; - link2.next = link2.prev = i1.edge; - } else if (link1.next == i2.edge || link1.prev == i2.edge - || link2.next == i1.edge || link2.prev == i1.edge) { -#ifdef DEBUG - checkLinkChain(intersections, i1); - checkLinkChain(intersections, i2); - Q_ASSERT(edgeInChain(i1, i2.edge)); -#endif - return; - } else if (link1.next == -1 || link2.next == -1) { - if (link2.next == -1) { - qSwap(i1, i2); - qSwap(link1, link2); - } - Q_ASSERT(link1.next == -1); -#ifdef DEBUG - checkLinkChain(intersections, i2); -#endif - // only i2 in list - link1.next = i2.edge; - link1.prev = link2.prev; - link2.prev = i1.edge; - Intersection other; - other.y = yi; - other.edge = link1.prev; - IntersectionLink link = intersections.value(other, emptyLink); - Q_ASSERT(link.next == i2.edge); - Q_ASSERT(link.prev != -1); - link.next = i1.edge; - intersections.insert(other, link); - } else { - bool connected = edgeInChain(i1, i2.edge); - if (connected) - return; -#ifdef DEBUG - checkLinkChain(intersections, i1); - checkLinkChain(intersections, i2); -#endif - // both already in some list. Have to make sure they are connected - // this can be done by cutting open the ring(s) after the two eges and - // connecting them again - Intersection other1; - other1.y = yi; - other1.edge = link1.next; - IntersectionLink linko1 = intersections.value(other1, emptyLink); - Intersection other2; - other2.y = yi; - other2.edge = link2.next; - IntersectionLink linko2 = intersections.value(other2, emptyLink); - - linko1.prev = i2.edge; - link2.next = other1.edge; - - linko2.prev = i1.edge; - link1.next = other2.edge; - intersections.insert(other1, linko1); - intersections.insert(other2, linko2); - } - intersections.insert(i1, link1); - intersections.insert(i2, link2); -#ifdef DEBUG - checkLinkChain(intersections, i1); - checkLinkChain(intersections, i2); - Q_ASSERT(edgeInChain(i1, i2.edge)); -#endif - return; - -} - - -void QTessellatorPrivate::addIntersections() -{ - if (scanline.size) { - QDEBUG() << "INTERSECTIONS"; - // check marked edges for intersections -#ifdef DEBUG - for (int i = 0; i < scanline.size; ++i) { - Edge *e = scanline.edges[i]; - QDEBUG() << " " << i << e->edge << "isect=(" << e->intersect_left << e->intersect_right - << ')'; - } -#endif - - for (int i = 0; i < scanline.size - 1; ++i) { - Edge *e1 = scanline.edges[i]; - Edge *e2 = scanline.edges[i + 1]; - // check for intersection - if (e1->intersect_right || e2->intersect_left) - addIntersection(e1, e2); - } - } -#if 0 - if (intersections.constBegin().key().y == y) { - QDEBUG() << "----------------> intersection on same line"; - scanline.clearMarks(); - scanline.processIntersections(y, &intersections); - goto redo; - } -#endif -} - - -QTessellator::QTessellator() -{ - d = new QTessellatorPrivate; -} - -QTessellator::~QTessellator() -{ - delete d; -} - -void QTessellator::setWinding(bool w) -{ - d->winding = w; -} - - -QRectF QTessellator::tessellate(const QPointF *points, int nPoints) -{ - Q_ASSERT(points[0] == points[nPoints-1]); - --nPoints; - -#ifdef DEBUG - QDEBUG()<< "POINTS:"; - for (int i = 0; i < nPoints; ++i) { - QDEBUG() << points[i]; - } -#endif - - // collect edges and calculate bounds - d->vertices.nPoints = nPoints; - d->vertices.init(nPoints); - - int maxActiveEdges = 0; - QRectF br = d->collectAndSortVertices(points, &maxActiveEdges); - d->cancelCoincidingEdges(); - -#ifdef DEBUG - QDEBUG() << "nPoints = " << nPoints << "using " << d->vertices.nPoints; - QDEBUG()<< "VERTICES:"; - for (int i = 0; i < d->vertices.nPoints; ++i) { - QDEBUG() << " " << i << ": " - << "point=" << d->vertices.position(d->vertices.sorted[i]) - << "flags=" << d->vertices.sorted[i]->flags - << "pos=(" << Q27Dot5ToDouble(d->vertices.sorted[i]->x) << '/' - << Q27Dot5ToDouble(d->vertices.sorted[i]->y) << ')'; - } -#endif - - d->scanline.init(maxActiveEdges); - d->y = INT_MIN/256; - d->currentVertex = 0; - - while (d->currentVertex < d->vertices.nPoints) { - d->scanline.clearMarks(); - - d->y = d->vertices.sorted[d->currentVertex]->y; - if (!d->intersections.isEmpty()) - d->y = qMin(d->y, d->intersections.constBegin().key().y); - - QDEBUG()<< "===== SCANLINE: y =" << Q27Dot5ToDouble(d->y) << " ====="; - - d->scanline.prepareLine(); - d->processIntersections(); - d->removeEdges(); - d->addEdges(); - d->addIntersections(); - d->emitEdges(this); - d->scanline.lineDone(); - -#ifdef DEBUG - QDEBUG()<< "===== edges:"; - for (int i = 0; i < d->scanline.size; ++i) { - QDEBUG() << " " << d->scanline.edges[i]->edge - << "p0= (" << Q27Dot5ToDouble(d->scanline.edges[i]->v0->x) - << '/' << Q27Dot5ToDouble(d->scanline.edges[i]->v0->y) - << ") p1= (" << Q27Dot5ToDouble(d->scanline.edges[i]->v1->x) - << '/' << Q27Dot5ToDouble(d->scanline.edges[i]->v1->y) << ')' - << "x=" << Q27Dot5ToDouble(d->scanline.edges[i]->positionAt(d->y)) - << "isLeftOfNext=" - << ((i < d->scanline.size - 1) - ? d->scanline.edges[i]->isLeftOf(*d->scanline.edges[i+1], d->y) - : true); - } -#endif -} - - d->scanline.done(); - d->intersections.clear(); - return br; -} - -// tessellates the given convex polygon -void QTessellator::tessellateConvex(const QPointF *points, int nPoints) -{ - Q_ASSERT(points[0] == points[nPoints-1]); - --nPoints; - - d->vertices.nPoints = nPoints; - d->vertices.init(nPoints); - - for (int i = 0; i < nPoints; ++i) { - d->vertices[i]->x = FloatToQ27Dot5(points[i].x()); - d->vertices[i]->y = FloatToQ27Dot5(points[i].y()); - } - - int left = 0, right = 0; - - int top = 0; - for (int i = 1; i < nPoints; ++i) { - if (d->vertices[i]->y < d->vertices[top]->y) - top = i; - } - - left = (top + nPoints - 1) % nPoints; - right = (top + 1) % nPoints; - - while (d->vertices[left]->x == d->vertices[top]->x && d->vertices[left]->y == d->vertices[top]->y && left != right) - left = (left + nPoints - 1) % nPoints; - - while (d->vertices[right]->x == d->vertices[top]->x && d->vertices[right]->y == d->vertices[top]->y && left != right) - right = (right + 1) % nPoints; - - if (left == right) - return; - - int dir = 1; - - Vertex dLeft = { d->vertices[top]->x - d->vertices[left]->x, - d->vertices[top]->y - d->vertices[left]->y }; - - Vertex dRight = { d->vertices[right]->x - d->vertices[top]->x, - d->vertices[right]->y - d->vertices[top]->y }; - - Q27Dot5 cross = dLeft.x * dRight.y - dLeft.y * dRight.x; - - // flip direction if polygon is clockwise - if (cross < 0 || (cross == 0 && dLeft.x > 0)) { - qSwap(left, right); - dir = -1; - } - - Vertex *lastLeft = d->vertices[top]; - Vertex *lastRight = d->vertices[top]; - - QTessellator::Trapezoid trap; - - while (lastLeft->y == d->vertices[left]->y && left != right) { - lastLeft = d->vertices[left]; - left = (left + nPoints - dir) % nPoints; - } - - while (lastRight->y == d->vertices[right]->y && left != right) { - lastRight = d->vertices[right]; - right = (right + nPoints + dir) % nPoints; - } - - while (true) { - trap.top = qMax(lastRight->y, lastLeft->y); - trap.bottom = qMin(d->vertices[left]->y, d->vertices[right]->y); - trap.topLeft = lastLeft; - trap.topRight = lastRight; - trap.bottomLeft = d->vertices[left]; - trap.bottomRight = d->vertices[right]; - - if (trap.bottom > trap.top) - addTrap(trap); - - if (left == right) - break; - - if (d->vertices[right]->y < d->vertices[left]->y) { - do { - lastRight = d->vertices[right]; - right = (right + nPoints + dir) % nPoints; - } - while (lastRight->y == d->vertices[right]->y && left != right); - } else { - do { - lastLeft = d->vertices[left]; - left = (left + nPoints - dir) % nPoints; - } - while (lastLeft->y == d->vertices[left]->y && left != right); - } - } -} - -// tessellates the stroke of the line from a_ to b_ with the given width and a flat cap -void QTessellator::tessellateRect(const QPointF &a_, const QPointF &b_, qreal width) -{ - Vertex a = { FloatToQ27Dot5(a_.x()), FloatToQ27Dot5(a_.y()) }; - Vertex b = { FloatToQ27Dot5(b_.x()), FloatToQ27Dot5(b_.y()) }; - - QPointF pa = a_, pb = b_; - - if (a.y > b.y) { - qSwap(a, b); - qSwap(pa, pb); - } - - Vertex delta = { b.x - a.x, b.y - a.y }; - - if (delta.x == 0 && delta.y == 0) - return; - - qreal hw = 0.5 * width; - - if (delta.x == 0) { - Q27Dot5 halfWidth = FloatToQ27Dot5(hw); - - if (halfWidth == 0) - return; - - Vertex topLeft = { a.x - halfWidth, a.y }; - Vertex topRight = { a.x + halfWidth, a.y }; - Vertex bottomLeft = { a.x - halfWidth, b.y }; - Vertex bottomRight = { a.x + halfWidth, b.y }; - - QTessellator::Trapezoid trap = { topLeft.y, bottomLeft.y, &topLeft, &bottomLeft, &topRight, &bottomRight }; - addTrap(trap); - } else if (delta.y == 0) { - Q27Dot5 halfWidth = FloatToQ27Dot5(hw); - - if (halfWidth == 0) - return; - - if (a.x > b.x) - qSwap(a.x, b.x); - - Vertex topLeft = { a.x, a.y - halfWidth }; - Vertex topRight = { b.x, a.y - halfWidth }; - Vertex bottomLeft = { a.x, a.y + halfWidth }; - Vertex bottomRight = { b.x, a.y + halfWidth }; - - QTessellator::Trapezoid trap = { topLeft.y, bottomLeft.y, &topLeft, &bottomLeft, &topRight, &bottomRight }; - addTrap(trap); - } else { - QPointF perp(pb.y() - pa.y(), pa.x() - pb.x()); - qreal length = qSqrt(perp.x() * perp.x() + perp.y() * perp.y()); - - if (qFuzzyIsNull(length)) - return; - - // need the half of the width - perp *= hw / length; - - QPointF pta = pa + perp; - QPointF ptb = pa - perp; - QPointF ptc = pb - perp; - QPointF ptd = pb + perp; - - Vertex ta = { FloatToQ27Dot5(pta.x()), FloatToQ27Dot5(pta.y()) }; - Vertex tb = { FloatToQ27Dot5(ptb.x()), FloatToQ27Dot5(ptb.y()) }; - Vertex tc = { FloatToQ27Dot5(ptc.x()), FloatToQ27Dot5(ptc.y()) }; - Vertex td = { FloatToQ27Dot5(ptd.x()), FloatToQ27Dot5(ptd.y()) }; - - if (ta.y < tb.y) { - if (tb.y < td.y) { - QTessellator::Trapezoid top = { ta.y, tb.y, &ta, &tb, &ta, &td }; - QTessellator::Trapezoid bottom = { td.y, tc.y, &tb, &tc, &td, &tc }; - addTrap(top); - addTrap(bottom); - - QTessellator::Trapezoid middle = { tb.y, td.y, &tb, &tc, &ta, &td }; - addTrap(middle); - } else { - QTessellator::Trapezoid top = { ta.y, td.y, &ta, &tb, &ta, &td }; - QTessellator::Trapezoid bottom = { tb.y, tc.y, &tb, &tc, &td, &tc }; - addTrap(top); - addTrap(bottom); - - if (tb.y != td.y) { - QTessellator::Trapezoid middle = { td.y, tb.y, &ta, &tb, &td, &tc }; - addTrap(middle); - } - } - } else { - if (ta.y < tc.y) { - QTessellator::Trapezoid top = { tb.y, ta.y, &tb, &tc, &tb, &ta }; - QTessellator::Trapezoid bottom = { tc.y, td.y, &tc, &td, &ta, &td }; - addTrap(top); - addTrap(bottom); - - QTessellator::Trapezoid middle = { ta.y, tc.y, &tb, &tc, &ta, &td }; - addTrap(middle); - } else { - QTessellator::Trapezoid top = { tb.y, tc.y, &tb, &tc, &tb, &ta }; - QTessellator::Trapezoid bottom = { ta.y, td.y, &tc, &td, &ta, &td }; - addTrap(top); - addTrap(bottom); - - if (ta.y != tc.y) { - QTessellator::Trapezoid middle = { tc.y, ta.y, &tc, &td, &tb, &ta }; - addTrap(middle); - } - } - } - } -} - -QT_END_NAMESPACE diff --git a/src/plugins/platforms/xcb/nativepainting/qtessellator_p.h b/src/plugins/platforms/xcb/nativepainting/qtessellator_p.h deleted file mode 100644 index ba1d3a971a3..00000000000 --- a/src/plugins/platforms/xcb/nativepainting/qtessellator_p.h +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (C) 2018 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 - -#pragma once - -#include <QPoint> -#include <QRect> - -QT_BEGIN_NAMESPACE - -class QTessellatorPrivate; - -typedef int Q27Dot5; -#define Q27Dot5ToDouble(i) ((i)/32.) -#define FloatToQ27Dot5(i) (int)((i) * 32) -#define IntToQ27Dot5(i) ((i) << 5) -#define Q27Dot5ToXFixed(i) ((i) << 11) -#define Q27Dot5Factor 32 - -class QTessellator { -public: - QTessellator(); - virtual ~QTessellator(); - - QRectF tessellate(const QPointF *points, int nPoints); - void tessellateConvex(const QPointF *points, int nPoints); - void tessellateRect(const QPointF &a, const QPointF &b, qreal width); - - void setWinding(bool w); - - struct Vertex { - Q27Dot5 x; - Q27Dot5 y; - }; - struct Trapezoid { - Q27Dot5 top; - Q27Dot5 bottom; - const Vertex *topLeft; - const Vertex *bottomLeft; - const Vertex *topRight; - const Vertex *bottomRight; - }; - virtual void addTrap(const Trapezoid &trap) = 0; - -private: - friend class QTessellatorPrivate; - QTessellatorPrivate *d; -}; - -QT_END_NAMESPACE diff --git a/src/plugins/platforms/xcb/nativepainting/qxcbnativepainting.cpp b/src/plugins/platforms/xcb/nativepainting/qxcbnativepainting.cpp deleted file mode 100644 index 23155ef2e62..00000000000 --- a/src/plugins/platforms/xcb/nativepainting/qxcbnativepainting.cpp +++ /dev/null @@ -1,288 +0,0 @@ -// Copyright (C) 2018 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 <QtCore/qrandom.h> - -#include "qxcbconnection.h" -#include "qcolormap_x11_p.h" -#include "qxcbnativepainting.h" -#include "qt_x11_p.h" - -QT_BEGIN_NAMESPACE - -QXcbX11Data *qt_x11Data = nullptr; - -void qt_xcb_native_x11_info_init(QXcbConnection *conn) -{ - qt_x11Data = new QXcbX11Data; - X11->display = static_cast<Display *>(conn->xlib_display()); - X11->defaultScreen = DefaultScreen(X11->display); - X11->screenCount = ScreenCount(X11->display); - - X11->screens = new QX11InfoData[X11->screenCount]; - X11->argbVisuals = new Visual *[X11->screenCount]; - X11->argbColormaps = new Colormap[X11->screenCount]; - - for (int s = 0; s < X11->screenCount; s++) { - QX11InfoData *screen = X11->screens + s; - //screen->ref = 1; // ensures it doesn't get deleted - screen->screen = s; - - int widthMM = DisplayWidthMM(X11->display, s); - if (widthMM != 0) { - screen->dpiX = (DisplayWidth(X11->display, s) * 254 + widthMM * 5) / (widthMM * 10); - } else { - screen->dpiX = 72; - } - - int heightMM = DisplayHeightMM(X11->display, s); - if (heightMM != 0) { - screen->dpiY = (DisplayHeight(X11->display, s) * 254 + heightMM * 5) / (heightMM * 10); - } else { - screen->dpiY = 72; - } - - X11->argbVisuals[s] = 0; - X11->argbColormaps[s] = 0; - } - - X11->use_xrender = conn->hasXRender() && !qEnvironmentVariableIsSet("QT_XCB_NATIVE_PAINTING_NO_XRENDER"); - -#if QT_CONFIG(xrender) - memset(X11->solid_fills, 0, sizeof(X11->solid_fills)); - for (int i = 0; i < X11->solid_fill_count; ++i) - X11->solid_fills[i].screen = -1; - memset(X11->pattern_fills, 0, sizeof(X11->pattern_fills)); - for (int i = 0; i < X11->pattern_fill_count; ++i) - X11->pattern_fills[i].screen = -1; -#endif - - QXcbColormap::initialize(); - -#if QT_CONFIG(xrender) - if (X11->use_xrender) { - // XRender is supported, let's see if we have a PictFormat for the - // default visual - XRenderPictFormat *format = - XRenderFindVisualFormat(X11->display, - (Visual *) QXcbX11Info::appVisual(X11->defaultScreen)); - - if (!format) { - X11->use_xrender = false; - } - } -#endif // QT_CONFIG(xrender) -} - -QList<XRectangle> qt_region_to_xrectangles(const QRegion &r) -{ - const int numRects = r.rectCount(); - const auto input = r.begin(); - QList<XRectangle> output(numRects); - for (int i = 0; i < numRects; ++i) { - const QRect &in = input[i]; - XRectangle &out = output[i]; - out.x = qMax(SHRT_MIN, in.x()); - out.y = qMax(SHRT_MIN, in.y()); - out.width = qMin((int)USHRT_MAX, in.width()); - out.height = qMin((int)USHRT_MAX, in.height()); - } - return output; -} - -class QXcbX11InfoData : public QSharedData, public QX11InfoData -{}; - -QXcbX11Info::QXcbX11Info() - : d(nullptr) -{} - -QXcbX11Info::~QXcbX11Info() -{} - -QXcbX11Info::QXcbX11Info(const QXcbX11Info &other) - : d(other.d) -{} - -QXcbX11Info &QXcbX11Info::operator=(const QXcbX11Info &other) -{ - d = other.d; - return *this; -} - -QXcbX11Info QXcbX11Info::fromScreen(int screen) -{ - QXcbX11InfoData *xd = new QXcbX11InfoData; - xd->screen = screen; - xd->depth = QXcbX11Info::appDepth(screen); - xd->cells = QXcbX11Info::appCells(screen); - xd->colormap = QXcbX11Info::appColormap(screen); - xd->defaultColormap = QXcbX11Info::appDefaultColormap(screen); - xd->visual = (Visual *)QXcbX11Info::appVisual(screen); - xd->defaultVisual = QXcbX11Info::appDefaultVisual(screen); - - QXcbX11Info info; - info.d = xd; - return info; -} - -void QXcbX11Info::setDepth(int depth) -{ - if (!d) - *this = fromScreen(appScreen()); - - d->depth = depth; -} - -Display *QXcbX11Info::display() -{ - return X11 ? X11->display : 0; -} - -int QXcbX11Info::screen() const -{ - return d ? d->screen : QXcbX11Info::appScreen(); -} - -int QXcbX11Info::depth() const -{ - return d ? d->depth : QXcbX11Info::appDepth(); -} - -Colormap QXcbX11Info::colormap() const -{ - return d ? d->colormap : QXcbX11Info::appColormap(); -} - -void *QXcbX11Info::visual() const -{ - return d ? d->visual : QXcbX11Info::appVisual(); -} - -void QXcbX11Info::setVisual(void *visual) -{ - if (!d) - *this = fromScreen(appScreen()); - - d->visual = (Visual *) visual; -} - -int QXcbX11Info::appScreen() -{ - return X11 ? X11->defaultScreen : 0; -} - -int QXcbX11Info::appDepth(int screen) -{ - return X11 ? X11->screens[screen == -1 ? X11->defaultScreen : screen].depth : 32; -} - -int QXcbX11Info::appCells(int screen) -{ - return X11 ? X11->screens[screen == -1 ? X11->defaultScreen : screen].cells : 0; -} - -Colormap QXcbX11Info::appColormap(int screen) -{ - return X11 ? X11->screens[screen == -1 ? X11->defaultScreen : screen].colormap : 0; -} - -void *QXcbX11Info::appVisual(int screen) -{ - return X11 ? X11->screens[screen == -1 ? X11->defaultScreen : screen].visual : 0; -} - -Window QXcbX11Info::appRootWindow(int screen) -{ - return X11 ? RootWindow(X11->display, screen == -1 ? X11->defaultScreen : screen) : 0; -} - -bool QXcbX11Info::appDefaultColormap(int screen) -{ - return X11 ? X11->screens[screen == -1 ? X11->defaultScreen : screen].defaultColormap : true; -} - -bool QXcbX11Info::appDefaultVisual(int screen) -{ - return X11 ? X11->screens[screen == -1 ? X11->defaultScreen : screen].defaultVisual : true; -} - -int QXcbX11Info::appDpiX(int screen) -{ - if (!X11) - return 75; - if (screen < 0) - screen = X11->defaultScreen; - if (screen > X11->screenCount) - return 0; - return X11->screens[screen].dpiX; -} - -int QXcbX11Info::appDpiY(int screen) -{ - if (!X11) - return 75; - if (screen < 0) - screen = X11->defaultScreen; - if (screen > X11->screenCount) - return 0; - return X11->screens[screen].dpiY; -} - -#if QT_CONFIG(xrender) -Picture QXcbX11Data::getSolidFill(int screen, const QColor &c) -{ - if (!X11->use_xrender) - return XNone; - - XRenderColor color = preMultiply(c); - for (int i = 0; i < X11->solid_fill_count; ++i) { - if (X11->solid_fills[i].screen == screen - && X11->solid_fills[i].color.alpha == color.alpha - && X11->solid_fills[i].color.red == color.red - && X11->solid_fills[i].color.green == color.green - && X11->solid_fills[i].color.blue == color.blue) - return X11->solid_fills[i].picture; - } - // none found, replace one - int i = QRandomGenerator::global()->generate() % 16; - - if (X11->solid_fills[i].screen != screen && X11->solid_fills[i].picture) { - XRenderFreePicture (X11->display, X11->solid_fills[i].picture); - X11->solid_fills[i].picture = 0; - } - - if (!X11->solid_fills[i].picture) { - Pixmap pixmap = XCreatePixmap (X11->display, RootWindow (X11->display, screen), 1, 1, 32); - XRenderPictureAttributes attrs; - attrs.repeat = True; - X11->solid_fills[i].picture = XRenderCreatePicture (X11->display, pixmap, - XRenderFindStandardFormat(X11->display, PictStandardARGB32), - CPRepeat, &attrs); - XFreePixmap (X11->display, pixmap); - } - - X11->solid_fills[i].color = color; - X11->solid_fills[i].screen = screen; - XRenderFillRectangle (X11->display, PictOpSrc, X11->solid_fills[i].picture, &color, 0, 0, 1, 1); - return X11->solid_fills[i].picture; -} - -XRenderColor QXcbX11Data::preMultiply(const QColor &c) -{ - XRenderColor color; - const uint A = c.alpha(), - R = c.red(), - G = c.green(), - B = c.blue(); - color.alpha = (A | A << 8); - color.red = (R | R << 8) * color.alpha / 0x10000; - color.green = (G | G << 8) * color.alpha / 0x10000; - color.blue = (B | B << 8) * color.alpha / 0x10000; - return color; -} -#endif // QT_CONFIG(xrender) - - -QT_END_NAMESPACE diff --git a/src/plugins/platforms/xcb/nativepainting/qxcbnativepainting.h b/src/plugins/platforms/xcb/nativepainting/qxcbnativepainting.h deleted file mode 100644 index dac9b101c43..00000000000 --- a/src/plugins/platforms/xcb/nativepainting/qxcbnativepainting.h +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (C) 2018 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 - -#pragma once - -#include <QSharedDataPointer> -#include "qt_x11_p.h" - -typedef struct _FcPattern FcPattern; -typedef unsigned long XID; -typedef XID Colormap; -typedef XID Window; -typedef struct _XDisplay Display; - -QT_BEGIN_NAMESPACE - -class QXcbConnection; -class QPixmap; - -void qt_xcb_native_x11_info_init(QXcbConnection *conn); -QList<XRectangle> qt_region_to_xrectangles(const QRegion &r); - -class QXcbX11InfoData; -class QXcbX11Info -{ -public: - QXcbX11Info(); - ~QXcbX11Info(); - QXcbX11Info(const QXcbX11Info &other); - QXcbX11Info &operator=(const QXcbX11Info &other); - - static QXcbX11Info fromScreen(int screen); - static Display *display(); - - int depth() const; - void setDepth(int depth); - - int screen() const; - Colormap colormap() const; - - void *visual() const; - void setVisual(void *visual); - - static int appScreen(); - static int appDepth(int screen = -1); - static int appCells(int screen = -1); - static Colormap appColormap(int screen = -1); - static void *appVisual(int screen = -1); - static Window appRootWindow(int screen = -1); - static bool appDefaultColormap(int screen = -1); - static bool appDefaultVisual(int screen = -1); - static int appDpiX(int screen = -1); - static int appDpiY(int screen = -1); - -private: - QSharedDataPointer<QXcbX11InfoData> d; - - friend class QX11PaintEngine; - friend class QX11PlatformPixmap; - friend void qt_x11SetScreen(QPixmap &pixmap, int screen); -}; - -QT_END_NAMESPACE diff --git a/src/plugins/platforms/xcb/qxcbimage.cpp b/src/plugins/platforms/xcb/qxcbimage.cpp index 5b78c2c08ab..4c978152975 100644 --- a/src/plugins/platforms/xcb/qxcbimage.cpp +++ b/src/plugins/platforms/xcb/qxcbimage.cpp @@ -80,12 +80,6 @@ bool qt_xcb_imageFormatForVisual(QXcbConnection *connection, uint8_t depth, cons *imageFormat = QImage::Format_Grayscale8; return true; } -#if QT_CONFIG(xcb_native_painting) - if (QXcbIntegration::instance() && QXcbIntegration::instance()->nativePaintingEnabled()) { - *imageFormat = QImage::Format_Indexed8; - return true; - } -#endif return false; } diff --git a/src/plugins/platforms/xcb/qxcbintegration.cpp b/src/plugins/platforms/xcb/qxcbintegration.cpp index c5c1fb1e638..d3a31dd35c7 100644 --- a/src/plugins/platforms/xcb/qxcbintegration.cpp +++ b/src/plugins/platforms/xcb/qxcbintegration.cpp @@ -37,11 +37,6 @@ #include <X11/Xlib.h> #undef register #endif -#if QT_CONFIG(xcb_native_painting) -#include "qxcbnativepainting.h" -#include "qpixmap_x11_p.h" -#include "qbackingstore_x11_p.h" -#endif #include <qpa/qplatforminputcontextfactory_p.h> #include <private/qgenericunixtheme_p.h> @@ -180,13 +175,6 @@ QXcbIntegration::QXcbIntegration(const QStringList ¶meters, int &argc, char m_services->setConnection(m_connection); m_fontDatabase.reset(new QGenericUnixFontDatabase()); - -#if QT_CONFIG(xcb_native_painting) - if (nativePaintingEnabled()) { - qCDebug(lcQpaXcb, "QXCB USING NATIVE PAINTING"); - qt_xcb_native_x11_info_init(connection()); - } -#endif } QXcbIntegration::~QXcbIntegration() @@ -198,11 +186,6 @@ QXcbIntegration::~QXcbIntegration() QPlatformPixmap *QXcbIntegration::createPlatformPixmap(QPlatformPixmap::PixelType type) const { -#if QT_CONFIG(xcb_native_painting) - if (nativePaintingEnabled()) - return new QX11PlatformPixmap(type); -#endif - return QPlatformIntegration::createPlatformPixmap(type); } @@ -281,10 +264,6 @@ QPlatformBackingStore *QXcbIntegration::createPlatformBackingStore(QWindow *wind const bool isTrayIconWindow = QXcbWindow::isTrayIconWindow(window); if (isTrayIconWindow) { backingStore = new QXcbSystemTrayBackingStore(window); -#if QT_CONFIG(xcb_native_painting) - } else if (nativePaintingEnabled()) { - backingStore = new QXcbNativeBackingStore(window); -#endif } else { backingStore = new QXcbBackingStore(window); } @@ -576,16 +555,6 @@ void QXcbIntegration::beep() const xcb_flush(connection); } -bool QXcbIntegration::nativePaintingEnabled() const -{ -#if QT_CONFIG(xcb_native_painting) - static bool enabled = qEnvironmentVariableIsSet("QT_XCB_NATIVE_PAINTING"); - return enabled; -#else - return false; -#endif -} - #if QT_CONFIG(vulkan) QPlatformVulkanInstance *QXcbIntegration::createPlatformVulkanInstance(QVulkanInstance *instance) const { diff --git a/src/plugins/platforms/xcb/qxcbintegration.h b/src/plugins/platforms/xcb/qxcbintegration.h index 10cc67af55f..041908e4552 100644 --- a/src/plugins/platforms/xcb/qxcbintegration.h +++ b/src/plugins/platforms/xcb/qxcbintegration.h @@ -94,8 +94,6 @@ public: void beep() const override; - bool nativePaintingEnabled() const; - #if QT_CONFIG(vulkan) QPlatformVulkanInstance *createPlatformVulkanInstance(QVulkanInstance *instance) const override; #endif diff --git a/src/plugins/styles/modernwindows/qwindows11style.cpp b/src/plugins/styles/modernwindows/qwindows11style.cpp index 0ed8e2890d9..410750d941d 100644 --- a/src/plugins/styles/modernwindows/qwindows11style.cpp +++ b/src/plugins/styles/modernwindows/qwindows11style.cpp @@ -109,6 +109,7 @@ inline ControlState calcControlState(const QStyleOption *option) #define More u"\uE712"_s #define Help u"\uE897"_s +#define Clear u"\uE894"_s template <typename R, typename P, typename B> static inline void drawRoundedRect(QPainter *p, R &&rect, P &&pen, B &&brush) @@ -580,21 +581,18 @@ void QWindows11Style::drawComplexControl(ComplexControl control, const QStyleOpt if (combobox->frame) drawLineEditFrame(painter, frameRect, combobox, combobox->editable); - const bool isMouseOver = state & State_MouseOver; const bool hasFocus = state & State_HasFocus; - if (isMouseOver && !hasFocus && !highContrastTheme) - drawRoundedRect(painter, frameRect, Qt::NoPen, winUI3Color(subtleHighlightColor)); + QStyleOption opt(*option); + opt.state.setFlag(QStyle::State_On, false); + drawRoundedRect(painter, frameRect, Qt::NoPen, controlFillBrush(&opt, ControlType::Control)); if (sub & SC_ComboBoxArrow) { QRectF rect = proxy()->subControlRect(CC_ComboBox, option, SC_ComboBoxArrow, widget).adjusted(4, 0, -4, 1); painter->setFont(d->assetFont); - painter->setPen(combobox->palette.text().color()); + painter->setPen(controlTextColor(option)); painter->drawText(rect, Qt::AlignCenter, ChevronDownMed); } - if (state & State_HasFocus) { - drawPrimitive(PE_FrameFocusRect, option, painter, widget); - } - if (state & State_KeyboardFocusChange && state & State_HasFocus) { + if (state & State_KeyboardFocusChange && hasFocus) { QStyleOptionFocusRect fropt; fropt.QStyleOption::operator=(*option); proxy()->drawPrimitive(PE_FrameFocusRect, &fropt, painter, widget); @@ -1048,12 +1046,27 @@ void QWindows11Style::drawPrimitive(PrimitiveElement element, const QStyleOption if (rect.width() <= 0) break; - painter->setPen(Qt::NoPen); - if (vopt->features & QStyleOptionViewItem::Alternate) - painter->setBrush(vopt->palette.alternateBase()); - else - painter->setBrush(vopt->palette.base()); - painter->drawRect(rect); + if (vopt->features & QStyleOptionViewItem::Alternate) { + QPalette::ColorGroup cg = + (widget ? widget->isEnabled() : (vopt->state & QStyle::State_Enabled)) + ? QPalette::Normal + : QPalette::Disabled; + if (cg == QPalette::Normal && !(vopt->state & QStyle::State_Active)) + cg = QPalette::Inactive; + painter->fillRect(rect, option->palette.brush(cg, QPalette::AlternateBase)); + } + + if (option->state & State_Selected && !highContrastTheme) { + // keep in sync with CE_ItemViewItem QListView indicator painting + const auto col = option->palette.accent().color(); + painter->setBrush(col); + painter->setPen(col); + const auto xPos = isRtl ? rect.right() - 4.5f : rect.left() + 3.5f; + const auto yOfs = rect.height() / 4.; + QRectF r(QPointF(xPos, rect.y() + yOfs), + QPointF(xPos + 1, rect.y() + rect.height() - yOfs)); + painter->drawRoundedRect(r, 1, 1); + } const bool isTreeDecoration = vopt->features.testFlag( QStyleOptionViewItem::IsDecorationForRootColumn); @@ -1182,11 +1195,14 @@ void QWindows11Style::drawControl(ControlElement element, const QStyleOption *op painter->setRenderHint(QPainter::Antialiasing); switch (element) { case QStyle::CE_ComboBoxLabel: +#if QT_CONFIG(combobox) if (const QStyleOptionComboBox *cb = qstyleoption_cast<const QStyleOptionComboBox *>(option)) { + painter->setPen(controlTextColor(option)); QStyleOptionComboBox newOption = *cb; newOption.rect.adjust(4,0,-4,0); QCommonStyle::drawControl(element, &newOption, painter, widget); } +#endif // QT_CONFIG(combobox) break; case QStyle::CE_TabBarTabShape: #if QT_CONFIG(tabbar) @@ -1705,9 +1721,10 @@ void QWindows11Style::drawControl(ControlElement element, const QStyleOption *op } case CE_ItemViewItem: { if (const QStyleOptionViewItem *vopt = qstyleoption_cast<const QStyleOptionViewItem *>(option)) { - QRect checkRect = proxy()->subElementRect(SE_ItemViewItemCheckIndicator, vopt, widget); - QRect iconRect = proxy()->subElementRect(SE_ItemViewItemDecoration, vopt, widget); - QRect textRect = proxy()->subElementRect(SE_ItemViewItemText, vopt, widget); + const auto p = proxy(); + QRect checkRect = p->subElementRect(SE_ItemViewItemCheckIndicator, vopt, widget); + QRect iconRect = p->subElementRect(SE_ItemViewItemDecoration, vopt, widget); + QRect textRect = p->subElementRect(SE_ItemViewItemText, vopt, widget); // draw the background proxy()->drawPrimitive(PE_PanelItemViewItem, option, painter, widget); @@ -1808,16 +1825,17 @@ void QWindows11Style::drawControl(ControlElement element, const QStyleOption *op d->viewItemDrawText(painter, vopt, textRect); // paint a vertical marker for QListView - if (vopt->state & State_Selected) { + if (vopt->state & State_Selected && !highContrastTheme) { if (const QListView *lv = qobject_cast<const QListView *>(widget); - lv && lv->viewMode() != QListView::IconMode && !highContrastTheme) { - painter->setPen(vopt->palette.accent().color()); - const auto xPos = isRtl ? rect.right() - 1 : rect.left(); - const QLineF lines[2] = { - QLineF(xPos, rect.y() + 2, xPos, rect.y() + rect.height() - 2), - QLineF(xPos + 1, rect.y() + 2, xPos + 1, rect.y() + rect.height() - 2), - }; - painter->drawLines(lines, 2); + lv && lv->viewMode() != QListView::IconMode) { + const auto col = vopt->palette.accent().color(); + painter->setBrush(col); + painter->setPen(col); + const auto xPos = isRtl ? rect.right() - 4.5f : rect.left() + 3.5f; + const auto yOfs = rect.height() / 4.; + QRectF r(QPointF(xPos, rect.y() + yOfs), + QPointF(xPos + 1, rect.y() + rect.height() - yOfs)); + painter->drawRoundedRect(r, 1, 1); } } } @@ -1868,22 +1886,29 @@ QRect QWindows11Style::subElementRect(QStyle::SubElement element, const QStyleOp case QStyle::SE_LineEditContents: ret = option->rect.adjusted(4,0,-4,0); break; - case QStyle::SE_ItemViewItemText: - if (const auto *item = qstyleoption_cast<const QStyleOptionViewItem *>(option)) { - const int decorationOffset = item->features.testFlag(QStyleOptionViewItem::HasDecoration) ? item->decorationSize.width() : 0; - const int checkboxOffset = item->features.testFlag(QStyleOptionViewItem::HasCheckIndicator) ? 16 : 0; - if (widget && qobject_cast<QComboBoxPrivateContainer *>(widget->parentWidget())) { - if (option->direction == Qt::LeftToRight) - ret = option->rect.adjusted(decorationOffset + checkboxOffset + 5, 0, -5, 0); - else - ret = option->rect.adjusted(5, 0, decorationOffset - checkboxOffset - 5, 0); + case SE_ItemViewItemCheckIndicator: + case SE_ItemViewItemDecoration: + case SE_ItemViewItemText: { + ret = QWindowsVistaStyle::subElementRect(element, option, widget); + if (!ret.isValid() || highContrastTheme) + return ret; + + if (const QListView *lv = qobject_cast<const QListView *>(widget); + lv && lv->viewMode() != QListView::IconMode) { + const int xOfs = contentHMargin; + const bool isRtl = option->direction == Qt::RightToLeft; + if (isRtl) { + ret.moveRight(ret.right() - xOfs); + if (ret.left() < option->rect.left()) + ret.setLeft(option->rect.left()); } else { - ret = QWindowsVistaStyle::subElementRect(element, option, widget); + ret.moveLeft(ret.left() + xOfs); + if (ret.right() > option->rect.right()) + ret.setRight(option->rect.right()); } - } else { - ret = QWindowsVistaStyle::subElementRect(element, option, widget); } break; + } #if QT_CONFIG(progressbar) case SE_ProgressBarGroove: case SE_ProgressBarContents: @@ -2062,6 +2087,33 @@ QRect QWindows11Style::subControlRect(ComplexControl control, const QStyleOption } break; } + case CC_ComboBox: { + if (subControl == SC_ComboBoxArrow) { + const auto indicatorWidth = + proxy()->pixelMetric(PM_MenuButtonIndicator, option, widget); + const int endX = option->rect.right() - contentHMargin - 2; + const int startX = endX - indicatorWidth; + const QRect rect(QPoint(startX, option->rect.top()), + QPoint(endX, option->rect.bottom())); + ret = visualRect(option->direction, option->rect, rect); + } else { + ret = QWindowsVistaStyle::subControlRect(control, option, subControl, widget); + } + break; + } +#if QT_CONFIG(groupbox) + case CC_GroupBox: { + ret = QWindowsVistaStyle::subControlRect(control, option, subControl, widget); + switch (subControl) { + case SC_GroupBoxCheckBox: + ret.moveTop(1); + break; + default: + break; + } + break; + } +#endif // QT_CONFIG(groupbox) default: ret = QWindowsVistaStyle::subControlRect(control, option, subControl, widget); } @@ -2152,14 +2204,18 @@ QSize QWindows11Style::sizeFromContents(ContentsType type, const QStyleOption *o break; } #endif +#if QT_CONFIG(combobox) case CT_ComboBox: if (const auto *comboBoxOpt = qstyleoption_cast<const QStyleOptionComboBox *>(option)) { contentSize = QWindowsStyle::sizeFromContents(type, option, size, widget); // don't rely on QWindowsThemeData contentSize += QSize(4, 4); // default win11 style margins - if (comboBoxOpt->subControls & SC_ComboBoxArrow) - contentSize += QSize(8, 0); // arrow margins + if (comboBoxOpt->subControls & SC_ComboBoxArrow) { + const auto w = proxy()->pixelMetric(PM_MenuButtonIndicator, option, widget); + contentSize.rwidth() += w + contentItemHMargin; + } } break; +#endif case CT_HeaderSection: // windows vista does not honor the indicator (as it was drawn above the text, not on the // side) so call QWindowsStyle::styleHint directly to get the correct size hint @@ -2201,6 +2257,25 @@ QSize QWindows11Style::sizeFromContents(ContentsType type, const QStyleOption *o contentSize.rwidth() += 2 * contentHMargin - oldMargin; break; } + case CT_ItemViewItem: { + if (const auto *viewItemOpt = qstyleoption_cast<const QStyleOptionViewItem *>(option)) { + if (const QListView *lv = qobject_cast<const QListView *>(widget); + lv && lv->viewMode() != QListView::IconMode) { + QStyleOptionViewItem vOpt(*viewItemOpt); + // viewItemSize only takes PM_FocusFrameHMargin into account but no additional + // margin, therefore adjust it here for a correct width during layouting when + // WrapText is enabled + vOpt.rect.setRight(vOpt.rect.right() - contentHMargin); + contentSize = QWindowsVistaStyle::sizeFromContents(type, &vOpt, size, widget); + contentSize.rwidth() += contentHMargin; + contentSize.rheight() += 2 * contentHMargin; + + } else { + contentSize = QWindowsVistaStyle::sizeFromContents(type, option, size, widget); + } + } + break; + } default: contentSize = QWindowsVistaStyle::sizeFromContents(type, option, size, widget); break; @@ -2290,6 +2365,9 @@ int QWindows11Style::pixelMetric(PixelMetric metric, const QStyleOption *option, case PM_ButtonShiftVertical: res = 0; break; + case PM_TreeViewIndentation: + res = 30; + break; default: res = QWindowsVistaStyle::pixelMetric(metric, option, widget); } @@ -2377,6 +2455,13 @@ void QWindows11Style::unpolish(QWidget *widget) widget->setProperty("_q_original_menubar_maxheight", QVariant()); } #endif + const auto comboBoxContainer = qobject_cast<const QComboBoxPrivateContainer *>(widget); + if (comboBoxContainer) { + widget->setAttribute(Qt::WA_OpaquePaintEvent, true); + widget->setAttribute(Qt::WA_TranslucentBackground, false); + widget->setWindowFlag(Qt::FramelessWindowHint, false); + widget->setWindowFlag(Qt::NoDropShadowWindowHint, false); + } if (const auto *scrollarea = qobject_cast<QAbstractScrollArea *>(widget); scrollarea @@ -2501,6 +2586,7 @@ void QWindows11Style::polish(QPalette& result) d->m_titleBarCloseIcon = QIcon(); d->m_titleBarNormalIcon = QIcon(); d->m_toolbarExtensionButton = QIcon(); + d->m_lineEditClearButton = QIcon(); } QPixmap QWindows11Style::standardPixmap(StandardPixmap standardPixmap, @@ -2525,6 +2611,13 @@ QIcon QWindows11Style::standardIcon(StandardPixmap standardIcon, { auto *d = const_cast<QWindows11StylePrivate*>(d_func()); switch (standardIcon) { + case SP_LineEditClearButton: { + if (d->m_lineEditClearButton.isNull()) { + auto e = new WinFontIconEngine(Clear.at(0), d->assetFont); + d->m_lineEditClearButton = QIcon(e); + } + return d->m_lineEditClearButton; + } case SP_ToolBarHorizontalExtensionButton: case SP_ToolBarVerticalExtensionButton: { if (d->m_toolbarExtensionButton.isNull()) { diff --git a/src/plugins/styles/modernwindows/qwindows11style_p.h b/src/plugins/styles/modernwindows/qwindows11style_p.h index 736caae956c..a51a93ddd9b 100644 --- a/src/plugins/styles/modernwindows/qwindows11style_p.h +++ b/src/plugins/styles/modernwindows/qwindows11style_p.h @@ -123,6 +123,7 @@ class QWindows11StylePrivate : public QWindowsVistaStylePrivate { protected: QIcon m_toolbarExtensionButton; + QIcon m_lineEditClearButton; }; QT_END_NAMESPACE diff --git a/src/plugins/styles/modernwindows/qwindowsvistastyle.cpp b/src/plugins/styles/modernwindows/qwindowsvistastyle.cpp index abe0bde540f..22ca18b10bf 100644 --- a/src/plugins/styles/modernwindows/qwindowsvistastyle.cpp +++ b/src/plugins/styles/modernwindows/qwindowsvistastyle.cpp @@ -1626,6 +1626,12 @@ void QWindowsVistaStyle::drawPrimitive(PrimitiveElement element, const QStyleOpt break; case PE_Frame: + if (widget && widget->inherits("QComboBoxPrivateContainer")){ + QStyleOption copy = *option; + copy.state |= State_Raised; + proxy()->drawPrimitive(PE_PanelMenu, ©, painter, widget); + break; + } #if QT_CONFIG(accessibility) if (QStyleHelper::isInstanceOf(option->styleObject, QAccessible::EditableText) || QStyleHelper::isInstanceOf(option->styleObject, QAccessible::StaticText) || @@ -1704,6 +1710,14 @@ void QWindowsVistaStyle::drawPrimitive(PrimitiveElement element, const QStyleOpt return; } + case PE_PanelMenu: + if (widget && widget->inherits("QComboBoxPrivateContainer")){ + //fill combobox popup background + QWindowsThemeData popupbackgroundTheme(widget, painter, QWindowsVistaStylePrivate::MenuTheme, + MENU_POPUPBACKGROUND, stateId, option->rect); + d->drawBackground(popupbackgroundTheme); + } + case PE_PanelMenuBar: break; diff --git a/src/testlib/qtestcase.cpp b/src/testlib/qtestcase.cpp index 784e69d2486..a98f5da2389 100644 --- a/src/testlib/qtestcase.cpp +++ b/src/testlib/qtestcase.cpp @@ -410,7 +410,7 @@ public: static QMetaMethod findMethod(const QObject *obj, const char *signature); private: - bool invokeTest(int index, QLatin1StringView tag, std::optional<WatchDog> &watchDog) const; + void invokeTest(int index, QLatin1StringView tag, std::optional<WatchDog> &watchDog) const; void invokeTestOnData(int index) const; QMetaMethod m_initTestCaseMethod; // might not exist, check isValid(). @@ -1315,11 +1315,8 @@ static void printUnknownDataTagError(QLatin1StringView name, QLatin1StringView t Call slot_data(), init(), slot(), cleanup(), init(), slot(), cleanup(), ... If data is set then it is the only test that is performed - - If the function was successfully called, true is returned, otherwise - false. */ -bool TestMethods::invokeTest(int index, QLatin1StringView tag, std::optional<WatchDog> &watchDog) const +void TestMethods::invokeTest(int index, QLatin1StringView tag, std::optional<WatchDog> &watchDog) const { QBenchmarkTestMethodData benchmarkData; QBenchmarkTestMethodData::current = &benchmarkData; @@ -1422,8 +1419,6 @@ bool TestMethods::invokeTest(int index, QLatin1StringView tag, std::optional<Wat QTestResult::finishedCurrentTestFunction(); QTestResult::setSkipCurrentTest(false); QTestResult::setBlacklistCurrentTest(false); - - return true; } void *fetchData(QTestData *data, const char *tagName, int typeId) @@ -1743,10 +1738,8 @@ void TestMethods::invokeTests(QObject *testObject) const const char *data = nullptr; if (i < QTest::testTags.size() && !QTest::testTags.at(i).isEmpty()) data = qstrdup(QTest::testTags.at(i).toLatin1().constData()); - const bool ok = invokeTest(i, QLatin1StringView(data), watchDog); + invokeTest(i, QLatin1StringView(data), watchDog); delete [] data; - if (!ok) - break; } } diff --git a/src/tools/windeployqt/main.cpp b/src/tools/windeployqt/main.cpp index e35330ebeb3..6cb935ef47d 100644 --- a/src/tools/windeployqt/main.cpp +++ b/src/tools/windeployqt/main.cpp @@ -592,15 +592,17 @@ static inline int parseArguments(const QStringList &arguments, QCommandLineParse } // default to deployment of compiler runtime for windows desktop configurations - if (options->platform == WindowsDesktopMinGW || options->platform.testFlags(WindowsDesktopMsvc) - || parser->isSet(compilerRunTimeOption)) + if (options->platform == WindowsDesktopMinGW || options->platform == WindowsDesktopClangMinGW + || options->platform.testFlags(WindowsDesktopMsvc) || parser->isSet(compilerRunTimeOption)) options->compilerRunTime = true; if (parser->isSet(noCompilerRunTimeOption)) options->compilerRunTime = false; if (options->compilerRunTime && options->platform != WindowsDesktopMinGW + && options->platform != WindowsDesktopClangMinGW && !options->platform.testFlags(WindowsDesktopMsvc)) { - *errorMessage = QStringLiteral("Deployment of the compiler runtime is implemented for Desktop MSVC/g++ only."); + *errorMessage = QStringLiteral("Deployment of the compiler runtime is implemented for " + "Desktop MSVC and MinGW (g++ and Clang) only."); return CommandLineParseError; } diff --git a/src/widgets/CMakeLists.txt b/src/widgets/CMakeLists.txt index ce30f50616b..c0be3debe49 100644 --- a/src/widgets/CMakeLists.txt +++ b/src/widgets/CMakeLists.txt @@ -681,6 +681,7 @@ qt_internal_extend_target(Widgets CONDITION MACOS AND (QT_FEATURE_menu OR QT_FEA qt_internal_extend_target(Widgets CONDITION QT_FEATURE_colordialog SOURCES dialogs/qcolordialog.cpp dialogs/qcolordialog.h + dialogs/qcolorwell_p.h ) qt_internal_extend_target(Widgets CONDITION QT_FEATURE_dialog diff --git a/src/widgets/accessible/itemviews.cpp b/src/widgets/accessible/itemviews.cpp index fc969e17380..265c523eae0 100644 --- a/src/widgets/accessible/itemviews.cpp +++ b/src/widgets/accessible/itemviews.cpp @@ -60,7 +60,7 @@ int QAccessibleTable::logicalIndex(const QModelIndex &index) const } QAccessibleTable::QAccessibleTable(QWidget *w) - : QAccessibleObject(w) + : QAccessibleWidgetV2(w) { Q_ASSERT(view()); @@ -677,7 +677,7 @@ void *QAccessibleTable::interface_cast(QAccessible::InterfaceType t) return static_cast<QAccessibleSelectionInterface*>(this); if (t == QAccessible::TableInterface) return static_cast<QAccessibleTableInterface*>(this); - return nullptr; + return QAccessibleWidgetV2::interface_cast(t); } void QAccessibleTable::modelChange(QAccessibleTableModelChangeEvent *event) diff --git a/src/widgets/accessible/itemviews_p.h b/src/widgets/accessible/itemviews_p.h index 9b30f36ced3..79f9a7f2f05 100644 --- a/src/widgets/accessible/itemviews_p.h +++ b/src/widgets/accessible/itemviews_p.h @@ -31,7 +31,9 @@ QT_BEGIN_NAMESPACE class QAccessibleTableCell; class QAccessibleTableHeaderCell; -class QAccessibleTable :public QAccessibleTableInterface, public QAccessibleSelectionInterface, public QAccessibleObject +class QAccessibleTable : public QAccessibleTableInterface, + public QAccessibleSelectionInterface, + public QAccessibleWidgetV2 { public: explicit QAccessibleTable(QWidget *w); diff --git a/src/widgets/accessible/rangecontrols.cpp b/src/widgets/accessible/rangecontrols.cpp index c0de5357c9a..1f7b20833dd 100644 --- a/src/widgets/accessible/rangecontrols.cpp +++ b/src/widgets/accessible/rangecontrols.cpp @@ -65,6 +65,16 @@ QAccessibleInterface *QAccessibleAbstractSpinBox::lineEditIface() const #endif } +QAccessible::State QAccessibleAbstractSpinBox::state() const +{ + QAccessible::State state = QAccessibleWidgetV2::state(); + if (abstractSpinBox()->isReadOnly()) + state.readOnly = true; + else + state.editable = true; + return state; +} + QString QAccessibleAbstractSpinBox::text(QAccessible::Text t) const { if (t == QAccessible::Value) diff --git a/src/widgets/accessible/rangecontrols_p.h b/src/widgets/accessible/rangecontrols_p.h index dd5a6a4531c..5a023d2f00b 100644 --- a/src/widgets/accessible/rangecontrols_p.h +++ b/src/widgets/accessible/rangecontrols_p.h @@ -42,6 +42,7 @@ public: explicit QAccessibleAbstractSpinBox(QWidget *w); virtual ~QAccessibleAbstractSpinBox(); + QAccessible::State state() const override; QString text(QAccessible::Text t) const override; void *interface_cast(QAccessible::InterfaceType t) override; diff --git a/src/widgets/dialogs/qcolordialog.cpp b/src/widgets/dialogs/qcolordialog.cpp index eac5de33d32..f8125204045 100644 --- a/src/widgets/dialogs/qcolordialog.cpp +++ b/src/widgets/dialogs/qcolordialog.cpp @@ -39,6 +39,7 @@ #include "qwindow.h" #include "private/qdialog_p.h" +#include "private/qcolorwell_p.h" #include <qpa/qplatformintegration.h> #include <qpa/qplatformservices.h> @@ -56,16 +57,12 @@ namespace QtPrivate { class QColorLuminancePicker; class QColorPicker; class QColorShower; -class QWellArray; -class QColorWell; class QColorPickingEventFilter; } // namespace QtPrivate using QColorLuminancePicker = QtPrivate::QColorLuminancePicker; using QColorPicker = QtPrivate::QColorPicker; using QColorShower = QtPrivate::QColorShower; -using QWellArray = QtPrivate::QWellArray; -using QColorWell = QtPrivate::QColorWell; using QColorPickingEventFilter = QtPrivate::QColorPickingEventFilter; class QColorDialogPrivate : public QDialogPrivate @@ -162,95 +159,6 @@ private: //////////// QWellArray BEGIN -namespace QtPrivate { - -class QWellArray : public QWidget -{ - Q_OBJECT - Q_PROPERTY(int selectedColumn READ selectedColumn) - Q_PROPERTY(int selectedRow READ selectedRow) - -public: - QWellArray(int rows, int cols, QWidget* parent=nullptr); - ~QWellArray() {} - - int selectedColumn() const { return selCol; } - int selectedRow() const { return selRow; } - - virtual void setCurrent(int row, int col); - virtual void setSelected(int row, int col); - - QSize sizeHint() const override; - - inline int cellWidth() const - { return cellw; } - - inline int cellHeight() const - { return cellh; } - - inline int rowAt(int y) const - { return y / cellh; } - - inline int columnAt(int x) const - { if (isRightToLeft()) return ncols - (x / cellw) - 1; return x / cellw; } - - inline int rowY(int row) const - { return cellh * row; } - - inline int columnX(int column) const - { if (isRightToLeft()) return cellw * (ncols - column - 1); return cellw * column; } - - inline int numRows() const - { return nrows; } - - inline int numCols() const - {return ncols; } - - inline QRect cellRect() const - { return QRect(0, 0, cellw, cellh); } - - inline QSize gridSize() const - { return QSize(ncols * cellw, nrows * cellh); } - - QRect cellGeometry(int row, int column) - { - QRect r; - if (row >= 0 && row < nrows && column >= 0 && column < ncols) - r.setRect(columnX(column), rowY(row), cellw, cellh); - return r; - } - - inline void updateCell(int row, int column) { update(cellGeometry(row, column)); } - -signals: - void selected(int row, int col); - void currentChanged(int row, int col); - void colorChanged(int index, QRgb color); - -protected: - virtual void paintCell(QPainter *, int row, int col, const QRect&); - virtual void paintCellContents(QPainter *, int row, int col, const QRect&); - - void mousePressEvent(QMouseEvent*) override; - void mouseReleaseEvent(QMouseEvent*) override; - void keyPressEvent(QKeyEvent*) override; - void focusInEvent(QFocusEvent*) override; - void focusOutEvent(QFocusEvent*) override; - void paintEvent(QPaintEvent *) override; - -private: - Q_DISABLE_COPY(QWellArray) - - int nrows; - int ncols; - int cellw; - int cellh; - int curRow; - int curCol; - int selRow; - int selCol; -}; - void QWellArray::paintEvent(QPaintEvent *e) { QRect r = e->rect(); @@ -475,11 +383,12 @@ void QWellArray::keyPressEvent(QKeyEvent* e) e->ignore(); // we don't accept the event return; } - -} // namespace QtPrivate +} //////////// QWellArray END +namespace QtPrivate { + // Event filter to be installed on the dialog while in color-picking mode. class QColorPickingEventFilter : public QObject { public: @@ -510,7 +419,7 @@ private: QColorDialogPrivate *m_dp; }; -} // unnamed namespace +} // namespace QtPrivate /*! Returns the number of custom colors supported by QColorDialog. All @@ -570,35 +479,6 @@ static inline void rgb2hsv(QRgb rgb, int &h, int &s, int &v) c.getHsv(&h, &s, &v); } -namespace QtPrivate { - -class QColorWell : public QWellArray -{ -public: - QColorWell(QWidget *parent, int r, int c, const QRgb *vals) - :QWellArray(r, c, parent), values(vals), mousePressed(false), oldCurrent(-1, -1) - { setSizePolicy(QSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum)); } - -protected: - void paintCellContents(QPainter *, int row, int col, const QRect&) override; - void mousePressEvent(QMouseEvent *e) override; - void mouseMoveEvent(QMouseEvent *e) override; - void mouseReleaseEvent(QMouseEvent *e) override; -#if QT_CONFIG(draganddrop) - void dragEnterEvent(QDragEnterEvent *e) override; - void dragLeaveEvent(QDragLeaveEvent *e) override; - void dragMoveEvent(QDragMoveEvent *e) override; - void dropEvent(QDropEvent *e) override; -#endif - -private: - const QRgb *values; - bool mousePressed; - QPoint pressPos; - QPoint oldCurrent; - -}; - void QColorWell::paintCellContents(QPainter *p, int row, int col, const QRect &r) { int i = row + col*numRows(); @@ -686,6 +566,8 @@ void QColorWell::mouseReleaseEvent(QMouseEvent *e) mousePressed = false; } +namespace QtPrivate { + class QColorPicker : public QFrame { Q_OBJECT @@ -703,18 +585,21 @@ signals: protected: QSize sizeHint() const override; void paintEvent(QPaintEvent*) override; + void keyPressEvent(QKeyEvent *event) override; void mouseMoveEvent(QMouseEvent *) override; void mousePressEvent(QMouseEvent *) override; void resizeEvent(QResizeEvent *) override; private: - int hue; - int sat; + QPoint m_pos; - QPoint colPt(); - int huePt(const QPoint &pt); - int satPt(const QPoint &pt); - void setCol(const QPoint &pt); + QPixmap createColorsPixmap(); + QPoint colPt(int hue, int sat); + int huePt(const QPoint &pt, const QSize &widgetSize); + int huePt(const QPoint &pt) { return huePt(pt, size()); } + int satPt(const QPoint &pt, const QSize &widgetSize); + int satPt(const QPoint &pt) { return satPt(pt, size()); } + void setCol(const QPoint &pt, bool notify = true); QPixmap pix; bool crossVisible; @@ -743,6 +628,7 @@ signals: protected: void paintEvent(QPaintEvent*) override; + void keyPressEvent(QKeyEvent *event) override; void mouseMoveEvent(QMouseEvent *) override; void mousePressEvent(QMouseEvent *) override; @@ -778,6 +664,7 @@ QColorLuminancePicker::QColorLuminancePicker(QWidget* parent) hue = 100; val = 100; sat = 100; pix = nullptr; // setAttribute(WA_NoErase, true); + setFocusPolicy(Qt::StrongFocus); } QColorLuminancePicker::~QColorLuminancePicker() @@ -785,6 +672,21 @@ QColorLuminancePicker::~QColorLuminancePicker() delete pix; } +void QColorLuminancePicker::keyPressEvent(QKeyEvent *event) +{ + switch (event->key()) { + case Qt::Key_Down: + setVal(std::clamp(val - 1, 0, 255)); + break; + case Qt::Key_Up: + setVal(std::clamp(val + 1, 0, 255)); + break; + default: + QWidget::keyPressEvent(event); + break; + } +} + void QColorLuminancePicker::mouseMoveEvent(QMouseEvent *m) { if (m->buttons() == Qt::NoButton) { @@ -855,38 +757,53 @@ void QColorLuminancePicker::setCol(int h, int s , int v) repaint(); } -QPoint QColorPicker::colPt() +QPoint QColorPicker::colPt(int hue, int sat) { QRect r = contentsRect(); return QPoint((360 - hue) * (r.width() - 1) / 360, (255 - sat) * (r.height() - 1) / 255); } -int QColorPicker::huePt(const QPoint &pt) +int QColorPicker::huePt(const QPoint &pt, const QSize &widgetSize) { - QRect r = contentsRect(); - return 360 - pt.x() * 360 / (r.width() - 1); + QRect r = QRect(QPoint(0, 0), widgetSize) - contentsMargins(); + return std::clamp(360 - pt.x() * 360 / (r.width() - 1), 0, 359); } -int QColorPicker::satPt(const QPoint &pt) +int QColorPicker::satPt(const QPoint &pt, const QSize &widgetSize) { - QRect r = contentsRect(); - return 255 - pt.y() * 255 / (r.height() - 1); + QRect r = QRect(QPoint(0, 0), widgetSize) - contentsMargins(); + return std::clamp(255 - pt.y() * 255 / (r.height() - 1), 0, 255); } -void QColorPicker::setCol(const QPoint &pt) +void QColorPicker::setCol(const QPoint &pt, bool notify) { - setCol(huePt(pt), satPt(pt)); + if (pt == m_pos) + return; + + QRect r(m_pos, QSize(20, 20)); + m_pos.setX(std::clamp(pt.x(), 0, pix.width() - 1)); + m_pos.setY(std::clamp(pt.y(), 0, pix.height() - 1)); + r = r.united(QRect(m_pos, QSize(20, 20))); + r.translate(contentsRect().x() - 9, contentsRect().y() - 9); + // update(r); + repaint(r); + + if (notify) + emit newCol(huePt(m_pos), satPt(m_pos)); } QColorPicker::QColorPicker(QWidget* parent) : QFrame(parent) , crossVisible(true) { - hue = 0; sat = 0; - setCol(150, 255); - setAttribute(Qt::WA_NoSystemBackground); + setFocusPolicy(Qt::StrongFocus); setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed) ); + adjustSize(); + + pix = createColorsPixmap(); + + setCol(150, 255); } QColorPicker::~QColorPicker() @@ -910,15 +827,31 @@ void QColorPicker::setCol(int h, int s) { int nhue = qMin(qMax(0,h), 359); int nsat = qMin(qMax(0,s), 255); - if (nhue == hue && nsat == sat) + if (nhue == huePt(m_pos) && nsat == satPt(m_pos)) return; - QRect r(colPt(), QSize(20,20)); - hue = nhue; sat = nsat; - r = r.united(QRect(colPt(), QSize(20,20))); - r.translate(contentsRect().x()-9, contentsRect().y()-9); - // update(r); - repaint(r); + setCol(colPt(nhue, nsat), false); +} + +void QColorPicker::keyPressEvent(QKeyEvent *event) +{ + switch (event->key()) { + case Qt::Key_Down: + setCol(m_pos + QPoint(0, 1)); + break; + case Qt::Key_Left: + setCol(m_pos + QPoint(-1, 0)); + break; + case Qt::Key_Right: + setCol(m_pos + QPoint(1, 0)); + break; + case Qt::Key_Up: + setCol(m_pos + QPoint(0, -1)); + break; + default: + QFrame::keyPressEvent(event); + break; + } } void QColorPicker::mouseMoveEvent(QMouseEvent *m) @@ -929,14 +862,12 @@ void QColorPicker::mouseMoveEvent(QMouseEvent *m) return; } setCol(p); - emit newCol(hue, sat); } void QColorPicker::mousePressEvent(QMouseEvent *m) { QPoint p = m->position().toPoint() - contentsRect().topLeft(); setCol(p); - emit newCol(hue, sat); } void QColorPicker::paintEvent(QPaintEvent* ) @@ -948,7 +879,7 @@ void QColorPicker::paintEvent(QPaintEvent* ) p.drawPixmap(r.topLeft(), pix); if (crossVisible) { - QPoint pt = colPt() + r.topLeft(); + QPoint pt = m_pos + r.topLeft(); p.setPen(Qt::black); p.fillRect(pt.x()-9, pt.y(), 20, 2, Qt::black); p.fillRect(pt.x(), pt.y()-9, 2, 20, Qt::black); @@ -959,6 +890,21 @@ void QColorPicker::resizeEvent(QResizeEvent *ev) { QFrame::resizeEvent(ev); + pix = createColorsPixmap(); + + const QSize &oldSize = ev->oldSize(); + if (!oldSize.isValid()) + return; + + // calculate hue/saturation based on previous widget size + // and update position accordingly + const int hue = huePt(m_pos, oldSize); + const int sat = satPt(m_pos, oldSize); + setCol(hue, sat); +} + +QPixmap QColorPicker::createColorsPixmap() +{ int w = width() - frameWidth() * 2; int h = height() - frameWidth() * 2; QImage img(w, h, QImage::Format_RGB32); @@ -976,10 +922,9 @@ void QColorPicker::resizeEvent(QResizeEvent *ev) ++x; } } - pix = QPixmap::fromImage(img); + return QPixmap::fromImage(img); } - class QColSpinBox : public QSpinBox { public: diff --git a/src/widgets/dialogs/qcolorwell_p.h b/src/widgets/dialogs/qcolorwell_p.h new file mode 100644 index 00000000000..31d69fabb13 --- /dev/null +++ b/src/widgets/dialogs/qcolorwell_p.h @@ -0,0 +1,142 @@ +// 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 QCOLORWELL_P_H +#define QCOLORWELL_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/qrect.h> +#include <QtWidgets/qwidget.h> + +QT_REQUIRE_CONFIG(colordialog); + +QT_BEGIN_NAMESPACE + +class QWellArray : public QWidget +{ + Q_OBJECT + Q_PROPERTY(int selectedColumn READ selectedColumn) + Q_PROPERTY(int selectedRow READ selectedRow) + +public: + QWellArray(int rows, int cols, QWidget *parent = nullptr); + ~QWellArray() { } + + int selectedColumn() const { return selCol; } + int selectedRow() const { return selRow; } + + virtual void setCurrent(int row, int col); + virtual void setSelected(int row, int col); + + QSize sizeHint() const override; + + inline int cellWidth() const { return cellw; } + + inline int cellHeight() const { return cellh; } + + inline int rowAt(int y) const { return y / cellh; } + + inline int columnAt(int x) const + { + if (isRightToLeft()) + return ncols - (x / cellw) - 1; + return x / cellw; + } + + inline int rowY(int row) const { return cellh * row; } + + inline int columnX(int column) const + { + if (isRightToLeft()) + return cellw * (ncols - column - 1); + return cellw * column; + } + + inline int numRows() const { return nrows; } + + inline int numCols() const { return ncols; } + + inline QRect cellRect() const { return QRect(0, 0, cellw, cellh); } + + inline QSize gridSize() const { return QSize(ncols * cellw, nrows * cellh); } + + QRect cellGeometry(int row, int column) + { + QRect r; + if (row >= 0 && row < nrows && column >= 0 && column < ncols) + r.setRect(columnX(column), rowY(row), cellw, cellh); + return r; + } + + inline void updateCell(int row, int column) { update(cellGeometry(row, column)); } + +signals: + void selected(int row, int col); + void currentChanged(int row, int col); + void colorChanged(int index, QRgb color); + +protected: + virtual void paintCell(QPainter *, int row, int col, const QRect &); + virtual void paintCellContents(QPainter *, int row, int col, const QRect &); + + void mousePressEvent(QMouseEvent *) override; + void mouseReleaseEvent(QMouseEvent *) override; + void keyPressEvent(QKeyEvent *) override; + void focusInEvent(QFocusEvent *) override; + void focusOutEvent(QFocusEvent *) override; + void paintEvent(QPaintEvent *) override; + +private: + Q_DISABLE_COPY(QWellArray) + + int nrows; + int ncols; + int cellw; + int cellh; + int curRow; + int curCol; + int selRow; + int selCol; +}; + +class QColorWell : public QWellArray +{ +public: + QColorWell(QWidget *parent, int r, int c, const QRgb *vals) + : QWellArray(r, c, parent), values(vals), mousePressed(false), oldCurrent(-1, -1) + { + setSizePolicy(QSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum)); + } + +protected: + void paintCellContents(QPainter *, int row, int col, const QRect &) override; + void mousePressEvent(QMouseEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; +#if QT_CONFIG(draganddrop) + void dragEnterEvent(QDragEnterEvent *e) override; + void dragLeaveEvent(QDragLeaveEvent *e) override; + void dragMoveEvent(QDragMoveEvent *e) override; + void dropEvent(QDropEvent *e) override; +#endif + +private: + const QRgb *values; + bool mousePressed; + QPoint pressPos; + QPoint oldCurrent; +}; + +QT_END_NAMESPACE + +#endif // QCOLORWELL_P_H diff --git a/src/widgets/dialogs/qfiledialog.ui b/src/widgets/dialogs/qfiledialog.ui index f275e20c633..97f39fa6194 100644 --- a/src/widgets/dialogs/qfiledialog.ui +++ b/src/widgets/dialogs/qfiledialog.ui @@ -20,7 +20,10 @@ <item row="0" column="0"> <widget class="QLabel" name="lookInLabel"> <property name="text"> - <string>Look in:</string> + <string>&Look in:</string> + </property> + <property name="buddy"> + <cstring>lookInCombo</cstring> </property> </widget> </item> @@ -284,7 +287,10 @@ </sizepolicy> </property> <property name="text"> - <string>Files of type:</string> + <string>Files of &type:</string> + </property> + <property name="buddy"> + <cstring>fileTypeCombo</cstring> </property> </widget> </item> diff --git a/src/widgets/dialogs/qfontdialog.cpp b/src/widgets/dialogs/qfontdialog.cpp index 870b0c40faf..c4f8af1a639 100644 --- a/src/widgets/dialogs/qfontdialog.cpp +++ b/src/widgets/dialogs/qfontdialog.cpp @@ -172,7 +172,7 @@ void QFontDialogPrivate::init() sizeAccel = new QLabel(q); #ifndef QT_NO_SHORTCUT - sizeAccel->setBuddy(sizeEdit); + sizeAccel->setBuddy(sizeList); #endif sizeAccel->setIndent(2); diff --git a/src/widgets/kernel/qlayout.cpp b/src/widgets/kernel/qlayout.cpp index 00f9766af29..5ba92714f6c 100644 --- a/src/widgets/kernel/qlayout.cpp +++ b/src/widgets/kernel/qlayout.cpp @@ -345,6 +345,17 @@ void QLayout::getContentsMargins(int *left, int *top, int *right, int *bottom) c } /*! + \property QLayout::contentsMargins + \since 4.6 + \brief the margins used around the layout + + By default, QLayout uses the values provided by the style. On + most platforms, the margin is 11 pixels in all directions. + + \sa setContentsMargins(), getContentsMargins() +*/ + +/*! \since 4.6 Returns the margins used around the layout. diff --git a/src/widgets/styles/qcommonstyle.cpp b/src/widgets/styles/qcommonstyle.cpp index c4b78539114..bb9f7ab27fc 100644 --- a/src/widgets/styles/qcommonstyle.cpp +++ b/src/widgets/styles/qcommonstyle.cpp @@ -5,15 +5,12 @@ #include "qcommonstyle.h" #include "qcommonstyle_p.h" -#include <qfile.h> #if QT_CONFIG(itemviews) #include <qabstractitemview.h> #endif #include <qapplication.h> #include <private/qguiapplication_p.h> #include <qpa/qplatformtheme.h> -#include <qbitmap.h> -#include <qcache.h> #if QT_CONFIG(dockwidget) #include <qdockwidget.h> #endif @@ -61,7 +58,6 @@ #endif #include <private/qcommonstylepixmaps_p.h> #include <private/qmath_p.h> -#include <qdebug.h> #include <qtextformat.h> #if QT_CONFIG(wizard) #include <qwizard.h> @@ -69,11 +65,6 @@ #if QT_CONFIG(filedialog) #include <qsidebar_p.h> #endif -#include <qfileinfo.h> -#include <qdir.h> -#if QT_CONFIG(settings) -#include <qsettings.h> -#endif #include <qvariant.h> #include <qpixmapcache.h> #if QT_CONFIG(animation) @@ -6199,17 +6190,17 @@ QPixmap QCommonStyle::generatedIconPixmap(QIcon::Mode iconMode, const QPixmap &p return QPixmap::fromImage(std::move(im)); } case QIcon::Selected: { - QImage img = pixmap.toImage().convertToFormat(QImage::Format_ARGB32_Premultiplied); QColor color = opt->palette.color(QPalette::Normal, QPalette::Highlight); color.setAlphaF(0.3f); - QPainter painter(&img); + QPixmap ret(pixmap); + QPainter painter(&ret); painter.setCompositionMode(QPainter::CompositionMode_SourceAtop); - painter.fillRect(0, 0, img.width(), img.height(), color); + painter.fillRect(0, 0, pixmap.width(), pixmap.height(), color); painter.end(); - return QPixmap::fromImage(std::move(img)); } + return ret; + } case QIcon::Active: - return pixmap; - default: + case QIcon::Normal: break; } return pixmap; diff --git a/src/widgets/styles/qwindowsstyle.cpp b/src/widgets/styles/qwindowsstyle.cpp index 9b06822c218..b9143a59ee7 100644 --- a/src/widgets/styles/qwindowsstyle.cpp +++ b/src/widgets/styles/qwindowsstyle.cpp @@ -851,6 +851,12 @@ void QWindowsStyle::drawPrimitive(PrimitiveElement pe, const QStyleOption *opt, } #ifndef QT_NO_FRAME case PE_Frame: + if (w && w->inherits("QComboBoxPrivateContainer")){ + QStyleOption copy = *opt; + copy.state |= State_Raised; + proxy()->drawPrimitive(PE_PanelMenu, ©, p, w); + break; + } case PE_FrameMenu: if (const QStyleOptionFrame *frame = qstyleoption_cast<const QStyleOptionFrame *>(opt)) { if (frame->lineWidth == 2 || pe == PE_Frame) { @@ -873,6 +879,7 @@ void QWindowsStyle::drawPrimitive(PrimitiveElement pe, const QStyleOption *opt, } } else { QPalette popupPal = opt->palette; + p->drawRect(opt->rect); popupPal.setColor(QPalette::Light, opt->palette.window().color()); popupPal.setColor(QPalette::Midlight, opt->palette.light().color()); qDrawWinPanel(p, opt->rect, popupPal, opt->state & State_Sunken); @@ -899,6 +906,12 @@ void QWindowsStyle::drawPrimitive(PrimitiveElement pe, const QStyleOption *opt, p->drawRect(opt->rect); } break; } + case PE_PanelMenu: + if (w && w->inherits("QComboBoxPrivateContainer")){ + const QBrush menuBackground = opt->palette.base().color(); + QColor borderColor = opt->palette.window().color(); + qDrawPlainRect(p, opt->rect, borderColor, 1, &menuBackground); + } case PE_FrameWindow: { QPalette popupPal = opt->palette; popupPal.setColor(QPalette::Light, opt->palette.window().color()); diff --git a/src/widgets/widgets/qlineedit_p.cpp b/src/widgets/widgets/qlineedit_p.cpp index 55e6137dba9..ee80cca649c 100644 --- a/src/widgets/widgets/qlineedit_p.cpp +++ b/src/widgets/widgets/qlineedit_p.cpp @@ -353,17 +353,16 @@ QLineEditPrivate *QLineEditIconButton::lineEditPrivate() const void QLineEditIconButton::paintEvent(QPaintEvent *) { QPainter painter(this); - QIcon::Mode state = QIcon::Disabled; + QIcon::Mode mode = QIcon::Disabled; if (isEnabled()) - state = isDown() ? QIcon::Active : QIcon::Normal; + mode = isDown() ? QIcon::Active : QIcon::Normal; const QLineEditPrivate *lep = lineEditPrivate(); const int iconWidth = lep ? lep->sideWidgetParameters().iconSize : 16; const QSize iconSize(iconWidth, iconWidth); - const QPixmap iconPixmap = icon().pixmap(iconSize, devicePixelRatio(), state, QIcon::Off); QRect pixmapRect = QRect(QPoint(0, 0), iconSize); pixmapRect.moveCenter(rect().center()); painter.setOpacity(m_opacity); - painter.drawPixmap(pixmapRect, iconPixmap); + icon().paint(&painter, pixmapRect, Qt::AlignCenter, mode, QIcon::Off); } void QLineEditIconButton::actionEvent(QActionEvent *e) diff --git a/src/widgets/widgets/qtabbar.cpp b/src/widgets/widgets/qtabbar.cpp index 0b562a47879..8e6f497d7f5 100644 --- a/src/widgets/widgets/qtabbar.cpp +++ b/src/widgets/widgets/qtabbar.cpp @@ -1003,7 +1003,7 @@ int QTabBar::insertTab(int index, const QIcon& icon, const QString &text) ++tab->lastTab; } - if (tabAt(d->mousePosition) == index) { + if (isVisible() && tabAt(d->mousePosition) == index) { d->hoverIndex = index; d->hoverRect = tabRect(index); } diff --git a/tests/auto/cmake/RunCMake/Sbom/CMakeLists.txt b/tests/auto/cmake/RunCMake/Sbom/CMakeLists.txt index d0ef37d817f..e9578d7246a 100644 --- a/tests/auto/cmake/RunCMake/Sbom/CMakeLists.txt +++ b/tests/auto/cmake/RunCMake/Sbom/CMakeLists.txt @@ -1,3 +1,7 @@ cmake_minimum_required(VERSION 3.16) project(${RunCMake_TEST} LANGUAGES CXX) -include(${RunCMake_TEST}.cmake) + +# To allow including the gen files in other subdirectory projects. +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") + +include(${SBOM_INCLUDE_FILE}.cmake) diff --git a/tests/auto/cmake/RunCMake/Sbom/RunCMakeTest.cmake b/tests/auto/cmake/RunCMake/Sbom/RunCMakeTest.cmake index 3103e651093..81e83ccead3 100644 --- a/tests/auto/cmake/RunCMake/Sbom/RunCMakeTest.cmake +++ b/tests/auto/cmake/RunCMake/Sbom/RunCMakeTest.cmake @@ -1,27 +1,68 @@ include(QtRunCMake) -function(run_cmake_and_build case) +function(run_cmake_and_build case format_case) + set(include_file "${case}") + set(case "${format_case}-${case}") + # Set common build directory for configure and build set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/${case}-build) set(options "-DQt6_DIR=${Qt6_DIR}" "-DCMAKE_INSTALL_PREFIX=${RunCMake_TEST_BINARY_DIR}/installed" + "-DSBOM_INCLUDE_FILE=${include_file}" + "-DFORMAT_CASE=${format_case}" ) + if(format_case STREQUAL "spdx23") + list(APPEND options + -DQT_GENERATE_SBOM=ON + -DQT_SBOM_GENERATE_SPDX_V2=ON + -DQT_SBOM_GENERATE_CYDX_V1_6=OFF + ) + elseif(format_case STREQUAL "cydx16") + list(APPEND options + -DQT_GENERATE_SBOM=ON + -DQT_SBOM_GENERATE_SPDX_V2=OFF + -DQT_SBOM_GENERATE_CYDX_V1_6=ON + ) + elseif(format_case STREQUAL "all") + list(APPEND options + -DQT_GENERATE_SBOM=ON + -DQT_SBOM_GENERATE_SPDX_V2=ON + -DQT_SBOM_GENERATE_CYDX_V1_6=ON + ) + elseif(format_case STREQUAL "none") + list(APPEND options + -DQT_GENERATE_SBOM=OFF + ) + endif() + + # Check CI environment variables for SBOM options to ensure we only enabled checks that + # require additional dependencies on machines that actually have them. + # Also allow force enabling all checks via QT_SBOM_FORCE_ALL_CHECKS env var. set(maybe_sbom_env_args "$ENV{SBOM_COMMON_ARGS}") + set(force_all_checks "$ENV{QT_SBOM_FORCE_ALL_CHECKS}") - if(maybe_sbom_env_args MATCHES "QT_INTERNAL_SBOM_DEFAULT_CHECKS=ON") + if(maybe_sbom_env_args MATCHES "QT_INTERNAL_SBOM_DEFAULT_CHECKS=ON" + OR force_all_checks) list(APPEND options "-DQT_INTERNAL_SBOM_DEFAULT_CHECKS=ON") endif() - if(maybe_sbom_env_args MATCHES "QT_INTERNAL_SBOM_AUDIT=ON") + if(maybe_sbom_env_args MATCHES "QT_INTERNAL_SBOM_AUDIT=ON" + OR force_all_checks) list(APPEND options "-DQT_INTERNAL_SBOM_AUDIT=ON") endif() - if(maybe_sbom_env_args MATCHES "QT_INTERNAL_SBOM_AUDIT_NO_ERROR=ON") + if(maybe_sbom_env_args MATCHES "QT_INTERNAL_SBOM_AUDIT_NO_ERROR=ON" + OR force_all_checks) list(APPEND options "-DQT_INTERNAL_SBOM_AUDIT_NO_ERROR=ON") endif() + if(maybe_sbom_env_args MATCHES "QT_SBOM_REQUIRE_GENERATE_CYDX_V1_6=ON" + OR force_all_checks) + list(APPEND options "-DQT_SBOM_REQUIRE_GENERATE_CYDX_V1_6=ON") + endif() + # Need to pass the python interpreter paths, to avoid sbom2doc not found errors. # This mirrors what coin/instructions/prepare_building_env.yaml does. set(maybe_python3_path "$ENV{PYTHON3_PATH}") @@ -42,9 +83,17 @@ function(run_cmake_and_build case) # fine. set(RunCMake_TEST_OUTPUT_MERGE 1) run_cmake_command(${case}-build ${CMAKE_COMMAND} --build .) + + # Check the sbom files are present after installation. + set(RunCMake-check-file "check.cmake") run_cmake_command(${case}-install ${CMAKE_COMMAND} --install .) + unset(RunCMake-check-file) endfunction() -run_cmake_and_build(minimal) -run_cmake_and_build(full) -run_cmake_and_build(versions) +set(format_cases spdx23 cydx16 all none) +foreach(format_case IN LISTS format_cases) + run_cmake_and_build(minimal "${format_case}") + run_cmake_and_build(full "${format_case}") + run_cmake_and_build(versions "${format_case}") +endforeach() + diff --git a/tests/auto/cmake/RunCMake/Sbom/check.cmake b/tests/auto/cmake/RunCMake/Sbom/check.cmake new file mode 100644 index 00000000000..9e143e35ab2 --- /dev/null +++ b/tests/auto/cmake/RunCMake/Sbom/check.cmake @@ -0,0 +1,63 @@ +function(check_exists file) + if(NOT EXISTS "${file}") + get_filename_component(file_dir "${file}" DIRECTORY) + file(GLOB dir_contents "${file_dir}/*") + string(APPEND RunCMake_TEST_FAILED "${file} does not exist\n. " + "Contents of directory ${file_dir}:\n ${dir_contents}\n") + endif() + set(RunCMake_TEST_FAILED "${RunCMake_TEST_FAILED}" PARENT_SCOPE) +endfunction() + +function(check_not_exists file) + if(EXISTS "${file}") + get_filename_component(file_dir "${file}" DIRECTORY) + file(GLOB dir_contents "${file_dir}/*") + string(APPEND RunCMake_TEST_FAILED "${file} exists\n. " + "Contents of directory ${file_dir}:\n ${dir_contents}\n") + endif() + set(RunCMake_TEST_FAILED "${RunCMake_TEST_FAILED}" PARENT_SCOPE) +endfunction() + +# Check that the correct option values are used for the project root sbom. +set(root_result_file "${RunCMake_TEST_BINARY_DIR}/result.cmake") +if(EXISTS "${root_result_file}") + include("${root_result_file}") + + if(NOT "${ORIGINAL_QT_GENERATE_SBOM}" STREQUAL "${RESULT_QT_GENERATE_SBOM}") + string(APPEND RunCMake_TEST_FAILED + "QT_GENERATE_SBOM is ${RESULT_QT_GENERATE_SBOM}, expected ${ORIGINAL_QT_GENERATE_SBOM} \n") + endif() + + if(NOT "${ORIGINAL_QT_SBOM_GENERATE_SPDX_V2}" STREQUAL "${RESULT_QT_SBOM_GENERATE_SPDX_V2}") + string(APPEND RunCMake_TEST_FAILED + "QT_SBOM_GENERATE_SPDX_V2 is ${RESULT_QT_SBOM_GENERATE_SPDX_V2}, " + "expected ${ORIGINAL_QT_SBOM_GENERATE_SPDX_V2} \n") + endif() + + if(NOT "${ORIGINAL_QT_SBOM_GENERATE_CYDX_V1_6}" STREQUAL "${RESULT_QT_SBOM_GENERATE_CYDX_V1_6}") + string(APPEND RunCMake_TEST_FAILED + "QT_SBOM_GENERATE_CYDX_V1_6 is ${RESULT_QT_SBOM_GENERATE_CYDX_V1_6}, " + "expected ${ORIGINAL_QT_SBOM_GENERATE_CYDX_V1_6} \n") + endif() +endif() + + +# Glob for all result.cmake files recursively in the root of the test binary dir, and run checks +# for each of them. +file(GLOB_RECURSE result_files + "${RunCMake_TEST_BINARY_DIR}/**/result.cmake" +) + +# Confirm that the all subproject sbom files are installed, including the root one. +foreach(result_file IN LISTS result_files) + include("${result_file}") + + foreach(sbom_doc IN LISTS SBOM_DOCUMENTS) + check_exists("${sbom_doc}") + endforeach() + + foreach(sbom_doc IN LISTS NO_SBOM_DOCUMENTS) + check_not_exists("${sbom_doc}") + endforeach() +endforeach() + diff --git a/tests/auto/cmake/RunCMake/Sbom/cmake/CommonResultGen.cmake b/tests/auto/cmake/RunCMake/Sbom/cmake/CommonResultGen.cmake new file mode 100644 index 00000000000..a892fee2082 --- /dev/null +++ b/tests/auto/cmake/RunCMake/Sbom/cmake/CommonResultGen.cmake @@ -0,0 +1,67 @@ +if(NOT SBOM_PROJECT_NAME) + set(SBOM_PROJECT_NAME "${PROJECT_NAME}") +endif() +# Convert to lower case, otherwise on case-sensitive filesystems the generated +# filenames may not match the expected ones. +string(TOLOWER "${SBOM_PROJECT_NAME}" SBOM_PROJECT_NAME) + +if(NOT SBOM_VERSION) + set(SBOM_VERSION "1.0.0") +endif() + +if(NOT SBOM_INSTALL_DIR) + set(SBOM_INSTALL_DIR "sbom") +endif() + +set(sbom_document_base_name "${SBOM_PROJECT_NAME}-${SBOM_VERSION}") +set(sbom_install_dir "${CMAKE_BINARY_DIR}/installed/${SBOM_INSTALL_DIR}") + +set(spdx_file "${sbom_install_dir}/${sbom_document_base_name}.spdx") +set(spdx_json_file "${sbom_install_dir}/${sbom_document_base_name}.spdx.json") +set(cydx_file "${sbom_install_dir}/${sbom_document_base_name}.cdx.json") + +set(sbom_documents "") +set(no_sbom_documents "") + +if(FORMAT_CASE STREQUAL "spdx23" OR FORMAT_CASE STREQUAL "all") + if(QT_SBOM_GENERATE_SPDX_V2) + list(APPEND sbom_documents "${spdx_file}") + else() + list(APPEND no_sbom_documents "${spdx_file}") + endif() + + if(QT_SBOM_GENERATE_SPDX_V2_JSON) + list(APPEND sbom_documents "${spdx_json_file}") + else() + list(APPEND no_sbom_documents "${spdx_json_file}") + endif() +endif() + +if(FORMAT_CASE STREQUAL "cydx16" OR FORMAT_CASE STREQUAL "all") + if(QT_SBOM_GENERATE_CYDX_V1_6) + list(APPEND sbom_documents "${cydx_file}") + else() + list(APPEND no_sbom_documents "${cydx_file}") + endif() +endif() + +if(FORMAT_CASE STREQUAL "none") + set(no_sbom_documents ${spdx_file} ${spdx_json_file} ${cydx_file}) + set(sbom_documents "") +endif() + +# These values will be used by the check.cmake script after installation. +file(GENERATE + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/result.cmake" + CONTENT + " +set(SBOM_DOCUMENTS \"${sbom_documents}\") +set(NO_SBOM_DOCUMENTS \"${no_sbom_documents}\") +set(ORIGINAL_QT_GENERATE_SBOM \"${original_QT_GENERATE_SBOM}\") +set(ORIGINAL_QT_SBOM_GENERATE_SPDX_V2 \"${original_QT_SBOM_GENERATE_SPDX_V2}\") +set(ORIGINAL_QT_SBOM_GENERATE_CYDX_V1_6 \"${original_QT_SBOM_GENERATE_CYDX_V1_6}\") +set(RESULT_QT_GENERATE_SBOM \"${QT_GENERATE_SBOM}\") +set(RESULT_QT_SBOM_GENERATE_SPDX_V2 \"${QT_SBOM_GENERATE_SPDX_V2}\") +set(RESULT_QT_SBOM_GENERATE_CYDX_V1_6 \"${QT_SBOM_GENERATE_CYDX_V1_6}\") +" +) diff --git a/tests/auto/cmake/RunCMake/Sbom/cmake/CommonResultGenIntro.cmake b/tests/auto/cmake/RunCMake/Sbom/cmake/CommonResultGenIntro.cmake new file mode 100644 index 00000000000..11ec50d76d6 --- /dev/null +++ b/tests/auto/cmake/RunCMake/Sbom/cmake/CommonResultGenIntro.cmake @@ -0,0 +1,12 @@ +# Record the sbom option values before they might be modified by an sbom_setup call, due to +# missing python dependencies. +set(original_QT_GENERATE_SBOM "${QT_GENERATE_SBOM}") +set(original_QT_SBOM_GENERATE_SPDX_V2 "${QT_SBOM_GENERATE_SPDX_V2}") +set(original_QT_SBOM_GENERATE_CYDX_V1_6 "${QT_SBOM_GENERATE_CYDX_V1_6}") + +# Explicitly set these because none case only has QT_GENERATE_SBOM passed as OFF. +# In this case, the defaults for the formats is to remain ON. +if(NOT QT_GENERATE_SBOM) + set(original_QT_SBOM_GENERATE_SPDX_V2 ON) + set(original_QT_SBOM_GENERATE_CYDX_V1_6 ON) +endif() diff --git a/tests/auto/cmake/RunCMake/Sbom/full.cmake b/tests/auto/cmake/RunCMake/Sbom/full.cmake index 7241b8e77a8..b5ac77c3ccc 100644 --- a/tests/auto/cmake/RunCMake/Sbom/full.cmake +++ b/tests/auto/cmake/RunCMake/Sbom/full.cmake @@ -1,23 +1,30 @@ # Needed to make the sbom functions available. find_package(Qt6 REQUIRED Core) +include(CommonResultGenIntro) + _qt_internal_setup_sbom( GENERATE_SBOM_DEFAULT "TRUE" ) set(IS_FULL_BUILD "TRUE") +# These are used by common_result_gen.cmake. +set(SBOM_VERSION "2.0.0") +set(SBOM_INSTALL_DIR "sbom_full") +set(SBOM_PROJECT_NAME "${PROJECT_NAME}ProjectFull") + _qt_internal_sbom_begin_project( - SBOM_PROJECT_NAME "${PROJECT_NAME}Project" + SBOM_PROJECT_NAME "${SBOM_PROJECT_NAME}" SUPPLIER "QtProjectTest" SUPPLIER_URL "https://qt-project.org/SbomTest" LICENSE_EXPRESSION "LGPL-3.0-only" COPYRIGHTS "2025 The Qt Company Ltd." INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}" - INSTALL_SBOM_DIR "sbom" + INSTALL_SBOM_DIR "${SBOM_INSTALL_DIR}" DOWNLOAD_LOCATION "https://download.qt.io/sbom" CPE "cpe:2.3:a:qt:qtprojecttest:1.0.0:*:*:*:*:*:*:*" - VERSION "1.0.0" + VERSION "${SBOM_VERSION}" DOCUMENT_CREATOR_TOOL "Test Build System Tool" LICENSE_DIR_PATHS "${CMAKE_CURRENT_SOURCE_DIR}/custom_licenses" ) @@ -26,3 +33,7 @@ include(common_targets.cmake) _qt_internal_sbom_end_project() +include(CommonResultGen) + +# Also create separate sboms for some sibling projects under this subdir. +add_subdirectory(subprojects) diff --git a/tests/auto/cmake/RunCMake/Sbom/minimal.cmake b/tests/auto/cmake/RunCMake/Sbom/minimal.cmake index 4422314b2d5..27dfc0a3b12 100644 --- a/tests/auto/cmake/RunCMake/Sbom/minimal.cmake +++ b/tests/auto/cmake/RunCMake/Sbom/minimal.cmake @@ -1,17 +1,26 @@ # Needed to make the sbom functions available. find_package(Qt6 REQUIRED Core) +include(CommonResultGenIntro) + _qt_internal_setup_sbom( GENERATE_SBOM_DEFAULT "TRUE" ) +# This is used by common_result_gen.cmake. +set(SBOM_VERSION "1.0.0") + _qt_internal_sbom_begin_project( SUPPLIER "QtProjectTest" SUPPLIER_URL "https://qt-project.org/SbomTest" - VERSION "1.0.0" + VERSION "${SBOM_VERSION}" ) include(common_targets.cmake) _qt_internal_sbom_end_project() +include(CommonResultGen) + +# Also create separate sboms for some sibling projects under this subdir. +add_subdirectory(subprojects) diff --git a/tests/auto/cmake/RunCMake/Sbom/subprojects/CMakeLists.txt b/tests/auto/cmake/RunCMake/Sbom/subprojects/CMakeLists.txt new file mode 100644 index 00000000000..0d9e12534df --- /dev/null +++ b/tests/auto/cmake/RunCMake/Sbom/subprojects/CMakeLists.txt @@ -0,0 +1,5 @@ +# These subprojects look up the same system library dependency separately in each subdirectory +# scope to ensure that we simulate the same scenario as in a top-level qt5.git build, so that +# we try to reuse the same system library entity in multiple sboms. +add_subdirectory(subproj1) +add_subdirectory(subproj2) diff --git a/tests/auto/cmake/RunCMake/Sbom/subprojects/subproj1/CMakeLists.txt b/tests/auto/cmake/RunCMake/Sbom/subprojects/subproj1/CMakeLists.txt new file mode 100644 index 00000000000..a59908d8889 --- /dev/null +++ b/tests/auto/cmake/RunCMake/Sbom/subprojects/subproj1/CMakeLists.txt @@ -0,0 +1,51 @@ +project(subproj1) + +# This is used by common_result_gen.cmake. +set(SBOM_VERSION "1.0.1") +set(SBOM_PROJECT_NAME "${PROJECT_NAME}") + +_qt_internal_sbom_begin_project( + SBOM_PROJECT_NAME "${SBOM_PROJECT_NAME}" + INSTALL_SBOM_DIR "${SBOM_INSTALL_DIR}" + SUPPLIER "QtProjectTest" + SUPPLIER_URL "https://qt-project.org/SbomTest" + VERSION "${SBOM_VERSION}" +) + +add_library(subproj1_helper STATIC) +target_sources(subproj1_helper PRIVATE subproj1_helper.cpp) +target_link_libraries(subproj1_helper) +install(TARGETS subproj1_helper + RUNTIME DESTINATION bin + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib +) +_qt_internal_add_sbom(subproj1_helper + TYPE "LIBRARY" + RUNTIME_PATH bin + ARCHIVE_PATH lib + LIBRARY_PATH lib +) + + +# This will actually refer to the root project Threads, that gets brought in via Qt6::Core +# dependency. +find_package(Threads) +if(TARGET Threads::Threads) + _qt_internal_add_sbom(Threads::Threads + TYPE SYSTEM_LIBRARY + ) + target_link_libraries(subproj1_helper PRIVATE Threads::Threads) +endif() + +# Create another IMPORTED target that is meant to simulate a system library, so we don't refer to +# a target that exists in the root scope. +add_library(FancySystemLib IMPORTED INTERFACE) +_qt_internal_add_sbom(FancySystemLib + TYPE SYSTEM_LIBRARY +) +target_link_libraries(subproj1_helper PRIVATE FancySystemLib) + +_qt_internal_sbom_end_project() + +include(CommonResultGen) diff --git a/tests/auto/cmake/RunCMake/Sbom/subprojects/subproj1/subproj1_helper.cpp b/tests/auto/cmake/RunCMake/Sbom/subprojects/subproj1/subproj1_helper.cpp new file mode 100644 index 00000000000..f6b17d4b9f8 --- /dev/null +++ b/tests/auto/cmake/RunCMake/Sbom/subprojects/subproj1/subproj1_helper.cpp @@ -0,0 +1,4 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +int func() { return 0; }; diff --git a/tests/auto/cmake/RunCMake/Sbom/subprojects/subproj2/CMakeLists.txt b/tests/auto/cmake/RunCMake/Sbom/subprojects/subproj2/CMakeLists.txt new file mode 100644 index 00000000000..0a408ddf47f --- /dev/null +++ b/tests/auto/cmake/RunCMake/Sbom/subprojects/subproj2/CMakeLists.txt @@ -0,0 +1,46 @@ +project(subproj2) + +# This is used by common_result_gen.cmake. +set(SBOM_VERSION "1.0.2") +set(SBOM_PROJECT_NAME "${PROJECT_NAME}") + +_qt_internal_sbom_begin_project( + SBOM_PROJECT_NAME "${SBOM_PROJECT_NAME}" + INSTALL_SBOM_DIR "${SBOM_INSTALL_DIR}" + SUPPLIER "QtProjectTest" + SUPPLIER_URL "https://qt-project.org/SbomTest" + VERSION "${SBOM_VERSION}" +) + +add_library(subproj2_helper STATIC) +target_sources(subproj2_helper PRIVATE subproj2_helper.cpp) +target_link_libraries(subproj2_helper) +install(TARGETS subproj2_helper + RUNTIME DESTINATION bin + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib +) +_qt_internal_add_sbom(subproj2_helper + TYPE "LIBRARY" + RUNTIME_PATH bin + ARCHIVE_PATH lib + LIBRARY_PATH lib +) + +find_package(Threads) +if(TARGET Threads::Threads) + _qt_internal_add_sbom(Threads::Threads + TYPE SYSTEM_LIBRARY + ) + target_link_libraries(subproj2_helper PRIVATE Threads::Threads) +endif() + +add_library(FancySystemLib IMPORTED INTERFACE) +_qt_internal_add_sbom(FancySystemLib + TYPE SYSTEM_LIBRARY +) +target_link_libraries(subproj2_helper PRIVATE FancySystemLib) + +_qt_internal_sbom_end_project() + +include(CommonResultGen) diff --git a/tests/auto/cmake/RunCMake/Sbom/subprojects/subproj2/subproj2_helper.cpp b/tests/auto/cmake/RunCMake/Sbom/subprojects/subproj2/subproj2_helper.cpp new file mode 100644 index 00000000000..f6b17d4b9f8 --- /dev/null +++ b/tests/auto/cmake/RunCMake/Sbom/subprojects/subproj2/subproj2_helper.cpp @@ -0,0 +1,4 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +int func() { return 0; }; diff --git a/tests/auto/corelib/global/qcheckedint/tst_qcheckedint.cpp b/tests/auto/corelib/global/qcheckedint/tst_qcheckedint.cpp index fe509e558c6..72175f8e04c 100644 --- a/tests/auto/corelib/global/qcheckedint/tst_qcheckedint.cpp +++ b/tests/auto/corelib/global/qcheckedint/tst_qcheckedint.cpp @@ -347,7 +347,7 @@ void tst_QCheckedInt::division() // This causes an internal compiler error on MSVC, so skipping it there // Integrity's compiler says this code isn't constexpr. -#if (!defined(Q_CC_MSVC) || Q_CC_MSVC > 1944) && !defined(Q_CC_GHS) +#if (!defined(Q_CC_MSVC) || Q_CC_MSVC > 1950) && !defined(Q_CC_GHS) template <typename T> constexpr bool checkedIntTypeProperties() diff --git a/tests/auto/corelib/global/qgetputenv/tst_qgetputenv.cpp b/tests/auto/corelib/global/qgetputenv/tst_qgetputenv.cpp index bbe355061eb..0474ad974cb 100644 --- a/tests/auto/corelib/global/qgetputenv/tst_qgetputenv.cpp +++ b/tests/auto/corelib/global/qgetputenv/tst_qgetputenv.cpp @@ -17,15 +17,26 @@ class tst_QGetPutEnv : public QObject { Q_OBJECT private slots: + void init(); + void getSetCheck(); void encoding(); void intValue_data(); void intValue(); + +private: + QByteArray uniqueEnvVarName; }; +void tst_QGetPutEnv::init() +{ + QUuid uuid = QUuid::createUuid(); + uniqueEnvVarName = "QT_TEST_ENV_VAR_" + uuid.toByteArray(QUuid::Id128); +} + void tst_QGetPutEnv::getSetCheck() { - const char varName[] = "should_not_exist"; + const char *varName = uniqueEnvVarName.constData(); bool ok; @@ -117,13 +128,14 @@ void tst_QGetPutEnv::encoding() // The LATIN SMALL LETTER A WITH ACUTE is NFC for NFD: // U+0061 U+0301 LATIN SMALL LETTER A + COMBINING ACUTE ACCENT - const char varName[] = "should_not_exist"; + const char *varName = uniqueEnvVarName.constData(); + static const wchar_t rawvalue[] = { 'a', 0x00E1, 0x03B1, 0x0430, 0 }; QString value = QString::fromWCharArray(rawvalue); #if defined(Q_OS_WIN) - const wchar_t wvarName[] = L"should_not_exist"; - _wputenv_s(wvarName, rawvalue); + std::wstring wvarName = QString::fromUtf8(varName).toStdWString(); + _wputenv_s(wvarName.data(), rawvalue); #else // confirm the locale is UTF-8 if (value.toLocal8Bit() != "a\xc3\xa1\xce\xb1\xd0\xb0") @@ -203,7 +215,7 @@ void tst_QGetPutEnv::intValue_data() void tst_QGetPutEnv::intValue() { const int maxlen = (sizeof(qint64) * CHAR_BIT + 2) / 3; - const char varName[] = "should_not_exist"; + const char *varName = uniqueEnvVarName.constData(); QFETCH(QByteArray, value); QFETCH(qint64, expected); diff --git a/tests/auto/corelib/global/qglobal/tst_qglobal.cpp b/tests/auto/corelib/global/qglobal/tst_qglobal.cpp index 0b65673c393..bbcc8c5e5e1 100644 --- a/tests/auto/corelib/global/qglobal/tst_qglobal.cpp +++ b/tests/auto/corelib/global/qglobal/tst_qglobal.cpp @@ -464,7 +464,7 @@ Q_COREAPP_STARTUP_FUNCTION(myStartupFunc) void tst_QGlobal::qCoreAppStartupFunction() { - QCOMPARE(qStartupFunctionValue, 0); + qStartupFunctionValue = 0; int argc = 1; char *argv[] = { const_cast<char*>(QTest::currentAppName()) }; QCoreApplication app(argc, argv); diff --git a/tests/auto/corelib/global/qnumeric/tst_qnumeric.cpp b/tests/auto/corelib/global/qnumeric/tst_qnumeric.cpp index d2fee5124db..34c0fd11bed 100644 --- a/tests/auto/corelib/global/qnumeric/tst_qnumeric.cpp +++ b/tests/auto/corelib/global/qnumeric/tst_qnumeric.cpp @@ -1011,7 +1011,7 @@ SUB_OVERFLOW_UNSIGNED_TYPE_TEST(ulong, ULONG_MAX) #if defined(QT_HAS_128_BIT_MULTIPLICATION) // Compiling this causes an ICE in MSVC, so skipping it -#if !defined(Q_CC_MSVC) || Q_CC_MSVC > 1944 +#if !defined(Q_CC_MSVC) || Q_CC_MSVC > 1950 SIGNED_TYPE_TEST(qlonglong, LLONG_MIN, LLONG_MAX) UNSIGNED_TYPE_TEST(qulonglong, ULLONG_MAX) #endif diff --git a/tests/auto/corelib/io/qdebug/tst_qdebug.cpp b/tests/auto/corelib/io/qdebug/tst_qdebug.cpp index e0f677e1511..7603d84623b 100644 --- a/tests/auto/corelib/io/qdebug/tst_qdebug.cpp +++ b/tests/auto/corelib/io/qdebug/tst_qdebug.cpp @@ -1623,6 +1623,8 @@ void tst_QDebug::threadSafety() const #ifdef Q_OS_WASM QSKIP("threadSafety does not run on wasm"); #else + s_messages = {}; + MessageHandlerSetter mhs(threadSafeMessageHandler); const int numThreads = 10; QThreadPool::globalInstance()->setMaxThreadCount(numThreads); diff --git a/tests/auto/corelib/io/qloggingregistry/tst_qloggingregistry.cpp b/tests/auto/corelib/io/qloggingregistry/tst_qloggingregistry.cpp index 0fd62d40f62..9aa99a1b653 100644 --- a/tests/auto/corelib/io/qloggingregistry/tst_qloggingregistry.cpp +++ b/tests/auto/corelib/io/qloggingregistry/tst_qloggingregistry.cpp @@ -186,6 +186,12 @@ private slots: Q_ASSERT(!qApp); // Rules should not require an app to resolve + static bool calledOnce = false; + if (calledOnce) + QSKIP("QLoggingRegistry_environment can only run once"); + + calledOnce = true; + qputenv("QT_LOGGING_RULES", "qt.foo.bar=true"); QLoggingCategory qtEnabledByLoggingRule("qt.foo.bar"); QCOMPARE(qtEnabledByLoggingRule.isDebugEnabled(), true); diff --git a/tests/auto/corelib/kernel/qchronotimer/tst_qchronotimer.cpp b/tests/auto/corelib/kernel/qchronotimer/tst_qchronotimer.cpp index 96bbd60ab83..9a5e2a5f89b 100644 --- a/tests/auto/corelib/kernel/qchronotimer/tst_qchronotimer.cpp +++ b/tests/auto/corelib/kernel/qchronotimer/tst_qchronotimer.cpp @@ -326,7 +326,7 @@ void tst_QChronoTimer::remainingTimeDuringActivation() if (!singleShot) { // do it again - see QTBUG-46940 - remainingTime = std::chrono::milliseconds::min(); + remainingTime = std::chrono::nanoseconds::min(); QVERIFY(timeoutSpy.wait()); QCOMPARE_LE(remainingTime, timeout); QCOMPARE_GT(remainingTime, 0ns); diff --git a/tests/auto/corelib/kernel/qmetaproperty/tst_qmetaproperty.cpp b/tests/auto/corelib/kernel/qmetaproperty/tst_qmetaproperty.cpp index bde70e32233..10769165ec4 100644 --- a/tests/auto/corelib/kernel/qmetaproperty/tst_qmetaproperty.cpp +++ b/tests/auto/corelib/kernel/qmetaproperty/tst_qmetaproperty.cpp @@ -307,6 +307,13 @@ Q_DECLARE_OPERATORS_FOR_FLAGS(EnumFlagsTester::TestFlags) void tst_QMetaProperty::readAndWriteWithLazyRegistration() { + static bool executedOnce = false; + if (executedOnce) { + QSKIP("lazy registration only runs once per type per process"); + return; + } + executedOnce = true; + QVERIFY(!QMetaType::fromName("CustomReadObject*").isValid()); QVERIFY(!QMetaType::fromName("CustomWriteObject*").isValid()); diff --git a/tests/auto/corelib/kernel/qmetatype/tst_qmetatype.cpp b/tests/auto/corelib/kernel/qmetatype/tst_qmetatype.cpp index 683025fd409..3cf16367777 100644 --- a/tests/auto/corelib/kernel/qmetatype/tst_qmetatype.cpp +++ b/tests/auto/corelib/kernel/qmetatype/tst_qmetatype.cpp @@ -528,11 +528,25 @@ void tst_QMetaType::qMetaTypeId() QCOMPARE(::qMetaTypeId<qint8>(), QMetaType::fromType<qint8>().id()); } +class QPropObject : public QObject +{ + Q_OBJECT + Q_PROPERTY(QList<QVariant> prop READ prop WRITE setProp) + +public: + QPropObject() { propList << 42 << "Hello"; } + + QList<QVariant> prop() const { return propList; } + void setProp(const QList<QVariant> &list) { propList = list; } + QList<QVariant> propList; +}; + void tst_QMetaType::properties() { qRegisterMetaType<QList<QVariant> >("QList<QVariant>"); + QPropObject sut; - QVariant v = property("prop"); + QVariant v = sut.property("prop"); QCOMPARE(v.typeName(), "QVariantList"); @@ -542,8 +556,8 @@ void tst_QMetaType::properties() values << 43 << "world"; - QVERIFY(setProperty("prop", values)); - v = property("prop"); + QVERIFY(sut.setProperty("prop", values)); + v = sut.property("prop"); QCOMPARE(v.toList().size(), 4); } @@ -1450,6 +1464,11 @@ void tst_QMetaType::defaultConstructTrivial_QTBUG_109594() void tst_QMetaType::typedConstruct() { + static bool calledOnce = false; + if (calledOnce) + QSKIP("tst_QMetaType::typedConstruct can only run once"); + calledOnce = true; + auto testMetaObjectWriteOnGadget = [](QVariant &gadget, const QList<GadgetPropertyType> &properties) { auto metaObject = QMetaType(gadget.userType()).metaObject(); diff --git a/tests/auto/corelib/kernel/qmetatype/tst_qmetatype.h b/tests/auto/corelib/kernel/qmetatype/tst_qmetatype.h index 6f218bb0651..e9a177356e6 100644 --- a/tests/auto/corelib/kernel/qmetatype/tst_qmetatype.h +++ b/tests/auto/corelib/kernel/qmetatype/tst_qmetatype.h @@ -25,7 +25,6 @@ struct MessageHandlerCustom : public MessageHandler class tst_QMetaType: public QObject { Q_OBJECT - Q_PROPERTY(QList<QVariant> prop READ prop WRITE setProp) public: struct GadgetPropertyType { @@ -34,14 +33,8 @@ public: QVariant testData; }; - tst_QMetaType() { propList << 42 << "Hello"; } - - QList<QVariant> prop() const { return propList; } - void setProp(const QList<QVariant> &list) { propList = list; } - private: void registerGadget(const char * name, const QList<GadgetPropertyType> &gadgetProperties); - QList<QVariant> propList; private slots: #if QT_CONFIG(thread) diff --git a/tests/auto/corelib/kernel/qmetatype/tst_qmetatype2.cpp b/tests/auto/corelib/kernel/qmetatype/tst_qmetatype2.cpp index 661c1f6072b..93f23259745 100644 --- a/tests/auto/corelib/kernel/qmetatype/tst_qmetatype2.cpp +++ b/tests/auto/corelib/kernel/qmetatype/tst_qmetatype2.cpp @@ -148,6 +148,11 @@ void registerCustomTypeConversions() void tst_QMetaType::convertCustomType_data() { + static bool calledOnce = false; + if (calledOnce) + QSKIP("convertCustomType can only run once"); + calledOnce = true; + customTypeNotYetConvertible(); registerCustomTypeConversions(); @@ -363,6 +368,11 @@ void tst_QMetaType::compareCustomEqualOnlyType() void tst_QMetaType::customDebugStream() { + static bool calledOnce = false; + if (calledOnce) + QSKIP("customDebugStream can only run once"); + calledOnce = true; + MessageHandlerCustom handler(::qMetaTypeId<CustomDebugStreamableType>()); QVariant v1 = QVariant::fromValue(CustomDebugStreamableType()); handler.expectedMessage = "QVariant(CustomDebugStreamableType, string-content)"; diff --git a/tests/auto/corelib/kernel/qobject/tst_qobject.cpp b/tests/auto/corelib/kernel/qobject/tst_qobject.cpp index afb0bf0169a..6d9f7d12dcb 100644 --- a/tests/auto/corelib/kernel/qobject/tst_qobject.cpp +++ b/tests/auto/corelib/kernel/qobject/tst_qobject.cpp @@ -8736,6 +8736,9 @@ QT_END_NAMESPACE void tst_QObject::emitToDestroyedClass() { using namespace EmitToDestroyedClass; + assertionCallCount = 0; + wouldHaveAssertedCount = 0; + std::unique_ptr ptr = std::make_unique<Derived>(); QObject::connect(ptr.get(), &Base::theSignal, ptr.get(), &Derived::doNothing); QCOMPARE(assertionCallCount, 0); diff --git a/tests/auto/corelib/kernel/qtimer/tst_qtimer.cpp b/tests/auto/corelib/kernel/qtimer/tst_qtimer.cpp index 81316b061d0..98c184872f9 100644 --- a/tests/auto/corelib/kernel/qtimer/tst_qtimer.cpp +++ b/tests/auto/corelib/kernel/qtimer/tst_qtimer.cpp @@ -1202,10 +1202,10 @@ void tst_QTimer::singleShotDestructionBeforeEventDispatcher() // would be deleted by the QObject destructor, which is too late to // unregister the timer. - auto thr = QThread::create([] { + std::unique_ptr<QThread> thr{QThread::create([] { QObject o; QTimer::singleShot(300s, &o, [] {}); - }); + })}; thr->start(); thr->wait(); } diff --git a/tests/auto/corelib/kernel/qvariant/tst_qvariant.cpp b/tests/auto/corelib/kernel/qvariant/tst_qvariant.cpp index e713901101c..721d3d3af98 100644 --- a/tests/auto/corelib/kernel/qvariant/tst_qvariant.cpp +++ b/tests/auto/corelib/kernel/qvariant/tst_qvariant.cpp @@ -432,6 +432,9 @@ private slots: void get_QTransform() { get_impl(QTransform{1, 2, 3, 4, 5, 6, 7, 8, 9}); } // too large void get_NonDefaultConstructible(); + void reference(); + void pointer(); + private: using StdVariant = std::variant<std::monostate, // list here all the types with which we instantiate getIf_impl: @@ -6213,6 +6216,73 @@ void tst_QVariant::get_NonDefaultConstructible() get_impl(NonDefaultConstructible{42}); } +struct QVariantWrapper +{ +public: + static constexpr bool canNoexceptConvertToQVariant + = std::is_nothrow_copy_constructible_v<QVariant>; + static constexpr bool canNoexceptAssignQVariant + = std::is_nothrow_copy_assignable_v<QVariant>; + + QVariantWrapper(QVariant *content = nullptr) noexcept : m_content(content) {} + + QVariant content() const noexcept(canNoexceptConvertToQVariant) { return *m_content; } + void setContent(const QVariant &content) noexcept(canNoexceptAssignQVariant) + { + *m_content = content; + } + +private: + QVariant *m_content = nullptr; +}; + +QT_BEGIN_NAMESPACE +template<> +QVariant::ConstReference<QVariantWrapper>::operator QVariant() const + noexcept(QVariantWrapper::canNoexceptConvertToQVariant) +{ + return m_referred.content(); +} + +template<> +QVariant::Reference<QVariantWrapper> &QVariant::Reference<QVariantWrapper>::operator=( + const QVariant &content) noexcept(QVariantWrapper::canNoexceptAssignQVariant) +{ + m_referred.setContent(content); + return *this; +} +QT_END_NAMESPACE + +void tst_QVariant::reference() +{ + QVariant content(5); + + QVariant::ConstReference<QVariantWrapper> constRef(&content); + QCOMPARE(QVariant(constRef), QVariant(5)); + + QVariant::Reference<QVariantWrapper> ref(&content); + QCOMPARE(QVariant(ref), QVariant(5)); + + ref = QVariant(12); + QCOMPARE(QVariant(ref), QVariant(12)); + QCOMPARE(content, QVariant(12)); +} + +void tst_QVariant::pointer() +{ + QVariant content(5); + + QVariant::ConstPointer<QVariantWrapper> constPtr(&content); + QCOMPARE(*constPtr, QVariant(5)); + + QVariant::Pointer<QVariantWrapper> ptr(&content); + QCOMPARE(*ptr, QVariant(5)); + + *ptr = QVariant(12); + QCOMPARE(*ptr, QVariant(12)); + QCOMPARE(content, QVariant(12)); +} + template <typename T> T mutate(const T &t) { return t + t; } template <> diff --git a/tests/auto/corelib/plugin/CMakeLists.txt b/tests/auto/corelib/plugin/CMakeLists.txt index 5518231ace2..82fa9d18749 100644 --- a/tests/auto/corelib/plugin/CMakeLists.txt +++ b/tests/auto/corelib/plugin/CMakeLists.txt @@ -7,7 +7,9 @@ endif() add_subdirectory(quuid) if(QT_FEATURE_library) add_subdirectory(qpluginloader) - add_subdirectory(qlibrary) + if(QT_FEATURE_private_tests) + add_subdirectory(qlibrary) + endif() endif() if(QT_BUILD_SHARED_LIBS AND QT_FEATURE_library) add_subdirectory(qplugin) diff --git a/tests/auto/corelib/plugin/qlibrary/tst/CMakeLists.txt b/tests/auto/corelib/plugin/qlibrary/tst/CMakeLists.txt index fc452f37f53..62dd4a06c17 100644 --- a/tests/auto/corelib/plugin/qlibrary/tst/CMakeLists.txt +++ b/tests/auto/corelib/plugin/qlibrary/tst/CMakeLists.txt @@ -13,7 +13,7 @@ qt_internal_add_test(tst_qlibrary SOURCES ../tst_qlibrary.cpp TESTDATA ${test_data} - LIBRARIES mylib mylib2 + LIBRARIES mylib mylib2 Qt::CorePrivate ) add_dependencies(tst_qlibrary mylib mylib2) diff --git a/tests/auto/corelib/plugin/qlibrary/tst_qlibrary.cpp b/tests/auto/corelib/plugin/qlibrary/tst_qlibrary.cpp index 5ae49ff1707..d708f6def1c 100644 --- a/tests/auto/corelib/plugin/qlibrary/tst_qlibrary.cpp +++ b/tests/auto/corelib/plugin/qlibrary/tst_qlibrary.cpp @@ -6,6 +6,7 @@ #include <qdir.h> #include <qlibrary.h> #include <QtCore/QRegularExpression> +#include <QtCore/private/qlibrary_p.h> // Helper macros to let us know if some suffixes and prefixes are valid @@ -162,7 +163,12 @@ void tst_QLibrary::cleanup() }; for (const auto &entry : libs) { - do {} while (QLibrary(entry.name, entry.version).unload()); + bool unloaded = false; + do { + QLibrary lib(entry.name, entry.version); + auto libPrivate = QLibraryPrivate::get(&lib); + unloaded = libPrivate->unload(); + } while (unloaded); } } diff --git a/tests/auto/corelib/thread/qthreadonce/tst_qthreadonce.cpp b/tests/auto/corelib/thread/qthreadonce/tst_qthreadonce.cpp index 37e1f744f34..763ec71e277 100644 --- a/tests/auto/corelib/thread/qthreadonce/tst_qthreadonce.cpp +++ b/tests/auto/corelib/thread/qthreadonce/tst_qthreadonce.cpp @@ -80,6 +80,14 @@ QSingleton<SingletonObject> IncrementThread::singleton; void tst_QThreadOnce::sameThread_data() { + static bool executedOnce = false; + if (executedOnce) { + QSKIP("tst_QThreadOnce can only run once"); + return; + } + executedOnce = true; + + SingletonObject::runCount = 0; QTest::addColumn<int>("expectedValue"); @@ -105,6 +113,14 @@ void tst_QThreadOnce::sameThread() void tst_QThreadOnce::multipleThreads() { + static bool executedOnce = false; + if (executedOnce) { + QSKIP("tst_QThreadOnce can only run once"); + return; + } + executedOnce = true; + + #if defined(Q_OS_VXWORKS) const int NumberOfThreads = 20; #else @@ -136,6 +152,13 @@ void tst_QThreadOnce::multipleThreads() void tst_QThreadOnce::nesting() { + static bool executedOnce = false; + if (executedOnce) { + QSKIP("tst_QThreadOnce can only run once"); + return; + } + executedOnce = true; + int variable = 0; Q_ONCE { Q_ONCE { @@ -148,6 +171,14 @@ void tst_QThreadOnce::nesting() static void reentrant(int control, int &counter) { + static bool executedOnce = false; + if (executedOnce) { + QSKIP("tst_QThreadOnce can only run once"); + return; + } + executedOnce = true; + + Q_ONCE { if (counter) reentrant(--control, counter); @@ -159,6 +190,13 @@ static void reentrant(int control, int &counter) void tst_QThreadOnce::reentering() { + static bool executedOnce = false; + if (executedOnce) { + QSKIP("tst_QThreadOnce can only run once"); + return; + } + executedOnce = true; + const int WantedRecursions = 5; int count = 0; SingletonObject::runCount = 0; @@ -181,6 +219,14 @@ static void exception_helper(int &val) #ifndef QT_NO_EXCEPTIONS void tst_QThreadOnce::exception() { + static bool executedOnce = false; + if (executedOnce) { + QSKIP("tst_QThreadOnce can only run once"); + return; + } + executedOnce = true; + + int count = 0; try { diff --git a/tests/auto/corelib/time/CMakeLists.txt b/tests/auto/corelib/time/CMakeLists.txt index 1fb95e9245c..29e882429f4 100644 --- a/tests/auto/corelib/time/CMakeLists.txt +++ b/tests/auto/corelib/time/CMakeLists.txt @@ -8,6 +8,7 @@ if(QT_FEATURE_datetimeparser) add_subdirectory(qdatetimeparser) endif() add_subdirectory(qtime) -if(QT_FEATURE_timezone) - add_subdirectory(qtimezone) +add_subdirectory(qtimezone) +if(QT_FEATURE_timezone AND QT_FEATURE_developer_build) + add_subdirectory(qtimezonebackend) endif() diff --git a/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp b/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp index fde06d2edd9..1e9aeeef799 100644 --- a/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp +++ b/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp @@ -1262,8 +1262,7 @@ void tst_QDateTime::toString_strformat() QCOMPARE(testDateTime.toString(u"yyyy-MM-dd hh:mm:ss tt"), u"2013-01-01 01:02:03 +0000"_s); QCOMPARE(testDateTime.toString(u"yyyy-MM-dd hh:mm:ss ttt"), u"2013-01-01 01:02:03 +00:00"_s); -#if QT_CONFIG(icu) && !defined(Q_STL_DINKUMWARE) - // The Dinkum (VxWorks) exception may just be because it has an old ICU. +#if QT_CONFIG(icu) // Hopefully other timezone backends shall eventually agree with this: const QString longForm = u"2013-01-01 01:02:03 Coordinated Universal Time"_s; #else diff --git a/tests/auto/corelib/time/qtimezone/tst_qtimezone.cpp b/tests/auto/corelib/time/qtimezone/tst_qtimezone.cpp index 66fbac97977..d1d86a4802f 100644 --- a/tests/auto/corelib/time/qtimezone/tst_qtimezone.cpp +++ b/tests/auto/corelib/time/qtimezone/tst_qtimezone.cpp @@ -3,7 +3,9 @@ #include <QTest> #include <qtimezone.h> -#include <private/qtimezoneprivate_p.h> +#if QT_CONFIG(timezone) +# include <private/qtimezoneprivate_p.h> +#endif #include <private/qcomparisontesthelper_p.h> #include <qlocale.h> @@ -51,33 +53,21 @@ private Q_SLOTS: void checkOffset(); void stressTest(); void windowsId(); - void isValidId_data(); - void isValidId(); void serialize(); void malformed(); - // Backend tests + // Backend tests (see also ../qtimezonebackend/) void utcTest(); - void icuTest(); - void tzTest(); - void macTest(); void darwinTypes(); - void winTest(); void localeSpecificDisplayName_data(); void localeSpecificDisplayName(); - void roundtripDisplayNames_data(); - void roundtripDisplayNames(); + // Compatibility with std::chrono::tzdb: void stdCompatibility_data(); void stdCompatibility(); -#endif // timezone backends +#endif // timezone backends exist private: - void printTimeZone(const QTimeZone &tz); #if QT_CONFIG(timezone) -# if defined(QT_BUILD_INTERNAL) - // Generic tests of privates, called by implementation-specific private tests: - void testCetPrivate(const QTimeZonePrivate &tzp); - void testEpochTranPrivate(const QTimeZonePrivate &tzp); -# endif // QT_BUILD_INTERNAL + void printTimeZone(const QTimeZone &tz); // Where tzdb contains a link between zones in different territories, CLDR // doesn't treat those as aliases for one another. For details see "Links in // the tz database" at: @@ -102,6 +92,7 @@ private: static constexpr bool debug = false; }; +#if QT_CONFIG(timezone) void tst_QTimeZone::printTimeZone(const QTimeZone &tz) { QDateTime now = QDateTime::currentDateTime(); @@ -167,9 +158,11 @@ void tst_QTimeZone::printTimeZone(const QTimeZone &tz) qDebug() << "Transition before 1 Jun = " << tz.previousTransition(jun).atUtc; qDebug() << ""; } +#endif // feature timezone void tst_QTimeZone::createTest() { +#if QT_CONFIG(timezone) const QTimeZone tz("Pacific/Auckland"); if constexpr (debug) @@ -265,13 +258,16 @@ void tst_QTimeZone::createTest() QCOMPARE(result.at(i).daylightTimeOffset, expected.at(i).daylightTimeOffset); } } +#else + QSKIP("Test depends on backends, enabled by feature timezone"); +#endif // feature timezone } void tst_QTimeZone::nullTest() { QTimeZone nullTz1; QTimeZone nullTz2; - QTimeZone utc("UTC"); + QTimeZone utc(QTimeZone::UTC); // Validity tests QCOMPARE(nullTz1.isValid(), false); @@ -290,6 +286,7 @@ void tst_QTimeZone::nullTest() utc = nullTz1; QCOMPARE(utc.isValid(), false); +#if QT_CONFIG(timezone) QCOMPARE(nullTz1.id(), QByteArray()); QCOMPARE(nullTz1.territory(), QLocale::AnyTerritory); QCOMPARE(nullTz1.comment(), QString()); @@ -335,6 +332,7 @@ void tst_QTimeZone::nullTest() QCOMPARE(data.offsetFromUtc, invalidOffset); QCOMPARE(data.standardTimeOffset, invalidOffset); QCOMPARE(data.daylightTimeOffset, invalidOffset); +#endif // feature timezone } void tst_QTimeZone::assign() @@ -487,7 +485,7 @@ void tst_QTimeZone::offset() void tst_QTimeZone::dataStreamTest() { -#ifndef QT_NO_DATASTREAM +#if QT_CONFIG(timezone) && !defined(QT_NO_DATASTREAM) // Test the OffsetFromUtc backend serialization. First with a custom timezone: QTimeZone tz1("QST"_ba, 23456, u"Qt Standard Time"_s, u"QST"_s, QLocale::Norway, u"Qt Testing"_s); @@ -546,7 +544,7 @@ void tst_QTimeZone::dataStreamTest() QCOMPARE(ds.status(), QDataStream::Ok); } QCOMPARE(tz2.id(), tz1.id()); -#endif +#endif // feature timezone and enabled datastream } #if QT_CONFIG(timezone) @@ -791,7 +789,7 @@ void tst_QTimeZone::hasAlternativeName() void tst_QTimeZone::specificTransition_data() { -#if QT_CONFIG(timezone) && QT_CONFIG(timezone_tzdb) && defined(__GLIBCXX__) +#if QT_CONFIG(timezone_tzdb) && defined(__GLIBCXX__) QSKIP("libstdc++'s C++20 misreads the IANA DB for Moscow's transitions (among others)."); #endif #if defined Q_OS_ANDROID && !QT_CONFIG(timezone_tzdb) @@ -1159,120 +1157,6 @@ void tst_QTimeZone::windowsId() } } -void tst_QTimeZone::isValidId_data() -{ -#ifdef QT_BUILD_INTERNAL - QTest::addColumn<QByteArray>("input"); - QTest::addColumn<bool>("valid"); - - // a-z, A-Z, 0-9, '.', '-', '_' are valid chars - // Can't start with '-' - // Parts separated by '/', each part min 1 and max of 14 chars - // (Android has parts with lengths up to 17, so tolerates this as a special case.) -#define TESTSET(name, section, valid) \ - QTest::newRow(name " front") << QByteArray(section "/xyz/xyz") << valid; \ - QTest::newRow(name " middle") << QByteArray("xyz/" section "/xyz") << valid; \ - QTest::newRow(name " back") << QByteArray("xyz/xyz/" section) << valid - - // a-z, A-Z, 0-9, '.', '-', '_' are valid chars - // Can't start with '-' - // Parts separated by '/', each part min 1 and max of 14 chars - TESTSET("empty", "", false); - TESTSET("minimal", "m", true); -#if (defined(Q_OS_ANDROID) || QT_CONFIG(icu)) && !QT_CONFIG(timezone_tzdb) - TESTSET("maximal", "East-Saskatchewan", true); // Android actually uses this - TESTSET("too long", "North-Saskatchewan", false); // ... but thankfully not this. -#else - TESTSET("maximal", "12345678901234", true); - TESTSET("maximal twice", "12345678901234/12345678901234", true); - TESTSET("too long", "123456789012345", false); - TESTSET("too-long/maximal", "123456789012345/12345678901234", false); - TESTSET("maximal/too-long", "12345678901234/123456789012345", false); -#endif - - TESTSET("bad hyphen", "-hyphen", false); - TESTSET("good hyphen", "hy-phen", true); - - TESTSET("valid char _", "_", true); - TESTSET("valid char .", ".", true); - TESTSET("valid char :", ":", true); - TESTSET("valid char +", "+", true); - TESTSET("valid char A", "A", true); - TESTSET("valid char Z", "Z", true); - TESTSET("valid char a", "a", true); - TESTSET("valid char z", "z", true); - TESTSET("valid char 0", "0", true); - TESTSET("valid char 9", "9", true); - - TESTSET("valid pair az", "az", true); - TESTSET("valid pair AZ", "AZ", true); - TESTSET("valid pair 09", "09", true); - TESTSET("valid pair .z", ".z", true); - TESTSET("valid pair _z", "_z", true); - TESTSET("invalid pair -z", "-z", false); - - TESTSET("valid triple a/z", "a/z", true); - TESTSET("valid triple a.z", "a.z", true); - TESTSET("valid triple a-z", "a-z", true); - TESTSET("valid triple a_z", "a_z", true); - TESTSET("invalid triple a z", "a z", false); - TESTSET("invalid triple a\\z", "a\\z", false); - TESTSET("invalid triple a,z", "a,z", false); - - TESTSET("invalid space", " ", false); - TESTSET("invalid char ^", "^", false); - TESTSET("invalid char \"", "\"", false); - TESTSET("invalid char $", "$", false); - TESTSET("invalid char %", "%", false); - TESTSET("invalid char &", "&", false); - TESTSET("invalid char (", "(", false); - TESTSET("invalid char )", ")", false); - TESTSET("invalid char =", "=", false); - TESTSET("invalid char -", "-", false); - TESTSET("invalid char ?", "?", false); - TESTSET("invalid char ß", "ß", false); - TESTSET("invalid char \\x01", "\x01", false); - TESTSET("invalid char ' '", " ", false); - -#undef TESTSET - - QTest::newRow("az alone") << QByteArray("az") << true; - QTest::newRow("AZ alone") << QByteArray("AZ") << true; - QTest::newRow("09 alone") << QByteArray("09") << true; - QTest::newRow("a/z alone") << QByteArray("a/z") << true; - QTest::newRow("a.z alone") << QByteArray("a.z") << true; - QTest::newRow("a-z alone") << QByteArray("a-z") << true; - QTest::newRow("a_z alone") << QByteArray("a_z") << true; - QTest::newRow(".z alone") << QByteArray(".z") << true; - QTest::newRow("_z alone") << QByteArray("_z") << true; - QTest::newRow("a z alone") << QByteArray("a z") << false; - QTest::newRow("a\\z alone") << QByteArray("a\\z") << false; - QTest::newRow("a,z alone") << QByteArray("a,z") << false; - QTest::newRow("/z alone") << QByteArray("/z") << false; - QTest::newRow("-z alone") << QByteArray("-z") << false; -#if (defined(Q_OS_ANDROID) || QT_CONFIG(icu)) && !QT_CONFIG(timezone_tzdb) - QTest::newRow("long alone") << QByteArray("12345678901234567") << true; - QTest::newRow("over-long alone") << QByteArray("123456789012345678") << false; -#else - QTest::newRow("long alone") << QByteArray("12345678901234") << true; - QTest::newRow("over-long alone") << QByteArray("123456789012345") << false; -#endif - -#else - QSKIP("This test requires a Qt -developer-build."); -#endif // QT_BUILD_INTERNAL -} - -void tst_QTimeZone::isValidId() -{ -#ifdef QT_BUILD_INTERNAL - QFETCH(QByteArray, input); - QFETCH(bool, valid); - - QCOMPARE(QTimeZonePrivate::isValidId(input), valid); -#endif -} - void tst_QTimeZone::serialize() { int parts = 0; @@ -1365,7 +1249,7 @@ void tst_QTimeZone::utcTest() tz = QTimeZone(15 * 3600); // no IANA ID, so uses minimal id, skipping :00 minutes QVERIFY(tz.isValid()); - QCOMPARE(tz.id(), "UTC+15"); + QCOMPARE(tz.id(), "UTC+15:00"); QCOMPARE(tz.offsetFromUtc(now), 15 * 3600); QCOMPARE(tz.standardTimeOffset(now), 15 * 3600); QCOMPARE(tz.daylightTimeOffset(now), 0); @@ -1402,314 +1286,6 @@ void tst_QTimeZone::utcTest() QCOMPARE(tz.daylightTimeOffset(now), 0); } -// Relies on local variable names: zone tzp and locale enUS. -#define ZONE_DNAME_CHECK(type, name, val) \ - QCOMPARE(tzp.displayName(QTimeZone::type, QTimeZone::name, enUS), val); - -void tst_QTimeZone::icuTest() -{ -#if defined(QT_BUILD_INTERNAL) && QT_CONFIG(icu) && !QT_CONFIG(timezone_tzdb) && !defined(Q_OS_UNIX) - // Known datetimes - qint64 std = QDateTime(QDate(2012, 1, 1), QTime(0, 0), QTimeZone::UTC).toMSecsSinceEpoch(); - qint64 dst = QDateTime(QDate(2012, 6, 1), QTime(0, 0), QTimeZone::UTC).toMSecsSinceEpoch(); - - // Test default constructor - QIcuTimeZonePrivate tzpd; - QVERIFY(tzpd.isValid()); - - // Test invalid is not available: - QVERIFY(!tzpd.isTimeZoneIdAvailable("Gondwana/Erewhon")); - // and construction gives an invalid result: - QIcuTimeZonePrivate tzpi("Gondwana/Erewhon"); - QCOMPARE(tzpi.isValid(), false); - - // Test named constructor - QIcuTimeZonePrivate tzp("Europe/Berlin"); - QVERIFY(tzp.isValid()); - - // Only test names in debug mode, names used can vary by ICU version installed - if constexpr (debug) { - // Test display names by type - QLocale enUS("en_US"); - ZONE_DNAME_CHECK(StandardTime, LongName, u"Central European Standard Time"); - ZONE_DNAME_CHECK(StandardTime, ShortName, u"GMT+01:00"); - ZONE_DNAME_CHECK(StandardTime, OffsetName, u"UTC+01:00"); - ZONE_DNAME_CHECK(DaylightTime, LongName, u"Central European Summer Time"); - ZONE_DNAME_CHECK(DaylightTime, ShortName, u"GMT+02:00"); - ZONE_DNAME_CHECK(DaylightTime, OffsetName, u"UTC+02:00"); - // ICU C api does not support Generic Time yet, C++ api does - ZONE_DNAME_CHECK(GenericTime, LongName, u"Central European Standard Time"); - ZONE_DNAME_CHECK(GenericTime, ShortName, u"GMT+01:00"); - ZONE_DNAME_CHECK(GenericTime, OffsetName, u"UTC+01:00"); - - // Test Abbreviations - QCOMPARE(tzp.abbreviation(std), u"CET"); - QCOMPARE(tzp.abbreviation(dst), u"CEST"); - } - - testCetPrivate(tzp); - if (QTest::currentTestFailed()) - return; - testEpochTranPrivate(QIcuTimeZonePrivate("America/Toronto")); -#endif // ICU not on Unix, without tzdb -} - -void tst_QTimeZone::tzTest() -{ -#if defined QT_BUILD_INTERNAL && defined Q_OS_UNIX \ - && !QT_CONFIG(timezone_tzdb) && !defined Q_OS_DARWIN && !defined Q_OS_ANDROID - const auto UTC = QTimeZone::UTC; - // Known datetimes - qint64 std = QDateTime(QDate(2012, 1, 1), QTime(0, 0), UTC).toMSecsSinceEpoch(); - qint64 dst = QDateTime(QDate(2012, 6, 1), QTime(0, 0), UTC).toMSecsSinceEpoch(); - - // Test default constructor - QTzTimeZonePrivate tzpd; - QVERIFY(tzpd.isValid()); - - // Test invalid constructor - QTzTimeZonePrivate tzpi("Gondwana/Erewhon"); - QVERIFY(!tzpi.isValid()); - - // Test named constructor - QTzTimeZonePrivate tzp("Europe/Berlin"); - QVERIFY(tzp.isValid()); - - // Test POSIX-format value for $TZ: - QTimeZone tzposix("MET-1METDST-2,M3.5.0/02:00:00,M10.5.0/03:00:00"); - QVERIFY(tzposix.isValid()); - QVERIFY(tzposix.hasDaylightTime()); - - // Cope with stray space at start of value (QTBUG-135109): - QTimeZone syd(" AEST-10AEDT,M10.1.0,M4.1.0/3"); - QVERIFY(syd.isValid()); - QVERIFY(syd.hasDaylightTime()); - - // RHEL has been seen with this as Africa/Casablanca's POSIX rule: - QTzTimeZonePrivate permaDst("<+00>0<+01>,0/0,J365/25"); - const QTimeZone utcP1("UTC+01:00"); // Should always have same offset as permaDst - QVERIFY(permaDst.isValid()); - QVERIFY(permaDst.hasDaylightTime()); - QVERIFY(permaDst.isDaylightTime(QDate(2020, 1, 1).startOfDay(utcP1).toMSecsSinceEpoch())); - QVERIFY(permaDst.isDaylightTime(QDate(2020, 12, 31).endOfDay(utcP1).toMSecsSinceEpoch())); - // Note that the final /25 could be misunderstood as putting a fall-back at - // 1am on the next year's Jan 1st; check we don't do that: - QVERIFY(permaDst.isDaylightTime( - QDateTime(QDate(2020, 1, 1), QTime(1, 30), utcP1).toMSecsSinceEpoch())); - // It shouldn't have any transitions. QTimeZone::hasTransitions() only says - // whether the backend supports them, so ask for transitions in a wide - // enough interval that one would show up, if there are any: - QVERIFY(permaDst.transitions(QDate(2015, 1, 1).startOfDay(UTC).toMSecsSinceEpoch(), - QDate(2020, 1, 1).startOfDay(UTC).toMSecsSinceEpoch() - ).isEmpty()); - - QTimeZone tzBrazil("BRT+3"); // parts of Northern Brazil, as a POSIX rule - QVERIFY(tzBrazil.isValid()); - QCOMPARE(tzBrazil.offsetFromUtc(QDateTime(QDate(1111, 11, 11).startOfDay())), -10800); - - // Test display names by type, either ICU or abbreviation only - QLocale enUS(u"en_US"); - // Only test names in debug mode, names used can vary by ICU version installed - if constexpr (debug) { -#if QT_CONFIG(icu) - ZONE_DNAME_CHECK(StandardTime, LongName, "Central European Standard Time"); - ZONE_DNAME_CHECK(StandardTime, ShortName, "GMT+01:00"); - ZONE_DNAME_CHECK(StandardTime, OffsetName, "UTC+01:00"); - ZONE_DNAME_CHECK(DaylightTime, LongName, "Central European Summer Time"); - ZONE_DNAME_CHECK(DaylightTime, ShortName, "GMT+02:00"); - ZONE_DNAME_CHECK(DaylightTime, OffsetName, "UTC+02:00"); - // ICU C api does not support Generic Time yet, C++ api does - ZONE_DNAME_CHECK(GenericTime, LongName, "Central European Standard Time"); - ZONE_DNAME_CHECK(GenericTime, ShortName, "GMT+01:00"); - ZONE_DNAME_CHECK(GenericTime, OffsetName, "UTC+01:00"); -#else - ZONE_DNAME_CHECK(StandardTime, LongName, "CET"); - ZONE_DNAME_CHECK(StandardTime, ShortName, "CET"); - ZONE_DNAME_CHECK(StandardTime, OffsetName, "CET"); - ZONE_DNAME_CHECK(DaylightTime, LongName, "CEST"); - ZONE_DNAME_CHECK(DaylightTime, ShortName, "CEST"); - ZONE_DNAME_CHECK(DaylightTime, OffsetName, "CEST"); - ZONE_DNAME_CHECK(GenericTime, LongName, "CET"); - ZONE_DNAME_CHECK(GenericTime, ShortName, "CET"); - ZONE_DNAME_CHECK(GenericTime, OffsetName, "CET"); -#endif // icu - - // Test Abbreviations - QCOMPARE(tzp.abbreviation(std), u"CET"); - QCOMPARE(tzp.abbreviation(dst), u"CEST"); - } - - testCetPrivate(tzp); - if (QTest::currentTestFailed()) - return; - testEpochTranPrivate(QTzTimeZonePrivate("America/Toronto")); - if (QTest::currentTestFailed()) - return; - - // Test first and last transition rule - // Warning: This could vary depending on age of TZ file! - - // Test low date uses first rule found - constexpr qint64 ancient = -Q_INT64_C(9999999999999); - // Note: Depending on the OS in question, the database may be carrying the - // Local Mean Time. which for Berlin is 0:53:28 - QTimeZonePrivate::Data dat = tzp.data(ancient); - QCOMPARE(dat.atMSecsSinceEpoch, ancient); - QCOMPARE(dat.daylightTimeOffset, 0); - if (dat.abbreviation == u"LMT") { - QCOMPARE(dat.standardTimeOffset, 3208); - } else { - QCOMPARE(dat.standardTimeOffset, 3600); - - constexpr qint64 invalidTime = std::numeric_limits<qint64>::min(); - constexpr int invalidOffset = std::numeric_limits<int>::min(); - // Test previous to low value is invalid - dat = tzp.previousTransition(ancient); - QCOMPARE(dat.atMSecsSinceEpoch, invalidTime); - QCOMPARE(dat.standardTimeOffset, invalidOffset); - QCOMPARE(dat.daylightTimeOffset, invalidOffset); - } - - dat = tzp.nextTransition(ancient); - QCOMPARE(QDateTime::fromMSecsSinceEpoch(dat.atMSecsSinceEpoch, - QTimeZone::fromSecondsAheadOfUtc(3600)), - QDateTime(QDate(1893, 4, 1), QTime(0, 6, 32), - QTimeZone::fromSecondsAheadOfUtc(3600))); - QCOMPARE(dat.standardTimeOffset, 3600); - QCOMPARE(dat.daylightTimeOffset, 0); - - // Date-times late enough to exercise POSIX rules: - qint64 stdHi = QDate(2100, 1, 1).startOfDay(UTC).toMSecsSinceEpoch(); - qint64 dstHi = QDate(2100, 6, 1).startOfDay(UTC).toMSecsSinceEpoch(); - // Relevant last Sundays in October and March: - QCOMPARE(Qt::DayOfWeek(QDate(2099, 10, 25).dayOfWeek()), Qt::Sunday); - QCOMPARE(Qt::DayOfWeek(QDate(2100, 3, 28).dayOfWeek()), Qt::Sunday); - QCOMPARE(Qt::DayOfWeek(QDate(2100, 10, 31).dayOfWeek()), Qt::Sunday); - - dat = tzp.data(stdHi); - QCOMPARE(dat.atMSecsSinceEpoch - stdHi, qint64(0)); - QCOMPARE(dat.offsetFromUtc, 3600); - QCOMPARE(dat.standardTimeOffset, 3600); - QCOMPARE(dat.daylightTimeOffset, 0); - - dat = tzp.data(dstHi); - QCOMPARE(dat.atMSecsSinceEpoch - dstHi, qint64(0)); - QCOMPARE(dat.offsetFromUtc, 7200); - QCOMPARE(dat.standardTimeOffset, 3600); - QCOMPARE(dat.daylightTimeOffset, 3600); - - dat = tzp.previousTransition(stdHi); - QCOMPARE(dat.abbreviation, u"CET"); - QCOMPARE(QDateTime::fromMSecsSinceEpoch(dat.atMSecsSinceEpoch, UTC), - QDateTime(QDate(2099, 10, 25), QTime(3, 0), QTimeZone::fromSecondsAheadOfUtc(7200))); - QCOMPARE(dat.offsetFromUtc, 3600); - QCOMPARE(dat.standardTimeOffset, 3600); - QCOMPARE(dat.daylightTimeOffset, 0); - - dat = tzp.previousTransition(dstHi); - QCOMPARE(dat.abbreviation, u"CEST"); - QCOMPARE(QDateTime::fromMSecsSinceEpoch(dat.atMSecsSinceEpoch, UTC), - QDateTime(QDate(2100, 3, 28), QTime(2, 0), QTimeZone::fromSecondsAheadOfUtc(3600))); - QCOMPARE(dat.offsetFromUtc, 7200); - QCOMPARE(dat.standardTimeOffset, 3600); - QCOMPARE(dat.daylightTimeOffset, 3600); - - dat = tzp.nextTransition(stdHi); - QCOMPARE(dat.abbreviation, u"CEST"); - QCOMPARE(QDateTime::fromMSecsSinceEpoch(dat.atMSecsSinceEpoch, UTC), - QDateTime(QDate(2100, 3, 28), QTime(2, 0), QTimeZone::fromSecondsAheadOfUtc(3600))); - QCOMPARE(dat.offsetFromUtc, 7200); - QCOMPARE(dat.standardTimeOffset, 3600); - QCOMPARE(dat.daylightTimeOffset, 3600); - - dat = tzp.nextTransition(dstHi); - QCOMPARE(dat.abbreviation, u"CET"); - QCOMPARE(QDateTime::fromMSecsSinceEpoch(dat.atMSecsSinceEpoch, - QTimeZone::fromSecondsAheadOfUtc(3600)), - QDateTime(QDate(2100, 10, 31), QTime(3, 0), QTimeZone::fromSecondsAheadOfUtc(7200))); - QCOMPARE(dat.offsetFromUtc, 3600); - QCOMPARE(dat.standardTimeOffset, 3600); - QCOMPARE(dat.daylightTimeOffset, 0); - - // Test TZ timezone vs UTC timezone for non-whole-hour negative offset: - QTzTimeZonePrivate tztz1("America/Caracas"); - QUtcTimeZonePrivate tzutc1("UTC-04:30"); - QVERIFY(tztz1.isValid()); - QVERIFY(tzutc1.isValid()); - QTzTimeZonePrivate::Data datatz1 = tztz1.data(std); - QTzTimeZonePrivate::Data datautc1 = tzutc1.data(std); - QCOMPARE(datatz1.offsetFromUtc, datautc1.offsetFromUtc); - - // Test TZ timezone vs UTC timezone for non-whole-hour positive offset: - QTzTimeZonePrivate tztz2k("Asia/Kolkata"); // New name - QTzTimeZonePrivate tztz2c("Asia/Calcutta"); // Legacy name - // Can't assign QtzTZP, so use a reference; prefer new name. - QTzTimeZonePrivate &tztz2 = tztz2k.isValid() ? tztz2k : tztz2c; - QUtcTimeZonePrivate tzutc2("UTC+05:30"); - QVERIFY2(tztz2.isValid(), tztz2.id().constData()); - QVERIFY(tzutc2.isValid()); - QTzTimeZonePrivate::Data datatz2 = tztz2.data(std); - QTzTimeZonePrivate::Data datautc2 = tzutc2.data(std); - QCOMPARE(datatz2.offsetFromUtc, datautc2.offsetFromUtc); - - // Test a timezone with an abbreviation that isn't all letters: - QTzTimeZonePrivate tzBarnaul("Asia/Barnaul"); - if (tzBarnaul.isValid()) { - QCOMPARE(tzBarnaul.data(std).abbreviation, u"+07"); - - // first full day of the new rule (tzdata2016b) - QDateTime dt(QDate(2016, 3, 28), QTime(0, 0), UTC); - QCOMPARE(tzBarnaul.data(dt.toMSecsSinceEpoch()).abbreviation, u"+07"); - } -#endif // QT_BUILD_INTERNAL && Q_OS_UNIX && !timezone_tzdb && !Q_OS_DARWIN && !Q_OS_ANDROID -} - -void tst_QTimeZone::macTest() -{ -#if defined(QT_BUILD_INTERNAL) && defined(Q_OS_DARWIN) && !QT_CONFIG(timezone_tzdb) - // Known datetimes - qint64 std = QDateTime(QDate(2012, 1, 1), QTime(0, 0), QTimeZone::UTC).toMSecsSinceEpoch(); - qint64 dst = QDateTime(QDate(2012, 6, 1), QTime(0, 0), QTimeZone::UTC).toMSecsSinceEpoch(); - - // Test default constructor - QMacTimeZonePrivate tzpd; - QVERIFY(tzpd.isValid()); - - // Test invalid constructor - QMacTimeZonePrivate tzpi("Gondwana/Erewhon"); - QCOMPARE(tzpi.isValid(), false); - - // Test named constructor - QMacTimeZonePrivate tzp("Europe/Berlin"); - QVERIFY(tzp.isValid()); - - // Only test names in debug mode, names used can vary by version - if constexpr (debug) { - // Test display names by type - QLocale enUS(u"en_US"); - ZONE_DNAME_CHECK(StandardTime, LongName, "Central European Standard Time"); - ZONE_DNAME_CHECK(StandardTime, ShortName, "GMT+01:00"); - ZONE_DNAME_CHECK(StandardTime, OffsetName, "UTC+01:00"); - ZONE_DNAME_CHECK(DaylightTime, LongName, "Central European Summer Time"); - ZONE_DNAME_CHECK(DaylightTime, ShortName, "GMT+02:00"); - ZONE_DNAME_CHECK(DaylightTime, OffsetName, "UTC+02:00"); - // ICU C api does not support Generic Time yet, C++ api does - ZONE_DNAME_CHECK(GenericTime, LongName, "Central European Time"); - ZONE_DNAME_CHECK(GenericTime, ShortName, "Germany Time"); - ZONE_DNAME_CHECK(GenericTime, OffsetName, "UTC+01:00"); - - // Test Abbreviations - QCOMPARE(tzp.abbreviation(std), u"CET"); - QCOMPARE(tzp.abbreviation(dst), u"CEST"); - } - - testCetPrivate(tzp); - if (QTest::currentTestFailed()) - return; - testEpochTranPrivate(QMacTimeZonePrivate("America/Toronto")); -#endif // QT_BUILD_INTERNAL && Q_OS_DARWIN without tzdb -} - void tst_QTimeZone::darwinTypes() { #ifndef Q_OS_DARWIN @@ -1720,59 +1296,6 @@ void tst_QTimeZone::darwinTypes() #endif } -void tst_QTimeZone::winTest() -{ -#if defined(QT_BUILD_INTERNAL) && defined(USING_WIN_TZ) - // Known datetimes - qint64 std = QDateTime(QDate(2012, 1, 1), QTime(0, 0), QTimeZone::UTC).toMSecsSinceEpoch(); - qint64 dst = QDateTime(QDate(2012, 6, 1), QTime(0, 0), QTimeZone::UTC).toMSecsSinceEpoch(); - - // Test default constructor - QWinTimeZonePrivate tzpd; - if constexpr (debug) - qDebug() << "System ID = " << tzpd.id() - << tzpd.displayName(QTimeZone::StandardTime, QTimeZone::LongName, QLocale()) - << tzpd.displayName(QTimeZone::GenericTime, QTimeZone::LongName, QLocale()); - QVERIFY(tzpd.isValid()); - - // Test invalid constructor - QWinTimeZonePrivate tzpi("Gondwana/Erewhon"); - QCOMPARE(tzpi.isValid(), false); - - // Test named constructor - QWinTimeZonePrivate tzp("Europe/Berlin"); - QVERIFY(tzp.isValid()); - - // Only test names in debug mode, names used can vary by version - if constexpr (debug) { - // Test display names by type - QLocale enUS(u"en_US"); - ZONE_DNAME_CHECK(StandardTime, LongName, "W. Europe Standard Time"); - ZONE_DNAME_CHECK(StandardTime, ShortName, "W. Europe Standard Time"); - ZONE_DNAME_CHECK(StandardTime, OffsetName, "UTC+01:00"); - ZONE_DNAME_CHECK(DaylightTime, LongName, "W. Europe Daylight Time"); - ZONE_DNAME_CHECK(DaylightTime, ShortName, "W. Europe Daylight Time"); - ZONE_DNAME_CHECK(DaylightTime, OffsetName, "UTC+02:00"); - ZONE_DNAME_CHECK(GenericTime, LongName, - "(UTC+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna"); - ZONE_DNAME_CHECK(GenericTime, ShortName, - "(UTC+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna"); - ZONE_DNAME_CHECK(GenericTime, OffsetName, "UTC+01:00"); - - // Test Abbreviations - QCOMPARE(tzp.abbreviation(std), u"CET"); - QCOMPARE(tzp.abbreviation(dst), u"CEST"); - } - - testCetPrivate(tzp); - if (QTest::currentTestFailed()) - return; - testEpochTranPrivate(QWinTimeZonePrivate("America/Toronto")); -#endif // QT_BUILD_INTERNAL && USING_WIN_TZ -} - -#undef ZONE_DNAME_CHECK - void tst_QTimeZone::localeSpecificDisplayName_data() { QTest::addColumn<QByteArray>("zoneName"); @@ -1849,285 +1372,6 @@ void tst_QTimeZone::localeSpecificDisplayName() #endif } -void tst_QTimeZone::roundtripDisplayNames_data() -{ -#ifdef QT_BUILD_INTERNAL - QTest::addColumn<QTimeZone>("zone"); - QTest::addColumn<QLocale>("locale"); - QTest::addColumn<QTimeZone::TimeType>("type"); - - constexpr QTimeZone::TimeType types[] = { - QTimeZone::GenericTime, QTimeZone::StandardTime, QTimeZone::DaylightTime - }; - const auto typeName = [](QTimeZone::TimeType type) { - switch (type) { - case QTimeZone::GenericTime: return "Gen"; - case QTimeZone::StandardTime: return "Std"; - case QTimeZone::DaylightTime: return "DST"; - } - Q_UNREACHABLE_RETURN("Unrecognised"); - }; - const QList<QByteArray> allList = (QTimeZone::availableTimeZoneIds() << "Vulcan/ShiKahr"_ba); -#ifdef EXHAUSTIVE_ZONE_DISPLAY - const QList<QByteArray> idList = allList; -#else - const QList<QByteArray> idList = { - "Africa/Casablanca"_ba, "Africa/Lagos"_ba, "Africa/Tunis"_ba, - "America/Caracas"_ba, "America/Indiana/Tell_City"_ba, "America/Managua"_ba, - "Asia/Bangkok"_ba, "Asia/Colombo"_ba, "Asia/Tokyo"_ba, - "Atlantic/Bermuda"_ba, "Atlantic/Faroe"_ba, "Atlantic/Madeira"_ba, - "Australia/Broken_Hill"_ba, "Australia/NSW"_ba, "Australia/Tasmania"_ba, - "Brazil/Acre"_ba, "CST6CDT"_ba, "Canada/Atlantic"_ba, - "Chile/EasterIsland"_ba, "Etc/Greenwich"_ba, "Etc/Universal"_ba, - "Europe/Guernsey"_ba, "Europe/Kaliningrad"_ba, "Europe/Kyiv"_ba, - "Europe/Prague"_ba, "Europe/Vatican"_ba, - "Indian/Comoro"_ba, "Mexico/BajaSur"_ba, - "Pacific/Bougainville"_ba, "Pacific/Midway"_ba, "Pacific/Wallis"_ba, - "US/Aleutian"_ba, - "UTC"_ba, - // Those named overtly in tst_QDateTime - special cases first: - "UTC-02:00"_ba, "UTC+02:00"_ba, "UTC+12:00"_ba, - "Etc/GMT+3"_ba, "GMT-0"_ba, "GMT"_ba, - // ... then ordinary names in alphabetic order: - "America/Anchorage"_ba, "America/Metlakatla"_ba, "America/New_York"_ba, - "America/Sao_Paulo"_ba, "America/Toronto"_ba, "America/Vancouver"_ba, - "Asia/Kathmandu"_ba, "Asia/Manila"_ba, "Asia/Singapore"_ba, - "Australia/Brisbane"_ba, "Australia/Eucla"_ba, "Australia/Sydney"_ba, - "Europe/Berlin"_ba, "Europe/Helsinki"_ba, "Europe/Lisbon"_ba, "Europe/Oslo"_ba, - "Europe/Rome"_ba, - "Pacific/Apia"_ba, "Pacific/Auckland"_ba, "Pacific/Kiritimati"_ba, - "Vulcan/ShiKahr"_ba // Invalid: also worth testing. - }; - // Some valid zones in that list may be absent from the platform's - // availableTimeZoneIds(), yet in fact work when used as it's asked to - // instantiate them (e.g. Etc/Universal on macOS). This can give them a - // displayName() that we fail to decode, without timezone_locale, due to - // only trying the availableTimeZoneIds() in findLongNamePrefix(). So we - // have to filter on membership of allList when creating rows. -#endif // Exhaustive - const QLocale fr(QLocale::French, QLocale::France); - const QLocale hi(QLocale::Hindi, QLocale::India); - for (const QByteArray &id : idList) { - if (id == "localtime"_ba || id == "posixrules"_ba || !allList.contains(id)) - continue; - QTimeZone zone = QTimeZone(id); - if (!zone.isValid()) - continue; - for (const auto type : types) { - QTest::addRow("%s@fr_FR/%s", id.constData(), typeName(type)) - << zone << fr << type; - QTest::addRow("%s@hi_IN/%s", id.constData(), typeName(type)) - << zone << hi << type; - } - } -#else - QSKIP("Test needs access to internal APIs"); -#endif -} - -void tst_QTimeZone::roundtripDisplayNames() -{ -#ifdef QT_BUILD_INTERNAL - QFETCH(const QTimeZone, zone); - QFETCH(const QLocale, locale); - QFETCH(const QTimeZone::TimeType, type); - static const QDateTime jan = QDateTime(QDate(2015, 1, 1), QTime(12, 0), QTimeZone::UTC); - static const QDateTime jul = QDateTime(QDate(2015, 7, 1), QTime(12, 0), QTimeZone::UTC); - const QDateTime dt = zone.isDaylightTime(jul) == (type == QTimeZone::DaylightTime) ? jul : jan; - - // Some zones exercise region format. - const QString name = zone.displayName(type, QTimeZone::LongName, locale); - if (!name.isEmpty()) { - const auto tran = QTimeZonePrivate::extractPrivate(zone)->data(type); - const qint64 when = tran.atMSecsSinceEpoch == QTimeZonePrivate::invalidMSecs() - ? dt.toMSecsSinceEpoch() : tran.atMSecsSinceEpoch; - const QString extended = name + "some spurious cruft"_L1; - auto match = - QTimeZonePrivate::findLongNamePrefix(extended, locale, when); - if (!match) - match = QTimeZonePrivate::findLongNamePrefix(extended, locale); - if (!match) - match = QTimeZonePrivate::findNarrowOffsetPrefix(extended, locale); - if (!match) - match = QTimeZonePrivate::findLongUtcPrefix(extended); - auto report = qScopeGuard([=]() { - qDebug() << "At" << QDateTime::fromMSecsSinceEpoch(when, QTimeZone::UTC) - << "via" << name; - }); - QCOMPARE(match.nameLength, name.size()); - report.dismiss(); -#if 0 - if (match.ianaId != zone.id()) { - const QTimeZone found = QTimeZone(match.ianaId); - if (QTimeZonePrivate::extractPrivate(found)->offsetFromUtc(when) - != QTimeZonePrivate::extractPrivate(zone)->offsetFromUtc(when)) { - // For DST, some zones haven't done it in ages, so tran may be ancient. - // Meanwhile, match.ianaId is typically the canonical zone for a metazone. - // That, in turn, may not have been doing DST when zone was. - // So we can't rely on a match, but can report the mismatches. - qDebug() << "Long name" << name << "on" - << QTimeZonePrivate::extractPrivate(zone)->offsetFromUtc(when) - << "at" << QDateTime::fromMSecsSinceEpoch(when, QTimeZone::UTC) - << "got" << match.ianaId << "on" - << QTimeZonePrivate::extractPrivate(found)->offsetFromUtc(when); - // There are also some absurdly over-generic names, that lead to - // ambiguities, e.g. "heure : West" - } - } -#endif // Debug code - } else if (type != QTimeZone::DaylightTime) { /* Zones with no DST have no DST-name */ - qDebug("Empty display name"); - } -#else - Q_ASSERT(!"Should be skipped when building data table"); -#endif -} - -#ifdef QT_BUILD_INTERNAL -// Test each private produces the same basic results for CET -void tst_QTimeZone::testCetPrivate(const QTimeZonePrivate &tzp) -{ - // Known datetimes - const auto UTC = QTimeZone::UTC; - const auto eastOneHour = QTimeZone::fromSecondsAheadOfUtc(3600); - qint64 std = QDateTime(QDate(2012, 1, 1), QTime(0, 0), UTC).toMSecsSinceEpoch(); - qint64 dst = QDateTime(QDate(2012, 6, 1), QTime(0, 0), UTC).toMSecsSinceEpoch(); - qint64 prev = QDateTime(QDate(2011, 1, 1), QTime(0, 0), UTC).toMSecsSinceEpoch(); - - QCOMPARE(tzp.offsetFromUtc(std), 3600); - QCOMPARE(tzp.offsetFromUtc(dst), 7200); - - QCOMPARE(tzp.standardTimeOffset(std), 3600); - QCOMPARE(tzp.standardTimeOffset(dst), 3600); - - QCOMPARE(tzp.daylightTimeOffset(std), 0); - QCOMPARE(tzp.daylightTimeOffset(dst), 3600); - - QCOMPARE(tzp.hasDaylightTime(), true); - QCOMPARE(tzp.isDaylightTime(std), false); - QCOMPARE(tzp.isDaylightTime(dst), true); - - QTimeZonePrivate::Data dat = tzp.data(std); - QCOMPARE(dat.atMSecsSinceEpoch, std); - QCOMPARE(dat.offsetFromUtc, 3600); - QCOMPARE(dat.standardTimeOffset, 3600); - QCOMPARE(dat.daylightTimeOffset, 0); - QCOMPARE(dat.abbreviation, tzp.abbreviation(std)); - - dat = tzp.data(dst); - QCOMPARE(dat.atMSecsSinceEpoch, dst); - QCOMPARE(dat.offsetFromUtc, 7200); - QCOMPARE(dat.standardTimeOffset, 3600); - QCOMPARE(dat.daylightTimeOffset, 3600); - QCOMPARE(dat.abbreviation, tzp.abbreviation(dst)); - - // Only test transitions if host system supports them - if (tzp.hasTransitions()) { - QTimeZonePrivate::Data tran = tzp.nextTransition(std); - // 2012-03-25 02:00 CET, +1 -> +2 - QCOMPARE(QDateTime::fromMSecsSinceEpoch(tran.atMSecsSinceEpoch, UTC), - QDateTime(QDate(2012, 3, 25), QTime(2, 0), eastOneHour)); - QCOMPARE(tran.offsetFromUtc, 7200); - QCOMPARE(tran.standardTimeOffset, 3600); - QCOMPARE(tran.daylightTimeOffset, 3600); - - tran = tzp.nextTransition(dst); - // 2012-10-28 03:00 CEST, +2 -> +1 - QCOMPARE(QDateTime::fromMSecsSinceEpoch(tran.atMSecsSinceEpoch, UTC), - QDateTime(QDate(2012, 10, 28), QTime(3, 0), - QTimeZone::fromSecondsAheadOfUtc(2 * 3600))); - QCOMPARE(tran.offsetFromUtc, 3600); - QCOMPARE(tran.standardTimeOffset, 3600); - QCOMPARE(tran.daylightTimeOffset, 0); - - tran = tzp.previousTransition(std); - // 2011-10-30 03:00 CEST, +2 -> +1 - QCOMPARE(QDateTime::fromMSecsSinceEpoch(tran.atMSecsSinceEpoch, UTC), - QDateTime(QDate(2011, 10, 30), QTime(3, 0), - QTimeZone::fromSecondsAheadOfUtc(2 * 3600))); - QCOMPARE(tran.offsetFromUtc, 3600); - QCOMPARE(tran.standardTimeOffset, 3600); - QCOMPARE(tran.daylightTimeOffset, 0); - - tran = tzp.previousTransition(dst); - // 2012-03-25 02:00 CET, +1 -> +2 (again) - QCOMPARE(QDateTime::fromMSecsSinceEpoch(tran.atMSecsSinceEpoch, UTC), - QDateTime(QDate(2012, 3, 25), QTime(2, 0), eastOneHour)); - QCOMPARE(tran.offsetFromUtc, 7200); - QCOMPARE(tran.standardTimeOffset, 3600); - QCOMPARE(tran.daylightTimeOffset, 3600); - - QTimeZonePrivate::DataList expected; - // 2011-03-27 02:00 CET, +1 -> +2 - tran.atMSecsSinceEpoch = QDateTime(QDate(2011, 3, 27), QTime(2, 0), - eastOneHour).toMSecsSinceEpoch(); - tran.offsetFromUtc = 7200; - tran.standardTimeOffset = 3600; - tran.daylightTimeOffset = 3600; - expected << tran; - // 2011-10-30 03:00 CEST, +2 -> +1 - tran.atMSecsSinceEpoch = QDateTime(QDate(2011, 10, 30), QTime(3, 0), - QTimeZone::fromSecondsAheadOfUtc(2 * 3600) - ).toMSecsSinceEpoch(); - tran.offsetFromUtc = 3600; - tran.standardTimeOffset = 3600; - tran.daylightTimeOffset = 0; - expected << tran; - QTimeZonePrivate::DataList result = tzp.transitions(prev, std); - QCOMPARE(result.size(), expected.size()); - for (int i = 0; i < expected.size(); ++i) { - QCOMPARE(QDateTime::fromMSecsSinceEpoch(result.at(i).atMSecsSinceEpoch, eastOneHour), - QDateTime::fromMSecsSinceEpoch(expected.at(i).atMSecsSinceEpoch, eastOneHour)); - QCOMPARE(result.at(i).offsetFromUtc, expected.at(i).offsetFromUtc); - QCOMPARE(result.at(i).standardTimeOffset, expected.at(i).standardTimeOffset); - QCOMPARE(result.at(i).daylightTimeOffset, expected.at(i).daylightTimeOffset); - } - } -} - -// Needs a zone with DST around the epoch; currently America/Toronto (EST5EDT) -void tst_QTimeZone::testEpochTranPrivate(const QTimeZonePrivate &tzp) -{ - if (!tzp.hasTransitions()) - return; // test only viable for transitions - - const auto UTC = QTimeZone::UTC; - const auto hour = std::chrono::hours{1}; - QTimeZonePrivate::Data tran = tzp.nextTransition(0); // i.e. first after epoch - // 1970-04-26 02:00 EST, -5 -> -4 - const QDateTime after = QDateTime(QDate(1970, 4, 26), QTime(2, 0), - QTimeZone::fromDurationAheadOfUtc(-5 * hour)); - const QDateTime found = QDateTime::fromMSecsSinceEpoch(tran.atMSecsSinceEpoch, UTC); -#ifdef USING_WIN_TZ // MS gets the date wrong: 5th April instead of 26th. - QCOMPARE(found.toOffsetFromUtc(-5 * 3600).time(), after.time()); -#else - QCOMPARE(found, after); -#endif - QCOMPARE(tran.offsetFromUtc, -4 * 3600); - QCOMPARE(tran.standardTimeOffset, -5 * 3600); - QCOMPARE(tran.daylightTimeOffset, 3600); - - // Pre-epoch time-zones might not be supported at all: - tran = tzp.nextTransition(QDateTime(QDate(1601, 1, 1), QTime(0, 0), UTC).toMSecsSinceEpoch()); - if (tran.atMSecsSinceEpoch != QTimeZonePrivate::invalidMSecs() - // Toronto *did* have a transition before 1970 (DST since 1918): - && tran.atMSecsSinceEpoch < 0) { - // ... but, if they are, we should be able to search back to them: - tran = tzp.previousTransition(0); // i.e. last before epoch - // 1969-10-26 02:00 EDT, -4 -> -5 - QCOMPARE(QDateTime::fromMSecsSinceEpoch(tran.atMSecsSinceEpoch, UTC), - QDateTime(QDate(1969, 10, 26), QTime(2, 0), - QTimeZone::fromDurationAheadOfUtc(-4 * hour))); - QCOMPARE(tran.offsetFromUtc, -5 * 3600); - QCOMPARE(tran.standardTimeOffset, -5 * 3600); - QCOMPARE(tran.daylightTimeOffset, 0); - } else { - // Do not use QSKIP(): that would discard the rest of this sub-test's caller. - qDebug() << "No support for pre-epoch time-zone transitions"; - } -} -#endif // QT_BUILD_INTERNAL - #if __cpp_lib_chrono >= 201907L Q_DECLARE_METATYPE(const std::chrono::time_zone *); #endif diff --git a/tests/auto/corelib/time/qtimezonebackend/CMakeLists.txt b/tests/auto/corelib/time/qtimezonebackend/CMakeLists.txt new file mode 100644 index 00000000000..6bead8b6549 --- /dev/null +++ b/tests/auto/corelib/time/qtimezonebackend/CMakeLists.txt @@ -0,0 +1,32 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +##################################################################### +## tst_qtimezonebackend Test: +##################################################################### + +if(NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT) + cmake_minimum_required(VERSION 3.16) + project(tst_qtimezonebackend LANGUAGES CXX) + find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST) +endif() + +qt_internal_add_test(tst_qtimezonebackend + SOURCES + tst_qtimezonebackend.cpp + DEFINES + QT_NO_CAST_FROM_ASCII + QT_NO_CAST_TO_ASCII + QT_NO_FOREACH + QT_NO_KEYWORDS + LIBRARIES + Qt::CorePrivate +) + +## Scopes: +##################################################################### + +qt_internal_extend_target(tst_qtimezonebackend CONDITION QT_FEATURE_icu + LIBRARIES + ICU::i18n ICU::uc ICU::data +) diff --git a/tests/auto/corelib/time/qtimezonebackend/tst_qtimezonebackend.cpp b/tests/auto/corelib/time/qtimezonebackend/tst_qtimezonebackend.cpp new file mode 100644 index 00000000000..71519ccd9fc --- /dev/null +++ b/tests/auto/corelib/time/qtimezonebackend/tst_qtimezonebackend.cpp @@ -0,0 +1,784 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include <QTest> +#include <qtimezone.h> +#include <private/qtimezoneprivate_p.h> + +#include <qlocale.h> +#include <qscopeguard.h> + +#if defined(Q_OS_WIN) && !QT_CONFIG(icu) && !QT_CONFIG(timezone_tzdb) +# define USING_WIN_TZ +#endif + +// Enable to test exhaustively - this is slow and expensive. +// It is also quite likely to trip over problems that we can't necessarily fix. +// #define EXHAUSTIVE_ZONE_DISPLAY + +using namespace Qt::StringLiterals; + +class tst_QTimeZoneBackend : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + // Tests of QTZP functionality: + void isValidId_data(); + void isValidId(); + void roundtripDisplayNames_data(); + void roundtripDisplayNames(); + // Tests of specific backends (see also ../qtimezone/): + void icuTest(); + void tzTest(); + void macTest(); + void winTest(); + +private: + // Generic tests of privates, called by implementation-specific private tests: + void testCetPrivate(const QTimeZonePrivate &tzp); + void testEpochTranPrivate(const QTimeZonePrivate &tzp); + // Set to true to print debug output, test Display Names and run long stress tests + static constexpr bool debug = false; +}; + +void tst_QTimeZoneBackend::isValidId_data() +{ + QTest::addColumn<QByteArray>("input"); + QTest::addColumn<bool>("valid"); + + // a-z, A-Z, 0-9, '.', '-', '_' are valid chars + // Can't start with '-' + // Parts separated by '/', each part min 1 and max of 14 chars + // (Android has parts with lengths up to 17, so tolerates this as a special case.) +#define TESTSET(name, section, valid) \ + QTest::newRow(name " front") << QByteArray(section "/xyz/xyz") << valid; \ + QTest::newRow(name " middle") << QByteArray("xyz/" section "/xyz") << valid; \ + QTest::newRow(name " back") << QByteArray("xyz/xyz/" section) << valid + + // a-z, A-Z, 0-9, '.', '-', '_' are valid chars + // Can't start with '-' + // Parts separated by '/', each part min 1 and max of 14 chars + TESTSET("empty", "", false); + TESTSET("minimal", "m", true); +#if (defined(Q_OS_ANDROID) || QT_CONFIG(icu)) && !QT_CONFIG(timezone_tzdb) + TESTSET("maximal", "East-Saskatchewan", true); // Android actually uses this + TESTSET("too long", "North-Saskatchewan", false); // ... but thankfully not this. +#else + TESTSET("maximal", "12345678901234", true); + TESTSET("maximal twice", "12345678901234/12345678901234", true); + TESTSET("too long", "123456789012345", false); + TESTSET("too-long/maximal", "123456789012345/12345678901234", false); + TESTSET("maximal/too-long", "12345678901234/123456789012345", false); +#endif + + TESTSET("bad hyphen", "-hyphen", false); + TESTSET("good hyphen", "hy-phen", true); + + TESTSET("valid char _", "_", true); + TESTSET("valid char .", ".", true); + TESTSET("valid char :", ":", true); + TESTSET("valid char +", "+", true); + TESTSET("valid char A", "A", true); + TESTSET("valid char Z", "Z", true); + TESTSET("valid char a", "a", true); + TESTSET("valid char z", "z", true); + TESTSET("valid char 0", "0", true); + TESTSET("valid char 9", "9", true); + + TESTSET("valid pair az", "az", true); + TESTSET("valid pair AZ", "AZ", true); + TESTSET("valid pair 09", "09", true); + TESTSET("valid pair .z", ".z", true); + TESTSET("valid pair _z", "_z", true); + TESTSET("invalid pair -z", "-z", false); + + TESTSET("valid triple a/z", "a/z", true); + TESTSET("valid triple a.z", "a.z", true); + TESTSET("valid triple a-z", "a-z", true); + TESTSET("valid triple a_z", "a_z", true); + TESTSET("invalid triple a z", "a z", false); + TESTSET("invalid triple a\\z", "a\\z", false); + TESTSET("invalid triple a,z", "a,z", false); + + TESTSET("invalid space", " ", false); + TESTSET("invalid char ^", "^", false); + TESTSET("invalid char \"", "\"", false); + TESTSET("invalid char $", "$", false); + TESTSET("invalid char %", "%", false); + TESTSET("invalid char &", "&", false); + TESTSET("invalid char (", "(", false); + TESTSET("invalid char )", ")", false); + TESTSET("invalid char =", "=", false); + TESTSET("invalid char -", "-", false); + TESTSET("invalid char ?", "?", false); + TESTSET("invalid char ß", "ß", false); + TESTSET("invalid char \\x01", "\x01", false); + TESTSET("invalid char ' '", " ", false); + +#undef TESTSET + + QTest::newRow("az alone") << QByteArray("az") << true; + QTest::newRow("AZ alone") << QByteArray("AZ") << true; + QTest::newRow("09 alone") << QByteArray("09") << true; + QTest::newRow("a/z alone") << QByteArray("a/z") << true; + QTest::newRow("a.z alone") << QByteArray("a.z") << true; + QTest::newRow("a-z alone") << QByteArray("a-z") << true; + QTest::newRow("a_z alone") << QByteArray("a_z") << true; + QTest::newRow(".z alone") << QByteArray(".z") << true; + QTest::newRow("_z alone") << QByteArray("_z") << true; + QTest::newRow("a z alone") << QByteArray("a z") << false; + QTest::newRow("a\\z alone") << QByteArray("a\\z") << false; + QTest::newRow("a,z alone") << QByteArray("a,z") << false; + QTest::newRow("/z alone") << QByteArray("/z") << false; + QTest::newRow("-z alone") << QByteArray("-z") << false; +#if (defined(Q_OS_ANDROID) || QT_CONFIG(icu)) && !QT_CONFIG(timezone_tzdb) + QTest::newRow("long alone") << QByteArray("12345678901234567") << true; + QTest::newRow("over-long alone") << QByteArray("123456789012345678") << false; +#else + QTest::newRow("long alone") << QByteArray("12345678901234") << true; + QTest::newRow("over-long alone") << QByteArray("123456789012345") << false; +#endif +} + +void tst_QTimeZoneBackend::isValidId() +{ + QFETCH(QByteArray, input); + QFETCH(bool, valid); + + QCOMPARE(QTimeZonePrivate::isValidId(input), valid); +} + +void tst_QTimeZoneBackend::roundtripDisplayNames_data() +{ + QTest::addColumn<QTimeZone>("zone"); + QTest::addColumn<QLocale>("locale"); + QTest::addColumn<QTimeZone::TimeType>("type"); + + constexpr QTimeZone::TimeType types[] = { + QTimeZone::GenericTime, QTimeZone::StandardTime, QTimeZone::DaylightTime + }; + const auto typeName = [](QTimeZone::TimeType type) { + switch (type) { + case QTimeZone::GenericTime: return "Gen"; + case QTimeZone::StandardTime: return "Std"; + case QTimeZone::DaylightTime: return "DST"; + } + Q_UNREACHABLE_RETURN("Unrecognised"); + }; + const QList<QByteArray> allList = (QTimeZone::availableTimeZoneIds() << "Vulcan/ShiKahr"_ba); +#ifdef EXHAUSTIVE_ZONE_DISPLAY + const QList<QByteArray> idList = allList; +#else + const QList<QByteArray> idList = { + "Africa/Casablanca"_ba, "Africa/Lagos"_ba, "Africa/Tunis"_ba, + "America/Caracas"_ba, "America/Coyhaique"_ba, + "America/Indiana/Tell_City"_ba, "America/Managua"_ba, + "Asia/Bangkok"_ba, "Asia/Colombo"_ba, "Asia/Tokyo"_ba, + "Atlantic/Bermuda"_ba, "Atlantic/Faroe"_ba, "Atlantic/Madeira"_ba, + "Australia/Broken_Hill"_ba, "Australia/NSW"_ba, "Australia/Tasmania"_ba, + "Brazil/Acre"_ba, "Canada/Atlantic"_ba, "Chile/EasterIsland"_ba, + "CST6CDT"_ba, "Etc/Greenwich"_ba, "Etc/Universal"_ba, + "Europe/Guernsey"_ba, "Europe/Kaliningrad"_ba, "Europe/Kyiv"_ba, + "Europe/Prague"_ba, "Europe/Vatican"_ba, + "Indian/Comoro"_ba, "Mexico/BajaSur"_ba, + "Pacific/Bougainville"_ba, "Pacific/Midway"_ba, "Pacific/Wallis"_ba, + "US/Aleutian"_ba, + "UTC"_ba, + // Those named overtly in tst_QDateTime - special cases first: + "UTC-02:00"_ba, "UTC+02:00"_ba, "UTC+12:00"_ba, + "Etc/GMT+3"_ba, "GMT-0"_ba, "GMT"_ba, + // ... then ordinary names in alphabetic order: + "America/Anchorage"_ba, "America/Metlakatla"_ba, "America/New_York"_ba, + "America/Sao_Paulo"_ba, "America/Toronto"_ba, "America/Vancouver"_ba, + "Asia/Kathmandu"_ba, "Asia/Manila"_ba, "Asia/Singapore"_ba, + "Australia/Brisbane"_ba, "Australia/Eucla"_ba, "Australia/Sydney"_ba, + "Europe/Berlin"_ba, "Europe/Helsinki"_ba, "Europe/Lisbon"_ba, "Europe/Oslo"_ba, + "Europe/Rome"_ba, + "Pacific/Apia"_ba, "Pacific/Auckland"_ba, "Pacific/Kiritimati"_ba, + "Vulcan/ShiKahr"_ba // Invalid: also worth testing. + }; + // Some valid zones in that list may be absent from the platform's + // availableTimeZoneIds(), yet in fact work when used as it's asked to + // instantiate them (e.g. Etc/Universal on macOS). This can give them a + // displayName() that we fail to decode, without timezone_locale, due to + // only trying the availableTimeZoneIds() in findLongNamePrefix(). So we + // have to filter on membership of allList when creating rows. +#endif // Exhaustive + const QLocale fr(QLocale::French, QLocale::France); + const QLocale hi(QLocale::Hindi, QLocale::India); + for (const QByteArray &id : idList) { + if (id == "localtime"_ba || id == "posixrules"_ba || !allList.contains(id)) + continue; + QTimeZone zone = QTimeZone(id); + if (!zone.isValid()) + continue; + for (const auto type : types) { + QTest::addRow("%s@fr_FR/%s", id.constData(), typeName(type)) + << zone << fr << type; + QTest::addRow("%s@hi_IN/%s", id.constData(), typeName(type)) + << zone << hi << type; + } + } +} + +void tst_QTimeZoneBackend::roundtripDisplayNames() +{ + QFETCH(const QTimeZone, zone); + QFETCH(const QLocale, locale); + QFETCH(const QTimeZone::TimeType, type); + static const QDateTime jan = QDateTime(QDate(2015, 1, 1), QTime(12, 0), QTimeZone::UTC); + static const QDateTime jul = QDateTime(QDate(2015, 7, 1), QTime(12, 0), QTimeZone::UTC); + const QDateTime dt = zone.isDaylightTime(jul) == (type == QTimeZone::DaylightTime) ? jul : jan; + + // Some zones exercise region format. + const QString name = zone.displayName(type, QTimeZone::LongName, locale); + if (!name.isEmpty()) { + const auto tran = QTimeZonePrivate::extractPrivate(zone)->data(type); + const qint64 when = tran.atMSecsSinceEpoch == QTimeZonePrivate::invalidMSecs() + ? dt.toMSecsSinceEpoch() : tran.atMSecsSinceEpoch; + const QString extended = name + "some spurious cruft"_L1; + auto match = + QTimeZonePrivate::findLongNamePrefix(extended, locale, when); + if (!match) + match = QTimeZonePrivate::findLongNamePrefix(extended, locale); + if (!match) + match = QTimeZonePrivate::findNarrowOffsetPrefix(extended, locale); + if (!match) + match = QTimeZonePrivate::findLongUtcPrefix(extended); + auto report = qScopeGuard([=]() { + qDebug() << "At" << QDateTime::fromMSecsSinceEpoch(when, QTimeZone::UTC) + << "via" << name; + }); + QCOMPARE(match.nameLength, name.size()); + report.dismiss(); +#if 0 + if (match.ianaId != zone.id()) { + const QTimeZone found = QTimeZone(match.ianaId); + if (QTimeZonePrivate::extractPrivate(found)->offsetFromUtc(when) + != QTimeZonePrivate::extractPrivate(zone)->offsetFromUtc(when)) { + // For DST, some zones haven't done it in ages, so tran may be ancient. + // Meanwhile, match.ianaId is typically the canonical zone for a metazone. + // That, in turn, may not have been doing DST when zone was. + // So we can't rely on a match, but can report the mismatches. + qDebug() << "Long name" << name << "on" + << QTimeZonePrivate::extractPrivate(zone)->offsetFromUtc(when) + << "at" << QDateTime::fromMSecsSinceEpoch(when, QTimeZone::UTC) + << "got" << match.ianaId << "on" + << QTimeZonePrivate::extractPrivate(found)->offsetFromUtc(when); + // There are also some absurdly over-generic names, that lead to + // ambiguities, e.g. "heure : West" + } + } +#endif // Debug code + } else if (type != QTimeZone::DaylightTime) { /* Zones with no DST have no DST-name */ + qDebug("Empty display name"); + } +} + +// Relies on local variable names: zone tzp and locale enUS. +#define ZONE_DNAME_CHECK(type, name, val) \ + QCOMPARE(tzp.displayName(QTimeZone::type, QTimeZone::name, enUS), val); + +void tst_QTimeZoneBackend::icuTest() +{ +#if QT_CONFIG(icu) && !QT_CONFIG(timezone_tzdb) && (defined(Q_OS_VXWORKS) || !defined(Q_OS_UNIX)) + // Known datetimes + qint64 std = QDateTime(QDate(2012, 1, 1), QTime(0, 0), QTimeZone::UTC).toMSecsSinceEpoch(); + qint64 dst = QDateTime(QDate(2012, 6, 1), QTime(0, 0), QTimeZone::UTC).toMSecsSinceEpoch(); + + // Test default constructor + QIcuTimeZonePrivate tzpd; + QVERIFY(tzpd.isValid()); + + // Test invalid is not available: + QVERIFY(!tzpd.isTimeZoneIdAvailable("Gondwana/Erewhon")); + // and construction gives an invalid result: + QIcuTimeZonePrivate tzpi("Gondwana/Erewhon"); + QCOMPARE(tzpi.isValid(), false); + + // Test named constructor + QIcuTimeZonePrivate tzp("Europe/Berlin"); + QVERIFY(tzp.isValid()); + + // Only test names in debug mode, names used can vary by ICU version installed + if constexpr (debug) { + // Test display names by type + QLocale enUS("en_US"); + ZONE_DNAME_CHECK(StandardTime, LongName, u"Central European Standard Time"); + ZONE_DNAME_CHECK(StandardTime, ShortName, u"GMT+01:00"); + ZONE_DNAME_CHECK(StandardTime, OffsetName, u"UTC+01:00"); + ZONE_DNAME_CHECK(DaylightTime, LongName, u"Central European Summer Time"); + ZONE_DNAME_CHECK(DaylightTime, ShortName, u"GMT+02:00"); + ZONE_DNAME_CHECK(DaylightTime, OffsetName, u"UTC+02:00"); + // ICU C api does not support Generic Time yet, C++ api does + ZONE_DNAME_CHECK(GenericTime, LongName, u"Central European Standard Time"); + ZONE_DNAME_CHECK(GenericTime, ShortName, u"GMT+01:00"); + ZONE_DNAME_CHECK(GenericTime, OffsetName, u"UTC+01:00"); + + // Test Abbreviations + QCOMPARE(tzp.abbreviation(std), u"CET"); + QCOMPARE(tzp.abbreviation(dst), u"CEST"); + } + + testCetPrivate(tzp); + if (QTest::currentTestFailed()) + return; + testEpochTranPrivate(QIcuTimeZonePrivate("America/Toronto")); +#endif // ICU without tzdb, on VxWorks or not on Unix +} + +void tst_QTimeZoneBackend::tzTest() +{ +#if defined(Q_OS_UNIX) && !(QT_CONFIG(timezone_tzdb) || defined(Q_OS_DARWIN) \ + || defined(Q_OS_ANDROID) || defined(Q_OS_VXWORKS)) + const auto UTC = QTimeZone::UTC; + // Known datetimes + qint64 std = QDateTime(QDate(2012, 1, 1), QTime(0, 0), UTC).toMSecsSinceEpoch(); + qint64 dst = QDateTime(QDate(2012, 6, 1), QTime(0, 0), UTC).toMSecsSinceEpoch(); + + // Test default constructor + QTzTimeZonePrivate tzpd; + QVERIFY(tzpd.isValid()); + + // Test invalid constructor + QTzTimeZonePrivate tzpi("Gondwana/Erewhon"); + QVERIFY(!tzpi.isValid()); + + // Test named constructor + QTzTimeZonePrivate tzp("Europe/Berlin"); + QVERIFY(tzp.isValid()); + + // Test POSIX-format value for $TZ: + QTimeZone tzposix("MET-1METDST-2,M3.5.0/02:00:00,M10.5.0/03:00:00"); + QVERIFY(tzposix.isValid()); + QVERIFY(tzposix.hasDaylightTime()); + + // Cope with stray space at start of value (QTBUG-135109): + QTimeZone syd(" AEST-10AEDT,M10.1.0,M4.1.0/3"); + QVERIFY(syd.isValid()); + QVERIFY(syd.hasDaylightTime()); + + // RHEL has been seen with this as Africa/Casablanca's POSIX rule: + QTzTimeZonePrivate permaDst("<+00>0<+01>,0/0,J365/25"); + const QTimeZone utcP1("UTC+01:00"); // Should always have same offset as permaDst + QVERIFY(permaDst.isValid()); + QVERIFY(permaDst.hasDaylightTime()); + QVERIFY(permaDst.isDaylightTime(QDate(2020, 1, 1).startOfDay(utcP1).toMSecsSinceEpoch())); + QVERIFY(permaDst.isDaylightTime(QDate(2020, 12, 31).endOfDay(utcP1).toMSecsSinceEpoch())); + // Note that the final /25 could be misunderstood as putting a fall-back at + // 1am on the next year's Jan 1st; check we don't do that: + QVERIFY(permaDst.isDaylightTime( + QDateTime(QDate(2020, 1, 1), QTime(1, 30), utcP1).toMSecsSinceEpoch())); + // It shouldn't have any transitions. QTimeZone::hasTransitions() only says + // whether the backend supports them, so ask for transitions in a wide + // enough interval that one would show up, if there are any: + QVERIFY(permaDst.transitions(QDate(2015, 1, 1).startOfDay(UTC).toMSecsSinceEpoch(), + QDate(2020, 1, 1).startOfDay(UTC).toMSecsSinceEpoch() + ).isEmpty()); + + QTimeZone tzBrazil("BRT+3"); // parts of Northern Brazil, as a POSIX rule + QVERIFY(tzBrazil.isValid()); + QCOMPARE(tzBrazil.offsetFromUtc(QDateTime(QDate(1111, 11, 11).startOfDay())), -10800); + + // Test display names by type, either ICU or abbreviation only + QLocale enUS(u"en_US"); + // Only test names in debug mode, names used can vary by ICU version installed + if constexpr (debug) { +#if QT_CONFIG(icu) + ZONE_DNAME_CHECK(StandardTime, LongName, "Central European Standard Time"); + ZONE_DNAME_CHECK(StandardTime, ShortName, "GMT+01:00"); + ZONE_DNAME_CHECK(StandardTime, OffsetName, "UTC+01:00"); + ZONE_DNAME_CHECK(DaylightTime, LongName, "Central European Summer Time"); + ZONE_DNAME_CHECK(DaylightTime, ShortName, "GMT+02:00"); + ZONE_DNAME_CHECK(DaylightTime, OffsetName, "UTC+02:00"); + // ICU C api does not support Generic Time yet, C++ api does + ZONE_DNAME_CHECK(GenericTime, LongName, "Central European Standard Time"); + ZONE_DNAME_CHECK(GenericTime, ShortName, "GMT+01:00"); + ZONE_DNAME_CHECK(GenericTime, OffsetName, "UTC+01:00"); +#else + ZONE_DNAME_CHECK(StandardTime, LongName, "CET"); + ZONE_DNAME_CHECK(StandardTime, ShortName, "CET"); + ZONE_DNAME_CHECK(StandardTime, OffsetName, "CET"); + ZONE_DNAME_CHECK(DaylightTime, LongName, "CEST"); + ZONE_DNAME_CHECK(DaylightTime, ShortName, "CEST"); + ZONE_DNAME_CHECK(DaylightTime, OffsetName, "CEST"); + ZONE_DNAME_CHECK(GenericTime, LongName, "CET"); + ZONE_DNAME_CHECK(GenericTime, ShortName, "CET"); + ZONE_DNAME_CHECK(GenericTime, OffsetName, "CET"); +#endif // icu + + // Test Abbreviations + QCOMPARE(tzp.abbreviation(std), u"CET"); + QCOMPARE(tzp.abbreviation(dst), u"CEST"); + } + + testCetPrivate(tzp); + if (QTest::currentTestFailed()) + return; + testEpochTranPrivate(QTzTimeZonePrivate("America/Toronto")); + if (QTest::currentTestFailed()) + return; + + // Test first and last transition rule + // Warning: This could vary depending on age of TZ file! + + // Test low date uses first rule found + constexpr qint64 ancient = -Q_INT64_C(9999999999999); + // Note: Depending on the OS in question, the database may be carrying the + // Local Mean Time. which for Berlin is 0:53:28 + QTimeZonePrivate::Data dat = tzp.data(ancient); + QCOMPARE(dat.atMSecsSinceEpoch, ancient); + QCOMPARE(dat.daylightTimeOffset, 0); + if (dat.abbreviation == u"LMT") { + QCOMPARE(dat.standardTimeOffset, 3208); + } else { + QCOMPARE(dat.standardTimeOffset, 3600); + + constexpr qint64 invalidTime = std::numeric_limits<qint64>::min(); + constexpr int invalidOffset = std::numeric_limits<int>::min(); + // Test previous to low value is invalid + dat = tzp.previousTransition(ancient); + QCOMPARE(dat.atMSecsSinceEpoch, invalidTime); + QCOMPARE(dat.standardTimeOffset, invalidOffset); + QCOMPARE(dat.daylightTimeOffset, invalidOffset); + } + + dat = tzp.nextTransition(ancient); + QCOMPARE(QDateTime::fromMSecsSinceEpoch(dat.atMSecsSinceEpoch, + QTimeZone::fromSecondsAheadOfUtc(3600)), + QDateTime(QDate(1893, 4, 1), QTime(0, 6, 32), + QTimeZone::fromSecondsAheadOfUtc(3600))); + QCOMPARE(dat.standardTimeOffset, 3600); + QCOMPARE(dat.daylightTimeOffset, 0); + + // Date-times late enough to exercise POSIX rules: + qint64 stdHi = QDate(2100, 1, 1).startOfDay(UTC).toMSecsSinceEpoch(); + qint64 dstHi = QDate(2100, 6, 1).startOfDay(UTC).toMSecsSinceEpoch(); + // Relevant last Sundays in October and March: + QCOMPARE(Qt::DayOfWeek(QDate(2099, 10, 25).dayOfWeek()), Qt::Sunday); + QCOMPARE(Qt::DayOfWeek(QDate(2100, 3, 28).dayOfWeek()), Qt::Sunday); + QCOMPARE(Qt::DayOfWeek(QDate(2100, 10, 31).dayOfWeek()), Qt::Sunday); + + dat = tzp.data(stdHi); + QCOMPARE(dat.atMSecsSinceEpoch - stdHi, qint64(0)); + QCOMPARE(dat.offsetFromUtc, 3600); + QCOMPARE(dat.standardTimeOffset, 3600); + QCOMPARE(dat.daylightTimeOffset, 0); + + dat = tzp.data(dstHi); + QCOMPARE(dat.atMSecsSinceEpoch - dstHi, qint64(0)); + QCOMPARE(dat.offsetFromUtc, 7200); + QCOMPARE(dat.standardTimeOffset, 3600); + QCOMPARE(dat.daylightTimeOffset, 3600); + + dat = tzp.previousTransition(stdHi); + QCOMPARE(dat.abbreviation, u"CET"); + QCOMPARE(QDateTime::fromMSecsSinceEpoch(dat.atMSecsSinceEpoch, UTC), + QDateTime(QDate(2099, 10, 25), QTime(3, 0), QTimeZone::fromSecondsAheadOfUtc(7200))); + QCOMPARE(dat.offsetFromUtc, 3600); + QCOMPARE(dat.standardTimeOffset, 3600); + QCOMPARE(dat.daylightTimeOffset, 0); + + dat = tzp.previousTransition(dstHi); + QCOMPARE(dat.abbreviation, u"CEST"); + QCOMPARE(QDateTime::fromMSecsSinceEpoch(dat.atMSecsSinceEpoch, UTC), + QDateTime(QDate(2100, 3, 28), QTime(2, 0), QTimeZone::fromSecondsAheadOfUtc(3600))); + QCOMPARE(dat.offsetFromUtc, 7200); + QCOMPARE(dat.standardTimeOffset, 3600); + QCOMPARE(dat.daylightTimeOffset, 3600); + + dat = tzp.nextTransition(stdHi); + QCOMPARE(dat.abbreviation, u"CEST"); + QCOMPARE(QDateTime::fromMSecsSinceEpoch(dat.atMSecsSinceEpoch, UTC), + QDateTime(QDate(2100, 3, 28), QTime(2, 0), QTimeZone::fromSecondsAheadOfUtc(3600))); + QCOMPARE(dat.offsetFromUtc, 7200); + QCOMPARE(dat.standardTimeOffset, 3600); + QCOMPARE(dat.daylightTimeOffset, 3600); + + dat = tzp.nextTransition(dstHi); + QCOMPARE(dat.abbreviation, u"CET"); + QCOMPARE(QDateTime::fromMSecsSinceEpoch(dat.atMSecsSinceEpoch, + QTimeZone::fromSecondsAheadOfUtc(3600)), + QDateTime(QDate(2100, 10, 31), QTime(3, 0), QTimeZone::fromSecondsAheadOfUtc(7200))); + QCOMPARE(dat.offsetFromUtc, 3600); + QCOMPARE(dat.standardTimeOffset, 3600); + QCOMPARE(dat.daylightTimeOffset, 0); + + // Test TZ timezone vs UTC timezone for non-whole-hour negative offset: + QTzTimeZonePrivate tztz1("America/Caracas"); + QUtcTimeZonePrivate tzutc1("UTC-04:30"); + QVERIFY(tztz1.isValid()); + QVERIFY(tzutc1.isValid()); + QTzTimeZonePrivate::Data datatz1 = tztz1.data(std); + QTzTimeZonePrivate::Data datautc1 = tzutc1.data(std); + QCOMPARE(datatz1.offsetFromUtc, datautc1.offsetFromUtc); + + // Test TZ timezone vs UTC timezone for non-whole-hour positive offset: + QTzTimeZonePrivate tztz2k("Asia/Kolkata"); // New name + QTzTimeZonePrivate tztz2c("Asia/Calcutta"); // Legacy name + // Can't assign QtzTZP, so use a reference; prefer new name. + QTzTimeZonePrivate &tztz2 = tztz2k.isValid() ? tztz2k : tztz2c; + QUtcTimeZonePrivate tzutc2("UTC+05:30"); + QVERIFY2(tztz2.isValid(), tztz2.id().constData()); + QVERIFY(tzutc2.isValid()); + QTzTimeZonePrivate::Data datatz2 = tztz2.data(std); + QTzTimeZonePrivate::Data datautc2 = tzutc2.data(std); + QCOMPARE(datatz2.offsetFromUtc, datautc2.offsetFromUtc); + + // Test a timezone with an abbreviation that isn't all letters: + QTzTimeZonePrivate tzBarnaul("Asia/Barnaul"); + if (tzBarnaul.isValid()) { + QCOMPARE(tzBarnaul.data(std).abbreviation, u"+07"); + + // first full day of the new rule (tzdata2016b) + QDateTime dt(QDate(2016, 3, 28), QTime(0, 0), UTC); + QCOMPARE(tzBarnaul.data(dt.toMSecsSinceEpoch()).abbreviation, u"+07"); + } +#endif // Unix && !(timezone_tzdb || Darwin || Android || VxWorks) +} + +void tst_QTimeZoneBackend::macTest() +{ +#if defined(Q_OS_DARWIN) && !QT_CONFIG(timezone_tzdb) + // Known datetimes + qint64 std = QDateTime(QDate(2012, 1, 1), QTime(0, 0), QTimeZone::UTC).toMSecsSinceEpoch(); + qint64 dst = QDateTime(QDate(2012, 6, 1), QTime(0, 0), QTimeZone::UTC).toMSecsSinceEpoch(); + + // Test default constructor + QMacTimeZonePrivate tzpd; + QVERIFY(tzpd.isValid()); + + // Test invalid constructor + QMacTimeZonePrivate tzpi("Gondwana/Erewhon"); + QCOMPARE(tzpi.isValid(), false); + + // Test named constructor + QMacTimeZonePrivate tzp("Europe/Berlin"); + QVERIFY(tzp.isValid()); + + // Only test names in debug mode, names used can vary by version + if constexpr (debug) { + // Test display names by type + QLocale enUS(u"en_US"); + ZONE_DNAME_CHECK(StandardTime, LongName, "Central European Standard Time"); + ZONE_DNAME_CHECK(StandardTime, ShortName, "GMT+01:00"); + ZONE_DNAME_CHECK(StandardTime, OffsetName, "UTC+01:00"); + ZONE_DNAME_CHECK(DaylightTime, LongName, "Central European Summer Time"); + ZONE_DNAME_CHECK(DaylightTime, ShortName, "GMT+02:00"); + ZONE_DNAME_CHECK(DaylightTime, OffsetName, "UTC+02:00"); + // ICU C api does not support Generic Time yet, C++ api does + ZONE_DNAME_CHECK(GenericTime, LongName, "Central European Time"); + ZONE_DNAME_CHECK(GenericTime, ShortName, "Germany Time"); + ZONE_DNAME_CHECK(GenericTime, OffsetName, "UTC+01:00"); + + // Test Abbreviations + QCOMPARE(tzp.abbreviation(std), u"CET"); + QCOMPARE(tzp.abbreviation(dst), u"CEST"); + } + + testCetPrivate(tzp); + if (QTest::currentTestFailed()) + return; + testEpochTranPrivate(QMacTimeZonePrivate("America/Toronto")); +#endif // Q_OS_DARWIN without tzdb +} + +void tst_QTimeZoneBackend::winTest() +{ +#if defined(USING_WIN_TZ) + // Known datetimes + qint64 std = QDateTime(QDate(2012, 1, 1), QTime(0, 0), QTimeZone::UTC).toMSecsSinceEpoch(); + qint64 dst = QDateTime(QDate(2012, 6, 1), QTime(0, 0), QTimeZone::UTC).toMSecsSinceEpoch(); + + // Test default constructor + QWinTimeZonePrivate tzpd; + if constexpr (debug) + qDebug() << "System ID = " << tzpd.id() + << tzpd.displayName(QTimeZone::StandardTime, QTimeZone::LongName, QLocale()) + << tzpd.displayName(QTimeZone::GenericTime, QTimeZone::LongName, QLocale()); + QVERIFY(tzpd.isValid()); + + // Test invalid constructor + QWinTimeZonePrivate tzpi("Gondwana/Erewhon"); + QCOMPARE(tzpi.isValid(), false); + + // Test named constructor + QWinTimeZonePrivate tzp("Europe/Berlin"); + QVERIFY(tzp.isValid()); + + // Only test names in debug mode, names used can vary by version + if constexpr (debug) { + // Test display names by type + QLocale enUS(u"en_US"); + ZONE_DNAME_CHECK(StandardTime, LongName, "W. Europe Standard Time"); + ZONE_DNAME_CHECK(StandardTime, ShortName, "W. Europe Standard Time"); + ZONE_DNAME_CHECK(StandardTime, OffsetName, "UTC+01:00"); + ZONE_DNAME_CHECK(DaylightTime, LongName, "W. Europe Daylight Time"); + ZONE_DNAME_CHECK(DaylightTime, ShortName, "W. Europe Daylight Time"); + ZONE_DNAME_CHECK(DaylightTime, OffsetName, "UTC+02:00"); + ZONE_DNAME_CHECK(GenericTime, LongName, + "(UTC+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna"); + ZONE_DNAME_CHECK(GenericTime, ShortName, + "(UTC+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna"); + ZONE_DNAME_CHECK(GenericTime, OffsetName, "UTC+01:00"); + + // Test Abbreviations + QCOMPARE(tzp.abbreviation(std), u"CET"); + QCOMPARE(tzp.abbreviation(dst), u"CEST"); + } + + testCetPrivate(tzp); + if (QTest::currentTestFailed()) + return; + testEpochTranPrivate(QWinTimeZonePrivate("America/Toronto")); +#endif // TZ backend +} + +#undef ZONE_DNAME_CHECK + +// Test each private produces the same basic results for CET +void tst_QTimeZoneBackend::testCetPrivate(const QTimeZonePrivate &tzp) +{ + // Known datetimes + const auto UTC = QTimeZone::UTC; + const auto eastOneHour = QTimeZone::fromSecondsAheadOfUtc(3600); + qint64 std = QDateTime(QDate(2012, 1, 1), QTime(0, 0), UTC).toMSecsSinceEpoch(); + qint64 dst = QDateTime(QDate(2012, 6, 1), QTime(0, 0), UTC).toMSecsSinceEpoch(); + qint64 prev = QDateTime(QDate(2011, 1, 1), QTime(0, 0), UTC).toMSecsSinceEpoch(); + + QCOMPARE(tzp.offsetFromUtc(std), 3600); + QCOMPARE(tzp.offsetFromUtc(dst), 7200); + + QCOMPARE(tzp.standardTimeOffset(std), 3600); + QCOMPARE(tzp.standardTimeOffset(dst), 3600); + + QCOMPARE(tzp.daylightTimeOffset(std), 0); + QCOMPARE(tzp.daylightTimeOffset(dst), 3600); + + QCOMPARE(tzp.hasDaylightTime(), true); + QCOMPARE(tzp.isDaylightTime(std), false); + QCOMPARE(tzp.isDaylightTime(dst), true); + + QTimeZonePrivate::Data dat = tzp.data(std); + QCOMPARE(dat.atMSecsSinceEpoch, std); + QCOMPARE(dat.offsetFromUtc, 3600); + QCOMPARE(dat.standardTimeOffset, 3600); + QCOMPARE(dat.daylightTimeOffset, 0); + QCOMPARE(dat.abbreviation, tzp.abbreviation(std)); + + dat = tzp.data(dst); + QCOMPARE(dat.atMSecsSinceEpoch, dst); + QCOMPARE(dat.offsetFromUtc, 7200); + QCOMPARE(dat.standardTimeOffset, 3600); + QCOMPARE(dat.daylightTimeOffset, 3600); + QCOMPARE(dat.abbreviation, tzp.abbreviation(dst)); + + // Only test transitions if host system supports them + if (tzp.hasTransitions()) { + QTimeZonePrivate::Data tran = tzp.nextTransition(std); + // 2012-03-25 02:00 CET, +1 -> +2 + QCOMPARE(QDateTime::fromMSecsSinceEpoch(tran.atMSecsSinceEpoch, UTC), + QDateTime(QDate(2012, 3, 25), QTime(2, 0), eastOneHour)); + QCOMPARE(tran.offsetFromUtc, 7200); + QCOMPARE(tran.standardTimeOffset, 3600); + QCOMPARE(tran.daylightTimeOffset, 3600); + + tran = tzp.nextTransition(dst); + // 2012-10-28 03:00 CEST, +2 -> +1 + QCOMPARE(QDateTime::fromMSecsSinceEpoch(tran.atMSecsSinceEpoch, UTC), + QDateTime(QDate(2012, 10, 28), QTime(3, 0), + QTimeZone::fromSecondsAheadOfUtc(2 * 3600))); + QCOMPARE(tran.offsetFromUtc, 3600); + QCOMPARE(tran.standardTimeOffset, 3600); + QCOMPARE(tran.daylightTimeOffset, 0); + + tran = tzp.previousTransition(std); + // 2011-10-30 03:00 CEST, +2 -> +1 + QCOMPARE(QDateTime::fromMSecsSinceEpoch(tran.atMSecsSinceEpoch, UTC), + QDateTime(QDate(2011, 10, 30), QTime(3, 0), + QTimeZone::fromSecondsAheadOfUtc(2 * 3600))); + QCOMPARE(tran.offsetFromUtc, 3600); + QCOMPARE(tran.standardTimeOffset, 3600); + QCOMPARE(tran.daylightTimeOffset, 0); + + tran = tzp.previousTransition(dst); + // 2012-03-25 02:00 CET, +1 -> +2 (again) + QCOMPARE(QDateTime::fromMSecsSinceEpoch(tran.atMSecsSinceEpoch, UTC), + QDateTime(QDate(2012, 3, 25), QTime(2, 0), eastOneHour)); + QCOMPARE(tran.offsetFromUtc, 7200); + QCOMPARE(tran.standardTimeOffset, 3600); + QCOMPARE(tran.daylightTimeOffset, 3600); + + QTimeZonePrivate::DataList expected; + // 2011-03-27 02:00 CET, +1 -> +2 + tran.atMSecsSinceEpoch = QDateTime(QDate(2011, 3, 27), QTime(2, 0), + eastOneHour).toMSecsSinceEpoch(); + tran.offsetFromUtc = 7200; + tran.standardTimeOffset = 3600; + tran.daylightTimeOffset = 3600; + expected << tran; + // 2011-10-30 03:00 CEST, +2 -> +1 + tran.atMSecsSinceEpoch = QDateTime(QDate(2011, 10, 30), QTime(3, 0), + QTimeZone::fromSecondsAheadOfUtc(2 * 3600) + ).toMSecsSinceEpoch(); + tran.offsetFromUtc = 3600; + tran.standardTimeOffset = 3600; + tran.daylightTimeOffset = 0; + expected << tran; + QTimeZonePrivate::DataList result = tzp.transitions(prev, std); + QCOMPARE(result.size(), expected.size()); + for (int i = 0; i < expected.size(); ++i) { + QCOMPARE(QDateTime::fromMSecsSinceEpoch(result.at(i).atMSecsSinceEpoch, eastOneHour), + QDateTime::fromMSecsSinceEpoch(expected.at(i).atMSecsSinceEpoch, eastOneHour)); + QCOMPARE(result.at(i).offsetFromUtc, expected.at(i).offsetFromUtc); + QCOMPARE(result.at(i).standardTimeOffset, expected.at(i).standardTimeOffset); + QCOMPARE(result.at(i).daylightTimeOffset, expected.at(i).daylightTimeOffset); + } + } +} + +// Needs a zone with DST around the epoch; currently America/Toronto (EST5EDT) +void tst_QTimeZoneBackend::testEpochTranPrivate(const QTimeZonePrivate &tzp) +{ + if (!tzp.hasTransitions()) + return; // test only viable for transitions + + const auto UTC = QTimeZone::UTC; + const auto hour = std::chrono::hours{1}; + QTimeZonePrivate::Data tran = tzp.nextTransition(0); // i.e. first after epoch + // 1970-04-26 02:00 EST, -5 -> -4 + const QDateTime after = QDateTime(QDate(1970, 4, 26), QTime(2, 0), + QTimeZone::fromDurationAheadOfUtc(-5 * hour)); + const QDateTime found = QDateTime::fromMSecsSinceEpoch(tran.atMSecsSinceEpoch, UTC); +#ifdef USING_WIN_TZ // MS gets the date wrong: 5th April instead of 26th. + QCOMPARE(found.toOffsetFromUtc(-5 * 3600).time(), after.time()); +#else + QCOMPARE(found, after); +#endif + QCOMPARE(tran.offsetFromUtc, -4 * 3600); + QCOMPARE(tran.standardTimeOffset, -5 * 3600); + QCOMPARE(tran.daylightTimeOffset, 3600); + + // Pre-epoch time-zones might not be supported at all: + tran = tzp.nextTransition(QDateTime(QDate(1601, 1, 1), QTime(0, 0), UTC).toMSecsSinceEpoch()); + if (tran.atMSecsSinceEpoch != QTimeZonePrivate::invalidMSecs() + // Toronto *did* have a transition before 1970 (DST since 1918): + && tran.atMSecsSinceEpoch < 0) { + // ... but, if they are, we should be able to search back to them: + tran = tzp.previousTransition(0); // i.e. last before epoch + // 1969-10-26 02:00 EDT, -4 -> -5 + QCOMPARE(QDateTime::fromMSecsSinceEpoch(tran.atMSecsSinceEpoch, UTC), + QDateTime(QDate(1969, 10, 26), QTime(2, 0), + QTimeZone::fromDurationAheadOfUtc(-4 * hour))); + QCOMPARE(tran.offsetFromUtc, -5 * 3600); + QCOMPARE(tran.standardTimeOffset, -5 * 3600); + QCOMPARE(tran.daylightTimeOffset, 0); + } else { + // Do not use QSKIP(): that would discard the rest of this sub-test's caller. + qDebug() << "No support for pre-epoch time-zone transitions"; + } +} + +QTEST_APPLESS_MAIN(tst_QTimeZoneBackend) +#include "tst_qtimezonebackend.moc" diff --git a/tests/auto/network/access/http2/tst_http2.cpp b/tests/auto/network/access/http2/tst_http2.cpp index 6ff905cb1cb..452b7dcef88 100644 --- a/tests/auto/network/access/http2/tst_http2.cpp +++ b/tests/auto/network/access/http2/tst_http2.cpp @@ -729,11 +729,12 @@ void tst_Http2::earlyError() : QHttpNetworkConnection::ConnectionTypeHTTP2; QHttpNetworkConnection connection(1, "127.0.0.1", serverPort, true, false, nullptr, connectionType); +#if QT_CONFIG(ssl) QSslConfiguration config = QSslConfiguration::defaultConfiguration(); config.setAllowedNextProtocols({"h2"}); connection.setSslConfiguration(config); connection.ignoreSslErrors(); - +#endif // SETUP manually setup the QHttpNetworkRequest QHttpNetworkRequest req; req.setSsl(true); @@ -809,11 +810,12 @@ void tst_Http2::abortReply() : QHttpNetworkConnection::ConnectionTypeHTTP2; QHttpNetworkConnection connection(1, "127.0.0.1", serverPort, true, false, nullptr, connectionType); +#if QT_CONFIG(ssl) QSslConfiguration config = QSslConfiguration::defaultConfiguration(); config.setAllowedNextProtocols({"h2"}); connection.setSslConfiguration(config); connection.ignoreSslErrors(); - +#endif // SETUP manually setup the QHttpNetworkRequest QHttpNetworkRequest req; req.setSsl(true); diff --git a/tests/auto/network/access/qnetworkreply/tst_qnetworkreply.cpp b/tests/auto/network/access/qnetworkreply/tst_qnetworkreply.cpp index fdc2dde7921..c2b03b70d9b 100644 --- a/tests/auto/network/access/qnetworkreply/tst_qnetworkreply.cpp +++ b/tests/auto/network/access/qnetworkreply/tst_qnetworkreply.cpp @@ -10290,7 +10290,7 @@ void tst_QNetworkReply::requestWithTimeout() QSignalSpy spy(reply.data(), &QNetworkReply::errorOccurred); QCOMPARE(waitForFinish(reply), int(Failure)); QCOMPARE(spy.size(), 1); - QCOMPARE(reply->error(), QNetworkReply::OperationCanceledError); + QCOMPARE(reply->error(), QNetworkReply::TimeoutError); } #endif diff --git a/tests/auto/other/languagechange/tst_languagechange.cpp b/tests/auto/other/languagechange/tst_languagechange.cpp index 8f99730a192..ab9244c8561 100644 --- a/tests/auto/other/languagechange/tst_languagechange.cpp +++ b/tests/auto/other/languagechange/tst_languagechange.cpp @@ -172,10 +172,10 @@ void tst_languageChange::retranslatability_data() << "QFileDialog::Back" << "QFileDialog::Create New Folder" << "QFileDialog::Detail View" - << "QFileDialog::Files of type:" + << "QFileDialog::Files of &type:" << "QFileDialog::Forward" << "QFileDialog::List View" - << "QFileDialog::Look in:" + << "QFileDialog::&Look in:" << "QFileDialog::Open" << "QFileDialog::Parent Directory" << "QFileDialog::Show " diff --git a/tests/auto/other/qaccessibility/tst_qaccessibility.cpp b/tests/auto/other/qaccessibility/tst_qaccessibility.cpp index 2e92cdffada..764644bb192 100644 --- a/tests/auto/other/qaccessibility/tst_qaccessibility.cpp +++ b/tests/auto/other/qaccessibility/tst_qaccessibility.cpp @@ -1793,6 +1793,15 @@ void tst_QAccessibility::spinBoxTest() QAccessibleTextInterface *textIface = interface->textInterface(); QVERIFY(textIface); + QVERIFY(!spinBox->isReadOnly()); + QVERIFY(interface->state().editable); + QVERIFY(!interface->state().readOnly); + + spinBox->setReadOnly(true); + QVERIFY(spinBox->isReadOnly()); + QVERIFY(!interface->state().editable); + QVERIFY(interface->state().readOnly); + QTestAccessibility::clearEvents(); } diff --git a/tests/auto/widgets/dialogs/qfiledialog/tst_qfiledialog.cpp b/tests/auto/widgets/dialogs/qfiledialog/tst_qfiledialog.cpp index 5f54471bc0f..df3bf7472d0 100644 --- a/tests/auto/widgets/dialogs/qfiledialog/tst_qfiledialog.cpp +++ b/tests/auto/widgets/dialogs/qfiledialog/tst_qfiledialog.cpp @@ -815,9 +815,9 @@ void tst_QFiledialog::labelText() QFileDialog fd; QDialogButtonBox buttonBox; QPushButton *cancelButton = buttonBox.addButton(QDialogButtonBox::Cancel); - QCOMPARE(fd.labelText(QFileDialog::LookIn), QString("Look in:")); + QCOMPARE(fd.labelText(QFileDialog::LookIn), QString("&Look in:")); QCOMPARE(fd.labelText(QFileDialog::FileName), QString("File &name:")); - QCOMPARE(fd.labelText(QFileDialog::FileType), QString("Files of type:")); + QCOMPARE(fd.labelText(QFileDialog::FileType), QString("Files of &type:")); QCOMPARE(fd.labelText(QFileDialog::Accept), QString("&Open")); ///### see task 241462 QCOMPARE(fd.labelText(QFileDialog::Reject), cancelButton->text()); diff --git a/tests/auto/widgets/widgets/qcombobox/tst_qcombobox.cpp b/tests/auto/widgets/widgets/qcombobox/tst_qcombobox.cpp index 6859f22c044..10a67daa02a 100644 --- a/tests/auto/widgets/widgets/qcombobox/tst_qcombobox.cpp +++ b/tests/auto/widgets/widgets/qcombobox/tst_qcombobox.cpp @@ -845,12 +845,12 @@ void tst_QComboBox::virtualAutocompletion() QKeyEvent kp3(QEvent::KeyPress, Qt::Key_R, {}, "r"); QKeyEvent kr3(QEvent::KeyRelease, Qt::Key_R, {}, "r"); - QTest::qWait(QApplication::keyboardInputInterval()); QApplication::sendEvent(testWidget, &kp3); + QTest::qWait(QApplication::keyboardInputInterval()); QApplication::sendEvent(testWidget, &kr3); QTRY_COMPARE(testWidget->currentIndex(), 3); - QTest::qWait(QApplication::keyboardInputInterval()); + QTest::qWait(2 * QApplication::keyboardInputInterval()); testWidget->view()->setKeyboardSearchFlags(Qt::MatchContains | Qt::MatchWrap); QApplication::sendEvent(testWidget, &kp3); QApplication::sendEvent(testWidget, &kr3); @@ -2247,10 +2247,11 @@ void tst_QComboBox::ignoreWheelEvents() QFETCH(bool, allowWheelScrolling); + AllowWheelScrollStyle style(allowWheelScrolling); WheelEventTestWidget widget; QComboBox *comboBox = new QComboBox(&widget); comboBox->addItems({ "0", "1" }); - comboBox->setStyle(new AllowWheelScrollStyle(allowWheelScrolling)); + comboBox->setStyle(&style); widget.show(); QVERIFY(QTest::qWaitForWindowExposed(&widget)); diff --git a/tests/manual/CMakeLists.txt b/tests/manual/CMakeLists.txt index 8ace9592141..ad9db868235 100644 --- a/tests/manual/CMakeLists.txt +++ b/tests/manual/CMakeLists.txt @@ -6,7 +6,6 @@ if(UIKIT) return() endif() -add_subdirectory(assets) add_subdirectory(corelib) add_subdirectory(filetest) # diaglib is broken in dev due to missing diff --git a/tests/manual/assets/CMakeLists.txt b/tests/manual/assets/CMakeLists.txt deleted file mode 100644 index 03643aa8537..00000000000 --- a/tests/manual/assets/CMakeLists.txt +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (C) 2024 The Qt Company Ltd. -# SPDX-License-Identifier: BSD-3-Clause - -add_subdirectory(downloader) diff --git a/tests/manual/assets/assets.pro b/tests/manual/assets/assets.pro deleted file mode 100644 index 43f09ba46e6..00000000000 --- a/tests/manual/assets/assets.pro +++ /dev/null @@ -1,3 +0,0 @@ -TEMPLATE=subdirs - -SUBDIRS = downloader diff --git a/tests/manual/assets/downloader/CMakeLists.txt b/tests/manual/assets/downloader/CMakeLists.txt deleted file mode 100644 index b95161ac02d..00000000000 --- a/tests/manual/assets/downloader/CMakeLists.txt +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (C) 2024 The Qt Company Ltd. -# SPDX-License-Identifier: BSD-3-Clause - -##################################################################### -## tst_manual_downloader Binary: -##################################################################### - -qt_internal_add_manual_test(tst_manual_downloader - GUI - SOURCES - main.cpp - LIBRARIES - Qt::ExamplesAssetDownloaderPrivate - Qt::Widgets -) diff --git a/tests/manual/assets/downloader/downloader.pro b/tests/manual/assets/downloader/downloader.pro deleted file mode 100644 index 53976c538f0..00000000000 --- a/tests/manual/assets/downloader/downloader.pro +++ /dev/null @@ -1,5 +0,0 @@ -QT += examples_asset_downloader-private widgets - -TARGET = tst_manual_downloader - -SOURCES += main.cpp diff --git a/tests/manual/assets/downloader/main.cpp b/tests/manual/assets/downloader/main.cpp deleted file mode 100644 index d9d711cfce5..00000000000 --- a/tests/manual/assets/downloader/main.cpp +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (C) 2024 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only - -#include <QtExamplesAssetDownloader/assetdownloader.h> - -#include <QApplication> -#include <QMessageBox> -#include <QProgressDialog> - -using namespace Assets::Downloader; - -int main(int argc, char *argv[]) -{ - QApplication app(argc, argv); - app.setOrganizationName("QtProject"); - app.setApplicationName("Asset Downloader"); - - QProgressDialog progress; - progress.setAutoClose(false); - progress.setRange(0, 0); - QObject::connect(&progress, &QProgressDialog::canceled, &app, &QApplication::quit); - - AssetDownloader downloader; - downloader.setJsonFileName("car-configurator-assets-v1.json"); - downloader.setZipFileName("car-configurator-assets-v1.zip"); - downloader.setDownloadBase(QUrl("https://download.qt.io/learning/examples/")); - - QObject::connect(&downloader, &AssetDownloader::started, - &progress, &QProgressDialog::show); - QObject::connect(&downloader, &AssetDownloader::progressChanged, &progress, [&progress]( - int progressValue, int progressMaximum, const QString &progressText) { - progress.setLabelText(progressText); - progress.setMaximum(progressMaximum); - progress.setValue(progressValue); - }); - QObject::connect(&downloader, &AssetDownloader::finished, &progress, [&](bool success) { - progress.reset(); - progress.hide(); - if (success) - QMessageBox::information(nullptr, "Asset Downloader", "Download Finished Successfully."); - else - QMessageBox::warning(nullptr, "Asset Downloader", "Download Finished with an Error."); - }); - - downloader.start(); - - return app.exec(); -} diff --git a/tests/manual/manual.pro b/tests/manual/manual.pro index 47f2ae9bd0f..e28eb5316a8 100644 --- a/tests/manual/manual.pro +++ b/tests/manual/manual.pro @@ -2,7 +2,6 @@ TEMPLATE=subdirs QT_FOR_CONFIG += network-private gui-private SUBDIRS = \ -assets \ filetest \ embeddedintoforeignwindow \ foreignwindows \ diff --git a/tests/manual/wasm/eventloop/suspendresumecontrol_auto/main.cpp b/tests/manual/wasm/eventloop/suspendresumecontrol_auto/main.cpp index 1fd6e0a0c4f..ff3e67e3fc2 100644 --- a/tests/manual/wasm/eventloop/suspendresumecontrol_auto/main.cpp +++ b/tests/manual/wasm/eventloop/suspendresumecontrol_auto/main.cpp @@ -3,6 +3,7 @@ #include <QtCore/qcoreapplication.h> #include <QtCore/private/qwasmsuspendresumecontrol_p.h> +#include <QtCore/qdebug.h> #include <qtwasmtestlib.h> using namespace emscripten; @@ -20,6 +21,7 @@ private slots: void reuseTimer(); void cancelTimer(); void deleteTimer(); + void suspendExclusive(); }; // Verify that a single timer fires @@ -138,6 +140,49 @@ void WasmSuspendResumeControlTest::deleteTimer() QWASMSUCCESS(); } +// Verify that an exclusive suspend resumes for the exclusive event only +void WasmSuspendResumeControlTest::suspendExclusive() +{ + QWasmSuspendResumeControl suspendResume; + + // (re) implement a native timer - this gives us a unique event handler + // index which we can suspend exclusively on. + bool exclusiveTimerFired = false; + auto exclusiveTimerHandler = [&exclusiveTimerFired](emscripten::val) { + exclusiveTimerFired = true; + }; + uint32_t exlusiveTimerHandlerIndex = suspendResume.registerEventHandler(std::move(exclusiveTimerHandler)); + + std::chrono::milliseconds exclusiveTimerTimeout = timerTimeout * 4; + double timoutValue = static_cast<double>(exclusiveTimerTimeout.count()); + val jsHandler = suspendResume.jsEventHandlerAt(exlusiveTimerHandlerIndex); + val::global("window").call<double>("setTimeout", jsHandler, timoutValue); + + // Schedule suppressedTimer to fire before the exclusive timer. Expected + // behavior is that it doesn't. + bool suppressedTimerFired = false; + QWasmTimer suppressedTimer(&suspendResume, [&suppressedTimerFired](){ + suppressedTimerFired = true; + }); + suppressedTimer.setTimeout(timerTimeout); + + // Suspend exclusively for the exclusive timer, and verify that + // the correct timers fired. + suspendResume.suspendExclusive(exlusiveTimerHandlerIndex); + suspendResume.sendPendingEvents(); // <- also clears exclusive mode + if (!exclusiveTimerFired) + QWASMFAIL("Exclusive timer did not fire"); + if (suppressedTimerFired) + QWASMFAIL("Suppressed timer did fire"); + + // Send (all) events, this should give is the suppressed timer + suspendResume.sendPendingEvents(); + if (!suppressedTimerFired) + QWASMFAIL("Suppressed timer did not fire"); + + QWASMSUCCESS(); +} + int main(int argc, char **argv) { auto testObject = std::make_shared<WasmSuspendResumeControlTest>(); diff --git a/tests/manual/wasm/qtwasmtestlib/qtwasmtestlib.cpp b/tests/manual/wasm/qtwasmtestlib/qtwasmtestlib.cpp index ec03c7209a4..1e49847c97f 100644 --- a/tests/manual/wasm/qtwasmtestlib/qtwasmtestlib.cpp +++ b/tests/manual/wasm/qtwasmtestlib/qtwasmtestlib.cpp @@ -127,7 +127,7 @@ void passTest() EMSCRIPTEN_BINDINGS(qtwebtestrunner) { emscripten::function("cleanupTestCase", &cleanupTestCase); emscripten::function("getTestFunctions", &getTestFunctions); - emscripten::function("runTestFunction", &runTestFunction); + emscripten::function("runTestFunction", &runTestFunction, emscripten::async()); emscripten::function("qtWasmFail", &failTest); emscripten::function("qtWasmPass", &passTest); } diff --git a/util/sbom/cyclonedx/.gitignore b/util/sbom/cyclonedx/.gitignore new file mode 100644 index 00000000000..140f5f55f12 --- /dev/null +++ b/util/sbom/cyclonedx/.gitignore @@ -0,0 +1,6 @@ +__pycache__ +*.egg-info +.coverage +.idea +*.pyc +build/ diff --git a/util/sbom/cyclonedx/Makefile b/util/sbom/cyclonedx/Makefile new file mode 100644 index 00000000000..711daf5da22 --- /dev/null +++ b/util/sbom/cyclonedx/Makefile @@ -0,0 +1,14 @@ +lint: check format_check + +env: + uv sync + +check: + uvx ruff check + uvx pyright + +format: + uvx ruff format + +format_check: + uvx ruff format --check diff --git a/util/sbom/cyclonedx/README.md b/util/sbom/cyclonedx/README.md new file mode 100644 index 00000000000..3a6394f4e74 --- /dev/null +++ b/util/sbom/cyclonedx/README.md @@ -0,0 +1,54 @@ +# Qt CycloneDX SBOM helper tool + +This repository contains a Python script to create a CycloneDX Software +Bill of Materials (SBOM) document for Qt-based projects. + +## Requirements + +- [Python 3.9](https://www.python.org/downloads/), +- `pip` to manage Python packages. +- [cyclonedx-python-lib package](https://pypi.org/project/cyclonedx-python-lib/) can be installed via pip +- optional [tomli package](https://pypi.org/project/tomli/) if using Python 3.9 and the vendored + tomli package is causing issues + +```sh +pip install 'cyclonedx-python-lib[json-validation]' tomli +``` +or +``` +pip install . # in the current directory to parse the `pyproject.toml` dependencies +``` + +## Description + +The tool is not meant to be run standalone. Instead, the Qt CMake build system generates an +intermediate TOML file during the build process, which is then processed by the provided Python +script to create the final CycloneDX SBOM in JSON format. + +## Development + +The script is using [uv](https://docs.astral.sh/uv/getting-started/installation/) to manage +a virtual environment for development. Install it before running the commands below. + +Run + +```sh +# Create a virtual environment with with the project dependencies using `uv` in the current dir +make env + +# Optionally install the package in editable mode, for IDE support if needed. +uv pip install -e . + +# Run the script +python ./qt_cyclonedx_generator/qt_cyclonedx_generator.py +``` + +To format the source code run: +```sh +make format # uses uvx ruff format +``` + +To run the lints: +```sh +make lint # uses uvx ruff check and pyright +``` diff --git a/util/sbom/cyclonedx/pyproject.toml b/util/sbom/cyclonedx/pyproject.toml new file mode 100644 index 00000000000..19500012555 --- /dev/null +++ b/util/sbom/cyclonedx/pyproject.toml @@ -0,0 +1,54 @@ +[project] +name = "qt-cyclonedx-generator" +version = "0.1.0" +description = "Tool to generate a CycloneDX v1.6 SBOM for a Qt project." +readme = "README.md" + +requires-python = ">=3.9" + +dependencies = [ + "cyclonedx-python-lib[json-validation]>=10.0.0", + 'tomli ; python_version < "3.11"' +] + +# Development dependencies. +[dependency-groups] +dev = [ + "setuptools>=65", + "ruff>=0.11.2", + "tomli>=2.3.0", + "pyright>=1.1.406", +] + +[project.scripts] +qt_cyclonedx_generator = "qt_cyclonedx_generator.qt_cyclonedx_generator:main" + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[tool.ruff.lint] +ignore = ["E402"] + +[tool.pyright] +# To find the imports created by uv +venvPath = "." +venv = ".venv" + +# Exclude the default folders, plus the build one. +exclude = [ + "**/node_modules", + "**/__pycache__", + "**/.*", + "build/*", +] + +# Because some of the toml libraries might not be found +reportMissingImports = "none" + +# Strict is too strict. +typeCheckingMode = "basic" + +pythonVersion = "3.9" +pythonPlatform = "All" + diff --git a/util/sbom/cyclonedx/qt_cyclonedx_generator/__init__.py b/util/sbom/cyclonedx/qt_cyclonedx_generator/__init__.py new file mode 100644 index 00000000000..50aa992f8ba --- /dev/null +++ b/util/sbom/cyclonedx/qt_cyclonedx_generator/__init__.py @@ -0,0 +1,2 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 diff --git a/util/sbom/cyclonedx/qt_cyclonedx_generator/qt_cyclonedx_generator.py b/util/sbom/cyclonedx/qt_cyclonedx_generator/qt_cyclonedx_generator.py new file mode 100644 index 00000000000..3b04a47d714 --- /dev/null +++ b/util/sbom/cyclonedx/qt_cyclonedx_generator/qt_cyclonedx_generator.py @@ -0,0 +1,881 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import importlib.util +import logging +import sys + + +def setup_log() -> logging.Logger: + logging.basicConfig(format="[%(levelname)s]: %(message)s", level=logging.INFO) + log_object = logging.getLogger("qt-cyclonedx-generator") + log_object.setLevel(logging.INFO) + return log_object + + +log = setup_log() + + +def import_tomllib(): + """Import the TOML library with fallback options for different Python versions. + + Stock toml lib is available on Python 3.11+, + if not found try the tomli library, and if that isn't found + try to use the pip vendored tomli library. + + Returns: + Tuple of (tomllib_module, success_flag) + """ + if sys.version_info >= (3, 11): + import tomllib as tomllib_local + + return tomllib_local, True + else: + try: + import tomli as tomllib_local + + return tomllib_local, True + except ModuleNotFoundError: + try: # noinspection PyProtectedMember + import pip._vendor.tomli as tomllib_local + + return tomllib_local, True + except ModuleNotFoundError: + return None, False + + +tomllib, tomllib_available = import_tomllib() + + +def check_package_dependencies() -> None: + """Check if required package dependencies are available.""" + missing_packages = [] + + if not tomllib_available: + missing_packages.append("tomli") + + if importlib.util.find_spec("cyclonedx") is None: + missing_packages.append("cyclonedx") + + if importlib.util.find_spec("packageurl") is None: + missing_packages.append("packageurl") + + if missing_packages: + log.error(f"Missing required dependencies: {', '.join(missing_packages)}") + log.error(f"Please install with: `pip install {' '.join(missing_packages)}`") + sys.exit(1) + + +check_package_dependencies() + +import argparse +from datetime import datetime, timezone +from license_expression import get_spdx_licensing +from pathlib import Path +from packageurl import PackageURL +from typing import ( + TYPE_CHECKING, + Optional, + Any, + TypedDict, + Union, + cast, + Callable, +) +from uuid import UUID + +from cyclonedx.factory.license import LicenseFactory +from cyclonedx.exception import MissingOptionalDependencyException +from cyclonedx.model import ( + XsUri, + ExternalReference, + ExternalReferenceType, + Property, + AttachedText, +) +from cyclonedx.model.bom import Bom +from cyclonedx.model.component import Component, ComponentType +from cyclonedx.model.contact import OrganizationalEntity, OrganizationalContact +from cyclonedx.model.license import LicenseAcknowledgement, LicenseExpression +from cyclonedx.model.lifecycle import PredefinedLifecycle, LifecyclePhase +from cyclonedx.output.json import JsonV1Dot6 +from cyclonedx.schema import SchemaVersion +from cyclonedx.validation.json import JsonStrictValidator + +if TYPE_CHECKING: + from cyclonedx.output.json import Json as JsonOutputter + + +class TomlRequiredFieldError(Exception): + pass + + +class MissingTomlFileError(Exception): + pass + + +class SBOMWriteFailedError(Exception): + pass + + +class ValidationFailedError(Exception): + pass + + +class ValidationDependencyMissingError(Exception): + pass + + +# TypedDict definitions for TOML data structures +class CyclonePropertyDict(TypedDict): + name: str + value: str + + +class LicenseDict(TypedDict): + license_id: str + text: str + + +class RootComponentDict(TypedDict): + name: str + spdx_id: str + # Optional fields (since TypedDict doesn't support NotRequired in Python 3.9) + # We'll use .get() to access these safely + version: str + description: str + download_location: str + build_date: str + supplier: str + supplier_url: str + serial_number_uuid: str + + +class ComponentDict(TypedDict): + name: str + spdx_id: str + # Optional fields + version: str + component_type: str + copyright: str + external_bom_link: str + supplier: str + purl_list: list[str] + cpe_list: list[str] + properties: list[CyclonePropertyDict] + dependencies: list[str] + + +class BuildToolDict(TypedDict): + # Optional fields + name: str + component_type: str + version: str + description: str + + +class TomlDataDict(TypedDict): + root_component: RootComponentDict + # Optional fields + project_build_tools: list[BuildToolDict] + components: list[ComponentDict] + licenses: list[LicenseDict] + + +# Type alias for licenses dictionary +LicensesDict = dict[str, LicenseDict] + + +def validate_required_field( + data: Union[dict[str, Any], TomlDataDict, ComponentDict, RootComponentDict], + field: str, + context: str, +) -> Any: + """Validate that a required field exists in the data dictionary. + + Args: + data: The dictionary to check + field: The required field name + context: Description of where this validation is happening (for error messages) + + Returns: + The value of the required field + + Raises: + TomlRequiredFieldError: If the required field is missing + """ + if field not in data: + raise TomlRequiredFieldError(f"Missing required field '{field}' in {context}") + return data[field] + + +def _parse_build_date(date_str: str) -> datetime: + """Parse build date string, handling Python 3.9+ compatibility for Z suffix.""" + override_to_utc = False + if sys.version_info < (3, 11) and date_str.endswith("Z"): + # Remove the Z at the end to support Python 3.9+, because + # datetime.fromisoformat learned to parse the Z in 3.11 only. + # In that case we need to override the timezone to be UTC manually. + date_str = date_str[:-1] + override_to_utc = True + + build_date = datetime.fromisoformat(date_str) + if override_to_utc: + build_date = build_date.replace(tzinfo=timezone.utc) + return build_date + + +def assign_optional_field( + source_data: Union[ + dict[str, Any], TomlDataDict, ComponentDict, RootComponentDict, BuildToolDict + ], + target_args: dict[str, Any], + source_field: str, + target_field: Optional[str] = None, + transform: Optional[Callable[[Any], Any]] = None, +) -> None: + """Assign an optional field from source data to target args if it exists. + + Args: + source_data: The source dictionary to read from + target_args: The target dictionary to write to + source_field: The field name in the source data + target_field: The field name in the target args (defaults to source_field if None) + transform: Optional function to transform the value before assignment + """ + if target_field is None: + target_field = source_field + + if value := source_data.get(source_field): + if transform: + value = transform(value) + # Only assign if the transformed value is not None + if value is not None: + target_args[target_field] = value + + +def main() -> None: + parser = get_cli_parser() + args = get_cli_args(parser) + + handle_verbose_logging(args.verbose) + + try: + create_cyclone_dx_sbom( + args.input_path, + args.output_path, + args.validate_json, + args.validate_json_required, + ) + except ( + TomlRequiredFieldError, + MissingTomlFileError, + SBOMWriteFailedError, + ValidationFailedError, + ValidationDependencyMissingError, + ) as e: + log.error(str(e)) + sys.exit(1) + + +def get_cli_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + formatter_class=argparse.RawTextHelpFormatter, + description=""" +Generates a CycloneDX v1.6 SBOM using intermediate information from a toml file created by Qt's +SBOM generator. + +Example usage: +python qt_cyclonedx_generator.py --input-path /qt/qtbase.cdx.toml --output-path /qt/qtbase.cdx.json +""", + ) + parser.add_argument( + "-i", + "--input-path", + type=Path, + help="Path to the intermediate toml file.", + required=True, + ) + parser.add_argument( + "-o", + "--output-path", + type=Path, + help="Path to the CycloneDX output file.", + required=True, + ) + parser.add_argument( + "--validate-json", + action="store_true", + help="Validate the generated CycloneDX JSON output against the CycloneDX schema.", + ) + parser.add_argument( + "--validate-json-required", + action="store_true", + help="Error out if validation dependencies are missing.", + ) + parser.add_argument( + "--verbose", + action="store_true", + help="Enable verbose logging output.", + ) + + return parser + + +def get_cli_args(parser: argparse.ArgumentParser) -> argparse.Namespace: + args = parser.parse_args(args=None if sys.argv[1:] else ["--help"]) + return args + + +def handle_verbose_logging(verbose: bool) -> None: + if verbose: + log.setLevel(logging.DEBUG) + + +# Reads a toml file and generates a CycloneDX SBOM file. +def create_cyclone_dx_sbom( + input_path: Path, + output_path: Path, + validate_json=False, + validate_json_required=False, +) -> None: + if not input_path or not input_path.exists(): + raise MissingTomlFileError( + f"Failed to read toml file. Input file '{input_path}' does not exist." + ) + + cydx_bom = get_cydx_bom() + toml_data = read_toml_file(input_path) + process_toml(toml_data, cydx_bom) + json = write_cydx_sbom(cydx_bom, output_path) + if validate_json: + validate_cydx_bom(json, validate_json_required) + + +def read_toml_file(input_path: Path) -> TomlDataDict: + log.debug(f"Reading toml file from '{input_path}'") + with open(input_path, "rb") as f: + # Manually read and decode the file contents, because older + # tomli versions vendored in pip don't handle this properly. + buffer = f.read() + toml_str = buffer.decode() + return cast(TomlDataDict, cast(Any, tomllib).loads(toml_str)) + + +# Serializes the CycloneDX BOM to JSON and writes it to the output path. +def write_cydx_sbom(cydx_bom: Bom, output_path: Path) -> str: + json_outputter: JsonOutputter = JsonV1Dot6(cydx_bom) + log.debug(f"Serializing CycloneDX BOM to JSON at '{output_path}'") + serialized_json = json_outputter.output_as_string(indent=2) + + try: + with open(output_path, "w") as f: + f.write(serialized_json) + except (OSError, IOError) as e: + raise SBOMWriteFailedError(f"Failed to write SBOM to '{output_path}': {e}") + + return serialized_json + + +def validate_cydx_bom(serialized_json: str, validate_json_required: bool) -> None: + log.debug("Validating CycloneDX JSON output against schema v1.6") + json_validator = JsonStrictValidator(SchemaVersion.V1_6) + try: + validation_errors = json_validator.validate_str(serialized_json) + if validation_errors: + raise ValidationFailedError(f"JSON validation failed: {validation_errors}") + except MissingOptionalDependencyException as error: + missing_dep_message = ( + f"JSON-validation was failed due to missing Python dependency: {error}" + ) + if validate_json_required: + raise ValidationDependencyMissingError(missing_dep_message) + else: + log.warning(missing_dep_message) + + +def get_cydx_bom() -> Bom: + bom = Bom() + return bom + + +# Reads toml data created by Qt's SBOM generator and populates the CycloneDX BOM. +# At minimum, expects a root component dict, and a list of components. +def process_toml(toml_data: TomlDataDict, cydx_bom: Bom) -> None: + components: dict[str, Component] = {} + + log.debug("Processing TOML data.") + + root_component_object, main_supplier, project_spdx_id = handle_root_component( + cydx_bom, toml_data + ) + components[project_spdx_id] = root_component_object + + if "project_build_tools" in toml_data: + project_build_tools = toml_data["project_build_tools"] + handle_project_build_tools(cydx_bom, project_build_tools) + + external_components_spdx_ids = set() + components_toml = [] + license_factory = LicenseFactory() + + # Create one component per CMake target (in spdx it would be a spdx package). + if "components" in toml_data: + components_toml = toml_data["components"] + + components_len = len(components_toml) + log.debug(f"Processing {components_len} components from TOML data.") + + licenses_dict: LicensesDict = {} + if "licenses" in toml_data: + licenses_toml = toml_data["licenses"] + for license_entry in licenses_toml: + license_id = license_entry["license_id"] + licenses_dict[license_id] = license_entry + + for component_toml in components_toml: + process_component( + cydx_bom, + component_toml, + licenses_dict, + components, + external_components_spdx_ids, + main_supplier, + license_factory, + ) + + # Register dependencies for each component. + log.debug("Processing component dependencies.") + for component_toml in components_toml: + handle_component_dependencies( + cydx_bom, + component_toml, + components, + external_components_spdx_ids, + root_component_object, + ) + + +# Creates the root component of the BOM. +# The CycloneDX root component is mapped to the Qt spdx project package e.g. qtbase. +def handle_root_component( + cydx_bom: Bom, toml_data: TomlDataDict +) -> tuple[Component, Optional[OrganizationalEntity], str]: + root_components_args: dict[str, Any] = { + "type": ComponentType.FRAMEWORK, + } + + root_component = validate_required_field(toml_data, "root_component", "TOML data") + error_context = "root_component" + project_name = validate_required_field(root_component, "name", error_context) + root_components_args["name"] = project_name + + project_spdx_id = validate_required_field(root_component, "spdx_id", error_context) + root_components_args["bom_ref"] = project_spdx_id + + # Process optional fields + assign_optional_field(root_component, root_components_args, "version") + assign_optional_field(root_component, root_components_args, "description") + + # Process optional fields with transformations + assign_optional_field( + root_component, + root_components_args, + source_field="download_location", + target_field="external_references", + transform=lambda url: [ + ExternalReference( + type=ExternalReferenceType.DISTRIBUTION, # pyright: ignore [reportCallIssue] + url=XsUri(url), # pyright: ignore [reportCallIssue] + ) + ], + ) + + if project_build_date_str := root_component.get("build_date"): + project_build_date = _parse_build_date(project_build_date_str) + cydx_bom.metadata.timestamp = project_build_date # pyright: ignore [reportAttributeAccessIssue] + + if serial_number_uuid := root_component.get("serial_number_uuid"): + cydx_bom.serial_number = UUID(serial_number_uuid) # pyright: ignore [reportAttributeAccessIssue] + + project_supplier = root_component.get("supplier") + project_supplier_url = root_component.get("supplier_url") + + # TODO: This is currently not supplied by the build system API. + project_supplier_email = root_component.get("supplier_email") + + main_supplier = create_organization_entity(project_supplier, project_supplier_url) + main_author = create_organization_contact(project_supplier, project_supplier_email) + + if main_supplier: + cydx_bom.metadata.manufacturer = main_supplier # pyright: ignore [reportAttributeAccessIssue] + cydx_bom.metadata.supplier = main_supplier # pyright: ignore [reportAttributeAccessIssue] + + # FOSSA complains if author is not set. + cydx_bom.metadata.authors = [main_author] # pyright: ignore [reportAttributeAccessIssue] + + root_components_args["supplier"] = main_supplier + root_components_args["manufacturer"] = main_supplier + + root_component_object = Component(**root_components_args) + cydx_bom.metadata.component = root_component_object # pyright: ignore [reportAttributeAccessIssue] + + cydx_bom.metadata.lifecycles = [PredefinedLifecycle(phase=LifecyclePhase.BUILD)] # pyright: ignore [reportAttributeAccessIssue, reportCallIssue] + + root_component_name_str = f"{root_component_object.name}" # pyright: ignore [reportAttributeAccessIssue] + root_component_version_str = f"(version {root_component_object.version})" # pyright: ignore [reportAttributeAccessIssue] + log.debug( + f"Created root component of the BOM. " + f"{root_component_name_str} ${root_component_version_str}" + ) + + return root_component_object, main_supplier, project_spdx_id + + +# Handles project build tools and adds them as components to the BOM metadata tools section. +# This is for tools like CMake, Ninja, etc. +# Unclear yet if compilers and linkers should also go here. +def handle_project_build_tools( + cydx_bom: Bom, project_build_tools_toml: list[BuildToolDict] +) -> None: + log.debug("Processing project build tools.") + + for build_tool_toml in project_build_tools_toml: + component_args = {} + + assign_optional_field(build_tool_toml, component_args, source_field="name") + assign_optional_field( + build_tool_toml, + component_args, + source_field="component_type", + target_field="type", + transform=get_component_type_for_str, + ) + assign_optional_field(build_tool_toml, component_args, "version") + assign_optional_field(build_tool_toml, component_args, "description") + + component = Component(**component_args) + log.debug( + f"Created build tool component '{component.name} (version {component.version})'." # pyright: ignore [reportAttributeAccessIssue] + ) + cydx_bom.metadata.tools.components.add(component) # pyright: ignore [reportAttributeAccessIssue] + + +# Creates a CycloneDX component from the toml component data and adds it to the BOM. +# The SPDX equivalent would be a SPDX package backed by a CMake target. +# We mostly reuse spdx ids as bom-refs, rather than generating new ones. Makes it +# easier to cross-check with the original SPDX data. And it's not like CycloneDX forbids it. +def process_component( + cydx_bom: Bom, + component_toml: ComponentDict, + licenses_dict: LicensesDict, + components: dict[str, Component], + external_components_spdx_ids: set[str], + main_supplier: Optional[OrganizationalEntity], + license_factory: LicenseFactory, +) -> None: + component_args = {} + properties = [] + + error_context = "component" + component_name = validate_required_field(component_toml, "name", error_context) + component_args["name"] = component_name + + assign_optional_field( + component_toml, + component_args, + "version", + transform=lambda v: v if v != "unknown" else None, + ) + + # A spdx id maps to a bom-ref in CycloneDX. + spdx_id = validate_required_field(component_toml, "spdx_id", error_context) + component_args["bom_ref"] = spdx_id + + # Some components might be external to the current BOM. + # For example for the QtSvg module, QtCore is external, because it's built in qtbase, + # not qtsvg. To be able to declare a dependency on QtCore, we still need to add it as a + # component. But we add a BOM external reference link to it, signalling that the full + # component information is in a different BOM. + assign_optional_field( + component_toml, + component_args, + source_field="external_bom_link", + target_field="external_references", + transform=lambda link: [ + ExternalReference( + type=ExternalReferenceType.BOM, # pyright: ignore [reportCallIssue] + url=XsUri(link), # pyright: ignore [reportCallIssue] + ) + ], + ) + if component_toml.get("external_bom_link"): + external_components_spdx_ids.add(spdx_id) + + assign_optional_field( + component_toml, + component_args, + source_field="component_type", + target_field="type", + transform=get_component_type_for_str, + ) + + assign_optional_field( + component_toml, + component_args, + source_field="copyright", + target_field="copyright", + ) + + handle_component_licenses( + component_toml, licenses_dict, license_factory, component_args + ) + + purl_list = component_toml.get("purl_list", []) + cpe_list = component_toml.get("cpe_list", []) + cpe_purl_args, extra_cpe_purl_properties = get_purl_and_cpe_component_args( + purl_list, cpe_list + ) + if extra_cpe_purl_properties: + properties.extend(extra_cpe_purl_properties) + + if supplier_name := component_toml.get("supplier"): + # If the specified supplier is the same as the main supplier, reuse it, because it has a + # url. + if main_supplier and supplier_name == main_supplier.name: # pyright: ignore [reportAttributeAccessIssue] + component_args["supplier"] = main_supplier + else: + component_supplier = create_organization_entity(supplier_name) + component_args["supplier"] = component_supplier + + # Properties are extra vendor-specific information that are not part of the official spec. + if properties_list := component_toml.get("properties"): + for prop in properties_list: + properties.append( + Property( + name=prop["name"], # pyright: ignore [reportCallIssue] + value=prop["value"], # pyright: ignore [reportCallIssue] + ) + ) + if properties: + component_args["properties"] = properties + + component = Component(**component_args, **cpe_purl_args) + + log.debug(f"Created component '{component.name} (version {component.version})'.") # pyright: ignore [reportAttributeAccessIssue] + + cydx_bom.components.add(component) # pyright: ignore [reportAttributeAccessIssue] + components[spdx_id] = component + + +# Creates dependencies between components in the BOM, as well as between the root component +# and non-external components that are part of the current BOM. +def handle_component_dependencies( + cydx_bom: Bom, + component_toml: ComponentDict, + components: dict[str, Component], + external_components_spdx_ids: set[str], + root_component_object: Component, +) -> None: + component_id = component_toml["spdx_id"] + component_object = components.get(component_id) + + # If the component is not external, register it as a dependency of the root component, + # aka the root component "contains" the non-external component. + if component_id not in external_components_spdx_ids: + cydx_bom.register_dependency(root_component_object, [component_object]) # pyright: ignore [reportAttributeAccessIssue, reportArgumentType] + + # Register dependencies declared in the toml. + # CycloneDX does not support different dependency types, like SPDX does. + if dependencies := component_toml.get("dependencies"): + for dependency_spdx_id in dependencies: + if dependency_spdx_id not in components: + log.warning( + f"Component {component_id} has a dependency on unknown component {dependency_spdx_id}, skipping." + ) + continue + dep_component = components[dependency_spdx_id] + cydx_bom.register_dependency(component_object, [dep_component]) # pyright: ignore [reportAttributeAccessIssue, reportArgumentType] + log.debug( + f"Created dependency from '{component_id}' to '{dependency_spdx_id}'." + ) + + +# Handles PURL and CPE entries for a component. +# The spec says that a component can have exactly one PURL. Qt generates several, because it's +# unclear what will end up being useful for tooling. The other PURLs are added as extra +# component properties. Same for CPEs. +# The field names were picked to match the names generated by the python spdx to cyclonedx converter. +def get_purl_and_cpe_component_args( + purl_list: list[str], + cpe_list: list[str], +) -> tuple[dict[str, Any], list[Property]]: + component_args = {} + properties = [] + + purl_objects = [PackageURL.from_string(purl) for purl in purl_list if purl] + + if len(purl_objects) > 0: + # First PURL will be the main one. + purl = purl_objects.pop(0) + component_args["purl"] = purl + + # Rest will be properties. + for purl_entry in purl_objects: + properties.append( + Property( + name="spdx:external-reference:package-manager:purl", # pyright: ignore [reportCallIssue] + value=str(purl_entry), # pyright: ignore [reportCallIssue] + ) + ) + + if len(cpe_list) > 0: + # First CPE will be the main one. + cpe = cpe_list.pop(0) + component_args["cpe"] = cpe + + # Rest will be properties. + for cpe_entry in cpe_list: + properties.append( + Property( + name="spdx:external-reference:security:cpe23", # pyright: ignore [reportCallIssue] + value=str(cpe_entry), # pyright: ignore [reportCallIssue] + ) + ) + + return component_args, properties + + +# Handles licenses for a component. +def handle_component_licenses( + component_toml: ComponentDict, + licenses_dict: LicensesDict, + license_factory: LicenseFactory, + component_args: dict[str, Any], +) -> None: + def transformer(license_expression: str) -> Optional[list[LicenseExpression]]: + acknowledgement = LicenseAcknowledgement.CONCLUDED + return convert_license_expression_license_objects( + license_expression, licenses_dict, license_factory, acknowledgement + ) + + assign_optional_field( + component_toml, + component_args, + source_field="license_concluded_expression", + target_field="licenses", + transform=transformer, + ) + + +# Transforms a spdx license expression into CycloneDX license objects. +def convert_license_expression_license_objects( + license_expression: str, + licenses_dict: LicensesDict, + license_factory: LicenseFactory, + acknowledgement: LicenseAcknowledgement, +) -> Optional[list[LicenseExpression]]: + if "LicenseRef-" in license_expression: + return handle_custom_license_ref_expression( + license_expression, licenses_dict, license_factory, acknowledgement + ) + else: + return handle_standard_license_ref_expression( + license_expression, license_factory, acknowledgement + ) + + +# CycloneDX v1.6 doesn't support an expression that contains LicenseRef- in it. +# CycloneDX v1.7 seemingly does. +# https://github.com/CycloneDX/specification/issues/454 +# https://github.com/CycloneDX/specification/issues/554 +# https://github.com/CycloneDX/specification/pull/582 +# +# Split LicenseRef-* expressions into separate licenses, by which we lose the expression structure, +# but at least preserve the license information. +# +# TODO: Improve this when CycloneDX v1.7 generation is added. +def handle_custom_license_ref_expression( + license_expression: str, + licenses_dict: LicensesDict, + license_factory: LicenseFactory, + acknowledgement: LicenseAcknowledgement, +) -> list[LicenseExpression]: + license_objects = [] + licensing = get_spdx_licensing() + symbols = licensing.license_symbols(license_expression) + + for symbol in symbols: + license_text = None + # This can be either a known public spdx id, in which case we won't have text for. + # Or it can be a LicenseRef-* for which we have license text in the licenses_dict. + maybe_license_id = symbol.key # pyright: ignore [reportAttributeAccessIssue] + + if maybe_license_id in licenses_dict: + # Get the text for the LicenseRef-* entry. + maybe_license_text = licenses_dict[maybe_license_id].get("text") + license_text = AttachedText(content=maybe_license_text) # pyright: ignore [reportCallIssue] + + license_obj = license_factory.make_from_string( + maybe_license_id, + license_text=license_text, + license_acknowledgement=acknowledgement, + ) + license_objects.append(license_obj) + + return license_objects + + +# Transforms a spdx license expression without LicenseRef-* into a CycloneDX license object. +def handle_standard_license_ref_expression( + license_expression: str, + license_factory: LicenseFactory, + acknowledgement: LicenseAcknowledgement, +) -> list[LicenseExpression]: + license_obj = license_factory.make_with_expression( + expression=license_expression, acknowledgement=acknowledgement + ) + return [license_obj] + + +# Transform from enum string value to actual enum. +def get_component_type_for_str(component_type_str: str) -> ComponentType: + return ComponentType(component_type_str) + + +# Creates an OrganizationalEntity for a supplier/manufacturer. +def create_organization_entity( + supplier_name: Optional[str] = None, supplier_url: Optional[str] = None +) -> Optional[OrganizationalEntity]: + args = {} + if supplier_name: + args["name"] = supplier_name + if supplier_url: + args["urls"] = [XsUri(supplier_url)] # pyright: ignore [reportCallIssue] + + # Check if args has at least one entry. + if len(args) > 0: + return OrganizationalEntity(**args) + + return None + + +# Creates an OrganizationalContact for a supplier/manufacturer. +def create_organization_contact( + contact_name: Optional[str] = None, contact_email: Optional[str] = None +) -> Optional[OrganizationalContact]: + args = {} + if contact_name: + args["name"] = contact_name + if contact_email: + args["email"] = contact_email # pyright: ignore [reportCallIssue] + + # Check if args has at least one entry. + if len(args) > 0: + return OrganizationalContact(**args) + + return None + + +if __name__ == "__main__": + main() diff --git a/util/sbom/cyclonedx/uv.lock b/util/sbom/cyclonedx/uv.lock new file mode 100644 index 00000000000..0b16a99e481 --- /dev/null +++ b/util/sbom/cyclonedx/uv.lock @@ -0,0 +1,625 @@ +version = 1 +revision = 3 +requires-python = ">=3.9" +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version < '3.10'", +] + +[[package]] +name = "arrow" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/33/032cdc44182491aa708d06a68b62434140d8c50820a087fac7af37703357/arrow-1.4.0.tar.gz", hash = "sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7", size = 152931, upload-time = "2025-10-18T17:46:46.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-py3-none-any.whl", hash = "sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205", size = 68797, upload-time = "2025-10-18T17:46:45.663Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "boolean-py" +version = "5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/cf/85379f13b76f3a69bca86b60237978af17d6aa0bc5998978c3b8cf05abb2/boolean_py-5.0.tar.gz", hash = "sha256:60cbc4bad079753721d32649545505362c754e121570ada4658b852a3a318d95", size = 37047, upload-time = "2025-04-03T10:39:49.734Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/ca/78d423b324b8d77900030fa59c4aa9054261ef0925631cd2501dd015b7b7/boolean_py-5.0-py3-none-any.whl", hash = "sha256:ef28a70bd43115208441b53a045d1549e2f0ec6e3d08a9d142cbc41c1938e8d9", size = 26577, upload-time = "2025-04-03T10:39:48.449Z" }, +] + +[[package]] +name = "cyclonedx-python-lib" +version = "11.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "license-expression" }, + { name = "packageurl-python" }, + { name = "py-serializable" }, + { name = "sortedcontainers" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/3b/c130406ae35bd264be7f816afbb01f95ccc4a63a5eeadfea5249d110efb2/cyclonedx_python_lib-11.3.0.tar.gz", hash = "sha256:ee3135a59ead90397339ea585b341a7ea9c53e734ad13f48a9865cfcf0f1139a", size = 1046415, upload-time = "2025-10-22T11:03:04.883Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/28/3bebcfe7f87e485164fba1dca08b6307bdbee6cc45d6bf57343552346f85/cyclonedx_python_lib-11.3.0-py3-none-any.whl", hash = "sha256:08094495d0cbbaddbe605f60f6e48ae98d29263386d7926df83ffe9bdea55f78", size = 383140, upload-time = "2025-10-22T11:03:03.102Z" }, +] + +[package.optional-dependencies] +json-validation = [ + { name = "jsonschema", extra = ["format-nongpl"] }, + { name = "referencing", version = "0.36.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "referencing", version = "0.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + +[[package]] +name = "fqdn" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/3e/a80a8c077fd798951169626cde3e239adeba7dab75deb3555716415bd9b0/fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f", size = 6015, upload-time = "2021-03-11T07:16:29.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014", size = 9121, upload-time = "2021-03-11T07:16:28.351Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "isoduration" +version = "20.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "arrow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/1a/3c8edc664e06e6bd06cce40c6b22da5f1429aa4224d0c590f3be21c91ead/isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9", size = 11649, upload-time = "2020-11-01T11:00:00.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042", size = 11321, upload-time = "2020-11-01T10:59:58.02Z" }, +] + +[[package]] +name = "jsonpointer" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114, upload-time = "2024-06-10T19:24:42.462Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload-time = "2024-06-10T19:24:40.698Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing", version = "0.36.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "referencing", version = "0.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, +] + +[package.optional-dependencies] +format-nongpl = [ + { name = "fqdn" }, + { name = "idna" }, + { name = "isoduration" }, + { name = "jsonpointer" }, + { name = "rfc3339-validator" }, + { name = "rfc3986-validator" }, + { name = "rfc3987-syntax" }, + { name = "uri-template" }, + { name = "webcolors" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing", version = "0.36.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "referencing", version = "0.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "lark" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/37/a13baf0135f348af608c667633cbe5d13aa2c5c15a56ae9ad3e6cba45ae3/lark-1.3.0.tar.gz", hash = "sha256:9a3839d0ca5e1faf7cfa3460e420e859b66bcbde05b634e73c369c8244c5fa48", size = 259551, upload-time = "2025-09-22T13:45:05.072Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/3e/1c6b43277de64fc3c0333b0e72ab7b52ddaaea205210d60d9b9f83c3d0c7/lark-1.3.0-py3-none-any.whl", hash = "sha256:80661f261fb2584a9828a097a2432efd575af27d20be0fd35d17f0fe37253831", size = 113002, upload-time = "2025-09-22T13:45:03.747Z" }, +] + +[[package]] +name = "license-expression" +version = "30.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "boolean-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/71/d89bb0e71b1415453980fd32315f2a037aad9f7f70f695c7cec7035feb13/license_expression-30.4.4.tar.gz", hash = "sha256:73448f0aacd8d0808895bdc4b2c8e01a8d67646e4188f887375398c761f340fd", size = 186402, upload-time = "2025-07-22T11:13:32.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/40/791891d4c0c4dab4c5e187c17261cedc26285fd41541577f900470a45a4d/license_expression-30.4.4-py3-none-any.whl", hash = "sha256:421788fdcadb41f049d2dc934ce666626265aeccefddd25e162a26f23bcbf8a4", size = 120615, upload-time = "2025-07-22T11:13:31.217Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, +] + +[[package]] +name = "packageurl-python" +version = "0.17.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/f0/de0ac00a4484c0d87b71e3d9985518278d89797fa725e90abd3453bccb42/packageurl_python-0.17.5.tar.gz", hash = "sha256:a7be3f3ba70d705f738ace9bf6124f31920245a49fa69d4b416da7037dd2de61", size = 43832, upload-time = "2025-08-06T14:08:20.235Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/78/9dbb7d2ef240d20caf6f79c0f66866737c9d0959601fd783ff635d1d019d/packageurl_python-0.17.5-py3-none-any.whl", hash = "sha256:f0e55452ab37b5c192c443de1458e3f3b4d8ac27f747df6e8c48adeab081d321", size = 30544, upload-time = "2025-08-06T14:08:19.055Z" }, +] + +[[package]] +name = "py-serializable" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "defusedxml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/21/d250cfca8ff30c2e5a7447bc13861541126ce9bd4426cd5d0c9f08b5547d/py_serializable-2.1.0.tar.gz", hash = "sha256:9d5db56154a867a9b897c0163b33a793c804c80cee984116d02d49e4578fc103", size = 52368, upload-time = "2025-07-21T09:56:48.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/bf/7595e817906a29453ba4d99394e781b6fabe55d21f3c15d240f85dd06bb1/py_serializable-2.1.0-py3-none-any.whl", hash = "sha256:b56d5d686b5a03ba4f4db5e769dc32336e142fc3bd4d68a8c25579ebb0a67304", size = 23045, upload-time = "2025-07-21T09:56:46.848Z" }, +] + +[[package]] +name = "pyright" +version = "1.1.406" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/16/6b4fbdd1fef59a0292cbb99f790b44983e390321eccbc5921b4d161da5d1/pyright-1.1.406.tar.gz", hash = "sha256:c4872bc58c9643dac09e8a2e74d472c62036910b3bd37a32813989ef7576ea2c", size = 4113151, upload-time = "2025-10-02T01:04:45.488Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/a2/e309afbb459f50507103793aaef85ca4348b66814c86bc73908bdeb66d12/pyright-1.1.406-py3-none-any.whl", hash = "sha256:1d81fb43c2407bf566e97e57abb01c811973fdb21b2df8df59f870f688bdca71", size = 5980982, upload-time = "2025-10-02T01:04:43.137Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "qt-cyclonedx-generator" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "cyclonedx-python-lib", extra = ["json-validation"] }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pyright" }, + { name = "ruff" }, + { name = "setuptools" }, + { name = "tomli" }, +] + +[package.metadata] +requires-dist = [ + { name = "cyclonedx-python-lib", extras = ["json-validation"], specifier = ">=10.0.0" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "pyright", specifier = ">=1.1.406" }, + { name = "ruff", specifier = ">=0.11.2" }, + { name = "setuptools", specifier = ">=65" }, + { name = "tomli", specifier = ">=2.3.0" }, +] + +[[package]] +name = "referencing" +version = "0.36.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "attrs", marker = "python_full_version < '3.10'" }, + { name = "rpds-py", marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "attrs", marker = "python_full_version >= '3.10'" }, + { name = "rpds-py", marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10' and python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "rfc3339-validator" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" }, +] + +[[package]] +name = "rfc3986-validator" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/88/f270de456dd7d11dcc808abfa291ecdd3f45ff44e3b549ffa01b126464d0/rfc3986_validator-0.1.1.tar.gz", hash = "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055", size = 6760, upload-time = "2019-10-28T16:00:19.144Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/51/17023c0f8f1869d8806b979a2bffa3f861f26a3f1a66b094288323fba52f/rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9", size = 4242, upload-time = "2019-10-28T16:00:13.976Z" }, +] + +[[package]] +name = "rfc3987-syntax" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lark" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/06/37c1a5557acf449e8e406a830a05bf885ac47d33270aec454ef78675008d/rfc3987_syntax-1.1.0.tar.gz", hash = "sha256:717a62cbf33cffdd16dfa3a497d81ce48a660ea691b1ddd7be710c22f00b4a0d", size = 14239, upload-time = "2025-07-18T01:05:05.015Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl", hash = "sha256:6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f", size = 8046, upload-time = "2025-07-18T01:05:03.843Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.27.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/dd/2c0cbe774744272b0ae725f44032c77bdcab6e8bcf544bffa3b6e70c8dba/rpds_py-0.27.1.tar.gz", hash = "sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8", size = 27479, upload-time = "2025-08-27T12:16:36.024Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/ed/3aef893e2dd30e77e35d20d4ddb45ca459db59cead748cad9796ad479411/rpds_py-0.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:68afeec26d42ab3b47e541b272166a0b4400313946871cba3ed3a4fc0cab1cef", size = 371606, upload-time = "2025-08-27T12:12:25.189Z" }, + { url = "https://files.pythonhosted.org/packages/6d/82/9818b443e5d3eb4c83c3994561387f116aae9833b35c484474769c4a8faf/rpds_py-0.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74e5b2f7bb6fa38b1b10546d27acbacf2a022a8b5543efb06cfebc72a59c85be", size = 353452, upload-time = "2025-08-27T12:12:27.433Z" }, + { url = "https://files.pythonhosted.org/packages/99/c7/d2a110ffaaa397fc6793a83c7bd3545d9ab22658b7cdff05a24a4535cc45/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9024de74731df54546fab0bfbcdb49fae19159ecaecfc8f37c18d2c7e2c0bd61", size = 381519, upload-time = "2025-08-27T12:12:28.719Z" }, + { url = "https://files.pythonhosted.org/packages/5a/bc/e89581d1f9d1be7d0247eaef602566869fdc0d084008ba139e27e775366c/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:31d3ebadefcd73b73928ed0b2fd696f7fefda8629229f81929ac9c1854d0cffb", size = 394424, upload-time = "2025-08-27T12:12:30.207Z" }, + { url = "https://files.pythonhosted.org/packages/ac/2e/36a6861f797530e74bb6ed53495f8741f1ef95939eed01d761e73d559067/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2e7f8f169d775dd9092a1743768d771f1d1300453ddfe6325ae3ab5332b4657", size = 523467, upload-time = "2025-08-27T12:12:31.808Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/c1bc2be32564fa499f988f0a5c6505c2f4746ef96e58e4d7de5cf923d77e/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d905d16f77eb6ab2e324e09bfa277b4c8e5e6b8a78a3e7ff8f3cdf773b4c013", size = 402660, upload-time = "2025-08-27T12:12:33.444Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ec/ef8bf895f0628dd0a59e54d81caed6891663cb9c54a0f4bb7da918cb88cf/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50c946f048209e6362e22576baea09193809f87687a95a8db24e5fbdb307b93a", size = 384062, upload-time = "2025-08-27T12:12:34.857Z" }, + { url = "https://files.pythonhosted.org/packages/69/f7/f47ff154be8d9a5e691c083a920bba89cef88d5247c241c10b9898f595a1/rpds_py-0.27.1-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:3deab27804d65cd8289eb814c2c0e807c4b9d9916c9225e363cb0cf875eb67c1", size = 401289, upload-time = "2025-08-27T12:12:36.085Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d9/ca410363efd0615814ae579f6829cafb39225cd63e5ea5ed1404cb345293/rpds_py-0.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8b61097f7488de4be8244c89915da8ed212832ccf1e7c7753a25a394bf9b1f10", size = 417718, upload-time = "2025-08-27T12:12:37.401Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a0/8cb5c2ff38340f221cc067cc093d1270e10658ba4e8d263df923daa18e86/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8a3f29aba6e2d7d90528d3c792555a93497fe6538aa65eb675b44505be747808", size = 558333, upload-time = "2025-08-27T12:12:38.672Z" }, + { url = "https://files.pythonhosted.org/packages/6f/8c/1b0de79177c5d5103843774ce12b84caa7164dfc6cd66378768d37db11bf/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd6cd0485b7d347304067153a6dc1d73f7d4fd995a396ef32a24d24b8ac63ac8", size = 589127, upload-time = "2025-08-27T12:12:41.48Z" }, + { url = "https://files.pythonhosted.org/packages/c8/5e/26abb098d5e01266b0f3a2488d299d19ccc26849735d9d2b95c39397e945/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f4461bf931108c9fa226ffb0e257c1b18dc2d44cd72b125bec50ee0ab1248a9", size = 554899, upload-time = "2025-08-27T12:12:42.925Z" }, + { url = "https://files.pythonhosted.org/packages/de/41/905cc90ced13550db017f8f20c6d8e8470066c5738ba480d7ba63e3d136b/rpds_py-0.27.1-cp310-cp310-win32.whl", hash = "sha256:ee5422d7fb21f6a00c1901bf6559c49fee13a5159d0288320737bbf6585bd3e4", size = 217450, upload-time = "2025-08-27T12:12:44.813Z" }, + { url = "https://files.pythonhosted.org/packages/75/3d/6bef47b0e253616ccdf67c283e25f2d16e18ccddd38f92af81d5a3420206/rpds_py-0.27.1-cp310-cp310-win_amd64.whl", hash = "sha256:3e039aabf6d5f83c745d5f9a0a381d031e9ed871967c0a5c38d201aca41f3ba1", size = 228447, upload-time = "2025-08-27T12:12:46.204Z" }, + { url = "https://files.pythonhosted.org/packages/b5/c1/7907329fbef97cbd49db6f7303893bd1dd5a4a3eae415839ffdfb0762cae/rpds_py-0.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:be898f271f851f68b318872ce6ebebbc62f303b654e43bf72683dbdc25b7c881", size = 371063, upload-time = "2025-08-27T12:12:47.856Z" }, + { url = "https://files.pythonhosted.org/packages/11/94/2aab4bc86228bcf7c48760990273653a4900de89c7537ffe1b0d6097ed39/rpds_py-0.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:62ac3d4e3e07b58ee0ddecd71d6ce3b1637de2d373501412df395a0ec5f9beb5", size = 353210, upload-time = "2025-08-27T12:12:49.187Z" }, + { url = "https://files.pythonhosted.org/packages/3a/57/f5eb3ecf434342f4f1a46009530e93fd201a0b5b83379034ebdb1d7c1a58/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4708c5c0ceb2d034f9991623631d3d23cb16e65c83736ea020cdbe28d57c0a0e", size = 381636, upload-time = "2025-08-27T12:12:50.492Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f4/ef95c5945e2ceb5119571b184dd5a1cc4b8541bbdf67461998cfeac9cb1e/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:abfa1171a9952d2e0002aba2ad3780820b00cc3d9c98c6630f2e93271501f66c", size = 394341, upload-time = "2025-08-27T12:12:52.024Z" }, + { url = "https://files.pythonhosted.org/packages/5a/7e/4bd610754bf492d398b61725eb9598ddd5eb86b07d7d9483dbcd810e20bc/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b507d19f817ebaca79574b16eb2ae412e5c0835542c93fe9983f1e432aca195", size = 523428, upload-time = "2025-08-27T12:12:53.779Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e5/059b9f65a8c9149361a8b75094864ab83b94718344db511fd6117936ed2a/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:168b025f8fd8d8d10957405f3fdcef3dc20f5982d398f90851f4abc58c566c52", size = 402923, upload-time = "2025-08-27T12:12:55.15Z" }, + { url = "https://files.pythonhosted.org/packages/f5/48/64cabb7daced2968dd08e8a1b7988bf358d7bd5bcd5dc89a652f4668543c/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb56c6210ef77caa58e16e8c17d35c63fe3f5b60fd9ba9d424470c3400bcf9ed", size = 384094, upload-time = "2025-08-27T12:12:57.194Z" }, + { url = "https://files.pythonhosted.org/packages/ae/e1/dc9094d6ff566bff87add8a510c89b9e158ad2ecd97ee26e677da29a9e1b/rpds_py-0.27.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:d252f2d8ca0195faa707f8eb9368955760880b2b42a8ee16d382bf5dd807f89a", size = 401093, upload-time = "2025-08-27T12:12:58.985Z" }, + { url = "https://files.pythonhosted.org/packages/37/8e/ac8577e3ecdd5593e283d46907d7011618994e1d7ab992711ae0f78b9937/rpds_py-0.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6e5e54da1e74b91dbc7996b56640f79b195d5925c2b78efaa8c5d53e1d88edde", size = 417969, upload-time = "2025-08-27T12:13:00.367Z" }, + { url = "https://files.pythonhosted.org/packages/66/6d/87507430a8f74a93556fe55c6485ba9c259949a853ce407b1e23fea5ba31/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ffce0481cc6e95e5b3f0a47ee17ffbd234399e6d532f394c8dce320c3b089c21", size = 558302, upload-time = "2025-08-27T12:13:01.737Z" }, + { url = "https://files.pythonhosted.org/packages/3a/bb/1db4781ce1dda3eecc735e3152659a27b90a02ca62bfeea17aee45cc0fbc/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a205fdfe55c90c2cd8e540ca9ceba65cbe6629b443bc05db1f590a3db8189ff9", size = 589259, upload-time = "2025-08-27T12:13:03.127Z" }, + { url = "https://files.pythonhosted.org/packages/7b/0e/ae1c8943d11a814d01b482e1f8da903f88047a962dff9bbdadf3bd6e6fd1/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:689fb5200a749db0415b092972e8eba85847c23885c8543a8b0f5c009b1a5948", size = 554983, upload-time = "2025-08-27T12:13:04.516Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d5/0b2a55415931db4f112bdab072443ff76131b5ac4f4dc98d10d2d357eb03/rpds_py-0.27.1-cp311-cp311-win32.whl", hash = "sha256:3182af66048c00a075010bc7f4860f33913528a4b6fc09094a6e7598e462fe39", size = 217154, upload-time = "2025-08-27T12:13:06.278Z" }, + { url = "https://files.pythonhosted.org/packages/24/75/3b7ffe0d50dc86a6a964af0d1cc3a4a2cdf437cb7b099a4747bbb96d1819/rpds_py-0.27.1-cp311-cp311-win_amd64.whl", hash = "sha256:b4938466c6b257b2f5c4ff98acd8128ec36b5059e5c8f8372d79316b1c36bb15", size = 228627, upload-time = "2025-08-27T12:13:07.625Z" }, + { url = "https://files.pythonhosted.org/packages/8d/3f/4fd04c32abc02c710f09a72a30c9a55ea3cc154ef8099078fd50a0596f8e/rpds_py-0.27.1-cp311-cp311-win_arm64.whl", hash = "sha256:2f57af9b4d0793e53266ee4325535a31ba48e2f875da81a9177c9926dfa60746", size = 220998, upload-time = "2025-08-27T12:13:08.972Z" }, + { url = "https://files.pythonhosted.org/packages/bd/fe/38de28dee5df58b8198c743fe2bea0c785c6d40941b9950bac4cdb71a014/rpds_py-0.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90", size = 361887, upload-time = "2025-08-27T12:13:10.233Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/4b6c7eedc7dd90986bf0fab6ea2a091ec11c01b15f8ba0a14d3f80450468/rpds_py-0.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5", size = 345795, upload-time = "2025-08-27T12:13:11.65Z" }, + { url = "https://files.pythonhosted.org/packages/6f/0e/e650e1b81922847a09cca820237b0edee69416a01268b7754d506ade11ad/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e", size = 385121, upload-time = "2025-08-27T12:13:13.008Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ea/b306067a712988e2bff00dcc7c8f31d26c29b6d5931b461aa4b60a013e33/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881", size = 398976, upload-time = "2025-08-27T12:13:14.368Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0a/26dc43c8840cb8fe239fe12dbc8d8de40f2365e838f3d395835dde72f0e5/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec", size = 525953, upload-time = "2025-08-27T12:13:15.774Z" }, + { url = "https://files.pythonhosted.org/packages/22/14/c85e8127b573aaf3a0cbd7fbb8c9c99e735a4a02180c84da2a463b766e9e/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb", size = 407915, upload-time = "2025-08-27T12:13:17.379Z" }, + { url = "https://files.pythonhosted.org/packages/ed/7b/8f4fee9ba1fb5ec856eb22d725a4efa3deb47f769597c809e03578b0f9d9/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5", size = 386883, upload-time = "2025-08-27T12:13:18.704Z" }, + { url = "https://files.pythonhosted.org/packages/86/47/28fa6d60f8b74fcdceba81b272f8d9836ac0340570f68f5df6b41838547b/rpds_py-0.27.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a", size = 405699, upload-time = "2025-08-27T12:13:20.089Z" }, + { url = "https://files.pythonhosted.org/packages/d0/fd/c5987b5e054548df56953a21fe2ebed51fc1ec7c8f24fd41c067b68c4a0a/rpds_py-0.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444", size = 423713, upload-time = "2025-08-27T12:13:21.436Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ba/3c4978b54a73ed19a7d74531be37a8bcc542d917c770e14d372b8daea186/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a", size = 562324, upload-time = "2025-08-27T12:13:22.789Z" }, + { url = "https://files.pythonhosted.org/packages/b5/6c/6943a91768fec16db09a42b08644b960cff540c66aab89b74be6d4a144ba/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1", size = 593646, upload-time = "2025-08-27T12:13:24.122Z" }, + { url = "https://files.pythonhosted.org/packages/11/73/9d7a8f4be5f4396f011a6bb7a19fe26303a0dac9064462f5651ced2f572f/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998", size = 558137, upload-time = "2025-08-27T12:13:25.557Z" }, + { url = "https://files.pythonhosted.org/packages/6e/96/6772cbfa0e2485bcceef8071de7821f81aeac8bb45fbfd5542a3e8108165/rpds_py-0.27.1-cp312-cp312-win32.whl", hash = "sha256:819064fa048ba01b6dadc5116f3ac48610435ac9a0058bbde98e569f9e785c39", size = 221343, upload-time = "2025-08-27T12:13:26.967Z" }, + { url = "https://files.pythonhosted.org/packages/67/b6/c82f0faa9af1c6a64669f73a17ee0eeef25aff30bb9a1c318509efe45d84/rpds_py-0.27.1-cp312-cp312-win_amd64.whl", hash = "sha256:d9199717881f13c32c4046a15f024971a3b78ad4ea029e8da6b86e5aa9cf4594", size = 232497, upload-time = "2025-08-27T12:13:28.326Z" }, + { url = "https://files.pythonhosted.org/packages/e1/96/2817b44bd2ed11aebacc9251da03689d56109b9aba5e311297b6902136e2/rpds_py-0.27.1-cp312-cp312-win_arm64.whl", hash = "sha256:33aa65b97826a0e885ef6e278fbd934e98cdcfed80b63946025f01e2f5b29502", size = 222790, upload-time = "2025-08-27T12:13:29.71Z" }, + { url = "https://files.pythonhosted.org/packages/cc/77/610aeee8d41e39080c7e14afa5387138e3c9fa9756ab893d09d99e7d8e98/rpds_py-0.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b", size = 361741, upload-time = "2025-08-27T12:13:31.039Z" }, + { url = "https://files.pythonhosted.org/packages/3a/fc/c43765f201c6a1c60be2043cbdb664013def52460a4c7adace89d6682bf4/rpds_py-0.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf", size = 345574, upload-time = "2025-08-27T12:13:32.902Z" }, + { url = "https://files.pythonhosted.org/packages/20/42/ee2b2ca114294cd9847d0ef9c26d2b0851b2e7e00bf14cc4c0b581df0fc3/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83", size = 385051, upload-time = "2025-08-27T12:13:34.228Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e8/1e430fe311e4799e02e2d1af7c765f024e95e17d651612425b226705f910/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf", size = 398395, upload-time = "2025-08-27T12:13:36.132Z" }, + { url = "https://files.pythonhosted.org/packages/82/95/9dc227d441ff2670651c27a739acb2535ccaf8b351a88d78c088965e5996/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2", size = 524334, upload-time = "2025-08-27T12:13:37.562Z" }, + { url = "https://files.pythonhosted.org/packages/87/01/a670c232f401d9ad461d9a332aa4080cd3cb1d1df18213dbd0d2a6a7ab51/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0", size = 407691, upload-time = "2025-08-27T12:13:38.94Z" }, + { url = "https://files.pythonhosted.org/packages/03/36/0a14aebbaa26fe7fab4780c76f2239e76cc95a0090bdb25e31d95c492fcd/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418", size = 386868, upload-time = "2025-08-27T12:13:40.192Z" }, + { url = "https://files.pythonhosted.org/packages/3b/03/8c897fb8b5347ff6c1cc31239b9611c5bf79d78c984430887a353e1409a1/rpds_py-0.27.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d", size = 405469, upload-time = "2025-08-27T12:13:41.496Z" }, + { url = "https://files.pythonhosted.org/packages/da/07/88c60edc2df74850d496d78a1fdcdc7b54360a7f610a4d50008309d41b94/rpds_py-0.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274", size = 422125, upload-time = "2025-08-27T12:13:42.802Z" }, + { url = "https://files.pythonhosted.org/packages/6b/86/5f4c707603e41b05f191a749984f390dabcbc467cf833769b47bf14ba04f/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd", size = 562341, upload-time = "2025-08-27T12:13:44.472Z" }, + { url = "https://files.pythonhosted.org/packages/b2/92/3c0cb2492094e3cd9baf9e49bbb7befeceb584ea0c1a8b5939dca4da12e5/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2", size = 592511, upload-time = "2025-08-27T12:13:45.898Z" }, + { url = "https://files.pythonhosted.org/packages/10/bb/82e64fbb0047c46a168faa28d0d45a7851cd0582f850b966811d30f67ad8/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002", size = 557736, upload-time = "2025-08-27T12:13:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/00/95/3c863973d409210da7fb41958172c6b7dbe7fc34e04d3cc1f10bb85e979f/rpds_py-0.27.1-cp313-cp313-win32.whl", hash = "sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3", size = 221462, upload-time = "2025-08-27T12:13:48.742Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2c/5867b14a81dc217b56d95a9f2a40fdbc56a1ab0181b80132beeecbd4b2d6/rpds_py-0.27.1-cp313-cp313-win_amd64.whl", hash = "sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83", size = 232034, upload-time = "2025-08-27T12:13:50.11Z" }, + { url = "https://files.pythonhosted.org/packages/c7/78/3958f3f018c01923823f1e47f1cc338e398814b92d83cd278364446fac66/rpds_py-0.27.1-cp313-cp313-win_arm64.whl", hash = "sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d", size = 222392, upload-time = "2025-08-27T12:13:52.587Z" }, + { url = "https://files.pythonhosted.org/packages/01/76/1cdf1f91aed5c3a7bf2eba1f1c4e4d6f57832d73003919a20118870ea659/rpds_py-0.27.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228", size = 358355, upload-time = "2025-08-27T12:13:54.012Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6f/bf142541229374287604caf3bb2a4ae17f0a580798fd72d3b009b532db4e/rpds_py-0.27.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92", size = 342138, upload-time = "2025-08-27T12:13:55.791Z" }, + { url = "https://files.pythonhosted.org/packages/1a/77/355b1c041d6be40886c44ff5e798b4e2769e497b790f0f7fd1e78d17e9a8/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2", size = 380247, upload-time = "2025-08-27T12:13:57.683Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a4/d9cef5c3946ea271ce2243c51481971cd6e34f21925af2783dd17b26e815/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723", size = 390699, upload-time = "2025-08-27T12:13:59.137Z" }, + { url = "https://files.pythonhosted.org/packages/3a/06/005106a7b8c6c1a7e91b73169e49870f4af5256119d34a361ae5240a0c1d/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802", size = 521852, upload-time = "2025-08-27T12:14:00.583Z" }, + { url = "https://files.pythonhosted.org/packages/e5/3e/50fb1dac0948e17a02eb05c24510a8fe12d5ce8561c6b7b7d1339ab7ab9c/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f", size = 402582, upload-time = "2025-08-27T12:14:02.034Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b0/f4e224090dc5b0ec15f31a02d746ab24101dd430847c4d99123798661bfc/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2", size = 384126, upload-time = "2025-08-27T12:14:03.437Z" }, + { url = "https://files.pythonhosted.org/packages/54/77/ac339d5f82b6afff1df8f0fe0d2145cc827992cb5f8eeb90fc9f31ef7a63/rpds_py-0.27.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21", size = 399486, upload-time = "2025-08-27T12:14:05.443Z" }, + { url = "https://files.pythonhosted.org/packages/d6/29/3e1c255eee6ac358c056a57d6d6869baa00a62fa32eea5ee0632039c50a3/rpds_py-0.27.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef", size = 414832, upload-time = "2025-08-27T12:14:06.902Z" }, + { url = "https://files.pythonhosted.org/packages/3f/db/6d498b844342deb3fa1d030598db93937a9964fcf5cb4da4feb5f17be34b/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081", size = 557249, upload-time = "2025-08-27T12:14:08.37Z" }, + { url = "https://files.pythonhosted.org/packages/60/f3/690dd38e2310b6f68858a331399b4d6dbb9132c3e8ef8b4333b96caf403d/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd", size = 587356, upload-time = "2025-08-27T12:14:10.034Z" }, + { url = "https://files.pythonhosted.org/packages/86/e3/84507781cccd0145f35b1dc32c72675200c5ce8d5b30f813e49424ef68fc/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7", size = 555300, upload-time = "2025-08-27T12:14:11.783Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ee/375469849e6b429b3516206b4580a79e9ef3eb12920ddbd4492b56eaacbe/rpds_py-0.27.1-cp313-cp313t-win32.whl", hash = "sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688", size = 216714, upload-time = "2025-08-27T12:14:13.629Z" }, + { url = "https://files.pythonhosted.org/packages/21/87/3fc94e47c9bd0742660e84706c311a860dcae4374cf4a03c477e23ce605a/rpds_py-0.27.1-cp313-cp313t-win_amd64.whl", hash = "sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797", size = 228943, upload-time = "2025-08-27T12:14:14.937Z" }, + { url = "https://files.pythonhosted.org/packages/70/36/b6e6066520a07cf029d385de869729a895917b411e777ab1cde878100a1d/rpds_py-0.27.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334", size = 362472, upload-time = "2025-08-27T12:14:16.333Z" }, + { url = "https://files.pythonhosted.org/packages/af/07/b4646032e0dcec0df9c73a3bd52f63bc6c5f9cda992f06bd0e73fe3fbebd/rpds_py-0.27.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33", size = 345676, upload-time = "2025-08-27T12:14:17.764Z" }, + { url = "https://files.pythonhosted.org/packages/b0/16/2f1003ee5d0af4bcb13c0cf894957984c32a6751ed7206db2aee7379a55e/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a", size = 385313, upload-time = "2025-08-27T12:14:19.829Z" }, + { url = "https://files.pythonhosted.org/packages/05/cd/7eb6dd7b232e7f2654d03fa07f1414d7dfc980e82ba71e40a7c46fd95484/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b", size = 399080, upload-time = "2025-08-27T12:14:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/20/51/5829afd5000ec1cb60f304711f02572d619040aa3ec033d8226817d1e571/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7", size = 523868, upload-time = "2025-08-27T12:14:23.485Z" }, + { url = "https://files.pythonhosted.org/packages/05/2c/30eebca20d5db95720ab4d2faec1b5e4c1025c473f703738c371241476a2/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136", size = 408750, upload-time = "2025-08-27T12:14:24.924Z" }, + { url = "https://files.pythonhosted.org/packages/90/1a/cdb5083f043597c4d4276eae4e4c70c55ab5accec078da8611f24575a367/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff", size = 387688, upload-time = "2025-08-27T12:14:27.537Z" }, + { url = "https://files.pythonhosted.org/packages/7c/92/cf786a15320e173f945d205ab31585cc43969743bb1a48b6888f7a2b0a2d/rpds_py-0.27.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9", size = 407225, upload-time = "2025-08-27T12:14:28.981Z" }, + { url = "https://files.pythonhosted.org/packages/33/5c/85ee16df5b65063ef26017bef33096557a4c83fbe56218ac7cd8c235f16d/rpds_py-0.27.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60", size = 423361, upload-time = "2025-08-27T12:14:30.469Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8e/1c2741307fcabd1a334ecf008e92c4f47bb6f848712cf15c923becfe82bb/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e", size = 562493, upload-time = "2025-08-27T12:14:31.987Z" }, + { url = "https://files.pythonhosted.org/packages/04/03/5159321baae9b2222442a70c1f988cbbd66b9be0675dd3936461269be360/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212", size = 592623, upload-time = "2025-08-27T12:14:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/ff/39/c09fd1ad28b85bc1d4554a8710233c9f4cefd03d7717a1b8fbfd171d1167/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675", size = 558800, upload-time = "2025-08-27T12:14:35.436Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d6/99228e6bbcf4baa764b18258f519a9035131d91b538d4e0e294313462a98/rpds_py-0.27.1-cp314-cp314-win32.whl", hash = "sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3", size = 221943, upload-time = "2025-08-27T12:14:36.898Z" }, + { url = "https://files.pythonhosted.org/packages/be/07/c802bc6b8e95be83b79bdf23d1aa61d68324cb1006e245d6c58e959e314d/rpds_py-0.27.1-cp314-cp314-win_amd64.whl", hash = "sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456", size = 233739, upload-time = "2025-08-27T12:14:38.386Z" }, + { url = "https://files.pythonhosted.org/packages/c8/89/3e1b1c16d4c2d547c5717377a8df99aee8099ff050f87c45cb4d5fa70891/rpds_py-0.27.1-cp314-cp314-win_arm64.whl", hash = "sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3", size = 223120, upload-time = "2025-08-27T12:14:39.82Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/dc7931dc2fa4a6e46b2a4fa744a9fe5c548efd70e0ba74f40b39fa4a8c10/rpds_py-0.27.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2", size = 358944, upload-time = "2025-08-27T12:14:41.199Z" }, + { url = "https://files.pythonhosted.org/packages/e6/22/4af76ac4e9f336bfb1a5f240d18a33c6b2fcaadb7472ac7680576512b49a/rpds_py-0.27.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4", size = 342283, upload-time = "2025-08-27T12:14:42.699Z" }, + { url = "https://files.pythonhosted.org/packages/1c/15/2a7c619b3c2272ea9feb9ade67a45c40b3eeb500d503ad4c28c395dc51b4/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e", size = 380320, upload-time = "2025-08-27T12:14:44.157Z" }, + { url = "https://files.pythonhosted.org/packages/a2/7d/4c6d243ba4a3057e994bb5bedd01b5c963c12fe38dde707a52acdb3849e7/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817", size = 391760, upload-time = "2025-08-27T12:14:45.845Z" }, + { url = "https://files.pythonhosted.org/packages/b4/71/b19401a909b83bcd67f90221330bc1ef11bc486fe4e04c24388d28a618ae/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec", size = 522476, upload-time = "2025-08-27T12:14:47.364Z" }, + { url = "https://files.pythonhosted.org/packages/e4/44/1a3b9715c0455d2e2f0f6df5ee6d6f5afdc423d0773a8a682ed2b43c566c/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a", size = 403418, upload-time = "2025-08-27T12:14:49.991Z" }, + { url = "https://files.pythonhosted.org/packages/1c/4b/fb6c4f14984eb56673bc868a66536f53417ddb13ed44b391998100a06a96/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8", size = 384771, upload-time = "2025-08-27T12:14:52.159Z" }, + { url = "https://files.pythonhosted.org/packages/c0/56/d5265d2d28b7420d7b4d4d85cad8ef891760f5135102e60d5c970b976e41/rpds_py-0.27.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48", size = 400022, upload-time = "2025-08-27T12:14:53.859Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e9/9f5fc70164a569bdd6ed9046486c3568d6926e3a49bdefeeccfb18655875/rpds_py-0.27.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb", size = 416787, upload-time = "2025-08-27T12:14:55.673Z" }, + { url = "https://files.pythonhosted.org/packages/d4/64/56dd03430ba491db943a81dcdef115a985aac5f44f565cd39a00c766d45c/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734", size = 557538, upload-time = "2025-08-27T12:14:57.245Z" }, + { url = "https://files.pythonhosted.org/packages/3f/36/92cc885a3129993b1d963a2a42ecf64e6a8e129d2c7cc980dbeba84e55fb/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb", size = 588512, upload-time = "2025-08-27T12:14:58.728Z" }, + { url = "https://files.pythonhosted.org/packages/dd/10/6b283707780a81919f71625351182b4f98932ac89a09023cb61865136244/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0", size = 555813, upload-time = "2025-08-27T12:15:00.334Z" }, + { url = "https://files.pythonhosted.org/packages/04/2e/30b5ea18c01379da6272a92825dd7e53dc9d15c88a19e97932d35d430ef7/rpds_py-0.27.1-cp314-cp314t-win32.whl", hash = "sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a", size = 217385, upload-time = "2025-08-27T12:15:01.937Z" }, + { url = "https://files.pythonhosted.org/packages/32/7d/97119da51cb1dd3f2f3c0805f155a3aa4a95fa44fe7d78ae15e69edf4f34/rpds_py-0.27.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772", size = 230097, upload-time = "2025-08-27T12:15:03.961Z" }, + { url = "https://files.pythonhosted.org/packages/7f/6c/252e83e1ce7583c81f26d1d884b2074d40a13977e1b6c9c50bbf9a7f1f5a/rpds_py-0.27.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c918c65ec2e42c2a78d19f18c553d77319119bf43aa9e2edf7fb78d624355527", size = 372140, upload-time = "2025-08-27T12:15:05.441Z" }, + { url = "https://files.pythonhosted.org/packages/9d/71/949c195d927c5aeb0d0629d329a20de43a64c423a6aa53836290609ef7ec/rpds_py-0.27.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1fea2b1a922c47c51fd07d656324531adc787e415c8b116530a1d29c0516c62d", size = 354086, upload-time = "2025-08-27T12:15:07.404Z" }, + { url = "https://files.pythonhosted.org/packages/9f/02/e43e332ad8ce4f6c4342d151a471a7f2900ed1d76901da62eb3762663a71/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbf94c58e8e0cd6b6f38d8de67acae41b3a515c26169366ab58bdca4a6883bb8", size = 382117, upload-time = "2025-08-27T12:15:09.275Z" }, + { url = "https://files.pythonhosted.org/packages/d0/05/b0fdeb5b577197ad72812bbdfb72f9a08fa1e64539cc3940b1b781cd3596/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c2a8fed130ce946d5c585eddc7c8eeef0051f58ac80a8ee43bd17835c144c2cc", size = 394520, upload-time = "2025-08-27T12:15:10.727Z" }, + { url = "https://files.pythonhosted.org/packages/67/1f/4cfef98b2349a7585181e99294fa2a13f0af06902048a5d70f431a66d0b9/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:037a2361db72ee98d829bc2c5b7cc55598ae0a5e0ec1823a56ea99374cfd73c1", size = 522657, upload-time = "2025-08-27T12:15:12.613Z" }, + { url = "https://files.pythonhosted.org/packages/44/55/ccf37ddc4c6dce7437b335088b5ca18da864b334890e2fe9aa6ddc3f79a9/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5281ed1cc1d49882f9997981c88df1a22e140ab41df19071222f7e5fc4e72125", size = 402967, upload-time = "2025-08-27T12:15:14.113Z" }, + { url = "https://files.pythonhosted.org/packages/74/e5/5903f92e41e293b07707d5bf00ef39a0eb2af7190aff4beaf581a6591510/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fd50659a069c15eef8aa3d64bbef0d69fd27bb4a50c9ab4f17f83a16cbf8905", size = 384372, upload-time = "2025-08-27T12:15:15.842Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e3/fbb409e18aeefc01e49f5922ac63d2d914328430e295c12183ce56ebf76b/rpds_py-0.27.1-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:c4b676c4ae3921649a15d28ed10025548e9b561ded473aa413af749503c6737e", size = 401264, upload-time = "2025-08-27T12:15:17.388Z" }, + { url = "https://files.pythonhosted.org/packages/55/79/529ad07794e05cb0f38e2f965fc5bb20853d523976719400acecc447ec9d/rpds_py-0.27.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:079bc583a26db831a985c5257797b2b5d3affb0386e7ff886256762f82113b5e", size = 418691, upload-time = "2025-08-27T12:15:19.144Z" }, + { url = "https://files.pythonhosted.org/packages/33/39/6554a7fd6d9906fda2521c6d52f5d723dca123529fb719a5b5e074c15e01/rpds_py-0.27.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4e44099bd522cba71a2c6b97f68e19f40e7d85399de899d66cdb67b32d7cb786", size = 558989, upload-time = "2025-08-27T12:15:21.087Z" }, + { url = "https://files.pythonhosted.org/packages/19/b2/76fa15173b6f9f445e5ef15120871b945fb8dd9044b6b8c7abe87e938416/rpds_py-0.27.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e202e6d4188e53c6661af813b46c37ca2c45e497fc558bacc1a7630ec2695aec", size = 589835, upload-time = "2025-08-27T12:15:22.696Z" }, + { url = "https://files.pythonhosted.org/packages/ee/9e/5560a4b39bab780405bed8a88ee85b30178061d189558a86003548dea045/rpds_py-0.27.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f41f814b8eaa48768d1bb551591f6ba45f87ac76899453e8ccd41dba1289b04b", size = 555227, upload-time = "2025-08-27T12:15:24.278Z" }, + { url = "https://files.pythonhosted.org/packages/52/d7/cd9c36215111aa65724c132bf709c6f35175973e90b32115dedc4ced09cb/rpds_py-0.27.1-cp39-cp39-win32.whl", hash = "sha256:9e71f5a087ead99563c11fdaceee83ee982fd39cf67601f4fd66cb386336ee52", size = 217899, upload-time = "2025-08-27T12:15:25.926Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e0/d75ab7b4dd8ba777f6b365adbdfc7614bbfe7c5f05703031dfa4b61c3d6c/rpds_py-0.27.1-cp39-cp39-win_amd64.whl", hash = "sha256:71108900c9c3c8590697244b9519017a400d9ba26a36c48381b3f64743a44aab", size = 228725, upload-time = "2025-08-27T12:15:27.398Z" }, + { url = "https://files.pythonhosted.org/packages/d5/63/b7cc415c345625d5e62f694ea356c58fb964861409008118f1245f8c3347/rpds_py-0.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7ba22cb9693df986033b91ae1d7a979bc399237d45fccf875b76f62bb9e52ddf", size = 371360, upload-time = "2025-08-27T12:15:29.218Z" }, + { url = "https://files.pythonhosted.org/packages/e5/8c/12e1b24b560cf378b8ffbdb9dc73abd529e1adcfcf82727dfd29c4a7b88d/rpds_py-0.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b640501be9288c77738b5492b3fd3abc4ba95c50c2e41273c8a1459f08298d3", size = 353933, upload-time = "2025-08-27T12:15:30.837Z" }, + { url = "https://files.pythonhosted.org/packages/9b/85/1bb2210c1f7a1b99e91fea486b9f0f894aa5da3a5ec7097cbad7dec6d40f/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb08b65b93e0c6dd70aac7f7890a9c0938d5ec71d5cb32d45cf844fb8ae47636", size = 382962, upload-time = "2025-08-27T12:15:32.348Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c9/a839b9f219cf80ed65f27a7f5ddbb2809c1b85c966020ae2dff490e0b18e/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d7ff07d696a7a38152ebdb8212ca9e5baab56656749f3d6004b34ab726b550b8", size = 394412, upload-time = "2025-08-27T12:15:33.839Z" }, + { url = "https://files.pythonhosted.org/packages/02/2d/b1d7f928b0b1f4fc2e0133e8051d199b01d7384875adc63b6ddadf3de7e5/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb7c72262deae25366e3b6c0c0ba46007967aea15d1eea746e44ddba8ec58dcc", size = 523972, upload-time = "2025-08-27T12:15:35.377Z" }, + { url = "https://files.pythonhosted.org/packages/a9/af/2cbf56edd2d07716df1aec8a726b3159deb47cb5c27e1e42b71d705a7c2f/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b002cab05d6339716b03a4a3a2ce26737f6231d7b523f339fa061d53368c9d8", size = 403273, upload-time = "2025-08-27T12:15:37.051Z" }, + { url = "https://files.pythonhosted.org/packages/c0/93/425e32200158d44ff01da5d9612c3b6711fe69f606f06e3895511f17473b/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23f6b69d1c26c4704fec01311963a41d7de3ee0570a84ebde4d544e5a1859ffc", size = 385278, upload-time = "2025-08-27T12:15:38.571Z" }, + { url = "https://files.pythonhosted.org/packages/eb/1a/1a04a915ecd0551bfa9e77b7672d1937b4b72a0fc204a17deef76001cfb2/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:530064db9146b247351f2a0250b8f00b289accea4596a033e94be2389977de71", size = 402084, upload-time = "2025-08-27T12:15:40.529Z" }, + { url = "https://files.pythonhosted.org/packages/51/f7/66585c0fe5714368b62951d2513b684e5215beaceab2c6629549ddb15036/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b90b0496570bd6b0321724a330d8b545827c4df2034b6ddfc5f5275f55da2ad", size = 419041, upload-time = "2025-08-27T12:15:42.191Z" }, + { url = "https://files.pythonhosted.org/packages/8e/7e/83a508f6b8e219bba2d4af077c35ba0e0cdd35a751a3be6a7cba5a55ad71/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:879b0e14a2da6a1102a3fc8af580fc1ead37e6d6692a781bd8c83da37429b5ab", size = 560084, upload-time = "2025-08-27T12:15:43.839Z" }, + { url = "https://files.pythonhosted.org/packages/66/66/bb945683b958a1b19eb0fe715594630d0f36396ebdef4d9b89c2fa09aa56/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:0d807710df3b5faa66c731afa162ea29717ab3be17bdc15f90f2d9f183da4059", size = 590115, upload-time = "2025-08-27T12:15:46.647Z" }, + { url = "https://files.pythonhosted.org/packages/12/00/ccfaafaf7db7e7adace915e5c2f2c2410e16402561801e9c7f96683002d3/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:3adc388fc3afb6540aec081fa59e6e0d3908722771aa1e37ffe22b220a436f0b", size = 556561, upload-time = "2025-08-27T12:15:48.219Z" }, + { url = "https://files.pythonhosted.org/packages/e1/b7/92b6ed9aad103bfe1c45df98453dfae40969eef2cb6c6239c58d7e96f1b3/rpds_py-0.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c796c0c1cc68cb08b0284db4229f5af76168172670c74908fdbd4b7d7f515819", size = 229125, upload-time = "2025-08-27T12:15:49.956Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ed/e1fba02de17f4f76318b834425257c8ea297e415e12c68b4361f63e8ae92/rpds_py-0.27.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdfe4bb2f9fe7458b7453ad3c33e726d6d1c7c0a72960bcc23800d77384e42df", size = 371402, upload-time = "2025-08-27T12:15:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/af/7c/e16b959b316048b55585a697e94add55a4ae0d984434d279ea83442e460d/rpds_py-0.27.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8fabb8fd848a5f75a2324e4a84501ee3a5e3c78d8603f83475441866e60b94a3", size = 354084, upload-time = "2025-08-27T12:15:53.219Z" }, + { url = "https://files.pythonhosted.org/packages/de/c1/ade645f55de76799fdd08682d51ae6724cb46f318573f18be49b1e040428/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eda8719d598f2f7f3e0f885cba8646644b55a187762bec091fa14a2b819746a9", size = 383090, upload-time = "2025-08-27T12:15:55.158Z" }, + { url = "https://files.pythonhosted.org/packages/1f/27/89070ca9b856e52960da1472efcb6c20ba27cfe902f4f23ed095b9cfc61d/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c64d07e95606ec402a0a1c511fe003873fa6af630bda59bac77fac8b4318ebc", size = 394519, upload-time = "2025-08-27T12:15:57.238Z" }, + { url = "https://files.pythonhosted.org/packages/b3/28/be120586874ef906aa5aeeae95ae8df4184bc757e5b6bd1c729ccff45ed5/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93a2ed40de81bcff59aabebb626562d48332f3d028ca2036f1d23cbb52750be4", size = 523817, upload-time = "2025-08-27T12:15:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/70cc197bc11cfcde02a86f36ac1eed15c56667c2ebddbdb76a47e90306da/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:387ce8c44ae94e0ec50532d9cb0edce17311024c9794eb196b90e1058aadeb66", size = 403240, upload-time = "2025-08-27T12:16:00.923Z" }, + { url = "https://files.pythonhosted.org/packages/cf/35/46936cca449f7f518f2f4996e0e8344db4b57e2081e752441154089d2a5f/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaf94f812c95b5e60ebaf8bfb1898a7d7cb9c1af5744d4a67fa47796e0465d4e", size = 385194, upload-time = "2025-08-27T12:16:02.802Z" }, + { url = "https://files.pythonhosted.org/packages/e1/62/29c0d3e5125c3270b51415af7cbff1ec587379c84f55a5761cc9efa8cd06/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:4848ca84d6ded9b58e474dfdbad4b8bfb450344c0551ddc8d958bf4b36aa837c", size = 402086, upload-time = "2025-08-27T12:16:04.806Z" }, + { url = "https://files.pythonhosted.org/packages/8f/66/03e1087679227785474466fdd04157fb793b3b76e3fcf01cbf4c693c1949/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2bde09cbcf2248b73c7c323be49b280180ff39fadcfe04e7b6f54a678d02a7cf", size = 419272, upload-time = "2025-08-27T12:16:06.471Z" }, + { url = "https://files.pythonhosted.org/packages/6a/24/e3e72d265121e00b063aef3e3501e5b2473cf1b23511d56e529531acf01e/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:94c44ee01fd21c9058f124d2d4f0c9dc7634bec93cd4b38eefc385dabe71acbf", size = 560003, upload-time = "2025-08-27T12:16:08.06Z" }, + { url = "https://files.pythonhosted.org/packages/26/ca/f5a344c534214cc2d41118c0699fffbdc2c1bc7046f2a2b9609765ab9c92/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:df8b74962e35c9249425d90144e721eed198e6555a0e22a563d29fe4486b51f6", size = 590482, upload-time = "2025-08-27T12:16:10.137Z" }, + { url = "https://files.pythonhosted.org/packages/ce/08/4349bdd5c64d9d193c360aa9db89adeee6f6682ab8825dca0a3f535f434f/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:dc23e6820e3b40847e2f4a7726462ba0cf53089512abe9ee16318c366494c17a", size = 556523, upload-time = "2025-08-27T12:16:12.188Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ea/5463cd5048a7a2fcdae308b6e96432802132c141bfb9420260142632a0f1/rpds_py-0.27.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:aa8933159edc50be265ed22b401125c9eebff3171f570258854dbce3ecd55475", size = 371778, upload-time = "2025-08-27T12:16:13.851Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c8/f38c099db07f5114029c1467649d308543906933eebbc226d4527a5f4693/rpds_py-0.27.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a50431bf02583e21bf273c71b89d710e7a710ad5e39c725b14e685610555926f", size = 354394, upload-time = "2025-08-27T12:16:15.609Z" }, + { url = "https://files.pythonhosted.org/packages/7d/79/b76f97704d9dd8ddbd76fed4c4048153a847c5d6003afe20a6b5c3339065/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78af06ddc7fe5cc0e967085a9115accee665fb912c22a3f54bad70cc65b05fe6", size = 382348, upload-time = "2025-08-27T12:16:17.251Z" }, + { url = "https://files.pythonhosted.org/packages/8a/3f/ef23d3c1be1b837b648a3016d5bbe7cfe711422ad110b4081c0a90ef5a53/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:70d0738ef8fee13c003b100c2fbd667ec4f133468109b3472d249231108283a3", size = 394159, upload-time = "2025-08-27T12:16:19.251Z" }, + { url = "https://files.pythonhosted.org/packages/74/8a/9e62693af1a34fd28b1a190d463d12407bd7cf561748cb4745845d9548d3/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2f6fd8a1cea5bbe599b6e78a6e5ee08db434fc8ffea51ff201c8765679698b3", size = 522775, upload-time = "2025-08-27T12:16:20.929Z" }, + { url = "https://files.pythonhosted.org/packages/36/0d/8d5bb122bf7a60976b54c5c99a739a3819f49f02d69df3ea2ca2aff47d5c/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8177002868d1426305bb5de1e138161c2ec9eb2d939be38291d7c431c4712df8", size = 402633, upload-time = "2025-08-27T12:16:22.548Z" }, + { url = "https://files.pythonhosted.org/packages/0f/0e/237948c1f425e23e0cf5a566d702652a6e55c6f8fbd332a1792eb7043daf/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:008b839781d6c9bf3b6a8984d1d8e56f0ec46dc56df61fd669c49b58ae800400", size = 384867, upload-time = "2025-08-27T12:16:24.29Z" }, + { url = "https://files.pythonhosted.org/packages/d6/0a/da0813efcd998d260cbe876d97f55b0f469ada8ba9cbc47490a132554540/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:a55b9132bb1ade6c734ddd2759c8dc132aa63687d259e725221f106b83a0e485", size = 401791, upload-time = "2025-08-27T12:16:25.954Z" }, + { url = "https://files.pythonhosted.org/packages/51/78/c6c9e8a8aaca416a6f0d1b6b4a6ee35b88fe2c5401d02235d0a056eceed2/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a46fdec0083a26415f11d5f236b79fa1291c32aaa4a17684d82f7017a1f818b1", size = 419525, upload-time = "2025-08-27T12:16:27.659Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/5af37e1d71487cf6d56dd1420dc7e0c2732c1b6ff612aa7a88374061c0a8/rpds_py-0.27.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:8a63b640a7845f2bdd232eb0d0a4a2dd939bcdd6c57e6bb134526487f3160ec5", size = 559255, upload-time = "2025-08-27T12:16:29.343Z" }, + { url = "https://files.pythonhosted.org/packages/40/7f/8b7b136069ef7ac3960eda25d832639bdb163018a34c960ed042dd1707c8/rpds_py-0.27.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:7e32721e5d4922deaaf963469d795d5bde6093207c52fec719bd22e5d1bedbc4", size = 590384, upload-time = "2025-08-27T12:16:31.005Z" }, + { url = "https://files.pythonhosted.org/packages/d8/06/c316d3f6ff03f43ccb0eba7de61376f8ec4ea850067dddfafe98274ae13c/rpds_py-0.27.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:2c426b99a068601b5f4623573df7a7c3d72e87533a2dd2253353a03e7502566c", size = 555959, upload-time = "2025-08-27T12:16:32.73Z" }, + { url = "https://files.pythonhosted.org/packages/60/94/384cf54c430b9dac742bbd2ec26c23feb78ded0d43d6d78563a281aec017/rpds_py-0.27.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4fc9b7fe29478824361ead6e14e4f5aed570d477e06088826537e202d25fe859", size = 228784, upload-time = "2025-08-27T12:16:34.428Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/58/6ca66896635352812de66f71cdf9ff86b3a4f79071ca5730088c0cd0fc8d/ruff-0.14.1.tar.gz", hash = "sha256:1dd86253060c4772867c61791588627320abcb6ed1577a90ef432ee319729b69", size = 5513429, upload-time = "2025-10-16T18:05:41.766Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/39/9cc5ab181478d7a18adc1c1e051a84ee02bec94eb9bdfd35643d7c74ca31/ruff-0.14.1-py3-none-linux_armv6l.whl", hash = "sha256:083bfc1f30f4a391ae09c6f4f99d83074416b471775b59288956f5bc18e82f8b", size = 12445415, upload-time = "2025-10-16T18:04:48.227Z" }, + { url = "https://files.pythonhosted.org/packages/ef/2e/1226961855ccd697255988f5a2474890ac7c5863b080b15bd038df820818/ruff-0.14.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f6fa757cd717f791009f7669fefb09121cc5f7d9bd0ef211371fad68c2b8b224", size = 12784267, upload-time = "2025-10-16T18:04:52.515Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ea/fd9e95863124ed159cd0667ec98449ae461de94acda7101f1acb6066da00/ruff-0.14.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d6191903d39ac156921398e9c86b7354d15e3c93772e7dbf26c9fcae59ceccd5", size = 11781872, upload-time = "2025-10-16T18:04:55.396Z" }, + { url = "https://files.pythonhosted.org/packages/1e/5a/e890f7338ff537dba4589a5e02c51baa63020acfb7c8cbbaea4831562c96/ruff-0.14.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed04f0e04f7a4587244e5c9d7df50e6b5bf2705d75059f409a6421c593a35896", size = 12226558, upload-time = "2025-10-16T18:04:58.166Z" }, + { url = "https://files.pythonhosted.org/packages/a6/7a/8ab5c3377f5bf31e167b73651841217542bcc7aa1c19e83030835cc25204/ruff-0.14.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5c9e6cf6cd4acae0febbce29497accd3632fe2025c0c583c8b87e8dbdeae5f61", size = 12187898, upload-time = "2025-10-16T18:05:01.455Z" }, + { url = "https://files.pythonhosted.org/packages/48/8d/ba7c33aa55406955fc124e62c8259791c3d42e3075a71710fdff9375134f/ruff-0.14.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6fa2458527794ecdfbe45f654e42c61f2503a230545a91af839653a0a93dbc6", size = 12939168, upload-time = "2025-10-16T18:05:04.397Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c2/70783f612b50f66d083380e68cbd1696739d88e9b4f6164230375532c637/ruff-0.14.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:39f1c392244e338b21d42ab29b8a6392a722c5090032eb49bb4d6defcdb34345", size = 14386942, upload-time = "2025-10-16T18:05:07.102Z" }, + { url = "https://files.pythonhosted.org/packages/48/44/cd7abb9c776b66d332119d67f96acf15830d120f5b884598a36d9d3f4d83/ruff-0.14.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7382fa12a26cce1f95070ce450946bec357727aaa428983036362579eadcc5cf", size = 13990622, upload-time = "2025-10-16T18:05:09.882Z" }, + { url = "https://files.pythonhosted.org/packages/eb/56/4259b696db12ac152fe472764b4f78bbdd9b477afd9bc3a6d53c01300b37/ruff-0.14.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd0bf2be3ae8521e1093a487c4aa3b455882f139787770698530d28ed3fbb37c", size = 13431143, upload-time = "2025-10-16T18:05:13.46Z" }, + { url = "https://files.pythonhosted.org/packages/e0/35/266a80d0eb97bd224b3265b9437bd89dde0dcf4faf299db1212e81824e7e/ruff-0.14.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cabcaa9ccf8089fb4fdb78d17cc0e28241520f50f4c2e88cb6261ed083d85151", size = 13132844, upload-time = "2025-10-16T18:05:16.1Z" }, + { url = "https://files.pythonhosted.org/packages/65/6e/d31ce218acc11a8d91ef208e002a31acf315061a85132f94f3df7a252b18/ruff-0.14.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:747d583400f6125ec11a4c14d1c8474bf75d8b419ad22a111a537ec1a952d192", size = 13401241, upload-time = "2025-10-16T18:05:19.395Z" }, + { url = "https://files.pythonhosted.org/packages/9f/b5/dbc4221bf0b03774b3b2f0d47f39e848d30664157c15b965a14d890637d2/ruff-0.14.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5a6e74c0efd78515a1d13acbfe6c90f0f5bd822aa56b4a6d43a9ffb2ae6e56cd", size = 12132476, upload-time = "2025-10-16T18:05:22.163Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/ac99194e790ccd092d6a8b5f341f34b6e597d698e3077c032c502d75ea84/ruff-0.14.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0ea6a864d2fb41a4b6d5b456ed164302a0d96f4daac630aeba829abfb059d020", size = 12139749, upload-time = "2025-10-16T18:05:25.162Z" }, + { url = "https://files.pythonhosted.org/packages/47/26/7df917462c3bb5004e6fdfcc505a49e90bcd8a34c54a051953118c00b53a/ruff-0.14.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0826b8764f94229604fa255918d1cc45e583e38c21c203248b0bfc9a0e930be5", size = 12544758, upload-time = "2025-10-16T18:05:28.018Z" }, + { url = "https://files.pythonhosted.org/packages/64/d0/81e7f0648e9764ad9b51dd4be5e5dac3fcfff9602428ccbae288a39c2c22/ruff-0.14.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cbc52160465913a1a3f424c81c62ac8096b6a491468e7d872cb9444a860bc33d", size = 13221811, upload-time = "2025-10-16T18:05:30.707Z" }, + { url = "https://files.pythonhosted.org/packages/c3/07/3c45562c67933cc35f6d5df4ca77dabbcd88fddaca0d6b8371693d29fd56/ruff-0.14.1-py3-none-win32.whl", hash = "sha256:e037ea374aaaff4103240ae79168c0945ae3d5ae8db190603de3b4012bd1def6", size = 12319467, upload-time = "2025-10-16T18:05:33.261Z" }, + { url = "https://files.pythonhosted.org/packages/02/88/0ee4ca507d4aa05f67e292d2e5eb0b3e358fbcfe527554a2eda9ac422d6b/ruff-0.14.1-py3-none-win_amd64.whl", hash = "sha256:59d599cdff9c7f925a017f6f2c256c908b094e55967f93f2821b1439928746a1", size = 13401123, upload-time = "2025-10-16T18:05:35.984Z" }, + { url = "https://files.pythonhosted.org/packages/b8/81/4b6387be7014858d924b843530e1b2a8e531846807516e9bea2ee0936bf7/ruff-0.14.1-py3-none-win_arm64.whl", hash = "sha256:e3b443c4c9f16ae850906b8d0a707b2a4c16f8d2f0a7fe65c475c5886665ce44", size = 12436636, upload-time = "2025-10-16T18:05:38.995Z" }, +] + +[[package]] +name = "setuptools" +version = "80.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + +[[package]] +name = "tomli" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, +] + +[[package]] +name = "uri-template" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/31/c7/0336f2bd0bcbada6ccef7aaa25e443c118a704f828a0620c6fa0207c1b64/uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7", size = 21678, upload-time = "2023-06-21T01:49:05.374Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl", hash = "sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363", size = 11140, upload-time = "2023-06-21T01:49:03.467Z" }, +] + +[[package]] +name = "webcolors" +version = "24.11.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/29/061ec845fb58521848f3739e466efd8250b4b7b98c1b6c5bf4d40b419b7e/webcolors-24.11.1.tar.gz", hash = "sha256:ecb3d768f32202af770477b8b65f318fa4f566c22948673a977b00d589dd80f6", size = 45064, upload-time = "2024-11-11T07:43:24.224Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/e8/c0e05e4684d13459f93d312077a9a2efbe04d59c393bc2b8802248c908d4/webcolors-24.11.1-py3-none-any.whl", hash = "sha256:515291393b4cdf0eb19c155749a096f779f7d909f7cceea072791cb9095b92e9", size = 14934, upload-time = "2024-11-11T07:43:22.529Z" }, +] |
