diff --git a/CMakeLists.txt b/CMakeLists.txt index e63c2e645..fb2fdec98 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,7 @@ cmake_policy(SET CMP0072 NEW) # new in 3.11. The NEW behavior for this policy is cmake_policy(SET CMP0068 NEW) # new in 3.9. The NEW behavior of this policy is to ignore the RPATH settings for install_name on macOS. -project(QtNodesLibrary CXX) +project(QtNodesLibrary VERSION 3.0.16 LANGUAGES CXX) set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake" ${CMAKE_MODULE_PATH}) @@ -66,12 +66,14 @@ set(CPP_SOURCE_FILES src/AbstractGraphModel.cpp src/AbstractNodeGeometry.cpp src/BasicGraphicsScene.cpp + src/BasicGraphicsSceneGroups.cpp src/ConnectionGraphicsObject.cpp src/ConnectionState.cpp src/ConnectionStyle.cpp src/DataFlowGraphModel.cpp src/DataFlowGraphicsScene.cpp src/DefaultConnectionPainter.cpp + src/DefaultNodeGeometryBase.cpp src/DefaultHorizontalNodeGeometry.cpp src/DefaultNodePainter.cpp src/DefaultVerticalNodeGeometry.cpp @@ -82,9 +84,11 @@ set(CPP_SOURCE_FILES src/NodeDelegateModel.cpp src/NodeDelegateModelRegistry.cpp src/NodeGraphicsObject.cpp + src/NodeRenderingUtils.cpp src/NodeState.cpp src/NodeStyle.cpp src/StyleCollection.cpp + src/SerializationValidation.cpp src/UndoCommands.cpp src/locateNode.cpp src/GroupGraphicsObject.cpp @@ -100,6 +104,7 @@ set(HPP_HEADER_FILES include/QtNodes/internal/BasicGraphicsScene.hpp include/QtNodes/internal/Compiler.hpp include/QtNodes/internal/ConnectionGraphicsObject.hpp + include/QtNodes/internal/ConnectionIdIndex.hpp include/QtNodes/internal/ConnectionIdHash.hpp include/QtNodes/internal/ConnectionIdUtils.hpp include/QtNodes/internal/ConnectionState.hpp @@ -115,12 +120,14 @@ set(HPP_HEADER_FILES include/QtNodes/internal/NodeDelegateModel.hpp include/QtNodes/internal/NodeDelegateModelRegistry.hpp include/QtNodes/internal/NodeGraphicsObject.hpp + include/QtNodes/internal/NodeRenderingUtils.hpp include/QtNodes/internal/NodeState.hpp include/QtNodes/internal/NodeStyle.hpp include/QtNodes/internal/OperatingSystem.hpp include/QtNodes/internal/QStringStdHash.hpp include/QtNodes/internal/QUuidStdHash.hpp include/QtNodes/internal/Serializable.hpp + include/QtNodes/internal/SerializationValidation.hpp include/QtNodes/internal/Style.hpp include/QtNodes/internal/StyleCollection.hpp include/QtNodes/internal/DefaultConnectionPainter.hpp @@ -187,14 +194,14 @@ if(NOT "${CMAKE_CXX_SIMULATE_ID}" STREQUAL "MSVC") ) endif() -if(QT_NODES_DEVELOPER_DEFAULTS) - target_compile_features(QtNodes PUBLIC cxx_std_14) - set_target_properties(QtNodes PROPERTIES CXX_EXTENSIONS OFF) -endif() +target_compile_features(QtNodes PUBLIC cxx_std_14) +set_target_properties(QtNodes PROPERTIES CXX_EXTENSIONS OFF) set_target_properties(QtNodes PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION ${PROJECT_VERSION_MAJOR} ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin @@ -232,7 +239,6 @@ set(INSTALL_CONFIGDIR ${CMAKE_INSTALL_LIBDIR}/cmake/QtNodes) install(TARGETS QtNodes EXPORT QtNodesTargets - RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} @@ -254,7 +260,14 @@ configure_package_config_file(${CMAKE_CURRENT_LIST_DIR}/cmake/QtNodesConfig.cmak INSTALL_DESTINATION ${INSTALL_CONFIGDIR} ) +write_basic_package_version_file( + ${CMAKE_CURRENT_BINARY_DIR}/QtNodesConfigVersion.cmake + VERSION ${PROJECT_VERSION} + COMPATIBILITY SameMajorVersion +) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/QtNodesConfig.cmake + ${CMAKE_CURRENT_BINARY_DIR}/QtNodesConfigVersion.cmake DESTINATION ${INSTALL_CONFIGDIR} ) diff --git a/cmake/FindPySide2.cmake b/cmake/FindPySide2.cmake deleted file mode 100644 index 7ebb33d02..000000000 --- a/cmake/FindPySide2.cmake +++ /dev/null @@ -1,160 +0,0 @@ -# -# SPDX-FileCopyrightText: 2020-2021 Klarälvdalens Datakonsult AB, a KDAB Group company -# Author: Renato Araujo Oliveira Filho -# -# SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only -# -# Contact KDAB at for commercial licensing options. -# - -# PYSIDE_BASEDIR - Top of the PySide2 installation -# PYSIDE_INCLUDE_DIR - Directories to include to use PySide2 -# PYSIDE_LIBRARY - Files to link against to use PySide2 -# PYSIDE_TYPESYSTEMS - Type system files that should be used by other bindings extending PySide2 -# -# You can install PySide2 from Qt repository with -# pip3 install --index-url=https://download.qt.io/official_releases/QtForPython --trusted-host download.qt.io pyside2 - -find_package(PkgConfig) -if(PKG_CONFIG_FOUND) - pkg_check_modules(PYSIDE2_PRIV QUIET pyside2) -endif() - -set(PYSIDE2_FOUND FALSE) - -if(PYSIDE2_PRIV_FOUND) - set(PYSIDE2_FOUND TRUE) - message(STATUS "Using PySide2 found in the system!") - pkg_get_variable(SHIBOKEN_BINARY - pyside2 - generator_location - ) - pkg_get_variable(PYSIDE2_BASEDIR - pyside2 - typesystemdir - ) - pkg_get_variable(PYSIDE_INCLUDE_DIR - pyside2 - includedir - ) - set(PYSIDE_TYPESYSTEMS ${PYSIDE2_BASEDIR}) - set(PYSIDE2_SO_VERSION ${PYSIDE2_PRIV_VERSION}) - set(PYSIDE_LIBRARY ${PYSIDE2_PRIV_LINK_LIBRARIES}) - list(GET PYSIDE_LIBRARY 0 PYSIDE_LIBRARY) -else() - # extract python library basename - list(GET Python3_LIBRARIES 0 PYTHON_LIBRARY_FILENAME) - get_filename_component(PYTHON_LIBRARY_FILENAME ${PYTHON_LIBRARY_FILENAME} NAME) - - execute_process( - COMMAND ${Python3_EXECUTABLE} -c "if True: - import os, sys - try: - import PySide2.QtCore as QtCore - print(os.path.dirname(QtCore.__file__)) - except Exception as error: - print(error, file=sys.stderr) - exit() - " - OUTPUT_VARIABLE PYSIDE2_BASEDIR - OUTPUT_STRIP_TRAILING_WHITESPACE - ) - - if(PYSIDE2_BASEDIR) - set(PYSIDE_BASEDIR ${PYSIDE2_BASEDIR} CACHE PATH "Top level install of PySide2" FORCE) - execute_process( - COMMAND ${Python3_EXECUTABLE} -c "if True: - import os - import PySide2.QtCore as QtCore - print(os.path.basename(QtCore.__file__).split('.', 1)[1]) - " - OUTPUT_VARIABLE PYSIDE2_SUFFIX - OUTPUT_STRIP_TRAILING_WHITESPACE - ) - - execute_process( - COMMAND ${Python3_EXECUTABLE} -c "if True: - import os - import PySide2.QtCore as QtCore - print(';'.join(map(str, QtCore.__version_info__))) - " - OUTPUT_VARIABLE PYSIDE2_SO_VERSION - OUTPUT_STRIP_TRAILING_WHITESPACE - ) - list(GET PYSIDE2_SO_VERSION 0 PYSIDE2_SO_MACRO_VERSION) - list(GET PYSIDE2_SO_VERSION 1 PYSIDE2_SO_MICRO_VERSION) - list(GET PYSIDE2_SO_VERSION 2 PYSIDE2_SO_MINOR_VERSION) - string(REPLACE ";" "." PYSIDE2_SO_VERSION "${PYSIDE2_SO_VERSION}") - - if(NOT APPLE) - set(PYSIDE2_SUFFIX "${PYSIDE2_SUFFIX}.${PYSIDE2_SO_MACRO_VERSION}.${PYSIDE2_SO_MICRO_VERSION}") - else() - string(REPLACE ".so" "" PYSIDE2_SUFFIX ${PYSIDE2_SUFFIX}) - set(PYSIDE2_SUFFIX "${PYSIDE2_SUFFIX}.${PYSIDE2_SO_MACRO_VERSION}.${PYSIDE2_SO_MICRO_VERSION}.dylib") - endif() - - set(PYSIDE2_FOUND TRUE) - message(STATUS "PySide2 base dir: ${PYSIDE2_BASEDIR}" ) - message(STATUS "PySide2 suffix: ${PYSIDE2_SUFFIX}") - endif() - - if (PYSIDE2_FOUND) - #PySide - #=============================================================================== - find_path(PYSIDE_INCLUDE_DIR - pyside.h - PATHS ${PYSIDE2_BASEDIR}/include ${PYSIDE2_CUSTOM_PREFIX}/include/PySide2 - NO_DEFAULT_PATH) - - # Platform specific library names - if(MSVC) - SET(PYSIDE_LIBRARY_BASENAMES "pyside2.abi3.lib") - elseif(CYGWIN) - SET(PYSIDE_LIBRARY_BASENAMES "") - elseif(WIN32) - SET(PYSIDE_LIBRARY_BASENAMES "libpyside2.${PYSIDE2_SUFFIX}") - else() - SET(PYSIDE_LIBRARY_BASENAMES "libpyside2.${PYSIDE2_SUFFIX}") - endif() - - find_file(PYSIDE_LIBRARY - ${PYSIDE_LIBRARY_BASENAMES} - PATHS ${PYSIDE2_BASEDIR} ${PYSIDE2_CUSTOM_PREFIX}/lib - NO_DEFAULT_PATH) - - find_path(PYSIDE_TYPESYSTEMS - typesystem_core.xml - PATHS ${PYSIDE2_BASEDIR}/typesystems ${PYSIDE2_CUSTOM_PREFIX}/share/PySide2/typesystems - NO_DEFAULT_PATH) - endif() -endif() - -if(PYSIDE2_FOUND) - message(STATUS "PySide include dir: ${PYSIDE_INCLUDE_DIR}") - message(STATUS "PySide library: ${PYSIDE_LIBRARY}") - message(STATUS "PySide typesystems: ${PYSIDE_TYPESYSTEMS}") - message(STATUS "PySide2 version: ${PYSIDE2_SO_VERSION}") - - # Create PySide2 target - add_library(PySide2::pyside2 SHARED IMPORTED GLOBAL) - if(MSVC) - set_property(TARGET PySide2::pyside2 PROPERTY - IMPORTED_IMPLIB ${PYSIDE_LIBRARY}) - endif() - set_property(TARGET PySide2::pyside2 PROPERTY - IMPORTED_LOCATION ${PYSIDE_LIBRARY}) - set_property(TARGET PySide2::pyside2 APPEND PROPERTY - INTERFACE_INCLUDE_DIRECTORIES - ${PYSIDE_INCLUDE_DIR} - ${PYSIDE_INCLUDE_DIR}/QtCore/ - ${PYSIDE_INCLUDE_DIR}/QtGui/ - ${PYSIDE_INCLUDE_DIR}/QtWidgets/ - ${Python3_INCLUDE_DIRS} - ) -endif() - - -find_package_handle_standard_args(PySide2 - REQUIRED_VARS PYSIDE2_BASEDIR PYSIDE_INCLUDE_DIR PYSIDE_LIBRARY PYSIDE_TYPESYSTEMS - VERSION_VAR PYSIDE2_SO_VERSION -) diff --git a/cmake/FindShiboken2.cmake b/cmake/FindShiboken2.cmake deleted file mode 100644 index b86e6ad14..000000000 --- a/cmake/FindShiboken2.cmake +++ /dev/null @@ -1,187 +0,0 @@ -# -# SPDX-FileCopyrightText: 2020-2021 Klarälvdalens Datakonsult AB, a KDAB Group company -# Author: Renato Araujo Oliveira Filho -# -# SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only -# -# Contact KDAB at for commercial licensing options. -# - -# SHIBOKEN_INCLUDE_DIR - Directories to include to use SHIBOKEN -# SHIBOKEN_LIBRARY - Files to link against to use SHIBOKEN -# SHIBOKEN_BINARY - Executable name -# SHIBOKEN_BUILD_TYPE - Tells if Shiboken was compiled in Release or Debug mode. - -# You can install Shiboken from Qt repository with -# pip3 install --index-url=https://download.qt.io/official_releases/QtForPython --trusted-host download.qt.io shiboken2-generator - -find_package(PkgConfig) -if(PKG_CONFIG_FOUND) - pkg_check_modules(SHIBOKEN2_PRIV QUIET shiboken2) -endif() - -set(SHIBOKEN_FOUND FALSE) - -if(SHIBOKEN2_PRIV_FOUND) - set(SHIBOKEN_FOUND TRUE) - message(STATUS "Using shiboken found in the system!") - pkg_get_variable(SHIBOKEN_BINARY - shiboken2 - generator_location - ) - pkg_get_variable(SHIBOKEN_BASEDIR - shiboken2 - libdir - ) - pkg_get_variable(SHIBOKEN_INCLUDE_DIR - shiboken2 - includedir - ) - set(SHIBOKEN_VERSION ${SHIBOKEN2_PRIV_VERSION}) - set(SHIBOKEN_LIBRARY ${SHIBOKEN2_PRIV_LINK_LIBRARIES}) -else() - execute_process( - COMMAND ${Python3_EXECUTABLE} -c "if True: - import os - try: - import shiboken2_generator - print(shiboken2_generator.__path__[0]) - except: - exit() - " - OUTPUT_VARIABLE SHIBOKEN_GENERATOR_BASEDIR - OUTPUT_STRIP_TRAILING_WHITESPACE - ) - execute_process( - COMMAND ${Python3_EXECUTABLE} -c "if True: - import os - try: - import shiboken2 - print(shiboken2.__path__[0]) - except: - exit() - " - OUTPUT_VARIABLE SHIBOKEN_BASEDIR - OUTPUT_STRIP_TRAILING_WHITESPACE - ) - execute_process( - COMMAND ${Python3_EXECUTABLE} -c "if True: - import os - import shiboken2 - print(';'.join(filter(None, map(str, shiboken2.__version_info__)))) - " - OUTPUT_VARIABLE SHIBOKEN_VERSION - OUTPUT_STRIP_TRAILING_WHITESPACE - ) - list(GET SHIBOKEN_VERSION 0 SHIBOKEN_MACRO_VERSION) - list(GET SHIBOKEN_VERSION 1 SHIBOKEN_MICRO_VERSION) - list(GET SHIBOKEN_VERSION 2 SHIBOKEN_MINOR_VERSION) - string(REPLACE ";" "." SHIBOKEN_VERSION "${SHIBOKEN_VERSION}") - - message(STATUS "ShibokenGenerator base dir: ${SHIBOKEN_GENERATOR_BASEDIR}") - message(STATUS "Shiboken base dir: ${SHIBOKEN_BASEDIR}") - message(STATUS "Shiboken custom path: ${SHIBOKEN_CUSTOM_PATH}") - - if(SHIBOKEN_BASEDIR) - find_path(SHIBOKEN_INCLUDE_DIR - shiboken.h - PATHS ${SHIBOKEN_CUSTOM_PATH} ${SHIBOKEN_GENERATOR_BASEDIR}/include - NO_DEFAULT_PATH) - if(MSVC) - SET(SHIBOKEN_LIBRARY_BASENAMES "shiboken2.abi3.lib") - elseif(CYGWIN) - SET(SHIBOKEN_LIBRARY_BASENAMES "") - elseif(WIN32) - SET(SHIBOKEN_LIBRARY_BASENAMES "libshiboken2.${PYSIDE2_SUFFIX}") - elseif(APPLE) - SET(SHIBOKEN_LIBRARY_BASENAMES - libshiboken2.abi3.dylib - libshiboken2.abi3.${SHIBOKEN_MACRO_VERSION}.dylib - libshiboken2.abi3.${SHIBOKEN_MACRO_VERSION}.${SHIBOKEN_MICRO_VERSION}.dylib - libshiboken2.abi3.${SHIBOKEN_VERSION}.dylib - ) - else() - SET(SHIBOKEN_LIBRARY_BASENAMES - libshiboken2.abi3.so - libshiboken2.abi3.so.${SHIBOKEN_MACRO_VERSION} - libshiboken2.abi3.so.${SHIBOKEN_MACRO_VERSION}.${SHIBOKEN_MICRO_VERSION} - libshiboken2.abi3.so.${SHIBOKEN_VERSION} - ) - endif() - - if (NOT SHIBOKEN_INCLUDE_DIR) - return() - endif() - set(SHIBOKEN_SEARCH_PATHS ${SHIBOKEN_CUSTOM_PATH}) - list(APPEND SHIBOKEN_SEARCH_PATHS ${SHIBOKEN_BASEDIR}) - list(APPEND SHIBOKEN_SEARCH_PATHS ${SHIBOKEN_GENERATOR_BASEDIR}) - find_file(SHIBOKEN_LIBRARY - ${SHIBOKEN_LIBRARY_BASENAMES} - PATHS ${SHIBOKEN_SEARCH_PATHS} - NO_DEFAULT_PATH) - - find_program(SHIBOKEN_BINARY - shiboken2 - PATHS ${SHIBOKEN_SEARCH_PATHS} - NO_DEFAULT_PATH - ) - endif() - if (SHIBOKEN_INCLUDE_DIR AND SHIBOKEN_LIBRARY AND SHIBOKEN_BINARY) - set(SHIBOKEN_FOUND TRUE) - endif() - - if(SHIBOKEN_FOUND) - endif() - - - if(MSVC) - # On Windows we must link to python3.dll that is a small library that links against python3x.dll - # that allow us to choose any python3x.dll at runtime - execute_process( - COMMAND ${Python3_EXECUTABLE} -c "if True: - for lib in '${Python3_LIBRARIES}'.split(';'): - if '/' in lib: - prefix, py = lib.rsplit('/', 1) - if py.startswith('python3'): - print(prefix + '/python3.lib') - break - " - OUTPUT_VARIABLE PYTHON_LIMITED_LIBRARIES - OUTPUT_STRIP_TRAILING_WHITESPACE - ) - else() - # On Linux and MacOs our modules should not link with any python library - # that must be handled by the main process - set(PYTHON_LIMITED_LIBRARIES "") - endif() -endif() -if (SHIBOKEN_FOUND) - message(STATUS "Shiboken include dir: ${SHIBOKEN_INCLUDE_DIR}") - message(STATUS "Shiboken library: ${SHIBOKEN_LIBRARY}") - message(STATUS "Shiboken binary: ${SHIBOKEN_BINARY}") - message(STATUS "Shiboken version: ${SHIBOKEN_VERSION}") - - # Create shiboke2 target - add_library(Shiboken2::libshiboken SHARED IMPORTED GLOBAL) - if(MSVC) - set_property(TARGET Shiboken2::libshiboken PROPERTY - IMPORTED_IMPLIB ${SHIBOKEN_LIBRARY}) - endif() - set_property(TARGET Shiboken2::libshiboken PROPERTY - IMPORTED_LOCATION ${SHIBOKEN_LIBRARY}) - set_property(TARGET Shiboken2::libshiboken APPEND PROPERTY - INTERFACE_INCLUDE_DIRECTORIES ${SHIBOKEN_INCLUDE_DIR} ${Python3_INCLUDE_DIRS}) - set_property(TARGET Shiboken2::libshiboken APPEND PROPERTY - INTERFACE_LINK_LIBRARIES ${PYTHON_LIMITED_LIBRARIES}) - - # Generator target - add_executable(Shiboken2::shiboken IMPORTED GLOBAL) - set_property(TARGET Shiboken2::shiboken PROPERTY - IMPORTED_LOCATION ${SHIBOKEN_BINARY}) -endif() - -find_package_handle_standard_args(Shiboken2 - REQUIRED_VARS SHIBOKEN_BASEDIR SHIBOKEN_INCLUDE_DIR SHIBOKEN_LIBRARY SHIBOKEN_BINARY - VERSION_VAR SHIBOKEN_VERSION -) - diff --git a/cmake/PySide2ModuleBuild.cmake b/cmake/PySide2ModuleBuild.cmake deleted file mode 100644 index 06f2b620e..000000000 --- a/cmake/PySide2ModuleBuild.cmake +++ /dev/null @@ -1,157 +0,0 @@ -# -# SPDX-FileCopyrightText: 2020-2021 Klarälvdalens Datakonsult AB, a KDAB Group company -# Author: Renato Araujo Oliveira Filho -# -# SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only -# -# Contact KDAB at for commercial licensing options. -# - -if (NOT ${PROJECT_NAME}_PYTHON_BINDINGS_INSTALL_PREFIX) - SET(${PROJECT_NAME}_PYTHON_BINDINGS_INSTALL_PREFIX ${CMAKE_INSTALL_PREFIX} CACHE FILEPATH "Custom path to install python bindings.") -endif() - -message(STATUS "PYTHON INSTALL PREFIX ${${PROJECT_NAME}_PYTHON_BINDINGS_INSTALL_PREFIX}") - -if (WIN32) - set(PATH_SEP "\;") -else() - set(PATH_SEP ":") -endif() -if (NOT CMAKE_CXX_STANDARD) - set(CMAKE_CXX_STANDARD 17) -endif() - -# On macOS, check if Qt is a framework build. This affects how include paths should be handled. -get_target_property(QtCore_is_framework Qt5::Core FRAMEWORK) -if (QtCore_is_framework) - # Get the path to the framework dir. - list(GET Qt5Core_INCLUDE_DIRS 0 QT_INCLUDE_DIR) - get_filename_component(QT_FRAMEWORK_INCLUDE_DIR "${QT_INCLUDE_DIR}/../" ABSOLUTE) - - # QT_INCLUDE_DIR points to the QtCore.framework directory, so we need to adjust this to point - # to the actual include directory, which has include files for non-framework parts of Qt. - get_filename_component(QT_INCLUDE_DIR "${QT_INCLUDE_DIR}/../../include" ABSOLUTE) -endif() - -# Flags that we will pass to shiboken-generator -# --generator-set=shiboken: tells the generator that we want to use shiboken to generate code, -# a doc generator is also available -# --enable-parent-ctor-heuristic: Enable heuristics to detect parent relationship on constructors, -# this try to guess parent ownership based on the arguments of the constructors -# --enable-pyside-extensionsL: This will generate code for Qt based classes, adding extra attributes, -# like signal, slot; -# --enable-return-value-heuristic: Similar as --enable-parent-ctor-heuristic this use some logic to guess -# parent child relationship based on the returned argument -# --use-isnull-as-nb_nonzero: If a class have an isNull() const method, it will be used to compute -# the value of boolean casts. -# Example, QImage::isNull() will be used when on python side you do `if (myQImage)` -set(GENERATOR_EXTRA_FLAGS --generator-set=shiboken - --enable-parent-ctor-heuristic - --enable-pyside-extensions - --enable-return-value-heuristic - --use-isnull-as-nb_nonzero - -std=c++${CMAKE_CXX_STANDARD}) - -# 2017-04-24 The protected hack can unfortunately not be disabled, because -# Clang does produce linker errors when we disable the hack. -# But the ugly workaround in Python is replaced by a shiboken change. -if(WIN32 OR DEFINED AVOID_PROTECTED_HACK) - set(GENERATOR_EXTRA_FLAGS ${GENERATOR_EXTRA_FLAGS} --avoid-protected-hack) - add_definitions(-DAVOID_PROTECTED_HACK) -endif() - -macro(make_path varname) - # accepts any number of path variables - string(REPLACE ";" "${PATH_SEP}" ${varname} "${ARGN}") -endmacro() - -# Creates a PySide module target based on the arguments -# This will: -# 1 - Create a Cmake custom-target that call shiboken-generator passign the correct arguments -# 2 - Create a Cmake library target called "Py${LIBRARY_NAME}" the output name of this target -# will be changed to match PySide template -# Args: -# LIBRARY_NAME - The name of the output module -# TYPESYSTEM_PATHS - A list of paths where shiboken should look for typesystem files -# INCLUDE_PATHS - Include pahts necessary to parse your class. *This is not the same as build* -# OUTPUT_SOURCES - The files that will be generated by shiboken -# TARGET_INCLUDE_DIRS - This will be passed to target_include_directories -# TARGET_LINK_LIBRARIES - This will be passed to target_link_libraries -# GLOBAL_INCLUDE - A header-file that contains alls classes that will be generated -# TYPESYSTEM_XML - The target binding typesystem (that should be the full path) -# DEPENDS - This var will be passed to add_custom_command(DEPENDS) so a new generation will be -# trigger if one of these files changes -# MODULE_OUTPUT_DIR - Where the library file should be stored -macro(CREATE_PYTHON_BINDINGS - LIBRARY_NAME - TYPESYSTEM_PATHS - INCLUDE_PATHS - OUTPUT_SOURCES - TARGET_INCLUDE_DIRS - TARGET_LINK_LIBRARIES - GLOBAL_INCLUDE - TYPESYSTEM_XML - DEPENDS - MODULE_OUTPUT_DIR) - - # Transform the path separators into something shiboken understands. - make_path(shiboken_include_dirs ${INCLUDE_PATHS}) - make_path(shiboken_typesystem_dirs ${TYPESYSTEM_PATHS}) - get_property(raw_python_dir_include_dirs DIRECTORY PROPERTY INCLUDE_DIRECTORIES) - make_path(python_dir_include_dirs ${raw_python_dir_include_dirs}) - set(shiboken_include_dirs "${shiboken_include_dirs}${PATH_SEP}${python_dir_include_dirs}") - - set(shiboken_framework_include_dirs_option "") - if(CMAKE_HOST_APPLE) - set(shiboken_framework_include_dirs "${QT_FRAMEWORK_INCLUDE_DIR}") - make_path(shiboken_framework_include_dirs ${shiboken_framework_include_dirs}) - set(shiboken_framework_include_dirs_option "--framework-include-paths=${shiboken_framework_include_dirs}") - endif() - set_property(SOURCE ${OUTPUT_SOURCES} PROPERTY SKIP_AUTOGEN ON) - add_custom_command(OUTPUT ${OUTPUT_SOURCES} - COMMAND $ ${GENERATOR_EXTRA_FLAGS} - ${GLOBAL_INCLUDE} - --include-paths=${shiboken_include_dirs} - --typesystem-paths=${shiboken_typesystem_dirs} - ${shiboken_framework_include_dirs_option} - --output-directory=${CMAKE_CURRENT_BINARY_DIR} - ${TYPESYSTEM_XML} - DEPENDS ${TYPESYSTEM_XML} ${DEPENDS} - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - COMMENT "Running generator for ${LIBRARY_NAME} binding...") - - set(TARGET_NAME "Py${LIBRARY_NAME}") - set(MODULE_NAME "${LIBRARY_NAME}") - add_library(${TARGET_NAME} MODULE ${OUTPUT_SOURCES}) - - set_target_properties(${TARGET_NAME} PROPERTIES - PREFIX "" - OUTPUT_NAME ${MODULE_NAME} - LIBRARY_OUTPUT_DIRECTORY ${MODULE_OUTPUT_DIR} - ) - - if(WIN32) - set_target_properties(${TARGET_NAME} PROPERTIES SUFFIX ".pyd") - endif() - - target_include_directories(${TARGET_NAME} PUBLIC - ${TARGET_INCLUDE_DIRS} - ${PYSIDE_EXTRA_INCLUDES} - ) - - target_link_libraries(${TARGET_NAME} - ${TARGET_LINK_LIBRARIES} - PySide2::pyside2 - Shiboken2::libshiboken - ) - target_compile_definitions(${TARGET_NAME} - PRIVATE Py_LIMITED_API=0x03050000 - ) - if(APPLE) - set_property(TARGET ${TARGET_NAME} APPEND PROPERTY - LINK_FLAGS "-undefined dynamic_lookup") - endif() - install(TARGETS ${TARGET_NAME} - LIBRARY DESTINATION ${${PROJECT_NAME}_PYTHON_BINDINGS_INSTALL_PREFIX}/${TARGET_NAME}) -endmacro() diff --git a/docs/api/index.rst b/docs/api/index.rst index 9c041d0aa..577b4e7f7 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -98,7 +98,7 @@ Quick Reference - Type, Position, Size, Caption, CaptionVisible, Style, InternalData, InPortCount, OutPortCount, Widget, ValidationState, ProcessingStatus * - ``PortRole`` - - Data, DataType, ConnectionPolicyRole, CaptionVisible, Caption + - Data, DataType, ConnectionPolicy, CaptionVisible, Caption * - ``NodeFlag`` - NoFlags, Resizable, Locked * - ``NodeValidationState::State`` diff --git a/docs/getting-started/concepts.rst b/docs/getting-started/concepts.rst index cfd7691df..d178e7cbb 100644 --- a/docs/getting-started/concepts.rst +++ b/docs/getting-started/concepts.rst @@ -131,7 +131,7 @@ Key roles: ``Type``, ``Position``, ``Caption``, ``InPortCount``, ``OutPortCount` // Get port data type auto type = model.portData(nodeId, PortType::In, 0, PortRole::DataType); -Key roles: ``Data``, ``DataType``, ``Caption``, ``ConnectionPolicyRole`` +Key roles: ``Data``, ``DataType``, ``Caption``, ``ConnectionPolicy`` See :doc:`/guide/graph-models` for the complete role reference. diff --git a/docs/getting-started/quickstart.rst b/docs/getting-started/quickstart.rst index fee0b8830..dd9be189a 100644 --- a/docs/getting-started/quickstart.rst +++ b/docs/getting-started/quickstart.rst @@ -39,10 +39,10 @@ First, we need a class to store our graph data. Create ``SimpleGraphModel.hpp``: QtNodes::NodeId newNodeId() override { return _nextId++; } // Required: Return all node IDs - std::unordered_set allNodeIds() const override; + NodeIdSet const &allNodeIds() const override; // Required: Return all connections for a node - std::unordered_set allConnectionIds(QtNodes::NodeId) const override; + ConnectionIdSet const &allConnectionIds(QtNodes::NodeId) const override; // ... (see full implementation in examples/simple_graph_model) diff --git a/docs/guide/advanced.rst b/docs/guide/advanced.rst index 601e4ef0c..137ff350e 100644 --- a/docs/guide/advanced.rst +++ b/docs/guide/advanced.rst @@ -199,7 +199,7 @@ Control how many connections a port accepts: QVariant MyModel::portData(NodeId nodeId, PortType portType, PortIndex portIndex, PortRole role) const override { - if (role == PortRole::ConnectionPolicyRole) { + if (role == PortRole::ConnectionPolicy) { if (portType == PortType::In) { // Inputs accept only one connection return QVariant::fromValue(ConnectionPolicy::One); diff --git a/docs/guide/graph-models.rst b/docs/guide/graph-models.rst index 3ce60bbc2..6daf0d3ef 100644 --- a/docs/guide/graph-models.rst +++ b/docs/guide/graph-models.rst @@ -24,14 +24,14 @@ Your model must implement these pure virtual methods: NodeId newNodeId() override; // Node queries - std::unordered_set allNodeIds() const override; + NodeIdSet const &allNodeIds() const override; bool nodeExists(NodeId) const override; QVariant nodeData(NodeId, NodeRole) const override; bool setNodeData(NodeId, NodeRole, QVariant) override; // Connection queries - std::unordered_set allConnectionIds(NodeId) const override; - std::unordered_set connections(NodeId, PortType, PortIndex) const override; + ConnectionIdSet const &allConnectionIds(NodeId) const override; + ConnectionIdSet const &connections(NodeId, PortType, PortIndex) const override; bool connectionExists(ConnectionId) const override; bool connectionPossible(ConnectionId) const override; @@ -89,7 +89,13 @@ Remove connections first, then the node: return false; // Remove all connections involving this node - for (auto& conn : allConnectionIds(nodeId)) { + std::vector attachedConnections; + auto const &connections = allConnectionIds(nodeId); + attachedConnections.reserve(connections.size()); + for (auto const &conn : connections) { + attachedConnections.push_back(conn); + } + for (auto const &conn : attachedConnections) { deleteConnection(conn); } @@ -183,23 +189,10 @@ Return connections filtered by node and port: .. code-block:: cpp - std::unordered_set + AbstractGraphModel::ConnectionIdSet const & MyGraphModel::connections(NodeId nodeId, PortType portType, PortIndex portIndex) const { - std::unordered_set result; - for (const auto& conn : _connections) { - if (portType == PortType::In && - conn.inNodeId == nodeId && - conn.inPortIndex == portIndex) { - result.insert(conn); - } - else if (portType == PortType::Out && - conn.outNodeId == nodeId && - conn.outPortIndex == portIndex) { - result.insert(conn); - } - } - return result; + return _connectionIndex.connections(nodeId, portType, portIndex); } **Connection Validation** @@ -244,9 +237,16 @@ Implement ``portData()`` for port-specific information: * - ``DataType`` - ``NodeDataType`` - Type descriptor for compatibility checks - * - ``ConnectionPolicyRole`` + * - ``ConnectionPolicy`` - ``ConnectionPolicy`` - ``One`` (single connection) or ``Many`` + +.. note:: + + ``allNodeIds()``, ``allConnectionIds()``, and ``connections()`` return + references to storage owned by the model. Implementations must keep those + containers alive for the duration of the call site rather than constructing + and returning temporaries. * - ``Caption`` - ``QString`` - Port label text diff --git a/examples/calculator/DivisionModel.hpp b/examples/calculator/DivisionModel.hpp index 1b7865c7c..bf97fd106 100644 --- a/examples/calculator/DivisionModel.hpp +++ b/examples/calculator/DivisionModel.hpp @@ -55,27 +55,25 @@ class DivisionModel : public MathOperationDataModel auto n1 = _number1.lock(); auto n2 = _number2.lock(); - QtNodes::NodeValidationState state; if (n2 && (n2->number() == 0.0)) { - state._state = QtNodes::NodeValidationState::State::Error; - state._stateMessage = QStringLiteral("Division by zero error"); - setValidationState(state); + setValidationState(QtNodes::NodeValidationState( + QtNodes::NodeValidationState::State::Error, + QStringLiteral("Division by zero error"))); _result.reset(); - } else if ( n2 && (n2->number() < 1e-5)) { - state._state = QtNodes::NodeValidationState::State::Warning; - state._stateMessage = QStringLiteral("Very small divident. Result might overflow"); - setValidationState(state); + } else if (n2 && (n2->number() < 1e-5)) { + setValidationState(QtNodes::NodeValidationState( + QtNodes::NodeValidationState::State::Warning, + QStringLiteral("Very small divident. Result might overflow"))); if (n1) { _result = std::make_shared(n1->number() / n2->number()); } else { _result.reset(); } } else if (n1 && n2) { - setValidationState(state); + setValidationState(QtNodes::NodeValidationState()); _result = std::make_shared(n1->number() / n2->number()); } else { - QtNodes::NodeValidationState state; - setValidationState(state); + setValidationState(QtNodes::NodeValidationState()); _result.reset(); } diff --git a/examples/custom_painter/SimpleGraphModel.cpp b/examples/custom_painter/SimpleGraphModel.cpp index 760cea69c..c041ce8ce 100644 --- a/examples/custom_painter/SimpleGraphModel.cpp +++ b/examples/custom_painter/SimpleGraphModel.cpp @@ -1,50 +1,33 @@ #include "SimpleGraphModel.hpp" +#include + SimpleGraphModel::SimpleGraphModel() : _nextNodeId{0} {} SimpleGraphModel::~SimpleGraphModel() {} -std::unordered_set SimpleGraphModel::allNodeIds() const +QtNodes::AbstractGraphModel::NodeIdSet const &SimpleGraphModel::allNodeIds() const { return _nodeIds; } -std::unordered_set SimpleGraphModel::allConnectionIds(NodeId const nodeId) const +QtNodes::AbstractGraphModel::ConnectionIdSet const & +SimpleGraphModel::allConnectionIds(NodeId const nodeId) const { - std::unordered_set result; - - std::copy_if(_connectivity.begin(), - _connectivity.end(), - std::inserter(result, std::end(result)), - [&nodeId](ConnectionId const &cid) { - return cid.inNodeId == nodeId || cid.outNodeId == nodeId; - }); - - return result; + return _connectionIndex.allConnectionIds(nodeId); } -std::unordered_set SimpleGraphModel::connections(NodeId nodeId, - PortType portType, - PortIndex portIndex) const +QtNodes::AbstractGraphModel::ConnectionIdSet const & +SimpleGraphModel::connections(NodeId nodeId, PortType portType, PortIndex portIndex) const { - std::unordered_set result; - - std::copy_if(_connectivity.begin(), - _connectivity.end(), - std::inserter(result, std::end(result)), - [&portType, &portIndex, &nodeId](ConnectionId const &cid) { - return (getNodeId(portType, cid) == nodeId - && getPortIndex(portType, cid) == portIndex); - }); - - return result; + return _connectionIndex.connections(nodeId, portType, portIndex); } bool SimpleGraphModel::connectionExists(ConnectionId const connectionId) const { - return (_connectivity.find(connectionId) != _connectivity.end()); + return _connectionIndex.contains(connectionId); } NodeId SimpleGraphModel::addNode(QString const nodeType) @@ -59,12 +42,12 @@ NodeId SimpleGraphModel::addNode(QString const nodeType) bool SimpleGraphModel::connectionPossible(ConnectionId const connectionId) const { - return _connectivity.find(connectionId) == _connectivity.end(); + return !_connectionIndex.contains(connectionId); } void SimpleGraphModel::addConnection(ConnectionId const connectionId) { - _connectivity.insert(connectionId); + _connectionIndex.add(connectionId); Q_EMIT connectionCreated(connectionId); } @@ -161,7 +144,7 @@ QVariant SimpleGraphModel::portData(NodeId nodeId, case PortRole::DataType: return QVariant(); - case PortRole::ConnectionPolicyRole: + case PortRole::ConnectionPolicy: return QVariant::fromValue(ConnectionPolicy::One); case PortRole::CaptionVisible: @@ -188,14 +171,7 @@ bool SimpleGraphModel::setPortData( bool SimpleGraphModel::deleteConnection(ConnectionId const connectionId) { - bool disconnected = false; - - auto it = _connectivity.find(connectionId); - - if (it != _connectivity.end()) { - disconnected = true; - _connectivity.erase(it); - } + bool const disconnected = _connectionIndex.remove(connectionId); if (disconnected) Q_EMIT connectionDeleted(connectionId); @@ -205,9 +181,14 @@ bool SimpleGraphModel::deleteConnection(ConnectionId const connectionId) bool SimpleGraphModel::deleteNode(NodeId const nodeId) { - auto connectionIds = allConnectionIds(nodeId); + std::vector connectionIds; + auto const &attachedConnections = allConnectionIds(nodeId); + connectionIds.reserve(attachedConnections.size()); + for (auto const &connectionId : attachedConnections) { + connectionIds.push_back(connectionId); + } - for (auto &cId : connectionIds) { + for (auto const &cId : connectionIds) { deleteConnection(cId); } diff --git a/examples/custom_painter/SimpleGraphModel.hpp b/examples/custom_painter/SimpleGraphModel.hpp index fccf99cbb..1bb4c5639 100644 --- a/examples/custom_painter/SimpleGraphModel.hpp +++ b/examples/custom_painter/SimpleGraphModel.hpp @@ -5,9 +5,12 @@ #include #include +#include #include #include +#include + using ConnectionId = QtNodes::ConnectionId; using ConnectionPolicy = QtNodes::ConnectionPolicy; using NodeFlag = QtNodes::NodeFlag; @@ -35,13 +38,13 @@ class SimpleGraphModel : public QtNodes::AbstractGraphModel ~SimpleGraphModel() override; - std::unordered_set allNodeIds() const override; + NodeIdSet const &allNodeIds() const override; - std::unordered_set allConnectionIds(NodeId const nodeId) const override; + ConnectionIdSet const &allConnectionIds(NodeId const nodeId) const override; - std::unordered_set connections(NodeId nodeId, - PortType portType, - PortIndex portIndex) const override; + ConnectionIdSet const &connections(NodeId nodeId, + PortType portType, + PortIndex portIndex) const override; bool connectionExists(ConnectionId const connectionId) const override; @@ -79,8 +82,8 @@ class SimpleGraphModel : public QtNodes::AbstractGraphModel NodeId newNodeId() override { return _nextNodeId++; } private: - std::unordered_set _nodeIds; - std::unordered_set _connectivity; + NodeIdSet _nodeIds; + QtNodes::ConnectionIdIndex _connectionIndex; mutable std::unordered_map _nodeGeometryData; NodeId _nextNodeId; }; diff --git a/examples/dynamic_ports/DynamicPortsModel.cpp b/examples/dynamic_ports/DynamicPortsModel.cpp index 74288e7a2..f3b17a18c 100644 --- a/examples/dynamic_ports/DynamicPortsModel.cpp +++ b/examples/dynamic_ports/DynamicPortsModel.cpp @@ -7,51 +7,33 @@ #include #include +#include DynamicPortsModel::DynamicPortsModel() : _nextNodeId{0} {} -std::unordered_set DynamicPortsModel::allNodeIds() const +QtNodes::AbstractGraphModel::NodeIdSet const &DynamicPortsModel::allNodeIds() const { return _nodeIds; } -std::unordered_set DynamicPortsModel::allConnectionIds(NodeId const nodeId) const +QtNodes::AbstractGraphModel::ConnectionIdSet const & +DynamicPortsModel::allConnectionIds(NodeId const nodeId) const { - std::unordered_set result; - - std::copy_if(_connectivity.begin(), - _connectivity.end(), - std::inserter(result, std::end(result)), - [&nodeId](ConnectionId const &cid) { - return cid.inNodeId == nodeId || cid.outNodeId == nodeId; - }); - - return result; + return _connectionIndex.allConnectionIds(nodeId); } -std::unordered_set DynamicPortsModel::connections(NodeId nodeId, - PortType portType, - PortIndex portIndex) const +QtNodes::AbstractGraphModel::ConnectionIdSet const & +DynamicPortsModel::connections(NodeId nodeId, PortType portType, PortIndex portIndex) const { - std::unordered_set result; - - std::copy_if(_connectivity.begin(), - _connectivity.end(), - std::inserter(result, std::end(result)), - [&portType, &portIndex, &nodeId](ConnectionId const &cid) { - return (getNodeId(portType, cid) == nodeId - && getPortIndex(portType, cid) == portIndex); - }); - - return result; + return _connectionIndex.connections(nodeId, portType, portIndex); } bool DynamicPortsModel::connectionExists(ConnectionId const connectionId) const { - return (_connectivity.find(connectionId) != _connectivity.end()); + return _connectionIndex.contains(connectionId); } NodeId DynamicPortsModel::addNode(QString const nodeType) @@ -68,12 +50,12 @@ NodeId DynamicPortsModel::addNode(QString const nodeType) bool DynamicPortsModel::connectionPossible(ConnectionId const connectionId) const { - return !connectionExists(connectionId); + return !_connectionIndex.contains(connectionId); } void DynamicPortsModel::addConnection(ConnectionId const connectionId) { - _connectivity.insert(connectionId); + _connectionIndex.add(connectionId); Q_EMIT connectionCreated(connectionId); } @@ -211,7 +193,7 @@ QVariant DynamicPortsModel::portData(NodeId nodeId, return QVariant(); break; - case PortRole::ConnectionPolicyRole: + case PortRole::ConnectionPolicy: return QVariant::fromValue(ConnectionPolicy::One); break; @@ -245,15 +227,7 @@ bool DynamicPortsModel::setPortData( bool DynamicPortsModel::deleteConnection(ConnectionId const connectionId) { - bool disconnected = false; - - auto it = _connectivity.find(connectionId); - - if (it != _connectivity.end()) { - disconnected = true; - - _connectivity.erase(it); - }; + bool const disconnected = _connectionIndex.remove(connectionId); if (disconnected) Q_EMIT connectionDeleted(connectionId); @@ -263,9 +237,14 @@ bool DynamicPortsModel::deleteConnection(ConnectionId const connectionId) bool DynamicPortsModel::deleteNode(NodeId const nodeId) { - // Delete connections to this node first. - auto connectionIds = allConnectionIds(nodeId); - for (auto &cId : connectionIds) { + std::vector connectionIds; + auto const &attachedConnections = allConnectionIds(nodeId); + connectionIds.reserve(attachedConnections.size()); + for (auto const &connectionId : attachedConnections) { + connectionIds.push_back(connectionId); + } + + for (auto const &cId : connectionIds) { deleteConnection(cId); } @@ -311,7 +290,7 @@ QJsonObject DynamicPortsModel::save() const sceneJson["nodes"] = nodesJsonArray; QJsonArray connJsonArray; - for (auto const &cid : _connectivity) { + for (auto const &cid : _connectionIndex.connectivity()) { connJsonArray.append(QtNodes::toJson(cid)); } sceneJson["connections"] = connJsonArray; diff --git a/examples/dynamic_ports/DynamicPortsModel.hpp b/examples/dynamic_ports/DynamicPortsModel.hpp index ba2886eba..878c12c2d 100644 --- a/examples/dynamic_ports/DynamicPortsModel.hpp +++ b/examples/dynamic_ports/DynamicPortsModel.hpp @@ -5,8 +5,11 @@ #include #include +#include #include +#include + using ConnectionId = QtNodes::ConnectionId; using ConnectionPolicy = QtNodes::ConnectionPolicy; using NodeFlag = QtNodes::NodeFlag; @@ -39,13 +42,13 @@ class DynamicPortsModel : public QtNodes::AbstractGraphModel ~DynamicPortsModel() override = default; - std::unordered_set allNodeIds() const override; + NodeIdSet const &allNodeIds() const override; - std::unordered_set allConnectionIds(NodeId const nodeId) const override; + ConnectionIdSet const &allConnectionIds(NodeId const nodeId) const override; - std::unordered_set connections(NodeId nodeId, - PortType portType, - PortIndex portIndex) const override; + ConnectionIdSet const &connections(NodeId nodeId, + PortType portType, + PortIndex portIndex) const override; bool connectionExists(ConnectionId const connectionId) const override; @@ -100,13 +103,13 @@ class DynamicPortsModel : public QtNodes::AbstractGraphModel NodeId newNodeId() override { return _nextNodeId++; } private: - std::unordered_set _nodeIds; + NodeIdSet _nodeIds; /// [Important] This is a user defined data structure backing your model. /// In your case it could be anything else representing a graph, for example, a /// table. Or a collection of structs with pointers to each other. Or an /// abstract syntax tree, you name it. - std::unordered_set _connectivity; + QtNodes::ConnectionIdIndex _connectionIndex; mutable std::unordered_map _nodeGeometryData; diff --git a/examples/node_validation/ValidatedModel.hpp b/examples/node_validation/ValidatedModel.hpp index 1c702fbcf..bb5264d81 100644 --- a/examples/node_validation/ValidatedModel.hpp +++ b/examples/node_validation/ValidatedModel.hpp @@ -61,10 +61,8 @@ class ValidatedModel : public NodeDelegateModel if (!textData || textData->isEmpty()) { // No input - set Empty status setNodeProcessingStatus(NodeProcessingStatus::Empty); - NodeValidationState state; - state._state = NodeValidationState::State::Warning; - state._stateMessage = "No input data"; - setValidationState(state); + setValidationState(NodeValidationState(NodeValidationState::State::Warning, + "No input data")); _outputData.reset(); if (_label) @@ -107,10 +105,9 @@ class ValidatedModel : public NodeDelegateModel if (text.length() < _minLength) { // Validation failed - NodeValidationState state; - state._state = NodeValidationState::State::Error; - state._stateMessage = QString("Text must be at least %1 characters").arg(_minLength); - setValidationState(state); + setValidationState(NodeValidationState( + NodeValidationState::State::Error, + QString("Text must be at least %1 characters").arg(_minLength))); setNodeProcessingStatus(NodeProcessingStatus::Failed); @@ -121,10 +118,8 @@ class ValidatedModel : public NodeDelegateModel Q_EMIT dataInvalidated(0); } else if (text.length() < _minLength * 2) { // Partial success - warning - NodeValidationState state; - state._state = NodeValidationState::State::Warning; - state._stateMessage = "Text is short but acceptable"; - setValidationState(state); + setValidationState(NodeValidationState(NodeValidationState::State::Warning, + "Text is short but acceptable")); setNodeProcessingStatus(NodeProcessingStatus::Partial); @@ -135,10 +130,7 @@ class ValidatedModel : public NodeDelegateModel Q_EMIT dataUpdated(0); } else { // Full success - NodeValidationState state; - state._state = NodeValidationState::State::Valid; - state._stateMessage = ""; - setValidationState(state); + setValidationState(NodeValidationState()); setNodeProcessingStatus(NodeProcessingStatus::Updated); diff --git a/examples/simple_graph_model/SimpleGraphModel.cpp b/examples/simple_graph_model/SimpleGraphModel.cpp index 7c04440f0..c11f1e0fb 100644 --- a/examples/simple_graph_model/SimpleGraphModel.cpp +++ b/examples/simple_graph_model/SimpleGraphModel.cpp @@ -1,5 +1,7 @@ #include "SimpleGraphModel.hpp" +#include + SimpleGraphModel::SimpleGraphModel() : _nextNodeId{0} {} @@ -9,45 +11,26 @@ SimpleGraphModel::~SimpleGraphModel() // } -std::unordered_set SimpleGraphModel::allNodeIds() const +QtNodes::AbstractGraphModel::NodeIdSet const &SimpleGraphModel::allNodeIds() const { return _nodeIds; } -std::unordered_set SimpleGraphModel::allConnectionIds(NodeId const nodeId) const +QtNodes::AbstractGraphModel::ConnectionIdSet const & +SimpleGraphModel::allConnectionIds(NodeId const nodeId) const { - std::unordered_set result; - - std::copy_if(_connectivity.begin(), - _connectivity.end(), - std::inserter(result, std::end(result)), - [&nodeId](ConnectionId const &cid) { - return cid.inNodeId == nodeId || cid.outNodeId == nodeId; - }); - - return result; + return _connectionIndex.allConnectionIds(nodeId); } -std::unordered_set SimpleGraphModel::connections(NodeId nodeId, - PortType portType, - PortIndex portIndex) const +QtNodes::AbstractGraphModel::ConnectionIdSet const & +SimpleGraphModel::connections(NodeId nodeId, PortType portType, PortIndex portIndex) const { - std::unordered_set result; - - std::copy_if(_connectivity.begin(), - _connectivity.end(), - std::inserter(result, std::end(result)), - [&portType, &portIndex, &nodeId](ConnectionId const &cid) { - return (getNodeId(portType, cid) == nodeId - && getPortIndex(portType, cid) == portIndex); - }); - - return result; + return _connectionIndex.connections(nodeId, portType, portIndex); } bool SimpleGraphModel::connectionExists(ConnectionId const connectionId) const { - return (_connectivity.find(connectionId) != _connectivity.end()); + return _connectionIndex.contains(connectionId); } NodeId SimpleGraphModel::addNode(QString const nodeType) @@ -63,12 +46,12 @@ NodeId SimpleGraphModel::addNode(QString const nodeType) bool SimpleGraphModel::connectionPossible(ConnectionId const connectionId) const { - return _connectivity.find(connectionId) == _connectivity.end(); + return !_connectionIndex.contains(connectionId); } void SimpleGraphModel::addConnection(ConnectionId const connectionId) { - _connectivity.insert(connectionId); + _connectionIndex.add(connectionId); Q_EMIT connectionCreated(connectionId); } @@ -188,7 +171,7 @@ QVariant SimpleGraphModel::portData(NodeId nodeId, return QVariant(); break; - case PortRole::ConnectionPolicyRole: + case PortRole::ConnectionPolicy: return QVariant::fromValue(ConnectionPolicy::One); break; @@ -222,15 +205,7 @@ bool SimpleGraphModel::setPortData( bool SimpleGraphModel::deleteConnection(ConnectionId const connectionId) { - bool disconnected = false; - - auto it = _connectivity.find(connectionId); - - if (it != _connectivity.end()) { - disconnected = true; - - _connectivity.erase(it); - } + bool const disconnected = _connectionIndex.remove(connectionId); if (disconnected) Q_EMIT connectionDeleted(connectionId); @@ -240,10 +215,14 @@ bool SimpleGraphModel::deleteConnection(ConnectionId const connectionId) bool SimpleGraphModel::deleteNode(NodeId const nodeId) { - // Delete connections to this node first. - auto connectionIds = allConnectionIds(nodeId); + std::vector connectionIds; + auto const &attachedConnections = allConnectionIds(nodeId); + connectionIds.reserve(attachedConnections.size()); + for (auto const &connectionId : attachedConnections) { + connectionIds.push_back(connectionId); + } - for (auto &cId : connectionIds) { + for (auto const &cId : connectionIds) { deleteConnection(cId); } diff --git a/examples/simple_graph_model/SimpleGraphModel.hpp b/examples/simple_graph_model/SimpleGraphModel.hpp index e3d07213c..724dda79c 100644 --- a/examples/simple_graph_model/SimpleGraphModel.hpp +++ b/examples/simple_graph_model/SimpleGraphModel.hpp @@ -5,9 +5,12 @@ #include #include +#include #include #include +#include + using ConnectionId = QtNodes::ConnectionId; using ConnectionPolicy = QtNodes::ConnectionPolicy; using NodeFlag = QtNodes::NodeFlag; @@ -38,13 +41,13 @@ class SimpleGraphModel : public QtNodes::AbstractGraphModel ~SimpleGraphModel() override; - std::unordered_set allNodeIds() const override; + NodeIdSet const &allNodeIds() const override; - std::unordered_set allConnectionIds(NodeId const nodeId) const override; + ConnectionIdSet const &allConnectionIds(NodeId const nodeId) const override; - std::unordered_set connections(NodeId nodeId, - PortType portType, - PortIndex portIndex) const override; + ConnectionIdSet const &connections(NodeId nodeId, + PortType portType, + PortIndex portIndex) const override; bool connectionExists(ConnectionId const connectionId) const override; @@ -91,7 +94,7 @@ class SimpleGraphModel : public QtNodes::AbstractGraphModel NodeId newNodeId() override { return _nextNodeId++; } private: - std::unordered_set _nodeIds; + NodeIdSet _nodeIds; /// [Important] This is a user defined data structure backing your model. /// In your case it could be anything else representing a graph, for example, a @@ -100,7 +103,7 @@ class SimpleGraphModel : public QtNodes::AbstractGraphModel /// /// This data structure contains the graph connectivity information in both /// directions, i.e. from Node1 to Node2 and from Node2 to Node1. - std::unordered_set _connectivity; + QtNodes::ConnectionIdIndex _connectionIndex; mutable std::unordered_map _nodeGeometryData; diff --git a/examples/vertical_layout/SimpleGraphModel.cpp b/examples/vertical_layout/SimpleGraphModel.cpp index 5b5e3b8b1..b2f9e4536 100644 --- a/examples/vertical_layout/SimpleGraphModel.cpp +++ b/examples/vertical_layout/SimpleGraphModel.cpp @@ -1,5 +1,7 @@ #include "SimpleGraphModel.hpp" +#include + SimpleGraphModel::SimpleGraphModel() : _nextNodeId{0} {} @@ -9,45 +11,26 @@ SimpleGraphModel::~SimpleGraphModel() // } -std::unordered_set SimpleGraphModel::allNodeIds() const +QtNodes::AbstractGraphModel::NodeIdSet const &SimpleGraphModel::allNodeIds() const { return _nodeIds; } -std::unordered_set SimpleGraphModel::allConnectionIds(NodeId const nodeId) const +QtNodes::AbstractGraphModel::ConnectionIdSet const & +SimpleGraphModel::allConnectionIds(NodeId const nodeId) const { - std::unordered_set result; - - std::copy_if(_connectivity.begin(), - _connectivity.end(), - std::inserter(result, std::end(result)), - [&nodeId](ConnectionId const &cid) { - return cid.inNodeId == nodeId || cid.outNodeId == nodeId; - }); - - return result; + return _connectionIndex.allConnectionIds(nodeId); } -std::unordered_set SimpleGraphModel::connections(NodeId nodeId, - PortType portType, - PortIndex portIndex) const +QtNodes::AbstractGraphModel::ConnectionIdSet const & +SimpleGraphModel::connections(NodeId nodeId, PortType portType, PortIndex portIndex) const { - std::unordered_set result; - - std::copy_if(_connectivity.begin(), - _connectivity.end(), - std::inserter(result, std::end(result)), - [&portType, &portIndex, &nodeId](ConnectionId const &cid) { - return (getNodeId(portType, cid) == nodeId - && getPortIndex(portType, cid) == portIndex); - }); - - return result; + return _connectionIndex.connections(nodeId, portType, portIndex); } bool SimpleGraphModel::connectionExists(ConnectionId const connectionId) const { - return (_connectivity.find(connectionId) != _connectivity.end()); + return _connectionIndex.contains(connectionId); } NodeId SimpleGraphModel::addNode(QString const nodeType) @@ -63,12 +46,12 @@ NodeId SimpleGraphModel::addNode(QString const nodeType) bool SimpleGraphModel::connectionPossible(ConnectionId const connectionId) const { - return _connectivity.find(connectionId) == _connectivity.end(); + return !_connectionIndex.contains(connectionId); } void SimpleGraphModel::addConnection(ConnectionId const connectionId) { - _connectivity.insert(connectionId); + _connectionIndex.add(connectionId); Q_EMIT connectionCreated(connectionId); } @@ -188,7 +171,7 @@ QVariant SimpleGraphModel::portData(NodeId nodeId, return QVariant(); break; - case PortRole::ConnectionPolicyRole: + case PortRole::ConnectionPolicy: return QVariant::fromValue(ConnectionPolicy::One); break; @@ -222,15 +205,7 @@ bool SimpleGraphModel::setPortData( bool SimpleGraphModel::deleteConnection(ConnectionId const connectionId) { - bool disconnected = false; - - auto it = _connectivity.find(connectionId); - - if (it != _connectivity.end()) { - disconnected = true; - - _connectivity.erase(it); - } + bool const disconnected = _connectionIndex.remove(connectionId); if (disconnected) Q_EMIT connectionDeleted(connectionId); @@ -240,9 +215,14 @@ bool SimpleGraphModel::deleteConnection(ConnectionId const connectionId) bool SimpleGraphModel::deleteNode(NodeId const nodeId) { - // Delete connections to this node first. - auto connectionIds = allConnectionIds(nodeId); - for (auto &cId : connectionIds) { + std::vector connectionIds; + auto const &attachedConnections = allConnectionIds(nodeId); + connectionIds.reserve(attachedConnections.size()); + for (auto const &connectionId : attachedConnections) { + connectionIds.push_back(connectionId); + } + + for (auto const &cId : connectionIds) { deleteConnection(cId); } diff --git a/examples/vertical_layout/SimpleGraphModel.hpp b/examples/vertical_layout/SimpleGraphModel.hpp index f9d1fd4d4..5ac098589 100644 --- a/examples/vertical_layout/SimpleGraphModel.hpp +++ b/examples/vertical_layout/SimpleGraphModel.hpp @@ -5,9 +5,12 @@ #include #include +#include #include #include +#include + using ConnectionId = QtNodes::ConnectionId; using ConnectionPolicy = QtNodes::ConnectionPolicy; using NodeFlag = QtNodes::NodeFlag; @@ -38,13 +41,13 @@ class SimpleGraphModel : public QtNodes::AbstractGraphModel ~SimpleGraphModel() override; - std::unordered_set allNodeIds() const override; + NodeIdSet const &allNodeIds() const override; - std::unordered_set allConnectionIds(NodeId const nodeId) const override; + ConnectionIdSet const &allConnectionIds(NodeId const nodeId) const override; - std::unordered_set connections(NodeId nodeId, - PortType portType, - PortIndex portIndex) const override; + ConnectionIdSet const &connections(NodeId nodeId, + PortType portType, + PortIndex portIndex) const override; bool connectionExists(ConnectionId const connectionId) const override; @@ -92,7 +95,7 @@ class SimpleGraphModel : public QtNodes::AbstractGraphModel NodeId newNodeId() override { return _nextNodeId++; } private: - std::unordered_set _nodeIds; + NodeIdSet _nodeIds; /// [Important] This is a user defined data structure backing your model. /// In your case it could be anything else representing a graph, for example, a @@ -101,7 +104,7 @@ class SimpleGraphModel : public QtNodes::AbstractGraphModel /// /// This data structure contains the graph connectivity information in both /// directions, i.e. from Node1 to Node2 and from Node2 to Node1. - std::unordered_set _connectivity; + QtNodes::ConnectionIdIndex _connectionIndex; mutable std::unordered_map _nodeGeometryData; diff --git a/external/Catch2/CMakeLists.txt b/external/Catch2/CMakeLists.txt index 4cc7e8c87..a85e23f5d 100644 --- a/external/Catch2/CMakeLists.txt +++ b/external/Catch2/CMakeLists.txt @@ -1,13 +1,10 @@ -if(NOT EXISTS "${CMAKE_CURRENT_BINARY_DIR}/single_include/catch2/catch.hpp") - file(DOWNLOAD https://raw.githubusercontent.com/catchorg/Catch2/v2.13.7/single_include/catch2/catch.hpp - "${CMAKE_CURRENT_BINARY_DIR}/single_include/catch2/catch.hpp" - EXPECTED_HASH SHA256=ea379c4a3cb5799027b1eb451163dff065a3d641aaba23bf4e24ee6b536bd9bc - ) -endif() +include(FetchContent) -add_library(Catch2 INTERFACE) -add_library(Catch2::Catch2 ALIAS Catch2) -target_include_directories(Catch2 - INTERFACE - "${CMAKE_CURRENT_BINARY_DIR}/single_include" +FetchContent_Declare( + Catch2 + GIT_REPOSITORY https://github.com/catchorg/Catch2.git + GIT_TAG v3.7.1 + GIT_SHALLOW TRUE ) + +FetchContent_MakeAvailable(Catch2) diff --git a/include/QtNodes/ConnectionIdIndex b/include/QtNodes/ConnectionIdIndex new file mode 100644 index 000000000..974ad9dd4 --- /dev/null +++ b/include/QtNodes/ConnectionIdIndex @@ -0,0 +1 @@ +#include "internal/ConnectionIdIndex.hpp" diff --git a/include/QtNodes/NodeGeometry b/include/QtNodes/NodeGeometry index 46a70a6d7..c4265e1dd 100644 --- a/include/QtNodes/NodeGeometry +++ b/include/QtNodes/NodeGeometry @@ -1,2 +1 @@ -#include "internal/NodeGeometry.hpp" - +#include "internal/AbstractNodeGeometry.hpp" diff --git a/include/QtNodes/internal/AbstractConnectionPainter.hpp b/include/QtNodes/internal/AbstractConnectionPainter.hpp index f838a0b99..fd81a1d4e 100644 --- a/include/QtNodes/internal/AbstractConnectionPainter.hpp +++ b/include/QtNodes/internal/AbstractConnectionPainter.hpp @@ -4,8 +4,6 @@ #include "Export.hpp" -class QPainter; - namespace QtNodes { class ConnectionGraphicsObject; diff --git a/include/QtNodes/internal/AbstractGraphModel.hpp b/include/QtNodes/internal/AbstractGraphModel.hpp index cf8e2491a..af4ab9e9c 100644 --- a/include/QtNodes/internal/AbstractGraphModel.hpp +++ b/include/QtNodes/internal/AbstractGraphModel.hpp @@ -26,8 +26,11 @@ class NODE_EDITOR_PUBLIC AbstractGraphModel : public QObject { Q_OBJECT public: + using NodeIdSet = std::unordered_set; + using ConnectionIdSet = std::unordered_set; + /// Generates a new unique NodeId. - virtual NodeId newNodeId() = 0; + [[nodiscard]] virtual NodeId newNodeId() = 0; /// @brief Returns the full set of unique Node Ids. /** @@ -35,25 +38,34 @@ class NODE_EDITOR_PUBLIC AbstractGraphModel : public QObject * Ids for all the nodes in the graph. From an Id it should be * possible to trace back to the model's internal representation of * the node. + * + * The returned reference must stay valid until the next mutating call on + * the graph model. */ - virtual std::unordered_set allNodeIds() const = 0; + [[nodiscard]] virtual NodeIdSet const &allNodeIds() const = 0; /** * A collection of all input and output connections for the given `nodeId`. + * + * The returned reference must stay valid until the next mutating call on + * the graph model. */ - virtual std::unordered_set allConnectionIds(NodeId const nodeId) const = 0; + [[nodiscard]] virtual ConnectionIdSet const &allConnectionIds(NodeId const nodeId) const = 0; /// @brief Returns all connected Node Ids for given port. /** * The returned set of nodes and port indices correspond to the type * opposite to the given `portType`. + * + * The returned reference must stay valid until the next mutating call on + * the graph model. */ - virtual std::unordered_set connections(NodeId nodeId, - PortType portType, - PortIndex index) const = 0; + [[nodiscard]] virtual ConnectionIdSet const &connections(NodeId nodeId, + PortType portType, + PortIndex index) const = 0; /// Checks if two nodes with the given `connectionId` are connected. - virtual bool connectionExists(ConnectionId const connectionId) const = 0; + [[nodiscard]] virtual bool connectionExists(ConnectionId const connectionId) const = 0; /// Creates a new node instance in the derived class. /** @@ -62,7 +74,7 @@ class NODE_EDITOR_PUBLIC AbstractGraphModel : public QObject * model on its own, it helps to distinguish between possible node * types and create a correct instance inside. */ - virtual NodeId addNode(QString const nodeType = QString()) = 0; + [[nodiscard]] virtual NodeId addNode(QString const nodeType = QString()) = 0; /// Model decides if a conection with a given connection Id possible. /** @@ -71,7 +83,7 @@ class NODE_EDITOR_PUBLIC AbstractGraphModel : public QObject * It is possible to override the function and connect non-equal * data types. */ - virtual bool connectionPossible(ConnectionId const connectionId) const = 0; + [[nodiscard]] virtual bool connectionPossible(ConnectionId const connectionId) const = 0; /// Defines if detaching the connection is possible. virtual bool detachPossible(ConnectionId const) const { return true; } @@ -91,7 +103,7 @@ class NODE_EDITOR_PUBLIC AbstractGraphModel : public QObject * @returns `true` if there is data in the model associated with the * given `nodeId`. */ - virtual bool nodeExists(NodeId const nodeId) const = 0; + [[nodiscard]] virtual bool nodeExists(NodeId const nodeId) const = 0; /// @brief Returns node-related data for requested NodeRole. /** diff --git a/include/QtNodes/internal/AbstractNodePainter.hpp b/include/QtNodes/internal/AbstractNodePainter.hpp index cd4c7d63e..aac648976 100644 --- a/include/QtNodes/internal/AbstractNodePainter.hpp +++ b/include/QtNodes/internal/AbstractNodePainter.hpp @@ -4,12 +4,9 @@ #include "Export.hpp" -class QPainter; - namespace QtNodes { class NodeGraphicsObject; -class NodeDataModel; /// Class enables custom painting. class NODE_EDITOR_PUBLIC AbstractNodePainter diff --git a/include/QtNodes/internal/BasicGraphicsScene.hpp b/include/QtNodes/internal/BasicGraphicsScene.hpp index 64b11d9a8..c3d1b207f 100644 --- a/include/QtNodes/internal/BasicGraphicsScene.hpp +++ b/include/QtNodes/internal/BasicGraphicsScene.hpp @@ -7,10 +7,7 @@ #include "Export.hpp" #include "GroupGraphicsObject.hpp" #include "NodeGroup.hpp" -#include "QUuidStdHash.hpp" #include "UndoCommands.hpp" -#include -#include #include #include @@ -28,12 +25,10 @@ class AbstractGraphModel; class AbstractNodePainter; class ConnectionGraphicsObject; class NodeGraphicsObject; -class NodeStyle; class DeleteCommand; class CopyCommand; class NodeGroup; class GroupGraphicsObject; -struct ConnectionId; /// An instance of QGraphicsScene , holds connections and nodes. class NODE_EDITOR_PUBLIC BasicGraphicsScene : public QGraphicsScene @@ -49,9 +44,9 @@ class NODE_EDITOR_PUBLIC BasicGraphicsScene : public QGraphicsScene public: /// @returns associated AbstractGraphModel. - AbstractGraphModel const &graphModel() const; + AbstractGraphModel const &graphModel() const noexcept; - AbstractGraphModel &graphModel(); + AbstractGraphModel &graphModel() noexcept; AbstractNodeGeometry const &nodeGeometry() const; @@ -109,10 +104,10 @@ class NODE_EDITOR_PUBLIC BasicGraphicsScene : public QGraphicsScene * @brief Creates a list of the connections that are incident only to nodes within a * given group. * @param groupID ID of the desired group. - * @return List of (pointers of) connections whose both endpoints belong to members of - * the specified group. + * @return List of connections whose both endpoints belong to members of the specified + * group. */ - std::vector> connectionsWithinGroup(GroupId groupID); + std::vector connectionsWithinGroup(GroupId groupID); /** * @brief Creates a group in the scene containing the given nodes. * @param nodes Reference to the list of nodes to be included in the group. @@ -195,7 +190,7 @@ class NODE_EDITOR_PUBLIC BasicGraphicsScene : public QGraphicsScene */ ConnectionGraphicsObject *connectionGraphicsObject(ConnectionId connectionId); - Qt::Orientation orientation() const { return _orientation; } + Qt::Orientation orientation() const noexcept { return _orientation; } void setOrientation(Qt::Orientation const orientation); diff --git a/include/QtNodes/internal/ConnectionGraphicsObject.hpp b/include/QtNodes/internal/ConnectionGraphicsObject.hpp index 03c06a03d..f4a491641 100644 --- a/include/QtNodes/internal/ConnectionGraphicsObject.hpp +++ b/include/QtNodes/internal/ConnectionGraphicsObject.hpp @@ -1,8 +1,10 @@ #pragma once +#include #include #include +#include #include #include "ConnectionState.hpp" @@ -23,7 +25,7 @@ class NODE_EDITOR_PUBLIC ConnectionGraphicsObject : public QGraphicsObject // Needed for qgraphicsitem_cast enum { Type = UserType + 2 }; - int type() const override { return Type; } + int type() const noexcept override { return Type; } public: ConnectionGraphicsObject(BasicGraphicsScene &scene, ConnectionId const connectionId); @@ -35,7 +37,7 @@ class NODE_EDITOR_PUBLIC ConnectionGraphicsObject : public QGraphicsObject BasicGraphicsScene *nodeScene() const; - ConnectionId const &connectionId() const; + ConnectionId const &connectionId() const noexcept; QRectF boundingRect() const override; @@ -43,12 +45,24 @@ class NODE_EDITOR_PUBLIC ConnectionGraphicsObject : public QGraphicsObject QPointF const &endPoint(PortType portType) const; - QPointF out() const { return _out; } + QPointF out() const noexcept { return _out; } - QPointF in() const { return _in; } + QPointF in() const noexcept { return _in; } std::pair pointsC1C2() const; + /// Cached cubic bezier path, rebuilt only when endpoints change. + QPainterPath const &cachedCubicPath() const; + + /// Cached painter stroke for hit testing, rebuilt only when endpoints change. + QPainterPath const &cachedStrokePath() const; + + int cachedSamplePointCount() const noexcept { return k_path_sample_count; } + + QPointF const &cachedSamplePoint(int index) const; + + QPointF const &cachedMidPoint() const noexcept; + void setEndPoint(PortType portType, QPointF const &point); /// Updates the position of both ends @@ -76,13 +90,11 @@ class NODE_EDITOR_PUBLIC ConnectionGraphicsObject : public QGraphicsObject private: void initializePosition(); - void addGraphicsEffect(); - - std::pair pointsC1C2Horizontal() const; + std::pair computeControlPoints(Qt::Orientation orientation) const; - std::pair pointsC1C2Vertical() const; + void rebuildCachedGeometry() const; + static constexpr int k_path_sample_count = 61; -private: ConnectionId _connectionId; AbstractGraphModel &_graphModel; @@ -91,6 +103,13 @@ class NODE_EDITOR_PUBLIC ConnectionGraphicsObject : public QGraphicsObject mutable QPointF _out; mutable QPointF _in; + + mutable bool _geometryDirty = true; + mutable QRectF _cachedBoundingRect; + mutable QPainterPath _cachedCubicPath; + mutable QPainterPath _cachedStrokePath; + mutable std::array _cachedSamplePoints{}; + mutable QPointF _cachedMidPoint; }; } // namespace QtNodes diff --git a/include/QtNodes/internal/ConnectionIdHash.hpp b/include/QtNodes/internal/ConnectionIdHash.hpp index 09eaced6c..1ab3f01eb 100644 --- a/include/QtNodes/internal/ConnectionIdHash.hpp +++ b/include/QtNodes/internal/ConnectionIdHash.hpp @@ -4,17 +4,14 @@ #include "Definitions.hpp" -inline void hash_combine(std::size_t &seed) -{ - Q_UNUSED(seed); -} - template inline void hash_combine(std::size_t &seed, const T &v, Rest... rest) { std::hash hasher; seed ^= hasher(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2); - hash_combine(seed, rest...); + if constexpr (sizeof...(rest) > 0) { + hash_combine(seed, rest...); + } } namespace std { @@ -29,27 +26,4 @@ struct hash } }; -template<> -struct hash> -{ - inline std::size_t operator()(std::pair const &nodePort) const - { - std::size_t h = 0; - hash_combine(h, nodePort.first, nodePort.second); - return h; - } -}; - -template<> -struct hash> -{ - using Key = std::tuple; - - inline std::size_t operator()(Key const &key) const - { - std::size_t h = 0; - hash_combine(h, std::get<0>(key), std::get<1>(key), std::get<2>(key)); - return h; - } -}; } // namespace std diff --git a/include/QtNodes/internal/ConnectionIdIndex.hpp b/include/QtNodes/internal/ConnectionIdIndex.hpp new file mode 100644 index 000000000..cb01ed8b2 --- /dev/null +++ b/include/QtNodes/internal/ConnectionIdIndex.hpp @@ -0,0 +1,140 @@ +#pragma once + +#include "ConnectionIdHash.hpp" + +#include +#include + +namespace QtNodes { + +class ConnectionIdIndex +{ +public: + using ConnectionSet = std::unordered_set; + using ConnectionsByPort = std::unordered_map; + +public: + ConnectionSet const &connectivity() const noexcept { return _connectivity; } + + ConnectionSet const &allConnectionIds(NodeId const nodeId) const noexcept + { + auto it = _nodeConnections.find(nodeId); + if (it == _nodeConnections.end()) { + return emptyConnections(); + } + + return it->second; + } + + ConnectionSet const &connections(NodeId nodeId, PortType portType, PortIndex portIndex) const noexcept + { + if (portType == PortType::None) { + return emptyConnections(); + } + + auto const &connectionsByPort = (portType == PortType::In) ? _inConnectionsByPort + : _outConnectionsByPort; + auto nodeIt = connectionsByPort.find(nodeId); + if (nodeIt == connectionsByPort.end()) { + return emptyConnections(); + } + + auto portIt = nodeIt->second.find(portIndex); + if (portIt == nodeIt->second.end()) { + return emptyConnections(); + } + + return portIt->second; + } + + bool contains(ConnectionId const connectionId) const noexcept + { + return _connectivity.find(connectionId) != _connectivity.end(); + } + + void add(ConnectionId const connectionId) + { + if (_connectivity.insert(connectionId).second) { + indexConnection(connectionId); + } + } + + bool remove(ConnectionId const connectionId) + { + auto it = _connectivity.find(connectionId); + if (it == _connectivity.end()) { + return false; + } + + _connectivity.erase(it); + unindexConnection(connectionId); + return true; + } + +private: + static ConnectionSet const &emptyConnections() noexcept + { + static ConnectionSet const empty{}; + return empty; + } + + void indexConnection(ConnectionId const connectionId) + { + _nodeConnections[connectionId.inNodeId].insert(connectionId); + _nodeConnections[connectionId.outNodeId].insert(connectionId); + _inConnectionsByPort[connectionId.inNodeId][connectionId.inPortIndex].insert(connectionId); + _outConnectionsByPort[connectionId.outNodeId][connectionId.outPortIndex].insert(connectionId); + } + + void unindexConnection(ConnectionId const connectionId) + { + auto eraseFromNode = [&](NodeId nodeId) { + auto nodeIt = _nodeConnections.find(nodeId); + if (nodeIt == _nodeConnections.end()) { + return; + } + + nodeIt->second.erase(connectionId); + if (nodeIt->second.empty()) { + _nodeConnections.erase(nodeIt); + } + }; + + auto eraseFromPortMap = + [&](std::unordered_map &connectionsByPort, + NodeId nodeId, + PortIndex portIndex) { + auto nodeIt = connectionsByPort.find(nodeId); + if (nodeIt == connectionsByPort.end()) { + return; + } + + auto portIt = nodeIt->second.find(portIndex); + if (portIt == nodeIt->second.end()) { + return; + } + + portIt->second.erase(connectionId); + if (portIt->second.empty()) { + nodeIt->second.erase(portIt); + } + + if (nodeIt->second.empty()) { + connectionsByPort.erase(nodeIt); + } + }; + + eraseFromNode(connectionId.inNodeId); + eraseFromNode(connectionId.outNodeId); + eraseFromPortMap(_inConnectionsByPort, connectionId.inNodeId, connectionId.inPortIndex); + eraseFromPortMap(_outConnectionsByPort, connectionId.outNodeId, connectionId.outPortIndex); + } + +private: + ConnectionSet _connectivity; + std::unordered_map _nodeConnections; + std::unordered_map _inConnectionsByPort; + std::unordered_map _outConnectionsByPort; +}; + +} // namespace QtNodes diff --git a/include/QtNodes/internal/ConnectionIdUtils.hpp b/include/QtNodes/internal/ConnectionIdUtils.hpp index 3e29babf2..7a606419f 100644 --- a/include/QtNodes/internal/ConnectionIdUtils.hpp +++ b/include/QtNodes/internal/ConnectionIdUtils.hpp @@ -1,15 +1,17 @@ #pragma once #include "Definitions.hpp" +#include "SerializationValidation.hpp" #include +#include #include #include namespace QtNodes { -inline PortIndex getNodeId(PortType portType, ConnectionId connectionId) +inline NodeId getNodeId(PortType portType, ConnectionId const &connectionId) noexcept { NodeId id = InvalidNodeId; @@ -22,7 +24,7 @@ inline PortIndex getNodeId(PortType portType, ConnectionId connectionId) return id; } -inline PortIndex getPortIndex(PortType portType, ConnectionId connectionId) +inline PortIndex getPortIndex(PortType portType, ConnectionId const &connectionId) noexcept { PortIndex index = InvalidPortIndex; @@ -35,35 +37,24 @@ inline PortIndex getPortIndex(PortType portType, ConnectionId connectionId) return index; } -inline PortType oppositePort(PortType port) +inline PortType oppositePort(PortType port) noexcept { - PortType result = PortType::None; - switch (port) { case PortType::In: - result = PortType::Out; - break; - + return PortType::Out; case PortType::Out: - result = PortType::In; - break; - - case PortType::None: - result = PortType::None; - break; - + return PortType::In; default: - break; + return PortType::None; } - return result; } -inline bool isPortIndexValid(PortIndex index) +inline bool isPortIndexValid(PortIndex index) noexcept { return index != InvalidPortIndex; } -inline bool isPortTypeValid(PortType portType) +inline bool isPortTypeValid(PortType portType) noexcept { return portType != PortType::None; } @@ -138,22 +129,42 @@ inline QJsonObject toJson(ConnectionId const &connId) return connJson; } -inline ConnectionId fromJson(QJsonObject const &connJson) +inline bool tryFromJson(QJsonObject const &connJson, ConnectionId &connId) { - // Support both "inNodeId" (correct) and "intNodeId" (legacy typo) for backward compatibility + NodeId outNodeId = InvalidNodeId; NodeId inNodeId = InvalidNodeId; - if (connJson.contains("inNodeId")) { - inNodeId = static_cast(connJson["inNodeId"].toInt(InvalidNodeId)); - } else if (connJson.contains("intNodeId")) { - inNodeId = static_cast(connJson["intNodeId"].toInt(InvalidNodeId)); + PortIndex outPortIndex = InvalidPortIndex; + PortIndex inPortIndex = InvalidPortIndex; + + if (!detail::read_node_id(connJson["outNodeId"], outNodeId) + || !detail::read_port_index(connJson["outPortIndex"], outPortIndex) + || !detail::read_port_index(connJson["inPortIndex"], inPortIndex)) { + return false; + } + + // Support both "inNodeId" (correct) and "intNodeId" (legacy typo) + if (!detail::read_node_id(connJson["inNodeId"], inNodeId) + && !detail::read_node_id(connJson["intNodeId"], inNodeId)) { + return false; } - ConnectionId connId{static_cast(connJson["outNodeId"].toInt(InvalidNodeId)), - static_cast(connJson["outPortIndex"].toInt(InvalidPortIndex)), - inNodeId, - static_cast(connJson["inPortIndex"].toInt(InvalidPortIndex))}; + connId = ConnectionId{outNodeId, outPortIndex, inNodeId, inPortIndex}; + return true; +} + +inline ConnectionId fromJson(QJsonObject const &connJson) +{ + ConnectionId connId{InvalidNodeId, InvalidPortIndex, InvalidNodeId, InvalidPortIndex}; + + bool const ok = tryFromJson(connJson, connId); + Q_ASSERT(ok); return connId; } +inline NodeRole portCountRole(PortType portType) noexcept +{ + return (portType == PortType::Out) ? NodeRole::OutPortCount : NodeRole::InPortCount; +} + } // namespace QtNodes diff --git a/include/QtNodes/internal/ConnectionState.hpp b/include/QtNodes/internal/ConnectionState.hpp index d13738260..c4d8f7bbb 100644 --- a/include/QtNodes/internal/ConnectionState.hpp +++ b/include/QtNodes/internal/ConnectionState.hpp @@ -1,13 +1,9 @@ #pragma once -#include - #include "Export.hpp" #include "Definitions.hpp" -class QPointF; - namespace QtNodes { class ConnectionGraphicsObject; @@ -40,8 +36,8 @@ class NODE_EDITOR_PUBLIC ConnectionState PortType requiredPort() const; bool requiresPort() const; - bool hovered() const; - void setHovered(bool hovered); + bool hovered() const noexcept; + void setHovered(bool hovered) noexcept; bool frozen() const { return _frozen; } void setFrozen(bool frozen) { _frozen = frozen; } @@ -50,7 +46,7 @@ class NODE_EDITOR_PUBLIC ConnectionState /// Caches NodeId for further interaction. void setLastHoveredNode(NodeId const nodeId); - NodeId lastHoveredNode() const; + NodeId lastHoveredNode() const noexcept; void resetLastHoveredNode(); diff --git a/include/QtNodes/internal/DataFlowGraphModel.hpp b/include/QtNodes/internal/DataFlowGraphModel.hpp index b1ef892e8..da6fb8a44 100644 --- a/include/QtNodes/internal/DataFlowGraphModel.hpp +++ b/include/QtNodes/internal/DataFlowGraphModel.hpp @@ -1,6 +1,7 @@ #pragma once #include "AbstractGraphModel.hpp" +#include "ConnectionIdIndex.hpp" #include "ConnectionIdUtils.hpp" #include "NodeDelegateModelRegistry.hpp" #include "Serializable.hpp" @@ -11,6 +12,9 @@ #include #include +#include +#include +#include namespace QtNodes { @@ -30,26 +34,29 @@ class NODE_EDITOR_PUBLIC DataFlowGraphModel public: DataFlowGraphModel(std::shared_ptr registry); - std::shared_ptr dataModelRegistry() { return _registry; } + [[nodiscard]] std::shared_ptr dataModelRegistry() noexcept + { + return _registry; + } public: - std::unordered_set allNodeIds() const override; + NodeIdSet const &allNodeIds() const override; - std::unordered_set allConnectionIds(NodeId const nodeId) const override; + ConnectionIdSet const &allConnectionIds(NodeId const nodeId) const override; - std::unordered_set connections(NodeId nodeId, - PortType portType, - PortIndex portIndex) const override; + ConnectionIdSet const &connections(NodeId nodeId, + PortType portType, + PortIndex portIndex) const override; - bool connectionExists(ConnectionId const connectionId) const override; + [[nodiscard]] bool connectionExists(ConnectionId const connectionId) const override; - NodeId addNode(QString const nodeType) override; + [[nodiscard]] NodeId addNode(QString const nodeType) override; - bool connectionPossible(ConnectionId const connectionId) const override; + [[nodiscard]] bool connectionPossible(ConnectionId const connectionId) const override; void addConnection(ConnectionId const connectionId) override; - bool nodeExists(NodeId const nodeId) const override; + [[nodiscard]] bool nodeExists(NodeId const nodeId) const override; QVariant nodeData(NodeId nodeId, NodeRole role) const override; @@ -99,13 +106,22 @@ class NODE_EDITOR_PUBLIC DataFlowGraphModel } /// Loops do not make any sense in uni-direction data propagation - bool loopsEnabled() const override { return false; } + bool loopsEnabled() const noexcept override { return false; } Q_SIGNALS: void inPortDataWasSet(NodeId const, PortType const, PortIndex const); private: - NodeId newNodeId() override { return _nextNodeId++; } + NodeId newNodeId() override + { + if (_nextNodeId == InvalidNodeId) { + throw std::overflow_error("No available node identifiers"); + } + + return _nextNodeId++; + } + + void connectDelegateModel(NodeDelegateModel *model, NodeId nodeId); void sendConnectionCreation(ConnectionId const connectionId); @@ -132,9 +148,11 @@ private Q_SLOTS: NodeId _nextNodeId; + NodeIdSet _nodeIds; + std::unordered_map> _models; - std::unordered_set _connectivity; + ConnectionIdIndex _connectionIndex; mutable std::unordered_map _nodeGeometryData; }; diff --git a/include/QtNodes/internal/DataFlowGraphicsScene.hpp b/include/QtNodes/internal/DataFlowGraphicsScene.hpp index 30bcc9491..e928f830d 100644 --- a/include/QtNodes/internal/DataFlowGraphicsScene.hpp +++ b/include/QtNodes/internal/DataFlowGraphicsScene.hpp @@ -1,10 +1,8 @@ #pragma once #include "BasicGraphicsScene.hpp" -#include "ConnectionGraphicsObject.hpp" #include "DataFlowGraphModel.hpp" #include "Export.hpp" -#include "NodeConnectionInteraction.hpp" namespace QtNodes { diff --git a/include/QtNodes/internal/DefaultConnectionPainter.hpp b/include/QtNodes/internal/DefaultConnectionPainter.hpp index b2c44d102..f4c2913f9 100644 --- a/include/QtNodes/internal/DefaultConnectionPainter.hpp +++ b/include/QtNodes/internal/DefaultConnectionPainter.hpp @@ -2,13 +2,13 @@ #include #include +#include #include "AbstractConnectionPainter.hpp" #include "Definitions.hpp" namespace QtNodes { -class ConnectionGeometry; class ConnectionGraphicsObject; class DefaultConnectionPainter : public AbstractConnectionPainter @@ -16,6 +16,7 @@ class DefaultConnectionPainter : public AbstractConnectionPainter public: void paint(QPainter *painter, ConnectionGraphicsObject const &cgo) const override; QPainterPath getPainterStroke(ConnectionGraphicsObject const &cgo) const override; + private: QPainterPath cubicPath(ConnectionGraphicsObject const &connection) const; void drawSketchLine(QPainter *painter, ConnectionGraphicsObject const &cgo) const; @@ -24,6 +25,12 @@ class DefaultConnectionPainter : public AbstractConnectionPainter #ifdef NODE_DEBUG_DRAWING void debugDrawing(QPainter *painter, ConnectionGraphicsObject const &cgo) const; #endif + +private: + QPixmap const &convertPixmap() const; + + mutable QPixmap _convertPixmap; + mutable bool _convertPixmapInitialized = false; }; } // namespace QtNodes diff --git a/include/QtNodes/internal/DefaultHorizontalNodeGeometry.hpp b/include/QtNodes/internal/DefaultHorizontalNodeGeometry.hpp index 33367e109..66abdec08 100644 --- a/include/QtNodes/internal/DefaultHorizontalNodeGeometry.hpp +++ b/include/QtNodes/internal/DefaultHorizontalNodeGeometry.hpp @@ -1,24 +1,17 @@ #pragma once -#include "AbstractNodeGeometry.hpp" - -#include +#include "DefaultNodeGeometryBase.hpp" namespace QtNodes { class AbstractGraphModel; -class BasicGraphicsScene; -class NODE_EDITOR_PUBLIC DefaultHorizontalNodeGeometry : public AbstractNodeGeometry +class NODE_EDITOR_PUBLIC DefaultHorizontalNodeGeometry : public DefaultNodeGeometryBase { public: DefaultHorizontalNodeGeometry(AbstractGraphModel &graphModel); public: - QRectF boundingRect(NodeId const nodeId) const override; - - QSize size(NodeId const nodeId) const override; - void recomputeSize(NodeId const nodeId) const override; QPointF portPosition(NodeId const nodeId, @@ -30,31 +23,13 @@ class NODE_EDITOR_PUBLIC DefaultHorizontalNodeGeometry : public AbstractNodeGeom PortIndex const PortIndex) const override; QPointF captionPosition(NodeId const nodeId) const override; - QRectF captionRect(NodeId const nodeId) const override; - QPointF widgetPosition(NodeId const nodeId) const override; QRect resizeHandleRect(NodeId const nodeId) const override; private: - QRectF portTextRect(NodeId const nodeId, - PortType const portType, - PortIndex const portIndex) const; - /// Finds max number of ports and multiplies by (a port height + interval) unsigned int maxVerticalPortsExtent(NodeId const nodeId) const; - - unsigned int maxPortsTextAdvance(NodeId const nodeId, PortType const portType) const; - -private: - // Some variables are mutable because we need to change drawing - // metrics corresponding to fontMetrics but this doesn't change - // constness of the Node. - - mutable unsigned int _portSize; - unsigned int _portSpasing; - mutable QFontMetrics _fontMetrics; - mutable QFontMetrics _boldFontMetrics; }; } // namespace QtNodes diff --git a/include/QtNodes/internal/DefaultNodeGeometryBase.hpp b/include/QtNodes/internal/DefaultNodeGeometryBase.hpp new file mode 100644 index 000000000..a5072121d --- /dev/null +++ b/include/QtNodes/internal/DefaultNodeGeometryBase.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include "AbstractNodeGeometry.hpp" + +#include + +namespace QtNodes { + +class AbstractGraphModel; + +/** + * Shared base for DefaultHorizontalNodeGeometry and DefaultVerticalNodeGeometry. + * + * Contains common member variables and methods that are identical in both + * orientations: boundingRect, size, captionRect, portTextRect, + * maxPortsTextAdvance, and maxPortsExtent. + */ +class NODE_EDITOR_PUBLIC DefaultNodeGeometryBase : public AbstractNodeGeometry +{ +public: + DefaultNodeGeometryBase(AbstractGraphModel &graphModel); + + QRectF boundingRect(NodeId const nodeId) const override; + + QSize size(NodeId const nodeId) const override; + + QRectF captionRect(NodeId const nodeId) const override; + +protected: + QRectF portTextRect(NodeId const nodeId, + PortType const portType, + PortIndex const portIndex) const; + + unsigned int maxPortsExtent(NodeId const nodeId) const; + + unsigned int maxPortsTextAdvance(NodeId const nodeId, PortType const portType) const; + +protected: + mutable unsigned int _portSize; + unsigned int _portSpacing; + mutable QFontMetrics _fontMetrics; + mutable QFontMetrics _boldFontMetrics; +}; + +} // namespace QtNodes diff --git a/include/QtNodes/internal/DefaultNodePainter.hpp b/include/QtNodes/internal/DefaultNodePainter.hpp index dbea2a3c0..be4d1eb89 100644 --- a/include/QtNodes/internal/DefaultNodePainter.hpp +++ b/include/QtNodes/internal/DefaultNodePainter.hpp @@ -5,36 +5,38 @@ #include "AbstractNodePainter.hpp" #include "Definitions.hpp" +#include "NodeStyle.hpp" namespace QtNodes { -class BasicGraphicsScene; -class GraphModel; -class NodeGeometry; +class GraphicsView; class NodeGraphicsObject; -class NodeState; /// @ Lightweight class incapsulating paint code. +/// +/// NOTE: Some draw helpers accept GraphicsView* for zoom-aware text rendering. +/// That paint-time coupling is intentional for now, but custom painters need to +/// account for it. class NODE_EDITOR_PUBLIC DefaultNodePainter : public AbstractNodePainter { public: void paint(QPainter *painter, NodeGraphicsObject &ngo) const override; - void drawNodeRect(QPainter *painter, NodeGraphicsObject &ngo) const; + void drawNodeRect(QPainter *painter, NodeGraphicsObject &ngo, NodeStyle const &style) const; - void drawConnectionPoints(QPainter *painter, NodeGraphicsObject &ngo) const; + void drawConnectionPoints(QPainter *painter, NodeGraphicsObject &ngo, NodeStyle const &style) const; - void drawFilledConnectionPoints(QPainter *painter, NodeGraphicsObject &ngo) const; + void drawFilledConnectionPoints(QPainter *painter, NodeGraphicsObject &ngo, NodeStyle const &style) const; - void drawNodeCaption(QPainter *painter, NodeGraphicsObject &ngo) const; + void drawNodeCaption(QPainter *painter, NodeGraphicsObject &ngo, NodeStyle const &style, GraphicsView *view) const; - void drawEntryLabels(QPainter *painter, NodeGraphicsObject &ngo) const; + void drawEntryLabels(QPainter *painter, NodeGraphicsObject &ngo, NodeStyle const &style, GraphicsView *view) const; void drawResizeRect(QPainter *painter, NodeGraphicsObject &ngo) const; void drawProcessingIndicator(QPainter *painter, NodeGraphicsObject &ngo) const; - void drawValidationIcon(QPainter *painter, NodeGraphicsObject &ngo) const; + void drawValidationIcon(QPainter *painter, NodeGraphicsObject &ngo, NodeStyle const &style) const; private: QIcon _toolTipIcon{":/info-tooltip.svg"}; diff --git a/include/QtNodes/internal/DefaultVerticalNodeGeometry.hpp b/include/QtNodes/internal/DefaultVerticalNodeGeometry.hpp index ce4dd9f17..2fda46fde 100644 --- a/include/QtNodes/internal/DefaultVerticalNodeGeometry.hpp +++ b/include/QtNodes/internal/DefaultVerticalNodeGeometry.hpp @@ -1,24 +1,17 @@ #pragma once -#include "AbstractNodeGeometry.hpp" - -#include +#include "DefaultNodeGeometryBase.hpp" namespace QtNodes { class AbstractGraphModel; -class BasicGraphicsScene; -class NODE_EDITOR_PUBLIC DefaultVerticalNodeGeometry : public AbstractNodeGeometry +class NODE_EDITOR_PUBLIC DefaultVerticalNodeGeometry : public DefaultNodeGeometryBase { public: DefaultVerticalNodeGeometry(AbstractGraphModel &graphModel); public: - QRectF boundingRect(NodeId const nodeId) const override; - - QSize size(NodeId const nodeId) const override; - void recomputeSize(NodeId const nodeId) const override; QPointF portPosition(NodeId const nodeId, @@ -31,32 +24,15 @@ class NODE_EDITOR_PUBLIC DefaultVerticalNodeGeometry : public AbstractNodeGeomet QPointF captionPosition(NodeId const nodeId) const override; - QRectF captionRect(NodeId const nodeId) const override; - QPointF widgetPosition(NodeId const nodeId) const override; QRect resizeHandleRect(NodeId const nodeId) const override; private: - QRectF portTextRect(NodeId const nodeId, - PortType const portType, - PortIndex const portIndex) const; /// Finds unsigned int maxHorizontalPortsExtent(NodeId const nodeId) const; - unsigned int maxPortsTextAdvance(NodeId const nodeId, PortType const portType) const; - unsigned int portCaptionsHeight(NodeId const nodeId, PortType const portType) const; - -private: - // Some variables are mutable because we need to change drawing - // metrics corresponding to fontMetrics but this doesn't change - // constness of the Node. - - mutable unsigned int _portSize; - unsigned int _portSpasing; - mutable QFontMetrics _fontMetrics; - mutable QFontMetrics _boldFontMetrics; }; } // namespace QtNodes diff --git a/include/QtNodes/internal/Definitions.hpp b/include/QtNodes/internal/Definitions.hpp index fc4ffc26d..a5dcdf9a4 100644 --- a/include/QtNodes/internal/Definitions.hpp +++ b/include/QtNodes/internal/Definitions.hpp @@ -23,12 +23,12 @@ Q_NAMESPACE_EXPORT(NODE_EDITOR_PUBLIC) */ enum class NodeRole { Type = 0, ///< Type of the current node, usually a string. - Position = 1, ///< `QPointF` positon of the node on the scene. + Position = 1, ///< `QPointF` position of the node on the scene. Size = 2, ///< `QSize` for resizable nodes. CaptionVisible = 3, ///< `bool` for caption visibility. Caption = 4, ///< `QString` for node caption. Style = 5, ///< Custom NodeStyle as QJsonDocument - InternalData = 6, ///< Node-stecific user data as QJsonObject + InternalData = 6, ///< Node-specific user data as QJsonObject InPortCount = 7, ///< `unsigned int` OutPortCount = 9, ///< `unsigned int` Widget = 10, ///< Optional `QWidget*` or `nullptr` @@ -38,7 +38,7 @@ Q_NAMESPACE_EXPORT(NODE_EDITOR_PUBLIC) Q_ENUM_NS(NodeRole) /** - * Specific flags regulating node features and appeaarence. + * Specific flags regulating node features and appearance. */ enum NodeFlag { NoFlags = 0x0, ///< Default NodeFlag @@ -54,11 +54,11 @@ Q_DECLARE_OPERATORS_FOR_FLAGS(NodeFlags) * Constants for fetching port-related information from the GraphModel. */ enum class PortRole { - Data = 0, ///< `std::shared_ptr`. - DataType = 1, ///< `QString` describing the port data type. - ConnectionPolicyRole = 2, ///< `enum` ConnectionPolicyRole - CaptionVisible = 3, ///< `bool` for caption visibility. - Caption = 4, ///< `QString` for port caption. + Data = 0, ///< `std::shared_ptr`. + DataType = 1, ///< `QString` describing the port data type. + ConnectionPolicy = 2, ///< `enum` ConnectionPolicy + CaptionVisible = 3, ///< `bool` for caption visibility. + Caption = 4, ///< `QString` for port caption. }; Q_ENUM_NS(PortRole) @@ -111,18 +111,18 @@ struct ConnectionId PortIndex inPortIndex; }; -inline bool operator==(ConnectionId const &a, ConnectionId const &b) +inline bool operator==(ConnectionId const &a, ConnectionId const &b) noexcept { return a.outNodeId == b.outNodeId && a.outPortIndex == b.outPortIndex && a.inNodeId == b.inNodeId && a.inPortIndex == b.inPortIndex; } -inline bool operator!=(ConnectionId const &a, ConnectionId const &b) +inline bool operator!=(ConnectionId const &a, ConnectionId const &b) noexcept { return !(a == b); } -inline void invertConnection(ConnectionId &id) +inline void invertConnection(ConnectionId &id) noexcept { std::swap(id.outNodeId, id.inNodeId); std::swap(id.outPortIndex, id.inPortIndex); diff --git a/include/QtNodes/internal/Export.hpp b/include/QtNodes/internal/Export.hpp index 41e6e3d0a..78ac2edfa 100644 --- a/include/QtNodes/internal/Export.hpp +++ b/include/QtNodes/internal/Export.hpp @@ -17,12 +17,6 @@ #define NODE_EDITOR_LOCAL #endif -#ifdef __cplusplus -#define NODE_EDITOR_DEMANGLED extern "C" -#else -#define NODE_EDITOR_DEMANGLED -#endif - #if defined(NODE_EDITOR_SHARED) && !defined(NODE_EDITOR_STATIC) #ifdef NODE_EDITOR_EXPORTS #define NODE_EDITOR_PUBLIC NODE_EDITOR_EXPORT diff --git a/include/QtNodes/internal/GraphicsView.hpp b/include/QtNodes/internal/GraphicsView.hpp index fdc5af7ce..4ed90f72f 100644 --- a/include/QtNodes/internal/GraphicsView.hpp +++ b/include/QtNodes/internal/GraphicsView.hpp @@ -4,6 +4,8 @@ #include "Export.hpp" +#include + namespace QtNodes { class BasicGraphicsScene; @@ -21,6 +23,19 @@ class NODE_EDITOR_PUBLIC GraphicsView : public QGraphicsView double maximum = 0; }; + enum class TextRenderingPolicy + { + QtText, + PathWhenZooming, + PathAlways, + }; + + enum class RasterizationPolicy + { + Crisp, + Consistent, + }; + public: GraphicsView(QWidget *parent = Q_NULLPTR); GraphicsView(BasicGraphicsScene *scene, QWidget *parent = Q_NULLPTR); @@ -43,6 +58,22 @@ class NODE_EDITOR_PUBLIC GraphicsView : public QGraphicsView double getScale() const; + bool isZoomAnimating() const; + + TextRenderingPolicy textRenderingPolicy() const; + + void setTextRenderingPolicy(TextRenderingPolicy policy); + + RasterizationPolicy rasterizationPolicy() const; + + void setRasterizationPolicy(RasterizationPolicy policy); + + void stopZoomAnimation(); + + static double zoomAnimationScaleFactor(double velocity, double elapsedTimerSteps); + + static double zoomAnimationVelocityAfter(double velocity, double elapsedTimerSteps); + public Q_SLOTS: void scaleUp(); @@ -82,6 +113,8 @@ public Q_SLOTS: void showEvent(QShowEvent *event) override; + void timerEvent(QTimerEvent *event) override; + protected: BasicGraphicsScene *nodeScene(); @@ -98,5 +131,17 @@ public Q_SLOTS: QPointF _clickPos; ScaleRange _scaleRange; + + void applyZoomStep(); + void applyZoomFactor(double factor); + void stopZoomTimer(); + void applyRasterizationPolicy(); + + double _zoomVelocity = 0.0; + QPointF _zoomPivot; + int _zoomTimerId = 0; + std::chrono::steady_clock::time_point _lastZoomStepTime; + TextRenderingPolicy _textRenderingPolicy = TextRenderingPolicy::PathAlways; + RasterizationPolicy _rasterizationPolicy = RasterizationPolicy::Consistent; }; } // namespace QtNodes diff --git a/include/QtNodes/internal/GraphicsViewStyle.hpp b/include/QtNodes/internal/GraphicsViewStyle.hpp index 32f51b9e4..e213ca6f6 100644 --- a/include/QtNodes/internal/GraphicsViewStyle.hpp +++ b/include/QtNodes/internal/GraphicsViewStyle.hpp @@ -25,8 +25,13 @@ class NODE_EDITOR_PUBLIC GraphicsViewStyle : public Style QJsonObject toJson() const override; public: - QColor BackgroundColor; - QColor FineGridColor; - QColor CoarseGridColor; + QColor backgroundColor() const; + QColor fineGridColor() const; + QColor coarseGridColor() const; + +private: + QColor _BackgroundColor; + QColor _FineGridColor; + QColor _CoarseGridColor; }; } // namespace QtNodes diff --git a/include/QtNodes/internal/GroupGraphicsObject.hpp b/include/QtNodes/internal/GroupGraphicsObject.hpp index 1b635fbd4..748264719 100644 --- a/include/QtNodes/internal/GroupGraphicsObject.hpp +++ b/include/QtNodes/internal/GroupGraphicsObject.hpp @@ -77,11 +77,6 @@ class GroupGraphicsObject */ NodeGroup const &group() const; - /** - * @copydoc QGraphicsItem::boundingRect() - */ - QRectF boundingRect() const override; - enum { Type = UserType + 3 }; /** @@ -107,6 +102,11 @@ class GroupGraphicsObject */ void moveConnections(); + /// Recomputes the group rect and icon positions. Call when child + /// nodes move or the possible-child changes instead of deferring + /// geometry work to paint(). + void updateGroupGeometry(); + /** * @brief Moves the position of all the nodes of this group by the amount given. * @param offset 2D vector representing the amount by which the group has moved. @@ -155,10 +155,10 @@ class GroupGraphicsObject void unsetPossibleChild(); /** - * @brief Returns all the connections that are incident strictly within the - * nodes of this group. - */ - std::vector> connections() const; + * @brief Returns all the connections that are incident strictly within the + * nodes of this group. + */ + std::vector connections() const; /** * @brief Sets the position of the group. @@ -182,6 +182,9 @@ class GroupGraphicsObject /** @copydoc QGraphicsItem::mouseMoveEvent() */ void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; + /** @copydoc QGraphicsItem::mouseReleaseEvent() */ + void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; + /** @copydoc QGraphicsItem::mouseDoubleClickEvent() */ void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override; @@ -210,6 +213,9 @@ class GroupGraphicsObject */ QPen _borderPen; +private: + QRectF computeGroupRect() const; + private: /** * @brief _scene Reference to the scene object in which this object is included. diff --git a/include/QtNodes/internal/NodeConnectionInteraction.hpp b/include/QtNodes/internal/NodeConnectionInteraction.hpp index f71ae97e0..4fe589d55 100644 --- a/include/QtNodes/internal/NodeConnectionInteraction.hpp +++ b/include/QtNodes/internal/NodeConnectionInteraction.hpp @@ -59,12 +59,6 @@ class NodeConnectionInteraction NodeGraphicsObject &nodeGraphicsObject() { return _ngo; } private: - PortType connectionRequiredPort() const; - - QPointF connectionEndScenePosition(PortType) const; - - QPointF nodePortScenePosition(PortType portType, PortIndex portIndex) const; - PortIndex nodePortIndexUnderScenePoint(PortType portType, QPointF const &p) const; private: diff --git a/include/QtNodes/internal/NodeDelegateModel.hpp b/include/QtNodes/internal/NodeDelegateModel.hpp index 0ba709112..f98c9f81b 100644 --- a/include/QtNodes/internal/NodeDelegateModel.hpp +++ b/include/QtNodes/internal/NodeDelegateModel.hpp @@ -1,8 +1,10 @@ #pragma once #include +#include #include +#include #include #include #include @@ -12,7 +14,6 @@ #include "NodeData.hpp" #include "NodeStyle.hpp" #include "Serializable.hpp" -#include namespace QtNodes { @@ -26,10 +27,17 @@ struct NodeValidationState Warning = 1, ///< Some inputs are missing or questionable, processing may be unreliable. Error = 2, ///< Inputs or settings are invalid, preventing successful computation. }; - bool isValid() { return _state == State::Valid; }; - QString const message() { return _stateMessage; } - State state() { return _state; } + NodeValidationState(State state = State::Valid, QString message = QString()) + : _state(state) + , _stateMessage(std::move(message)) + {} + + bool isValid() const noexcept { return _state == State::Valid; }; + QString message() const { return _stateMessage; } + State state() const noexcept { return _state; } + +private: State _state{State::Valid}; QString _stateMessage{""}; }; @@ -47,8 +55,6 @@ enum class NodeProcessingStatus : int { Partial = 6, ///< Computation finished incompletely; only partial results are available. }; -class StyleCollection; - /** * The class wraps Node-specific data operations and propagates it to * the nesting DataFlowGraphModel which is a subclass of @@ -109,7 +115,8 @@ class NODE_EDITOR_PUBLIC NodeDelegateModel /// Convenience helper to change the node background color. void setBackgroundColor(QColor const &color); - QPixmap processingStatusIcon() const; + QImage processingStatusImage(qreal dpr) const; + ProcessingIconStyle processingIconStyle() const; void setStatusIcon(NodeProcessingStatus status, const QPixmap &pixmap); @@ -196,6 +203,13 @@ public Q_SLOTS: NodeValidationState _nodeValidationState; NodeProcessingStatus _processingStatus{NodeProcessingStatus::NoStatus}; + + mutable bool _processingStatusIconDirty{true}; + mutable NodeProcessingStatus _cachedProcessingStatus{NodeProcessingStatus::NoStatus}; + mutable int _cachedProcessingStatusResolution{0}; + mutable qreal _cachedProcessingStatusDpr{0.0}; + mutable QImage _cachedProcessingStatusImage; + mutable std::mutex _processingStatusIconMutex; }; } // namespace QtNodes diff --git a/include/QtNodes/internal/NodeDelegateModelRegistry.hpp b/include/QtNodes/internal/NodeDelegateModelRegistry.hpp index 3ab66062b..2f00fb7d6 100644 --- a/include/QtNodes/internal/NodeDelegateModelRegistry.hpp +++ b/include/QtNodes/internal/NodeDelegateModelRegistry.hpp @@ -27,8 +27,6 @@ class NODE_EDITOR_PUBLIC NodeDelegateModelRegistry using RegisteredModelsCategoryMap = std::unordered_map; using CategoriesSet = std::set; - //using RegisteredTypeConvertersMap = std::map; - NodeDelegateModelRegistry() = default; ~NodeDelegateModelRegistry() = default; @@ -68,34 +66,7 @@ class NODE_EDITOR_PUBLIC NodeDelegateModelRegistry } -#if 0 - template - void - registerModel(RegistryItemCreator creator, - QString const& category = "Nodes") - { - registerModel(std::move(creator), category); - } - - - template - void - registerModel(QString const& category, ModelCreator&& creator) - { - registerModel(std::forward(creator), category); - } - - - void - registerTypeConverter(TypeConverterId const& id, - TypeConverter typeConverter) - { - _registeredTypeConverters[id] = std::move(typeConverter); - } - -#endif - - std::unique_ptr create(QString const &modelName); + [[nodiscard]] std::unique_ptr create(QString const &modelName); RegisteredModelCreatorsMap const ®isteredModelCreators() const; @@ -103,12 +74,6 @@ class NODE_EDITOR_PUBLIC NodeDelegateModelRegistry CategoriesSet const &categories() const; -#if 0 - TypeConverter - getTypeConverter(NodeDataType const& d1, - NodeDataType const& d2) const; -#endif - private: RegisteredModelsCategoryMap _registeredModelsCategory; @@ -116,10 +81,6 @@ class NODE_EDITOR_PUBLIC NodeDelegateModelRegistry RegisteredModelCreatorsMap _registeredItemCreators; -#if 0 - RegisteredTypeConvertersMap _registeredTypeConverters; -#endif - private: // If the registered ModelType class has the static member method // `static QString Name();`, use it. Otherwise use the non-static diff --git a/include/QtNodes/internal/NodeGraphicsObject.hpp b/include/QtNodes/internal/NodeGraphicsObject.hpp index e3226802e..08d7e952f 100644 --- a/include/QtNodes/internal/NodeGraphicsObject.hpp +++ b/include/QtNodes/internal/NodeGraphicsObject.hpp @@ -5,11 +5,9 @@ #include "NodeState.hpp" #include #include -#include #include #include "Export.hpp" -#include "NodeState.hpp" class QGraphicsProxyWidget; @@ -17,8 +15,8 @@ namespace QtNodes { class BasicGraphicsScene; class AbstractGraphModel; +class GraphicsView; class NodeGroup; -class NodeDelegateModel; class GroupGraphicsObject; class NODE_EDITOR_PUBLIC NodeGraphicsObject : public QGraphicsObject @@ -28,7 +26,7 @@ class NODE_EDITOR_PUBLIC NodeGraphicsObject : public QGraphicsObject // Needed for qgraphicsitem_cast enum { Type = UserType + 1 }; - int type() const override { return Type; } + int type() const noexcept override { return Type; } public: NodeGraphicsObject(BasicGraphicsScene &scene, NodeId node); @@ -40,18 +38,22 @@ class NODE_EDITOR_PUBLIC NodeGraphicsObject : public QGraphicsObject BasicGraphicsScene *nodeScene() const; - NodeId nodeId() { return _nodeId; } + NodeId nodeId() noexcept { return _nodeId; } - NodeId nodeId() const { return _nodeId; } + NodeId nodeId() const noexcept { return _nodeId; } NodeState &nodeState() { return _nodeState; } NodeState const &nodeState() const { return _nodeState; } + GraphicsView *currentGraphicsView() const noexcept { return _currentGraphicsView; } + QRectF boundingRect() const override; void setGeometryChanged(); + void updateValidationTooltip(); + /// Visits all attached connections and corrects /// their corresponding end points. void moveConnections() const; @@ -73,7 +75,7 @@ class NODE_EDITOR_PUBLIC NodeGraphicsObject : public QGraphicsObject void setNodeGroup(std::shared_ptr group); /// Unsets NodeGroup, setting it to an empty pointer. - void unsetNodeGroup() { _nodeGroup = std::weak_ptr(); } + void unsetNodeGroup() noexcept { _nodeGroup = std::weak_ptr(); } /// Getter for the NodeGroup object. std::weak_ptr nodeGroup() const { return _nodeGroup; } @@ -115,5 +117,8 @@ class NODE_EDITOR_PUBLIC NodeGraphicsObject : public QGraphicsObject QGraphicsProxyWidget *_proxyWidget; std::weak_ptr _nodeGroup{}; + // Transient paint-time context: set only while this node is being painted. + // A future cleanup should prefer threading this through explicitly. + GraphicsView *_currentGraphicsView = nullptr; }; } // namespace QtNodes diff --git a/include/QtNodes/internal/NodeGroup.hpp b/include/QtNodes/internal/NodeGroup.hpp index f8accc0a8..ec09e26f1 100644 --- a/include/QtNodes/internal/NodeGroup.hpp +++ b/include/QtNodes/internal/NodeGroup.hpp @@ -1,24 +1,18 @@ #pragma once +#include +#include #include #include #include -#include "DataFlowGraphModel.hpp" #include "Definitions.hpp" #include "Export.hpp" -#include "GroupGraphicsObject.hpp" -#include "NodeConnectionInteraction.hpp" -#include "NodeState.hpp" namespace QtNodes { -class DataFlowGraphModel; class GroupGraphicsObject; -class NodeState; -class NodeConnectionInteraction; class NodeGraphicsObject; -struct ConnectionId; /** * @brief The NodeGroup class defines a controller for node groups. It is @@ -42,6 +36,8 @@ class NODE_EDITOR_PUBLIC NodeGroup : public QObject QString name = QString(), QObject *parent = nullptr); + ~NodeGroup() override; + public: /** * @brief Prepares a byte array containing this group's data to be saved in a @@ -94,7 +90,7 @@ class NODE_EDITOR_PUBLIC NodeGroup : public QObject * @brief Returns the number of groups created during the program's execution. * Used when automatically naming groups. */ - static int groupCount(); + static int groupCount() noexcept; public Q_SLOTS: /** @@ -121,7 +117,7 @@ public Q_SLOTS: * @brief Identifier of this group. It is the only unique identifier of * the group. */ - GroupId _id; + GroupId _groupId; // data /** @@ -139,6 +135,6 @@ public Q_SLOTS: * @brief Static variable to count the number of instances of groups that * were created during execution. Used when automatically naming groups. */ - static int _groupCount; + static std::atomic _groupCount; }; } // namespace QtNodes diff --git a/include/QtNodes/internal/NodeRenderingUtils.hpp b/include/QtNodes/internal/NodeRenderingUtils.hpp new file mode 100644 index 000000000..4f9f61161 --- /dev/null +++ b/include/QtNodes/internal/NodeRenderingUtils.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include "AbstractGraphModel.hpp" +#include "NodeStyle.hpp" + +#include + +#include +#include + +class QIcon; +class QImage; +class QSize; + +namespace QtNodes::node_rendering { + +inline constexpr qreal k_shadow_offset_x = 2.0; +inline constexpr qreal k_shadow_offset_y = 2.0; +inline constexpr qreal k_node_radius = 3.0; +inline constexpr int k_blur_radius = 5; +inline constexpr int k_blur_passes = 3; +inline constexpr int k_outer_margin = 18; +inline constexpr int k_inner_margin = 16; +inline constexpr int k_shadow_margin = k_outer_margin + k_inner_margin; +inline constexpr int k_body_size = 64; +inline constexpr int k_atlas_size = k_body_size + 2 * k_shadow_margin; +inline constexpr int k_shadow_opacity = 210; +inline constexpr qreal k_port_margin = 20.0; + +inline constexpr qreal shadow_left_extent() { return k_shadow_margin - k_shadow_offset_x; } +inline constexpr qreal shadow_top_extent() { return k_shadow_margin - k_shadow_offset_y; } +inline constexpr qreal shadow_right_extent() { return k_shadow_margin + k_shadow_offset_x; } +inline constexpr qreal shadow_bottom_extent() { return k_shadow_margin + k_shadow_offset_y; } + +inline QMarginsF node_visual_margins(bool shadow_enabled) +{ + if (!shadow_enabled) { + return QMarginsF(k_port_margin, k_port_margin, k_port_margin, k_port_margin); + } + + return QMarginsF(std::max(k_port_margin, shadow_left_extent()), + std::max(k_port_margin, shadow_top_extent()), + std::max(k_port_margin, shadow_right_extent()), + std::max(k_port_margin, shadow_bottom_extent())); +} + +NodeStyle const &resolved_node_style( + AbstractGraphModel &model, + NodeId node_id, + std::optional &fallback_storage); + +QImage render_icon_image(QIcon const &icon, QSize const &logical_size, qreal dpr); + +} // namespace QtNodes::node_rendering diff --git a/include/QtNodes/internal/NodeState.hpp b/include/QtNodes/internal/NodeState.hpp index 82394430e..f1af78f9f 100644 --- a/include/QtNodes/internal/NodeState.hpp +++ b/include/QtNodes/internal/NodeState.hpp @@ -23,13 +23,13 @@ class NODE_EDITOR_PUBLIC NodeState NodeState(NodeGraphicsObject &ngo); public: - bool hovered() const { return _hovered; } + bool hovered() const noexcept { return _hovered; } - void setHovered(bool hovered = true) { _hovered = hovered; } + void setHovered(bool hovered = true) noexcept { _hovered = hovered; } void setResizing(bool resizing); - bool resizing() const; + bool resizing() const noexcept; ConnectionGraphicsObject const *connectionForReaction() const; diff --git a/include/QtNodes/internal/NodeStyle.hpp b/include/QtNodes/internal/NodeStyle.hpp index 01ecd2ea1..098bedd61 100644 --- a/include/QtNodes/internal/NodeStyle.hpp +++ b/include/QtNodes/internal/NodeStyle.hpp @@ -53,39 +53,74 @@ class NODE_EDITOR_PUBLIC NodeStyle : public Style QColor backgroundColor() const; public: - QColor NormalBoundaryColor; - QColor SelectedBoundaryColor; - QColor GradientColor0; - QColor GradientColor1; - QColor GradientColor2; - QColor GradientColor3; - QColor ShadowColor; - bool ShadowEnabled; - QColor FontColor; - QColor FontColorFaded; - - QColor ConnectionPointColor; - QColor FilledConnectionPointColor; - - QColor WarningColor; - QColor ErrorColor; - QColor ToolTipIconColor; - - float PenWidth; - float HoveredPenWidth; - - float ConnectionPointDiameter; - - float Opacity; - - // Status icons - initialized in constructor after Q_INIT_RESOURCE - QIcon statusUpdated; - QIcon statusProcessing; - QIcon statusPending; - QIcon statusInvalid; - QIcon statusEmpty; - QIcon statusPartial; - - ProcessingIconStyle processingIconStyle{}; + QColor normalBoundaryColor() const; + QColor selectedBoundaryColor() const; + QColor gradientColor0() const; + QColor gradientColor1() const; + QColor gradientColor2() const; + QColor gradientColor3() const; + QColor shadowColor() const; + bool shadowEnabled() const; + QColor fontColor() const; + QColor fontColorFaded() const; + QColor connectionPointColor() const; + QColor filledConnectionPointColor() const; + QColor warningColor() const; + QColor errorColor() const; + QColor toolTipIconColor() const; + float penWidth() const; + float hoveredPenWidth() const; + float connectionPointDiameter() const; + float opacity() const; + QIcon const &statusUpdated() const; + QIcon const &statusProcessing() const; + QIcon const &statusPending() const; + QIcon const &statusInvalid() const; + QIcon const &statusEmpty() const; + QIcon const &statusPartial() const; + ProcessingIconStyle const &processingIconStyle() const; + + void setStatusUpdated(QIcon const &icon); + void setStatusProcessing(QIcon const &icon); + void setStatusPending(QIcon const &icon); + void setStatusInvalid(QIcon const &icon); + void setStatusEmpty(QIcon const &icon); + void setStatusPartial(QIcon const &icon); + void setProcessingIconStyle(ProcessingIconStyle const &style); + +private: + QColor _NormalBoundaryColor; + QColor _SelectedBoundaryColor; + QColor _GradientColor0; + QColor _GradientColor1; + QColor _GradientColor2; + QColor _GradientColor3; + QColor _ShadowColor; + bool _ShadowEnabled{false}; + QColor _FontColor; + QColor _FontColorFaded; + + QColor _ConnectionPointColor; + QColor _FilledConnectionPointColor; + + QColor _WarningColor; + QColor _ErrorColor; + QColor _ToolTipIconColor; + + float _PenWidth{0.0f}; + float _HoveredPenWidth{0.0f}; + + float _ConnectionPointDiameter{0.0f}; + + float _Opacity{0.0f}; + + QIcon _statusUpdated; + QIcon _statusProcessing; + QIcon _statusPending; + QIcon _statusInvalid; + QIcon _statusEmpty; + QIcon _statusPartial; + + ProcessingIconStyle _processingIconStyle{}; }; } // namespace QtNodes diff --git a/include/QtNodes/internal/QStringStdHash.hpp b/include/QtNodes/internal/QStringStdHash.hpp index 617d9b50d..bbf7b8455 100644 --- a/include/QtNodes/internal/QStringStdHash.hpp +++ b/include/QtNodes/internal/QStringStdHash.hpp @@ -9,7 +9,6 @@ #include #include -#include namespace std { template<> diff --git a/include/QtNodes/internal/QUuidStdHash.hpp b/include/QtNodes/internal/QUuidStdHash.hpp index 224bc9f49..63ff51a5d 100644 --- a/include/QtNodes/internal/QUuidStdHash.hpp +++ b/include/QtNodes/internal/QUuidStdHash.hpp @@ -3,7 +3,6 @@ #include #include -#include namespace std { template<> diff --git a/include/QtNodes/internal/SerializationValidation.hpp b/include/QtNodes/internal/SerializationValidation.hpp new file mode 100644 index 000000000..638050d27 --- /dev/null +++ b/include/QtNodes/internal/SerializationValidation.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include "Definitions.hpp" + +#include +#include +#include +#include +#include + +namespace QtNodes::detail { + +bool read_unsigned_number(QJsonValue const &value, quint64 maxValue, quint64 &result); + +bool read_node_id(QJsonValue const &value, NodeId &nodeId); + +bool read_group_id(QJsonValue const &value, GroupId &groupId); + +bool read_port_index(QJsonValue const &value, PortIndex &portIndex); + +bool read_finite_number(QJsonValue const &value, double &result); + +bool read_required_object(QJsonObject const &obj, QString const &key, QJsonObject &result); + +bool read_required_array(QJsonObject const &obj, QString const &key, QJsonArray &result); + +bool read_required_string(QJsonObject const &obj, QString const &key, QString &result); + +bool read_optional_bool(QJsonObject const &obj, QString const &key, bool &result); + +bool read_required_point(QJsonObject const &obj, QString const &key, QPointF &result); + +} // namespace QtNodes::detail diff --git a/include/QtNodes/internal/Style.hpp b/include/QtNodes/internal/Style.hpp index f878083e2..f0adeed77 100644 --- a/include/QtNodes/internal/Style.hpp +++ b/include/QtNodes/internal/Style.hpp @@ -3,16 +3,14 @@ #include #include #include +#include #include -#include #include namespace QtNodes { -class Style // : public QObject +class Style { - //Q_OBJECT - public: virtual ~Style() = default; @@ -45,4 +43,66 @@ class Style // : public QObject } }; +namespace detail { + +inline bool readColor(QJsonObject const &obj, QString const &key, QColor &color) +{ + if (!obj.contains(key)) + return false; + + QJsonValue value = obj[key]; + if (value.isArray()) { + auto colorArray = value.toArray(); + int rgb[] = {colorArray[0].toInt(), colorArray[1].toInt(), colorArray[2].toInt()}; + color = QColor(rgb[0], rgb[1], rgb[2]); + } else { + color = QColor(value.toString()); + } + return true; +} + +inline void writeColor(QJsonObject &obj, QString const &key, QColor const &color) +{ + obj[key] = color.name(); +} + +inline bool readFloat(QJsonObject const &obj, QString const &key, double &val) +{ + if (!obj.contains(key)) + return false; + val = obj[key].toDouble(); + return true; +} + +inline bool readFloat(QJsonObject const &obj, QString const &key, float &val) +{ + double tmp{}; + + if (!readFloat(obj, key, tmp)) + return false; + + val = static_cast(tmp); + return true; +} + +inline void writeFloat(QJsonObject &obj, QString const &key, double val) +{ + obj[key] = val; +} + +inline bool readBool(QJsonObject const &obj, QString const &key, bool &val) +{ + if (!obj.contains(key)) + return false; + val = obj[key].toBool(); + return true; +} + +inline void writeBool(QJsonObject &obj, QString const &key, bool val) +{ + obj[key] = val; +} + +} // namespace detail + } // namespace QtNodes diff --git a/include/QtNodes/internal/UndoCommands.hpp b/include/QtNodes/internal/UndoCommands.hpp index 7aed4d60b..b2f61c953 100644 --- a/include/QtNodes/internal/UndoCommands.hpp +++ b/include/QtNodes/internal/UndoCommands.hpp @@ -64,7 +64,7 @@ class NODE_EDITOR_PUBLIC PasteCommand : public QUndoCommand private: BasicGraphicsScene *_scene; - QPointF const &_mouseScenePos; + QPointF _mouseScenePos; QJsonObject _newSceneJson; }; diff --git a/src/AbstractGraphModel.cpp b/src/AbstractGraphModel.cpp index 3ea47435d..c16594a9a 100644 --- a/src/AbstractGraphModel.cpp +++ b/src/AbstractGraphModel.cpp @@ -2,6 +2,8 @@ #include +#include + namespace QtNodes { void AbstractGraphModel::portsAboutToBeDeleted(NodeId const nodeId, @@ -11,11 +13,9 @@ void AbstractGraphModel::portsAboutToBeDeleted(NodeId const nodeId, { _shiftedByDynamicPortsConnections.clear(); - auto portCountRole = portType == PortType::In ? NodeRole::InPortCount : NodeRole::OutPortCount; - - unsigned int portCount = nodeData(nodeId, portCountRole).toUInt(); + unsigned int portCount = nodeData(nodeId, portCountRole(portType)).toUInt(); - if (first > portCount - 1) + if (portCount == 0 || first >= portCount) return; if (last < first) @@ -24,9 +24,14 @@ void AbstractGraphModel::portsAboutToBeDeleted(NodeId const nodeId, auto clampedLast = std::min(last, portCount - 1); for (PortIndex portIndex = first; portIndex <= clampedLast; ++portIndex) { - std::unordered_set conns = connections(nodeId, portType, portIndex); + std::vector conns; + auto const &attachedConnections = connections(nodeId, portType, portIndex); + conns.reserve(attachedConnections.size()); + for (auto const &connectionId : attachedConnections) { + conns.push_back(connectionId); + } - for (auto connectionId : conns) { + for (auto const connectionId : conns) { deleteConnection(connectionId); } } @@ -34,9 +39,14 @@ void AbstractGraphModel::portsAboutToBeDeleted(NodeId const nodeId, std::size_t const nRemovedPorts = clampedLast - first + 1; for (PortIndex portIndex = clampedLast + 1; portIndex < portCount; ++portIndex) { - std::unordered_set conns = connections(nodeId, portType, portIndex); + std::vector conns; + auto const &attachedConnections = connections(nodeId, portType, portIndex); + conns.reserve(attachedConnections.size()); + for (auto const &connectionId : attachedConnections) { + conns.push_back(connectionId); + } - for (auto connectionId : conns) { + for (auto const connectionId : conns) { // Erases the information about the port on one side; auto c = makeIncompleteConnectionId(connectionId, portType); @@ -67,9 +77,7 @@ void AbstractGraphModel::portsAboutToBeInserted(NodeId const nodeId, { _shiftedByDynamicPortsConnections.clear(); - auto portCountRole = portType == PortType::In ? NodeRole::InPortCount : NodeRole::OutPortCount; - - unsigned int portCount = nodeData(nodeId, portCountRole).toUInt(); + unsigned int portCount = nodeData(nodeId, portCountRole(portType)).toUInt(); if (first > portCount) return; @@ -80,9 +88,14 @@ void AbstractGraphModel::portsAboutToBeInserted(NodeId const nodeId, std::size_t const nNewPorts = last - first + 1; for (PortIndex portIndex = first; portIndex < portCount; ++portIndex) { - std::unordered_set conns = connections(nodeId, portType, portIndex); + std::vector conns; + auto const &attachedConnections = connections(nodeId, portType, portIndex); + conns.reserve(attachedConnections.size()); + for (auto const &connectionId : attachedConnections) { + conns.push_back(connectionId); + } - for (auto connectionId : conns) { + for (auto const connectionId : conns) { // Erases the information about the port on one side; auto c = makeIncompleteConnectionId(connectionId, portType); diff --git a/src/AbstractNodeGeometry.cpp b/src/AbstractNodeGeometry.cpp index f6c893df7..12629a044 100644 --- a/src/AbstractNodeGeometry.cpp +++ b/src/AbstractNodeGeometry.cpp @@ -1,10 +1,9 @@ #include "AbstractNodeGeometry.hpp" #include "AbstractGraphModel.hpp" +#include "ConnectionIdUtils.hpp" #include "StyleCollection.hpp" -#include - #include namespace QtNodes { @@ -36,12 +35,9 @@ PortIndex AbstractNodeGeometry::checkPortHit(NodeId const nodeId, if (portType == PortType::None) return result; - double const tolerance = 2.0 * nodeStyle.ConnectionPointDiameter; + double const tolerance = 2.0 * nodeStyle.connectionPointDiameter(); - size_t const n = _graphModel.nodeData(nodeId, - (portType == PortType::Out) - ? NodeRole::OutPortCount - : NodeRole::InPortCount); + size_t const n = _graphModel.nodeData(nodeId, portCountRole(portType)); for (unsigned int portIndex = 0; portIndex < n; ++portIndex) { auto pp = portPosition(nodeId, portType, portIndex); diff --git a/src/BasicGraphicsScene.cpp b/src/BasicGraphicsScene.cpp index adf928270..1a7d776e8 100644 --- a/src/BasicGraphicsScene.cpp +++ b/src/BasicGraphicsScene.cpp @@ -14,70 +14,11 @@ #include -#include -#include -#include -#include -#include #include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include -#include -#include -#include -#include #include -namespace { - -using QtNodes::GroupId; -using QtNodes::InvalidGroupId; -using QtNodes::InvalidNodeId; -using QtNodes::NodeId; - -NodeId jsonValueToNodeId(QJsonValue const &value) -{ - if (value.isDouble()) { - return static_cast(value.toInt()); - } - - if (value.isString()) { - auto const textValue = value.toString(); - - bool ok = false; - auto const numericValue = textValue.toULongLong(&ok, 10); - if (ok) { - return static_cast(numericValue); - } - - QUuid uuidValue(textValue); - if (!uuidValue.isNull()) { - auto const bytes = uuidValue.toRfc4122(); - if (bytes.size() >= static_cast(sizeof(quint32))) { - QDataStream stream(bytes); - quint32 value32 = 0U; - stream >> value32; - return static_cast(value32); - } - } - } - - return InvalidNodeId; -} - -} // namespace - namespace QtNodes { BasicGraphicsScene::BasicGraphicsScene(AbstractGraphModel &graphModel, QObject *parent) @@ -132,12 +73,12 @@ BasicGraphicsScene::BasicGraphicsScene(AbstractGraphModel &graphModel, QObject * BasicGraphicsScene::~BasicGraphicsScene() = default; -AbstractGraphModel const &BasicGraphicsScene::graphModel() const +AbstractGraphModel const &BasicGraphicsScene::graphModel() const noexcept { return _graphModel; } -AbstractGraphModel &BasicGraphicsScene::graphModel() +AbstractGraphModel &BasicGraphicsScene::graphModel() noexcept { return _graphModel; } @@ -226,33 +167,16 @@ void BasicGraphicsScene::resetDraftConnection() void BasicGraphicsScene::clearScene() { + std::vector nodeIds; auto const &allNodeIds = graphModel().allNodeIds(); - - for (auto nodeId : allNodeIds) { - graphModel().deleteNode(nodeId); + nodeIds.reserve(allNodeIds.size()); + for (auto const nodeId : allNodeIds) { + nodeIds.push_back(nodeId); } -} -std::vector> BasicGraphicsScene::connectionsWithinGroup(GroupId groupID) -{ - if (!_groupingEnabled) - return {}; - - std::vector> ret{}; - - for (auto const &connection : _connectionGraphicsObjects) { - auto outNode = nodeGraphicsObject(connection.first.outNodeId); - auto inNode = nodeGraphicsObject(connection.first.inNodeId); - if (outNode && inNode) { - auto group1 = outNode->nodeGroup().lock(); - auto group2 = inNode->nodeGroup().lock(); - if (group1 && group2 && group1->id() == group2->id() && group1->id() == groupID) { - ret.push_back(std::make_shared(connection.first)); - } - } + for (auto const nodeId : nodeIds) { + graphModel().deleteNode(nodeId); } - - return ret; } NodeGraphicsObject *BasicGraphicsScene::nodeGraphicsObject(NodeId nodeId) @@ -304,11 +228,12 @@ QMenu *BasicGraphicsScene::createSceneMenu(QPointF const scenePos) void BasicGraphicsScene::traverseGraphAndPopulateGraphicsObjects() { - auto allNodeIds = _graphModel.allNodeIds(); + auto const &allNodeIds = _graphModel.allNodeIds(); // First create all the nodes. for (NodeId const nodeId : allNodeIds) { _nodeGraphicsObjects[nodeId] = std::make_unique(*this, nodeId); + _nodeGraphicsObjects[nodeId]->updateValidationTooltip(); } // Then for each node check output connections and insert them. @@ -332,6 +257,7 @@ void BasicGraphicsScene::updateAttachedNodes(ConnectionId const connectionId, auto node = nodeGraphicsObject(getNodeId(portType, connectionId)); if (node) { + node->updateValidationTooltip(); node->update(); } } @@ -379,6 +305,7 @@ void BasicGraphicsScene::onNodeDeleted(NodeId const nodeId) void BasicGraphicsScene::onNodeCreated(NodeId const nodeId) { _nodeGraphicsObjects[nodeId] = std::make_unique(*this, nodeId); + _nodeGraphicsObjects[nodeId]->updateValidationTooltip(); Q_EMIT modified(this); } @@ -388,6 +315,9 @@ void BasicGraphicsScene::onNodePositionUpdated(NodeId const nodeId) auto node = nodeGraphicsObject(nodeId); if (node) { node->setPos(_graphModel.nodeData(nodeId, NodeRole::Position).value()); + if (auto group = node->nodeGroup().lock()) { + group->groupGraphicsObject().updateGroupGeometry(); + } node->update(); _nodeDrag = true; } @@ -402,6 +332,10 @@ void BasicGraphicsScene::onNodeUpdated(NodeId const nodeId) _nodeGeometry->recomputeSize(nodeId); + node->updateValidationTooltip(); + if (auto group = node->nodeGroup().lock()) { + group->groupGraphicsObject().updateGroupGeometry(); + } node->updateQWidgetEmbedPos(); node->update(); node->moveConnections(); @@ -433,9 +367,9 @@ void BasicGraphicsScene::freezeModelAndConnections(bool isFreeze) if (auto n = qgraphicsitem_cast(item)) { int portCount = graphModel().nodeData(n->nodeId(), NodeRole::OutPortCount).toInt(); for (int i = 0; i < portCount; i++) { - auto graphConnections = graphModel().connections(n->nodeId(), - QtNodes::PortType::Out, - QtNodes::PortIndex(i)); + auto const &graphConnections = graphModel().connections(n->nodeId(), + QtNodes::PortType::Out, + QtNodes::PortIndex(i)); for (auto const &c : graphConnections) { if (auto *cgo = connectionGraphicsObject(c)) { @@ -454,128 +388,36 @@ void BasicGraphicsScene::freezeModelAndConnections(bool isFreeze) } } -std::weak_ptr BasicGraphicsScene::createGroup(std::vector &nodes, - QString groupName, - GroupId groupId) -{ - if (!_groupingEnabled) - return std::weak_ptr(); - - if (nodes.empty()) - return std::weak_ptr(); - - for (auto *node : nodes) { - if (!node->nodeGroup().expired()) - removeNodeFromGroup(node->nodeId()); - } - - if (groupName.isEmpty()) { - groupName = "Group " + QString::number(NodeGroup::groupCount()); - } - - if (groupId == InvalidGroupId) { - groupId = nextGroupId(); - } else { - if (_groups.count(groupId) != 0) { - throw std::runtime_error("Group identifier collision"); - } - - if (groupId >= _nextGroupId && _nextGroupId != InvalidGroupId) { - _nextGroupId = groupId + 1; - } - } - - auto group = std::make_shared(nodes, groupId, groupName, this); - auto ggo = std::make_unique(*this, *group); - - group->setGraphicsObject(std::move(ggo)); - - for (auto &nodePtr : nodes) { - auto node = _nodeGraphicsObjects[nodePtr->nodeId()].get(); - - node->setNodeGroup(group); - } - - std::weak_ptr groupWeakPtr = group; - - _groups[group->id()] = std::move(group); - - return groupWeakPtr; -} - -std::vector BasicGraphicsScene::selectedNodes() const -{ - QList graphicsItems = selectedItems(); - - std::vector result; - result.reserve(graphicsItems.size()); - - for (QGraphicsItem *item : graphicsItems) { - auto ngo = qgraphicsitem_cast(item); - - if (ngo) { - result.push_back(ngo); - } - } - - return result; -} - -std::vector BasicGraphicsScene::selectedGroups() const +namespace { +template +std::vector selectedItemsOfType(QGraphicsScene const *scene) { - if (!_groupingEnabled) - return {}; - - QList graphicsItems = selectedItems(); + QList graphicsItems = scene->selectedItems(); - std::vector result; + std::vector result; result.reserve(graphicsItems.size()); for (QGraphicsItem *item : graphicsItems) { - auto ngo = qgraphicsitem_cast(item); - - if (ngo) { - result.push_back(ngo); + if (auto typed = qgraphicsitem_cast(item)) { + result.push_back(typed); } } return result; } +} // namespace -void BasicGraphicsScene::addNodeToGroup(NodeId nodeId, GroupId groupId) +std::vector BasicGraphicsScene::selectedNodes() const { - if (!_groupingEnabled) - return; - - auto groupIt = _groups.find(groupId); - auto nodeIt = _nodeGraphicsObjects.find(nodeId); - if (groupIt == _groups.end() || nodeIt == _nodeGraphicsObjects.end()) - return; - - auto group = groupIt->second; - auto node = nodeIt->second.get(); - group->addNode(node); - node->setNodeGroup(group); + return selectedItemsOfType(this); } -void BasicGraphicsScene::removeNodeFromGroup(NodeId nodeId) +std::vector BasicGraphicsScene::selectedGroups() const { if (!_groupingEnabled) - return; - - auto nodeIt = _nodeGraphicsObjects.find(nodeId); - if (nodeIt == _nodeGraphicsObjects.end()) - return; + return {}; - auto group = nodeIt->second->nodeGroup().lock(); - if (group) { - group->removeNode(nodeIt->second.get()); - if (group->empty()) { - _groups.erase(group->id()); - } - } - nodeIt->second->unsetNodeGroup(); - nodeIt->second->lock(false); + return selectedItemsOfType(this); } std::weak_ptr BasicGraphicsScene::createGroupFromSelection(QString groupName) @@ -587,97 +429,6 @@ std::weak_ptr BasicGraphicsScene::createGroupFromSelection(Q return createGroup(nodes, groupName); } -NodeGraphicsObject &BasicGraphicsScene::loadNodeToMap(QJsonObject nodeJson, bool keepOriginalId) -{ - NodeId newNodeId = InvalidNodeId; - - if (keepOriginalId) { - newNodeId = jsonValueToNodeId(nodeJson["id"]); - } - - if (newNodeId == InvalidNodeId) { - newNodeId = _graphModel.newNodeId(); - nodeJson["id"] = static_cast(newNodeId); - } - - _graphModel.loadNode(nodeJson); - - auto *nodeObject = nodeGraphicsObject(newNodeId); - if (!nodeObject) { - auto graphicsObject = std::make_unique(*this, newNodeId); - nodeObject = graphicsObject.get(); - _nodeGraphicsObjects[newNodeId] = std::move(graphicsObject); - } - - return *nodeObject; -} - -void BasicGraphicsScene::loadConnectionToMap(QJsonObject const &connectionJson, - std::unordered_map const &nodeIdMap) -{ - ConnectionId connId = fromJson(connectionJson); - - auto const outIt = nodeIdMap.find(connId.outNodeId); - auto const inIt = nodeIdMap.find(connId.inNodeId); - - if (outIt == nodeIdMap.end() || inIt == nodeIdMap.end()) { - return; - } - - ConnectionId remapped{outIt->second, connId.outPortIndex, inIt->second, connId.inPortIndex}; - - if (_graphModel.connectionExists(remapped)) { - return; - } - - if (_graphModel.connectionPossible(remapped)) { - _graphModel.addConnection(remapped); - } -} - -std::pair, std::unordered_map> -BasicGraphicsScene::restoreGroup(QJsonObject const &groupJson) -{ - if (!_groupingEnabled) - return {std::weak_ptr(), {}}; - - // since the new nodes will have the same IDs as in the file and the connections - // need these old IDs to be restored, we must create new IDs and map them to the - // old ones so the connections are properly restored - std::unordered_map IDsMap{}; - std::unordered_map nodeIdMap{}; - - std::vector group_children{}; - - QJsonArray nodesJson = groupJson["nodes"].toArray(); - for (const QJsonValueRef nodeJson : nodesJson) { - QJsonObject nodeObject = nodeJson.toObject(); - NodeId const oldNodeId = jsonValueToNodeId(nodeObject["id"]); - - NodeGraphicsObject &nodeRef = loadNodeToMap(nodeObject, false); - NodeId const newNodeId = nodeRef.nodeId(); - - if (oldNodeId != InvalidNodeId) { - nodeIdMap.emplace(oldNodeId, newNodeId); - IDsMap.emplace(static_cast(oldNodeId), static_cast(newNodeId)); - } - - group_children.push_back(&nodeRef); - } - - QJsonArray connectionJsonArray = groupJson["connections"].toArray(); - for (auto connection : connectionJsonArray) { - loadConnectionToMap(connection.toObject(), nodeIdMap); - } - - return std::make_pair(createGroup(group_children, groupJson["name"].toString()), IDsMap); -} - -std::unordered_map> const &BasicGraphicsScene::groups() const -{ - return _groups; -} - QMenu *BasicGraphicsScene::createStdMenu(QPointF const scenePos) { Q_UNUSED(scenePos); @@ -758,79 +509,4 @@ QMenu *BasicGraphicsScene::createGroupMenu(QPointF const scenePos, GroupGraphics return menu; } -void BasicGraphicsScene::saveGroupFile(GroupId groupID) -{ - if (!_groupingEnabled) - return; - - QString fileName = QFileDialog::getSaveFileName(nullptr, - tr("Save Node Group"), - QDir::homePath(), - tr("Node Group files (*.group)")); - - if (!fileName.isEmpty()) { - if (!fileName.endsWith("group", Qt::CaseInsensitive)) - fileName += ".group"; - - if (auto groupIt = _groups.find(groupID); groupIt != _groups.end()) { - QFile file(fileName); - if (file.open(QIODevice::WriteOnly)) { - file.write(groupIt->second->saveToFile()); - } else { - qDebug() << "Error saving group file!"; - } - } else { - qDebug() << "Error! Couldn't find group while saving."; - } - } -} - -std::weak_ptr BasicGraphicsScene::loadGroupFile() -{ - if (!_groupingEnabled) - return std::weak_ptr(); - - QString fileName = QFileDialog::getOpenFileName(nullptr, - tr("Open Node Group"), - QDir::currentPath(), - tr("Node Group files (*.group)")); - - if (!QFileInfo::exists(fileName)) - return std::weak_ptr(); - - QFile file(fileName); - - if (!file.open(QIODevice::ReadOnly)) { - qDebug() << "Error loading group file!"; - } - - QDir d = QFileInfo(fileName).absoluteDir(); - QString absolute = d.absolutePath(); - QDir::setCurrent(absolute); - - QByteArray wholeFile = file.readAll(); - - const QJsonObject fileJson = QJsonDocument::fromJson(wholeFile).object(); - - return restoreGroup(fileJson).first; -} - -GroupId BasicGraphicsScene::nextGroupId() -{ - if (_nextGroupId == InvalidGroupId) { - throw std::runtime_error("No available group identifiers"); - } - - while (_groups.count(_nextGroupId) != 0) { - ++_nextGroupId; - if (_nextGroupId == InvalidGroupId) { - throw std::runtime_error("No available group identifiers"); - } - } - - GroupId const newId = _nextGroupId; - ++_nextGroupId; - return newId; -} - } // namespace QtNodes diff --git a/src/BasicGraphicsSceneGroups.cpp b/src/BasicGraphicsSceneGroups.cpp new file mode 100644 index 000000000..0d18e3385 --- /dev/null +++ b/src/BasicGraphicsSceneGroups.cpp @@ -0,0 +1,398 @@ +#include "BasicGraphicsScene.hpp" + +#include "ConnectionIdUtils.hpp" +#include "NodeGraphicsObject.hpp" +#include "SerializationValidation.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace { + +using QtNodes::ConnectionId; +using QtNodes::GroupId; +using QtNodes::InvalidGroupId; +using QtNodes::InvalidNodeId; +using QtNodes::NodeId; + +NodeId json_value_to_node_id(QJsonValue const &value) +{ + NodeId nodeId = InvalidNodeId; + + if (!QtNodes::detail::read_node_id(value, nodeId)) { + return InvalidNodeId; + } + + return nodeId; +} + +void validate_group_json(QJsonObject const &groupJson) +{ + QString groupName; + if (!QtNodes::detail::read_required_string(groupJson, "name", groupName)) { + throw std::logic_error("Serialized group contains invalid name"); + } + Q_UNUSED(groupName); + + QJsonArray nodesJson; + if (!QtNodes::detail::read_required_array(groupJson, "nodes", nodesJson)) { + throw std::logic_error("Serialized group contains invalid nodes array"); + } + + QJsonArray connectionsJson; + if (!QtNodes::detail::read_required_array(groupJson, "connections", connectionsJson)) { + throw std::logic_error("Serialized group contains invalid connections array"); + } + + for (QJsonValue const &nodeValue : nodesJson) { + if (!nodeValue.isObject()) { + throw std::logic_error("Serialized group contains invalid node entry"); + } + } + + for (QJsonValue const &connectionValue : connectionsJson) { + if (!connectionValue.isObject()) { + throw std::logic_error("Serialized group contains invalid connection entry"); + } + + ConnectionId connId; + if (!QtNodes::tryFromJson(connectionValue.toObject(), connId)) { + throw std::logic_error("Serialized group contains invalid connection id"); + } + } +} + +} // namespace + +namespace QtNodes { + +std::vector BasicGraphicsScene::connectionsWithinGroup(GroupId groupID) +{ + if (!_groupingEnabled) + return {}; + + std::vector ret{}; + ret.reserve(_connectionGraphicsObjects.size()); + + for (auto const &connection : _connectionGraphicsObjects) { + auto outNode = nodeGraphicsObject(connection.first.outNodeId); + auto inNode = nodeGraphicsObject(connection.first.inNodeId); + if (outNode && inNode) { + auto group1 = outNode->nodeGroup().lock(); + auto group2 = inNode->nodeGroup().lock(); + if (group1 && group2 && group1->id() == group2->id() && group1->id() == groupID) { + ret.push_back(connection.first); + } + } + } + + return ret; +} + +std::weak_ptr BasicGraphicsScene::createGroup(std::vector &nodes, + QString groupName, + GroupId groupId) +{ + if (!_groupingEnabled || nodes.empty()) + return {}; + + for (auto *node : nodes) { + if (!node->nodeGroup().expired()) + removeNodeFromGroup(node->nodeId()); + } + + if (groupName.isEmpty()) { + groupName = "Group " + QString::number(NodeGroup::groupCount()); + } + + if (groupId == InvalidGroupId) { + groupId = nextGroupId(); + } else { + if (_groups.count(groupId) != 0) { + throw std::runtime_error("Group identifier collision"); + } + + if (groupId >= _nextGroupId && _nextGroupId != InvalidGroupId) { + _nextGroupId = groupId + 1; + } + } + + auto group = std::make_shared(nodes, groupId, groupName, this); + auto ggo = std::make_unique(*this, *group); + + group->setGraphicsObject(std::move(ggo)); + + for (auto *nodePtr : nodes) { + auto *node = _nodeGraphicsObjects[nodePtr->nodeId()].get(); + node->setNodeGroup(group); + } + + std::weak_ptr groupWeakPtr = group; + _groups[group->id()] = std::move(group); + return groupWeakPtr; +} + +void BasicGraphicsScene::addNodeToGroup(NodeId nodeId, GroupId groupId) +{ + if (!_groupingEnabled) + return; + + auto groupIt = _groups.find(groupId); + auto nodeIt = _nodeGraphicsObjects.find(nodeId); + if (groupIt == _groups.end() || nodeIt == _nodeGraphicsObjects.end()) + return; + + auto group = groupIt->second; + auto *node = nodeIt->second.get(); + group->addNode(node); + node->setNodeGroup(group); +} + +void BasicGraphicsScene::removeNodeFromGroup(NodeId nodeId) +{ + if (!_groupingEnabled) + return; + + auto nodeIt = _nodeGraphicsObjects.find(nodeId); + if (nodeIt == _nodeGraphicsObjects.end()) + return; + + auto group = nodeIt->second->nodeGroup().lock(); + if (group) { + group->removeNode(nodeIt->second.get()); + if (group->empty()) { + _groups.erase(group->id()); + } + } + nodeIt->second->unsetNodeGroup(); + nodeIt->second->lock(false); +} + +NodeGraphicsObject &BasicGraphicsScene::loadNodeToMap(QJsonObject nodeJson, bool keepOriginalId) +{ + NodeId newNodeId = InvalidNodeId; + + if (keepOriginalId) { + newNodeId = json_value_to_node_id(nodeJson["id"]); + if (newNodeId == InvalidNodeId) { + throw std::logic_error("Invalid node id in serialized node"); + } + } else { + newNodeId = _graphModel.newNodeId(); + nodeJson["id"] = static_cast(newNodeId); + } + + _graphModel.loadNode(nodeJson); + + auto *nodeObject = nodeGraphicsObject(newNodeId); + if (!nodeObject) { + auto graphicsObject = std::make_unique(*this, newNodeId); + nodeObject = graphicsObject.get(); + _nodeGraphicsObjects[newNodeId] = std::move(graphicsObject); + } + + return *nodeObject; +} + +void BasicGraphicsScene::loadConnectionToMap(QJsonObject const &connectionJson, + std::unordered_map const &nodeIdMap) +{ + ConnectionId connId; + if (!tryFromJson(connectionJson, connId)) { + throw std::logic_error("Invalid serialized connection"); + } + + auto const outIt = nodeIdMap.find(connId.outNodeId); + auto const inIt = nodeIdMap.find(connId.inNodeId); + + if (outIt == nodeIdMap.end() || inIt == nodeIdMap.end()) { + throw std::logic_error("Serialized connection references unknown node id"); + } + + ConnectionId remapped{outIt->second, connId.outPortIndex, inIt->second, connId.inPortIndex}; + + if (_graphModel.connectionExists(remapped)) { + return; + } + + if (!_graphModel.connectionPossible(remapped)) { + throw std::logic_error("Serialized connection is not valid for restored nodes"); + } + + _graphModel.addConnection(remapped); +} + +std::pair, std::unordered_map> +BasicGraphicsScene::restoreGroup(QJsonObject const &groupJson) +{ + if (!_groupingEnabled) + return {{}, {}}; + + validate_group_json(groupJson); + + std::unordered_map IDsMap{}; + std::unordered_map nodeIdMap{}; + std::vector groupChildren{}; + std::vector createdNodeIds{}; + + try { + QJsonArray const nodesJson = groupJson["nodes"].toArray(); + for (QJsonValue const &nodeJson : nodesJson) { + QJsonObject nodeObject = nodeJson.toObject(); + NodeId const oldNodeId = json_value_to_node_id(nodeObject["id"]); + + NodeGraphicsObject &nodeRef = loadNodeToMap(nodeObject, false); + NodeId const newNodeId = nodeRef.nodeId(); + + createdNodeIds.push_back(newNodeId); + + if (oldNodeId != InvalidNodeId) { + nodeIdMap.emplace(oldNodeId, newNodeId); + IDsMap.emplace(static_cast(oldNodeId), static_cast(newNodeId)); + } + + groupChildren.push_back(&nodeRef); + } + + QJsonArray const connectionJsonArray = groupJson["connections"].toArray(); + for (QJsonValue const &connection : connectionJsonArray) { + loadConnectionToMap(connection.toObject(), nodeIdMap); + } + + return {createGroup(groupChildren, groupJson["name"].toString()), IDsMap}; + } catch (...) { + for (NodeId const nodeId : createdNodeIds) { + if (_graphModel.nodeExists(nodeId)) { + _graphModel.deleteNode(nodeId); + } + } + + throw; + } +} + +std::unordered_map> const &BasicGraphicsScene::groups() const +{ + return _groups; +} + +void BasicGraphicsScene::saveGroupFile(GroupId groupID) +{ + if (!_groupingEnabled) + return; + + QString fileName = QFileDialog::getSaveFileName(nullptr, + tr("Save Node Group"), + QDir::homePath(), + tr("Node Group files (*.group)")); + + if (fileName.isEmpty()) + return; + + if (!fileName.endsWith("group", Qt::CaseInsensitive)) + fileName += ".group"; + + auto groupIt = _groups.find(groupID); + if (groupIt == _groups.end()) { + qDebug() << "Error! Couldn't find group while saving."; + return; + } + + QFile file(fileName); + if (file.open(QIODevice::WriteOnly)) { + file.write(groupIt->second->saveToFile()); + } else { + qDebug() << "Error saving group file!"; + } +} + +std::weak_ptr BasicGraphicsScene::loadGroupFile() +{ + if (!_groupingEnabled) + return {}; + + QString fileName = QFileDialog::getOpenFileName(nullptr, + tr("Open Node Group"), + QDir::currentPath(), + tr("Node Group files (*.group)")); + + if (!QFileInfo::exists(fileName)) + return {}; + + QFile file(fileName); + + if (!file.open(QIODevice::ReadOnly)) { + qDebug() << "Error loading group file!"; + return {}; + } + + struct CurrentDirGuard + { + QString path; + + ~CurrentDirGuard() + { + if (!path.isEmpty()) { + QDir::setCurrent(path); + } + } + }; + + CurrentDirGuard currentDirGuard{QDir::currentPath()}; + QDir const directory = QFileInfo(fileName).absoluteDir(); + QDir::setCurrent(directory.absolutePath()); + + QByteArray const wholeFile = file.readAll(); + + QJsonParseError parseError{}; + QJsonDocument const groupDocument = QJsonDocument::fromJson(wholeFile, &parseError); + if (parseError.error != QJsonParseError::NoError || !groupDocument.isObject()) { + return {}; + } + + try { + return restoreGroup(groupDocument.object()).first; + } catch (std::exception const &ex) { + qWarning() << "Failed to load group file:" << ex.what(); + return {}; + } catch (...) { + qWarning() << "Failed to load group file due to an unknown error"; + return {}; + } +} + +GroupId BasicGraphicsScene::nextGroupId() +{ + if (_nextGroupId == InvalidGroupId) { + throw std::runtime_error("No available group identifiers"); + } + + while (_groups.count(_nextGroupId) != 0) { + ++_nextGroupId; + if (_nextGroupId == InvalidGroupId) { + throw std::runtime_error("No available group identifiers"); + } + } + + GroupId const newId = _nextGroupId; + ++_nextGroupId; + return newId; +} + +} // namespace QtNodes diff --git a/src/ConnectionGraphicsObject.cpp b/src/ConnectionGraphicsObject.cpp index 05ae46b34..9cee2cfec 100644 --- a/src/ConnectionGraphicsObject.cpp +++ b/src/ConnectionGraphicsObject.cpp @@ -12,6 +12,8 @@ #include "StyleCollection.hpp" #include "locateNode.hpp" +#include +#include #include #include #include @@ -40,8 +42,6 @@ ConnectionGraphicsObject::ConnectionGraphicsObject(BasicGraphicsScene &scene, setAcceptHoverEvents(true); - //addGraphicsEffect(); - setZValue(-1.0); initializePosition(); @@ -88,48 +88,91 @@ AbstractGraphModel &ConnectionGraphicsObject::graphModel() const BasicGraphicsScene *ConnectionGraphicsObject::nodeScene() const { - return dynamic_cast(scene()); + auto *sceneObject = dynamic_cast(scene()); + Q_ASSERT(sceneObject != nullptr); + return sceneObject; } -ConnectionId const &ConnectionGraphicsObject::connectionId() const +ConnectionId const &ConnectionGraphicsObject::connectionId() const noexcept { return _connectionId; } -QRectF ConnectionGraphicsObject::boundingRect() const +void ConnectionGraphicsObject::rebuildCachedGeometry() const { - auto points = pointsC1C2(); + if (!_geometryDirty) { + return; + } + _geometryDirty = false; - // `normalized()` fixes inverted rects. - QRectF basicRect = QRectF(_out, _in).normalized(); + // Cubic path + auto const c1c2 = pointsC1C2(); + _cachedCubicPath = QPainterPath(_out); + _cachedCubicPath.cubicTo(c1c2.first, c1c2.second, _in); - QRectF c1c2Rect = QRectF(points.first, points.second).normalized(); + for (int i = 0; i < k_path_sample_count; ++i) { + double const ratio = double(i) / (k_path_sample_count - 1); + _cachedSamplePoints[i] = _cachedCubicPath.pointAtPercent(ratio); + } + _cachedMidPoint = _cachedSamplePoints[k_path_sample_count / 2]; + // Bounding rect + QRectF basicRect = QRectF(_out, _in).normalized(); + QRectF c1c2Rect = QRectF(c1c2.first, c1c2.second).normalized(); QRectF commonRect = basicRect.united(c1c2Rect); auto const &connectionStyle = StyleCollection::connectionStyle(); float const diam = connectionStyle.pointDiameter(); QPointF const cornerOffset(diam, diam); - - // Expand rect by port circle diameter commonRect.setTopLeft(commonRect.topLeft() - cornerOffset); commonRect.setBottomRight(commonRect.bottomRight() + 2 * cornerOffset); + _cachedBoundingRect = commonRect; - return commonRect; + // Stroke path for hit testing + QPainterPath linearized(_out); + for (int i = 1; i < k_path_sample_count; ++i) { + linearized.lineTo(_cachedSamplePoints[i]); + } + QPainterPathStroker stroker; + stroker.setWidth(10.0); + _cachedStrokePath = stroker.createStroke(linearized); +} + +QRectF ConnectionGraphicsObject::boundingRect() const +{ + rebuildCachedGeometry(); + return _cachedBoundingRect; } QPainterPath ConnectionGraphicsObject::shape() const { -#ifdef DEBUG_DRAWING + rebuildCachedGeometry(); + return _cachedStrokePath; +} + +QPainterPath const &ConnectionGraphicsObject::cachedCubicPath() const +{ + rebuildCachedGeometry(); + return _cachedCubicPath; +} - //QPainterPath path; +QPainterPath const &ConnectionGraphicsObject::cachedStrokePath() const +{ + rebuildCachedGeometry(); + return _cachedStrokePath; +} - //path.addRect(boundingRect()); - //return path; +QPointF const &ConnectionGraphicsObject::cachedSamplePoint(int index) const +{ + rebuildCachedGeometry(); + Q_ASSERT(index >= 0 && index < k_path_sample_count); + return _cachedSamplePoints[index]; +} -#else - return nodeScene()->connectionPainter().getPainterStroke(*this); -#endif +QPointF const &ConnectionGraphicsObject::cachedMidPoint() const noexcept +{ + rebuildCachedGeometry(); + return _cachedMidPoint; } QPointF const &ConnectionGraphicsObject::endPoint(PortType portType) const @@ -145,11 +188,16 @@ void ConnectionGraphicsObject::setEndPoint(PortType portType, QPointF const &poi _in = point; else _out = point; + + _geometryDirty = true; } void ConnectionGraphicsObject::move() { - auto moveEnd = [this](ConnectionId cId, PortType portType) { + QPointF newOut = _out; + QPointF newIn = _in; + + auto moveEnd = [this](ConnectionId cId, PortType portType, QPointF &endPoint) { NodeId nodeId = getNodeId(portType, cId); if (nodeId == InvalidNodeId) @@ -165,16 +213,21 @@ void ConnectionGraphicsObject::move() getPortIndex(portType, cId), ngo->sceneTransform()); - QPointF connectionPos = sceneTransform().inverted().map(scenePos); - - setEndPoint(portType, connectionPos); + endPoint = sceneTransform().inverted().map(scenePos); } }; - moveEnd(_connectionId, PortType::Out); - moveEnd(_connectionId, PortType::In); + moveEnd(_connectionId, PortType::Out, newOut); + moveEnd(_connectionId, PortType::In, newIn); + + if (newOut == _out && newIn == _in) { + return; + } prepareGeometryChange(); + _out = newOut; + _in = newIn; + _geometryDirty = true; update(); } @@ -289,90 +342,53 @@ void ConnectionGraphicsObject::hoverLeaveEvent(QGraphicsSceneHoverEvent *event) std::pair ConnectionGraphicsObject::pointsC1C2() const { - switch (nodeScene()->orientation()) { - case Qt::Horizontal: - return pointsC1C2Horizontal(); - break; - - case Qt::Vertical: - return pointsC1C2Vertical(); - break; - } - - throw std::logic_error("Unreachable code after switch statement"); -} - -void ConnectionGraphicsObject::addGraphicsEffect() -{ - auto effect = new QGraphicsBlurEffect; - - effect->setBlurRadius(5); - setGraphicsEffect(effect); - - //auto effect = new QGraphicsDropShadowEffect; - //auto effect = new ConnectionBlurEffect(this); - //effect->setOffset(4, 4); - //effect->setColor(QColor(Qt::gray).darker(800)); + return computeControlPoints(nodeScene()->orientation()); } -std::pair ConnectionGraphicsObject::pointsC1C2Horizontal() const +std::pair ConnectionGraphicsObject::computeControlPoints( + Qt::Orientation orientation) const { double const defaultOffset = 200; - double xDistance = _in.x() - _out.x(); - - double horizontalOffset = qMin(defaultOffset, std::abs(xDistance)); - - double verticalOffset = 0; - - double ratioX = 0.5; - - if (xDistance <= 0) { - double yDistance = _in.y() - _out.y() + 20; - - double vector = yDistance < 0 ? -1.0 : 1.0; - - verticalOffset = qMin(defaultOffset, std::abs(yDistance)) * vector; - - ratioX = 1.0; - } - - horizontalOffset *= ratioX; - - QPointF c1(_out.x() + horizontalOffset, _out.y() + verticalOffset); - - QPointF c2(_in.x() - horizontalOffset, _in.y() - verticalOffset); - - return std::make_pair(c1, c2); -} - -std::pair ConnectionGraphicsObject::pointsC1C2Vertical() const -{ - double const defaultOffset = 200; + // In horizontal mode the primary axis is X; in vertical mode it is Y. + auto primary = [&](QPointF const &p) { + return orientation == Qt::Horizontal ? p.x() : p.y(); + }; + auto secondary = [&](QPointF const &p) { + return orientation == Qt::Horizontal ? p.y() : p.x(); + }; - double yDistance = _in.y() - _out.y(); + double primaryDistance = primary(_in) - primary(_out); - double verticalOffset = qMin(defaultOffset, std::abs(yDistance)); + double primaryOffset = qMin(defaultOffset, std::abs(primaryDistance)); - double horizontalOffset = 0; + double secondaryOffset = 0; - double ratioY = 0.5; + double ratio = 0.5; - if (yDistance <= 0) { - double xDistance = _in.x() - _out.x() + 20; + if (primaryDistance <= 0) { + double secDistance = secondary(_in) - secondary(_out) + 20; - double vector = xDistance < 0 ? -1.0 : 1.0; + double vector = secDistance < 0 ? -1.0 : 1.0; - horizontalOffset = qMin(defaultOffset, std::abs(xDistance)) * vector; + secondaryOffset = qMin(defaultOffset, std::abs(secDistance)) * vector; - ratioY = 1.0; + ratio = 1.0; } - verticalOffset *= ratioY; + primaryOffset *= ratio; - QPointF c1(_out.x() + horizontalOffset, _out.y() + verticalOffset); + double hOff, vOff; + if (orientation == Qt::Horizontal) { + hOff = primaryOffset; + vOff = secondaryOffset; + } else { + hOff = secondaryOffset; + vOff = primaryOffset; + } - QPointF c2(_in.x() - horizontalOffset, _in.y() - verticalOffset); + QPointF c1(_out.x() + hOff, _out.y() + vOff); + QPointF c2(_in.x() - hOff, _in.y() - vOff); return std::make_pair(c1, c2); } diff --git a/src/ConnectionState.cpp b/src/ConnectionState.cpp index eed24a2ff..aa8fa4eb5 100644 --- a/src/ConnectionState.cpp +++ b/src/ConnectionState.cpp @@ -5,15 +5,11 @@ #include "NodeGraphicsObject.hpp" #include -#include namespace QtNodes { -ConnectionState::~ConnectionState() -{ - //resetLastHoveredNode(); -} +ConnectionState::~ConnectionState() = default; PortType ConnectionState::requiredPort() const { @@ -34,12 +30,12 @@ bool ConnectionState::requiresPort() const return id.outNodeId == InvalidNodeId || id.inNodeId == InvalidNodeId; } -bool ConnectionState::hovered() const +bool ConnectionState::hovered() const noexcept { return _hovered; } -void ConnectionState::setHovered(bool hovered) +void ConnectionState::setHovered(bool hovered) noexcept { _hovered = hovered; } @@ -49,7 +45,7 @@ void ConnectionState::setLastHoveredNode(NodeId const nodeId) _lastHoveredNode = nodeId; } -NodeId ConnectionState::lastHoveredNode() const +NodeId ConnectionState::lastHoveredNode() const noexcept { return _lastHoveredNode; } @@ -57,8 +53,11 @@ NodeId ConnectionState::lastHoveredNode() const void ConnectionState::resetLastHoveredNode() { if (_lastHoveredNode != InvalidNodeId) { - auto ngo = _cgo.nodeScene()->nodeGraphicsObject(_lastHoveredNode); - ngo->update(); + if (auto *scene = _cgo.nodeScene()) { + if (auto *ngo = scene->nodeGraphicsObject(_lastHoveredNode)) { + ngo->update(); + } + } } _lastHoveredNode = InvalidNodeId; diff --git a/src/ConnectionStyle.cpp b/src/ConnectionStyle.cpp index 812658989..a784aa9de 100644 --- a/src/ConnectionStyle.cpp +++ b/src/ConnectionStyle.cpp @@ -2,15 +2,14 @@ #include "StyleCollection.hpp" -#include #include -#include #include #include using QtNodes::ConnectionStyle; +using namespace QtNodes::detail; inline void initResources() { @@ -40,103 +39,38 @@ void ConnectionStyle::setConnectionStyle(QString jsonText) StyleCollection::setConnectionStyle(style); } -#ifdef STYLE_DEBUG -#define CONNECTION_STYLE_CHECK_UNDEFINED_VALUE(v, variable) \ - { \ - if (v.type() == QJsonValue::Undefined || v.type() == QJsonValue::Null) \ - qWarning() << "Undefined value for parameter:" << #variable; \ - } -#else -#define CONNECTION_STYLE_CHECK_UNDEFINED_VALUE(v, variable) -#endif - -#define CONNECTION_VALUE_EXISTS(v) \ - (v.type() != QJsonValue::Undefined && v.type() != QJsonValue::Null) - -#define CONNECTION_STYLE_READ_COLOR(values, variable) \ - { \ - auto valueRef = values[#variable]; \ - CONNECTION_STYLE_CHECK_UNDEFINED_VALUE(valueRef, variable) \ - if (CONNECTION_VALUE_EXISTS(valueRef)) { \ - if (valueRef.isArray()) { \ - auto colorArray = valueRef.toArray(); \ - std::vector rgb; \ - rgb.reserve(3); \ - for (auto it = colorArray.begin(); it != colorArray.end(); ++it) { \ - rgb.push_back((*it).toInt()); \ - } \ - variable = QColor(rgb[0], rgb[1], rgb[2]); \ - } else { \ - variable = QColor(valueRef.toString()); \ - } \ - } \ - } - -#define CONNECTION_STYLE_WRITE_COLOR(values, variable) \ - { \ - values[#variable] = variable.name(); \ - } - -#define CONNECTION_STYLE_READ_FLOAT(values, variable) \ - { \ - auto valueRef = values[#variable]; \ - CONNECTION_STYLE_CHECK_UNDEFINED_VALUE(valueRef, variable) \ - if (CONNECTION_VALUE_EXISTS(valueRef)) \ - variable = valueRef.toDouble(); \ - } - -#define CONNECTION_STYLE_WRITE_FLOAT(values, variable) \ - { \ - values[#variable] = variable; \ - } - -#define CONNECTION_STYLE_READ_BOOL(values, variable) \ - { \ - auto valueRef = values[#variable]; \ - CONNECTION_STYLE_CHECK_UNDEFINED_VALUE(valueRef, variable) \ - if (CONNECTION_VALUE_EXISTS(valueRef)) \ - variable = valueRef.toBool(); \ - } - -#define CONNECTION_STYLE_WRITE_BOOL(values, variable) \ - { \ - values[#variable] = variable; \ - } - void ConnectionStyle::loadJson(QJsonObject const &json) { - QJsonValue nodeStyleValues = json["ConnectionStyle"]; - - QJsonObject obj = nodeStyleValues.toObject(); + QJsonObject obj = json["ConnectionStyle"].toObject(); - CONNECTION_STYLE_READ_COLOR(obj, ConstructionColor); - CONNECTION_STYLE_READ_COLOR(obj, NormalColor); - CONNECTION_STYLE_READ_COLOR(obj, SelectedColor); - CONNECTION_STYLE_READ_COLOR(obj, SelectedHaloColor); - CONNECTION_STYLE_READ_COLOR(obj, HoveredColor); + readColor(obj, "ConstructionColor", ConstructionColor); + readColor(obj, "NormalColor", NormalColor); + readColor(obj, "SelectedColor", SelectedColor); + readColor(obj, "SelectedHaloColor", SelectedHaloColor); + readColor(obj, "HoveredColor", HoveredColor); - CONNECTION_STYLE_READ_FLOAT(obj, LineWidth); - CONNECTION_STYLE_READ_FLOAT(obj, ConstructionLineWidth); - CONNECTION_STYLE_READ_FLOAT(obj, PointDiameter); + readFloat(obj, "LineWidth", LineWidth); + readFloat(obj, "ConstructionLineWidth", ConstructionLineWidth); + readFloat(obj, "PointDiameter", PointDiameter); - CONNECTION_STYLE_READ_BOOL(obj, UseDataDefinedColors); + readBool(obj, "UseDataDefinedColors", UseDataDefinedColors); } QJsonObject ConnectionStyle::toJson() const { QJsonObject obj; - CONNECTION_STYLE_WRITE_COLOR(obj, ConstructionColor); - CONNECTION_STYLE_WRITE_COLOR(obj, NormalColor); - CONNECTION_STYLE_WRITE_COLOR(obj, SelectedColor); - CONNECTION_STYLE_WRITE_COLOR(obj, SelectedHaloColor); - CONNECTION_STYLE_WRITE_COLOR(obj, HoveredColor); + writeColor(obj, "ConstructionColor", ConstructionColor); + writeColor(obj, "NormalColor", NormalColor); + writeColor(obj, "SelectedColor", SelectedColor); + writeColor(obj, "SelectedHaloColor", SelectedHaloColor); + writeColor(obj, "HoveredColor", HoveredColor); - CONNECTION_STYLE_WRITE_FLOAT(obj, LineWidth); - CONNECTION_STYLE_WRITE_FLOAT(obj, ConstructionLineWidth); - CONNECTION_STYLE_WRITE_FLOAT(obj, PointDiameter); + writeFloat(obj, "LineWidth", LineWidth); + writeFloat(obj, "ConstructionLineWidth", ConstructionLineWidth); + writeFloat(obj, "PointDiameter", PointDiameter); - CONNECTION_STYLE_WRITE_BOOL(obj, UseDataDefinedColors); + writeBool(obj, "UseDataDefinedColors", UseDataDefinedColors); QJsonObject root; root["ConnectionStyle"] = obj; diff --git a/src/DataFlowGraphModel.cpp b/src/DataFlowGraphModel.cpp index d269dc9db..4902a46a2 100644 --- a/src/DataFlowGraphModel.cpp +++ b/src/DataFlowGraphModel.cpp @@ -1,62 +1,69 @@ #include "DataFlowGraphModel.hpp" -#include "ConnectionIdHash.hpp" +#include "ConnectionIdUtils.hpp" #include "Definitions.hpp" #include +#include #include #include +#include +#include namespace QtNodes { -DataFlowGraphModel::DataFlowGraphModel(std::shared_ptr registry) - : _registry(std::move(registry)) - , _nextNodeId{0} -{} +namespace { -std::unordered_set DataFlowGraphModel::allNodeIds() const +NodeId json_value_to_node_id(QJsonValue const &value) { - std::unordered_set nodeIds; - for_each(_models.begin(), _models.end(), [&nodeIds](auto const &p) { nodeIds.insert(p.first); }); + NodeId nodeId = InvalidNodeId; + + if (!detail::read_node_id(value, nodeId)) { + return InvalidNodeId; + } - return nodeIds; + return nodeId; } -std::unordered_set DataFlowGraphModel::allConnectionIds(NodeId const nodeId) const +QPointF json_object_to_point(QJsonObject const &obj, QString const &key) { - std::unordered_set result; + QPointF point; - std::copy_if(_connectivity.begin(), - _connectivity.end(), - std::inserter(result, std::end(result)), - [&nodeId](ConnectionId const &cid) { - return cid.inNodeId == nodeId || cid.outNodeId == nodeId; - }); + if (!detail::read_required_point(obj, key, point)) { + throw std::logic_error("Invalid node position in serialized node"); + } - return result; + return point; } -std::unordered_set DataFlowGraphModel::connections(NodeId nodeId, - PortType portType, - PortIndex portIndex) const +} // namespace + +DataFlowGraphModel::DataFlowGraphModel(std::shared_ptr registry) + : _registry(std::move(registry)) + , _nextNodeId{0} +{} + +AbstractGraphModel::NodeIdSet const &DataFlowGraphModel::allNodeIds() const { - std::unordered_set result; + return _nodeIds; +} - std::copy_if(_connectivity.begin(), - _connectivity.end(), - std::inserter(result, std::end(result)), - [&portType, &portIndex, &nodeId](ConnectionId const &cid) { - return (getNodeId(portType, cid) == nodeId - && getPortIndex(portType, cid) == portIndex); - }); +AbstractGraphModel::ConnectionIdSet const & +DataFlowGraphModel::allConnectionIds(NodeId const nodeId) const +{ + return _connectionIndex.allConnectionIds(nodeId); +} - return result; +AbstractGraphModel::ConnectionIdSet const &DataFlowGraphModel::connections( + NodeId nodeId, PortType portType, PortIndex portIndex) const +{ + return _connectionIndex.connections(nodeId, portType, portIndex); } bool DataFlowGraphModel::connectionExists(ConnectionId const connectionId) const { - return (_connectivity.find(connectionId) != _connectivity.end()); + return _connectionIndex.contains(connectionId); } NodeId DataFlowGraphModel::addNode(QString const nodeType) @@ -66,40 +73,9 @@ NodeId DataFlowGraphModel::addNode(QString const nodeType) if (model) { NodeId newId = newNodeId(); - connect(model.get(), - &NodeDelegateModel::dataUpdated, - [newId, this](PortIndex const portIndex) { - onOutPortDataUpdated(newId, portIndex); - }); - - connect(model.get(), - &NodeDelegateModel::portsAboutToBeDeleted, - this, - [newId, this](PortType const portType, PortIndex const first, PortIndex const last) { - portsAboutToBeDeleted(newId, portType, first, last); - }); - - connect(model.get(), - &NodeDelegateModel::portsDeleted, - this, - &DataFlowGraphModel::portsDeleted); - - connect(model.get(), - &NodeDelegateModel::portsAboutToBeInserted, - this, - [newId, this](PortType const portType, PortIndex const first, PortIndex const last) { - portsAboutToBeInserted(newId, portType, first, last); - }); - - connect(model.get(), - &NodeDelegateModel::portsInserted, - this, - &DataFlowGraphModel::portsInserted); - - connect(model.get(), &NodeDelegateModel::requestNodeUpdate, this, [newId, this]() { - Q_EMIT nodeUpdated(newId); - }); + connectDelegateModel(model.get(), newId); + _nodeIds.insert(newId); _models[newId] = std::move(model); Q_EMIT nodeCreated(newId); @@ -120,10 +96,7 @@ bool DataFlowGraphModel::connectionPossible(ConnectionId const connectionId) con // Check port bounds, i.e. that we do not connect non-existing port numbers auto checkPortBounds = [&](PortType const portType) { NodeId const nodeId = getNodeId(portType, connectionId); - auto portCountRole = (portType == PortType::Out) ? NodeRole::OutPortCount - : NodeRole::InPortCount; - - std::size_t const portCount = nodeData(nodeId, portCountRole).toUInt(); + std::size_t const portCount = nodeData(nodeId, portCountRole(portType)).toUInt(); return getPortIndex(portType, connectionId) < portCount; }; @@ -139,17 +112,21 @@ bool DataFlowGraphModel::connectionPossible(ConnectionId const connectionId) con auto portVacant = [&](PortType const portType) { NodeId const nodeId = getNodeId(portType, connectionId); PortIndex const portIndex = getPortIndex(portType, connectionId); - auto const connected = connections(nodeId, portType, portIndex); + auto const &connected = connections(nodeId, portType, portIndex); - auto policy = portData(nodeId, portType, portIndex, PortRole::ConnectionPolicyRole) + auto policy = portData(nodeId, portType, portIndex, PortRole::ConnectionPolicy) .value(); return connected.empty() || (policy == ConnectionPolicy::Many); }; + bool const portsValid = checkPortBounds(PortType::Out) && checkPortBounds(PortType::In); + if (!portsValid) { + return false; + } + bool const basicChecks = getDataType(PortType::Out).id == getDataType(PortType::In).id - && portVacant(PortType::Out) && portVacant(PortType::In) - && checkPortBounds(PortType::Out) && checkPortBounds(PortType::In); + && portVacant(PortType::Out) && portVacant(PortType::In); // In data-flow mode (this class) it's important to forbid graph loops. // We perform depth-first graph traversal starting from the "Input" port of @@ -157,12 +134,17 @@ bool DataFlowGraphModel::connectionPossible(ConnectionId const connectionId) con auto hasLoops = [this, &connectionId]() -> bool { std::stack filo; + std::unordered_set visited; filo.push(connectionId.inNodeId); while (!filo.empty()) { auto id = filo.top(); filo.pop(); + if (!visited.insert(id).second) { + continue; + } + if (id == connectionId.outNodeId) { // LOOP! return true; } @@ -187,7 +169,11 @@ bool DataFlowGraphModel::connectionPossible(ConnectionId const connectionId) con void DataFlowGraphModel::addConnection(ConnectionId const connectionId) { - _connectivity.insert(connectionId); + if (connectionExists(connectionId) || !connectionPossible(connectionId)) { + return; + } + + _connectionIndex.add(connectionId); sendConnectionCreation(connectionId); @@ -203,6 +189,43 @@ void DataFlowGraphModel::addConnection(ConnectionId const connectionId) PortRole::Data); } +void DataFlowGraphModel::connectDelegateModel(NodeDelegateModel *model, NodeId nodeId) +{ + connect(model, + &NodeDelegateModel::dataUpdated, + [nodeId, this](PortIndex const portIndex) { + onOutPortDataUpdated(nodeId, portIndex); + }); + + connect(model, + &NodeDelegateModel::portsAboutToBeDeleted, + this, + [nodeId, this](PortType const portType, PortIndex const first, PortIndex const last) { + portsAboutToBeDeleted(nodeId, portType, first, last); + }); + + connect(model, + &NodeDelegateModel::portsDeleted, + this, + &DataFlowGraphModel::portsDeleted); + + connect(model, + &NodeDelegateModel::portsAboutToBeInserted, + this, + [nodeId, this](PortType const portType, PortIndex const first, PortIndex const last) { + portsAboutToBeInserted(nodeId, portType, first, last); + }); + + connect(model, + &NodeDelegateModel::portsInserted, + this, + &DataFlowGraphModel::portsInserted); + + connect(model, &NodeDelegateModel::requestNodeUpdate, this, [nodeId, this]() { + Q_EMIT nodeUpdated(nodeId); + }); +} + void DataFlowGraphModel::sendConnectionCreation(ConnectionId const connectionId) { Q_EMIT connectionCreated(connectionId); @@ -252,11 +275,21 @@ QVariant DataFlowGraphModel::nodeData(NodeId nodeId, NodeRole role) const break; case NodeRole::Position: - result = _nodeGeometryData[nodeId].pos; + if (auto geometryIt = _nodeGeometryData.find(nodeId); geometryIt != _nodeGeometryData.end()) { + result = geometryIt->second.pos; + } + else { + result = QPointF{}; + } break; case NodeRole::Size: - result = _nodeGeometryData[nodeId].size; + if (auto geometryIt = _nodeGeometryData.find(nodeId); geometryIt != _nodeGeometryData.end()) { + result = geometryIt->second.size; + } + else { + result = QSize{}; + } break; case NodeRole::CaptionVisible: @@ -320,9 +353,9 @@ NodeFlags DataFlowGraphModel::nodeFlags(NodeId nodeId) const bool DataFlowGraphModel::setNodeData(NodeId nodeId, NodeRole role, QVariant value) { - Q_UNUSED(nodeId); - Q_UNUSED(role); - Q_UNUSED(value); + if (!nodeExists(nodeId)) { + return false; + } bool result = false; @@ -368,6 +401,7 @@ bool DataFlowGraphModel::setNodeData(NodeId nodeId, NodeRole role, QVariant valu auto state = value.value(); if (auto node = delegateModel(nodeId); node != nullptr) { node->setValidationState(state); + result = true; } } Q_EMIT nodeUpdated(nodeId); @@ -378,6 +412,7 @@ bool DataFlowGraphModel::setNodeData(NodeId nodeId, NodeRole role, QVariant valu auto status = value.value(); if (auto node = delegateModel(nodeId); node != nullptr) { node->setNodeProcessingStatus(status); + result = true; } } Q_EMIT nodeUpdated(nodeId); @@ -398,6 +433,15 @@ QVariant DataFlowGraphModel::portData(NodeId nodeId, if (it == _models.end()) return result; + if (portType == PortType::None) { + return result; + } + + PortCount const portCount = nodeData(nodeId, portCountRole(portType)).toUInt(); + if (portIndex >= portCount) { + return result; + } + auto &model = it->second; switch (role) { @@ -411,7 +455,7 @@ QVariant DataFlowGraphModel::portData(NodeId nodeId, result = QVariant::fromValue(model->dataType(portType, portIndex)); break; - case PortRole::ConnectionPolicyRole: + case PortRole::ConnectionPolicy: result = QVariant::fromValue(model->portConnectionPolicy(portType, portIndex)); break; @@ -431,14 +475,19 @@ QVariant DataFlowGraphModel::portData(NodeId nodeId, bool DataFlowGraphModel::setPortData( NodeId nodeId, PortType portType, PortIndex portIndex, QVariant const &value, PortRole role) { - Q_UNUSED(nodeId); - - QVariant result; - auto it = _models.find(nodeId); if (it == _models.end()) return false; + if (portType == PortType::None) { + return false; + } + + PortCount const portCount = nodeData(nodeId, portCountRole(portType)).toUInt(); + if (portIndex >= portCount) { + return false; + } + auto &model = it->second; switch (role) { @@ -451,6 +500,7 @@ bool DataFlowGraphModel::setPortData( // Triggers repainting on the scene. Q_EMIT inPortDataWasSet(nodeId, portType, portIndex); + return true; } break; @@ -463,15 +513,7 @@ bool DataFlowGraphModel::setPortData( bool DataFlowGraphModel::deleteConnection(ConnectionId const connectionId) { - bool disconnected = false; - - auto it = _connectivity.find(connectionId); - - if (it != _connectivity.end()) { - disconnected = true; - - _connectivity.erase(it); - } + bool const disconnected = _connectionIndex.remove(connectionId); if (disconnected) { sendConnectionDeletion(connectionId); @@ -486,11 +528,18 @@ bool DataFlowGraphModel::deleteConnection(ConnectionId const connectionId) bool DataFlowGraphModel::deleteNode(NodeId const nodeId) { // Delete connections to this node first. - auto connectionIds = allConnectionIds(nodeId); - for (auto &cId : connectionIds) { + std::vector connectionIds; + auto const &attachedConnections = allConnectionIds(nodeId); + connectionIds.reserve(attachedConnections.size()); + for (auto const &cId : attachedConnections) { + connectionIds.push_back(cId); + } + + for (auto const &cId : connectionIds) { deleteConnection(cId); } + _nodeIds.erase(nodeId); _nodeGeometryData.erase(nodeId); _models.erase(nodeId); @@ -530,7 +579,7 @@ QJsonObject DataFlowGraphModel::save() const sceneJson["nodes"] = nodesJsonArray; QJsonArray connJsonArray; - for (auto const &cid : _connectivity) { + for (auto const &cid : _connectionIndex.connectivity()) { connJsonArray.append(toJson(cid)); } sceneJson["connections"] = connJsonArray; @@ -547,63 +596,49 @@ void DataFlowGraphModel::loadNode(QJsonObject const &nodeJson) // loading. // 2. When undoing the deletion command. Conflict is not possible // because all the new ids were created past the removed nodes. - NodeId restoredNodeId = nodeJson["id"].toInt(); + NodeId restoredNodeId = json_value_to_node_id(nodeJson["id"]); + if (restoredNodeId == InvalidNodeId) { + throw std::logic_error("Invalid node id in serialized node"); + } - _nextNodeId = std::max(_nextNodeId, restoredNodeId + 1); + if (_models.find(restoredNodeId) != _models.end()) { + throw std::logic_error("Node identifier collision in serialized node"); + } - QJsonObject const internalDataJson = nodeJson["internal-data"].toObject(); + quint64 const nextNodeIdCandidate = static_cast(restoredNodeId) + 1ull; + _nextNodeId = std::max(_nextNodeId, static_cast(nextNodeIdCandidate)); - QString delegateModelName = internalDataJson["model-name"].toString(); + QJsonObject internalDataJson; + if (!detail::read_required_object(nodeJson, "internal-data", internalDataJson)) { + throw std::logic_error("Missing internal-data object in serialized node"); + } + + QString delegateModelName; + if (!detail::read_required_string(internalDataJson, "model-name", delegateModelName) + || delegateModelName.isEmpty()) { + throw std::logic_error("Missing model-name in serialized node"); + } + + QPointF const pos = json_object_to_point(nodeJson, "position"); std::unique_ptr model = _registry->create(delegateModelName); if (model) { - connect(model.get(), - &NodeDelegateModel::dataUpdated, - [restoredNodeId, this](PortIndex const portIndex) { - onOutPortDataUpdated(restoredNodeId, portIndex); - }); - - connect(model.get(), - &NodeDelegateModel::portsAboutToBeDeleted, - this, - [restoredNodeId, - this](PortType const portType, PortIndex const first, PortIndex const last) { - portsAboutToBeDeleted(restoredNodeId, portType, first, last); - }); - - connect(model.get(), - &NodeDelegateModel::portsDeleted, - this, - &DataFlowGraphModel::portsDeleted); - - connect(model.get(), - &NodeDelegateModel::portsAboutToBeInserted, - this, - [restoredNodeId, - this](PortType const portType, PortIndex const first, PortIndex const last) { - portsAboutToBeInserted(restoredNodeId, portType, first, last); - }); - - connect(model.get(), - &NodeDelegateModel::portsInserted, - this, - &DataFlowGraphModel::portsInserted); - - connect(model.get(), &NodeDelegateModel::requestNodeUpdate, this, [restoredNodeId, this]() { - Q_EMIT nodeUpdated(restoredNodeId); - }); + connectDelegateModel(model.get(), restoredNodeId); + _nodeIds.insert(restoredNodeId); _models[restoredNodeId] = std::move(model); Q_EMIT nodeCreated(restoredNodeId); - QJsonObject posJson = nodeJson["position"].toObject(); - QPointF const pos(posJson["x"].toDouble(), posJson["y"].toDouble()); - setNodeData(restoredNodeId, NodeRole::Position, pos); - _models[restoredNodeId]->load(internalDataJson); + try { + _models[restoredNodeId]->load(internalDataJson); + } catch (...) { + deleteNode(restoredNodeId); + throw; + } } else { throw std::logic_error(std::string("No registered model with name ") + delegateModelName.toLocal8Bit().data()); @@ -612,21 +647,70 @@ void DataFlowGraphModel::loadNode(QJsonObject const &nodeJson) void DataFlowGraphModel::load(QJsonObject const &jsonDocument) { - QJsonArray nodesJsonArray = jsonDocument["nodes"].toArray(); + QJsonArray nodesJsonArray; + if (!detail::read_required_array(jsonDocument, "nodes", nodesJsonArray)) { + throw std::logic_error("Serialized graph is missing nodes array"); + } - for (QJsonValueRef nodeJson : nodesJsonArray) { - loadNode(nodeJson.toObject()); + QJsonArray connectionJsonArray; + if (!detail::read_required_array(jsonDocument, "connections", connectionJsonArray)) { + throw std::logic_error("Serialized graph is missing connections array"); + } + + for (QJsonValue const &nodeJson : nodesJsonArray) { + if (!nodeJson.isObject()) { + throw std::logic_error("Serialized graph contains invalid node entry"); + } } - QJsonArray connectionJsonArray = jsonDocument["connections"].toArray(); + std::vector parsedConnections; + parsedConnections.reserve(connectionJsonArray.size()); - for (QJsonValueRef connection : connectionJsonArray) { - QJsonObject connJson = connection.toObject(); + for (QJsonValue const &connection : connectionJsonArray) { + if (!connection.isObject()) { + throw std::logic_error("Serialized graph contains invalid connection entry"); + } + + ConnectionId connId; + if (!tryFromJson(connection.toObject(), connId)) { + throw std::logic_error("Serialized graph contains invalid connection id"); + } + + parsedConnections.push_back(connId); + } - ConnectionId connId = fromJson(connJson); + std::vector loadedNodeIds; + loadedNodeIds.reserve(nodesJsonArray.size()); + std::vector loadedConnections; + loadedConnections.reserve(parsedConnections.size()); + + try { + for (QJsonValueRef nodeJson : nodesJsonArray) { + NodeId const nodeId = json_value_to_node_id(nodeJson.toObject()["id"]); + loadNode(nodeJson.toObject()); + loadedNodeIds.push_back(nodeId); + } + + for (ConnectionId const connId : parsedConnections) { + if (!connectionPossible(connId)) { + throw std::logic_error("Serialized graph contains invalid connection"); + } + + addConnection(connId); + loadedConnections.push_back(connId); + } + } catch (...) { + for (auto it = loadedConnections.rbegin(); it != loadedConnections.rend(); ++it) { + deleteConnection(*it); + } + + for (auto it = loadedNodeIds.rbegin(); it != loadedNodeIds.rend(); ++it) { + if (nodeExists(*it)) { + deleteNode(*it); + } + } - // Restore the connection - addConnection(connId); + throw; } } diff --git a/src/DataFlowGraphicsScene.cpp b/src/DataFlowGraphicsScene.cpp index 5a0e390ca..2964928c0 100644 --- a/src/DataFlowGraphicsScene.cpp +++ b/src/DataFlowGraphicsScene.cpp @@ -4,6 +4,7 @@ #include "GraphicsView.hpp" #include "NodeDelegateModelRegistry.hpp" #include "NodeGraphicsObject.hpp" +#include "SerializationValidation.hpp" #include "UndoCommands.hpp" #include @@ -15,7 +16,6 @@ #include #include -#include #include #include #include @@ -23,10 +23,10 @@ #include #include #include -#include #include #include +#include #include #include @@ -34,35 +34,173 @@ namespace { using QtNodes::GroupId; using QtNodes::InvalidGroupId; +using QtNodes::InvalidNodeId; +using QtNodes::NodeId; -GroupId jsonValueToGroupId(QJsonValue const &value) +std::unordered_set collect_node_ids(QJsonArray const &nodesJsonArray) { - if (value.isDouble()) { - return static_cast(value.toInt()); + std::unordered_set nodeIds; + nodeIds.reserve(nodesJsonArray.size()); + + for (QJsonValue const &nodeValue : nodesJsonArray) { + if (!nodeValue.isObject()) { + throw std::logic_error("Serialized scene contains invalid node entry"); + } + + NodeId nodeId = InvalidNodeId; + if (!QtNodes::detail::read_node_id(nodeValue.toObject()["id"], nodeId)) { + throw std::logic_error("Serialized scene contains invalid node id"); + } + + if (!nodeIds.insert(nodeId).second) { + throw std::logic_error("Serialized scene contains duplicate node ids"); + } } - if (value.isString()) { - auto const textValue = value.toString(); + return nodeIds; +} - bool ok = false; - auto const numericValue = textValue.toULongLong(&ok, 10); - if (ok) { - return static_cast(numericValue); +void validate_groups_json(QJsonObject const &sceneJson) +{ + if (!sceneJson.contains("groups")) { + return; + } + + QJsonArray groupsJsonArray; + if (!QtNodes::detail::read_required_array(sceneJson, "groups", groupsJsonArray)) { + throw std::logic_error("Serialized scene contains invalid groups array"); + } + + QJsonArray nodesJsonArray; + if (!QtNodes::detail::read_required_array(sceneJson, "nodes", nodesJsonArray)) { + throw std::logic_error("Serialized scene is missing nodes array"); + } + + std::unordered_set const nodeIds = collect_node_ids(nodesJsonArray); + std::unordered_set seenGroupIds; + + for (QJsonValue const &groupValue : groupsJsonArray) { + if (!groupValue.isObject()) { + throw std::logic_error("Serialized scene contains invalid group entry"); + } + + QJsonObject const groupObject = groupValue.toObject(); + + GroupId groupId = InvalidGroupId; + if (!QtNodes::detail::read_group_id(groupObject["id"], groupId)) { + throw std::logic_error("Serialized scene contains invalid group id"); } - QUuid uuidValue(textValue); - if (!uuidValue.isNull()) { - auto const bytes = uuidValue.toRfc4122(); - if (bytes.size() >= static_cast(sizeof(quint32))) { - QDataStream stream(bytes); - quint32 value32 = 0U; - stream >> value32; - return static_cast(value32); + if (!seenGroupIds.insert(groupId).second) { + throw std::logic_error("Serialized scene contains duplicate group ids"); + } + + QString groupName; + if (!QtNodes::detail::read_required_string(groupObject, "name", groupName)) { + throw std::logic_error("Serialized scene contains invalid group name"); + } + Q_UNUSED(groupName); + + QJsonArray nodeIdsJson; + if (!QtNodes::detail::read_required_array(groupObject, "nodes", nodeIdsJson)) { + throw std::logic_error("Serialized scene contains invalid group nodes"); + } + + bool locked = true; + if (!QtNodes::detail::read_optional_bool(groupObject, "locked", locked)) { + throw std::logic_error("Serialized scene contains invalid group lock state"); + } + Q_UNUSED(locked); + + for (QJsonValue const &idValue : nodeIdsJson) { + NodeId nodeId = InvalidNodeId; + if (!QtNodes::detail::read_node_id(idValue, nodeId) || nodeIds.count(nodeId) == 0) { + throw std::logic_error("Serialized scene group references unknown node id"); } } } +} + +QJsonObject scene_json_with_groups(QtNodes::DataFlowGraphModel const &graphModel, + QtNodes::BasicGraphicsScene const &scene) +{ + QJsonObject sceneJson = graphModel.save(); + + QJsonArray groupsJsonArray; + for (auto const &groupEntry : scene.groups()) { + GroupId const groupId = groupEntry.first; + auto const &groupPtr = groupEntry.second; + + if (!groupPtr) { + continue; + } + + QJsonObject groupJson; + groupJson["id"] = static_cast(groupId); + groupJson["name"] = groupPtr->name(); + + QJsonArray nodeIdsJson; + for (NodeId const nodeId : groupPtr->nodeIDs()) { + nodeIdsJson.append(static_cast(nodeId)); + } + groupJson["nodes"] = nodeIdsJson; + groupJson["locked"] = groupPtr->groupGraphicsObject().locked(); + + groupsJsonArray.append(groupJson); + } - return InvalidGroupId; + if (!groupsJsonArray.isEmpty()) { + sceneJson["groups"] = groupsJsonArray; + } + + return sceneJson; +} + +void restore_groups_from_json(QJsonObject const &sceneJson, QtNodes::BasicGraphicsScene &scene) +{ + if (!sceneJson.contains("groups")) { + return; + } + + QJsonArray const groupsJsonArray = sceneJson["groups"].toArray(); + + for (QJsonValue const groupValue : groupsJsonArray) { + QJsonObject const groupObject = groupValue.toObject(); + + QJsonArray const nodeIdsJson = groupObject["nodes"].toArray(); + std::vector groupNodes; + groupNodes.reserve(nodeIdsJson.size()); + + for (QJsonValue const idValue : nodeIdsJson) { + NodeId nodeId = InvalidNodeId; + QtNodes::detail::read_node_id(idValue, nodeId); + if (auto *nodeObject = scene.nodeGraphicsObject(nodeId)) { + groupNodes.push_back(nodeObject); + } + } + + if (groupNodes.empty()) { + continue; + } + + QString const groupName = groupObject["name"].toString(); + GroupId groupId = InvalidGroupId; + QtNodes::detail::read_group_id(groupObject["id"], groupId); + + auto const groupWeak = scene.createGroup(groupNodes, groupName, groupId); + if (auto group = groupWeak.lock()) { + bool const locked = groupObject["locked"].toBool(true); + group->groupGraphicsObject().lock(locked); + } + } +} + +void load_scene_json(QJsonObject const &sceneJson, + QtNodes::DataFlowGraphModel &graphModel, + QtNodes::BasicGraphicsScene &scene) +{ + graphModel.load(sceneJson); + restore_groups_from_json(sceneJson, scene); } } // namespace @@ -198,30 +336,7 @@ bool DataFlowGraphicsScene::save() const QFile file(fileName); if (file.open(QIODevice::WriteOnly)) { - QJsonObject sceneJson = _graphModel.save(); - - QJsonArray groupsJsonArray; - for (auto const &[groupId, groupPtr] : groups()) { - if (!groupPtr) - continue; - - QJsonObject groupJson; - groupJson["id"] = static_cast(groupId); - groupJson["name"] = groupPtr->name(); - - QJsonArray nodeIdsJson; - for (NodeId const nodeId : groupPtr->nodeIDs()) { - nodeIdsJson.append(static_cast(nodeId)); - } - groupJson["nodes"] = nodeIdsJson; - groupJson["locked"] = groupPtr->groupGraphicsObject().locked(); - - groupsJsonArray.append(groupJson); - } - - if (!groupsJsonArray.isEmpty()) { - sceneJson["groups"] = groupsJsonArray; - } + QJsonObject sceneJson = scene_json_with_groups(_graphModel, *this); file.write(QJsonDocument(sceneJson).toJson()); return true; @@ -245,8 +360,6 @@ bool DataFlowGraphicsScene::load() if (!file.open(QIODevice::ReadOnly)) return false; - clearScene(); - QByteArray const wholeFile = file.readAll(); QJsonParseError parseError{}; @@ -256,37 +369,37 @@ bool DataFlowGraphicsScene::load() QJsonObject const sceneJson = sceneDocument.object(); - _graphModel.load(sceneJson); - - if (sceneJson.contains("groups")) { - QJsonArray const groupsJsonArray = sceneJson["groups"].toArray(); + try { + validate_groups_json(sceneJson); + } catch (...) { + return false; + } - for (QJsonValue groupValue : groupsJsonArray) { - QJsonObject const groupObject = groupValue.toObject(); + DataFlowGraphModel stagingModel(_graphModel.dataModelRegistry()); + DataFlowGraphicsScene stagingScene(stagingModel); - QJsonArray const nodeIdsJson = groupObject["nodes"].toArray(); - std::vector groupNodes; - groupNodes.reserve(nodeIdsJson.size()); + try { + load_scene_json(sceneJson, stagingModel, stagingScene); + } catch (...) { + return false; + } - for (QJsonValue idValue : nodeIdsJson) { - NodeId const nodeId = static_cast(idValue.toInt()); - if (auto *nodeObject = nodeGraphicsObject(nodeId)) { - groupNodes.push_back(nodeObject); - } - } + QJsonObject const previousSceneJson = scene_json_with_groups(_graphModel, *this); - if (groupNodes.empty()) - continue; + clearScene(); - QString const groupName = groupObject["name"].toString(); - GroupId const groupId = jsonValueToGroupId(groupObject["id"]); + try { + load_scene_json(sceneJson, _graphModel, *this); + } catch (...) { + clearScene(); - auto const groupWeak = createGroup(groupNodes, groupName, groupId); - if (auto group = groupWeak.lock()) { - bool const locked = groupObject["locked"].toBool(true); - group->groupGraphicsObject().lock(locked); - } + try { + load_scene_json(previousSceneJson, _graphModel, *this); + } catch (...) { + clearScene(); } + + return false; } Q_EMIT sceneLoaded(); diff --git a/src/DefaultConnectionPainter.cpp b/src/DefaultConnectionPainter.cpp index d9c113335..041cb3574 100644 --- a/src/DefaultConnectionPainter.cpp +++ b/src/DefaultConnectionPainter.cpp @@ -7,23 +7,35 @@ #include "NodeData.hpp" #include "StyleCollection.hpp" -#include - namespace QtNodes { -QPainterPath DefaultConnectionPainter::cubicPath(ConnectionGraphicsObject const &connection) const +namespace { + +QPen make_connection_pen(QColor const &color, qreal width, Qt::PenStyle style = Qt::SolidLine) { - QPointF const &in = connection.endPoint(PortType::In); - QPointF const &out = connection.endPoint(PortType::Out); + QPen pen(color); + pen.setWidthF(width); + pen.setStyle(style); + pen.setCapStyle(Qt::RoundCap); + pen.setJoinStyle(Qt::RoundJoin); + return pen; +} - auto const c1c2 = connection.pointsC1C2(); +} // namespace - // cubic spline - QPainterPath cubic(out); +QPixmap const &DefaultConnectionPainter::convertPixmap() const +{ + if (!_convertPixmapInitialized) { + _convertPixmap = QIcon(QStringLiteral(":/convert.png")).pixmap(QSize(22, 22)); + _convertPixmapInitialized = true; + } - cubic.cubicTo(c1c2.first, c1c2.second, in); + return _convertPixmap; +} - return cubic; +QPainterPath DefaultConnectionPainter::cubicPath(ConnectionGraphicsObject const &connection) const +{ + return connection.cachedCubicPath(); } void DefaultConnectionPainter::drawSketchLine(QPainter *painter, @@ -34,10 +46,9 @@ void DefaultConnectionPainter::drawSketchLine(QPainter *painter, if (state.requiresPort() || state.frozen()) { auto const &connectionStyle = QtNodes::StyleCollection::connectionStyle(); - QPen pen; - pen.setWidth(static_cast(connectionStyle.constructionLineWidth())); - pen.setColor(connectionStyle.constructionColor()); - pen.setStyle(Qt::DashLine); + QPen pen = make_connection_pen(connectionStyle.constructionColor(), + connectionStyle.constructionLineWidth(), + Qt::DashLine); painter->setPen(pen); painter->setBrush(Qt::NoBrush); @@ -61,10 +72,9 @@ void DefaultConnectionPainter::drawHoveredOrSelected(QPainter *painter, double const lineWidth = connectionStyle.lineWidth(); - QPen pen; - pen.setWidth(static_cast(2 * lineWidth)); - pen.setColor(selected ? connectionStyle.selectedHaloColor() - : connectionStyle.hoveredColor()); + QPen pen = make_connection_pen(selected ? connectionStyle.selectedHaloColor() + : connectionStyle.hoveredColor(), + 2.0 * lineWidth); painter->setPen(pen); painter->setBrush(Qt::NoBrush); @@ -123,13 +133,11 @@ void DefaultConnectionPainter::drawNormalLine(QPainter *painter, double const lineWidth = connectionStyle.lineWidth(); // draw normal line - QPen p; - - p.setWidth(lineWidth); + QPen p = make_connection_pen(normalColorOut, lineWidth); bool const selected = cgo.isSelected(); - auto cubic = cubicPath(cgo); + auto const cubic = cubicPath(cgo); if (useGradientColor) { painter->setBrush(Qt::NoBrush); @@ -140,13 +148,8 @@ void DefaultConnectionPainter::drawNormalLine(QPainter *painter, p.setColor(cOut); painter->setPen(p); - unsigned int constexpr segments = 60; - - for (unsigned int i = 0ul; i < segments; ++i) { - double ratioPrev = double(i) / segments; - double ratio = double(i + 1) / segments; - - if (i == segments / 2) { + for (int i = 0; i < cgo.cachedSamplePointCount() - 1; ++i) { + if (i == (cgo.cachedSamplePointCount() - 1) / 2) { QColor cIn = normalColorIn; if (selected) cIn = cIn.darker(200); @@ -154,17 +157,15 @@ void DefaultConnectionPainter::drawNormalLine(QPainter *painter, p.setColor(cIn); painter->setPen(p); } - painter->drawLine(cubic.pointAtPercent(ratioPrev), cubic.pointAtPercent(ratio)); + painter->drawLine(cgo.cachedSamplePoint(i), cgo.cachedSamplePoint(i + 1)); } - { - QIcon icon(":convert.png"); - - QPixmap pixmap = icon.pixmap(QSize(22, 22)); - painter->drawPixmap(cubic.pointAtPercent(0.50) - - QPoint(pixmap.width() / 2, pixmap.height() / 2), - pixmap); - } + QPixmap const &conversionPixmap = convertPixmap(); + QRectF const targetRect(cgo.cachedMidPoint().x() - conversionPixmap.width() / 2.0, + cgo.cachedMidPoint().y() - conversionPixmap.height() / 2.0, + conversionPixmap.width(), + conversionPixmap.height()); + painter->drawPixmap(targetRect, conversionPixmap, QRectF(conversionPixmap.rect())); } else { p.setColor(normalColorOut); @@ -181,6 +182,8 @@ void DefaultConnectionPainter::drawNormalLine(QPainter *painter, void DefaultConnectionPainter::paint(QPainter *painter, ConnectionGraphicsObject const &cgo) const { + painter->setRenderHint(QPainter::Antialiasing, true); + drawHoveredOrSelected(painter, cgo); drawSketchLine(painter, cgo); @@ -206,22 +209,7 @@ void DefaultConnectionPainter::paint(QPainter *painter, ConnectionGraphicsObject QPainterPath DefaultConnectionPainter::getPainterStroke( ConnectionGraphicsObject const &connection) const { - auto cubic = cubicPath(connection); - - QPointF const &out = connection.endPoint(PortType::Out); - QPainterPath result(out); - - unsigned int constexpr segments = 20; - - for (auto i = 0ul; i < segments; ++i) { - double ratio = double(i + 1) / segments; - result.lineTo(cubic.pointAtPercent(ratio)); - } - - QPainterPathStroker stroker; - stroker.setWidth(10.0); - - return stroker.createStroke(result); + return connection.cachedStrokePath(); } #ifdef NODE_DEBUG_DRAWING diff --git a/src/DefaultHorizontalNodeGeometry.cpp b/src/DefaultHorizontalNodeGeometry.cpp index 586795117..d94246142 100644 --- a/src/DefaultHorizontalNodeGeometry.cpp +++ b/src/DefaultHorizontalNodeGeometry.cpp @@ -1,7 +1,6 @@ #include "DefaultHorizontalNodeGeometry.hpp" #include "AbstractGraphModel.hpp" -#include "NodeData.hpp" #include #include @@ -10,35 +9,8 @@ namespace QtNodes { DefaultHorizontalNodeGeometry::DefaultHorizontalNodeGeometry(AbstractGraphModel &graphModel) - : AbstractNodeGeometry(graphModel) - , _portSize(20) - , _portSpasing(10) - , _fontMetrics(QFont()) - , _boldFontMetrics(QFont()) -{ - QFont f; - f.setBold(true); - _boldFontMetrics = QFontMetrics(f); - - _portSize = _fontMetrics.height(); -} - -QRectF DefaultHorizontalNodeGeometry::boundingRect(NodeId const nodeId) const -{ - QSize s = size(nodeId); - - qreal marginSize = 2.0 * _portSpasing; - QMargins margins(marginSize, marginSize, marginSize, marginSize); - - QRectF r(QPointF(0, 0), s); - - return r.marginsAdded(margins); -} - -QSize DefaultHorizontalNodeGeometry::size(NodeId const nodeId) const -{ - return _graphModel.nodeData(nodeId, NodeRole::Size); -} + : DefaultNodeGeometryBase(graphModel) +{} void DefaultHorizontalNodeGeometry::recomputeSize(NodeId const nodeId) const { @@ -52,8 +24,8 @@ void DefaultHorizontalNodeGeometry::recomputeSize(NodeId const nodeId) const height += capRect.height(); - height += _portSpasing; // space above caption - height += _portSpasing; // space below caption + height += _portSpacing; // space above caption + height += _portSpacing; // space below caption QVariant var = _graphModel.nodeData(nodeId, NodeRole::ProcessingStatus); auto processingStatusValue = var.value(); @@ -64,13 +36,13 @@ void DefaultHorizontalNodeGeometry::recomputeSize(NodeId const nodeId) const unsigned int inPortWidth = maxPortsTextAdvance(nodeId, PortType::In); unsigned int outPortWidth = maxPortsTextAdvance(nodeId, PortType::Out); - unsigned int width = inPortWidth + outPortWidth + 4 * _portSpasing; + unsigned int width = inPortWidth + outPortWidth + 4 * _portSpacing; if (auto w = _graphModel.nodeData(nodeId, NodeRole::Widget)) { width += w->width(); } - width = std::max(width, static_cast(capRect.width()) + 2 * _portSpasing); + width = std::max(width, static_cast(capRect.width()) + 2 * _portSpacing); QSize size(width, height); @@ -81,14 +53,14 @@ QPointF DefaultHorizontalNodeGeometry::portPosition(NodeId const nodeId, PortType const portType, PortIndex const portIndex) const { - unsigned int const step = _portSize + _portSpasing; + unsigned int const step = _portSize + _portSpacing; QPointF result; double totalHeight = 0.0; totalHeight += captionRect(nodeId).height(); - totalHeight += _portSpasing; + totalHeight += _portSpacing; totalHeight += step * portIndex; totalHeight += step / 2.0; @@ -131,11 +103,11 @@ QPointF DefaultHorizontalNodeGeometry::portTextPosition(NodeId const nodeId, switch (portType) { case PortType::In: - p.setX(_portSpasing); + p.setX(_portSpacing); break; case PortType::Out: - p.setX(size.width() - _portSpasing - rect.width()); + p.setX(size.width() - _portSpacing - rect.width()); break; default: @@ -145,21 +117,11 @@ QPointF DefaultHorizontalNodeGeometry::portTextPosition(NodeId const nodeId, return p; } -QRectF DefaultHorizontalNodeGeometry::captionRect(NodeId const nodeId) const -{ - if (!_graphModel.nodeData(nodeId, NodeRole::CaptionVisible)) - return QRect(); - - QString name = _graphModel.nodeData(nodeId, NodeRole::Caption); - - return _boldFontMetrics.boundingRect(name); -} - QPointF DefaultHorizontalNodeGeometry::captionPosition(NodeId const nodeId) const { QSize size = _graphModel.nodeData(nodeId, NodeRole::Size); return QPointF(0.5 * (size.width() - captionRect(nodeId).width()), - 0.5 * _portSpasing + captionRect(nodeId).height()); + 0.5 * _portSpacing + captionRect(nodeId).height()); } QPointF DefaultHorizontalNodeGeometry::widgetPosition(NodeId const nodeId) const @@ -172,10 +134,10 @@ QPointF DefaultHorizontalNodeGeometry::widgetPosition(NodeId const nodeId) const // If the widget wants to use as much vertical space as possible, // place it immediately after the caption. if (w->sizePolicy().verticalPolicy() & QSizePolicy::ExpandFlag) { - return QPointF(2.0 * _portSpasing + maxPortsTextAdvance(nodeId, PortType::In), - _portSpasing + captionHeight); + return QPointF(2.0 * _portSpacing + maxPortsTextAdvance(nodeId, PortType::In), + _portSpacing + captionHeight); } else { - return QPointF(2.0 * _portSpasing + maxPortsTextAdvance(nodeId, PortType::In), + return QPointF(2.0 * _portSpacing + maxPortsTextAdvance(nodeId, PortType::In), (captionHeight + size.height() - w->height()) / 2.0); } } @@ -188,70 +150,12 @@ QRect DefaultHorizontalNodeGeometry::resizeHandleRect(NodeId const nodeId) const unsigned int rectSize = 7; - return QRect(size.width() - _portSpasing, size.height() - _portSpasing, rectSize, rectSize); -} - -QRectF DefaultHorizontalNodeGeometry::portTextRect(NodeId const nodeId, - PortType const portType, - PortIndex const portIndex) const -{ - QString s; - if (_graphModel.portData(nodeId, portType, portIndex, PortRole::CaptionVisible)) { - s = _graphModel.portData(nodeId, portType, portIndex, PortRole::Caption); - } else { - auto portData = _graphModel.portData(nodeId, portType, portIndex, PortRole::DataType); - - s = portData.value().name; - } - - return _fontMetrics.boundingRect(s); + return QRect(size.width() - _portSpacing, size.height() - _portSpacing, rectSize, rectSize); } unsigned int DefaultHorizontalNodeGeometry::maxVerticalPortsExtent(NodeId const nodeId) const { - PortCount nInPorts = _graphModel.nodeData(nodeId, NodeRole::InPortCount); - - PortCount nOutPorts = _graphModel.nodeData(nodeId, NodeRole::OutPortCount); - - unsigned int maxNumOfEntries = std::max(nInPorts, nOutPorts); - unsigned int step = _portSize + _portSpasing; - - return step * maxNumOfEntries; -} - -unsigned int DefaultHorizontalNodeGeometry::maxPortsTextAdvance(NodeId const nodeId, - PortType const portType) const -{ - unsigned int width = 0; - - size_t const n = _graphModel - .nodeData(nodeId, - (portType == PortType::Out) ? NodeRole::OutPortCount - : NodeRole::InPortCount) - .toUInt(); - - for (PortIndex portIndex = 0ul; portIndex < n; ++portIndex) { - QString name; - - if (_graphModel.portData(nodeId, portType, portIndex, PortRole::CaptionVisible)) { - name = _graphModel.portData(nodeId, portType, portIndex, PortRole::Caption); - } else { - NodeDataType portData = _graphModel.portData(nodeId, - portType, - portIndex, - PortRole::DataType); - - name = portData.name; - } - -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) - width = std::max(unsigned(_fontMetrics.horizontalAdvance(name)), width); -#else - width = std::max(unsigned(_fontMetrics.width(name)), width); -#endif - } - - return width; + return maxPortsExtent(nodeId); } } // namespace QtNodes diff --git a/src/DefaultNodeGeometryBase.cpp b/src/DefaultNodeGeometryBase.cpp new file mode 100644 index 000000000..bd39e4808 --- /dev/null +++ b/src/DefaultNodeGeometryBase.cpp @@ -0,0 +1,112 @@ +#include "DefaultNodeGeometryBase.hpp" + +#include "AbstractGraphModel.hpp" +#include "ConnectionIdUtils.hpp" +#include "NodeRenderingUtils.hpp" +#include "NodeData.hpp" + +#include + +namespace QtNodes { + +DefaultNodeGeometryBase::DefaultNodeGeometryBase(AbstractGraphModel &graphModel) + : AbstractNodeGeometry(graphModel) + , _portSize(20) + , _portSpacing(10) + , _fontMetrics(QFont()) + , _boldFontMetrics(QFont()) +{ + QFont f; + f.setBold(true); + _boldFontMetrics = QFontMetrics(f); + + _portSize = _fontMetrics.height(); +} + +QRectF DefaultNodeGeometryBase::boundingRect(NodeId const nodeId) const +{ + QSize s = size(nodeId); + std::optional fallback_style; + NodeStyle const &style = node_rendering::resolved_node_style(_graphModel, nodeId, fallback_style); + QMarginsF const margins = node_rendering::node_visual_margins(style.shadowEnabled()); + + QRectF r(QPointF(0, 0), s); + + return r.marginsAdded(margins); +} + +QSize DefaultNodeGeometryBase::size(NodeId const nodeId) const +{ + return _graphModel.nodeData(nodeId, NodeRole::Size); +} + +QRectF DefaultNodeGeometryBase::captionRect(NodeId const nodeId) const +{ + if (!_graphModel.nodeData(nodeId, NodeRole::CaptionVisible)) + return QRect(); + + QString name = _graphModel.nodeData(nodeId, NodeRole::Caption); + + return _boldFontMetrics.boundingRect(name); +} + +QRectF DefaultNodeGeometryBase::portTextRect(NodeId const nodeId, + PortType const portType, + PortIndex const portIndex) const +{ + QString s; + if (_graphModel.portData(nodeId, portType, portIndex, PortRole::CaptionVisible)) { + s = _graphModel.portData(nodeId, portType, portIndex, PortRole::Caption); + } else { + auto portData = _graphModel.portData(nodeId, portType, portIndex, PortRole::DataType); + + s = portData.value().name; + } + + return _fontMetrics.boundingRect(s); +} + +unsigned int DefaultNodeGeometryBase::maxPortsExtent(NodeId const nodeId) const +{ + PortCount nInPorts = _graphModel.nodeData(nodeId, NodeRole::InPortCount); + + PortCount nOutPorts = _graphModel.nodeData(nodeId, NodeRole::OutPortCount); + + unsigned int maxNumOfEntries = std::max(nInPorts, nOutPorts); + unsigned int step = _portSize + _portSpacing; + + return step * maxNumOfEntries; +} + +unsigned int DefaultNodeGeometryBase::maxPortsTextAdvance(NodeId const nodeId, + PortType const portType) const +{ + unsigned int width = 0; + + size_t const n = _graphModel.nodeData(nodeId, portCountRole(portType)).toUInt(); + + for (PortIndex portIndex = 0ul; portIndex < n; ++portIndex) { + QString name; + + if (_graphModel.portData(nodeId, portType, portIndex, PortRole::CaptionVisible)) { + name = _graphModel.portData(nodeId, portType, portIndex, PortRole::Caption); + } else { + NodeDataType portData = _graphModel.portData(nodeId, + portType, + portIndex, + PortRole::DataType); + + name = portData.name; + } + +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + width = std::max(unsigned(_fontMetrics.horizontalAdvance(name)), width); +#else + width = std::max(unsigned(_fontMetrics.width(name)), width); +#endif + } + + return width; +} + +} // namespace QtNodes diff --git a/src/DefaultNodePainter.cpp b/src/DefaultNodePainter.cpp index 1d90ae5c9..1078ca6a5 100644 --- a/src/DefaultNodePainter.cpp +++ b/src/DefaultNodePainter.cpp @@ -6,41 +6,399 @@ #include "ConnectionGraphicsObject.hpp" #include "ConnectionIdUtils.hpp" #include "DataFlowGraphModel.hpp" +#include "GraphicsView.hpp" +#include "NodeRenderingUtils.hpp" #include "NodeDelegateModel.hpp" #include "NodeGraphicsObject.hpp" #include "NodeState.hpp" #include "StyleCollection.hpp" -#include +#include +#include +#include +#include #include +#include +#include +#include namespace QtNodes { +namespace { + +void box_blur_alpha(QImage &img, int radius) +{ + const int w = img.width(); + const int h = img.height(); + if (w == 0 || h == 0 || radius <= 0) { + return; + } + + const int span = 2 * radius + 1; + std::vector buf(static_cast(w) * h); + + // Read alpha from premultiplied ARGB scanlines. + auto alpha_at = [&](int x, int y) -> int { + return qAlpha(reinterpret_cast(img.constScanLine(y))[x]); + }; + + // Horizontal pass → buf + for (int y = 0; y < h; ++y) { + int sum = 0; + for (int x = -radius; x <= radius; ++x) { + sum += alpha_at(std::clamp(x, 0, w - 1), y); + } + buf[y * w] = static_cast(sum / span); + for (int x = 1; x < w; ++x) { + sum += alpha_at(std::min(x + radius, w - 1), y); + sum -= alpha_at(std::max(x - radius - 1, 0), y); + buf[y * w + x] = static_cast(sum / span); + } + } + + // Vertical pass → back into image + for (int x = 0; x < w; ++x) { + int sum = 0; + for (int y = -radius; y <= radius; ++y) { + sum += buf[std::clamp(y, 0, h - 1) * w + x]; + } + reinterpret_cast(img.scanLine(0))[x] = qPremultiply(qRgba(0, 0, 0, sum / span)); + for (int y = 1; y < h; ++y) { + sum += buf[std::min(y + radius, h - 1) * w + x]; + sum -= buf[std::max(y - radius - 1, 0) * w + x]; + reinterpret_cast(img.scanLine(y))[x] = qPremultiply(qRgba(0, 0, 0, sum / span)); + } + } +} + +QImage generate_shadow_atlas(QColor shadow_color, qreal dpr) +{ + const int phys = static_cast(std::ceil(node_rendering::k_atlas_size * dpr)); + + QImage img(phys, phys, QImage::Format_ARGB32_Premultiplied); + img.setDevicePixelRatio(dpr); + img.fill(Qt::transparent); + + // Draw an opaque rounded rect in the center of the atlas. + { + QPainter p(&img); + p.setRenderHint(QPainter::Antialiasing, true); + p.setPen(Qt::NoPen); + p.setBrush(Qt::white); + QRectF body(node_rendering::k_shadow_margin, + node_rendering::k_shadow_margin, + node_rendering::k_body_size, + node_rendering::k_body_size); + p.drawRoundedRect(body, node_rendering::k_node_radius, node_rendering::k_node_radius); + } + + // Multi-pass box blur on the alpha channel. + const int phys_blur = std::max( + 1, + static_cast(std::round(node_rendering::k_blur_radius * dpr))); + for (int pass = 0; pass < node_rendering::k_blur_passes; ++pass) { + box_blur_alpha(img, phys_blur); + } + + // Tint with shadow color while preserving the style alpha and applying the + // configured global strength multiplier. + const int sr = shadow_color.red(); + const int sg = shadow_color.green(); + const int sb = shadow_color.blue(); + const int sa = shadow_color.alpha(); + for (int y = 0; y < phys; ++y) { + auto *line = reinterpret_cast(img.scanLine(y)); + for (int x = 0; x < phys; ++x) { + const int blurred_alpha = (qAlpha(line[x]) * sa) / 255; + const int a = (blurred_alpha * node_rendering::k_shadow_opacity) / 255; + line[x] = qPremultiply(qRgba(sr, sg, sb, a)); + } + } + + return img; +} + +struct Shadow_cache_key +{ + QRgb color; + int dpr_micro; // dpr * 1e6 as int for reliable comparison + + bool operator==(Shadow_cache_key const &o) const + { + return color == o.color && dpr_micro == o.dpr_micro; + } +}; + +struct Shadow_cache_key_hash +{ + std::size_t operator()(Shadow_cache_key const &k) const + { + return std::hash()( + (static_cast(k.color) << 32) | + static_cast(k.dpr_micro)); + } +}; + +std::unordered_map s_shadow_cache; +std::mutex s_shadow_cache_mutex; + +QImage cached_shadow_atlas(QColor shadow_color, qreal dpr) +{ + std::lock_guard lock(s_shadow_cache_mutex); + + Shadow_cache_key key{shadow_color.rgba(), + static_cast(dpr * 1000000.0)}; + auto it = s_shadow_cache.find(key); + if (it != s_shadow_cache.end()) { + return it->second; + } + + if (s_shadow_cache.size() >= 32) { + // Arbitrary eviction is sufficient here; the cache is tiny. + s_shadow_cache.erase(s_shadow_cache.begin()); + } + + return s_shadow_cache.emplace(key, generate_shadow_atlas(shadow_color, dpr)) + .first->second; +} + +void draw_nine_slice_shadow( + QPainter *painter, + QColor shadow_color, + QRectF const &node_rect) +{ + const qreal dpr = painter->device() + ? painter->device()->devicePixelRatioF() + : 1.0; + QImage const atlas = cached_shadow_atlas(shadow_color, dpr); + if (atlas.isNull()) { + return; + } + + // Margin in logical coords — covers the full blur transition + // (outer falloff + inward ramp to plateau). + const qreal m = node_rendering::k_shadow_margin; + + // Destination rect: node rect shifted by shadow offset, expanded by margin. + const qreal dx = node_rect.x() + node_rendering::k_shadow_offset_x - m; + const qreal dy = node_rect.y() + node_rendering::k_shadow_offset_y - m; + const qreal dw = node_rect.width() + 2.0 * m; + const qreal dh = node_rect.height() + 2.0 * m; + const qreal inner_w = dw - 2.0 * m; + const qreal inner_h = dh - 2.0 * m; + + if (inner_w <= 0.0 || inner_h <= 0.0) { + return; + } + + // Source margin/body in logical atlas coords (atlas has DPR set). + const qreal sm = m; // source margin + const qreal sb = static_cast(node_rendering::k_body_size); // source body + + // Snap target rects to device pixels to prevent hairline gaps. + QTransform const &dt = painter->deviceTransform(); + bool inv_ok = false; + QTransform const inv = dt.inverted(&inv_ok); + auto snap = [&](qreal lx, qreal ly, qreal lw, qreal lh) -> QRectF { + if (!inv_ok) { + return QRectF(lx, ly, lw, lh); + } + QPointF p0 = dt.map(QPointF(lx, ly)); + QPointF p1 = dt.map(QPointF(lx + lw, ly + lh)); + p0 = QPointF(std::round(p0.x()), std::round(p0.y())); + p1 = QPointF(std::round(p1.x()), std::round(p1.y())); + return QRectF(inv.map(p0), inv.map(p1)); + }; + + // 9 source rects (logical coords in the atlas). + const QRectF s_tl(0, 0, sm, sm); + const QRectF s_tc(sm, 0, sb, sm); + const QRectF s_tr(sm + sb, 0, sm, sm); + const QRectF s_ml(0, sm, sm, sb); + const QRectF s_mc(sm, sm, sb, sb); + const QRectF s_mr(sm + sb, sm, sm, sb); + const QRectF s_bl(0, sm + sb, sm, sm); + const QRectF s_bc(sm, sm + sb, sb, sm); + const QRectF s_br(sm + sb, sm + sb, sm, sm); + + // 9 target rects (snapped to device pixels). + painter->drawImage(snap(dx, dy, m, m), atlas, s_tl); + painter->drawImage(snap(dx + m, dy, inner_w, m), atlas, s_tc); + painter->drawImage(snap(dx + m + inner_w, dy, m, m), atlas, s_tr); + painter->drawImage(snap(dx, dy + m, m, inner_h), atlas, s_ml); + painter->drawImage(snap(dx + m, dy + m, inner_w, inner_h), atlas, s_mc); + painter->drawImage(snap(dx + m + inner_w, dy + m, m, inner_h), atlas, s_mr); + painter->drawImage(snap(dx, dy + m + inner_h, m, m), atlas, s_bl); + painter->drawImage(snap(dx + m, dy + m + inner_h, inner_w, m), atlas, s_bc); + painter->drawImage(snap(dx + m + inner_w, dy + m + inner_h, m, m), atlas, s_br); +} + +// ============================================================================ + +GraphicsView *graphics_view(NodeGraphicsObject &ngo) +{ + if (auto *view = ngo.currentGraphicsView()) { + return view; + } + + if (!ngo.scene()) { + return nullptr; + } + + QList const views = ngo.scene()->views(); + for (QGraphicsView *view : views) { + if (auto *graphicsView = qobject_cast(view)) { + return graphicsView; + } + } + + return nullptr; +} + +bool should_draw_text_as_path(GraphicsView *view) +{ + if (!view) { + return false; + } + + switch (view->textRenderingPolicy()) { + case GraphicsView::TextRenderingPolicy::QtText: + return false; + case GraphicsView::TextRenderingPolicy::PathWhenZooming: + return view->isZoomAnimating(); + case GraphicsView::TextRenderingPolicy::PathAlways: + return true; + } + + return false; +} + +void configure_text_painter(QPainter *painter, GraphicsView *view) +{ + painter->setRenderHint(QPainter::TextAntialiasing, true); + + if (should_draw_text_as_path(view)) { + return; + } + + if (!view || !view->isZoomAnimating()) { + return; + } + + QFont font = painter->font(); + font.setHintingPreference(QFont::PreferNoHinting); + painter->setFont(font); +} + +// Paths are cached at the origin and the painter is translated to the +// draw position. The cache key combines QFont::key() (which encodes +// family, size, weight, style, hinting, etc.) with the text string. +// For typical node scenes the cache holds ~15 entries and never evicts. +QHash s_text_path_cache; +std::mutex s_text_path_cache_mutex; +std::unordered_map s_validation_icon_cache; +std::mutex s_validation_icon_cache_mutex; + +QImage validation_icon(QIcon const &icon, QColor const &color, qreal dpr) +{ + std::lock_guard lock(s_validation_icon_cache_mutex); + + Shadow_cache_key key{color.rgba(), + static_cast(dpr * 1000000.0)}; + auto it = s_validation_icon_cache.find(key); + if (it != s_validation_icon_cache.end()) { + return it->second; + } + + QImage image = node_rendering::render_icon_image(icon, QSize(16, 16), dpr); + if (image.isNull()) { + return image; + } + + QPainter imgPainter(&image); + imgPainter.setCompositionMode(QPainter::CompositionMode_SourceIn); + imgPainter.fillRect(QRect(QPoint(0, 0), QSize(16, 16)), color); + imgPainter.end(); + + if (s_validation_icon_cache.size() >= 32) { + s_validation_icon_cache.erase(s_validation_icon_cache.begin()); + } + + return s_validation_icon_cache.emplace(key, std::move(image)).first->second; +} + +void draw_text( + QPainter *painter, + GraphicsView *view, + QPointF const &position, + QString const &text, + QColor const &color, + QFont const &font) +{ + if (should_draw_text_as_path(view)) { + QString const key = font.key() + text; + QPainterPath path; + + { + std::lock_guard lock(s_text_path_cache_mutex); + + auto it = s_text_path_cache.constFind(key); + if (it == s_text_path_cache.constEnd()) { + if (s_text_path_cache.size() >= 500) { + // Arbitrary eviction keeps insertion cost predictable without LRU bookkeeping. + s_text_path_cache.erase(s_text_path_cache.begin()); + } + + QPainterPath new_path; + new_path.addText(QPointF(0, 0), font, text); + it = s_text_path_cache.insert(key, std::move(new_path)); + } + + path = *it; + } + + painter->setPen(Qt::NoPen); + painter->translate(position); + painter->fillPath(path, color); + painter->translate(-position); + return; + } + + painter->setFont(font); + painter->setPen(color); + painter->drawText(position, text); +} + +} // namespace + void DefaultNodePainter::paint(QPainter *painter, NodeGraphicsObject &ngo) const { - // TODO? - //AbstractNodeGeometry & geometry = ngo.nodeScene()->nodeGeometry(); - //geometry.recomputeSizeIfFontChanged(painter->font()); + AbstractGraphModel &model = ngo.graphModel(); + NodeId const nodeId = ngo.nodeId(); + GraphicsView *view = graphics_view(ngo); + + std::optional fallback_style; + NodeStyle const &style = node_rendering::resolved_node_style(model, nodeId, fallback_style); - drawNodeRect(painter, ngo); + drawNodeRect(painter, ngo, style); - drawConnectionPoints(painter, ngo); + drawConnectionPoints(painter, ngo, style); - drawFilledConnectionPoints(painter, ngo); + drawFilledConnectionPoints(painter, ngo, style); - drawNodeCaption(painter, ngo); + drawNodeCaption(painter, ngo, style, view); - drawEntryLabels(painter, ngo); + drawEntryLabels(painter, ngo, style, view); drawProcessingIndicator(painter, ngo); drawResizeRect(painter, ngo); - drawValidationIcon(painter, ngo); + drawValidationIcon(painter, ngo, style); } -void DefaultNodePainter::drawNodeRect(QPainter *painter, NodeGraphicsObject &ngo) const +void DefaultNodePainter::drawNodeRect(QPainter *painter, NodeGraphicsObject &ngo, NodeStyle const &nodeStyle) const { AbstractGraphModel &model = ngo.graphModel(); @@ -50,77 +408,75 @@ void DefaultNodePainter::drawNodeRect(QPainter *painter, NodeGraphicsObject &ngo QSize size = geometry.size(nodeId); - QJsonDocument json = QJsonDocument::fromVariant(model.nodeData(nodeId, NodeRole::Style)); - - NodeStyle nodeStyle(json.object()); - QVariant var = model.nodeData(nodeId, NodeRole::ValidationState); bool invalid = false; - QColor color = ngo.isSelected() ? nodeStyle.SelectedBoundaryColor - : nodeStyle.NormalBoundaryColor; + QColor color = ngo.isSelected() ? nodeStyle.selectedBoundaryColor() + : nodeStyle.normalBoundaryColor(); if (var.canConvert()) { auto state = var.value(); - switch (state._state) { + switch (state.state()) { case NodeValidationState::State::Error: { invalid = true; - color = nodeStyle.ErrorColor; + color = nodeStyle.errorColor(); } break; case NodeValidationState::State::Warning: { invalid = true; - color = nodeStyle.WarningColor; - break; + color = nodeStyle.warningColor(); + } break; default: break; } - } + } + + QRectF boundary(0, 0, size.width(), size.height()); + + double const radius = 3.0; + + // 9-slice shadow: a precomputed blurred atlas is sliced into 9 tiles + // and stretched to fit the node. One atlas per (color, DPR), size- + // independent. Much faster than QGraphicsDropShadowEffect and smoother + // than stacked translucent rounded rects. + if (nodeStyle.shadowEnabled()) { + draw_nine_slice_shadow(painter, nodeStyle.shadowColor(), boundary); } if (ngo.nodeState().hovered()) { - QPen p(color, nodeStyle.HoveredPenWidth); - painter->setPen(p); - } else { - QPen p(color, nodeStyle.PenWidth); - painter->setPen(p); + painter->setPen(QPen(color, nodeStyle.hoveredPenWidth())); + } + else { + painter->setPen(QPen(color, nodeStyle.penWidth())); } if (invalid) { painter->setBrush(color); - } else { + } + else { QLinearGradient gradient(QPointF(0.0, 0.0), QPointF(2.0, size.height())); - gradient.setColorAt(0.0, nodeStyle.GradientColor0); - gradient.setColorAt(0.10, nodeStyle.GradientColor1); - gradient.setColorAt(0.90, nodeStyle.GradientColor2); - gradient.setColorAt(1.0, nodeStyle.GradientColor3); - + gradient.setColorAt(0.0, nodeStyle.gradientColor0()); + gradient.setColorAt(0.10, nodeStyle.gradientColor1()); + gradient.setColorAt(0.90, nodeStyle.gradientColor2()); + gradient.setColorAt(1.0, nodeStyle.gradientColor3()); painter->setBrush(gradient); } - QRectF boundary(0, 0, size.width(), size.height()); - - double const radius = 3.0; painter->drawRoundedRect(boundary, radius, radius); } -void DefaultNodePainter::drawConnectionPoints(QPainter *painter, NodeGraphicsObject &ngo) const +void DefaultNodePainter::drawConnectionPoints(QPainter *painter, NodeGraphicsObject &ngo, NodeStyle const &nodeStyle) const { AbstractGraphModel &model = ngo.graphModel(); NodeId const nodeId = ngo.nodeId(); AbstractNodeGeometry &geometry = ngo.nodeScene()->nodeGeometry(); - QJsonDocument json = QJsonDocument::fromVariant(model.nodeData(nodeId, NodeRole::Style)); - NodeStyle nodeStyle(json.object()); - auto const &connectionStyle = StyleCollection::connectionStyle(); - float diameter = nodeStyle.ConnectionPointDiameter; + float diameter = nodeStyle.connectionPointDiameter(); auto reducedDiameter = diameter * 0.6; for (PortType portType : {PortType::Out, PortType::In}) { - auto portCountRole = (portType == PortType::Out) ? NodeRole::OutPortCount - : NodeRole::InPortCount; - size_t const n = model.nodeData(nodeId, portCountRole).toUInt(); + size_t const n = model.nodeData(nodeId, portCountRole(portType)).toUInt(); for (PortIndex portIndex = 0; portIndex < n; ++portIndex) { QPointF p = geometry.portPosition(nodeId, portType, portIndex); @@ -161,7 +517,7 @@ void DefaultNodePainter::drawConnectionPoints(QPainter *painter, NodeGraphicsObj if (connectionStyle.useDataDefinedColors()) { painter->setBrush(connectionStyle.normalColor(dataType.id)); } else { - painter->setBrush(nodeStyle.ConnectionPointColor); + painter->setBrush(nodeStyle.connectionPointColor()); } painter->drawEllipse(p, reducedDiameter * r, reducedDiameter * r); @@ -173,23 +529,16 @@ void DefaultNodePainter::drawConnectionPoints(QPainter *painter, NodeGraphicsObj } } -void DefaultNodePainter::drawFilledConnectionPoints(QPainter *painter, NodeGraphicsObject &ngo) const +void DefaultNodePainter::drawFilledConnectionPoints(QPainter *painter, NodeGraphicsObject &ngo, NodeStyle const &nodeStyle) const { AbstractGraphModel &model = ngo.graphModel(); NodeId const nodeId = ngo.nodeId(); AbstractNodeGeometry &geometry = ngo.nodeScene()->nodeGeometry(); - QJsonDocument json = QJsonDocument::fromVariant(model.nodeData(nodeId, NodeRole::Style)); - NodeStyle nodeStyle(json.object()); - - auto diameter = nodeStyle.ConnectionPointDiameter; + auto diameter = nodeStyle.connectionPointDiameter(); for (PortType portType : {PortType::Out, PortType::In}) { - size_t const n = model - .nodeData(nodeId, - (portType == PortType::Out) ? NodeRole::OutPortCount - : NodeRole::InPortCount) - .toUInt(); + size_t const n = model.nodeData(nodeId, portCountRole(portType)).toUInt(); for (PortIndex portIndex = 0; portIndex < n; ++portIndex) { QPointF p = geometry.portPosition(nodeId, portType, portIndex); @@ -207,8 +556,8 @@ void DefaultNodePainter::drawFilledConnectionPoints(QPainter *painter, NodeGraph painter->setPen(c); painter->setBrush(c); } else { - painter->setPen(nodeStyle.FilledConnectionPointColor); - painter->setBrush(nodeStyle.FilledConnectionPointColor); + painter->setPen(nodeStyle.filledConnectionPointColor()); + painter->setBrush(nodeStyle.filledConnectionPointColor()); } painter->drawEllipse(p, diameter * 0.4, diameter * 0.4); @@ -217,7 +566,7 @@ void DefaultNodePainter::drawFilledConnectionPoints(QPainter *painter, NodeGraph } } -void DefaultNodePainter::drawNodeCaption(QPainter *painter, NodeGraphicsObject &ngo) const +void DefaultNodePainter::drawNodeCaption(QPainter *painter, NodeGraphicsObject &ngo, NodeStyle const &nodeStyle, GraphicsView *view) const { AbstractGraphModel &model = ngo.graphModel(); NodeId const nodeId = ngo.nodeId(); @@ -230,34 +579,32 @@ void DefaultNodePainter::drawNodeCaption(QPainter *painter, NodeGraphicsObject & QFont f = painter->font(); f.setBold(true); + if (!should_draw_text_as_path(view) && view && view->isZoomAnimating()) { + f.setHintingPreference(QFont::PreferNoHinting); + } + else { + f.setHintingPreference(QFont::PreferDefaultHinting); + } QPointF position = geometry.captionPosition(nodeId); - QJsonDocument json = QJsonDocument::fromVariant(model.nodeData(nodeId, NodeRole::Style)); - NodeStyle nodeStyle(json.object()); - - painter->setFont(f); - painter->setPen(nodeStyle.FontColor); - painter->drawText(position, name); + painter->setRenderHint(QPainter::TextAntialiasing, true); + draw_text(painter, view, position, name, nodeStyle.fontColor(), f); f.setBold(false); painter->setFont(f); } -void DefaultNodePainter::drawEntryLabels(QPainter *painter, NodeGraphicsObject &ngo) const +void DefaultNodePainter::drawEntryLabels(QPainter *painter, NodeGraphicsObject &ngo, NodeStyle const &nodeStyle, GraphicsView *view) const { + configure_text_painter(painter, view); + AbstractGraphModel &model = ngo.graphModel(); NodeId const nodeId = ngo.nodeId(); AbstractNodeGeometry &geometry = ngo.nodeScene()->nodeGeometry(); - QJsonDocument json = QJsonDocument::fromVariant(model.nodeData(nodeId, NodeRole::Style)); - NodeStyle nodeStyle(json.object()); - for (PortType portType : {PortType::Out, PortType::In}) { - unsigned int n = model.nodeData(nodeId, - (portType == PortType::Out) - ? NodeRole::OutPortCount - : NodeRole::InPortCount); + unsigned int n = model.nodeData(nodeId, portCountRole(portType)); for (PortIndex portIndex = 0; portIndex < n; ++portIndex) { auto const &connected = model.connections(nodeId, portType, portIndex); @@ -265,9 +612,9 @@ void DefaultNodePainter::drawEntryLabels(QPainter *painter, NodeGraphicsObject & QPointF p = geometry.portTextPosition(nodeId, portType, portIndex); if (connected.empty()) - painter->setPen(nodeStyle.FontColorFaded); + painter->setPen(nodeStyle.fontColorFaded()); else - painter->setPen(nodeStyle.FontColor); + painter->setPen(nodeStyle.fontColor()); QString s; @@ -279,7 +626,9 @@ void DefaultNodePainter::drawEntryLabels(QPainter *painter, NodeGraphicsObject & s = portData.value().name; } - painter->drawText(p, s); + QColor const textColor = connected.empty() ? nodeStyle.fontColorFaded() + : nodeStyle.fontColor(); + draw_text(painter, view, p, s, textColor, painter->font()); } } } @@ -310,19 +659,18 @@ void DefaultNodePainter::drawProcessingIndicator(QPainter *painter, NodeGraphics if (!delegate) return; - // Skip if status is NoStatus - if (delegate->processingStatus() == NodeProcessingStatus::NoStatus) - return; - AbstractNodeGeometry &geometry = ngo.nodeScene()->nodeGeometry(); QSize size = geometry.size(nodeId); - QPixmap pixmap = delegate->processingStatusIcon(); - if (pixmap.isNull()) + qreal const dpr = painter->device() + ? painter->device()->devicePixelRatioF() + : 1.0; + QImage const image = delegate->processingStatusImage(dpr); + if (image.isNull()) return; - ProcessingIconStyle const &iconStyle = delegate->nodeStyle().processingIconStyle; + ProcessingIconStyle const iconStyle = delegate->processingIconStyle(); qreal iconSize = iconStyle._size; qreal margin = iconStyle._margin; @@ -339,11 +687,15 @@ void DefaultNodePainter::drawProcessingIndicator(QPainter *painter, NodeGraphics x = size.width() - iconSize - margin; } - QRect r(x, size.height() - iconSize - margin, iconSize, iconSize); - painter->drawPixmap(r, pixmap); + QRectF const targetRect(x, size.height() - iconSize - margin, iconSize, iconSize); + qreal const image_dpr = image.devicePixelRatio(); + QRectF const sourceRect(QPointF(0, 0), + QSizeF(image.width() / image_dpr, + image.height() / image_dpr)); + painter->drawImage(targetRect, image, sourceRect); } -void DefaultNodePainter::drawValidationIcon(QPainter *painter, NodeGraphicsObject &ngo) const +void DefaultNodePainter::drawValidationIcon(QPainter *painter, NodeGraphicsObject &ngo, NodeStyle const &nodeStyle) const { AbstractGraphModel &model = ngo.graphModel(); NodeId const nodeId = ngo.nodeId(); @@ -354,31 +706,31 @@ void DefaultNodePainter::drawValidationIcon(QPainter *painter, NodeGraphicsObjec return; auto state = var.value(); - if (state._state == NodeValidationState::State::Valid) + if (state.isValid()) return; - QJsonDocument json = QJsonDocument::fromVariant(model.nodeData(nodeId, NodeRole::Style)); - NodeStyle nodeStyle(json.object()); - QSize size = geometry.size(nodeId); - QIcon icon(":/info-tooltip.svg"); - QSize iconSize(16, 16); - QPixmap pixmap = icon.pixmap(iconSize); - - QColor color = (state._state == NodeValidationState::State::Error) ? nodeStyle.ErrorColor - : nodeStyle.WarningColor; + QSize const iconSize(16, 16); - QPainter imgPainter(&pixmap); - imgPainter.setCompositionMode(QPainter::CompositionMode_SourceIn); - imgPainter.fillRect(pixmap.rect(), color); - imgPainter.end(); + QColor color = (state.state() == NodeValidationState::State::Error) ? nodeStyle.errorColor() + : nodeStyle.warningColor(); + qreal const dpr = painter->device() + ? painter->device()->devicePixelRatioF() + : 1.0; + QImage const image = validation_icon(_toolTipIcon, color, dpr); + if (image.isNull()) { + return; + } QPointF center(size.width(), 0.0); center += QPointF(iconSize.width() / 2.0, -iconSize.height() / 2.0); - painter->drawPixmap(center.toPoint() - QPoint(iconSize.width() / 2, iconSize.height() / 2), - pixmap); + QRectF const targetRect(center.x() - iconSize.width() / 2.0, + center.y() - iconSize.height() / 2.0, + iconSize.width(), + iconSize.height()); + painter->drawImage(targetRect, image, QRectF(QPointF(0, 0), QSizeF(iconSize))); } } // namespace QtNodes diff --git a/src/DefaultVerticalNodeGeometry.cpp b/src/DefaultVerticalNodeGeometry.cpp index f20617f25..cbeed2854 100644 --- a/src/DefaultVerticalNodeGeometry.cpp +++ b/src/DefaultVerticalNodeGeometry.cpp @@ -1,7 +1,6 @@ #include "DefaultVerticalNodeGeometry.hpp" #include "AbstractGraphModel.hpp" -#include "NodeData.hpp" #include #include @@ -10,39 +9,12 @@ namespace QtNodes { DefaultVerticalNodeGeometry::DefaultVerticalNodeGeometry(AbstractGraphModel &graphModel) - : AbstractNodeGeometry(graphModel) - , _portSize(20) - , _portSpasing(10) - , _fontMetrics(QFont()) - , _boldFontMetrics(QFont()) -{ - QFont f; - f.setBold(true); - _boldFontMetrics = QFontMetrics(f); - - _portSize = _fontMetrics.height(); -} - -QRectF DefaultVerticalNodeGeometry::boundingRect(NodeId const nodeId) const -{ - QSize s = size(nodeId); - - qreal marginSize = 2.0 * _portSpasing; - QMargins margins(marginSize, marginSize, marginSize, marginSize); - - QRectF r(QPointF(0, 0), s); - - return r.marginsAdded(margins); -} - -QSize DefaultVerticalNodeGeometry::size(NodeId const nodeId) const -{ - return _graphModel.nodeData(nodeId, NodeRole::Size); -} + : DefaultNodeGeometryBase(graphModel) +{} void DefaultVerticalNodeGeometry::recomputeSize(NodeId const nodeId) const { - unsigned int height = _portSpasing; // maxHorizontalPortsExtent(nodeId); + unsigned int height = _portSpacing; if (auto w = _graphModel.nodeData(nodeId, NodeRole::Widget)) { height = std::max(height, static_cast(w->height())); @@ -52,8 +24,8 @@ void DefaultVerticalNodeGeometry::recomputeSize(NodeId const nodeId) const height += capRect.height(); - height += _portSpasing; - height += _portSpasing; + height += _portSpacing; + height += _portSpacing; PortCount nInPorts = _graphModel.nodeData(nodeId, NodeRole::InPortCount); PortCount nOutPorts = _graphModel.nodeData(nodeId, NodeRole::OutPortCount); @@ -67,11 +39,11 @@ void DefaultVerticalNodeGeometry::recomputeSize(NodeId const nodeId) const unsigned int outPortWidth = maxPortsTextAdvance(nodeId, PortType::Out); unsigned int totalInPortsWidth = nInPorts > 0 - ? inPortWidth * nInPorts + _portSpasing * (nInPorts - 1) + ? inPortWidth * nInPorts + _portSpacing * (nInPorts - 1) : 0; unsigned int totalOutPortsWidth = nOutPorts > 0 ? outPortWidth * nOutPorts - + _portSpasing * (nOutPorts - 1) + + _portSpacing * (nOutPorts - 1) : 0; unsigned int width = std::max(totalInPortsWidth, totalOutPortsWidth); @@ -82,8 +54,8 @@ void DefaultVerticalNodeGeometry::recomputeSize(NodeId const nodeId) const width = std::max(width, static_cast(capRect.width())); - width += _portSpasing; - width += _portSpasing; + width += _portSpacing; + width += _portSpacing; QSize size(width, height); @@ -100,7 +72,7 @@ QPointF DefaultVerticalNodeGeometry::portPosition(NodeId const nodeId, switch (portType) { case PortType::In: { - unsigned int inPortWidth = maxPortsTextAdvance(nodeId, PortType::In) + _portSpasing; + unsigned int inPortWidth = maxPortsTextAdvance(nodeId, PortType::In) + _portSpacing; PortCount nInPorts = _graphModel.nodeData(nodeId, NodeRole::InPortCount); @@ -114,7 +86,7 @@ QPointF DefaultVerticalNodeGeometry::portPosition(NodeId const nodeId, } case PortType::Out: { - unsigned int outPortWidth = maxPortsTextAdvance(nodeId, PortType::Out) + _portSpasing; + unsigned int outPortWidth = maxPortsTextAdvance(nodeId, PortType::Out) + _portSpacing; PortCount nOutPorts = _graphModel.nodeData(nodeId, NodeRole::OutPortCount); double x = (size.width() - (nOutPorts - 1) * outPortWidth) / 2.0 + portIndex * outPortWidth; @@ -161,22 +133,12 @@ QPointF DefaultVerticalNodeGeometry::portTextPosition(NodeId const nodeId, return p; } -QRectF DefaultVerticalNodeGeometry::captionRect(NodeId const nodeId) const -{ - if (!_graphModel.nodeData(nodeId, NodeRole::CaptionVisible)) - return QRect(); - - QString name = _graphModel.nodeData(nodeId, NodeRole::Caption); - - return _boldFontMetrics.boundingRect(name); -} - QPointF DefaultVerticalNodeGeometry::captionPosition(NodeId const nodeId) const { QSize size = _graphModel.nodeData(nodeId, NodeRole::Size); unsigned int step = portCaptionsHeight(nodeId, PortType::In); - step += _portSpasing; + step += _portSpacing; auto rect = captionRect(nodeId); @@ -193,9 +155,9 @@ QPointF DefaultVerticalNodeGeometry::widgetPosition(NodeId const nodeId) const // If the widget wants to use as much vertical space as possible, // place it immediately after the caption. if (w->sizePolicy().verticalPolicy() & QSizePolicy::ExpandFlag) { - return QPointF(_portSpasing + maxPortsTextAdvance(nodeId, PortType::In), captionHeight); + return QPointF(_portSpacing + maxPortsTextAdvance(nodeId, PortType::In), captionHeight); } else { - return QPointF(_portSpasing + maxPortsTextAdvance(nodeId, PortType::In), + return QPointF(_portSpacing + maxPortsTextAdvance(nodeId, PortType::In), (captionHeight + size.height() - w->height()) / 2.0); } } @@ -211,67 +173,9 @@ QRect DefaultVerticalNodeGeometry::resizeHandleRect(NodeId const nodeId) const return QRect(size.width() - rectSize, size.height() - rectSize, rectSize, rectSize); } -QRectF DefaultVerticalNodeGeometry::portTextRect(NodeId const nodeId, - PortType const portType, - PortIndex const portIndex) const -{ - QString s; - if (_graphModel.portData(nodeId, portType, portIndex, PortRole::CaptionVisible)) { - s = _graphModel.portData(nodeId, portType, portIndex, PortRole::Caption); - } else { - auto portData = _graphModel.portData(nodeId, portType, portIndex, PortRole::DataType); - - s = portData.value().name; - } - - return _fontMetrics.boundingRect(s); -} - unsigned int DefaultVerticalNodeGeometry::maxHorizontalPortsExtent(NodeId const nodeId) const { - PortCount nInPorts = _graphModel.nodeData(nodeId, NodeRole::InPortCount); - - PortCount nOutPorts = _graphModel.nodeData(nodeId, NodeRole::OutPortCount); - - unsigned int maxNumOfEntries = std::max(nInPorts, nOutPorts); - unsigned int step = _portSize + _portSpasing; - - return step * maxNumOfEntries; -} - -unsigned int DefaultVerticalNodeGeometry::maxPortsTextAdvance(NodeId const nodeId, - PortType const portType) const -{ - unsigned int width = 0; - - size_t const n = _graphModel - .nodeData(nodeId, - (portType == PortType::Out) ? NodeRole::OutPortCount - : NodeRole::InPortCount) - .toUInt(); - - for (PortIndex portIndex = 0ul; portIndex < n; ++portIndex) { - QString name; - - if (_graphModel.portData(nodeId, portType, portIndex, PortRole::CaptionVisible)) { - name = _graphModel.portData(nodeId, portType, portIndex, PortRole::Caption); - } else { - NodeDataType portData = _graphModel.portData(nodeId, - portType, - portIndex, - PortRole::DataType); - - name = portData.name; - } - -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) - width = std::max(unsigned(_fontMetrics.horizontalAdvance(name)), width); -#else - width = std::max(unsigned(_fontMetrics.width(name)), width); -#endif - } - - return width; + return maxPortsExtent(nodeId); } unsigned int DefaultVerticalNodeGeometry::portCaptionsHeight(NodeId const nodeId, @@ -284,7 +188,7 @@ unsigned int DefaultVerticalNodeGeometry::portCaptionsHeight(NodeId const nodeId PortCount nInPorts = _graphModel.nodeData(nodeId, NodeRole::InPortCount); for (PortIndex i = 0; i < nInPorts; ++i) { if (_graphModel.portData(nodeId, PortType::In, i, PortRole::CaptionVisible)) { - h += _portSpasing; + h += _portSpacing; break; } } @@ -295,7 +199,7 @@ unsigned int DefaultVerticalNodeGeometry::portCaptionsHeight(NodeId const nodeId PortCount nOutPorts = _graphModel.nodeData(nodeId, NodeRole::OutPortCount); for (PortIndex i = 0; i < nOutPorts; ++i) { if (_graphModel.portData(nodeId, PortType::Out, i, PortRole::CaptionVisible)) { - h += _portSpasing; + h += _portSpacing; break; } } diff --git a/src/GraphicsView.cpp b/src/GraphicsView.cpp index 42d2854a5..54936eddf 100644 --- a/src/GraphicsView.cpp +++ b/src/GraphicsView.cpp @@ -24,8 +24,118 @@ #include #include +#include + +#include #include +namespace { +constexpr double zoom_friction = 0.75; +constexpr double zoom_impulse_per_step = 1.0; +constexpr double zoom_max_velocity = 5.0; +constexpr double zoom_per_notch = 1.05; +constexpr int zoom_timer_interval_ms = 16; +constexpr double zoom_velocity_epsilon = 0.001; + +double zoom_base_k() +{ + static double const value = std::pow(zoom_per_notch, + (1.0 - zoom_friction) / zoom_impulse_per_step); + return value; +} + +QPointF map_to_scene_exact(QGraphicsView const &view, QPointF const &viewPoint) +{ + bool invertible = false; + QTransform const invertedTransform = view.viewportTransform().inverted(&invertible); + if (!invertible) { + return view.mapToScene(viewPoint.toPoint()); + } + + return invertedTransform.map(viewPoint); +} + +QList graphics_views(QGraphicsScene *scene) +{ + QList result; + if (!scene) { + return result; + } + + QList const views = scene->views(); + for (QGraphicsView *view : views) { + if (auto *graphicsView = qobject_cast(view)) { + result.push_back(graphicsView); + } + } + + return result; +} + +QGraphicsItem::CacheMode effective_node_cache_mode(QGraphicsScene *scene) +{ + QList const views = graphics_views(scene); + if (views.empty()) { + return QGraphicsItem::DeviceCoordinateCache; + } + + // Item cache mode is shared by all attached views. Mixed-view scenes keep node + // rendering uncached so one view cannot force another into an incompatible mode. + if (views.size() > 1) { + return QGraphicsItem::NoCache; + } + + QtNodes::GraphicsView const &view = *views.front(); + if (view.isZoomAnimating()) { + return QGraphicsItem::NoCache; + } + + return view.rasterizationPolicy() == QtNodes::GraphicsView::RasterizationPolicy::Consistent + ? QGraphicsItem::NoCache + : QGraphicsItem::DeviceCoordinateCache; +} + +void set_node_cache_mode( + QGraphicsScene *scene, + QGraphicsItem::CacheMode mode, + bool invalidate_cached_content = false) +{ + if (!scene) { + return; + } + + if (auto *nodeScene = dynamic_cast(scene)) { + for (auto const nodeId : nodeScene->graphModel().allNodeIds()) { + if (auto *item = nodeScene->nodeGraphicsObject(nodeId)) { + if (invalidate_cached_content && mode != QGraphicsItem::NoCache) { + item->setCacheMode(QGraphicsItem::NoCache); + } + item->setCacheMode(mode); + item->update(); + } + } + return; + } + + for (QGraphicsItem *item : scene->items()) { + if (qgraphicsitem_cast(item)) { + if (invalidate_cached_content && mode != QGraphicsItem::NoCache) { + item->setCacheMode(QGraphicsItem::NoCache); + } + item->setCacheMode(mode); + item->update(); + } + } +} + +void refresh_node_cache_mode(QGraphicsScene *scene, bool invalidate_cached_content = false) +{ + set_node_cache_mode(scene, + effective_node_cache_mode(scene), + invalidate_cached_content); +} +} // namespace + using QtNodes::BasicGraphicsScene; using QtNodes::DataFlowGraphModel; using QtNodes::GraphicsView; @@ -46,7 +156,7 @@ GraphicsView::GraphicsView(QWidget *parent) auto const &flowViewStyle = StyleCollection::flowViewStyle(); - setBackgroundBrush(flowViewStyle.BackgroundColor); + setBackgroundBrush(flowViewStyle.backgroundColor()); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); @@ -62,6 +172,8 @@ GraphicsView::GraphicsView(QWidget *parent) // re-calculation when expanding the all QGraphicsItems common rect. int maxSize = 32767; setSceneRect(-maxSize, -maxSize, (maxSize * 2), (maxSize * 2)); + + applyRasterizationPolicy(); } GraphicsView::GraphicsView(BasicGraphicsScene *scene, QWidget *parent) @@ -82,7 +194,14 @@ QAction *GraphicsView::deleteSelectionAction() const void GraphicsView::setScene(BasicGraphicsScene *scene) { + QGraphicsScene *oldScene = this->scene(); + QGraphicsView::setScene(scene); + if (oldScene && oldScene != scene) { + refresh_node_cache_mode(oldScene, true); + oldScene->update(); + } + if (!scene) { // Clear actions. delete _clearSelectionAction; @@ -98,6 +217,8 @@ void GraphicsView::setScene(BasicGraphicsScene *scene) return; } + applyRasterizationPolicy(); + { // setup actions delete _clearSelectionAction; @@ -246,12 +367,154 @@ void GraphicsView::wheelEvent(QWheelEvent *event) return; } - double const d = delta.y() / std::abs(delta.y()); + double const steps = delta.y() / 120.0; + _zoomVelocity = std::clamp(_zoomVelocity + steps * zoom_impulse_per_step, + -zoom_max_velocity, zoom_max_velocity); + _zoomPivot = event->position(); + + if (_zoomTimerId == 0 && std::abs(_zoomVelocity) >= zoom_velocity_epsilon) { + // Seed the timestamp one reference interval in the past so the immediate + // applyZoomStep() call below sees dt ≈ 1.0 and applies a full first step. + _lastZoomStepTime = std::chrono::steady_clock::now() + - std::chrono::milliseconds(zoom_timer_interval_ms); + _zoomTimerId = startTimer(zoom_timer_interval_ms); + refresh_node_cache_mode(scene(), true); + } + + applyZoomStep(); + + event->accept(); +} + +void GraphicsView::timerEvent(QTimerEvent *event) +{ + if (event->timerId() == _zoomTimerId) { + applyZoomStep(); + } else { + QGraphicsView::timerEvent(event); + } +} + +double GraphicsView::zoomAnimationScaleFactor(double velocity, double elapsedTimerSteps) +{ + if (elapsedTimerSteps <= 0.0 || std::abs(velocity) < zoom_velocity_epsilon) { + return 1.0; + } + + double const velocityDecay = std::pow(zoom_friction, elapsedTimerSteps); + double const integratedVelocity = std::abs(1.0 - zoom_friction) > 1e-12 + ? velocity * (1.0 - velocityDecay) / (1.0 - zoom_friction) + : velocity * elapsedTimerSteps; + + return std::pow(zoom_base_k(), integratedVelocity); +} + +double GraphicsView::zoomAnimationVelocityAfter(double velocity, double elapsedTimerSteps) +{ + if (elapsedTimerSteps <= 0.0 || std::abs(velocity) < zoom_velocity_epsilon) { + return velocity; + } + + return velocity * std::pow(zoom_friction, elapsedTimerSteps); +} + +void GraphicsView::applyZoomStep() +{ + if (std::abs(_zoomVelocity) < zoom_velocity_epsilon) { + stopZoomTimer(); + return; + } + + auto now = std::chrono::steady_clock::now(); + double elapsed_ms = std::chrono::duration(now - _lastZoomStepTime).count(); + _lastZoomStepTime = now; + + if (elapsed_ms <= 0.0) { + elapsed_ms = zoom_timer_interval_ms; + } + + double const dt = elapsed_ms / zoom_timer_interval_ms; + double const factor = zoomAnimationScaleFactor(_zoomVelocity, dt); + double const current_scale = transform().m11(); + double const new_scale = current_scale * factor; + + if (_scaleRange.maximum > 0 && new_scale > _scaleRange.maximum) { + applyZoomFactor(_scaleRange.maximum / current_scale); + stopZoomTimer(); + return; + } + if (_scaleRange.minimum > 0 && new_scale < _scaleRange.minimum) { + applyZoomFactor(_scaleRange.minimum / current_scale); + stopZoomTimer(); + return; + } + + applyZoomFactor(factor); + _zoomVelocity = zoomAnimationVelocityAfter(_zoomVelocity, dt); +} + +void GraphicsView::applyZoomFactor(double factor) +{ + QPointF const scenePivot = map_to_scene_exact(*this, _zoomPivot); + double const newScale = transform().m11() * factor; + + auto const savedAnchor = transformationAnchor(); + setTransformationAnchor(QGraphicsView::NoAnchor); + + QTransform scaledTransform; + scaledTransform.scale(newScale, newScale); + setTransform(scaledTransform, false); + + QPointF const pivotAfterScale = viewportTransform().map(scenePivot); + QPointF const shift = pivotAfterScale - _zoomPivot; + horizontalScrollBar()->setValue(horizontalScrollBar()->value() + qRound(shift.x())); + verticalScrollBar()->setValue(verticalScrollBar()->value() + qRound(shift.y())); + + QPointF const pivotAfterScroll = viewportTransform().map(scenePivot); + QPointF const residual = _zoomPivot - pivotAfterScroll; + + QTransform preciseTransform; + preciseTransform.translate(residual.x(), residual.y()); + preciseTransform.scale(newScale, newScale); + setTransform(preciseTransform, false); + + setTransformationAnchor(savedAnchor); + + Q_EMIT scaleChanged(newScale); +} + +void GraphicsView::stopZoomTimer() +{ + bool const hadFractionalOffset = std::abs(transform().dx()) > 1e-6 || std::abs(transform().dy()) > 1e-6; + bool const was_zoom_animating = (_zoomTimerId != 0); + + if (_zoomTimerId != 0) { + killTimer(_zoomTimerId); + _zoomTimerId = 0; + } + + if (hadFractionalOffset && _rasterizationPolicy == RasterizationPolicy::Crisp) { + QPointF const viewOrigin(0.0, 0.0); + QPointF const sceneAtOrigin = map_to_scene_exact(*this, viewOrigin); + double const s = transform().m11(); + QTransform clean; + clean.scale(s, s); + setTransform(clean, false); + + QPointF const originAfterCleanup = viewportTransform().map(sceneAtOrigin); + horizontalScrollBar()->setValue(horizontalScrollBar()->value() + qRound(originAfterCleanup.x())); + verticalScrollBar()->setValue(verticalScrollBar()->value() + qRound(originAfterCleanup.y())); + } + + _zoomVelocity = 0.0; - if (d > 0.0) - scaleUp(); - else - scaleDown(); + if (was_zoom_animating) { + refresh_node_cache_mode(scene(), true); + if (scene()) { + scene()->update(); + } + viewport()->update(); + } } double GraphicsView::getScale() const @@ -259,6 +522,51 @@ double GraphicsView::getScale() const return transform().m11(); } +bool GraphicsView::isZoomAnimating() const +{ + return _zoomTimerId != 0; +} + +GraphicsView::TextRenderingPolicy GraphicsView::textRenderingPolicy() const +{ + return _textRenderingPolicy; +} + +void GraphicsView::setTextRenderingPolicy(TextRenderingPolicy policy) +{ + if (_textRenderingPolicy == policy) { + return; + } + + _textRenderingPolicy = policy; + + refresh_node_cache_mode(scene(), true); + if (scene()) { + scene()->update(); + } + viewport()->update(); +} + +GraphicsView::RasterizationPolicy GraphicsView::rasterizationPolicy() const +{ + return _rasterizationPolicy; +} + +void GraphicsView::setRasterizationPolicy(RasterizationPolicy policy) +{ + if (_rasterizationPolicy == policy) { + return; + } + + _rasterizationPolicy = policy; + applyRasterizationPolicy(); +} + +void GraphicsView::stopZoomAnimation() +{ + stopZoomTimer(); +} + void GraphicsView::setScaleRange(double minimum, double maximum) { if (maximum < minimum) @@ -278,6 +586,8 @@ void GraphicsView::setScaleRange(ScaleRange range) void GraphicsView::scaleUp() { + stopZoomTimer(); + double const step = 1.2; double const factor = std::pow(step, 1.0); @@ -296,6 +606,8 @@ void GraphicsView::scaleUp() void GraphicsView::scaleDown() { + stopZoomTimer(); + double const step = 1.2; double const factor = std::pow(step, -1.0); @@ -314,6 +626,8 @@ void GraphicsView::scaleDown() void GraphicsView::setupScale(double scale) { + stopZoomTimer(); + scale = std::max(_scaleRange.minimum, std::min(_scaleRange.maximum, scale)); if (scale <= 0) @@ -394,6 +708,10 @@ void GraphicsView::keyReleaseEvent(QKeyEvent *event) void GraphicsView::mousePressEvent(QMouseEvent *event) { + if (event->button() != Qt::NoButton) { + stopZoomTimer(); + } + QGraphicsView::mousePressEvent(event); if (event->button() == Qt::LeftButton) { _clickPos = mapToScene(event->pos()); @@ -420,6 +738,24 @@ void GraphicsView::drawBackground(QPainter *painter, const QRectF &r) { QGraphicsView::drawBackground(painter, r); + painter->setRenderHint(QPainter::Antialiasing, true); + + qreal x_offset = 0.0; + qreal y_offset = 0.0; + bool const crisp_grid = (_rasterizationPolicy == RasterizationPolicy::Crisp); + + if (crisp_grid) { + QTransform const view_transform = transform(); + qreal const scale_x = std::abs(view_transform.m11()); + qreal const scale_y = std::abs(view_transform.m22()); + if (scale_x > 0.0) { + x_offset = 0.5 / scale_x; + } + if (scale_y > 0.0) { + y_offset = 0.5 / scale_y; + } + } + auto drawGrid = [&](double gridStep) { QRect windowRect = rect(); QPointF tl = mapToScene(windowRect.topLeft()); @@ -432,26 +768,30 @@ void GraphicsView::drawBackground(QPainter *painter, const QRectF &r) // vertical lines for (int xi = int(left); xi <= int(right); ++xi) { - QLineF line(xi * gridStep, bottom * gridStep, xi * gridStep, top * gridStep); + qreal const x = xi * gridStep + x_offset; + QLineF line(x, bottom * gridStep, x, top * gridStep); painter->drawLine(line); } // horizontal lines for (int yi = int(bottom); yi <= int(top); ++yi) { - QLineF line(left * gridStep, yi * gridStep, right * gridStep, yi * gridStep); + qreal const y = yi * gridStep + y_offset; + QLineF line(left * gridStep, y, right * gridStep, y); painter->drawLine(line); } }; auto const &flowViewStyle = StyleCollection::flowViewStyle(); - QPen pfine(flowViewStyle.FineGridColor, 1.0); + QPen pfine(flowViewStyle.fineGridColor(), 1.0); + pfine.setCosmetic(crisp_grid); painter->setPen(pfine); drawGrid(15); - QPen p(flowViewStyle.CoarseGridColor, 1.0); + QPen p(flowViewStyle.coarseGridColor(), 1.0); + p.setCosmetic(crisp_grid); painter->setPen(p); drawGrid(150); @@ -482,11 +822,15 @@ QPointF GraphicsView::scenePastePosition() void GraphicsView::zoomFitAll() { + stopZoomTimer(); + fitInView(scene()->itemsBoundingRect(), Qt::KeepAspectRatio); } void GraphicsView::zoomFitSelected() { + stopZoomTimer(); + if (scene()->selectedItems().count() > 0) { QRectF unitedBoundingRect{}; @@ -498,3 +842,32 @@ void GraphicsView::zoomFitSelected() fitInView(unitedBoundingRect, Qt::KeepAspectRatio); } } + +void GraphicsView::applyRasterizationPolicy() +{ + if (_rasterizationPolicy == RasterizationPolicy::Consistent) { + setCacheMode(QGraphicsView::CacheNone); + } + else { + if (std::abs(transform().dx()) > 1e-6 || std::abs(transform().dy()) > 1e-6) { + QPointF const viewOrigin(0.0, 0.0); + QPointF const sceneAtOrigin = map_to_scene_exact(*this, viewOrigin); + double const s = transform().m11(); + QTransform clean; + clean.scale(s, s); + setTransform(clean, false); + + QPointF const originAfterCleanup = viewportTransform().map(sceneAtOrigin); + horizontalScrollBar()->setValue(horizontalScrollBar()->value() + qRound(originAfterCleanup.x())); + verticalScrollBar()->setValue(verticalScrollBar()->value() + qRound(originAfterCleanup.y())); + } + + setCacheMode(QGraphicsView::CacheBackground); + } + + refresh_node_cache_mode(scene(), true); + if (scene()) { + scene()->update(); + } + viewport()->update(); +} diff --git a/src/GraphicsViewStyle.cpp b/src/GraphicsViewStyle.cpp index 3138e8eee..00ecf7eec 100644 --- a/src/GraphicsViewStyle.cpp +++ b/src/GraphicsViewStyle.cpp @@ -2,13 +2,10 @@ #include "StyleCollection.hpp" -#include -#include #include -#include - using QtNodes::GraphicsViewStyle; +using namespace QtNodes::detail; inline void initResources() { @@ -37,59 +34,29 @@ void GraphicsViewStyle::setStyle(QString jsonText) StyleCollection::setGraphicsViewStyle(style); } -#ifdef STYLE_DEBUG -#define FLOW_VIEW_STYLE_CHECK_UNDEFINED_VALUE(v, variable) \ - { \ - if (v.type() == QJsonValue::Undefined || v.type() == QJsonValue::Null) \ - qWarning() << "Undefined value for parameter:" << #variable; \ - } -#else -#define FLOW_VIEW_STYLE_CHECK_UNDEFINED_VALUE(v, variable) -#endif - -#define FLOW_VIEW_STYLE_READ_COLOR(values, variable) \ - { \ - auto valueRef = values[#variable]; \ - FLOW_VIEW_STYLE_CHECK_UNDEFINED_VALUE(valueRef, variable) \ - if (valueRef.isArray()) { \ - auto colorArray = valueRef.toArray(); \ - std::vector rgb; \ - rgb.reserve(3); \ - for (auto it = colorArray.begin(); it != colorArray.end(); ++it) { \ - rgb.push_back((*it).toInt()); \ - } \ - variable = QColor(rgb[0], rgb[1], rgb[2]); \ - } else { \ - variable = QColor(valueRef.toString()); \ - } \ - } - -#define FLOW_VIEW_STYLE_WRITE_COLOR(values, variable) \ - { \ - values[#variable] = variable.name(); \ - } - void GraphicsViewStyle::loadJson(QJsonObject const &json) { - QJsonValue nodeStyleValues = json["GraphicsViewStyle"]; + QJsonObject obj = json["GraphicsViewStyle"].toObject(); - QJsonObject obj = nodeStyleValues.toObject(); - - FLOW_VIEW_STYLE_READ_COLOR(obj, BackgroundColor); - FLOW_VIEW_STYLE_READ_COLOR(obj, FineGridColor); - FLOW_VIEW_STYLE_READ_COLOR(obj, CoarseGridColor); + readColor(obj, "BackgroundColor", _BackgroundColor); + readColor(obj, "FineGridColor", _FineGridColor); + readColor(obj, "CoarseGridColor", _CoarseGridColor); } QJsonObject GraphicsViewStyle::toJson() const { QJsonObject obj; - FLOW_VIEW_STYLE_WRITE_COLOR(obj, BackgroundColor); - FLOW_VIEW_STYLE_WRITE_COLOR(obj, FineGridColor); - FLOW_VIEW_STYLE_WRITE_COLOR(obj, CoarseGridColor); + writeColor(obj, "BackgroundColor", _BackgroundColor); + writeColor(obj, "FineGridColor", _FineGridColor); + writeColor(obj, "CoarseGridColor", _CoarseGridColor); QJsonObject root; root["GraphicsViewStyle"] = obj; return root; } + +QColor GraphicsViewStyle::backgroundColor() const { return _BackgroundColor; } +QColor GraphicsViewStyle::fineGridColor() const { return _FineGridColor; } +QColor GraphicsViewStyle::coarseGridColor() const { return _CoarseGridColor; } diff --git a/src/GroupGraphicsObject.cpp b/src/GroupGraphicsObject.cpp index f9c995fb4..f844f266f 100644 --- a/src/GroupGraphicsObject.cpp +++ b/src/GroupGraphicsObject.cpp @@ -14,6 +14,20 @@ using QtNodes::GroupGraphicsObject; using QtNodes::NodeConnectionInteraction; using QtNodes::NodeGraphicsObject; using QtNodes::NodeGroup; +using QtNodes::NodeRole; + +namespace { + +void update_model_position_if_needed(NodeGraphicsObject &node) +{ + QPointF const scenePos = node.pos(); + QPointF const modelPos = node.graphModel().nodeData(node.nodeId(), NodeRole::Position); + if (scenePos != modelPos) { + node.graphModel().setNodeData(node.nodeId(), NodeRole::Position, scenePos); + } +} + +} // namespace IconGraphicsItem::IconGraphicsItem(QGraphicsItem *parent) : QGraphicsPixmapItem(parent) @@ -57,6 +71,8 @@ GroupGraphicsObject::GroupGraphicsObject(BasicGraphicsScene &scene, NodeGroup &n setZValue(-_groupAreaZValue); setAcceptHoverEvents(true); + + updateGroupGeometry(); } GroupGraphicsObject::~GroupGraphicsObject() @@ -74,7 +90,7 @@ NodeGroup const &GroupGraphicsObject::group() const return _group; } -QRectF GroupGraphicsObject::boundingRect() const +QRectF GroupGraphicsObject::computeGroupRect() const { QRectF ret{}; for (auto &node : _group.childNodes()) { @@ -112,6 +128,7 @@ void GroupGraphicsObject::moveNodes(const QPointF &offset) node->setPos(newPosition); node->update(); } + updateGroupGeometry(); } void GroupGraphicsObject::lock(bool locked) @@ -133,12 +150,11 @@ bool GroupGraphicsObject::locked() const void GroupGraphicsObject::positionLockedIcon() { - _lockedGraphicsItem->setPos( - boundingRect().topRight() - + QPointF(-(_roundedBorderRadius + IconGraphicsItem::iconSize()), _roundedBorderRadius)); - _unlockedGraphicsItem->setPos( - boundingRect().topRight() - + QPointF(-(_roundedBorderRadius + IconGraphicsItem::iconSize()), _roundedBorderRadius)); + QPointF const icon_pos = rect().topRight() + + QPointF(-(_roundedBorderRadius + IconGraphicsItem::iconSize()), + _roundedBorderRadius); + _lockedGraphicsItem->setPos(icon_pos); + _unlockedGraphicsItem->setPos(icon_pos); } void GroupGraphicsObject::setHovered(bool hovered) @@ -157,14 +173,16 @@ void GroupGraphicsObject::setHovered(bool hovered) void GroupGraphicsObject::setPossibleChild(QtNodes::NodeGraphicsObject *possibleChild) { _possibleChild = possibleChild; + updateGroupGeometry(); } void GroupGraphicsObject::unsetPossibleChild() { _possibleChild = nullptr; + updateGroupGeometry(); } -std::vector> GroupGraphicsObject::connections() const +std::vector GroupGraphicsObject::connections() const { return _scene.connectionsWithinGroup(group().id()); } @@ -198,20 +216,40 @@ void GroupGraphicsObject::mouseMoveEvent(QGraphicsSceneMouseEvent *event) } } +void GroupGraphicsObject::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) +{ + QGraphicsItem::mouseReleaseEvent(event); + + for (auto &node : group().childNodes()) { + update_model_position_if_needed(*node); + } + + moveConnections(); + updateGroupGeometry(); +} + void GroupGraphicsObject::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) { QGraphicsItem::mouseDoubleClickEvent(event); lock(!locked()); } +void GroupGraphicsObject::updateGroupGeometry() +{ + QRectF const newRect = computeGroupRect(); + if (newRect != rect()) { + prepareGeometryChange(); + setRect(newRect); + } + positionLockedIcon(); + update(); +} + void GroupGraphicsObject::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { Q_UNUSED(widget); - prepareGeometryChange(); - setRect(boundingRect()); - positionLockedIcon(); painter->setClipRect(option->exposedRect); painter->setBrush(_currentFillColor); diff --git a/src/NodeConnectionInteraction.cpp b/src/NodeConnectionInteraction.cpp index c43fe36f1..61474f90b 100644 --- a/src/NodeConnectionInteraction.cpp +++ b/src/NodeConnectionInteraction.cpp @@ -96,36 +96,18 @@ bool NodeConnectionInteraction::disconnect(PortType portToDisconnect) const //Repaint connection points. NodeId connectedNodeId = getNodeId(oppositePort(portToDisconnect), connectionId); - _scene.nodeGraphicsObject(connectedNodeId)->update(); + if (auto *connectedNode = _scene.nodeGraphicsObject(connectedNodeId)) { + connectedNode->update(); + } NodeId disconnectedNodeId = getNodeId(portToDisconnect, connectionId); - _scene.nodeGraphicsObject(disconnectedNodeId)->update(); + if (auto *disconnectedNode = _scene.nodeGraphicsObject(disconnectedNodeId)) { + disconnectedNode->update(); + } return true; } -// ------------------ util functions below - -PortType NodeConnectionInteraction::connectionRequiredPort() const -{ - auto const &state = _cgo.connectionState(); - - return state.requiredPort(); -} - -QPointF NodeConnectionInteraction::nodePortScenePosition(PortType portType, - PortIndex portIndex) const -{ - AbstractNodeGeometry &geometry = _scene.nodeGeometry(); - - QPointF p = geometry.portScenePosition(_ngo.nodeId(), - portType, - portIndex, - _ngo.sceneTransform()); - - return p; -} - PortIndex NodeConnectionInteraction::nodePortIndexUnderScenePoint(PortType portType, QPointF const &scenePoint) const { diff --git a/src/NodeDelegateModel.cpp b/src/NodeDelegateModel.cpp index fc8f3e8dd..b0f19b569 100644 --- a/src/NodeDelegateModel.cpp +++ b/src/NodeDelegateModel.cpp @@ -1,9 +1,36 @@ #include "NodeDelegateModel.hpp" +#include "NodeRenderingUtils.hpp" #include "StyleCollection.hpp" namespace QtNodes { +namespace { + +QIcon const &status_icon(NodeStyle const &style, NodeProcessingStatus status) +{ + switch (status) { + case NodeProcessingStatus::Updated: + return style.statusUpdated(); + case NodeProcessingStatus::Processing: + return style.statusProcessing(); + case NodeProcessingStatus::Pending: + return style.statusPending(); + case NodeProcessingStatus::Empty: + return style.statusEmpty(); + case NodeProcessingStatus::Failed: + return style.statusInvalid(); + case NodeProcessingStatus::Partial: + return style.statusPartial(); + case NodeProcessingStatus::NoStatus: + break; + } + + return style.statusEmpty(); +} + +} // namespace + NodeDelegateModel::NodeDelegateModel() : _nodeStyle(StyleCollection::nodeStyle()) { @@ -53,66 +80,88 @@ NodeStyle const &NodeDelegateModel::nodeStyle() const void NodeDelegateModel::setNodeStyle(NodeStyle const &style) { + std::lock_guard lock(_processingStatusIconMutex); _nodeStyle = style; + _processingStatusIconDirty = true; } -QPixmap NodeDelegateModel::processingStatusIcon() const +QImage NodeDelegateModel::processingStatusImage(qreal dpr) const { - int resolution = _nodeStyle.processingIconStyle._resolution; - switch (_processingStatus) { - case NodeProcessingStatus::NoStatus: + std::lock_guard lock(_processingStatusIconMutex); + + int const resolution = _nodeStyle.processingIconStyle()._resolution; + + if (_processingStatus == NodeProcessingStatus::NoStatus) { return {}; - case NodeProcessingStatus::Updated: - return _nodeStyle.statusUpdated.pixmap(resolution); - case NodeProcessingStatus::Processing: - return _nodeStyle.statusProcessing.pixmap(resolution); - case NodeProcessingStatus::Pending: - return _nodeStyle.statusPending.pixmap(resolution); - case NodeProcessingStatus::Empty: - return _nodeStyle.statusEmpty.pixmap(resolution); - case NodeProcessingStatus::Failed: - return _nodeStyle.statusInvalid.pixmap(resolution); - case NodeProcessingStatus::Partial: - return _nodeStyle.statusPartial.pixmap(resolution); } - return {}; + if (!_processingStatusIconDirty && _cachedProcessingStatus == _processingStatus + && _cachedProcessingStatusResolution == resolution + && qFuzzyCompare(_cachedProcessingStatusDpr, dpr)) { + return _cachedProcessingStatusImage; + } + + _cachedProcessingStatusImage = node_rendering::render_icon_image( + status_icon(_nodeStyle, _processingStatus), + QSize(resolution, resolution), + dpr); + + _cachedProcessingStatus = _processingStatus; + _cachedProcessingStatusResolution = resolution; + _cachedProcessingStatusDpr = dpr; + _processingStatusIconDirty = false; + + return _cachedProcessingStatusImage; +} + +ProcessingIconStyle NodeDelegateModel::processingIconStyle() const +{ + std::lock_guard lock(_processingStatusIconMutex); + return _nodeStyle.processingIconStyle(); } void NodeDelegateModel::setStatusIcon(NodeProcessingStatus status, const QPixmap &pixmap) { + std::lock_guard lock(_processingStatusIconMutex); + switch (status) { case NodeProcessingStatus::NoStatus: break; case NodeProcessingStatus::Updated: - _nodeStyle.statusUpdated = QIcon(pixmap); + _nodeStyle.setStatusUpdated(QIcon(pixmap)); break; case NodeProcessingStatus::Processing: - _nodeStyle.statusProcessing = QIcon(pixmap); + _nodeStyle.setStatusProcessing(QIcon(pixmap)); break; case NodeProcessingStatus::Pending: - _nodeStyle.statusPending = QIcon(pixmap); + _nodeStyle.setStatusPending(QIcon(pixmap)); break; case NodeProcessingStatus::Empty: - _nodeStyle.statusEmpty = QIcon(pixmap); + _nodeStyle.setStatusEmpty(QIcon(pixmap)); break; case NodeProcessingStatus::Failed: - _nodeStyle.statusInvalid = QIcon(pixmap); + _nodeStyle.setStatusInvalid(QIcon(pixmap)); break; case NodeProcessingStatus::Partial: - _nodeStyle.statusPartial = QIcon(pixmap); + _nodeStyle.setStatusPartial(QIcon(pixmap)); break; } + + _processingStatusIconDirty = true; } void NodeDelegateModel::setStatusIconStyle(const ProcessingIconStyle &style) { - _nodeStyle.processingIconStyle = style; + std::lock_guard lock(_processingStatusIconMutex); + _nodeStyle.setProcessingIconStyle(style); + _processingStatusIconDirty = true; } void NodeDelegateModel::setNodeProcessingStatus(NodeProcessingStatus status) { + std::lock_guard lock(_processingStatusIconMutex); _processingStatus = status; + _processingStatusIconDirty = true; } void NodeDelegateModel::setBackgroundColor(QColor const &color) diff --git a/src/NodeDelegateModelRegistry.cpp b/src/NodeDelegateModelRegistry.cpp index 11a1f84aa..1698c7538 100644 --- a/src/NodeDelegateModelRegistry.cpp +++ b/src/NodeDelegateModelRegistry.cpp @@ -1,9 +1,5 @@ #include "NodeDelegateModelRegistry.hpp" -#include -#include - -using QtNodes::NodeDataType; using QtNodes::NodeDelegateModel; using QtNodes::NodeDelegateModelRegistry; diff --git a/src/NodeGraphicsObject.cpp b/src/NodeGraphicsObject.cpp index 61babbb4c..e441586a0 100644 --- a/src/NodeGraphicsObject.cpp +++ b/src/NodeGraphicsObject.cpp @@ -5,6 +5,8 @@ #include "BasicGraphicsScene.hpp" #include "ConnectionGraphicsObject.hpp" #include "ConnectionIdUtils.hpp" +#include "GraphicsView.hpp" +#include "NodeRenderingUtils.hpp" #include "NodeConnectionInteraction.hpp" #include "NodeDelegateModel.hpp" #include "NodeGroup.hpp" @@ -12,13 +14,66 @@ #include "UndoCommands.hpp" #include -#include #include #include +#include namespace QtNodes { +namespace { + +GraphicsView *graphics_view_from_widget(QWidget *widget) +{ + while (widget) { + if (auto *graphicsView = qobject_cast(widget)) { + return graphicsView; + } + widget = widget->parentWidget(); + } + + return nullptr; +} + +QGraphicsItem::CacheMode initial_cache_mode(BasicGraphicsScene &scene) +{ + QList const views = scene.views(); + int graphics_view_count = 0; + + for (QGraphicsView *view : views) { + if (auto *graphicsView = qobject_cast(view)) { + ++graphics_view_count; + if (graphicsView->rasterizationPolicy() == GraphicsView::RasterizationPolicy::Consistent) { + return QGraphicsItem::NoCache; + } + } + } + + // Cache mode is shared per item, so scenes rendered by multiple views fall back + // to uncached rendering instead of letting one view's policy govern the others. + if (graphics_view_count > 1) { + return QGraphicsItem::NoCache; + } + + return QGraphicsItem::DeviceCoordinateCache; +} + +void update_model_position_if_needed(NodeGraphicsObject &node) +{ + auto *scene = node.nodeScene(); + if (!scene) { + return; + } + + QPointF const scenePos = node.pos(); + QPointF const modelPos = node.graphModel().nodeData(node.nodeId(), NodeRole::Position); + if (scenePos != modelPos) { + node.graphModel().setNodeData(node.nodeId(), NodeRole::Position, scenePos); + } +} + +} // namespace + NodeGraphicsObject::NodeGraphicsObject(BasicGraphicsScene &scene, NodeId nodeId) : _nodeId(nodeId) , _graphModel(scene.graphModel()) @@ -36,22 +91,14 @@ NodeGraphicsObject::NodeGraphicsObject(BasicGraphicsScene &scene, NodeId nodeId) setLockedState(); - setCacheMode(QGraphicsItem::DeviceCoordinateCache); + setCacheMode(initial_cache_mode(scene)); - QJsonObject nodeStyleJson = _graphModel.nodeData(_nodeId, NodeRole::Style).toJsonObject(); + std::optional fallback_style; + NodeStyle const &nodeStyle = node_rendering::resolved_node_style(_graphModel, + _nodeId, + fallback_style); - NodeStyle nodeStyle(nodeStyleJson); - - if (nodeStyle.ShadowEnabled) { - auto effect = new QGraphicsDropShadowEffect; - effect->setOffset(4, 4); - effect->setBlurRadius(20); - effect->setColor(nodeStyle.ShadowColor); - - setGraphicsEffect(effect); - } - - setOpacity(nodeStyle.Opacity); + setOpacity(nodeStyle.opacity()); setAcceptHoverEvents(true); @@ -78,7 +125,9 @@ AbstractGraphModel &NodeGraphicsObject::graphModel() const BasicGraphicsScene *NodeGraphicsObject::nodeScene() const { - return dynamic_cast(scene()); + auto *sceneObject = dynamic_cast(scene()); + Q_ASSERT(sceneObject != nullptr); + return sceneObject; } void NodeGraphicsObject::updateQWidgetEmbedPos() @@ -114,7 +163,7 @@ void NodeGraphicsObject::embedQWidget() updateQWidgetEmbedPos(); - //update(); + _proxyWidget->setOpacity(1.0); _proxyWidget->setFlag(QGraphicsItem::ItemIgnoresParentOpacity); @@ -136,7 +185,6 @@ QRectF NodeGraphicsObject::boundingRect() const { AbstractNodeGeometry &geometry = nodeScene()->nodeGeometry(); return geometry.boundingRect(_nodeId); - //return NodeGeometry(_nodeId, _graphModel, nodeScene()).boundingRect(); } void NodeGraphicsObject::setGeometryChanged() @@ -168,21 +216,26 @@ void NodeGraphicsObject::reactToConnection(ConnectionGraphicsObject const *cgo) update(); } -void NodeGraphicsObject::paint(QPainter *painter, QStyleOptionGraphicsItem const *option, QWidget *) +void NodeGraphicsObject::updateValidationTooltip() { QString tooltip; QVariant var = _graphModel.nodeData(_nodeId, NodeRole::ValidationState); if (var.canConvert()) { auto state = var.value(); - if (state._state != NodeValidationState::State::Valid) { - tooltip = state._stateMessage; + if (!state.isValid()) { + tooltip = state.message(); } } setToolTip(tooltip); +} +void NodeGraphicsObject::paint(QPainter *painter, QStyleOptionGraphicsItem const *option, QWidget *widget) +{ painter->setClipRect(option->exposedRect); + _currentGraphicsView = graphics_view_from_widget(widget); nodeScene()->nodePainter().paint(painter, *this); + _currentGraphicsView = nullptr; } QVariant NodeGraphicsObject::itemChange(GraphicsItemChange change, const QVariant &value) @@ -216,15 +269,12 @@ void NodeGraphicsObject::mousePressEvent(QGraphicsSceneMouseEvent *event) // Start dragging existing connection. if (!connected.empty() && portToCheck == PortType::In) { auto const &cnId = *connected.begin(); + if (auto *connection = nodeScene()->connectionGraphicsObject(cnId)) { + NodeConnectionInteraction interaction(*this, *connection, *nodeScene()); - // Need ConnectionGraphicsObject - - NodeConnectionInteraction interaction(*this, - *nodeScene()->connectionGraphicsObject(cnId), - *nodeScene()); - - if (_graphModel.detachPossible(cnId)) - interaction.disconnect(portToCheck); + if (_graphModel.detachPossible(cnId)) + interaction.disconnect(portToCheck); + } } else // initialize new Connection { if (portToCheck == PortType::Out) { @@ -232,7 +282,7 @@ void NodeGraphicsObject::mousePressEvent(QGraphicsSceneMouseEvent *event) .portData(_nodeId, portToCheck, portIndex, - PortRole::ConnectionPolicyRole) + PortRole::ConnectionPolicy) .value(); if (!connected.empty() && outPolicy == ConnectionPolicy::One) { @@ -310,6 +360,8 @@ void NodeGraphicsObject::mouseMoveEvent(QGraphicsSceneMouseEvent *event) nodeGroup->groupGraphicsObject().moveConnections(); if (nodeGroup->groupGraphicsObject().locked()) { nodeGroup->groupGraphicsObject().moveNodes(diff); + } else { + nodeGroup->groupGraphicsObject().updateGroupGeometry(); } } else { moveConnections(); @@ -362,6 +414,7 @@ void NodeGraphicsObject::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) // position connections precisely after fast node move moveConnections(); + update_model_position_if_needed(*this); if (nodeScene()->groupingEnabled() && _draggingIntoGroup && _possibleGroup && _nodeGroup.expired()) { @@ -420,10 +473,9 @@ void NodeGraphicsObject::hoverMoveEvent(QGraphicsSceneHoverEvent *event) { auto pos = event->pos(); - //NodeGeometry geometry(_nodeId, _graphModel, nodeScene()); AbstractNodeGeometry &geometry = nodeScene()->nodeGeometry(); - if ((_graphModel.nodeFlags(_nodeId) | NodeFlag::Resizable) + if ((_graphModel.nodeFlags(_nodeId) & NodeFlag::Resizable) && geometry.resizeHandleRect(_nodeId).contains(QPoint(pos.x(), pos.y()))) { setCursor(QCursor(Qt::SizeFDiagCursor)); } else { diff --git a/src/NodeGroup.cpp b/src/NodeGroup.cpp index 901fb5e25..d7757d437 100644 --- a/src/NodeGroup.cpp +++ b/src/NodeGroup.cpp @@ -1,19 +1,19 @@ #include "NodeGroup.hpp" #include "ConnectionIdUtils.hpp" -#include "NodeConnectionInteraction.hpp" +#include "GroupGraphicsObject.hpp" +#include "NodeGraphicsObject.hpp" #include #include +#include #include -using QtNodes::DataFlowGraphModel; using QtNodes::GroupGraphicsObject; -using QtNodes::NodeConnectionInteraction; using QtNodes::NodeGraphicsObject; using QtNodes::NodeGroup; using QtNodes::NodeId; -int NodeGroup::_groupCount = 0; +std::atomic NodeGroup::_groupCount{0}; NodeGroup::NodeGroup(std::vector nodes, GroupId groupId, @@ -21,19 +21,21 @@ NodeGroup::NodeGroup(std::vector nodes, QObject *parent) : QObject(parent) , _name(std::move(name)) - , _id(groupId) + , _groupId(groupId) , _childNodes(std::move(nodes)) , _groupGraphicsObject(nullptr) { - _groupCount++; + _groupCount.fetch_add(1, std::memory_order_relaxed); } +NodeGroup::~NodeGroup() = default; + QByteArray NodeGroup::saveToFile() const { QJsonObject groupJson; groupJson["name"] = _name; - groupJson["id"] = static_cast(_id); + groupJson["id"] = static_cast(_groupId); QJsonArray nodesJson; for (auto const &node : _childNodes) { @@ -42,9 +44,9 @@ QByteArray NodeGroup::saveToFile() const groupJson["nodes"] = nodesJson; QJsonArray connectionsJson; - auto groupConnections = _groupGraphicsObject->connections(); + auto const &groupConnections = _groupGraphicsObject->connections(); for (auto const &connection : groupConnections) { - connectionsJson.append(toJson(*connection)); + connectionsJson.append(toJson(connection)); } groupJson["connections"] = connectionsJson; @@ -55,7 +57,7 @@ QByteArray NodeGroup::saveToFile() const QtNodes::GroupId NodeGroup::id() const { - return _id; + return _groupId; } GroupGraphicsObject &NodeGroup::groupGraphicsObject() @@ -101,9 +103,9 @@ bool NodeGroup::empty() const return _childNodes.empty(); } -int NodeGroup::groupCount() +int NodeGroup::groupCount() noexcept { - return _groupCount; + return _groupCount.load(std::memory_order_relaxed); } void NodeGroup::addNode(NodeGraphicsObject *node) @@ -112,6 +114,9 @@ void NodeGroup::addNode(NodeGraphicsObject *node) if (_groupGraphicsObject && _groupGraphicsObject->locked()) { node->lock(true); } + if (_groupGraphicsObject) { + _groupGraphicsObject->updateGroupGeometry(); + } } void NodeGroup::removeNode(NodeGraphicsObject *node) @@ -121,6 +126,8 @@ void NodeGroup::removeNode(NodeGraphicsObject *node) if (nodeIt != _childNodes.end()) { (*nodeIt)->unsetNodeGroup(); _childNodes.erase(nodeIt); - groupGraphicsObject().positionLockedIcon(); + if (_groupGraphicsObject) { + _groupGraphicsObject->updateGroupGeometry(); + } } } diff --git a/src/NodeRenderingUtils.cpp b/src/NodeRenderingUtils.cpp new file mode 100644 index 000000000..fcf2c836a --- /dev/null +++ b/src/NodeRenderingUtils.cpp @@ -0,0 +1,57 @@ +#include "NodeRenderingUtils.hpp" + +#include "DataFlowGraphModel.hpp" +#include "NodeDelegateModel.hpp" +#include "StyleCollection.hpp" + +#include +#include +#include + +#include +#include + +namespace QtNodes::node_rendering { + +NodeStyle const &resolved_node_style( + AbstractGraphModel &model, + NodeId node_id, + std::optional &fallback_storage) +{ + if (auto *df_model = dynamic_cast(&model)) { + if (auto *delegate = df_model->delegateModel(node_id)) { + return delegate->nodeStyle(); + } + } + + QVariant const style_data = model.nodeData(node_id, NodeRole::Style); + if (!style_data.isValid() || style_data.isNull()) { + return StyleCollection::nodeStyle(); + } + + QJsonObject const style_json = QJsonDocument::fromVariant(style_data).object(); + if (style_json.isEmpty()) { + return StyleCollection::nodeStyle(); + } + + fallback_storage.emplace(style_json); + return *fallback_storage; +} + +QImage render_icon_image(QIcon const &icon, QSize const &logical_size, qreal dpr) +{ + QSize const physical_size(std::max(1, static_cast(std::ceil(logical_size.width() * dpr))), + std::max(1, static_cast(std::ceil(logical_size.height() * dpr)))); + + QImage image(physical_size, QImage::Format_ARGB32_Premultiplied); + image.setDevicePixelRatio(dpr); + image.fill(Qt::transparent); + + QPainter painter(&image); + icon.paint(&painter, QRect(QPoint(0, 0), logical_size)); + painter.end(); + + return image; +} + +} // namespace QtNodes::node_rendering diff --git a/src/NodeState.cpp b/src/NodeState.cpp index f6ed43e15..b614f1d50 100644 --- a/src/NodeState.cpp +++ b/src/NodeState.cpp @@ -19,7 +19,7 @@ void NodeState::setResizing(bool resizing) _resizing = resizing; } -bool NodeState::resizing() const +bool NodeState::resizing() const noexcept { return _resizing; } diff --git a/src/NodeStyle.cpp b/src/NodeStyle.cpp index 0cfacf51b..c9f33b7d8 100644 --- a/src/NodeStyle.cpp +++ b/src/NodeStyle.cpp @@ -2,13 +2,12 @@ #include "StyleCollection.hpp" -#include #include -#include #include using QtNodes::NodeStyle; +using namespace QtNodes::detail; inline void initResources() { @@ -22,12 +21,12 @@ NodeStyle::NodeStyle() initResources(); // Initialize status icons after resources are loaded - statusUpdated = QIcon(":/status_icons/updated.svg"); - statusProcessing = QIcon(":/status_icons/processing.svg"); - statusPending = QIcon(":/status_icons/pending.svg"); - statusInvalid = QIcon(":/status_icons/failed.svg"); - statusEmpty = QIcon(":/status_icons/empty.svg"); - statusPartial = QIcon(":/status_icons/partial.svg"); + _statusUpdated = QIcon(":/status_icons/updated.svg"); + _statusProcessing = QIcon(":/status_icons/processing.svg"); + _statusPending = QIcon(":/status_icons/pending.svg"); + _statusInvalid = QIcon(":/status_icons/failed.svg"); + _statusEmpty = QIcon(":/status_icons/empty.svg"); + _statusPartial = QIcon(":/status_icons/partial.svg"); // This configuration is stored inside the compiled unit and is loaded statically loadJsonFile(":DefaultStyle.json"); @@ -50,114 +49,56 @@ void NodeStyle::setNodeStyle(QString jsonText) StyleCollection::setNodeStyle(style); } -#ifdef STYLE_DEBUG -#define NODE_STYLE_CHECK_UNDEFINED_VALUE(v, variable) \ - { \ - if (v.type() == QJsonValue::Undefined || v.type() == QJsonValue::Null) \ - qWarning() << "Undefined value for parameter:" << #variable; \ - } -#else -#define NODE_STYLE_CHECK_UNDEFINED_VALUE(v, variable) -#endif - -#define NODE_STYLE_READ_COLOR(values, variable) \ - { \ - auto valueRef = values[#variable]; \ - NODE_STYLE_CHECK_UNDEFINED_VALUE(valueRef, variable) \ - if (valueRef.isArray()) { \ - auto colorArray = valueRef.toArray(); \ - std::vector rgb; \ - rgb.reserve(3); \ - for (auto it = colorArray.begin(); it != colorArray.end(); ++it) { \ - rgb.push_back((*it).toInt()); \ - } \ - variable = QColor(rgb[0], rgb[1], rgb[2]); \ - } else { \ - variable = QColor(valueRef.toString()); \ - } \ - } - -#define NODE_STYLE_WRITE_COLOR(values, variable) \ - { \ - values[#variable] = variable.name(); \ - } - -#define NODE_STYLE_READ_FLOAT(values, variable) \ - { \ - auto valueRef = values[#variable]; \ - NODE_STYLE_CHECK_UNDEFINED_VALUE(valueRef, variable) \ - variable = valueRef.toDouble(); \ - } - -#define NODE_STYLE_WRITE_FLOAT(values, variable) \ - { \ - values[#variable] = variable; \ - } - -#define NODE_STYLE_READ_BOOL(values, variable) \ - { \ - auto valueRef = values[#variable]; \ - NODE_STYLE_CHECK_UNDEFINED_VALUE(valueRef, variable) \ - variable = valueRef.toBool(); \ - } - -#define NODE_STYLE_WRITE_BOOL(values, variable) \ - { \ - values[#variable] = variable; \ - } - void NodeStyle::loadJson(QJsonObject const &json) { - QJsonValue nodeStyleValues = json["NodeStyle"]; - - QJsonObject obj = nodeStyleValues.toObject(); - - NODE_STYLE_READ_COLOR(obj, NormalBoundaryColor); - NODE_STYLE_READ_COLOR(obj, SelectedBoundaryColor); - NODE_STYLE_READ_COLOR(obj, GradientColor0); - NODE_STYLE_READ_COLOR(obj, GradientColor1); - NODE_STYLE_READ_COLOR(obj, GradientColor2); - NODE_STYLE_READ_COLOR(obj, GradientColor3); - NODE_STYLE_READ_COLOR(obj, ShadowColor); - NODE_STYLE_READ_BOOL(obj, ShadowEnabled); - NODE_STYLE_READ_COLOR(obj, FontColor); - NODE_STYLE_READ_COLOR(obj, FontColorFaded); - NODE_STYLE_READ_COLOR(obj, ConnectionPointColor); - NODE_STYLE_READ_COLOR(obj, FilledConnectionPointColor); - NODE_STYLE_READ_COLOR(obj, WarningColor); - NODE_STYLE_READ_COLOR(obj, ErrorColor); - - NODE_STYLE_READ_FLOAT(obj, PenWidth); - NODE_STYLE_READ_FLOAT(obj, HoveredPenWidth); - NODE_STYLE_READ_FLOAT(obj, ConnectionPointDiameter); - - NODE_STYLE_READ_FLOAT(obj, Opacity); + QJsonObject obj = json["NodeStyle"].toObject(); + + readColor(obj, "NormalBoundaryColor", _NormalBoundaryColor); + readColor(obj, "SelectedBoundaryColor", _SelectedBoundaryColor); + readColor(obj, "GradientColor0", _GradientColor0); + readColor(obj, "GradientColor1", _GradientColor1); + readColor(obj, "GradientColor2", _GradientColor2); + readColor(obj, "GradientColor3", _GradientColor3); + readColor(obj, "ShadowColor", _ShadowColor); + readBool(obj, "ShadowEnabled", _ShadowEnabled); + readColor(obj, "FontColor", _FontColor); + readColor(obj, "FontColorFaded", _FontColorFaded); + readColor(obj, "ConnectionPointColor", _ConnectionPointColor); + readColor(obj, "FilledConnectionPointColor", _FilledConnectionPointColor); + readColor(obj, "WarningColor", _WarningColor); + readColor(obj, "ErrorColor", _ErrorColor); + + readFloat(obj, "PenWidth", _PenWidth); + readFloat(obj, "HoveredPenWidth", _HoveredPenWidth); + readFloat(obj, "ConnectionPointDiameter", _ConnectionPointDiameter); + + readFloat(obj, "Opacity", _Opacity); } QJsonObject NodeStyle::toJson() const { QJsonObject obj; - NODE_STYLE_WRITE_COLOR(obj, NormalBoundaryColor); - NODE_STYLE_WRITE_COLOR(obj, SelectedBoundaryColor); - NODE_STYLE_WRITE_COLOR(obj, GradientColor0); - NODE_STYLE_WRITE_COLOR(obj, GradientColor1); - NODE_STYLE_WRITE_COLOR(obj, GradientColor2); - NODE_STYLE_WRITE_COLOR(obj, GradientColor3); - NODE_STYLE_WRITE_COLOR(obj, ShadowColor); - NODE_STYLE_WRITE_BOOL(obj, ShadowEnabled); - NODE_STYLE_WRITE_COLOR(obj, FontColor); - NODE_STYLE_WRITE_COLOR(obj, FontColorFaded); - NODE_STYLE_WRITE_COLOR(obj, ConnectionPointColor); - NODE_STYLE_WRITE_COLOR(obj, FilledConnectionPointColor); - NODE_STYLE_WRITE_COLOR(obj, WarningColor); - NODE_STYLE_WRITE_COLOR(obj, ErrorColor); - - NODE_STYLE_WRITE_FLOAT(obj, PenWidth); - NODE_STYLE_WRITE_FLOAT(obj, HoveredPenWidth); - NODE_STYLE_WRITE_FLOAT(obj, ConnectionPointDiameter); - - NODE_STYLE_WRITE_FLOAT(obj, Opacity); + writeColor(obj, "NormalBoundaryColor", _NormalBoundaryColor); + writeColor(obj, "SelectedBoundaryColor", _SelectedBoundaryColor); + writeColor(obj, "GradientColor0", _GradientColor0); + writeColor(obj, "GradientColor1", _GradientColor1); + writeColor(obj, "GradientColor2", _GradientColor2); + writeColor(obj, "GradientColor3", _GradientColor3); + writeColor(obj, "ShadowColor", _ShadowColor); + writeBool(obj, "ShadowEnabled", _ShadowEnabled); + writeColor(obj, "FontColor", _FontColor); + writeColor(obj, "FontColorFaded", _FontColorFaded); + writeColor(obj, "ConnectionPointColor", _ConnectionPointColor); + writeColor(obj, "FilledConnectionPointColor", _FilledConnectionPointColor); + writeColor(obj, "WarningColor", _WarningColor); + writeColor(obj, "ErrorColor", _ErrorColor); + + writeFloat(obj, "PenWidth", _PenWidth); + writeFloat(obj, "HoveredPenWidth", _HoveredPenWidth); + writeFloat(obj, "ConnectionPointDiameter", _ConnectionPointDiameter); + + writeFloat(obj, "Opacity", _Opacity); QJsonObject root; root["NodeStyle"] = obj; @@ -167,13 +108,48 @@ QJsonObject NodeStyle::toJson() const void NodeStyle::setBackgroundColor(QColor const &color) { - GradientColor0 = color; - GradientColor1 = color; - GradientColor2 = color; - GradientColor3 = color; + _GradientColor0 = color; + _GradientColor1 = color; + _GradientColor2 = color; + _GradientColor3 = color; } QColor NodeStyle::backgroundColor() const { - return GradientColor0; + return _GradientColor0; } + +QColor NodeStyle::normalBoundaryColor() const { return _NormalBoundaryColor; } +QColor NodeStyle::selectedBoundaryColor() const { return _SelectedBoundaryColor; } +QColor NodeStyle::gradientColor0() const { return _GradientColor0; } +QColor NodeStyle::gradientColor1() const { return _GradientColor1; } +QColor NodeStyle::gradientColor2() const { return _GradientColor2; } +QColor NodeStyle::gradientColor3() const { return _GradientColor3; } +QColor NodeStyle::shadowColor() const { return _ShadowColor; } +bool NodeStyle::shadowEnabled() const { return _ShadowEnabled; } +QColor NodeStyle::fontColor() const { return _FontColor; } +QColor NodeStyle::fontColorFaded() const { return _FontColorFaded; } +QColor NodeStyle::connectionPointColor() const { return _ConnectionPointColor; } +QColor NodeStyle::filledConnectionPointColor() const { return _FilledConnectionPointColor; } +QColor NodeStyle::warningColor() const { return _WarningColor; } +QColor NodeStyle::errorColor() const { return _ErrorColor; } +QColor NodeStyle::toolTipIconColor() const { return _ToolTipIconColor; } +float NodeStyle::penWidth() const { return _PenWidth; } +float NodeStyle::hoveredPenWidth() const { return _HoveredPenWidth; } +float NodeStyle::connectionPointDiameter() const { return _ConnectionPointDiameter; } +float NodeStyle::opacity() const { return _Opacity; } +QIcon const &NodeStyle::statusUpdated() const { return _statusUpdated; } +QIcon const &NodeStyle::statusProcessing() const { return _statusProcessing; } +QIcon const &NodeStyle::statusPending() const { return _statusPending; } +QIcon const &NodeStyle::statusInvalid() const { return _statusInvalid; } +QIcon const &NodeStyle::statusEmpty() const { return _statusEmpty; } +QIcon const &NodeStyle::statusPartial() const { return _statusPartial; } +ProcessingIconStyle const &NodeStyle::processingIconStyle() const { return _processingIconStyle; } + +void NodeStyle::setStatusUpdated(QIcon const &icon) { _statusUpdated = icon; } +void NodeStyle::setStatusProcessing(QIcon const &icon) { _statusProcessing = icon; } +void NodeStyle::setStatusPending(QIcon const &icon) { _statusPending = icon; } +void NodeStyle::setStatusInvalid(QIcon const &icon) { _statusInvalid = icon; } +void NodeStyle::setStatusEmpty(QIcon const &icon) { _statusEmpty = icon; } +void NodeStyle::setStatusPartial(QIcon const &icon) { _statusPartial = icon; } +void NodeStyle::setProcessingIconStyle(ProcessingIconStyle const &style) { _processingIconStyle = style; } diff --git a/src/SerializationValidation.cpp b/src/SerializationValidation.cpp new file mode 100644 index 000000000..e47eac407 --- /dev/null +++ b/src/SerializationValidation.cpp @@ -0,0 +1,149 @@ +#include "SerializationValidation.hpp" + +#include + +namespace QtNodes::detail { + +bool read_unsigned_number(QJsonValue const &value, quint64 maxValue, quint64 &result) +{ + if (!value.isDouble()) { + return false; + } + + double const parsed = value.toDouble(); + if (!std::isfinite(parsed) || parsed < 0.0 || parsed > static_cast(maxValue)) { + return false; + } + + quint64 const integral = static_cast(parsed); + if (parsed != static_cast(integral)) { + return false; + } + + result = integral; + return true; +} + +bool read_node_id(QJsonValue const &value, NodeId &nodeId) +{ + quint64 parsed = 0; + + if (!read_unsigned_number(value, InvalidNodeId - 1ull, parsed)) { + return false; + } + + nodeId = static_cast(parsed); + return true; +} + +bool read_group_id(QJsonValue const &value, GroupId &groupId) +{ + quint64 parsed = 0; + + if (!read_unsigned_number(value, InvalidGroupId - 1ull, parsed)) { + return false; + } + + groupId = static_cast(parsed); + return true; +} + +bool read_port_index(QJsonValue const &value, PortIndex &portIndex) +{ + quint64 parsed = 0; + + if (!read_unsigned_number(value, InvalidPortIndex - 1ull, parsed)) { + return false; + } + + portIndex = static_cast(parsed); + return true; +} + +bool read_finite_number(QJsonValue const &value, double &result) +{ + if (!value.isDouble()) { + return false; + } + + double const parsed = value.toDouble(); + if (!std::isfinite(parsed)) { + return false; + } + + result = parsed; + return true; +} + +bool read_required_object(QJsonObject const &obj, QString const &key, QJsonObject &result) +{ + auto const it = obj.find(key); + + if (it == obj.end() || !it->isObject()) { + return false; + } + + result = it->toObject(); + return true; +} + +bool read_required_array(QJsonObject const &obj, QString const &key, QJsonArray &result) +{ + auto const it = obj.find(key); + + if (it == obj.end() || !it->isArray()) { + return false; + } + + result = it->toArray(); + return true; +} + +bool read_required_string(QJsonObject const &obj, QString const &key, QString &result) +{ + auto const it = obj.find(key); + + if (it == obj.end() || !it->isString()) { + return false; + } + + result = it->toString(); + return true; +} + +bool read_optional_bool(QJsonObject const &obj, QString const &key, bool &result) +{ + auto const it = obj.find(key); + + if (it == obj.end()) { + return true; + } + + if (!it->isBool()) { + return false; + } + + result = it->toBool(); + return true; +} + +bool read_required_point(QJsonObject const &obj, QString const &key, QPointF &result) +{ + QJsonObject pointObject; + + if (!read_required_object(obj, key, pointObject)) { + return false; + } + + double x = 0.0; + double y = 0.0; + + if (!read_finite_number(pointObject["x"], x) || !read_finite_number(pointObject["y"], y)) { + return false; + } + + result = QPointF(x, y); + return true; +} + +} // namespace QtNodes::detail diff --git a/src/UndoCommands.cpp b/src/UndoCommands.cpp index 265d7e17c..150199a21 100644 --- a/src/UndoCommands.cpp +++ b/src/UndoCommands.cpp @@ -6,7 +6,9 @@ #include "Definitions.hpp" #include "GroupGraphicsObject.hpp" #include "NodeGraphicsObject.hpp" +#include "SerializationValidation.hpp" +#include #include #include #include @@ -17,6 +19,43 @@ namespace QtNodes { +namespace { + +NodeId json_value_to_node_id(QJsonValue const &value) +{ + NodeId nodeId = InvalidNodeId; + + if (!detail::read_node_id(value, nodeId)) { + throw std::logic_error("Invalid node id in serialized command payload"); + } + + return nodeId; +} + +ConnectionId connection_id_from_json(QJsonObject const &connJson) +{ + ConnectionId connId; + + if (!tryFromJson(connJson, connId)) { + throw std::logic_error("Invalid connection id in serialized command payload"); + } + + return connId; +} + +QPointF point_from_json(QJsonObject const &json, QString const &key) +{ + QPointF point; + + if (!detail::read_required_point(json, key, point)) { + throw std::logic_error("Invalid node position in serialized command payload"); + } + + return point; +} + +} // namespace + static QJsonObject serializeSelectedItems(BasicGraphicsScene *scene) { QJsonObject serializedScene; @@ -24,6 +63,7 @@ static QJsonObject serializeSelectedItems(BasicGraphicsScene *scene) auto &graphModel = scene->graphModel(); std::unordered_set selectedNodes; + std::unordered_set selectedConnections; QJsonArray nodesJsonArray; QJsonArray groupsJsonArray; @@ -39,14 +79,21 @@ static QJsonObject serializeSelectedItems(BasicGraphicsScene *scene) } }; + auto appendConnection = [&](ConnectionId const &connectionId) { + if (selectedNodes.count(connectionId.outNodeId) == 0 + || selectedNodes.count(connectionId.inNodeId) == 0) { + return; + } + + if (selectedConnections.insert(connectionId).second) { + connJsonArray.append(toJson(connectionId)); + } + }; + for (QGraphicsItem *item : scene->selectedItems()) { if (auto group = qgraphicsitem_cast(item)) { for (auto *node : group->group().childNodes()) { appendNode(node); - - for (auto const &connectionId : graphModel.allConnectionIds(node->nodeId())) { - connJsonArray.append(toJson(connectionId)); - } } } } @@ -54,10 +101,12 @@ static QJsonObject serializeSelectedItems(BasicGraphicsScene *scene) for (QGraphicsItem *item : scene->selectedItems()) { if (auto ngo = qgraphicsitem_cast(item)) { appendNode(ngo); + } + } - for (auto const &connectionId : graphModel.allConnectionIds(ngo->nodeId())) { - connJsonArray.append(toJson(connectionId)); - } + for (NodeId const nodeId : selectedNodes) { + for (auto const &connectionId : graphModel.allConnectionIds(nodeId)) { + appendConnection(connectionId); } } @@ -68,6 +117,7 @@ static QJsonObject serializeSelectedItems(BasicGraphicsScene *scene) QJsonObject groupJson; groupJson["id"] = static_cast(group.id()); groupJson["name"] = group.name(); + groupJson["locked"] = groupGo->locked(); QJsonArray nodeIdsJson; for (NodeGraphicsObject *node : group.childNodes()) { @@ -81,11 +131,7 @@ static QJsonObject serializeSelectedItems(BasicGraphicsScene *scene) for (QGraphicsItem *item : scene->selectedItems()) { if (auto c = qgraphicsitem_cast(item)) { - auto const &cid = c->connectionId(); - - if (selectedNodes.count(cid.outNodeId) > 0 && selectedNodes.count(cid.inNodeId) > 0) { - connJsonArray.append(toJson(cid)); - } + appendConnection(c->connectionId()); } } @@ -107,9 +153,11 @@ static void insertSerializedItems(QJsonObject const &json, BasicGraphicsScene *s graphModel.loadNode(obj); - auto id = obj["id"].toInt(); - scene->nodeGraphicsObject(id)->setZValue(1.0); - scene->nodeGraphicsObject(id)->setSelected(true); + auto id = json_value_to_node_id(obj["id"]); + if (auto *nodeObject = scene->nodeGraphicsObject(id)) { + nodeObject->setZValue(1.0); + nodeObject->setSelected(true); + } } QJsonArray const &connJsonArray = json["connections"].toArray(); @@ -117,12 +165,14 @@ static void insertSerializedItems(QJsonObject const &json, BasicGraphicsScene *s for (QJsonValue connection : connJsonArray) { QJsonObject connJson = connection.toObject(); - ConnectionId connId = fromJson(connJson); + ConnectionId connId = connection_id_from_json(connJson); // Restore the connection graphModel.addConnection(connId); - scene->connectionGraphicsObject(connId)->setSelected(true); + if (auto *connectionObject = scene->connectionGraphicsObject(connId)) { + connectionObject->setSelected(true); + } } if (json.contains("groups")) { @@ -131,19 +181,25 @@ static void insertSerializedItems(QJsonObject const &json, BasicGraphicsScene *s for (const QJsonValue &groupValue : groupsJsonArray) { QJsonObject groupJson = groupValue.toObject(); - QString name = QString("Group %1").arg(NodeGroup::groupCount()); + QString name = groupJson["name"].toString(); + if (name.isEmpty()) { + name = QString("Group %1").arg(NodeGroup::groupCount()); + } QJsonArray nodeIdsJson = groupJson["nodes"].toArray(); std::vector groupNodes; for (const QJsonValue &idVal : nodeIdsJson) { - NodeId nodeId = static_cast(idVal.toInt()); + NodeId nodeId = json_value_to_node_id(idVal); if (auto *ngo = scene->nodeGraphicsObject(nodeId)) { groupNodes.push_back(ngo); } } - scene->createGroup(groupNodes, name); + auto const groupWeak = scene->createGroup(groupNodes, name); + if (auto group = groupWeak.lock()) { + group->groupGraphicsObject().lock(groupJson["locked"].toBool(true)); + } } } } @@ -155,7 +211,7 @@ static void deleteSerializedItems(QJsonObject &sceneJson, AbstractGraphModel &gr for (QJsonValueRef connection : connectionJsonArray) { QJsonObject connJson = connection.toObject(); - ConnectionId connId = fromJson(connJson); + ConnectionId connId = connection_id_from_json(connJson); graphModel.deleteConnection(connId); } @@ -164,7 +220,7 @@ static void deleteSerializedItems(QJsonObject &sceneJson, AbstractGraphModel &gr for (QJsonValueRef node : nodesJsonArray) { QJsonObject nodeJson = node.toObject(); - graphModel.deleteNode(nodeJson["id"].toInt()); + graphModel.deleteNode(json_value_to_node_id(nodeJson["id"])); } } @@ -173,12 +229,13 @@ static QPointF computeAverageNodePosition(QJsonObject const &sceneJson) QPointF averagePos(0, 0); QJsonArray nodesJsonArray = sceneJson["nodes"].toArray(); + if (nodesJsonArray.isEmpty()) { + return averagePos; + } for (QJsonValueRef node : nodesJsonArray) { QJsonObject nodeJson = node.toObject(); - - averagePos += QPointF(nodeJson["position"].toObject()["x"].toDouble(), - nodeJson["position"].toObject()["y"].toDouble()); + averagePos += point_from_json(nodeJson, "position"); } averagePos /= static_cast(nodesJsonArray.size()); @@ -274,6 +331,7 @@ DeleteCommand::DeleteCommand(BasicGraphicsScene *scene) QJsonObject groupJson; groupJson["id"] = static_cast(groupData.id()); groupJson["name"] = groupData.name(); + groupJson["locked"] = groupGo->locked(); groupJson["nodes"] = groupNodeIdsJsonArray; groupsJsonArray.append(groupJson); } @@ -440,7 +498,7 @@ QJsonObject PasteCommand::makeNewNodeIdsInScene(QJsonObject const &sceneJson) for (QJsonValueRef node : nodesJsonArray) { QJsonObject nodeJson = node.toObject(); - NodeId oldNodeId = nodeJson["id"].toInt(); + NodeId oldNodeId = json_value_to_node_id(nodeJson["id"]); NodeId newNodeId = graphModel.newNodeId(); @@ -458,11 +516,17 @@ QJsonObject PasteCommand::makeNewNodeIdsInScene(QJsonObject const &sceneJson) for (QJsonValueRef connection : connectionJsonArray) { QJsonObject connJson = connection.toObject(); - ConnectionId connId = fromJson(connJson); + ConnectionId connId = connection_id_from_json(connJson); - ConnectionId newConnId{mapNodeIds[connId.outNodeId], + auto const outIt = mapNodeIds.find(connId.outNodeId); + auto const inIt = mapNodeIds.find(connId.inNodeId); + if (outIt == mapNodeIds.end() || inIt == mapNodeIds.end()) { + continue; + } + + ConnectionId newConnId{outIt->second, connId.outPortIndex, - mapNodeIds[connId.inNodeId], + inIt->second, connId.inPortIndex}; newConnJsonArray.append(toJson(newConnId)); @@ -483,9 +547,11 @@ QJsonObject PasteCommand::makeNewNodeIdsInScene(QJsonObject const &sceneJson) QJsonArray newNodeIdsJson; for (const QJsonValue &idVal : nodeIdsJson) { - NodeId oldId = static_cast(idVal.toInt()); - NodeId newId = mapNodeIds[oldId]; - newNodeIdsJson.append(static_cast(newId)); + NodeId oldId = json_value_to_node_id(idVal); + auto const nodeIt = mapNodeIds.find(oldId); + if (nodeIt != mapNodeIds.end()) { + newNodeIdsJson.append(static_cast(nodeIt->second)); + } } groupJson["nodes"] = newNodeIdsJson; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 6432f73b3..f502ac615 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -5,7 +5,6 @@ else() endif() add_executable(test_nodes - test_main.cpp src/TestAbstractGraphModel.cpp src/TestAbstractGraphModelSignals.cpp src/TestDataFlowGraphModel.cpp @@ -36,7 +35,7 @@ target_include_directories(test_nodes target_link_libraries(test_nodes PRIVATE QtNodes::QtNodes - Catch2::Catch2 + Catch2::Catch2WithMain Qt${QT_VERSION_MAJOR}::Test ) diff --git a/test/include/TestGraphModel.hpp b/test/include/TestGraphModel.hpp index ee8918aa4..55970ef2d 100644 --- a/test/include/TestGraphModel.hpp +++ b/test/include/TestGraphModel.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -9,6 +10,7 @@ #include #include +#include using QtNodes::AbstractGraphModel; using QtNodes::ConnectionId; @@ -31,38 +33,23 @@ class TestGraphModel : public AbstractGraphModel NodeId newNodeId() override { return _nextNodeId++; } - std::unordered_set allNodeIds() const override { return _nodeIds; } + NodeIdSet const &allNodeIds() const override { return _nodeIds; } - std::unordered_set allConnectionIds(NodeId const nodeId) const override + ConnectionIdSet const &allConnectionIds(NodeId const nodeId) const override { - std::unordered_set result; - for (const auto &conn : _connections) { - if (conn.inNodeId == nodeId || conn.outNodeId == nodeId) { - result.insert(conn); - } - } - return result; + return _connection_index.allConnectionIds(nodeId); } - std::unordered_set connections(NodeId nodeId, - PortType portType, - PortIndex portIndex) const override + ConnectionIdSet const &connections(NodeId nodeId, + PortType portType, + PortIndex portIndex) const override { - std::unordered_set result; - for (const auto &conn : _connections) { - if (portType == PortType::In && conn.inNodeId == nodeId && conn.inPortIndex == portIndex) { - result.insert(conn); - } else if (portType == PortType::Out && conn.outNodeId == nodeId - && conn.outPortIndex == portIndex) { - result.insert(conn); - } - } - return result; + return _connection_index.connections(nodeId, portType, portIndex); } bool connectionExists(ConnectionId const connectionId) const override { - return _connections.find(connectionId) != _connections.end(); + return _connection_index.contains(connectionId); } NodeId addNode(QString const nodeType = QString()) override @@ -88,7 +75,7 @@ class TestGraphModel : public AbstractGraphModel void addConnection(ConnectionId const connectionId) override { if (connectionPossible(connectionId)) { - _connections.insert(connectionId); + _connection_index.add(connectionId); Q_EMIT connectionCreated(connectionId); } } @@ -193,9 +180,7 @@ class TestGraphModel : public AbstractGraphModel bool deleteConnection(ConnectionId const connectionId) override { - auto it = _connections.find(connectionId); - if (it != _connections.end()) { - _connections.erase(it); + if (_connection_index.remove(connectionId)) { Q_EMIT connectionDeleted(connectionId); return true; } @@ -207,15 +192,14 @@ class TestGraphModel : public AbstractGraphModel if (!nodeExists(nodeId)) return false; - // Remove all connections involving this node std::vector connectionsToRemove; - for (const auto &conn : _connections) { - if (conn.inNodeId == nodeId || conn.outNodeId == nodeId) { - connectionsToRemove.push_back(conn); - } + auto const &attachedConnections = allConnectionIds(nodeId); + connectionsToRemove.reserve(attachedConnections.size()); + for (auto const &conn : attachedConnections) { + connectionsToRemove.push_back(conn); } - for (const auto &conn : connectionsToRemove) { + for (auto const &conn : connectionsToRemove) { deleteConnection(conn); } @@ -278,7 +262,7 @@ class TestGraphModel : public AbstractGraphModel private: NodeId _nextNodeId = 1; - std::unordered_set _nodeIds; - std::unordered_set _connections; + NodeIdSet _nodeIds; + QtNodes::ConnectionIdIndex _connection_index; std::unordered_map> _nodeData; }; diff --git a/test/src/TestAbstractGraphModel.cpp b/test/src/TestAbstractGraphModel.cpp index 0d2abb7e0..0f7674c4d 100644 --- a/test/src/TestAbstractGraphModel.cpp +++ b/test/src/TestAbstractGraphModel.cpp @@ -1,6 +1,6 @@ #include "TestGraphModel.hpp" -#include +#include #include #include diff --git a/test/src/TestAbstractGraphModelSignals.cpp b/test/src/TestAbstractGraphModelSignals.cpp index 9ec084362..be51051c7 100644 --- a/test/src/TestAbstractGraphModelSignals.cpp +++ b/test/src/TestAbstractGraphModelSignals.cpp @@ -4,7 +4,7 @@ #include #include -#include +#include #include #include diff --git a/test/src/TestBasicGraphicsScene.cpp b/test/src/TestBasicGraphicsScene.cpp index 99c548f8f..4ab00ae1e 100644 --- a/test/src/TestBasicGraphicsScene.cpp +++ b/test/src/TestBasicGraphicsScene.cpp @@ -2,10 +2,15 @@ #include "TestGraphModel.hpp" #include +#include +#include +#include -#include +#include #include +#include +#include #include using QtNodes::BasicGraphicsScene; @@ -13,6 +18,24 @@ using QtNodes::ConnectionId; using QtNodes::NodeId; using QtNodes::NodeRole; +namespace { + +QVariantMap shadow_enabled_style() +{ + return QtNodes::StyleCollection::nodeStyle().toJson().toVariantMap(); +} + +QVariantMap shadow_disabled_style() +{ + QVariantMap style = shadow_enabled_style(); + QVariantMap nodeStyle = style["NodeStyle"].toMap(); + nodeStyle["ShadowEnabled"] = false; + style["NodeStyle"] = nodeStyle; + return style; +} + +} // namespace + TEST_CASE("BasicGraphicsScene functionality", "[graphics]") { auto app = applicationSetup(); @@ -90,6 +113,17 @@ TEST_CASE("BasicGraphicsScene functionality", "[graphics]") // Don't call view.show() to avoid potential graphics system issues } + + SECTION("Nodes without explicit style fall back to collection defaults") + { + NodeId const nodeId = model.addNode("TestNode"); + QCoreApplication::processEvents(); + + auto *nodeGraphics = scene.nodeGraphicsObject(nodeId); + REQUIRE(nodeGraphics != nullptr); + + CHECK(nodeGraphics->opacity() == Approx(QtNodes::StyleCollection::nodeStyle().opacity())); + } } TEST_CASE("BasicGraphicsScene undo/redo support", "[graphics]") @@ -119,3 +153,48 @@ TEST_CASE("BasicGraphicsScene undo/redo support", "[graphics]") CHECK(undoStack.count() >= 0); } } + +TEST_CASE("Node shadow bounds follow visual margins", "[graphics]") +{ + auto app = applicationSetup(); + TestGraphModel model; + BasicGraphicsScene scene(model); + + SECTION("Shadow-enabled bounds include the full painter shadow") + { + NodeId const nodeId = model.addNode("TestNode"); + model.setNodeData(nodeId, NodeRole::Style, shadow_enabled_style()); + QCoreApplication::processEvents(); + + auto *nodeGraphics = scene.nodeGraphicsObject(nodeId); + REQUIRE(nodeGraphics != nullptr); + + QRectF const bounds = nodeGraphics->boundingRect(); + QSize const size = scene.nodeGeometry().size(nodeId); + QMarginsF const margins = QtNodes::node_rendering::node_visual_margins(true); + + CHECK(bounds.left() == Approx(-margins.left())); + CHECK(bounds.top() == Approx(-margins.top())); + CHECK(bounds.right() == Approx(size.width() + margins.right())); + CHECK(bounds.bottom() == Approx(size.height() + margins.bottom())); + } + + SECTION("Shadow-disabled bounds fall back to port margins only") + { + NodeId const nodeId = model.addNode("TestNode"); + model.setNodeData(nodeId, NodeRole::Style, shadow_disabled_style()); + QCoreApplication::processEvents(); + + auto *nodeGraphics = scene.nodeGraphicsObject(nodeId); + REQUIRE(nodeGraphics != nullptr); + + QRectF const bounds = nodeGraphics->boundingRect(); + QSize const size = scene.nodeGeometry().size(nodeId); + QMarginsF const margins = QtNodes::node_rendering::node_visual_margins(false); + + CHECK(bounds.left() == Approx(-margins.left())); + CHECK(bounds.top() == Approx(-margins.top())); + CHECK(bounds.right() == Approx(size.width() + margins.right())); + CHECK(bounds.bottom() == Approx(size.height() + margins.bottom())); + } +} diff --git a/test/src/TestConnectionId.cpp b/test/src/TestConnectionId.cpp index 95f9a29cf..de0e80969 100644 --- a/test/src/TestConnectionId.cpp +++ b/test/src/TestConnectionId.cpp @@ -1,7 +1,7 @@ #include #include -#include +#include using QtNodes::ConnectionId; using QtNodes::NodeId; diff --git a/test/src/TestCopyPaste.cpp b/test/src/TestCopyPaste.cpp index 808b4cc14..3e4da465e 100644 --- a/test/src/TestCopyPaste.cpp +++ b/test/src/TestCopyPaste.cpp @@ -1,7 +1,7 @@ #include "ApplicationSetup.hpp" #include "TestGraphModel.hpp" -#include +#include #include #include diff --git a/test/src/TestCustomPainters.cpp b/test/src/TestCustomPainters.cpp index 26d0402b3..f38045b7d 100644 --- a/test/src/TestCustomPainters.cpp +++ b/test/src/TestCustomPainters.cpp @@ -2,7 +2,7 @@ #include "TestGraphModel.hpp" #include "UITestHelper.hpp" -#include +#include #include #include diff --git a/test/src/TestDataFlow.cpp b/test/src/TestDataFlow.cpp index 95ec307b8..a115dc0cf 100644 --- a/test/src/TestDataFlow.cpp +++ b/test/src/TestDataFlow.cpp @@ -9,7 +9,7 @@ #include #include -#include +#include #include #include diff --git a/test/src/TestDataFlowGraphModel.cpp b/test/src/TestDataFlowGraphModel.cpp index db0f78913..ca09bedd5 100644 --- a/test/src/TestDataFlowGraphModel.cpp +++ b/test/src/TestDataFlowGraphModel.cpp @@ -5,7 +5,7 @@ #include #include -#include +#include using QtNodes::ConnectionId; using QtNodes::DataFlowGraphModel; diff --git a/test/src/TestLoopDetection.cpp b/test/src/TestLoopDetection.cpp index 7e5255034..460fb4807 100644 --- a/test/src/TestLoopDetection.cpp +++ b/test/src/TestLoopDetection.cpp @@ -2,7 +2,7 @@ #include "TestGraphModel.hpp" #include "TestDataFlowNodes.hpp" -#include +#include #include #include diff --git a/test/src/TestNodeDelegateModelRegistry.cpp b/test/src/TestNodeDelegateModelRegistry.cpp index 4efb91b35..04c9e0d5e 100644 --- a/test/src/TestNodeDelegateModelRegistry.cpp +++ b/test/src/TestNodeDelegateModelRegistry.cpp @@ -1,7 +1,7 @@ #include #include -#include +#include using QtNodes::NodeDelegateModel; using QtNodes::NodeDelegateModelRegistry; diff --git a/test/src/TestNodeGroup.cpp b/test/src/TestNodeGroup.cpp index 6599656eb..0a416a4c2 100644 --- a/test/src/TestNodeGroup.cpp +++ b/test/src/TestNodeGroup.cpp @@ -8,7 +8,7 @@ #include #include -#include +#include #include #include @@ -82,6 +82,17 @@ std::set toNodeIdSet(std::vector const &ids) return {ids.begin(), ids.end()}; } +QRectF groupSceneRect(NodeGroup const &group) +{ + auto const &groupGraphics = group.groupGraphicsObject(); + return groupGraphics.mapRectToScene(groupGraphics.rect()).boundingRect(); +} + +QRectF nodeSceneRect(NodeGraphicsObject const &node) +{ + return node.mapRectToScene(node.boundingRect()).boundingRect(); +} + } // namespace TEST_CASE("Node group creation", "[node-group]") @@ -200,6 +211,52 @@ TEST_CASE("Adding and removing nodes from a group", "[node-group]") CHECK(nodeGroup->id() == group->id()); } + SECTION("Group bounds follow added removed and moved nodes") + { + NodeId firstNodeId = createNode(model, scene); + NodeId secondNodeId = createNode(model, scene); + + model.setNodeData(firstNodeId, NodeRole::Position, QPointF(0.0, 0.0)); + model.setNodeData(secondNodeId, NodeRole::Position, QPointF(600.0, 0.0)); + QCoreApplication::processEvents(); + + auto *firstNode = scene.nodeGraphicsObject(firstNodeId); + auto *secondNode = scene.nodeGraphicsObject(secondNodeId); + REQUIRE(firstNode != nullptr); + REQUIRE(secondNode != nullptr); + + std::vector nodes{firstNode}; + auto group = scene.createGroup(nodes, QStringLiteral("BoundsGroup")).lock(); + REQUIRE(group); + + QRectF initialGroupRect = groupSceneRect(*group); + CHECK(initialGroupRect.contains(nodeSceneRect(*firstNode))); + CHECK_FALSE(initialGroupRect.contains(nodeSceneRect(*secondNode))); + + scene.addNodeToGroup(secondNodeId, group->id()); + QCoreApplication::processEvents(); + + QRectF expandedGroupRect = groupSceneRect(*group); + CHECK(expandedGroupRect.contains(nodeSceneRect(*firstNode))); + CHECK(expandedGroupRect.contains(nodeSceneRect(*secondNode))); + CHECK(expandedGroupRect.width() > initialGroupRect.width()); + + model.setNodeData(secondNodeId, NodeRole::Position, QPointF(900.0, 120.0)); + QCoreApplication::processEvents(); + + QRectF movedGroupRect = groupSceneRect(*group); + CHECK(movedGroupRect.contains(nodeSceneRect(*secondNode))); + CHECK(movedGroupRect.width() >= expandedGroupRect.width()); + + scene.removeNodeFromGroup(secondNodeId); + QCoreApplication::processEvents(); + + QRectF shrunkGroupRect = groupSceneRect(*group); + CHECK(shrunkGroupRect.contains(nodeSceneRect(*firstNode))); + CHECK_FALSE(shrunkGroupRect.contains(nodeSceneRect(*secondNode))); + CHECK(shrunkGroupRect.width() < movedGroupRect.width()); + } + SECTION("Removing nodes from a group and clearing empty groups") { std::vector nodes; diff --git a/test/src/TestNodeValidation.cpp b/test/src/TestNodeValidation.cpp index b7b3b96e7..3db34bc48 100644 --- a/test/src/TestNodeValidation.cpp +++ b/test/src/TestNodeValidation.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include @@ -43,10 +43,7 @@ class TestValidatedModel : public NodeDelegateModel // Expose validation methods for testing void setTestValidationState(NodeValidationState::State state, QString const &message) { - NodeValidationState vs; - vs._state = state; - vs._stateMessage = message; - setValidationState(vs); + setValidationState(NodeValidationState(state, message)); } void setTestProcessingStatus(NodeProcessingStatus status) { setNodeProcessingStatus(status); } @@ -64,9 +61,7 @@ TEST_CASE("NodeValidationState basic functionality", "[validation]") SECTION("Validation state can be set to Warning") { - NodeValidationState state; - state._state = NodeValidationState::State::Warning; - state._stateMessage = "Test warning"; + NodeValidationState state(NodeValidationState::State::Warning, "Test warning"); CHECK_FALSE(state.isValid()); CHECK(state.state() == NodeValidationState::State::Warning); @@ -75,9 +70,7 @@ TEST_CASE("NodeValidationState basic functionality", "[validation]") SECTION("Validation state can be set to Error") { - NodeValidationState state; - state._state = NodeValidationState::State::Error; - state._stateMessage = "Test error"; + NodeValidationState state(NodeValidationState::State::Error, "Test error"); CHECK_FALSE(state.isValid()); CHECK(state.state() == NodeValidationState::State::Error); diff --git a/test/src/TestSerialization.cpp b/test/src/TestSerialization.cpp index 5b856154e..d0d350945 100644 --- a/test/src/TestSerialization.cpp +++ b/test/src/TestSerialization.cpp @@ -5,7 +5,7 @@ #include #include -#include +#include #include #include diff --git a/test/src/TestUIInteraction.cpp b/test/src/TestUIInteraction.cpp index cca0faeb7..1d9d4d6f6 100644 --- a/test/src/TestUIInteraction.cpp +++ b/test/src/TestUIInteraction.cpp @@ -8,7 +8,7 @@ #include #include -#include +#include #include #include #include diff --git a/test/src/TestUndoCommands.cpp b/test/src/TestUndoCommands.cpp index 0ed033c08..63742a41c 100644 --- a/test/src/TestUndoCommands.cpp +++ b/test/src/TestUndoCommands.cpp @@ -4,7 +4,7 @@ #include #include -#include +#include #include diff --git a/test/src/TestZoomFeatures.cpp b/test/src/TestZoomFeatures.cpp index 90fd2e3aa..01f0ae3a9 100644 --- a/test/src/TestZoomFeatures.cpp +++ b/test/src/TestZoomFeatures.cpp @@ -1,7 +1,7 @@ #include "ApplicationSetup.hpp" #include "TestGraphModel.hpp" -#include +#include #include #include @@ -9,6 +9,10 @@ #include #include +#include +#include + +#include using QtNodes::BasicGraphicsScene; using QtNodes::GraphicsView; @@ -69,6 +73,147 @@ TEST_CASE("GraphicsView scale range", "[zoom]") view.setupScale(10.0); CHECK(view.getScale() <= 4.0); } + + SECTION("Smooth wheel zoom keeps the cursor anchor stable") + { + view.setupScale(1.0); + + QPointF const pivot(321.25, 247.75); + bool invertible = false; + QTransform const invertedTransform = view.viewportTransform().inverted(&invertible); + REQUIRE(invertible); + + QPointF const trackedScenePoint = invertedTransform.map(pivot); + + QWheelEvent wheelEvent(pivot, + view.mapToGlobal(pivot.toPoint()), + QPoint(0, 0), + QPoint(0, 120), + Qt::NoButton, + Qt::NoModifier, + Qt::ScrollPhase::NoScrollPhase, + false); + QApplication::sendEvent(view.viewport(), &wheelEvent); + QTest::qWait(20); + + QPointF const mappedPivot = view.viewportTransform().map(trackedScenePoint); + CHECK(mappedPivot.x() == Approx(pivot.x()).margin(1.0)); + CHECK(mappedPivot.y() == Approx(pivot.y()).margin(1.0)); + } + + SECTION("Smooth wheel zoom settles back to a clean transform") + { + QWheelEvent wheelEvent(QPointF(320.0, 240.0), + view.mapToGlobal(QPoint(320, 240)), + QPoint(0, 0), + QPoint(0, 120), + Qt::NoButton, + Qt::NoModifier, + Qt::ScrollPhase::NoScrollPhase, + false); + QApplication::sendEvent(view.viewport(), &wheelEvent); + QTest::qWait(500); + + CHECK(view.transform().dx() == Approx(0.0).margin(0.01)); + CHECK(view.transform().dy() == Approx(0.0).margin(0.01)); + } + + SECTION("Smooth wheel zoom math is invariant to timer cadence") + { + struct Zoom_state + { + double scale = 1.0; + double velocity = 0.0; + }; + + auto advanceZoom = [](double initialVelocity, std::vector const &elapsedSteps) { + Zoom_state state{1.0, initialVelocity}; + for (double const elapsedStep : elapsedSteps) { + state.scale *= GraphicsView::zoomAnimationScaleFactor(state.velocity, elapsedStep); + state.velocity = GraphicsView::zoomAnimationVelocityAfter(state.velocity, elapsedStep); + } + return state; + }; + + Zoom_state const singleGap = advanceZoom(4.0, {10.0}); + Zoom_state const splitGap = advanceZoom(4.0, {4.0, 6.0}); + Zoom_state const fineSteps = advanceZoom(4.0, {1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0}); + + CHECK(singleGap.scale == Approx(splitGap.scale).epsilon(1e-10)); + CHECK(singleGap.velocity == Approx(splitGap.velocity).epsilon(1e-10)); + CHECK(singleGap.scale == Approx(fineSteps.scale).epsilon(1e-10)); + CHECK(singleGap.velocity == Approx(fineSteps.velocity).epsilon(1e-10)); + } +} + +TEST_CASE("GraphicsView node cache policy", "[zoom]") +{ + auto app = applicationSetup(); + + auto model = std::make_shared(); + BasicGraphicsScene scene(*model); + GraphicsView view(&scene); + + view.resize(800, 600); + view.show(); + REQUIRE(QTest::qWaitForWindowExposed(&view)); + + NodeId const nodeId = model->addNode("Node1"); + QCoreApplication::processEvents(); + + auto *nodeGraphics = scene.nodeGraphicsObject(nodeId); + REQUIRE(nodeGraphics != nullptr); + + SECTION("Rasterization policy toggles node cache mode") + { + view.setRasterizationPolicy(GraphicsView::RasterizationPolicy::Crisp); + QCoreApplication::processEvents(); + CHECK(nodeGraphics->cacheMode() == QGraphicsItem::DeviceCoordinateCache); + + view.setRasterizationPolicy(GraphicsView::RasterizationPolicy::Consistent); + QCoreApplication::processEvents(); + CHECK(nodeGraphics->cacheMode() == QGraphicsItem::NoCache); + } + + SECTION("Smooth zoom disables node cache while animating") + { + view.setRasterizationPolicy(GraphicsView::RasterizationPolicy::Crisp); + QCoreApplication::processEvents(); + REQUIRE(nodeGraphics->cacheMode() == QGraphicsItem::DeviceCoordinateCache); + + QWheelEvent wheelEvent(QPointF(320.0, 240.0), + view.mapToGlobal(QPoint(320, 240)), + QPoint(0, 0), + QPoint(0, 120), + Qt::NoButton, + Qt::NoModifier, + Qt::ScrollPhase::NoScrollPhase, + false); + QApplication::sendEvent(view.viewport(), &wheelEvent); + QCoreApplication::processEvents(); + + CHECK(view.isZoomAnimating()); + CHECK(nodeGraphics->cacheMode() == QGraphicsItem::NoCache); + + QTest::qWait(500); + CHECK_FALSE(view.isZoomAnimating()); + CHECK(nodeGraphics->cacheMode() == QGraphicsItem::DeviceCoordinateCache); + } + + SECTION("Shared scenes keep node cache disabled") + { + GraphicsView secondView(&scene); + secondView.resize(800, 600); + secondView.show(); + REQUIRE(QTest::qWaitForWindowExposed(&secondView)); + + view.setRasterizationPolicy(GraphicsView::RasterizationPolicy::Crisp); + secondView.setRasterizationPolicy(GraphicsView::RasterizationPolicy::Crisp); + QCoreApplication::processEvents(); + + CHECK(nodeGraphics->cacheMode() == QGraphicsItem::NoCache); + } } TEST_CASE("scaleChanged signal", "[zoom]") diff --git a/test/test_main.cpp b/test/test_main.cpp deleted file mode 100644 index 4ed06df1f..000000000 --- a/test/test_main.cpp +++ /dev/null @@ -1,2 +0,0 @@ -#define CATCH_CONFIG_MAIN -#include