diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 6867d90c..3330014d 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.8.0 +current_version = 1.8.2 tag_name = {new_major}-{new_minor}-{new_patch} commit = True tag = True diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5b47a12a..0fa51118 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -40,7 +40,7 @@ windowsBuildDebug_2.0: script: - $env:path = "$env:GTLAB_DEV_TOOLS\ThirdPartyLibraries\Python\Python_39;$env:path" - $env:PATH+=(";.\build;"+$env:GTLAB_DEV_TOOLS+"\binDebug;"+$env:GTLAB_DEV_TOOLS+"\binDebug\modules;") - - cmake -B build -S . -G "Ninja" -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=install-msvc2019-dbg -DGTLAB_DEVTOOLS_DIR="$env:GTLAB_DEV_TOOLS" -DBUILD_UNITTESTS=ON + - cmake -B build -S . -G "Ninja" -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=install-msvc2019-dbg -DGTlabDevtools_ROOT="$env:GTLAB_DEV_TOOLS" -DBUILD_UNITTESTS=ON - cmake --build build --target install windowsBuildRelease_2.0: @@ -49,7 +49,7 @@ windowsBuildRelease_2.0: - .build-win_20 - .run-master-and-tags script: - - cmake -B build -S . -G "Ninja" -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=install-msvc2019 -DGTLAB_DEVTOOLS_DIR="$env:GTLAB_DEV_TOOLS" + - cmake -B build -S . -G "Ninja" -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=install-msvc2019 -DGTlabDevtools_ROOT="$env:GTLAB_DEV_TOOLS" - cmake --build build --target install linuxBuildDebug_2.0: @@ -57,7 +57,7 @@ linuxBuildDebug_2.0: extends: - .build-linux_20 script: - - cmake -B build -S . -G "Ninja" -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=install-linux-dbg -DGTLAB_DEVTOOLS_DIR=$GTLAB_DEV_TOOLS -DBUILD_UNITTESTS=ON + - cmake -B build -S . -G "Ninja" -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=install-linux-dbg -DGTlabDevtools_ROOT=$GTLAB_DEV_TOOLS -DBUILD_UNITTESTS=ON - cmake --build build --target install linuxBuildRelease_2.0: @@ -66,7 +66,7 @@ linuxBuildRelease_2.0: - .build-linux_20 - .run-master-and-tags script: - - cmake -B build -S . -G "Ninja" -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=install-linux -DGTLAB_DEVTOOLS_DIR=$GTLAB_DEV_TOOLS + - cmake -B build -S . -G "Ninja" -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=install-linux -DGTlabDevtools_ROOT=$GTLAB_DEV_TOOLS - cmake --build build --target install # run tests using the binary built before @@ -125,7 +125,7 @@ code-coverage: extends: - .build-linux_20 script: - - cmake -B build -S . -G "Ninja" -DCMAKE_BUILD_TYPE=Debug -DGTLAB_DEVTOOLS_DIR=$GTLAB_DEV_TOOLS -DBUILD_UNITTESTS=ON -DBUILD_WITH_COVERAGE=ON + - cmake -B build -S . -G "Ninja" -DCMAKE_BUILD_TYPE=Debug -DGTlabDevtools_ROOT=$GTLAB_DEV_TOOLS -DBUILD_UNITTESTS=ON -DBUILD_WITH_COVERAGE=ON - cd build - LD_LIBRARY_PATH="$GTLAB_DEV_TOOLS/binDebug" ninja test-coverage - mv test-coverage .. diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c076b6f..b5dfeb87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,21 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [1.8.2] - 2026-05-08 + +### Fixed + - Fixed deadlock when running python tasks from GTlabConsole when DLRp2 has been loaded as well. This was a regression introduced in 1.8.1, + that allows the python module be loaded also from a native python interpreter. The code has been changed to avoid deadlocking. + +## [1.8.1] - 2026-03-12 ### Fixed - footprint() now correctly distinguishes between the application and project footprint. By default, it returns the application footprint. When called with only_active=True, it returns only the modules that are part of the data model of the current project. - #630 - Fixed a crash when creating a calculator with missing author information in a Python Task - #627 + - Fixed an issue where empty lines were printed to the Python console even when the evaluated script did not contain any print statements. - #622 + - Fixed accessing tasks from other task groups. This can be done via `findGtTask("groupname/taskname")` or `runProcess("groupname/taskname")`. - #639 ## [1.8.0] - 2025-07-11 diff --git a/CMakeLists.txt b/CMakeLists.txt index 44c175ee..4d5da8cf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,19 +14,33 @@ project(GTlab-Python VERSION 1.5.0) set(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake) -find_package(Qt5 REQUIRED COMPONENTS Widgets Core Gui Xml Svg) find_package(Python3 REQUIRED COMPONENTS Development) set(Python3_VERSION_SUFFIX "${Python3_VERSION_MAJOR}${Python3_VERSION_MINOR}") -include(GTlab) -gtlab_standard_setup() -enable_gtlab_devtools() +# Try to find DevTools (optionally) +if (GTLAB_DEVTOOLS_DIR AND NOT GTlabDevtools_ROOT) + message(WARNING "GTLAB_DEVTOOLS_DIR is deprecated. Use GTlabDevtools_ROOT instead.") + set(GTlabDevtools_ROOT ${GTLAB_DEVTOOLS_DIR}) +endif() + +# set via GTlabDevtools_ROOT= +find_package(GTlabDevtools QUIET) # optional if(NOT TARGET GTlab::Core) find_package(GTlab REQUIRED) endif() +include(${GTlab_DIR}/GTlab.cmake) +gtlab_standard_setup() + +require_qt(COMPONENTS Widgets Core Gui Xml Svg) + +# include SvgWidget, if Qt major version is 6 or higher +if (QT_VERSION_MAJOR GREATER_EQUAL 6) + require_qt(COMPONENTS SvgWidgets) +endif() + if (GTlab_VERSION_MAJOR LESS 2) if (NOT TARGET GTlab::Numerics) find_package(GTlabNumerics REQUIRED) diff --git a/LICENSES/MPL-2.0.txt b/LICENSES/MPL-2.0.txt deleted file mode 100644 index ee6256cd..00000000 --- a/LICENSES/MPL-2.0.txt +++ /dev/null @@ -1,373 +0,0 @@ -Mozilla Public License Version 2.0 -================================== - -1. Definitions --------------- - -1.1. "Contributor" - means each individual or legal entity that creates, contributes to - the creation of, or owns Covered Software. - -1.2. "Contributor Version" - means the combination of the Contributions of others (if any) used - by a Contributor and that particular Contributor's Contribution. - -1.3. "Contribution" - means Covered Software of a particular Contributor. - -1.4. "Covered Software" - means Source Code Form to which the initial Contributor has attached - the notice in Exhibit A, the Executable Form of such Source Code - Form, and Modifications of such Source Code Form, in each case - including portions thereof. - -1.5. "Incompatible With Secondary Licenses" - means - - (a) that the initial Contributor has attached the notice described - in Exhibit B to the Covered Software; or - - (b) that the Covered Software was made available under the terms of - version 1.1 or earlier of the License, but not also under the - terms of a Secondary License. - -1.6. "Executable Form" - means any form of the work other than Source Code Form. - -1.7. "Larger Work" - means a work that combines Covered Software with other material, in - a separate file or files, that is not Covered Software. - -1.8. "License" - means this document. - -1.9. "Licensable" - means having the right to grant, to the maximum extent possible, - whether at the time of the initial grant or subsequently, any and - all of the rights conveyed by this License. - -1.10. "Modifications" - means any of the following: - - (a) any file in Source Code Form that results from an addition to, - deletion from, or modification of the contents of Covered - Software; or - - (b) any new file in Source Code Form that contains any Covered - Software. - -1.11. "Patent Claims" of a Contributor - means any patent claim(s), including without limitation, method, - process, and apparatus claims, in any patent Licensable by such - Contributor that would be infringed, but for the grant of the - License, by the making, using, selling, offering for sale, having - made, import, or transfer of either its Contributions or its - Contributor Version. - -1.12. "Secondary License" - means either the GNU General Public License, Version 2.0, the GNU - Lesser General Public License, Version 2.1, the GNU Affero General - Public License, Version 3.0, or any later versions of those - licenses. - -1.13. "Source Code Form" - means the form of the work preferred for making modifications. - -1.14. "You" (or "Your") - means an individual or a legal entity exercising rights under this - License. For legal entities, "You" includes any entity that - controls, is controlled by, or is under common control with You. For - purposes of this definition, "control" means (a) the power, direct - or indirect, to cause the direction or management of such entity, - whether by contract or otherwise, or (b) ownership of more than - fifty percent (50%) of the outstanding shares or beneficial - ownership of such entity. - -2. License Grants and Conditions --------------------------------- - -2.1. Grants - -Each Contributor hereby grants You a world-wide, royalty-free, -non-exclusive license: - -(a) under intellectual property rights (other than patent or trademark) - Licensable by such Contributor to use, reproduce, make available, - modify, display, perform, distribute, and otherwise exploit its - Contributions, either on an unmodified basis, with Modifications, or - as part of a Larger Work; and - -(b) under Patent Claims of such Contributor to make, use, sell, offer - for sale, have made, import, and otherwise transfer either its - Contributions or its Contributor Version. - -2.2. Effective Date - -The licenses granted in Section 2.1 with respect to any Contribution -become effective for each Contribution on the date the Contributor first -distributes such Contribution. - -2.3. Limitations on Grant Scope - -The licenses granted in this Section 2 are the only rights granted under -this License. No additional rights or licenses will be implied from the -distribution or licensing of Covered Software under this License. -Notwithstanding Section 2.1(b) above, no patent license is granted by a -Contributor: - -(a) for any code that a Contributor has removed from Covered Software; - or - -(b) for infringements caused by: (i) Your and any other third party's - modifications of Covered Software, or (ii) the combination of its - Contributions with other software (except as part of its Contributor - Version); or - -(c) under Patent Claims infringed by Covered Software in the absence of - its Contributions. - -This License does not grant any rights in the trademarks, service marks, -or logos of any Contributor (except as may be necessary to comply with -the notice requirements in Section 3.4). - -2.4. Subsequent Licenses - -No Contributor makes additional grants as a result of Your choice to -distribute the Covered Software under a subsequent version of this -License (see Section 10.2) or under the terms of a Secondary License (if -permitted under the terms of Section 3.3). - -2.5. Representation - -Each Contributor represents that the Contributor believes its -Contributions are its original creation(s) or it has sufficient rights -to grant the rights to its Contributions conveyed by this License. - -2.6. Fair Use - -This License is not intended to limit any rights You have under -applicable copyright doctrines of fair use, fair dealing, or other -equivalents. - -2.7. Conditions - -Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted -in Section 2.1. - -3. Responsibilities -------------------- - -3.1. Distribution of Source Form - -All distribution of Covered Software in Source Code Form, including any -Modifications that You create or to which You contribute, must be under -the terms of this License. You must inform recipients that the Source -Code Form of the Covered Software is governed by the terms of this -License, and how they can obtain a copy of this License. You may not -attempt to alter or restrict the recipients' rights in the Source Code -Form. - -3.2. Distribution of Executable Form - -If You distribute Covered Software in Executable Form then: - -(a) such Covered Software must also be made available in Source Code - Form, as described in Section 3.1, and You must inform recipients of - the Executable Form how they can obtain a copy of such Source Code - Form by reasonable means in a timely manner, at a charge no more - than the cost of distribution to the recipient; and - -(b) You may distribute such Executable Form under the terms of this - License, or sublicense it under different terms, provided that the - license for the Executable Form does not attempt to limit or alter - the recipients' rights in the Source Code Form under this License. - -3.3. Distribution of a Larger Work - -You may create and distribute a Larger Work under terms of Your choice, -provided that You also comply with the requirements of this License for -the Covered Software. If the Larger Work is a combination of Covered -Software with a work governed by one or more Secondary Licenses, and the -Covered Software is not Incompatible With Secondary Licenses, this -License permits You to additionally distribute such Covered Software -under the terms of such Secondary License(s), so that the recipient of -the Larger Work may, at their option, further distribute the Covered -Software under the terms of either this License or such Secondary -License(s). - -3.4. Notices - -You may not remove or alter the substance of any license notices -(including copyright notices, patent notices, disclaimers of warranty, -or limitations of liability) contained within the Source Code Form of -the Covered Software, except that You may alter any license notices to -the extent required to remedy known factual inaccuracies. - -3.5. Application of Additional Terms - -You may choose to offer, and to charge a fee for, warranty, support, -indemnity or liability obligations to one or more recipients of Covered -Software. However, You may do so only on Your own behalf, and not on -behalf of any Contributor. You must make it absolutely clear that any -such warranty, support, indemnity, or liability obligation is offered by -You alone, and You hereby agree to indemnify every Contributor for any -liability incurred by such Contributor as a result of warranty, support, -indemnity or liability terms You offer. You may include additional -disclaimers of warranty and limitations of liability specific to any -jurisdiction. - -4. Inability to Comply Due to Statute or Regulation ---------------------------------------------------- - -If it is impossible for You to comply with any of the terms of this -License with respect to some or all of the Covered Software due to -statute, judicial order, or regulation then You must: (a) comply with -the terms of this License to the maximum extent possible; and (b) -describe the limitations and the code they affect. Such description must -be placed in a text file included with all distributions of the Covered -Software under this License. Except to the extent prohibited by statute -or regulation, such description must be sufficiently detailed for a -recipient of ordinary skill to be able to understand it. - -5. Termination --------------- - -5.1. The rights granted under this License will terminate automatically -if You fail to comply with any of its terms. However, if You become -compliant, then the rights granted under this License from a particular -Contributor are reinstated (a) provisionally, unless and until such -Contributor explicitly and finally terminates Your grants, and (b) on an -ongoing basis, if such Contributor fails to notify You of the -non-compliance by some reasonable means prior to 60 days after You have -come back into compliance. Moreover, Your grants from a particular -Contributor are reinstated on an ongoing basis if such Contributor -notifies You of the non-compliance by some reasonable means, this is the -first time You have received notice of non-compliance with this License -from such Contributor, and You become compliant prior to 30 days after -Your receipt of the notice. - -5.2. If You initiate litigation against any entity by asserting a patent -infringement claim (excluding declaratory judgment actions, -counter-claims, and cross-claims) alleging that a Contributor Version -directly or indirectly infringes any patent, then the rights granted to -You by any and all Contributors for the Covered Software under Section -2.1 of this License shall terminate. - -5.3. In the event of termination under Sections 5.1 or 5.2 above, all -end user license agreements (excluding distributors and resellers) which -have been validly granted by You or Your distributors under this License -prior to termination shall survive termination. - -************************************************************************ -* * -* 6. Disclaimer of Warranty * -* ------------------------- * -* * -* Covered Software is provided under this License on an "as is" * -* basis, without warranty of any kind, either expressed, implied, or * -* statutory, including, without limitation, warranties that the * -* Covered Software is free of defects, merchantable, fit for a * -* particular purpose or non-infringing. The entire risk as to the * -* quality and performance of the Covered Software is with You. * -* Should any Covered Software prove defective in any respect, You * -* (not any Contributor) assume the cost of any necessary servicing, * -* repair, or correction. This disclaimer of warranty constitutes an * -* essential part of this License. No use of any Covered Software is * -* authorized under this License except under this disclaimer. * -* * -************************************************************************ - -************************************************************************ -* * -* 7. Limitation of Liability * -* -------------------------- * -* * -* Under no circumstances and under no legal theory, whether tort * -* (including negligence), contract, or otherwise, shall any * -* Contributor, or anyone who distributes Covered Software as * -* permitted above, be liable to You for any direct, indirect, * -* special, incidental, or consequential damages of any character * -* including, without limitation, damages for lost profits, loss of * -* goodwill, work stoppage, computer failure or malfunction, or any * -* and all other commercial damages or losses, even if such party * -* shall have been informed of the possibility of such damages. This * -* limitation of liability shall not apply to liability for death or * -* personal injury resulting from such party's negligence to the * -* extent applicable law prohibits such limitation. Some * -* jurisdictions do not allow the exclusion or limitation of * -* incidental or consequential damages, so this exclusion and * -* limitation may not apply to You. * -* * -************************************************************************ - -8. Litigation -------------- - -Any litigation relating to this License may be brought only in the -courts of a jurisdiction where the defendant maintains its principal -place of business and such litigation shall be governed by laws of that -jurisdiction, without reference to its conflict-of-law provisions. -Nothing in this Section shall prevent a party's ability to bring -cross-claims or counter-claims. - -9. Miscellaneous ----------------- - -This License represents the complete agreement concerning the subject -matter hereof. If any provision of this License is held to be -unenforceable, such provision shall be reformed only to the extent -necessary to make it enforceable. Any law or regulation which provides -that the language of a contract shall be construed against the drafter -shall not be used to construe this License against a Contributor. - -10. Versions of the License ---------------------------- - -10.1. New Versions - -Mozilla Foundation is the license steward. Except as provided in Section -10.3, no one other than the license steward has the right to modify or -publish new versions of this License. Each version will be given a -distinguishing version number. - -10.2. Effect of New Versions - -You may distribute the Covered Software under the terms of the version -of the License under which You originally received the Covered Software, -or under the terms of any subsequent version published by the license -steward. - -10.3. Modified Versions - -If you create software not governed by this License, and you want to -create a new license for such software, you may create and use a -modified version of this License if you rename the license and remove -any references to the name of the license steward (except to note that -such modified license differs from this License). - -10.4. Distributing Source Code Form that is Incompatible With Secondary -Licenses - -If You choose to distribute Source Code Form that is Incompatible With -Secondary Licenses under the terms of this version of the License, the -notice described in Exhibit B of this License must be attached. - -Exhibit A - Source Code Form License Notice -------------------------------------------- - - This Source Code Form is subject to the terms of the Mozilla Public - License, v. 2.0. If a copy of the MPL was not distributed with this - file, You can obtain one at https://mozilla.org/MPL/2.0/. - -If it is not possible or desirable to put the notice in a particular -file, then You may include the notice in a location (such as a LICENSE -file in a relevant directory) where a recipient would be likely to look -for such a notice. - -You may add additional accurate notices of copyright ownership. - -Exhibit B - "Incompatible With Secondary Licenses" Notice ---------------------------------------------------------- - - This Source Code Form is "Incompatible With Secondary Licenses", as - defined by the Mozilla Public License, v. 2.0. diff --git a/cmake/GTlab.cmake b/cmake/GTlab.cmake deleted file mode 100644 index 106fe435..00000000 --- a/cmake/GTlab.cmake +++ /dev/null @@ -1,169 +0,0 @@ -# SPDX-FileCopyrightText: 2023 German Aerospace Center (DLR) -# -# SPDX-License-Identifier: MPL-2.0+ - -# This function set some variables that are common -# to the gtlab build system. -# It makes sure, that libraries are put into the root build folder, -# sets the debug postfix "-d" and sets the "binDebug" folder in -# Debug mode -# -# usage: -# gtlab_standard_setup() -macro(gtlab_standard_setup) - message("Setting up GTlab standards configuration") - set(CMAKE_CXX_STANDARD 14) - set(CMAKE_CXX_STANDARD_REQUIRED ON) - - # store all dlls inside build directory - set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}) - set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE ${PROJECT_BINARY_DIR}) - set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG ${PROJECT_BINARY_DIR}) - set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}) - set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_RELEASE ${PROJECT_BINARY_DIR}) - set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_DEBUG ${PROJECT_BINARY_DIR}) - - - if(NOT DEFINED CMAKE_INSTALL_BINDIR) - set(CMAKE_INSTALL_BINDIR "$<$:binDebug>$<$>:bin>") - endif(NOT DEFINED CMAKE_INSTALL_BINDIR) - - if(NOT DEFINED CMAKE_INSTALL_MODULEDIR) - if (IS_DIRECTORY ${CMAKE_INSTALL_PREFIX}/build) - message(STATUS "Deploying into GTlab build dir") - set(CMAKE_INSTALL_MODULEDIR build/modules) - - else () - set(CMAKE_INSTALL_MODULEDIR ${CMAKE_INSTALL_BINDIR}/modules) - endif() - endif(NOT DEFINED CMAKE_INSTALL_MODULEDIR) - - if(NOT DEFINED CMAKE_INSTALL_LIBDIR) - set(CMAKE_INSTALL_LIBDIR "lib") - endif(NOT DEFINED CMAKE_INSTALL_LIBDIR) - - if(NOT DEFINED CMAKE_DEBUG_POSTFIX) - set(CMAKE_DEBUG_POSTFIX "-d") - endif() - - # Instruct CMake to run moc automatically when needed. - set(CMAKE_AUTOMOC ON) -endmacro() - -# This function adds the packages of GTlab's devtools -# to the cmake prefix path. It adds the GTLAB_DEVTOOLS_DIR -# cache variable, to set the path to GTlab's devtools -# -# Usage: -# enable_gtlab_devtools() -macro(enable_gtlab_devtools) - # prefix 3rdparty stuff from devtools - set(GTLAB_DEVTOOLS_DIR "" CACHE PATH "Path of gtlab devtools") - - if (EXISTS ${GTLAB_DEVTOOLS_DIR}) - message("GTlab DevTools enabled at " ${GTLAB_DEVTOOLS_DIR}) - - set (CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} ${GTLAB_DEVTOOLS_DIR}) - - set(bladegen_ROOT ${GTLAB_DEVTOOLS_DIR}/ThirdPartyLibraries/BladeGenInterface) - set(ceres_ROOT ${GTLAB_DEVTOOLS_DIR}/ThirdPartyLibraries/ceres) - set(GTest_ROOT ${GTLAB_DEVTOOLS_DIR}/ThirdPartyLibraries/GoogleTest) - set(hdf5_ROOT ${GTLAB_DEVTOOLS_DIR}/ThirdPartyLibraries/hdf5) - set(LibXml2_ROOT ${GTLAB_DEVTOOLS_DIR}/ThirdPartyLibraries/LibXML) - set(CMinpack_ROOT ${GTLAB_DEVTOOLS_DIR}/ThirdPartyLibraries/minpack) - set(nlopt_ROOT ${GTLAB_DEVTOOLS_DIR}/ThirdPartyLibraries/NLopt) - set(optymal_ROOT ${GTLAB_DEVTOOLS_DIR}/ThirdPartyLibraries/Optymal) - - set(Qwt_ROOT ${GTLAB_DEVTOOLS_DIR}/ThirdPartyLibraries/Qwt) - set(SplineLib_ROOT ${GTLAB_DEVTOOLS_DIR}/ThirdPartyLibraries/SplineLib) - - set(PythonQt_ROOT ${GTLAB_DEVTOOLS_DIR}/ThirdPartyLibraries/PythonQt/PythonQt_${Python3_VERSION_SUFFIX}) - - set(GTlabCompat_ROOT ${GTLAB_DEVTOOLS_DIR}/../../tools/CompatibilityLib) - - endif() - -endmacro() - -# Function to add a gtlab module -# -# This makes sure to add a module id and deploy to the correct -# folders. -# -# Arguments: -# MODULE_ID: String containing the module ID of the module -# SOURCES: Sources (and headers) of the module to be built -# -# Optional arguments: -# README_FILE: Location to the readme. This is used to copy the readme to the module's meta directory. -# CHANGELOG_FILE: Location to the changelog, which is also copied to the meta directory in the install step. -# EXAMPLES_DIR: Directory containing the example folders of the project for the automatic example presentation of GTlab -# Each example should be in a subdirectory of the following structure: -# EXAMPLE_DIR/ -# -> project/ -# -> CONTENT OF THE GTLAB PROJECT (e.g. performance.gtmod) -# -> index.json -# -> picture.png -# -# For more details see the documentation of gtlab about offering examples -# -# Usage: -# add_gtlab_module(mymodule -# SOURCES mod.cpp mod2.cpp mod.h -# MODULE_ID "my mod" -# README_FILE "${PROJECT_SOURCE_DIR}/README.md" -# CHANGELOG_FILE "${PROJECT_SOURCE_DIR}/CHANGELOG.md" -# EXAMPLES_DIR "${PROJECT_SOURCE_DIR}/examples/" -# ) -function(add_gtlab_module GTLAB_ADD_MODULE_TARGET) - cmake_parse_arguments( - PARSE_ARGV 1 GTLAB_ADD_MODULE "" "MODULE_ID;README_FILE;CHANGELOG_FILE;EXAMPLES_DIR" "SOURCES" - ) - - if (NOT DEFINED GTLAB_ADD_MODULE_MODULE_ID) - message(FATAL_ERROR "In add_gtlab_module: Missing argument MODULE_ID for target ${GTLAB_ADD_MODULE_TARGET}") - endif() - - add_library(${GTLAB_ADD_MODULE_TARGET} SHARED ${GTLAB_ADD_MODULE_SOURCES}) - - # add module id - target_compile_definitions(${GTLAB_ADD_MODULE_TARGET} - PRIVATE -DGT_MODULE_ID="${GTLAB_ADD_MODULE_MODULE_ID}") - - # set rpath for linux - if (UNIX) - set_target_properties(${GTLAB_ADD_MODULE_TARGET} - PROPERTIES INSTALL_RPATH "$ORIGIN:$ORIGIN/.." - INSTALL_RPATH_USE_LINK_PATH FALSE) - endif (UNIX) - - - if (UNIX) - install (TARGETS ${GTLAB_ADD_MODULE_TARGET} - LIBRARY DESTINATION - ${CMAKE_INSTALL_MODULEDIR} - ) - else(UNIX) - install (TARGETS ${GTLAB_ADD_MODULE_TARGET} - RUNTIME DESTINATION - ${CMAKE_INSTALL_MODULEDIR} - ) - endif(UNIX) - - # Copy of README and CHANGELOG to meta directory - if (DEFINED GTLAB_ADD_MODULE_README_FILE) - install(FILES ${GTLAB_ADD_MODULE_README_FILE} - DESTINATION ${CMAKE_INSTALL_MODULEDIR}/meta/${GTLAB_ADD_MODULE_MODULE_ID}) - endif() - - if (DEFINED GTLAB_ADD_MODULE_CHANGELOG_FILE) - install(FILES ${GTLAB_ADD_MODULE_CHANGELOG_FILE} - DESTINATION ${CMAKE_INSTALL_MODULEDIR}/meta/${GTLAB_ADD_MODULE_MODULE_ID}) - endif() - - if (DEFINED GTLAB_ADD_MODULE_EXAMPLES_DIR) - install(DIRECTORY ${GTLAB_ADD_MODULE_EXAMPLES_DIR} - DESTINATION examples/) - endif() - -endfunction() diff --git a/src/batch/CMakeLists.txt b/src/batch/CMakeLists.txt index 0773d2b9..7dc5af23 100644 --- a/src/batch/CMakeLists.txt +++ b/src/batch/CMakeLists.txt @@ -27,8 +27,8 @@ target_compile_definitions(GTlabPythonConsole PRIVATE target_link_libraries(GTlabPythonConsole PRIVATE - Qt5::Widgets - Qt5::Xml + Qt${QT_VERSION_MAJOR}::Widgets + Qt${QT_VERSION_MAJOR}::Xml GTlab::Python ) diff --git a/src/module/CMakeLists.txt b/src/module/CMakeLists.txt index c24f99ad..f24e4276 100644 --- a/src/module/CMakeLists.txt +++ b/src/module/CMakeLists.txt @@ -63,6 +63,7 @@ set(HEADERS utilities/gtpy_regexp.h utilities/gtpy_scriptrunnable.h utilities/gtpy_utils.h + utilities/gtpy_taskapi.h utilities/gtpy_tempdir.h utilities/gtpy_transfer.h utilities/pythonextensions/gtpy_createhelperfunction.h @@ -90,6 +91,7 @@ set(HEADERS wizards/gtpy_wizardgeometryitem.h wizards/python_task/gtpy_taskwizardpage.h wizards/script_calculator/gtpy_scriptcalculatorwizardpage.h + utilities/gtpy_moduleupgrader.h ) set(SOURCES @@ -135,6 +137,7 @@ set(SOURCES utilities/gtpy_regexp.cpp utilities/gtpy_scriptrunnable.cpp utilities/gtpy_utils.cpp + utilities/gtpy_taskapi.cpp utilities/gtpy_tempdir.cpp utilities/gtpy_transfer.cpp utilities/pythonextensions/gtpy_calculatorsmodule.cpp @@ -162,6 +165,7 @@ set(SOURCES wizards/gtpy_wizardgeometryitem.cpp wizards/python_task/gtpy_taskwizardpage.cpp wizards/script_calculator/gtpy_scriptcalculatorwizardpage.cpp + utilities/gtpy_moduleupgrader.cpp ) set(MODULE_ID "Python Module (Python ${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR})") @@ -211,16 +215,23 @@ target_include_directories(GTlabPython PUBLIC target_link_libraries(GTlabPython PRIVATE - Qt5::Widgets - Qt5::Gui - Qt5::Xml - Qt5::Svg + Qt${QT_VERSION_MAJOR}::Widgets + Qt${QT_VERSION_MAJOR}::Gui + Qt${QT_VERSION_MAJOR}::Xml + Qt${QT_VERSION_MAJOR}::Svg GTlab::Logging GTlab::Core PUBLIC PythonQt::PythonQt ) +# include SvgWidgets, if Qt major version is 6 or higher +if (QT_VERSION_MAJOR GREATER_EQUAL 6) + target_link_libraries(GTlabPython + PRIVATE Qt6::SvgWidgets + ) +endif() + if (GTlab_VERSION_MAJOR GREATER_EQUAL 2) target_link_libraries(GTlabPython PRIVATE GTlab::Gui) else() diff --git a/src/module/gt_python.cpp b/src/module/gt_python.cpp index 4b23cc72..0c763696 100644 --- a/src/module/gt_python.cpp +++ b/src/module/gt_python.cpp @@ -55,14 +55,16 @@ #include "gt_accessdataconnection.h" #include "gtpy_scriptcollectionsettings.h" +#include "gtpy_moduleupgrader.h" #include "gt_python.h" + #if GT_VERSION >= 0x010700 GtVersionNumber GtPythonModule::version() { - return GtVersionNumber(1, 8, 0); + return GtVersionNumber(1, 8, 2); } #else int @@ -327,6 +329,18 @@ QList GtPythonModule::sharedFunctions() const } #endif +QList +GtPythonModule::upgradeRoutines() const +{ + return { +#if GT_VERSION >= GT_VERSION_CHECK(2, 1, 0) + gt::VersionUpgradeRoutine {GtVersionNumber(2, 0, 0), + >py::module_upgrader::to_2_0_0::run} +#endif + }; +} + + namespace PythonExecution { @@ -350,6 +364,7 @@ parseScriptFile(QFile& file) return out.readAll(); } + int runPythonInterpreter(const QStringList& args) { diff --git a/src/module/gt_python.h b/src/module/gt_python.h index a538bcee..b41702e9 100644 --- a/src/module/gt_python.h +++ b/src/module/gt_python.h @@ -177,6 +177,9 @@ class GtPythonModule: public QObject, public GtModuleInterface, QList commandLineFunctions() const override; QList sharedFunctions() const override; + + /** We are providing routines to upgrade the datamodel */ + QList upgradeRoutines() const override; #endif private: diff --git a/src/module/gt_python.json b/src/module/gt_python.json index 93197dd4..63a2ba60 100644 --- a/src/module/gt_python.json +++ b/src/module/gt_python.json @@ -1,6 +1,6 @@ { "dependencies" : [ - { "name" : "Python Setup", "version" : "1.8.0" } + { "name" : "Python Setup", "version" : "1.8.2" } ], "allowSuppressionBy": ["Python Setup"] } diff --git a/src/module/models/gtpy_completermodel.cpp b/src/module/models/gtpy_completermodel.cpp index 44a1bc5d..3fc593aa 100644 --- a/src/module/models/gtpy_completermodel.cpp +++ b/src/module/models/gtpy_completermodel.cpp @@ -50,7 +50,7 @@ GtpyCompleterModel::data(const QModelIndex& index, int role) const } void -GtpyCompleterModel::setFound(const QMap& found) +GtpyCompleterModel::setFound(const QMultiMap& found) { m_found = found; diff --git a/src/module/models/gtpy_completermodel.h b/src/module/models/gtpy_completermodel.h index dafea75a..ccfd9741 100644 --- a/src/module/models/gtpy_completermodel.h +++ b/src/module/models/gtpy_completermodel.h @@ -48,7 +48,7 @@ class GtpyCompleterModel: public QAbstractListModel * @brief Sets the data to the model. * @param found Found completions. */ - void setFound(const QMap& found); + void setFound(const QMultiMap& found); /** * @brief Returns the completion for the given index. @@ -80,7 +80,7 @@ class GtpyCompleterModel: public QAbstractListModel private: /// Found Completions - QMap m_found; + QMultiMap m_found; }; #endif // GTPY_COMPLETERMODEL_H diff --git a/src/module/models/gtpy_taskstylemodel.cpp b/src/module/models/gtpy_taskstylemodel.cpp index 9fbd1ad9..9c0bbc07 100644 --- a/src/module/models/gtpy_taskstylemodel.cpp +++ b/src/module/models/gtpy_taskstylemodel.cpp @@ -152,7 +152,7 @@ GtpyTaskStyleModel::flags(const QModelIndex& index) const { if (!index.isValid()) { - return 0; + return {}; } // collect default flags diff --git a/src/module/processcomponents/gtpy_abstractscriptcomponent.cpp b/src/module/processcomponents/gtpy_abstractscriptcomponent.cpp index 7ab7005c..70b8fe0f 100644 --- a/src/module/processcomponents/gtpy_abstractscriptcomponent.cpp +++ b/src/module/processcomponents/gtpy_abstractscriptcomponent.cpp @@ -41,13 +41,14 @@ setStructContainerValue(GtPropertyStructContainer& con, const QString& argName, { auto entry = std::find_if(con.begin(), con.end(), [&argName](const GtPropertyStructInstance& entry){ +#if GT_VERSION >= GT_VERSION_CHECK(2, 1, 0) + return entry.ident() == argName; +#elif GT_VERSION >= GT_VERSION_CHECK(2, 0, 0) return entry.getMemberVal("name") == argName; +#endif }); - if (entry == con.end()) - { - return false; - } + if (entry == con.end()) return false; return entry->setMemberVal("value", value); } @@ -57,13 +58,15 @@ structContainerValue(const GtPropertyStructContainer& con, const QString& argNam { auto entry = std::find_if(con.begin(), con.end(), [&argName](const GtPropertyStructInstance& entry){ +#if GT_VERSION >= GT_VERSION_CHECK(2, 1, 0) + return entry.ident() == argName; +#elif GT_VERSION >= GT_VERSION_CHECK(2, 0, 0) return entry.getMemberVal("name") == argName; +#endif + }); - if (entry == con.end()) - { - return {}; - } + if (entry == con.end()) return {}; return entry->getMemberValToVariant("value"); } @@ -92,9 +95,11 @@ GtpyAbstractScriptComponent::GtpyAbstractScriptComponent() : m_pyThreadId{-1}, m_replaceTabBySpaces{"replaceTab", "Replace tab by spaces"}, m_tabSize{"tabSize", "Tab size"}, - m_script{"script", "Script"} -#if GT_VERSION >= GT_VERSION_CHECK(2, 0, 0) - , + m_script{"script", "Script"}, +#if GT_VERSION >= GT_VERSION_CHECK(2, 1, 0) + m_inputArgs{"input_args", GtPropertyStructContainer::Associative}, + m_outputArgs{"output_args", GtPropertyStructContainer::Associative} +#elif GT_VERSION >= GT_VERSION_CHECK(2, 0, 0) m_inputArgs{"input_args"}, m_outputArgs{"output_args"} #endif @@ -104,7 +109,9 @@ GtpyAbstractScriptComponent::GtpyAbstractScriptComponent() : auto createStructDef = [](const QString& typeName, gt::PropertyFactoryFunction f){ GtPropertyStructDefinition structDef(typeName); + #if GT_VERSION < GT_VERSION_CHECK(2, 1, 0) structDef.defineMember("name", gt::makeStringProperty()); + #endif // GT_VERSION < GT_VERSION_CHECK(2, 1, 0) structDef.defineMember("value", f); return structDef; }; diff --git a/src/module/processcomponents/gtpy_scriptcalculator.cpp b/src/module/processcomponents/gtpy_scriptcalculator.cpp index 70aa5fcc..3c588457 100644 --- a/src/module/processcomponents/gtpy_scriptcalculator.cpp +++ b/src/module/processcomponents/gtpy_scriptcalculator.cpp @@ -37,6 +37,8 @@ GtpyScriptCalculator::GtpyScriptCalculator() registerProperty(*pathProp); } + hideLabelProperty(true); + connect(this, SIGNAL(stateChanged(GtProcessComponent::STATE)), this, SLOT(onStateChanged(GtProcessComponent::STATE))); diff --git a/src/module/utilities/gtpy_codegen.cpp b/src/module/utilities/gtpy_codegen.cpp index 0f778b56..6e3791de 100644 --- a/src/module/utilities/gtpy_codegen.cpp +++ b/src/module/utilities/gtpy_codegen.cpp @@ -224,17 +224,17 @@ gtpy::codegen::pyIdentifier(const QString& str) // if the match is not at the start of the string and there is not // already an underscore before the match, prepend an underscore - if (startPos > 0 && ident.at(startPos - 1) != "_") + if (startPos > 0 && ident.at(startPos - 1) != '_') { - matchedStr.prepend("_"); + matchedStr.prepend('_'); } // if the match is not at the end of the string and it consists of // more than one letter, append an underscore if (!singleLetter && endPos < ident.length() && - ident.at(endPos) != "_") + ident.at(endPos) != '_') { - matchedStr.append("_"); + matchedStr.append('_'); } ident.replace(startPos, endPos - startPos, matchedStr); diff --git a/src/module/utilities/gtpy_contextmanager.cpp b/src/module/utilities/gtpy_contextmanager.cpp index cd5dd47d..136aa9df 100644 --- a/src/module/utilities/gtpy_contextmanager.cpp +++ b/src/module/utilities/gtpy_contextmanager.cpp @@ -63,6 +63,38 @@ namespace { +void ensurePythonOwnerEnvVar() +{ + if (qEnvironmentVariableIsSet(gtpy::constants::env::PYTHON_OWNER_ENV_VAR)) + { + return; + } + + auto exeBaseName = QFileInfo(QCoreApplication::applicationFilePath()) + .completeBaseName() + .toLower(); + + bool gtlabOwnsPython = (exeBaseName == QStringLiteral("gtlab") || + exeBaseName == QStringLiteral("gtlabconsole")); + + qputenv(gtpy::constants::env::PYTHON_OWNER_ENV_VAR, + gtlabOwnsPython + ? gtpy::constants::env::PYTHON_OWNER_GTLAB + : gtpy::constants::env::PYTHON_OWNER_EXTERNAL); +} + +bool isGtlabPythonOwner() +{ + ensurePythonOwnerEnvVar(); + + auto owner = qEnvironmentVariable( + gtpy::constants::env::PYTHON_OWNER_ENV_VAR) + .trimmed() + .toLower(); + + return owner == QLatin1String(gtpy::constants::env::PYTHON_OWNER_GTLAB); +} + GtpyContext::ContextType contextTypeEnumConvert( GtpyContextManager::Context type) { @@ -126,6 +158,7 @@ GtpyContextManager::GtpyContextManager(QObject* parent) : QObject(parent), m_decorator(nullptr), m_errorEmitted(false), m_pyThreadState(nullptr), + m_ownsPythonInterpreter{true}, m_contextsInitialized(false) { qRegisterMetaType @@ -139,7 +172,18 @@ GtpyContextManager::GtpyContextManager(QObject* parent) : dlopen(PYTHON_LIBRARY, RTLD_LAZY | RTLD_GLOBAL); #endif - PythonQt::init(PythonQt::RedirectStdOut); + // Determine interpreter ownership via GTLAB_PYTHON_INTERPRETER_OWNER. + // Ownership controls whether we initialize Python in embedded mode + // or attach to an externally hosted Python interpreter. + m_ownsPythonInterpreter = isGtlabPythonOwner(); + + const bool pythonAlreadyInitialized = (Py_IsInitialized() != 0); + int pythonQtFlags = pythonAlreadyInitialized + ? PythonQt::PythonAlreadyInitialized + : PythonQt::RedirectStdOut; + + PythonQt::init(pythonQtFlags); + GtpyGilScope::setGILScopeEnabled(true); @@ -175,7 +219,14 @@ GtpyContextManager::GtpyContextManager(QObject* parent) : connect(PythonQt::self(), SIGNAL(systemExitExceptionRaised(int)), this, SLOT(onSystemExitExceptionRaised(int))); - m_pyThreadState = PyEval_SaveThread(); + if (m_ownsPythonInterpreter) + { + m_pyThreadState = PyEval_SaveThread(); + } + else + { + m_pyThreadState = nullptr; + } GtpyCustomization::customizeSlotCalling(); @@ -186,8 +237,10 @@ GtpyContextManager::GtpyContextManager(QObject* parent) : GtpyContextManager* GtpyContextManager::instance() { - static GtpyContextManager mgr(nullptr); - return &mgr; + // Intentionally leaked singleton: in external-Python mode, shutdown order + // between Python/PythonQt/Qt and static destruction is fragile. + static GtpyContextManager* mgr = new GtpyContextManager(nullptr); + return mgr; } GtpyContextManager::~GtpyContextManager() @@ -716,7 +769,9 @@ GtpyContextManager::initContexts() } } - initStdOut(); + // if we own the python interpreter, we need to redirect print message to stdout / stderro + // Otherwise, python will take care of it + if (m_ownsPythonInterpreter) initStdOut(); m_contextsInitialized = true; } @@ -902,7 +957,6 @@ GtpyContextManager::context(int contextId) } -#ifdef PY3K PyPPObject GtpyContextManager::initExtensionModule(const QString& moduleName, PyModuleDef* def) @@ -932,24 +986,7 @@ GtpyContextManager::initExtensionModule(const QString& moduleName, return myMod; } -#else -PyPPObject -GtpyContextManager::initExtensionModule(const QString& moduleName, - PyMethodDef* methods) -{ - GTPY_GIL_SCOPE - - QByteArray name = moduleName.toUtf8(); - PyObject* myMod = nullptr; - myMod = Py_InitModule(name.constData(), methods); - Py_INCREF(myMod); - - modulenameToBuiltins(moduleName); - - return PyPPObject::NewRef(myMod); -} -#endif void GtpyContextManager::modulenameToBuiltins(QString name) @@ -990,13 +1027,8 @@ void GtpyContextManager::initCalculatorsModule() { GTPY_GIL_SCOPE -#ifdef PY3K initExtensionModule(gtpy::code::modules::GT_CALCULATORS, &GtpyCalculatorsModule::GtpyCalculators_Module); -#else - initExtensionModule(gtpy::code::modules::GT_CALCULATORS, - GtpyCalculatorsModule::GtpyCalculatorsModule_StaticMethods); -#endif GtpyCalculatorsModule::createCalcConstructors(); } @@ -1005,13 +1037,8 @@ GtpyContextManager::initLoggingModuleC() { GTPY_GIL_SCOPE -#ifdef PY3K initExtensionModule(gtpy::code::modules::GT_LOGGING, &GtpyLoggingModule::GtpyLogging_Module); -#else - initExtensionModule(gtpy::code::modules::GT_LOGGING, - GtpyLoggingModule::GtpyLoggingModule_StaticMethods); -#endif } void @@ -1072,14 +1099,9 @@ GtpyContextManager::initWrapperModule() { GTPY_GIL_SCOPE -#ifdef PY3K auto mod = initExtensionModule( gtpy::code::modules::GT_OBJECT_WRAPPER_MODULE, &GtpyExtendedWrapperModule::GtpyExtendedWrapper_Module); -#else - auto mod = initExtensionModule( - gtpy::code::modules::GT_OBJECT_WRAPPER_MODULE, nullptr); -#endif auto wrapperType = PyPPObject::NewRef( (PyObject*)&GtpyExtendedWrapperModule::GtpyExtendedWrapper_Type); @@ -1701,7 +1723,7 @@ GtpyContextManager::setImportableModulesCompletions(int contextId) foreach (QString name, modules) { - if (name.startsWith(name.startsWith(QStringLiteral("_")))) + if (name.startsWith(QStringLiteral("_"))) { continue; } diff --git a/src/module/utilities/gtpy_contextmanager.h b/src/module/utilities/gtpy_contextmanager.h index 130c85be..500a71f2 100644 --- a/src/module/utilities/gtpy_contextmanager.h +++ b/src/module/utilities/gtpy_contextmanager.h @@ -409,8 +409,6 @@ class GT_PYTHON_EXPORT GtpyContextManager: public QObject */ bool createCustomModule(const QString& moduleName, const QString& code); - -#ifdef PY3K /** * @brief Initializes the extension module defined in def and adds it to * the built-in modules of Python. The new module is named after moduleName. @@ -419,18 +417,6 @@ class GT_PYTHON_EXPORT GtpyContextManager: public QObject * @return A new reference of the extension module. */ PyPPObject initExtensionModule(const QString& moduleName, PyModuleDef* def); -#else - /** - * @brief Initializes the extension module and assigns it the static - * methods defined in methods. The new module is added to the built-in - * modules of Python and is named after moduleName. - * @param moduleName The name of the extension module. - * @param methods Definition of the static methods. - * @return A new reference of the extension module. - */ - PyPPObject initExtensionModule(const QString& moduleName, - PyMethodDef* methods); -#endif /** * @brief Adds the given name to the list of built-in modules in Python. @@ -610,6 +596,9 @@ class GT_PYTHON_EXPORT GtpyContextManager: public QObject /// Python main thread state PyThreadState* m_pyThreadState; + /// True if this module initialized/owns the Python interpreter lifecycle + bool m_ownsPythonInterpreter; + bool m_contextsInitialized; QMutex m_evalMutex; diff --git a/src/module/utilities/gtpy_convert.h b/src/module/utilities/gtpy_convert.h index c7f4cb93..c8e00481 100644 --- a/src/module/utilities/gtpy_convert.h +++ b/src/module/utilities/gtpy_convert.h @@ -11,7 +11,7 @@ #ifndef GTPYCONVERT_H #define GTPYCONVERT_H -#include "gt_globals.h" +#include #include #include "gtpypp.h" diff --git a/src/module/utilities/gtpy_decorator.cpp b/src/module/utilities/gtpy_decorator.cpp index 419e769f..f2eb8ab5 100644 --- a/src/module/utilities/gtpy_decorator.cpp +++ b/src/module/utilities/gtpy_decorator.cpp @@ -37,6 +37,7 @@ #include "gtpy_scriptcalculator.h" #include "gtpy_convert.h" #include "gtpy_threadscope.h" +#include "gtpy_taskapi.h" #include "gtpy_decorator.h" @@ -497,7 +498,8 @@ GtpyDecorator::runProcess(GtProject* pro, const QString& processId, qDebug() << "process run starts... processId == " << processId; // run process - GtTask* process = pro->findProcess(processId); + auto spec = gtpy::parseTaskSpec(processId); + GtTask* process = gtpy::getTask(pro->processData(), spec); if (process == nullptr) { @@ -1038,6 +1040,58 @@ GtpyDecorator::getPropertyContainerVal(GtObject* obj, QString const& id, return structCon.getMemberValToVariant(memberId); } +QVariant +GtpyDecorator::getPropertyContainerVal(GtObject* obj, + const QString& id, + const QString& entryId, + const QString& memberId) +{ + GtPropertyStructContainer* s = structContainerOfObject(obj, id); + + if (!s) + { + gtError() << __func__ << " -> PropertyStruct container of " + "object not found!"; + return {}; + } + + GtPropertyStructContainer::const_iterator it = s->findEntry(entryId); + + if (it != s->end()) + { + return it->getMemberValToVariant(memberId); + } + + gtError() << __func__ << tr("-> Cannot get parameter %1 of entry %2" + "in container %3").arg(memberId, entryId, id); + + return {}; +} + +QStringList +GtpyDecorator::getPropertyContainerEntryIds(GtObject* obj, + const QString& containerId) +{ + GtPropertyStructContainer* s = structContainerOfObject(obj, containerId); + + if (!s) + { + gtError() << __func__ << " -> PropertyStruct container of " + "object not found!"; + return {}; + } + + QStringList retVal; + + std::transform(s->begin(), s->end(), std::back_inserter(retVal), + [](const auto& v) + { + return v.ident(); + }); + + return retVal; +} + bool GtpyDecorator::setPropertyContainerVal(GtObject* obj, const QString& id, int index, const QString& memberId, @@ -1059,11 +1113,39 @@ GtpyDecorator::setPropertyContainerVal(GtObject* obj, const QString& id, return false; } - GtPropertyStructInstance& structCon = s->at(index); return structCon.setMemberVal(memberId, val); } + +bool +GtpyDecorator::setPropertyContainerVal(GtObject* obj, const QString& id, + const QString& entryId, + const QString& memberId, + const QVariant& val) +{ + GtPropertyStructContainer* s = structContainerOfObject(obj, id); + + if (!s) + { + gtError() << __func__ << " -> PropertyStruct container of " + "object not found!"; + return false; + } + + GtPropertyStructContainer::iterator it = s->findEntry(entryId); + + if (it != s->end()) + { + return it->setMemberVal(memberId, val); + } + + gtError() << __func__ << tr("-> Cannot set parameter %1 of entry %2" + "in container %3").arg(memberId, entryId, id); + + return false; +} + #endif QList GtpyDecorator::findGtProperties(GtObject* obj) diff --git a/src/module/utilities/gtpy_decorator.h b/src/module/utilities/gtpy_decorator.h index 8014671f..8f32162e 100644 --- a/src/module/utilities/gtpy_decorator.h +++ b/src/module/utilities/gtpy_decorator.h @@ -423,13 +423,40 @@ public slots: * @param id of the property struct container * @param index of the element of the items of the container * @param memberId of the property to call - * @return the value as a variant - * + * @return the value as a variant - empty for invalid inputs * Example call in python: * task.getPropertyContainerVal("args_input", 0, "name") */ QVariant getPropertyContainerVal (GtObject* obj, const QString& id, - int index, const QString& memberId); + int index, const QString& memberId); + + /** + * @brief Find a struct container property and return a value of + * a defined property. This function uses the id string of the + * container element. + * + * @param obj - parent object of the struct propety + * @param id of the property struct container + * @param entryId - id of the entry to read the value from. + * @param memberId of the property to call + * @return the value as a variant - empty for invalid inputs + * Example call in python: + * task.getPropertyContainerVal("constraints", "minEta", "value") + */ + QVariant getPropertyContainerVal(GtObject* obj, const QString& id, + const QString& entryId, + const QString& memberId); + + /** + * @brief getPropertyContainerEntryIds + * Returns a list with all property entry ids of a given container element + * The container element is specified by its containerId + * @param obj - object which has the property container + * @param containerId - Id of the property container to evaluate + * @return a ist of all ids of the contained properties + */ + QStringList getPropertyContainerEntryIds(GtObject* obj, + const QString& containerId); /** * @brief setPropertyContainerVal @@ -444,6 +471,20 @@ public slots: */ bool setPropertyContainerVal(GtObject* obj, QString const& id, int index, QString const& memberId, const QVariant& val); + + /** + * @brief Find a struct container property and sets a value of + * a defined propety + * @param obj - parent object of the struct propety + * @param id of the property struct container + * @param entryId - Id of the container element to modify. + * @param memberId of the property to call + * @param val - value to set + * @return true in case of success, else false + */ + bool setPropertyContainerVal(GtObject* obj, QString const& id, + const QString& entryId, + QString const& memberId, const QVariant& val); #endif /** * @brief findGtProperties returns all properties of a GtObject diff --git a/src/module/utilities/gtpy_globals.h b/src/module/utilities/gtpy_globals.h index 7705f5e9..ea6d2a43 100644 --- a/src/module/utilities/gtpy_globals.h +++ b/src/module/utilities/gtpy_globals.h @@ -27,6 +27,14 @@ constexpr const char* COLLECTION_CAT = "category"; constexpr const char* COLLECTION_SUBCAT = "subcategory"; constexpr const char* PROJ_PY_SCRIPTS_DIR = "scripts/python"; +/// Environment +namespace env +{ +constexpr const char* PYTHON_OWNER_ENV_VAR = "GTLAB_PYTHON_INTERPRETER_OWNER"; +constexpr const char* PYTHON_OWNER_GTLAB = "gtlab"; +constexpr const char* PYTHON_OWNER_EXTERNAL = "external"; +} + } // namespace constants } // namespace gtpy diff --git a/src/module/utilities/gtpy_moduleupgrader.cpp b/src/module/utilities/gtpy_moduleupgrader.cpp new file mode 100644 index 00000000..e02928ec --- /dev/null +++ b/src/module/utilities/gtpy_moduleupgrader.cpp @@ -0,0 +1,210 @@ +/* GTlab - Gas Turbine laboratory + * Source File: gtpy_code.h + * + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2025 German Aerospace Center (DLR) + * + * Created on: 14.11.2025 + * Author: Jens Schmeink (DLR AT-TWK) + */ + +#include "gtpy_moduleupgrader.h" + +#include "gt_logging.h" +#include "gt_xmlexpr.h" + +namespace { + + +bool handleElement(const QDomNode& node, + const QStringList& classNames, + QList& results, + bool allowNestedClassElements) +{ + QDomElement elem = node.toElement(); + QString c = elem.attribute(gt::xml::S_CLASS_TAG); + + if (classNames.contains(c)) + { + results.append(elem); + return allowNestedClassElements; + } + return true; +} + + +// internal helper +void findElementsByClass(const QDomNode& node, + const QStringList& classNames, + QList& results, + bool allowNestedClassElements = false) +{ + if (node.isElement()) + { + if (!handleElement(node, classNames, results, allowNestedClassElements)) + { + return; + } + } + + QDomNode child = node.firstChild(); + while (!child.isNull()) + { + if (child.isElement()) + { + if (!handleElement(child, classNames, results, allowNestedClassElements)) + { + child = child.nextSibling(); + continue; + } + } + + findElementsByClass(child, classNames, results, allowNestedClassElements); + child = child.nextSibling(); + } +} + +/** + * @brief normalizePropertyContainerId + * Important change of the update is the replacement of the names + * of the property elements. + * Therefore a map is used to collect the old and new names of the entries. + * @param container - QDomElement of the container property + * @param formerNameKey - identifier if the property in the old structure + * @param replaceMap - map to collect the required renamings + */ +void normalizePropertyContainerId( + QDomElement const& container, QString const& formerNameKey, + QMap& replaceMap) +{ + QDomNode child = container.firstChild(); + + while (!child.isNull()) + { + if (child.isElement() == false) + { + child = child.nextSibling(); + continue; + } + + QDomElement prop = child.toElement(); + + if (prop.tagName() != gt::xml::S_PROPERTY_TAG) + { + child = child.nextSibling(); + continue; + } + + QString oldUUID = prop.attribute(gt::xml::S_NAME_TAG); + + if (oldUUID.isEmpty()) + { + child = child.nextSibling(); + continue; + } + + // Search for sub element NewName + QDomElement nameElem; + QDomNode sub = prop.firstChild(); + + while (!sub.isNull()) + { + QDomElement e = sub.toElement(); + if (!e.isNull() + && e.tagName() == gt::xml::S_PROPERTY_TAG + && e.attribute(gt::xml::S_NAME_TAG) == formerNameKey) + { + nameElem = e; + break; + } + sub = sub.nextSibling(); + } + + if (!nameElem.isNull()) + { + QString newName = nameElem.text().trimmed(); + + // keep connection info + replaceMap.insert(oldUUID, newName); + + // Replace UUID in name attribute + prop.setAttribute(gt::xml::S_NAME_TAG, newName); + + // Remove subelement + prop.removeChild(nameElem); + } + + child = child.nextSibling(); + } +} + +void replaceUUIDsInTextNodes(QDomNode node, + const QMap& replaceMap) +{ + QDomNode child = node.firstChild(); + + while (!child.isNull()) + { + + if (child.isText()) + { + QString txt = child.nodeValue(); + QString newTxt = txt; + + for (auto it = replaceMap.begin(); it != replaceMap.end(); ++it) + { + newTxt.replace(it.key(), it.value()); + } + + if (newTxt != txt) + child.setNodeValue(newTxt); + } + + replaceUUIDsInTextNodes(child, replaceMap); + child = child.nextSibling(); + } +} +} + +bool +gtpy::module_upgrader::to_2_0_0::run(QDomElement& root, + const QString& targetPath) +{ + // only do updates for process elements + if (!targetPath.endsWith(".gttask")) return true; + + if (root.isNull()) + { + gtError() << QObject::tr("Invalid .gttask file!"); + return false; + } + + QList found; + + // Step 1: Find elements with class=... + findElementsByClass(root, {"GtpyScriptCalculator", "GtpyTask"}, found); + + // Step 2: Replace UUIDs in input/output_args and build map + QMap replaceMap; + + for (QDomElement& elem : found) + { + QDomElement c = elem.firstChildElement(gt::xml::S_PROPERTYCONT_TAG); + while (!c.isNull()) + { + QString name = c.attribute(gt::xml::S_NAME_TAG); + + if (name == "input_args" || name == "output_args") + { + normalizePropertyContainerId(c, gt::xml::S_NAME_TAG, replaceMap); + } + + c = c.nextSiblingElement(gt::xml::S_PROPERTYCONT_TAG); + } + } + + // Step 3: Replace all uuids in the document + replaceUUIDsInTextNodes(root, replaceMap); + + return true; +} diff --git a/src/module/utilities/gtpy_moduleupgrader.h b/src/module/utilities/gtpy_moduleupgrader.h new file mode 100644 index 00000000..300d76d8 --- /dev/null +++ b/src/module/utilities/gtpy_moduleupgrader.h @@ -0,0 +1,31 @@ +/* GTlab - Gas Turbine laboratory + * Source File: gtpy_code.h + * + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2025 German Aerospace Center (DLR) + * + * Created on: 14.11.2025 + * Author: Jens Schmeink (DLR AT-TWK) + */ +#ifndef GTPY_MODULEUPGRADER_H +#define GTPY_MODULEUPGRADER_H + +#include + +namespace gtpy { + +namespace module_upgrader { + +namespace to_2_0_0 { + /** + * @brief Performs the upgrade to predign data model 3.0.0 + * + * @return true in case of success + */ + bool run(QDomElement& root, const QString& targetPath); +} // to_2_0_0 +} // module_upgrader +} // gtpy + + +#endif // GTPY_MODULEUPGRADER_H diff --git a/src/module/utilities/gtpy_processdatadistributor.cpp b/src/module/utilities/gtpy_processdatadistributor.cpp index 4712122a..8ab1b135 100644 --- a/src/module/utilities/gtpy_processdatadistributor.cpp +++ b/src/module/utilities/gtpy_processdatadistributor.cpp @@ -14,6 +14,7 @@ #include "gt_task.h" #include "gtpy_processdatadistributor.h" +#include "gtpy_taskapi.h" GtpyProcessDataDistributor::GtpyProcessDataDistributor(GtTask* pythonTask) { @@ -30,35 +31,14 @@ GtpyProcessDataDistributor::taskElement(const QString& name) return nullptr; } -#if GT_VERSION >= 0x020000 - GtTaskGroup* group = data->taskGroup(); + auto taskSpec = gtpy::parseTaskSpec(name); + GtTask* task = getTask(data, taskSpec); - if (!group) - { - return nullptr; - } - - GtTask* task = group->findDirectChild(name); -#else - GtTask* task = data->findDirectChild(name); -#endif if (!task) { return task; } - // QList children = m_pythonTask->findDirectChildren(); - - // foreach (GtTask* child, children) - // { - // if(child->uuid() == task->uuid()) - // { - // delete child; - // child = nullptr; - // break; - // } - // } - task = qobject_cast(task->clone()); task->moveToThread(m_pythonTask->thread()); diff --git a/src/module/utilities/gtpy_regexp.cpp b/src/module/utilities/gtpy_regexp.cpp index dd643823..e7b4824e 100644 --- a/src/module/utilities/gtpy_regexp.cpp +++ b/src/module/utilities/gtpy_regexp.cpp @@ -10,10 +10,10 @@ #include "gtpy_regexp.h" -QRegExp +QRegularExpression gtpy::re::exceptLettersAndNumbers() { - return QRegExp{"[^A-Za-z0-9]+"}; + return QRegularExpression{"[^A-Za-z0-9]+"}; } diff --git a/src/module/utilities/gtpy_regexp.h b/src/module/utilities/gtpy_regexp.h index 4051a060..1d97e29c 100644 --- a/src/module/utilities/gtpy_regexp.h +++ b/src/module/utilities/gtpy_regexp.h @@ -11,7 +11,6 @@ #ifndef GTPYREGEXP_H #define GTPYREGEXP_H -#include #include /** @@ -26,7 +25,7 @@ namespace re * @brief Everything except letters and numbers. * @return Returns a QRegExp instance. */ -QRegExp exceptLettersAndNumbers(); +QRegularExpression exceptLettersAndNumbers(); /** * @brief Returns a regular expression that matches valid Python identifiers. diff --git a/src/module/utilities/gtpy_taskapi.cpp b/src/module/utilities/gtpy_taskapi.cpp new file mode 100644 index 00000000..76904dae --- /dev/null +++ b/src/module/utilities/gtpy_taskapi.cpp @@ -0,0 +1,145 @@ +/* GTlab - Gas Turbine laboratory + * + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2026 German Aerospace Center (DLR) + */ + +#include "gtpy_taskapi.h" + +#include +#include + +#include + +namespace gtpy +{ + +TaskSpec +parseTaskSpec(const QString& spec) +{ + TaskSpec out; + const int slash = spec.indexOf('/'); + + if (slash < 0) + { + out.taskId = spec; + + return out; + } + + out.hasExplicitGroup = true; + out.groupId = spec.left(slash); + out.taskId = spec.mid(slash + 1); + + return out; +} + +/** + * @brief Tries to initialize a task group + * @param group + * @param scope + * @return + */ +bool +initTaskGroup(GtTaskGroup* group, GtTaskGroup::SCOPE scope) +{ + if (!group) return false; + + if (group->isInitialized()) return true; + + if (!gtApp || !gtApp->currentProject() || !group) return false; + + auto initGroupImpl = [group, scope]() { + group->read(gtApp->currentProject()->path(), scope); + }; + + // task group intialization must be done in the main thread + if (QThread::currentThread() == gtApp->thread()) + { + initGroupImpl(); + } + else + { + QMetaObject::invokeMethod(gtApp, initGroupImpl, + Qt::BlockingQueuedConnection); + } + + if (!group->isInitialized()) + { + gtError() << QObject::tr("Error initializing task group '%1'") + .arg(group->objectName()); + return false; + } + + return true; +} + +#if GT_VERSION >= 0x020000 +GtTaskGroup* +getTaskGroup(GtProcessData* data, TaskSpec spec) +{ + if (!data) return nullptr; + + GtTaskGroup* group = nullptr; + + auto& groupName = spec.groupId; + + if (groupName.isEmpty()) return data->taskGroup(); + + GtTaskGroup::SCOPE scope = GtTaskGroup::UNDEFINED; + + if (data->customGroupIds().contains(groupName)) + { + scope = GtTaskGroup::CUSTOM; + } + else if(data->userGroupIds().contains(groupName)) + { + scope = GtTaskGroup::USER; + } + + if (scope == GtTaskGroup::UNDEFINED) + { + return nullptr; + } + + auto* scopeElement = data->findDirectChild( + GtTaskGroup::scopeId(scope)); + + if (!scopeElement) + { + return nullptr; + } + + group = scopeElement->findDirectChild(groupName); + + if (!group) + { + return nullptr; + } + + if (!initTaskGroup(group, scope)) return nullptr; + + return group; +} +#endif + +GtTask* +getTask(GtProcessData* data, TaskSpec spec) +{ + if (!data) return nullptr; + +#if GT_VERSION >= 0x020000 + GtTaskGroup* group = getTaskGroup(data, spec); + + if (!group) + { + return nullptr; + } + + return group->findDirectChild(spec.taskId); +#else + return data->findDirectChild(spec.taskId); +#endif +} + +} // namespace gtpy diff --git a/src/module/utilities/gtpy_taskapi.h b/src/module/utilities/gtpy_taskapi.h new file mode 100644 index 00000000..42d8d62e --- /dev/null +++ b/src/module/utilities/gtpy_taskapi.h @@ -0,0 +1,46 @@ +/* GTlab - Gas Turbine laboratory + * + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2026 German Aerospace Center (DLR) + */ + +#ifndef GTPY_TASKAPI_H +#define GTPY_TASKAPI_H + +#include +#include + +#include + +namespace gtpy +{ + +/** + * @brief Parsed task specification in `taskId` or `groupId/taskId` form. + */ +struct TaskSpec +{ + QString groupId; + QString taskId; + bool hasExplicitGroup{false}; +}; + +/** + * @brief Parse a task specification string. + * + * @param spec Task identifier in `taskId` or `groupId/taskId` form. + * @return Parsed task specification parts. + */ +TaskSpec parseTaskSpec(const QString& spec); + +/** + * @brief Gets the task from the process + * + * @param data The processdata owning the task. + * @param taskSpec Task spec in `taskId` or `groupId/taskId` form. + */ +GtTask* getTask(GtProcessData* data, TaskSpec spec); + +} // namespace gtpy + +#endif // GTPY_TASKAPI_H diff --git a/src/module/utilities/gtpy_transfer.cpp b/src/module/utilities/gtpy_transfer.cpp index 7cf31549..0fad2501 100644 --- a/src/module/utilities/gtpy_transfer.cpp +++ b/src/module/utilities/gtpy_transfer.cpp @@ -65,7 +65,11 @@ gtpy::transfer::propStructToPython( for (const auto& instance : container) { +#if GT_VERSION >= GT_VERSION_CHECK(2, 1, 0) + auto name = instance.ident(); +#else auto name = instance.getMemberVal("name"); +#endif if (!name.isEmpty()) { @@ -92,8 +96,11 @@ gtpy::transfer::propStructFromPython( for (auto& entry : container) { +#if GT_VERSION >= GT_VERSION_CHECK(2, 1, 0) + auto argName = entry.ident(); +#else auto argName = entry.getMemberVal("name"); - +#endif auto iter = dict.find(argName); if (iter != dict.end()) { diff --git a/src/module/utilities/pythonextensions/gtpy_calculatorsmodule.cpp b/src/module/utilities/pythonextensions/gtpy_calculatorsmodule.cpp index 3519216a..82600729 100644 --- a/src/module/utilities/pythonextensions/gtpy_calculatorsmodule.cpp +++ b/src/module/utilities/pythonextensions/gtpy_calculatorsmodule.cpp @@ -146,9 +146,9 @@ calcClassName(GtpyCreateCalculator* func) if (func->m_calcClassName) { - if (PyString_Check(func->m_calcClassName)) + if (PyUnicode_Check(func->m_calcClassName)) { - className = PyString_AsString(func->m_calcClassName); + className = PyUnicode_AsUTF8(func->m_calcClassName); } } diff --git a/src/module/utilities/pythonextensions/gtpy_calculatorsmodule.h b/src/module/utilities/pythonextensions/gtpy_calculatorsmodule.h index b687404a..0058f1f4 100644 --- a/src/module/utilities/pythonextensions/gtpy_calculatorsmodule.h +++ b/src/module/utilities/pythonextensions/gtpy_calculatorsmodule.h @@ -49,7 +49,6 @@ GtpyCalculatorsModule_StaticMethods[] = void createCalcConstructors(); -#ifdef PY3K static PyModuleDef GtpyCalculators_Module = { @@ -63,7 +62,6 @@ GtpyCalculators_Module = NULL, NULL }; -#endif } diff --git a/src/module/utilities/pythonextensions/gtpy_createhelperfunction.cpp b/src/module/utilities/pythonextensions/gtpy_createhelperfunction.cpp index af271b35..16f32c24 100644 --- a/src/module/utilities/pythonextensions/gtpy_createhelperfunction.cpp +++ b/src/module/utilities/pythonextensions/gtpy_createhelperfunction.cpp @@ -52,7 +52,7 @@ GtpyCreateHelperFunction_Call(PyObject* func, PyObject* args_py, auto args = PyPPObject::Borrow(args_py); Py_ssize_t argc = PyPPTuple_Size(args); - if (!f->m_helperName || !PyString_Check(f->m_helperName)) + if (!f->m_helperName || !PyUnicode_Check(f->m_helperName)) { QString error = "GtpyCreateHelperFunction is invalid!"; @@ -61,7 +61,7 @@ GtpyCreateHelperFunction_Call(PyObject* func, PyObject* args_py, return nullptr; } - QString helperName = QString(PyString_AsString(f->m_helperName)); + QString helperName = QString(PyUnicode_AsUTF8(f->m_helperName)); if (argc == 1) { diff --git a/src/module/utilities/pythonextensions/gtpy_extendedwrapper.cpp b/src/module/utilities/pythonextensions/gtpy_extendedwrapper.cpp index 7b5ea5ce..429802b1 100644 --- a/src/module/utilities/pythonextensions/gtpy_extendedwrapper.cpp +++ b/src/module/utilities/pythonextensions/gtpy_extendedwrapper.cpp @@ -22,12 +22,14 @@ #include "gtpy_extendedwrapper.h" #include "gtpypp.h" +#include + using namespace GtpyExtendedWrapperModule; static QString pyValidGtPropertyId(QString id) { - return id.replace(QRegExp("[^A-Za-z0-9]+"), ""); + return id.replace(QRegularExpression("[^A-Za-z0-9]+"), ""); } static QString @@ -236,7 +238,7 @@ GtpyExtendedWrapper_methods[] = static int GtpyExtendedWrapper_setattro(PyObject* obj, PyObject* name, PyObject* value) { - QString strName = QString(PyString_AsString(name)); + QString strName = QString(PyUnicode_AsUTF8(name)); if (strName.isEmpty()) { @@ -336,13 +338,13 @@ GtpyExtendedWrapper_getattro(PyObject* obj, PyObject* name) } // Check if the given name is a string object - if (!PyString_Check(name)) + if (!PyUnicode_Check(name)) { PyErr_SetString(PyExc_AttributeError, "invalid attribute name"); return nullptr; } - QString strName{PyString_AsString(name)}; + QString strName{PyUnicode_AsUTF8(name)}; if(strName.isEmpty()) { @@ -541,9 +543,6 @@ GtpyExtendedWrapper_as_number = 0, /* nb_add */ 0, /* nb_subtract */ 0, /* nb_multiply */ -#ifndef PY3K - 0, /* nb_divide */ -#endif 0, /* nb_remainder */ 0, /* nb_divmod */ 0, /* nb_power */ @@ -557,22 +556,12 @@ GtpyExtendedWrapper_as_number = 0, /* nb_and */ 0, /* nb_xor */ 0, /* nb_or */ -#ifndef PY3K - 0, /* nb_coerce */ -#endif 0, /* nb_int */ 0, /* nb_long / nb_reserved in Py3K */ 0, /* nb_float */ -#ifndef PY3K - 0, /* nb_oct */ - 0, /* nb_hex */ -#endif 0, /* nb_inplace_add */ 0, /* nb_inplace_subtract */ 0, /* nb_inplace_multiply */ -#ifndef PY3K - 0, /* nb_inplace_divide */ -#endif 0, /* nb_inplace_remainder */ 0, /* nb_inplace_power */ 0, /* nb_inplace_lshift */ @@ -584,9 +573,7 @@ GtpyExtendedWrapper_as_number = 0, /* nb_true_divide */ 0, /* nb_inplace_floor_divide */ 0, /* nb_inplace_true_divide */ -#ifdef PY3K 0, /* nb_index in Py3K */ -#endif }; PyTypeObject @@ -612,9 +599,6 @@ GtpyExtendedWrapperModule::GtpyExtendedWrapper_Type = GtpyExtendedWrapper_setattro, /*tp_setattro*/ 0, /*tp_as_buffer*/ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE -#ifndef PY3K - | Py_TPFLAGS_CHECKTYPES -#endif , /*tp_flags*/ "GtpyExtendedWrapper object", /* tp_doc */ 0, /* tp_traverse */ @@ -623,11 +607,7 @@ GtpyExtendedWrapperModule::GtpyExtendedWrapper_Type = 0, /* tp_weaklistoffset */ 0, /* tp_iter */ 0, /* tp_iternext */ -#ifdef PY3K - GtpyExtendedWrapper_methods, -#else GtpyExtendedWrapper_methods, /* tp_methods */ -#endif 0, /* tp_members */ 0, /* tp_getset */ 0, /* tp_base */ diff --git a/src/module/utilities/pythonextensions/gtpy_extendedwrapper.h b/src/module/utilities/pythonextensions/gtpy_extendedwrapper.h index bd90d6bc..a0b6522f 100644 --- a/src/module/utilities/pythonextensions/gtpy_extendedwrapper.h +++ b/src/module/utilities/pythonextensions/gtpy_extendedwrapper.h @@ -37,7 +37,6 @@ struct GtpyExtendedWrapper QObject* getObject() const; }; -#ifdef PY3K static PyModuleDef GtpyExtendedWrapper_Module = { @@ -51,7 +50,6 @@ GtpyExtendedWrapper_Module = NULL, NULL }; -#endif } namespace GtpyCustomization diff --git a/src/module/utilities/pythonextensions/gtpy_loggingmodule.cpp b/src/module/utilities/pythonextensions/gtpy_loggingmodule.cpp index 2060d796..d775e9ca 100644 --- a/src/module/utilities/pythonextensions/gtpy_loggingmodule.cpp +++ b/src/module/utilities/pythonextensions/gtpy_loggingmodule.cpp @@ -264,7 +264,7 @@ GtpyPyLogger_lshift(PyObject* self, PyObject* arg) const auto* msg = PyPPUnicode_AsUTF8(pyMsg); if (!msg) return nullptr; - auto logLevel = static_cast(PyInt_AsLong(logger->m_logLevel)); + auto logLevel = static_cast(PyLong_AsLong(logger->m_logLevel)); printToConsols(logLevel, msg); @@ -277,9 +277,6 @@ static PyNumberMethods 0, /* nb_add */ 0, /* nb_subtract */ 0, /* nb_multiply */ -#ifndef PY3K - 0, /* nb_divide */ -#endif 0, /* nb_remainder */ 0, /* nb_divmod */ 0, /* nb_power */ @@ -293,22 +290,12 @@ static PyNumberMethods 0, /* nb_and */ 0, /* nb_xor */ 0, /* nb_or */ -#ifndef PY3K - 0, /* nb_coerce */ -#endif 0, /* nb_int */ 0, /* nb_long / nb_reserved in Py3K */ 0, /* nb_float */ -#ifndef PY3K - 0, /* nb_oct */ - 0, /* nb_hex */ -#endif 0, /* nb_inplace_add */ 0, /* nb_inplace_subtract */ 0, /* nb_inplace_multiply */ -#ifndef PY3K - 0, /* nb_inplace_divide */ -#endif 0, /* nb_inplace_remainder */ 0, /* nb_inplace_power */ 0, /* nb_inplace_lshift */ @@ -320,9 +307,7 @@ static PyNumberMethods 0, /* nb_true_divide */ 0, /* nb_inplace_floor_divide */ 0, /* nb_inplace_true_divide */ -#ifdef PY3K - 0, /* nb_index in Py3K */ -#endif + 0, /* nb_index */ }; PyTypeObject @@ -348,11 +333,7 @@ PyTypeObject 0, /*tp_setattro*/ 0, /*tp_as_buffer*/ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE /*tp_flags*/ -#ifndef PY3K - | Py_TPFLAGS_CHECKTYPES, -#else , -#endif "GtpyPyLogger doc",/* tp_doc */ 0, /* tp_traverse */ 0, /* tp_clear */ diff --git a/src/module/utilities/pythonextensions/gtpy_loggingmodule.h b/src/module/utilities/pythonextensions/gtpy_loggingmodule.h index 6e686111..1a86f6d8 100644 --- a/src/module/utilities/pythonextensions/gtpy_loggingmodule.h +++ b/src/module/utilities/pythonextensions/gtpy_loggingmodule.h @@ -105,7 +105,6 @@ GtpyLoggingModule_StaticMethods[] = }; -#ifdef PY3K static PyModuleDef GtpyLogging_Module = { @@ -119,7 +118,6 @@ GtpyLogging_Module = NULL, NULL }; -#endif } diff --git a/src/module/utilities/pythonextensions/gtpy_propertysetter.cpp b/src/module/utilities/pythonextensions/gtpy_propertysetter.cpp index c979172d..b00dafec 100644 --- a/src/module/utilities/pythonextensions/gtpy_propertysetter.cpp +++ b/src/module/utilities/pythonextensions/gtpy_propertysetter.cpp @@ -53,14 +53,14 @@ meth_repr(GtpyPropertySetterObject* f) if (f->m_self->ob_type == &GtpyExtendedWrapper_Type) { QString repr = "m_propId)) + + PyUnicode_AsUTF8(f->m_propId)) + " value>"; - return PyString_FromFormat(repr.toStdString().c_str()); + return PyUnicode_FromFormat(repr.toStdString().c_str()); } else { - return PyString_FromFormat(""); + return PyUnicode_FromFormat(""); } } @@ -89,7 +89,7 @@ GtpyPropertySetter_Call(PyObject* func, PyObject* args, Py_ssize_t argc = PyTuple_Size(args); - QString propId = QString(PyString_AsString(f->m_propId)); + QString propId = QString(PyUnicode_AsUTF8(f->m_propId)); if (argc != 1) { @@ -252,11 +252,7 @@ GtpyPropertySetter_Type = 0, /* tp_print */ 0, /* tp_getattr */ 0, /* tp_setattr */ -#ifdef PY3K 0, -#else - (cmpfunc)meth_compare, /* tp_compare */ -#endif (reprfunc)meth_repr, /* tp_repr */ 0, /* tp_as_number */ 0, /* tp_as_sequence */ diff --git a/src/module/utilities/pythonextensions/gtpy_stdout.cpp b/src/module/utilities/pythonextensions/gtpy_stdout.cpp index ae7be671..9023b9f5 100644 --- a/src/module/utilities/pythonextensions/gtpy_stdout.cpp +++ b/src/module/utilities/pythonextensions/gtpy_stdout.cpp @@ -83,22 +83,7 @@ GtpyStdOutRedirect_write(PyObject* self, PyObject* args) if (PyPPUnicode_Check(obj)) { -#ifdef PY3K message = PyPPString_AsQString(obj); -#else - PyObject* tmp = PyUnicode_AsUTF8String(obj.get()); - - if (tmp) - { - message = QString::fromUtf8(PyString_AS_STRING(tmp)); - Py_DECREF(tmp); - } - else - { - return NULL; - } - -#endif } else { diff --git a/src/module/widgets/gtpy_console.cpp b/src/module/widgets/gtpy_console.cpp index 45278f9f..3cf1cbbb 100644 --- a/src/module/widgets/gtpy_console.cpp +++ b/src/module/widgets/gtpy_console.cpp @@ -82,7 +82,12 @@ GtpyConsole::GtpyConsole(int contextId, const int tabStop = 4; QFontMetrics metrics(font); + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + setTabStopDistance(tabStop * metrics.horizontalAdvance(' ')); +#else setTabStopWidth(tabStop * metrics.width(' ')); +#endif } void diff --git a/src/module/widgets/gtpy_scripteditor.cpp b/src/module/widgets/gtpy_scripteditor.cpp index dff28ac3..de88eaf7 100644 --- a/src/module/widgets/gtpy_scripteditor.cpp +++ b/src/module/widgets/gtpy_scripteditor.cpp @@ -138,16 +138,24 @@ GtpyScriptEditor::wheelEvent(QWheelEvent* event) { if (event->modifiers() == Qt::ControlModifier && !isReadOnly()) { - if (event->delta() > 0) +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + const int dy = event->angleDelta().y(); +#else + const int dy = event->delta(); +#endif + + if (dy > 0) { zoomIn(2); } - else + else if (dy < 0) { zoomOut(2); } setTabSize(m_tabSize); + + event->accept(); } else { @@ -293,10 +301,10 @@ GtpyScriptEditor::replaceBlockHeaders(const QString& oldHeader, } void -GtpyScriptEditor::searchAndReplace(const QRegExp& searchRegExp, const +GtpyScriptEditor::searchAndReplace(const QRegularExpression& searchRegExp, const QString& replaceBy, bool all) { - if (searchRegExp.isEmpty() || !searchRegExp.isValid()) + if (!searchRegExp.isValid() || searchRegExp.pattern().isEmpty()) { return; } @@ -1217,7 +1225,7 @@ GtpyScriptEditor::isSelectionIndentable() QString selection = cursor.selectedText(); QStringList lines = selection.split(QChar::ParagraphSeparator, - QString::SkipEmptyParts); + Qt::SkipEmptyParts); int count = lines.count(); diff --git a/src/module/widgets/gtpy_scripteditor.h b/src/module/widgets/gtpy_scripteditor.h index 2e411c68..a1a7ef76 100644 --- a/src/module/widgets/gtpy_scripteditor.h +++ b/src/module/widgets/gtpy_scripteditor.h @@ -16,6 +16,8 @@ #include "gt_calculator.h" #include "gt_codeeditor.h" +#include + class GtpyCompleter; /** @@ -91,7 +93,7 @@ class GT_PYTHON_EXPORT GtpyScriptEditor : public GtCodeEditor * @param all If it is true, all strings in the document will be replaced. * Otherwise it only replaces the first one found. */ - void searchAndReplace(const QRegExp& searchRegExp, const QString& replaceBy, + void searchAndReplace(const QRegularExpression& searchRegExp, const QString& replaceBy, bool all = true); /** * @brief Searchs for the given string searchFor and replaces it diff --git a/src/module/widgets/gtpy_scripteditorwidget.cpp b/src/module/widgets/gtpy_scripteditorwidget.cpp index 70ad35e1..87a03d75 100644 --- a/src/module/widgets/gtpy_scripteditorwidget.cpp +++ b/src/module/widgets/gtpy_scripteditorwidget.cpp @@ -47,7 +47,6 @@ GtpyScriptEditorWidget::GtpyScriptEditorWidget(int contextId, QWidget* parent) : /// top layout auto* topLayout = new QHBoxLayout(); topLayout->setSpacing(0); - topLayout->setMargin(0); topLayout->setContentsMargins(0, 0, 0, 0); m_pimpl->m_undoButton = new QPushButton; diff --git a/src/module/widgets/gtpy_taskdelegate.cpp b/src/module/widgets/gtpy_taskdelegate.cpp index b6588000..30e1c824 100644 --- a/src/module/widgets/gtpy_taskdelegate.cpp +++ b/src/module/widgets/gtpy_taskdelegate.cpp @@ -29,12 +29,12 @@ GtpyTaskDelegate::createEditor(QWidget* parent, QLineEdit* lineEdit = new QLineEdit(parent); #if GT_VERSION < 0x020000 - QRegExp regExp = GtRegExp::onlyLettersAndNumbersAndSpace(); + QRegExp onlyLettersAndNumbersAndSpaceExp = GtRegExp::onlyLettersAndNumbersAndSpace(); #else - QRegExp regExp = gt::re::onlyLettersAndNumbersAndSpace(); + static QRegularExpression onlyLettersAndNumbersAndSpaceExp("[A-Za-z0-9\\_\\-\\[\\]\\s\\␣]+"); #endif - QValidator* validator = new QRegExpValidator(regExp, this->parent());; + QValidator* validator = new QRegularExpressionValidator(onlyLettersAndNumbersAndSpaceExp, this->parent());; lineEdit->setValidator(validator); diff --git a/src/module/wizards/gtpy_abstractscriptingwizardpage.cpp b/src/module/wizards/gtpy_abstractscriptingwizardpage.cpp index 0064e00c..a6d5e7b1 100644 --- a/src/module/wizards/gtpy_abstractscriptingwizardpage.cpp +++ b/src/module/wizards/gtpy_abstractscriptingwizardpage.cpp @@ -101,7 +101,6 @@ GtpyAbstractScriptingWizardPage::GtpyAbstractScriptingWizardPage( QHBoxLayout* separatorLay = new QHBoxLayout(this); separatorLay->setSpacing(0); - separatorLay->setMargin(0); separatorLay->setContentsMargins(0, 0, 0, 0); QLabel* label = new QLabel("Output Console:", this); @@ -542,7 +541,7 @@ GtpyAbstractScriptingWizardPage::replaceBlockHeaders(const QString& oldHeader, void GtpyAbstractScriptingWizardPage::searchAndReplaceEditorText( - const QRegExp& searchFor, const QString& replaceBy, bool all) + const QRegularExpression& searchFor, const QString& replaceBy, bool all) { if (!m_editor) { @@ -586,28 +585,9 @@ GtpyAbstractScriptingWizardPage::setConsoleVisible(bool visible) QString GtpyAbstractScriptingWizardPage::indentation(const QString& codeLine) const { - QString indent; - QRegExp spacesRegExp("^ +\t*"); - QRegExp tabRegExp("^\t+ *"); - QRegExp regExp; - - if (codeLine.contains(spacesRegExp)) - { - regExp = spacesRegExp; - } - else if (codeLine.contains(tabRegExp)) - { - regExp = tabRegExp; - } - - int pos = regExp.indexIn(codeLine); - - if (pos > -1) - { - indent = regExp.cap(0); - } - - return indent; + static const QRegularExpression leadingIndent(R"(^[ \t]+)"); + const QRegularExpressionMatch match = leadingIndent.match(codeLine); + return match.hasMatch() ? match.captured(0) : QString{}; } void diff --git a/src/module/wizards/gtpy_abstractscriptingwizardpage.h b/src/module/wizards/gtpy_abstractscriptingwizardpage.h index a0fbe72c..73ba8245 100644 --- a/src/module/wizards/gtpy_abstractscriptingwizardpage.h +++ b/src/module/wizards/gtpy_abstractscriptingwizardpage.h @@ -140,7 +140,7 @@ class GT_PYTHON_EXPORT GtpyAbstractScriptingWizardPage : * @param all If it is true, all founds in the document will be replaced. * Otherwise it replaces only the first one found. */ - void searchAndReplaceEditorText(const QRegExp& searchFor, + void searchAndReplaceEditorText(const QRegularExpression& searchFor, const QString& replaceBy, bool all); /** diff --git a/src/module/wizards/python_task/gtpy_taskwizardpage.cpp b/src/module/wizards/python_task/gtpy_taskwizardpage.cpp index c8e3ea0b..65ef199b 100644 --- a/src/module/wizards/python_task/gtpy_taskwizardpage.cpp +++ b/src/module/wizards/python_task/gtpy_taskwizardpage.cpp @@ -70,7 +70,7 @@ GtpyTaskWizardPage::GtpyTaskWizardPage() : #if GT_VERSION < GT_VERSION_CHECK(2, 0, 0) addElementButton->setStyleSheet(GtStyleSheets::buttonStyleSheet()); #else - addElementButton->setStyleSheet(gt::gui::stylesheet::buttonStyleSheet()); + addElementButton->setStyleSheet(gt::gui::stylesheet::button()); #endif addElementButton->setIcon(GTPY_ICON(add)); @@ -78,7 +78,6 @@ GtpyTaskWizardPage::GtpyTaskWizardPage() : QVBoxLayout* treeViewLay = new QVBoxLayout; treeViewLay->setSpacing(1); - treeViewLay->setMargin(0); treeViewLay->setContentsMargins(0, 0, 0, 0); treeViewLay->addWidget(addElementButton); @@ -690,7 +689,7 @@ GtpyTaskWizardPage::onProcessComponentRenamed(QString const& className, QString pattern = "(" + className + "\\( *\"" + oldName + "\" *\\))"; QString replace = className + "(\"" + newName + "\")"; - searchAndReplaceEditorText(QRegExp(pattern), replace, true); + searchAndReplaceEditorText(QRegularExpression(pattern), replace, true); } void diff --git a/src/setup_module/gt_python_setup.cpp b/src/setup_module/gt_python_setup.cpp index 300a9d87..2a56bc52 100644 --- a/src/setup_module/gt_python_setup.cpp +++ b/src/setup_module/gt_python_setup.cpp @@ -31,7 +31,7 @@ GtVersionNumber GtPythonSetupModule::version() { - return GtVersionNumber(1, 8, 0); + return GtVersionNumber(1, 8, 2); } QString diff --git a/src/setup_module/gtps_pythonpreferencepage.cpp b/src/setup_module/gtps_pythonpreferencepage.cpp index 67444983..497b34dc 100644 --- a/src/setup_module/gtps_pythonpreferencepage.cpp +++ b/src/setup_module/gtps_pythonpreferencepage.cpp @@ -172,7 +172,7 @@ void GtPythonPreferencePage::setStatusTextColor(const QColor& color) { auto palette = ui->lbPythonStatus->palette(); - palette.setColor(QPalette::Foreground, color); + palette.setColor(QPalette::WindowText, color); ui->lbPythonStatus->setPalette(palette); } diff --git a/tests/unittests/CMakeLists.txt b/tests/unittests/CMakeLists.txt index ab0bcc9e..994d9076 100644 --- a/tests/unittests/CMakeLists.txt +++ b/tests/unittests/CMakeLists.txt @@ -3,13 +3,13 @@ project(GTlabPython-UnitTests) -if (NOT TARGET Qt5::Core) - find_package(Qt5 REQUIRED COMPONENTS Core) -endif() - - list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake) +if (NOT TARGET Qt5::Core AND NOT TARGET Qt6::Core) + include(RequireQt) + require_qt(COMPONENTS Core) +endif() + if (NOT TARGET gtest) include(AddGoogleTest) endif() @@ -27,7 +27,7 @@ target_compile_definitions(GTlabPythonUnitTest PRIVATE GT_MODULE_ID="Python Unit Tests" ) -target_link_libraries(GTlabPythonUnitTest PRIVATE GTlab::Core GTlab::Python gtest Qt5::Core) +target_link_libraries(GTlabPythonUnitTest PRIVATE GTlab::Core GTlab::Python gtest Qt${QT_VERSION_MAJOR}::Core) include(GoogleTest) gtest_discover_tests(GTlabPythonUnitTest TEST_PREFIX "PythonModule." DISCOVERY_MODE PRE_TEST) diff --git a/tests/unittests/cmake/RequireQt.cmake b/tests/unittests/cmake/RequireQt.cmake new file mode 100644 index 00000000..8cacd12d --- /dev/null +++ b/tests/unittests/cmake/RequireQt.cmake @@ -0,0 +1,99 @@ +# SPDX-FileCopyrightText: 2025, German Aerospace Center (DLR) +# SPDX-License-Identifier: BSD-3-Clause + +# ============================================================================== +# require_qt( +# COMPONENTS ... +# ) +# +# Summary: +# Locate and configure a single Qt major version (5 or 6) for the entire +# build and ensure the requested components are available. +# +# Behavior: +# - Uses Qt’s “dual version” pattern: +# find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS ) +# find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS ) +# +# - On the first call, if QT_VERSION_MAJOR is NOT defined: +# * If Qt has already been found by the parent project +# (e.g. via find_package(QT ...) or find_package(Qt6/Qt5 ...)), +# QT_VERSION_MAJOR is taken from the existing Qt configuration or +# inferred from existing Qt6::*/Qt5::* targets. +# * Otherwise, require_qt() calls: +# find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS ) +# which sets QT_VERSION_MAJOR to 5 or 6 according to what CMake +# finds on the search path. +# * QT_VERSION_MAJOR is then cached so all subsequent calls reuse +# the same major version. +# +# - On subsequent calls (QT_VERSION_MAJOR already defined): +# * require_qt() simply calls: +# find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS ) +# to make sure the requested modules for the chosen major are available. +# +# Guarantees: +# - All callers (core and modules) in a single build tree use the same Qt +# major version, as determined by the first successful Qt detection. +# - If the requested Qt components for that major cannot be found, +# configuration fails with an error. +# +# User control of the chosen Qt version: +# - Standard CMake mechanisms apply. The user can steer which Qt is found by: +# * Adjusting CMAKE_PREFIX_PATH / Qt installation order (preferred), or +# * Setting Qt5_DIR / Qt6_DIR to point at a specific Qt installation +# +# Notes: +# - This function does NOT create versionless Qt:: targets. Callers are +# expected to link against versioned targets, e.g.: +# Qt${QT_VERSION_MAJOR}::Core +# Qt${QT_VERSION_MAJOR}::Widgets +# Qt${QT_VERSION_MAJOR}:: +# ============================================================================== +function(require_qt) + set(options) + set(oneValueArgs) + set(multiValueArgs COMPONENTS) + cmake_parse_arguments(RQT "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + if (NOT RQT_COMPONENTS) + message(FATAL_ERROR "require_qt() called without COMPONENTS") + endif() + + # -------------------------------------------------------- + # 1. Decide QT_VERSION_MAJOR once + # -------------------------------------------------------- + if (NOT DEFINED QT_VERSION_MAJOR) + + # 1a) If user hinted a specific major via Qt6_DIR / Qt5_DIR, respect that + if (DEFINED Qt6_DIR AND NOT DEFINED Qt5_DIR) + set(QT_VERSION_MAJOR 6) + elseif (DEFINED Qt5_DIR AND NOT DEFINED Qt6_DIR) + set(QT_VERSION_MAJOR 5) + + elseif (DEFINED Qt5_DIR AND DEFINED Qt6_DIR) + set(QT_VERSION_MAJOR 6) + else() + # 1b) No specific *_DIR hints: use the standard Qt dual-version pattern + # This will honor QT_DIR, CMAKE_PREFIX_PATH, etc. + find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS ${RQT_COMPONENTS}) + endif() + + # One-time status message + if (NOT DEFINED GTLAB_QT_VERSION_REPORTED AND DEFINED QT_VERSION_MAJOR) + message(STATUS "GTlab: using Qt${QT_VERSION_MAJOR} for this build") + set(GTLAB_QT_VERSION_REPORTED TRUE CACHE INTERNAL + "Whether the selected Qt version has been reported") + endif() + endif() + + if (NOT DEFINED QT_VERSION_MAJOR) + message(FATAL_ERROR + "require_qt(): QT_VERSION_MAJOR is still undefined after detection. " + "Check your Qt hints: QT_DIR, Qt5_DIR, Qt6_DIR, CMAKE_PREFIX_PATH.") + endif() + + # actually find qt + find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS ${RQT_COMPONENTS}) + set (QT_VERSION_MAJOR ${QT_VERSION_MAJOR} PARENT_SCOPE) +endfunction() \ No newline at end of file diff --git a/thirdparty/pythonqt/CMakeLists.txt b/thirdparty/pythonqt/CMakeLists.txt index 8c9b4245..858cd932 100644 --- a/thirdparty/pythonqt/CMakeLists.txt +++ b/thirdparty/pythonqt/CMakeLists.txt @@ -8,9 +8,15 @@ cmake_minimum_required(VERSION 3.15) project(PythonQt) +set(PythonQT_VERSION Qt6.5) + #---------------------------------------------------------------------------- -set(PythonQt_QT_VERSION 5) -set(PythonQT_VERSION 3.4.2) +set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}) + +include(RequireQt) +require_qt(COMPONENTS Core Widgets Network OpenGL Sql Svg UiTools Xml) + +set(PythonQt_QT_VERSION ${QT_VERSION_MAJOR}) set(CMAKE_AUTOMOC ON) @@ -18,17 +24,12 @@ set(CMAKE_AUTOMOC ON) set(minimum_required_qt5_version "5.3.0") set(minimum_required_qt_version ${minimum_required_qt${PythonQt_QT_VERSION}_version}) -find_package(Qt5 ${minimum_required_qt_version} QUIET) -set(QT_VERSION_MAJOR ${Qt5_VERSION_MAJOR}) -set(QT_VERSION_MINOR ${Qt5_VERSION_MINOR}) - - # Download pythonqt source code include(FetchContent) FetchContent_Declare( pythonqt - GIT_REPOSITORY https://github.com/MeVisLab/pythonqt.git - GIT_TAG v${PythonQT_VERSION} + GIT_REPOSITORY https://github.com/dlr-gtlab/pythonqt.git + GIT_TAG ${PythonQT_VERSION} ) if(NOT pythonqt_POPULATED) @@ -162,7 +163,7 @@ endif() list(REMOVE_DUPLICATES qt_required_components) message(STATUS "${PROJECT_NAME}: Required Qt components [${qt_required_components}]") -find_package(Qt5 ${minimum_required_qt_version} COMPONENTS ${qt_required_components} REQUIRED) +require_qt(COMPONENTS ${qt_required_components}) if(UNIX) find_package(OpenGL) @@ -175,18 +176,16 @@ endif() # The variable "generated_cpp_suffix" allows to conditionnally compile the generated wrappers # associated with the Qt version being used. -set(generated_cpp_suffix_52 _50) -set(generated_cpp_suffix_51 _50) +set(generated_cpp_suffix_515 _515) +set(generated_cpp_suffix_65 _65) set(generated_cpp_suffix "_${QT_VERSION_MAJOR}${QT_VERSION_MINOR}") if(DEFINED generated_cpp_suffix_${QT_VERSION_MAJOR}${QT_VERSION_MINOR}) set(generated_cpp_suffix "${generated_cpp_suffix_${QT_VERSION_MAJOR}${QT_VERSION_MINOR}}") -elseif("${QT_VERSION_MAJOR}.${QT_VERSION_MINOR}" VERSION_GREATER "5.10") - set(generated_cpp_suffix "_511") -elseif("${QT_VERSION_MAJOR}.${QT_VERSION_MINOR}" VERSION_GREATER "5.5") - set(generated_cpp_suffix "_56") -elseif("${QT_VERSION_MAJOR}.${QT_VERSION_MINOR}" VERSION_GREATER "5.3") - set(generated_cpp_suffix "_54") +elseif("${QT_VERSION_MAJOR}.${QT_VERSION_MINOR}" VERSION_GREATER "6.4") + set(generated_cpp_suffix "_65") +elseif("${QT_VERSION_MAJOR}.${QT_VERSION_MINOR}" VERSION_GREATER "5.14") + set(generated_cpp_suffix "_515") endif() #----------------------------------------------------------------------------- diff --git a/thirdparty/pythonqt/RequireQt.cmake b/thirdparty/pythonqt/RequireQt.cmake new file mode 100644 index 00000000..17384ceb --- /dev/null +++ b/thirdparty/pythonqt/RequireQt.cmake @@ -0,0 +1,100 @@ +# SPDX-FileCopyrightText: 2025, German Aerospace Center (DLR) +# SPDX-License-Identifier: BSD-3-Clause + +# ============================================================================== +# require_qt( +# COMPONENTS ... +# ) +# +# Summary: +# Locate and configure a single Qt major version (5 or 6) for the entire +# build and ensure the requested components are available. +# +# Behavior: +# - Uses Qt’s “dual version” pattern: +# find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS ) +# find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS ) +# +# - On the first call, if QT_VERSION_MAJOR is NOT defined: +# * If Qt has already been found by the parent project +# (e.g. via find_package(QT ...) or find_package(Qt6/Qt5 ...)), +# QT_VERSION_MAJOR is taken from the existing Qt configuration or +# inferred from existing Qt6::*/Qt5::* targets. +# * Otherwise, require_qt() calls: +# find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS ) +# which sets QT_VERSION_MAJOR to 5 or 6 according to what CMake +# finds on the search path. +# * QT_VERSION_MAJOR is then cached so all subsequent calls reuse +# the same major version. +# +# - On subsequent calls (QT_VERSION_MAJOR already defined): +# * require_qt() simply calls: +# find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS ) +# to make sure the requested modules for the chosen major are available. +# +# Guarantees: +# - All callers (core and modules) in a single build tree use the same Qt +# major version, as determined by the first successful Qt detection. +# - If the requested Qt components for that major cannot be found, +# configuration fails with an error. +# +# User control of the chosen Qt version: +# - Standard CMake mechanisms apply. The user can steer which Qt is found by: +# * Adjusting CMAKE_PREFIX_PATH / Qt installation order (preferred), or +# * Setting Qt5_DIR / Qt6_DIR to point at a specific Qt installation +# +# Notes: +# - This function does NOT create versionless Qt:: targets. Callers are +# expected to link against versioned targets, e.g.: +# Qt${QT_VERSION_MAJOR}::Core +# Qt${QT_VERSION_MAJOR}::Widgets +# Qt${QT_VERSION_MAJOR}:: +# ============================================================================== +function(require_qt) + set(options) + set(oneValueArgs) + set(multiValueArgs COMPONENTS) + cmake_parse_arguments(RQT "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + if (NOT RQT_COMPONENTS) + message(FATAL_ERROR "require_qt() called without COMPONENTS") + endif() + + # -------------------------------------------------------- + # 1. Decide QT_VERSION_MAJOR once + # -------------------------------------------------------- + if (NOT DEFINED QT_VERSION_MAJOR) + + # 1a) If user hinted a specific major via Qt6_DIR / Qt5_DIR, respect that + if (DEFINED Qt6_DIR AND NOT DEFINED Qt5_DIR) + set(QT_VERSION_MAJOR 6) + elseif (DEFINED Qt5_DIR AND NOT DEFINED Qt6_DIR) + set(QT_VERSION_MAJOR 5) + + elseif (DEFINED Qt5_DIR AND DEFINED Qt6_DIR) + set(QT_VERSION_MAJOR 6) + else() + # 1b) No specific *_DIR hints: use the standard Qt dual-version pattern + # This will honor QT_DIR, CMAKE_PREFIX_PATH, etc. + find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS ${RQT_COMPONENTS}) + endif() + + # One-time status message + if (NOT DEFINED GTLAB_QT_VERSION_REPORTED AND DEFINED QT_VERSION_MAJOR) + message(STATUS "GTlab: using Qt${QT_VERSION_MAJOR} for this build") + set(GTLAB_QT_VERSION_REPORTED TRUE CACHE INTERNAL + "Whether the selected Qt version has been reported") + endif() + endif() + + if (NOT DEFINED QT_VERSION_MAJOR) + message(FATAL_ERROR + "require_qt(): QT_VERSION_MAJOR is still undefined after detection. " + "Check your Qt hints: QT_DIR, Qt5_DIR, Qt6_DIR, CMAKE_PREFIX_PATH.") + endif() + + # actually find qt + find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS ${RQT_COMPONENTS}) + set (QT_VERSION_MAJOR ${QT_VERSION_MAJOR} PARENT_SCOPE) + set (QT_VERSION_MINOR ${Qt${QT_VERSION_MAJOR}_VERSION_MINOR} PARENT_SCOPE) +endfunction() \ No newline at end of file