From 41255f2cec7f9ee4c3dea77c23f6a2fec749e887 Mon Sep 17 00:00:00 2001 From: Anthony Rabine Date: Sun, 28 May 2023 14:50:48 +0200 Subject: [PATCH] delete submodule --- .gitmodules | 5 +- story-editor/nodeeditor | 1 - story-editor/nodeeditor/CMakeLists.txt | 274 +++++++++ story-editor/nodeeditor/LICENSE.rst | 28 + story-editor/nodeeditor/README.rst | 261 +++++++++ .../include/QtNodes/AbstractGraphModel | 1 + .../include/QtNodes/AbstractNodePainter | 1 + .../include/QtNodes/BasicGraphicsScene | 1 + .../include/QtNodes/ConnectionIdUtils | 1 + .../include/QtNodes/ConnectionStyle | 1 + .../include/QtNodes/DataFlowGraphModel | 1 + .../include/QtNodes/DataFlowGraphicsScene | 1 + .../include/QtNodes/DefaultNodePainter | 1 + .../nodeeditor/include/QtNodes/Definitions | 1 + .../nodeeditor/include/QtNodes/GraphicsView | 1 + .../include/QtNodes/GraphicsViewStyle | 1 + .../nodeeditor/include/QtNodes/NodeData | 1 + .../include/QtNodes/NodeDelegateModel | 1 + .../include/QtNodes/NodeDelegateModelRegistry | 1 + .../nodeeditor/include/QtNodes/NodeGeometry | 2 + .../nodeeditor/include/QtNodes/NodeState | 1 + .../nodeeditor/include/QtNodes/NodeStyle | 1 + .../include/QtNodes/StyleCollection | 1 + .../nodeeditor/include/QtNodes/UndoCommands | 1 + .../QtNodes/internal/AbstractGraphModel.hpp | 249 ++++++++ .../QtNodes/internal/AbstractNodeGeometry.hpp | 82 +++ .../QtNodes/internal/AbstractNodePainter.hpp | 29 + .../QtNodes/internal/BasicGraphicsScene.hpp | 184 ++++++ .../include/QtNodes/internal/Compiler.hpp | 40 ++ .../internal/ConnectionGraphicsObject.hpp | 96 ++++ .../QtNodes/internal/ConnectionIdHash.hpp | 55 ++ .../QtNodes/internal/ConnectionIdUtils.hpp | 151 +++++ .../QtNodes/internal/ConnectionPainter.hpp | 21 + .../QtNodes/internal/ConnectionState.hpp | 60 ++ .../QtNodes/internal/ConnectionStyle.hpp | 54 ++ .../QtNodes/internal/DataFlowGraphModel.hpp | 135 +++++ .../internal/DataFlowGraphicsScene.hpp | 40 ++ .../DefaultHorizontalNodeGeometry.hpp | 58 ++ .../QtNodes/internal/DefaultNodePainter.hpp | 34 ++ .../internal/DefaultVerticalNodeGeometry.hpp | 60 ++ .../include/QtNodes/internal/Definitions.hpp | 125 ++++ .../include/QtNodes/internal/Export.hpp | 48 ++ .../include/QtNodes/internal/GraphicsView.hpp | 97 ++++ .../QtNodes/internal/GraphicsViewStyle.hpp | 32 ++ .../internal/NodeConnectionInteraction.hpp | 68 +++ .../include/QtNodes/internal/NodeData.hpp | 43 ++ .../QtNodes/internal/NodeDelegateModel.hpp | 133 +++++ .../internal/NodeDelegateModelRegistry.hpp | 171 ++++++ .../QtNodes/internal/NodeGraphicsObject.hpp | 91 +++ .../include/QtNodes/internal/NodeState.hpp | 52 ++ .../include/QtNodes/internal/NodeStyle.hpp | 53 ++ .../QtNodes/internal/OperatingSystem.hpp | 49 ++ .../QtNodes/internal/QStringStdHash.hpp | 22 + .../include/QtNodes/internal/QUuidStdHash.hpp | 14 + .../include/QtNodes/internal/Serializable.hpp | 16 + .../include/QtNodes/internal/Style.hpp | 48 ++ .../QtNodes/internal/StyleCollection.hpp | 43 ++ .../include/QtNodes/internal/UndoCommands.hpp | 123 ++++ .../include/QtNodes/internal/locateNode.hpp | 16 + .../nodeeditor/resources/DefaultStyle.json | 42 ++ .../nodeeditor/resources/resources.qrc | 5 + .../nodeeditor/src/AbstractGraphModel.cpp | 105 ++++ .../nodeeditor/src/AbstractNodeGeometry.cpp | 81 +++ .../nodeeditor/src/BasicGraphicsScene.cpp | 308 ++++++++++ .../src/ConnectionGraphicsObject.cpp | 380 +++++++++++++ .../nodeeditor/src/ConnectionPainter.cpp | 254 +++++++++ .../nodeeditor/src/ConnectionState.cpp | 66 +++ .../nodeeditor/src/ConnectionStyle.cpp | 205 +++++++ .../nodeeditor/src/DataFlowGraphModel.cpp | 536 ++++++++++++++++++ .../nodeeditor/src/DataFlowGraphicsScene.cpp | 189 ++++++ .../src/DefaultHorizontalNodeGeometry.cpp | 239 ++++++++ .../nodeeditor/src/DefaultNodePainter.cpp | 284 ++++++++++ .../src/DefaultVerticalNodeGeometry.cpp | 300 ++++++++++ story-editor/nodeeditor/src/Definitions.cpp | 1 + story-editor/nodeeditor/src/GraphicsView.cpp | 405 +++++++++++++ .../nodeeditor/src/GraphicsViewStyle.cpp | 94 +++ .../src/NodeConnectionInteraction.cpp | 152 +++++ .../nodeeditor/src/NodeDelegateModel.cpp | 54 ++ .../src/NodeDelegateModelRegistry.cpp | 36 ++ .../nodeeditor/src/NodeGraphicsObject.cpp | 364 ++++++++++++ story-editor/nodeeditor/src/NodeState.cpp | 42 ++ story-editor/nodeeditor/src/NodeStyle.cpp | 146 +++++ .../nodeeditor/src/StyleCollection.cpp | 43 ++ story-editor/nodeeditor/src/UndoCommands.cpp | 464 +++++++++++++++ story-editor/nodeeditor/src/locateNode.cpp | 42 ++ 85 files changed, 8013 insertions(+), 5 deletions(-) delete mode 160000 story-editor/nodeeditor create mode 100644 story-editor/nodeeditor/CMakeLists.txt create mode 100644 story-editor/nodeeditor/LICENSE.rst create mode 100644 story-editor/nodeeditor/README.rst create mode 100644 story-editor/nodeeditor/include/QtNodes/AbstractGraphModel create mode 100644 story-editor/nodeeditor/include/QtNodes/AbstractNodePainter create mode 100644 story-editor/nodeeditor/include/QtNodes/BasicGraphicsScene create mode 100644 story-editor/nodeeditor/include/QtNodes/ConnectionIdUtils create mode 100644 story-editor/nodeeditor/include/QtNodes/ConnectionStyle create mode 100644 story-editor/nodeeditor/include/QtNodes/DataFlowGraphModel create mode 100644 story-editor/nodeeditor/include/QtNodes/DataFlowGraphicsScene create mode 100644 story-editor/nodeeditor/include/QtNodes/DefaultNodePainter create mode 100644 story-editor/nodeeditor/include/QtNodes/Definitions create mode 100644 story-editor/nodeeditor/include/QtNodes/GraphicsView create mode 100644 story-editor/nodeeditor/include/QtNodes/GraphicsViewStyle create mode 100644 story-editor/nodeeditor/include/QtNodes/NodeData create mode 100644 story-editor/nodeeditor/include/QtNodes/NodeDelegateModel create mode 100644 story-editor/nodeeditor/include/QtNodes/NodeDelegateModelRegistry create mode 100644 story-editor/nodeeditor/include/QtNodes/NodeGeometry create mode 100644 story-editor/nodeeditor/include/QtNodes/NodeState create mode 100644 story-editor/nodeeditor/include/QtNodes/NodeStyle create mode 100644 story-editor/nodeeditor/include/QtNodes/StyleCollection create mode 100644 story-editor/nodeeditor/include/QtNodes/UndoCommands create mode 100644 story-editor/nodeeditor/include/QtNodes/internal/AbstractGraphModel.hpp create mode 100644 story-editor/nodeeditor/include/QtNodes/internal/AbstractNodeGeometry.hpp create mode 100644 story-editor/nodeeditor/include/QtNodes/internal/AbstractNodePainter.hpp create mode 100644 story-editor/nodeeditor/include/QtNodes/internal/BasicGraphicsScene.hpp create mode 100644 story-editor/nodeeditor/include/QtNodes/internal/Compiler.hpp create mode 100644 story-editor/nodeeditor/include/QtNodes/internal/ConnectionGraphicsObject.hpp create mode 100644 story-editor/nodeeditor/include/QtNodes/internal/ConnectionIdHash.hpp create mode 100644 story-editor/nodeeditor/include/QtNodes/internal/ConnectionIdUtils.hpp create mode 100644 story-editor/nodeeditor/include/QtNodes/internal/ConnectionPainter.hpp create mode 100644 story-editor/nodeeditor/include/QtNodes/internal/ConnectionState.hpp create mode 100644 story-editor/nodeeditor/include/QtNodes/internal/ConnectionStyle.hpp create mode 100644 story-editor/nodeeditor/include/QtNodes/internal/DataFlowGraphModel.hpp create mode 100644 story-editor/nodeeditor/include/QtNodes/internal/DataFlowGraphicsScene.hpp create mode 100644 story-editor/nodeeditor/include/QtNodes/internal/DefaultHorizontalNodeGeometry.hpp create mode 100644 story-editor/nodeeditor/include/QtNodes/internal/DefaultNodePainter.hpp create mode 100644 story-editor/nodeeditor/include/QtNodes/internal/DefaultVerticalNodeGeometry.hpp create mode 100644 story-editor/nodeeditor/include/QtNodes/internal/Definitions.hpp create mode 100644 story-editor/nodeeditor/include/QtNodes/internal/Export.hpp create mode 100644 story-editor/nodeeditor/include/QtNodes/internal/GraphicsView.hpp create mode 100644 story-editor/nodeeditor/include/QtNodes/internal/GraphicsViewStyle.hpp create mode 100644 story-editor/nodeeditor/include/QtNodes/internal/NodeConnectionInteraction.hpp create mode 100644 story-editor/nodeeditor/include/QtNodes/internal/NodeData.hpp create mode 100644 story-editor/nodeeditor/include/QtNodes/internal/NodeDelegateModel.hpp create mode 100644 story-editor/nodeeditor/include/QtNodes/internal/NodeDelegateModelRegistry.hpp create mode 100644 story-editor/nodeeditor/include/QtNodes/internal/NodeGraphicsObject.hpp create mode 100644 story-editor/nodeeditor/include/QtNodes/internal/NodeState.hpp create mode 100644 story-editor/nodeeditor/include/QtNodes/internal/NodeStyle.hpp create mode 100644 story-editor/nodeeditor/include/QtNodes/internal/OperatingSystem.hpp create mode 100644 story-editor/nodeeditor/include/QtNodes/internal/QStringStdHash.hpp create mode 100644 story-editor/nodeeditor/include/QtNodes/internal/QUuidStdHash.hpp create mode 100644 story-editor/nodeeditor/include/QtNodes/internal/Serializable.hpp create mode 100644 story-editor/nodeeditor/include/QtNodes/internal/Style.hpp create mode 100644 story-editor/nodeeditor/include/QtNodes/internal/StyleCollection.hpp create mode 100644 story-editor/nodeeditor/include/QtNodes/internal/UndoCommands.hpp create mode 100644 story-editor/nodeeditor/include/QtNodes/internal/locateNode.hpp create mode 100644 story-editor/nodeeditor/resources/DefaultStyle.json create mode 100644 story-editor/nodeeditor/resources/resources.qrc create mode 100644 story-editor/nodeeditor/src/AbstractGraphModel.cpp create mode 100644 story-editor/nodeeditor/src/AbstractNodeGeometry.cpp create mode 100644 story-editor/nodeeditor/src/BasicGraphicsScene.cpp create mode 100644 story-editor/nodeeditor/src/ConnectionGraphicsObject.cpp create mode 100644 story-editor/nodeeditor/src/ConnectionPainter.cpp create mode 100644 story-editor/nodeeditor/src/ConnectionState.cpp create mode 100644 story-editor/nodeeditor/src/ConnectionStyle.cpp create mode 100644 story-editor/nodeeditor/src/DataFlowGraphModel.cpp create mode 100644 story-editor/nodeeditor/src/DataFlowGraphicsScene.cpp create mode 100644 story-editor/nodeeditor/src/DefaultHorizontalNodeGeometry.cpp create mode 100644 story-editor/nodeeditor/src/DefaultNodePainter.cpp create mode 100644 story-editor/nodeeditor/src/DefaultVerticalNodeGeometry.cpp create mode 100644 story-editor/nodeeditor/src/Definitions.cpp create mode 100644 story-editor/nodeeditor/src/GraphicsView.cpp create mode 100644 story-editor/nodeeditor/src/GraphicsViewStyle.cpp create mode 100644 story-editor/nodeeditor/src/NodeConnectionInteraction.cpp create mode 100644 story-editor/nodeeditor/src/NodeDelegateModel.cpp create mode 100644 story-editor/nodeeditor/src/NodeDelegateModelRegistry.cpp create mode 100644 story-editor/nodeeditor/src/NodeGraphicsObject.cpp create mode 100644 story-editor/nodeeditor/src/NodeState.cpp create mode 100644 story-editor/nodeeditor/src/NodeStyle.cpp create mode 100644 story-editor/nodeeditor/src/StyleCollection.cpp create mode 100644 story-editor/nodeeditor/src/UndoCommands.cpp create mode 100644 story-editor/nodeeditor/src/locateNode.cpp diff --git a/.gitmodules b/.gitmodules index 62a8466..59ed8a5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ [submodule "story-editor/QHexView"] path = story-editor/QHexView - url = https://github.com/arabine/QHexView -[submodule "story-editor/nodeeditor"] - path = story-editor/nodeeditor - url = https://github.com/arabine/nodeeditor.git + url = https://github.com/arabine/QHexView \ No newline at end of file diff --git a/story-editor/nodeeditor b/story-editor/nodeeditor deleted file mode 160000 index bf458f2..0000000 --- a/story-editor/nodeeditor +++ /dev/null @@ -1 +0,0 @@ -Subproject commit bf458f2e4adef93ef75eeac40b20d50ae9c50726 diff --git a/story-editor/nodeeditor/CMakeLists.txt b/story-editor/nodeeditor/CMakeLists.txt new file mode 100644 index 0000000..ccfa378 --- /dev/null +++ b/story-editor/nodeeditor/CMakeLists.txt @@ -0,0 +1,274 @@ +cmake_minimum_required(VERSION 3.8) + +cmake_policy(SET CMP0072 NEW) # new in 3.11. The NEW behavior for this policy is to set OpenGL_GL_PREFERENCE to GLVND. +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) + +set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake" ${CMAKE_MODULE_PATH}) + +set(CMAKE_DISABLE_IN_SOURCE_BUILD ON) +set(CMAKE_DISABLE_SOURCE_CHANGES ON) +set(OpenGL_GL_PREFERENCE LEGACY) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +get_directory_property(_has_parent PARENT_DIRECTORY) +if(_has_parent) + set(is_root_project OFF) +else() + set(is_root_project ON) +endif() + +set(QT_NODES_DEVELOPER_DEFAULTS "${is_root_project}" CACHE BOOL "Turns on default settings for development of QtNodes") + +option(BUILD_TESTING "Build tests" "${QT_NODES_DEVELOPER_DEFAULTS}") +option(BUILD_EXAMPLES "Build Examples" "${QT_NODES_DEVELOPER_DEFAULTS}") +option(BUILD_DOCS "Build Documentation" "${QT_NODES_DEVELOPER_DEFAULTS}") +option(BUILD_SHARED_LIBS "Build as shared library" ON) +option(BUILD_DEBUG_POSTFIX_D "Append d suffix to debug libraries" OFF) +option(QT_NODES_FORCE_TEST_COLOR "Force colorized unit test output" OFF) + +enable_testing() + +if(QT_NODES_DEVELOPER_DEFAULTS) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/bin") + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/lib") + set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/lib") +endif() + +if(BUILD_DEBUG_POSTFIX_D) + set(CMAKE_DEBUG_POSTFIX "d") + set(CMAKE_RELEASE_POSTFIX "") + set(CMAKE_RELWITHDEBINFO_POSTFIX "rd") + set(CMAKE_MINSIZEREL_POSTFIX "s") +endif() + +add_subdirectory(external) + +find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets) +find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Widgets Gui OpenGL) +message(STATUS "QT_VERSION: ${QT_VERSION}, QT_DIR: ${QT_DIR}") + +if (${QT_VERSION} VERSION_LESS 5.11.0) + message(FATAL_ERROR "Requires qt version >= 5.11.0, Your current version is ${QT_VERSION}") +endif() + +if (${QT_VERSION_MAJOR} EQUAL 6) + qt_add_resources(RESOURCES ./resources/resources.qrc) +else() + qt5_add_resources(RESOURCES ./resources/resources.qrc) +endif() + +# Unfortunately, as we have a split include/src, AUTOMOC doesn't work. +# We'll have to manually specify some files +set(CMAKE_AUTOMOC ON) + +set(CPP_SOURCE_FILES + src/AbstractGraphModel.cpp + src/AbstractNodeGeometry.cpp + src/BasicGraphicsScene.cpp + src/ConnectionGraphicsObject.cpp + src/ConnectionPainter.cpp + src/ConnectionState.cpp + src/ConnectionStyle.cpp + src/DataFlowGraphModel.cpp + src/DataFlowGraphicsScene.cpp + src/DefaultHorizontalNodeGeometry.cpp + src/DefaultVerticalNodeGeometry.cpp + src/Definitions.cpp + src/GraphicsView.cpp + src/GraphicsViewStyle.cpp + src/NodeDelegateModelRegistry.cpp + src/NodeConnectionInteraction.cpp + src/NodeDelegateModel.cpp + src/NodeGraphicsObject.cpp + src/DefaultNodePainter.cpp + src/NodeState.cpp + src/NodeStyle.cpp + src/StyleCollection.cpp + src/UndoCommands.cpp + src/locateNode.cpp +) + +set(HPP_HEADER_FILES + include/QtNodes/internal/AbstractGraphModel.hpp + include/QtNodes/internal/AbstractNodeGeometry.hpp + include/QtNodes/internal/AbstractNodePainter.hpp + include/QtNodes/internal/BasicGraphicsScene.hpp + include/QtNodes/internal/Compiler.hpp + include/QtNodes/internal/ConnectionGraphicsObject.hpp + include/QtNodes/internal/ConnectionIdHash.hpp + include/QtNodes/internal/ConnectionIdUtils.hpp + include/QtNodes/internal/ConnectionState.hpp + include/QtNodes/internal/ConnectionStyle.hpp + include/QtNodes/internal/DataFlowGraphicsScene.hpp + include/QtNodes/internal/DataFlowGraphModel.hpp + include/QtNodes/internal/DefaultNodePainter.hpp + include/QtNodes/internal/Definitions.hpp + include/QtNodes/internal/Export.hpp + include/QtNodes/internal/GraphicsView.hpp + include/QtNodes/internal/GraphicsViewStyle.hpp + include/QtNodes/internal/locateNode.hpp + include/QtNodes/internal/NodeData.hpp + include/QtNodes/internal/NodeDelegateModel.hpp + include/QtNodes/internal/NodeDelegateModelRegistry.hpp + include/QtNodes/internal/NodeGraphicsObject.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/Style.hpp + include/QtNodes/internal/StyleCollection.hpp + include/QtNodes/internal/ConnectionPainter.hpp + include/QtNodes/internal/DefaultHorizontalNodeGeometry.hpp + include/QtNodes/internal/DefaultVerticalNodeGeometry.hpp + include/QtNodes/internal/NodeConnectionInteraction.hpp + include/QtNodes/internal/UndoCommands.hpp +) + +# If we want to give the option to build a static library, +# set BUILD_SHARED_LIBS option to OFF +add_library(QtNodes + ${CPP_SOURCE_FILES} + ${HPP_HEADER_FILES} + ${RESOURCES} +) + +add_library(QtNodes::QtNodes ALIAS QtNodes) + + +target_include_directories(QtNodes + PUBLIC + $ + $ + PRIVATE + $ + $ +) + +target_link_libraries(QtNodes + PUBLIC + Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Widgets + Qt${QT_VERSION_MAJOR}::Gui + Qt${QT_VERSION_MAJOR}::OpenGL +) + +target_compile_definitions(QtNodes + PUBLIC + NODE_EDITOR_SHARED + PRIVATE + NODE_EDITOR_EXPORTS + #NODE_DEBUG_DRAWING + QT_NO_KEYWORDS +) + + +target_compile_options(QtNodes + PRIVATE + $<$:/W4 /wd4127 /EHsc /utf-8> + $<$:-Wall -Wextra> + $<$:-Wall -Wextra -Werror> +) +if(NOT "${CMAKE_CXX_SIMULATE_ID}" STREQUAL "MSVC") + # Clang-Cl on MSVC identifies as "Clang" but behaves more like MSVC: + target_compile_options(QtNodes + PRIVATE + $<$:-Wall -Wextra> + ) +endif() + +if(QT_NODES_DEVELOPER_DEFAULTS) + target_compile_features(QtNodes PUBLIC cxx_std_14) + set_target_properties(QtNodes PROPERTIES CXX_EXTENSIONS OFF) +endif() + + +set_target_properties(QtNodes + PROPERTIES + ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin +) + +###### +# Moc +## + +file(GLOB_RECURSE HEADERS_TO_MOC include/QtNodes/internal/*.hpp) + +if (${QT_VERSION_MAJOR} EQUAL 6) + qt_wrap_cpp(nodes_moc + ${HEADERS_TO_MOC} + TARGET QtNodes + OPTIONS --no-notes # Don't display a note for the headers which don't produce a moc_*.cpp + ) +else() + qt5_wrap_cpp(nodes_moc + ${HEADERS_TO_MOC} + TARGET QtNodes + OPTIONS --no-notes # Don't display a note for the headers which don't produce a moc_*.cpp + ) +endif() + +target_sources(QtNodes PRIVATE ${nodes_moc}) + +########### +# Examples +## + +if(BUILD_EXAMPLES) + add_subdirectory(examples) +endif() + +if(BUILD_DOCS) + add_subdirectory(docs) +endif() + +################## +# Automated Tests +## + +if(BUILD_TESTING) + #add_subdirectory(test) +endif() + +############### +# Installation +## + +include(GNUInstallDirs) + +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} +) + +install(DIRECTORY include/ + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) + +install(EXPORT QtNodesTargets + FILE QtNodesTargets.cmake + NAMESPACE QtNodes:: + DESTINATION ${INSTALL_CONFIGDIR} +) + +include(CMakePackageConfigHelpers) + +configure_package_config_file(${CMAKE_CURRENT_LIST_DIR}/cmake/QtNodesConfig.cmake.in + ${CMAKE_CURRENT_BINARY_DIR}/QtNodesConfig.cmake + INSTALL_DESTINATION ${INSTALL_CONFIGDIR} +) + +install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/QtNodesConfig.cmake + DESTINATION ${INSTALL_CONFIGDIR} +) diff --git a/story-editor/nodeeditor/LICENSE.rst b/story-editor/nodeeditor/LICENSE.rst new file mode 100644 index 0000000..c485d7f --- /dev/null +++ b/story-editor/nodeeditor/LICENSE.rst @@ -0,0 +1,28 @@ +BSD-3-Clause license +==================== + +Copyright (c) 2022, Dmitry Pinaev +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of copyright holder, nor the names of its contributors may + be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS +OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/story-editor/nodeeditor/README.rst b/story-editor/nodeeditor/README.rst new file mode 100644 index 0000000..553fc41 --- /dev/null +++ b/story-editor/nodeeditor/README.rst @@ -0,0 +1,261 @@ +QtNodes +####### + +Introduction +============ + +**QtNodes** is conceived as a general-purpose Qt-based library aimed at +developing Node Editors for various applications. The library could be used for +simple graph visualization and editing or extended further for using the +`Dataflow paradigm `_ . + +The library is written using the Model-View approach. The whole graph structure +is defined by a class derived from ``AbstractGraphModel``. It is possible to +create or add Nodes and Connections. The underlying data structures could be of +any arbitrary type or representation. + +An instance of ``AbstractGraphModel`` could or could not be attached to +specialized ``QGraphicsScene`` and ``QGraphicsView`` objects. I.e. the so-called +"headless" `modus operandi` is possible. + +Documentation +============= + +`Read the Docs for QtNodes `_ + +Warning + Many classes were changed in the version ``3.0``. If you had a large project + based on ``2.x.x``, make sure you read the documentation first and see the + examples before checking out the new code. + +Branches +-------- + +There are branchses ``v2`` and ``v3`` for versions ``2.x.x`` and ``3.x`` +respectively. The branch ``master`` contains the latest dev state. + + +.. contents:: Navigation + :depth: 2 + + +Data Flow Paradigm +================== + +The extended model class ``DataFlowGraphModel`` allows to register "processing +algorithms" represented by nodes and is equipped with a set of Qt's signals and +slots for propagating the data though the nodes. + +The node's algorithm is triggered upon arriving of any new input data. The +computed result is propagated to the output connections. Each new connection +fetches available data and propagates is further. Each change in the source node +is immediately propagated through all the connections updating the whole graph. + + +Supported Environments +====================== + +Platforms +--------- + +* Linux (x64, gcc-7.0, clang-7) |ImageLink|_ +* OSX (Apple Clang - LLVM 3.6) |ImageLink|_ + +.. |ImageLink| image:: https://travis-ci.org/paceholder/nodeeditor.svg?branch=master +.. _ImageLink: https://travis-ci.org/paceholder/nodeeditor + + +* Windows (Win32, x64, msvc2017, MinGW 5.3) |AppveyorImage|_ + +.. |AppveyorImage| image:: https://ci.appveyor.com/api/projects/status/wxp47wv3uyyiujjw/branch/master?svg=true +.. _AppveyorImage: https://ci.appveyor.com/project/paceholder/nodeeditor/branch/master) + +Dependencies +------------ + +* Qt >5.15 +* CMake 3.8 +* Catch2 + + +Current State (v3) +================== + +* Model-based graph +* Headless mode + You can create, populate, modify the derivative of ``AbstractGraphModel`` + without adding it to the actual Flow Scene. The library is now designed to be + general-purpose graph visualization and modification tool, without + specialization on only data propagation. +* Automatic data propagation built on top of the graph-model code + The library could be used for both pure graph visualization purposes and for + originally implemented data propagation. +* Dynamic ports +* Datatype-aware connections +* Embedded Qt widgets +* One-output to many-input connections +* JSON-based interface styles +* Saving scenes to JSON files +* Custom Node Geometry +* Vertical and Horizontal graph layouts +* Undo/Redo, Duplication (CTRL+D) + + +Building +======== + +Linux +----- + +:: + + git clone git@github.com:paceholder/nodeeditor.git + cd nodeeditor + mkdir build + cd build + cmake .. + make -j && make install + + +Qt Creator +---------- + +1. Open `CMakeLists.txt` as project. +2. If you don't have the `Catch2` library installed, go to `Build Settings`, disable the checkbox `BUILD_TESTING`. +3. `Build -> Run CMake` +4. `Build -> Build All` +5. Click the button `Run` + + +With Cmake using `vcpkg` +^^^^^^^^^^^^^^^^^^^^^^^^ + +1. Install `vcpkg` +2. Add the following flag in configuration step of `CMake` + +:: + + -DCMAKE_TOOLCHAIN_FILE=/scripts/buildsystems/scripts/buildsystems/vcpkg.cmake + + +Help Needed +=========== + +#. Python wrappring using PySide. +#. QML frontend. +#. Wirting a ClangFormat config. + +Any suggestions are welcome! + + +Contribution +============ + +#. Be polite, respectful and collaborative. +#. For submitting a bug: + + #. Describe your environment (Qt version, compiler, OS etc) + #. Describe steps to reproduce the issue + +#. For submitting a pull request: + + #. Create a proposal task first. We can come up with a better design together. + #. Create a pull-request. If applicable, create a simple example for your + problem, describe the changes in details, provide use cases. + +#. For submitting a development request: + + #. Describe your issue in details + #. Provide some use cases. + +#. I maintain this probject in my free time, when I am not busy with my work or + my family. **If I do not react or do not answer for too long, please ping + me**. + + +Citing +====== + +:: + + Dmitry Pinaev et al, Qt Nodes, (2022), GitHub repository, https://github.com/paceholder/nodeeditor + +BibTeX:: + + @misc{Pinaev2022, + author = {Dmitry Pinaev et al}, + title = {QtNodes. Node Editor}, + year = {2017}, + publisher = {GitHub}, + journal = {GitHub repository}, + howpublished = {\url{https://github.com/paceholder/nodeeditor}}, + commit = {877ddb8c447a7a061a5022e9956a3194132e3dd9} + } + +Support +======= + +If you like the project you could donate me on PayPal |ImagePaypal|_ + +.. |ImagePaypal| image:: https://img.shields.io/badge/Donate-PayPal-green.svg +.. _ImagePaypal: https://www.paypal.com/paypalme/DmitryPinaev + + +If you send more than $100, I'll forward $100 to some fund supporting sick +children and report to you back. + + +Thanks +====== + +The version 3 was released with a generous help of +`Davide Faconti `_ + + +Showcase +======== + +Youtube videos +-------------- + +.. image:: https://img.youtube.com/vi/pxMXjSvlOFw/0.jpg + :target: https://www.youtube.com/watch?v=pxMXjSvlOFw + +| + +.. image:: https://img.youtube.com/vi/i_pB-Y0hCYQ/0.jpg + :target: https://www.youtube.com/watch?v=i_pB-Y0hCYQ + +CANdevStudio +------------ + +`CANdevStudio `_ is a cost-effective, +cross-platform replacement for CAN simulation software. CANdevStudio enables to +simulate CAN signals such as ignition status, doors status or reverse gear by +every automotive developer. Thanks to modularity it is easy to implement new, +custom features. + + +.. image:: docs/_static/showcase_CANdevStudio.png + + +Chigraph +-------- + +`Chigraph `_ is a visual programming +language for beginners that is unique in that it is an intuitive flow graph: + +.. image:: docs/_static/chigraph.png + + +It features easy bindings to C/C++, package management, and a cool interface. + + +Spkgen particle editor +---------------------- + +`Spkgen `_ is an editor for the SPARK +particles engine that uses a node-based interface to create particles effects for +games + +.. image:: docs/_static/spkgen.png diff --git a/story-editor/nodeeditor/include/QtNodes/AbstractGraphModel b/story-editor/nodeeditor/include/QtNodes/AbstractGraphModel new file mode 100644 index 0000000..0ad0ac0 --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/AbstractGraphModel @@ -0,0 +1 @@ +#include "internal/AbstractGraphModel.hpp" diff --git a/story-editor/nodeeditor/include/QtNodes/AbstractNodePainter b/story-editor/nodeeditor/include/QtNodes/AbstractNodePainter new file mode 100644 index 0000000..a62405c --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/AbstractNodePainter @@ -0,0 +1 @@ +#include "internal/AbstractNodePainter.hpp" diff --git a/story-editor/nodeeditor/include/QtNodes/BasicGraphicsScene b/story-editor/nodeeditor/include/QtNodes/BasicGraphicsScene new file mode 100644 index 0000000..7d17e8d --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/BasicGraphicsScene @@ -0,0 +1 @@ +#include "internal/BasicGraphicsScene.hpp" diff --git a/story-editor/nodeeditor/include/QtNodes/ConnectionIdUtils b/story-editor/nodeeditor/include/QtNodes/ConnectionIdUtils new file mode 100644 index 0000000..4c97c07 --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/ConnectionIdUtils @@ -0,0 +1 @@ +#include "internal/ConnectionIdUtils.hpp" diff --git a/story-editor/nodeeditor/include/QtNodes/ConnectionStyle b/story-editor/nodeeditor/include/QtNodes/ConnectionStyle new file mode 100644 index 0000000..b771bf3 --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/ConnectionStyle @@ -0,0 +1 @@ +#include "internal/ConnectionStyle.hpp" diff --git a/story-editor/nodeeditor/include/QtNodes/DataFlowGraphModel b/story-editor/nodeeditor/include/QtNodes/DataFlowGraphModel new file mode 100644 index 0000000..28e8cbe --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/DataFlowGraphModel @@ -0,0 +1 @@ +#include "internal/DataFlowGraphModel.hpp" diff --git a/story-editor/nodeeditor/include/QtNodes/DataFlowGraphicsScene b/story-editor/nodeeditor/include/QtNodes/DataFlowGraphicsScene new file mode 100644 index 0000000..3585527 --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/DataFlowGraphicsScene @@ -0,0 +1 @@ +#include "internal/DataFlowGraphicsScene.hpp" diff --git a/story-editor/nodeeditor/include/QtNodes/DefaultNodePainter b/story-editor/nodeeditor/include/QtNodes/DefaultNodePainter new file mode 100644 index 0000000..c808518 --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/DefaultNodePainter @@ -0,0 +1 @@ +#include "internal/DefaultNodePainter.hpp" diff --git a/story-editor/nodeeditor/include/QtNodes/Definitions b/story-editor/nodeeditor/include/QtNodes/Definitions new file mode 100644 index 0000000..2ac05a0 --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/Definitions @@ -0,0 +1 @@ +#include "internal/Definitions.hpp" diff --git a/story-editor/nodeeditor/include/QtNodes/GraphicsView b/story-editor/nodeeditor/include/QtNodes/GraphicsView new file mode 100644 index 0000000..da4e21a --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/GraphicsView @@ -0,0 +1 @@ +#include "internal/GraphicsView.hpp" diff --git a/story-editor/nodeeditor/include/QtNodes/GraphicsViewStyle b/story-editor/nodeeditor/include/QtNodes/GraphicsViewStyle new file mode 100644 index 0000000..8a98541 --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/GraphicsViewStyle @@ -0,0 +1 @@ +#include "internal/GraphicsViewStyle.hpp" diff --git a/story-editor/nodeeditor/include/QtNodes/NodeData b/story-editor/nodeeditor/include/QtNodes/NodeData new file mode 100644 index 0000000..5fcbd7b --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/NodeData @@ -0,0 +1 @@ +#include "internal/NodeData.hpp" diff --git a/story-editor/nodeeditor/include/QtNodes/NodeDelegateModel b/story-editor/nodeeditor/include/QtNodes/NodeDelegateModel new file mode 100644 index 0000000..70f25f7 --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/NodeDelegateModel @@ -0,0 +1 @@ +#include "internal/NodeDelegateModel.hpp" diff --git a/story-editor/nodeeditor/include/QtNodes/NodeDelegateModelRegistry b/story-editor/nodeeditor/include/QtNodes/NodeDelegateModelRegistry new file mode 100644 index 0000000..9d78da5 --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/NodeDelegateModelRegistry @@ -0,0 +1 @@ +#include "internal/NodeDelegateModelRegistry.hpp" diff --git a/story-editor/nodeeditor/include/QtNodes/NodeGeometry b/story-editor/nodeeditor/include/QtNodes/NodeGeometry new file mode 100644 index 0000000..46a70a6 --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/NodeGeometry @@ -0,0 +1,2 @@ +#include "internal/NodeGeometry.hpp" + diff --git a/story-editor/nodeeditor/include/QtNodes/NodeState b/story-editor/nodeeditor/include/QtNodes/NodeState new file mode 100644 index 0000000..5b531fd --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/NodeState @@ -0,0 +1 @@ +#include "internal/NodeState.hpp" diff --git a/story-editor/nodeeditor/include/QtNodes/NodeStyle b/story-editor/nodeeditor/include/QtNodes/NodeStyle new file mode 100644 index 0000000..d96092d --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/NodeStyle @@ -0,0 +1 @@ +#include "internal/NodeStyle.hpp" diff --git a/story-editor/nodeeditor/include/QtNodes/StyleCollection b/story-editor/nodeeditor/include/QtNodes/StyleCollection new file mode 100644 index 0000000..e1f93ec --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/StyleCollection @@ -0,0 +1 @@ +#include "internal/StyleCollection.hpp" diff --git a/story-editor/nodeeditor/include/QtNodes/UndoCommands b/story-editor/nodeeditor/include/QtNodes/UndoCommands new file mode 100644 index 0000000..2ff49a5 --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/UndoCommands @@ -0,0 +1 @@ +#include "internal/UndoCommands.hpp" diff --git a/story-editor/nodeeditor/include/QtNodes/internal/AbstractGraphModel.hpp b/story-editor/nodeeditor/include/QtNodes/internal/AbstractGraphModel.hpp new file mode 100644 index 0000000..96d2c64 --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/internal/AbstractGraphModel.hpp @@ -0,0 +1,249 @@ +#pragma once + +#include "Export.hpp" + +#include +#include + +#include +#include +#include + +#include "ConnectionIdHash.hpp" +#include "Definitions.hpp" + +namespace QtNodes { + +/** + * The central class in the Model-View approach. It delivers all kinds + * of information from the backing user data structures that represent + * the graph. The class allows to modify the graph structure: create + * and remove nodes and connections. + * + * We use two types of the unique ids for graph manipulations: + * - NodeId + * - ConnectionId + */ +class NODE_EDITOR_PUBLIC AbstractGraphModel : public QObject +{ + Q_OBJECT +public: + /// Generates a new unique NodeId. + virtual NodeId newNodeId() = 0; + + /// @brief Returns the full set of unique Node Ids. + /** + * Model creator is responsible for generating unique `unsigned int` + * 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. + */ + virtual std::unordered_set allNodeIds() const = 0; + + /** + * A collection of all input and output connections for the given `nodeId`. + */ + virtual std::unordered_set 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`. + */ + virtual std::unordered_set 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; + + /// Creates a new node instance in the derived class. + /** + * The model is responsible for generating a unique `NodeId`. + * @param[in] nodeType is free to be used and interpreted by the + * 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; + + /// Model decides if a conection with a given connection Id possible. + /** + * The default implementation compares corresponding data types. + * + * It is possible to override the function and connect non-equal + * data types. + */ + virtual bool connectionPossible(ConnectionId const connectionId) const = 0; + + /// Defines if detaching the connection is possible. + virtual bool detachPossible(ConnectionId const) const { return true; } + + /// Creates a new connection between two nodes. + /** + * Default implementation emits signal + * `connectionCreated(connectionId)` + * + * In the derived classes user must emite the signal to notify the + * scene about the changes. + */ + virtual void addConnection(ConnectionId const connectionId) = 0; + + /** + * @returns `true` if there is data in the model associated with the + * given `nodeId`. + */ + virtual bool nodeExists(NodeId const nodeId) const = 0; + + /// @brief Returns node-related data for requested NodeRole. + /** + * @returns Node Caption, Node Caption Visibility, Node Position etc. + */ + virtual QVariant nodeData(NodeId nodeId, NodeRole role) const = 0; + + /** + * A utility function that unwraps the `QVariant` value returned from the + * standard `QVariant AbstractGraphModel::nodeData(NodeId, NodeRole)` function. + */ + template + T nodeData(NodeId nodeId, NodeRole role) const + { + return nodeData(nodeId, role).value(); + } + + virtual NodeFlags nodeFlags(NodeId nodeId) const + { + Q_UNUSED(nodeId); + return NodeFlag::NoFlags; + } + + /// @brief Sets node properties. + /** + * Sets: Node Caption, Node Caption Visibility, + * Shyle, State, Node Position etc. + * @see NodeRole. + */ + virtual bool setNodeData(NodeId nodeId, NodeRole role, QVariant value) = 0; + + /// @brief Returns port-related data for requested NodeRole. + /** + * @returns Port Data Type, Port Data, Connection Policy, Port + * Caption. + */ + virtual QVariant portData(NodeId nodeId, PortType portType, PortIndex index, PortRole role) const + = 0; + + /** + * A utility function that unwraps the `QVariant` value returned from the + * standard `QVariant AbstractGraphModel::portData(...)` function. + */ + template + T portData(NodeId nodeId, PortType portType, PortIndex index, PortRole role) const + { + return portData(nodeId, portType, index, role).value(); + } + + virtual bool setPortData(NodeId nodeId, + PortType portType, + PortIndex index, + QVariant const &value, + PortRole role = PortRole::Data) + = 0; + + virtual bool deleteConnection(ConnectionId const connectionId) = 0; + + virtual bool deleteNode(NodeId const nodeId) = 0; + + /** + * Reimplement the function if you want to store/restore the node's + * inner state during undo/redo node deletion operations. + */ + virtual QJsonObject saveNode(NodeId const) const { return {}; } + + /** + * Reimplement the function if you want to support: + * + * - graph save/restore operations, + * - undo/redo operations after deleting the node. + * + * QJsonObject must contain following fields: + * + * + * ``` + * { + * id : 5, + * position : { x : 100, y : 200 }, + * internal-data { + * "your model specific data here" + * } + * } + * ``` + * + * The function must do almost exacly the same thing as the normal addNode(). + * The main difference is in a model-specific `inner-data` processing. + */ + virtual void loadNode(QJsonObject const &) {} + +public: + /** + * Function clears connections attached to the ports that are scheduled to be + * deleted. It must be called right before the model removes its old port data. + * + * @param nodeId Defines the node to be modified + * @param portType Is either PortType::In or PortType::Out + * @param first Index of the first port to be removed + * @param last Index of the last port to be removed + */ + void portsAboutToBeDeleted(NodeId const nodeId, + PortType const portType, + PortIndex const first, + PortIndex const last); + + /** + * Signal emitted when model no longer has the old data associated with the + * given port indices and when the node must be repainted. + */ + void portsDeleted(); + + /** + * Signal emitted when model is about to create new ports on the given node. + * @param first Is the first index of the new port after insertion. + * @param last Is the last index of the new port after insertion. + * + * Function caches existing connections that are located after the `last` port + * index. For such connections the new "post-insertion" addresses are computed + * and stored until the function AbstractGraphModel::portsInserted is called. + */ + void portsAboutToBeInserted(NodeId const nodeId, + PortType const portType, + PortIndex const first, + PortIndex const last); + + /** + * Function re-creates the connections that were shifted during the port + * insertion. After that the node is updated. + */ + void portsInserted(); + +Q_SIGNALS: + void connectionCreated(ConnectionId const connectionId); + + void connectionDeleted(ConnectionId const connectionId); + + void nodeCreated(NodeId const nodeId); + + void nodeDeleted(NodeId const nodeId); + + void nodeUpdated(NodeId const nodeId); + + void nodeFlagsUpdated(NodeId const nodeId); + + void nodePositionUpdated(NodeId const nodeId); + + void modelReset(); + +private: + std::vector _shiftedByDynamicPortsConnections; +}; + +} // namespace QtNodes diff --git a/story-editor/nodeeditor/include/QtNodes/internal/AbstractNodeGeometry.hpp b/story-editor/nodeeditor/include/QtNodes/internal/AbstractNodeGeometry.hpp new file mode 100644 index 0000000..36cc9a2 --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/internal/AbstractNodeGeometry.hpp @@ -0,0 +1,82 @@ +#pragma once + +#include "Definitions.hpp" +#include "Export.hpp" + +#include +#include +#include + +namespace QtNodes { + +class AbstractGraphModel; + +class NODE_EDITOR_PUBLIC AbstractNodeGeometry +{ +public: + AbstractNodeGeometry(AbstractGraphModel &, double marginsRatio = 0.2); + virtual ~AbstractNodeGeometry() {} + + /** + * The node's size plus some additional margin around it to account for drawing + * effects (for example shadows) or node's parts outside the size rectangle + * (for example port points). + * + * The default implementation returns QSize + 20 percent of width and heights + * at each side of the rectangle. + */ + virtual QRectF boundingRect(NodeId const nodeId) const; + + virtual void setMarginsRatio(double marginsRatio); + + /// A direct rectangle defining the borders of the node's rectangle. + virtual QSize size(NodeId const nodeId) const = 0; + + /** + * The function is triggeren when a nuber of ports is changed or when an + * embedded widget needs an update. + */ + virtual void recomputeSize(NodeId const nodeId) const = 0; + + /// Port position in node's coordinate system. + virtual QPointF portPosition(NodeId const nodeId, + PortType const portType, + PortIndex const index) const + = 0; + + /// A convenience function using the `portPosition` and a given transformation. + virtual QPointF portScenePosition(NodeId const nodeId, + PortType const portType, + PortIndex const index, + QTransform const &t) const; + + /// Defines where to draw port label. The point corresponds to a font baseline. + virtual QPointF portTextPosition(NodeId const nodeId, + PortType const portType, + PortIndex const portIndex) const + = 0; + + /** + * Defines where to start drawing the caption. The point corresponds to a font + * baseline. + */ + virtual QPointF captionPosition(NodeId const nodeId) const = 0; + + /// Caption rect is needed for estimating the total node size. + virtual QRectF captionRect(NodeId const nodeId) const = 0; + + /// Position for an embedded widget. Return any value if you don't embed. + virtual QPointF widgetPosition(NodeId const nodeId) const = 0; + + virtual PortIndex checkPortHit(NodeId const nodeId, + PortType const portType, + QPointF const nodePoint) const; + + virtual QRect resizeHandleRect(NodeId const nodeId) const = 0; + +protected: + AbstractGraphModel &_graphModel; + double _marginsRatio{0.0}; +}; + +} // namespace QtNodes diff --git a/story-editor/nodeeditor/include/QtNodes/internal/AbstractNodePainter.hpp b/story-editor/nodeeditor/include/QtNodes/internal/AbstractNodePainter.hpp new file mode 100644 index 0000000..cd4c7d6 --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/internal/AbstractNodePainter.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include + +#include "Export.hpp" + +class QPainter; + +namespace QtNodes { + +class NodeGraphicsObject; +class NodeDataModel; + +/// Class enables custom painting. +class NODE_EDITOR_PUBLIC AbstractNodePainter +{ +public: + virtual ~AbstractNodePainter() = default; + + /** + * Reimplement this function in order to have a custom painting. + * + * Useful functions: + * `NodeGraphicsObject::nodeScene()->nodeGeometry()` + * `NodeGraphicsObject::graphModel()` + */ + virtual void paint(QPainter *painter, NodeGraphicsObject &ngo) const = 0; +}; +} // namespace QtNodes diff --git a/story-editor/nodeeditor/include/QtNodes/internal/BasicGraphicsScene.hpp b/story-editor/nodeeditor/include/QtNodes/internal/BasicGraphicsScene.hpp new file mode 100644 index 0000000..0cc6abd --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/internal/BasicGraphicsScene.hpp @@ -0,0 +1,184 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include + +#include "AbstractGraphModel.hpp" +#include "AbstractNodeGeometry.hpp" +#include "ConnectionIdHash.hpp" +#include "Definitions.hpp" +#include "Export.hpp" + +#include "QUuidStdHash.hpp" + +class QUndoStack; + +namespace QtNodes { + +class AbstractGraphModel; +class AbstractNodePainter; +class ConnectionGraphicsObject; +class NodeGraphicsObject; +class NodeStyle; + +/// An instance of QGraphicsScene, holds connections and nodes. +class NODE_EDITOR_PUBLIC BasicGraphicsScene : public QGraphicsScene +{ + Q_OBJECT +public: + BasicGraphicsScene(AbstractGraphModel &graphModel, QObject *parent = nullptr); + + // Scenes without models are not supported + BasicGraphicsScene() = delete; + + ~BasicGraphicsScene(); + +public: + /// @returns associated AbstractGraphModel. + AbstractGraphModel const &graphModel() const; + + AbstractGraphModel &graphModel(); + + AbstractNodeGeometry &nodeGeometry(); + + AbstractNodePainter &nodePainter(); + + void setNodePainter(std::unique_ptr newPainter); + + QUndoStack &undoStack(); + + void setDropShadowEffect(bool enable); + + bool isDropShadowEffectEnabled() const; + +public: + /// Creates a "draft" instance of ConnectionGraphicsObject. + /** + * The scene caches a "draft" connection which has one loose end. + * After attachment the "draft" instance is deleted and instead a + * normal "full" connection is created. + * Function @returns the "draft" instance for further geometry + * manipulations. + */ + std::unique_ptr const &makeDraftConnection( + ConnectionId const newConnectionId); + + /// Deletes "draft" connection. + /** + * The function is called when user releases the mouse button during + * the construction of the new connection without attaching it to any + * node. + */ + void resetDraftConnection(); + + /// Deletes all the nodes. Connections are removed automatically. + void clearScene(); + +public: + /// @returns NodeGraphicsObject associated with the given nodeId. + /** + * @returns nullptr when the object is not found. + */ + NodeGraphicsObject *nodeGraphicsObject(NodeId nodeId); + + /// @returns ConnectionGraphicsObject corresponding to `connectionId`. + /** + * @returns `nullptr` when the object is not found. + */ + ConnectionGraphicsObject *connectionGraphicsObject(ConnectionId connectionId); + + Qt::Orientation orientation() const { return _orientation; } + + void setOrientation(Qt::Orientation const orientation); + +public: + /// Can @return an instance of the scene context menu in subclass. + /** + * Default implementation returns `nullptr`. + */ + virtual QMenu *createSceneMenu(QPointF const scenePos); + +Q_SIGNALS: + void nodeMoved(NodeId const nodeId, QPointF const &newLocation); + + void nodeClicked(NodeId const nodeId); + + void nodeSelected(NodeId const nodeId); + + void nodeDoubleClicked(NodeId const nodeId); + + void nodeHovered(NodeId const nodeId, QPoint const screenPos); + + void nodeHoverLeft(NodeId const nodeId); + + void connectionHovered(ConnectionId const connectionId, QPoint const screenPos); + + void connectionHoverLeft(ConnectionId const connectionId); + + /// Signal allows showing custom context menu upon clicking a node. + void nodeContextMenu(NodeId const nodeId, QPointF const pos); + +private: + /// @brief Creates Node and Connection graphics objects. + /** + * Function is used to populate an empty scene in the constructor. We + * perform depth-first AbstractGraphModel traversal. The connections are + * created by checking non-empty node `Out` ports. + */ + void traverseGraphAndPopulateGraphicsObjects(); + + /// Redraws adjacent nodes for given `connectionId` + void updateAttachedNodes(ConnectionId const connectionId, PortType const portType); + +public Q_SLOTS: + /// Slot called when the `connectionId` is erased form the AbstractGraphModel. + void onConnectionDeleted(ConnectionId const connectionId); + + /// Slot called when the `connectionId` is created in the AbstractGraphModel. + void onConnectionCreated(ConnectionId const connectionId); + + void onNodeDeleted(NodeId const nodeId); + + void onNodeCreated(NodeId const nodeId); + + void onNodePositionUpdated(NodeId const nodeId); + + void onNodeUpdated(NodeId const nodeId); + + void onNodeClicked(NodeId const nodeId); + + void onModelReset(); + +private: + AbstractGraphModel &_graphModel; + + using UniqueNodeGraphicsObject = std::unique_ptr; + + using UniqueConnectionGraphicsObject = std::unique_ptr; + + std::unordered_map _nodeGraphicsObjects; + + std::unordered_map _connectionGraphicsObjects; + + std::unique_ptr _draftConnection; + + std::unique_ptr _nodeGeometry; + + std::unique_ptr _nodePainter; + + bool _nodeDrag; + + QUndoStack *_undoStack; + + Qt::Orientation _orientation; + + bool _dropShadowEffect{false}; +}; + +} // namespace QtNodes diff --git a/story-editor/nodeeditor/include/QtNodes/internal/Compiler.hpp b/story-editor/nodeeditor/include/QtNodes/internal/Compiler.hpp new file mode 100644 index 0000000..9842657 --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/internal/Compiler.hpp @@ -0,0 +1,40 @@ +#pragma once + +#if defined(__MINGW32__) || defined(__MINGW64__) +#define NODE_EDITOR_COMPILER "MinGW" +#define NODE_EDITOR_COMPILER_MINGW +#elif defined(__clang__) +#define NODE_EDITOR_COMPILER "Clang" +#define NODE_EDITOR_COMPILER_CLANG +#elif defined(_MSC_VER) +#define NODE_EDITOR_COMPILER "Microsoft Visual C++" +#define NODE_EDITOR_COMPILER_MICROSOFT +#elif defined(__GNUC__) +#define NODE_EDITOR_COMPILER "GNU" +#define NODE_EDITOR_COMPILER_GNU +#define NODE_EDITOR_COMPILER_GNU_VERSION_MAJOR __GNUC__ +#define NODE_EDITOR_COMPILER_GNU_VERSION_MINOR __GNUC_MINOR__ +#define NODE_EDITOR_COMPILER_GNU_VERSION_PATCH __GNUC_PATCHLEVEL__ +#elif defined(__BORLANDC__) +#define NODE_EDITOR_COMPILER "Borland C++ Builder" +#define NODE_EDITOR_COMPILER_BORLAND +#elif defined(__CODEGEARC__) +#define NODE_EDITOR_COMPILER "CodeGear C++ Builder" +#define NODE_EDITOR_COMPILER_CODEGEAR +#elif defined(__INTEL_COMPILER) || defined(__ICL) +#define NODE_EDITOR_COMPILER "Intel C++" +#define NODE_EDITOR_COMPILER_INTEL +#elif defined(__xlC__) || defined(__IBMCPP__) +#define NODE_EDITOR_COMPILER "IBM XL C++" +#define NODE_EDITOR_COMPILER_IBM +#elif defined(__HP_aCC) +#define NODE_EDITOR_COMPILER "HP aC++" +#define NODE_EDITOR_COMPILER_HP +#elif defined(__WATCOMC__) +#define NODE_EDITOR_COMPILER "Watcom C++" +#define NODE_EDITOR_COMPILER_WATCOM +#endif + +#ifndef NODE_EDITOR_COMPILER +#error "Current compiler is not supported." +#endif diff --git a/story-editor/nodeeditor/include/QtNodes/internal/ConnectionGraphicsObject.hpp b/story-editor/nodeeditor/include/QtNodes/internal/ConnectionGraphicsObject.hpp new file mode 100644 index 0000000..f0e569d --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/internal/ConnectionGraphicsObject.hpp @@ -0,0 +1,96 @@ +#pragma once + +#include + +#include +#include + +#include "ConnectionState.hpp" +#include "Definitions.hpp" + +class QGraphicsSceneMouseEvent; + +namespace QtNodes { + +class AbstractGraphModel; +class BasicGraphicsScene; + +/// Graphic Object for connection. Adds itself to scene +class ConnectionGraphicsObject : public QGraphicsObject +{ + Q_OBJECT +public: + // Needed for qgraphicsitem_cast + enum { Type = UserType + 2 }; + + int type() const override { return Type; } + +public: + ConnectionGraphicsObject(BasicGraphicsScene &scene, ConnectionId const connectionId); + + ~ConnectionGraphicsObject() = default; + +public: + AbstractGraphModel &graphModel() const; + + BasicGraphicsScene *nodeScene() const; + + ConnectionId const &connectionId() const; + + QRectF boundingRect() const override; + + QPainterPath shape() const override; + + QPointF const &endPoint(PortType portType) const; + + QPointF out() const { return _out; } + + QPointF in() const { return _in; } + + std::pair pointsC1C2() const; + + void setEndPoint(PortType portType, QPointF const &point); + + /// Updates the position of both ends + void move(); + + ConnectionState const &connectionState() const; + + ConnectionState &connectionState(); + +protected: + void paint(QPainter *painter, + QStyleOptionGraphicsItem const *option, + QWidget *widget = 0) override; + + void mousePressEvent(QGraphicsSceneMouseEvent *event) override; + + void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; + + void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; + + void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override; + + void hoverLeaveEvent(QGraphicsSceneHoverEvent *event) override; + +private: + void initializePosition(); + + void addGraphicsEffect(); + + std::pair pointsC1C2Horizontal() const; + + std::pair pointsC1C2Vertical() const; + +private: + ConnectionId _connectionId; + + AbstractGraphModel &_graphModel; + + ConnectionState _connectionState; + + mutable QPointF _out; + mutable QPointF _in; +}; + +} // namespace QtNodes diff --git a/story-editor/nodeeditor/include/QtNodes/internal/ConnectionIdHash.hpp b/story-editor/nodeeditor/include/QtNodes/internal/ConnectionIdHash.hpp new file mode 100644 index 0000000..09eaced --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/internal/ConnectionIdHash.hpp @@ -0,0 +1,55 @@ +#pragma once + +#include + +#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...); +} + +namespace std { +template<> +struct hash +{ + inline std::size_t operator()(QtNodes::ConnectionId const &id) const + { + std::size_t h = 0; + hash_combine(h, id.outNodeId, id.outPortIndex, id.inNodeId, id.inPortIndex); + return h; + } +}; + +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/story-editor/nodeeditor/include/QtNodes/internal/ConnectionIdUtils.hpp b/story-editor/nodeeditor/include/QtNodes/internal/ConnectionIdUtils.hpp new file mode 100644 index 0000000..7f70a1b --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/internal/ConnectionIdUtils.hpp @@ -0,0 +1,151 @@ +#pragma once + +#include "Definitions.hpp" + +#include + +#include +#include + +namespace QtNodes { + +inline PortIndex getNodeId(PortType portType, ConnectionId connectionId) +{ + NodeId id = InvalidNodeId; + + if (portType == PortType::Out) { + id = connectionId.outNodeId; + } else if (portType == PortType::In) { + id = connectionId.inNodeId; + } + + return id; +} + +inline PortIndex getPortIndex(PortType portType, ConnectionId connectionId) +{ + PortIndex index = InvalidPortIndex; + + if (portType == PortType::Out) { + index = connectionId.outPortIndex; + } else if (portType == PortType::In) { + index = connectionId.inPortIndex; + } + + return index; +} + +inline PortType oppositePort(PortType port) +{ + PortType result = PortType::None; + + switch (port) { + case PortType::In: + result = PortType::Out; + break; + + case PortType::Out: + result = PortType::In; + break; + + case PortType::None: + result = PortType::None; + break; + + default: + break; + } + return result; +} + +inline bool isPortIndexValid(PortIndex index) +{ + return index != InvalidPortIndex; +} + +inline bool isPortTypeValid(PortType portType) +{ + return portType != PortType::None; +} + +/** + * Creates a connection Id instance filled just on one side. + */ +inline ConnectionId makeIncompleteConnectionId(NodeId const connectedNodeId, + PortType const connectedPort, + PortIndex const connectedPortIndex) +{ + return (connectedPort == PortType::In) + ? ConnectionId{InvalidNodeId, InvalidPortIndex, connectedNodeId, connectedPortIndex} + : ConnectionId{connectedNodeId, connectedPortIndex, InvalidNodeId, InvalidPortIndex}; +} + +/** + * Turns a full connection Id into an incomplete one by removing the + * data on the given side + */ +inline ConnectionId makeIncompleteConnectionId(ConnectionId connectionId, + PortType const portToDisconnect) +{ + if (portToDisconnect == PortType::Out) { + connectionId.outNodeId = InvalidNodeId; + connectionId.outPortIndex = InvalidPortIndex; + } else { + connectionId.inNodeId = InvalidNodeId; + connectionId.inPortIndex = InvalidPortIndex; + } + + return connectionId; +} + +inline ConnectionId makeCompleteConnectionId(ConnectionId incompleteConnectionId, + NodeId const nodeId, + PortIndex const portIndex) +{ + if (incompleteConnectionId.outNodeId == InvalidNodeId) { + incompleteConnectionId.outNodeId = nodeId; + incompleteConnectionId.outPortIndex = portIndex; + } else { + incompleteConnectionId.inNodeId = nodeId; + incompleteConnectionId.inPortIndex = portIndex; + } + + return incompleteConnectionId; +} + +inline std::ostream &operator<<(std::ostream &ostr, ConnectionId const connectionId) +{ + ostr << "(" << connectionId.outNodeId << ", " + << (isPortIndexValid(connectionId.outPortIndex) ? std::to_string(connectionId.outPortIndex) + : "INVALID") + << ", " << connectionId.inNodeId << ", " + << (isPortIndexValid(connectionId.inPortIndex) ? std::to_string(connectionId.inPortIndex) + : "INVALID") + << ")" << std::endl; + + return ostr; +} + +inline QJsonObject toJson(ConnectionId const &connId) +{ + QJsonObject connJson; + + connJson["outNodeId"] = static_cast(connId.outNodeId); + connJson["outPortIndex"] = static_cast(connId.outPortIndex); + connJson["intNodeId"] = static_cast(connId.inNodeId); + connJson["inPortIndex"] = static_cast(connId.inPortIndex); + + return connJson; +} + +inline ConnectionId fromJson(QJsonObject const &connJson) +{ + ConnectionId connId{static_cast(connJson["outNodeId"].toInt(InvalidNodeId)), + static_cast(connJson["outPortIndex"].toInt(InvalidPortIndex)), + static_cast(connJson["intNodeId"].toInt(InvalidNodeId)), + static_cast(connJson["inPortIndex"].toInt(InvalidPortIndex))}; + + return connId; +} + +} // namespace QtNodes diff --git a/story-editor/nodeeditor/include/QtNodes/internal/ConnectionPainter.hpp b/story-editor/nodeeditor/include/QtNodes/internal/ConnectionPainter.hpp new file mode 100644 index 0000000..8db24d8 --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/internal/ConnectionPainter.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include +#include + +#include "Definitions.hpp" + +namespace QtNodes { + +class ConnectionGeometry; +class ConnectionGraphicsObject; + +class ConnectionPainter +{ +public: + static void paint(QPainter *painter, ConnectionGraphicsObject const &cgo); + + static QPainterPath getPainterStroke(ConnectionGraphicsObject const &cgo); +}; + +} // namespace QtNodes diff --git a/story-editor/nodeeditor/include/QtNodes/internal/ConnectionState.hpp b/story-editor/nodeeditor/include/QtNodes/internal/ConnectionState.hpp new file mode 100644 index 0000000..ddd31cd --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/internal/ConnectionState.hpp @@ -0,0 +1,60 @@ +#pragma once + +#include + +#include "Export.hpp" + +#include "Definitions.hpp" + +class QPointF; + +namespace QtNodes { + +class ConnectionGraphicsObject; + +/// Stores currently draggind end. +/// Remembers last hovered Node. +class NODE_EDITOR_PUBLIC ConnectionState +{ +public: + /// Defines whether we construct a new connection + /// or it is already binding two nodes. + enum LooseEnd { Pending = 0, Connected = 1 }; + +public: + ConnectionState(ConnectionGraphicsObject &cgo) + : _cgo(cgo) + , _hovered(false) + {} + + ConnectionState(ConnectionState const &) = delete; + ConnectionState(ConnectionState &&) = delete; + + ConnectionState &operator=(ConnectionState const &) = delete; + ConnectionState &operator=(ConnectionState &&) = delete; + + ~ConnectionState(); + +public: + PortType requiredPort() const; + bool requiresPort() const; + + bool hovered() const; + void setHovered(bool hovered); + +public: + /// Caches NodeId for further interaction. + void setLastHoveredNode(NodeId const nodeId); + + NodeId lastHoveredNode() const; + + void resetLastHoveredNode(); + +private: + ConnectionGraphicsObject &_cgo; + + bool _hovered; + + NodeId _lastHoveredNode{InvalidNodeId}; +}; +} // namespace QtNodes diff --git a/story-editor/nodeeditor/include/QtNodes/internal/ConnectionStyle.hpp b/story-editor/nodeeditor/include/QtNodes/internal/ConnectionStyle.hpp new file mode 100644 index 0000000..b718bcf --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/internal/ConnectionStyle.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include + +#include "Export.hpp" +#include "Style.hpp" + +namespace QtNodes { + +class NODE_EDITOR_PUBLIC ConnectionStyle : public Style +{ +public: + ConnectionStyle(); + + ConnectionStyle(QString jsonText); + + ~ConnectionStyle() = default; + +public: + static void setConnectionStyle(QString jsonText); + +public: + void loadJson(QJsonObject const &json) override; + + QJsonObject toJson() const override; + +public: + QColor constructionColor() const; + QColor normalColor() const; + QColor normalColor(QString typeId) const; + QColor selectedColor() const; + QColor selectedHaloColor() const; + QColor hoveredColor() const; + + float lineWidth() const; + float constructionLineWidth() const; + float pointDiameter() const; + + bool useDataDefinedColors() const; + +private: + QColor ConstructionColor; + QColor NormalColor; + QColor SelectedColor; + QColor SelectedHaloColor; + QColor HoveredColor; + + float LineWidth; + float ConstructionLineWidth; + float PointDiameter; + + bool UseDataDefinedColors; +}; +} // namespace QtNodes diff --git a/story-editor/nodeeditor/include/QtNodes/internal/DataFlowGraphModel.hpp b/story-editor/nodeeditor/include/QtNodes/internal/DataFlowGraphModel.hpp new file mode 100644 index 0000000..2688002 --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/internal/DataFlowGraphModel.hpp @@ -0,0 +1,135 @@ +#pragma once + +#include "AbstractGraphModel.hpp" +#include "ConnectionIdUtils.hpp" +#include "NodeDelegateModelRegistry.hpp" +#include "Serializable.hpp" +#include "StyleCollection.hpp" + +#include "Export.hpp" + +#include + +#include + +namespace QtNodes { + +class NODE_EDITOR_PUBLIC DataFlowGraphModel : public AbstractGraphModel, public Serializable +{ + Q_OBJECT + +public: + struct NodeGeometryData + { + QSize size; + QPointF pos; + }; + +public: + DataFlowGraphModel(std::shared_ptr registry); + + std::shared_ptr dataModelRegistry() { return _registry; } + +public: + std::unordered_set allNodeIds() const override; + + std::unordered_set allConnectionIds(NodeId const nodeId) const override; + + std::unordered_set connections(NodeId nodeId, + PortType portType, + PortIndex portIndex) const override; + + bool connectionExists(ConnectionId const connectionId) const override; + + NodeId addNode(QString const nodeType) override; + + bool connectionPossible(ConnectionId const connectionId) const override; + + void addConnection(ConnectionId const connectionId) override; + + bool nodeExists(NodeId const nodeId) const override; + + QVariant nodeData(NodeId nodeId, NodeRole role) const override; + + NodeFlags nodeFlags(NodeId nodeId) const override; + + bool setNodeData(NodeId nodeId, NodeRole role, QVariant value) override; + + QVariant portData(NodeId nodeId, + PortType portType, + PortIndex portIndex, + PortRole role) const override; + + bool setPortData(NodeId nodeId, + PortType portType, + PortIndex portIndex, + QVariant const &value, + PortRole role = PortRole::Data) override; + + bool deleteConnection(ConnectionId const connectionId) override; + + bool deleteNode(NodeId const nodeId) override; + + QJsonObject saveNode(NodeId const) const override; + + QJsonObject save() const override; + + void loadNode(QJsonObject const &nodeJson) override; + + void load(QJsonObject const &json) override; + + /** + * Fetches the NodeDelegateModel for the given `nodeId` and tries to cast the + * stored pointer to the given type + */ + template + NodeDelegateModelType *delegateModel(NodeId const nodeId) + { + auto it = _models.find(nodeId); + if (it == _models.end()) + return nullptr; + + auto model = dynamic_cast(it->second.get()); + + return model; + } + +Q_SIGNALS: + void inPortDataWasSet(NodeId const, PortType const, PortIndex const); + +private: + NodeId newNodeId() override { return _nextNodeId++; } + + void sendConnectionCreation(ConnectionId const connectionId); + + void sendConnectionDeletion(ConnectionId const connectionId); + +private Q_SLOTS: + /** + * Fuction is called in three cases: + * + * - By underlying NodeDelegateModel when a node has new data to propagate. + * @see DataFlowGraphModel::addNode + * - When a new connection is created. + * @see DataFlowGraphModel::addConnection + * - When a node restored from JSON an needs to send data downstream. + * @see DataFlowGraphModel::loadNode + */ + void onOutPortDataUpdated(NodeId const nodeId, PortIndex const portIndex); + + /// Function is called after detaching a connection. + void propagateEmptyDataTo(NodeId const nodeId, PortIndex const portIndex); + +private: + std::shared_ptr _registry; + + NodeId _nextNodeId; + + std::unordered_map> _models; + + std::unordered_set _connectivity; + + mutable std::unordered_map _nodeGeometryData; +}; + +} // namespace QtNodes diff --git a/story-editor/nodeeditor/include/QtNodes/internal/DataFlowGraphicsScene.hpp b/story-editor/nodeeditor/include/QtNodes/internal/DataFlowGraphicsScene.hpp new file mode 100644 index 0000000..25698eb --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/internal/DataFlowGraphicsScene.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include "BasicGraphicsScene.hpp" +#include "DataFlowGraphModel.hpp" +#include "Export.hpp" + +namespace QtNodes { + +/// @brief An advanced scene working with data-propagating graphs. +/** + * The class represents a scene that existed in v2.x but built wit the + * new model-view approach in mind. + */ +class NODE_EDITOR_PUBLIC DataFlowGraphicsScene : public BasicGraphicsScene +{ + Q_OBJECT +public: + DataFlowGraphicsScene(DataFlowGraphModel &graphModel, QObject *parent = nullptr); + + ~DataFlowGraphicsScene() = default; + +public: + std::vector selectedNodes() const; + +public: + QMenu *createSceneMenu(QPointF const scenePos) override; + +public Q_SLOTS: + void save() const; + + void load(); + +Q_SIGNALS: + void sceneLoaded(); + +private: + DataFlowGraphModel &_graphModel; +}; + +} // namespace QtNodes diff --git a/story-editor/nodeeditor/include/QtNodes/internal/DefaultHorizontalNodeGeometry.hpp b/story-editor/nodeeditor/include/QtNodes/internal/DefaultHorizontalNodeGeometry.hpp new file mode 100644 index 0000000..f5f6607 --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/internal/DefaultHorizontalNodeGeometry.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include "AbstractNodeGeometry.hpp" + +#include + +namespace QtNodes { + +class AbstractGraphModel; +class BasicGraphicsScene; + +class NODE_EDITOR_PUBLIC DefaultHorizontalNodeGeometry : public AbstractNodeGeometry +{ +public: + DefaultHorizontalNodeGeometry(AbstractGraphModel &graphModel); + +public: + QSize size(NodeId const nodeId) const override; + + void recomputeSize(NodeId const nodeId) const override; + + QPointF portPosition(NodeId const nodeId, + PortType const portType, + PortIndex const index) const override; + + QPointF portTextPosition(NodeId const nodeId, + PortType const portType, + 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/story-editor/nodeeditor/include/QtNodes/internal/DefaultNodePainter.hpp b/story-editor/nodeeditor/include/QtNodes/internal/DefaultNodePainter.hpp new file mode 100644 index 0000000..484969f --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/internal/DefaultNodePainter.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include + +#include "AbstractNodePainter.hpp" +#include "Definitions.hpp" + +namespace QtNodes { + +class BasicGraphicsScene; +class GraphModel; +class NodeGeometry; +class NodeGraphicsObject; +class NodeState; + +/// @ Lightweight class incapsulating paint code. +class NODE_EDITOR_PUBLIC DefaultNodePainter : public AbstractNodePainter +{ +public: + void paint(QPainter *painter, NodeGraphicsObject &ngo) const override; + + void drawNodeRect(QPainter *painter, NodeGraphicsObject &ngo) const; + + void drawConnectionPoints(QPainter *painter, NodeGraphicsObject &ngo) const; + + void drawFilledConnectionPoints(QPainter *painter, NodeGraphicsObject &ngo) const; + + void drawNodeCaption(QPainter *painter, NodeGraphicsObject &ngo) const; + + void drawEntryLabels(QPainter *painter, NodeGraphicsObject &ngo) const; + + void drawResizeRect(QPainter *painter, NodeGraphicsObject &ngo) const; +}; +} // namespace QtNodes diff --git a/story-editor/nodeeditor/include/QtNodes/internal/DefaultVerticalNodeGeometry.hpp b/story-editor/nodeeditor/include/QtNodes/internal/DefaultVerticalNodeGeometry.hpp new file mode 100644 index 0000000..ae9b628 --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/internal/DefaultVerticalNodeGeometry.hpp @@ -0,0 +1,60 @@ +#pragma once + +#include "AbstractNodeGeometry.hpp" + +#include + +namespace QtNodes { + +class AbstractGraphModel; +class BasicGraphicsScene; + +class NODE_EDITOR_PUBLIC DefaultVerticalNodeGeometry : public AbstractNodeGeometry +{ +public: + DefaultVerticalNodeGeometry(AbstractGraphModel &graphModel); + +public: + QSize size(NodeId const nodeId) const override; + + void recomputeSize(NodeId const nodeId) const override; + + QPointF portPosition(NodeId const nodeId, + PortType const portType, + PortIndex const index) const override; + + QPointF portTextPosition(NodeId const nodeId, + PortType const portType, + 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 + 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/story-editor/nodeeditor/include/QtNodes/internal/Definitions.hpp b/story-editor/nodeeditor/include/QtNodes/internal/Definitions.hpp new file mode 100644 index 0000000..26b94ab --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/internal/Definitions.hpp @@ -0,0 +1,125 @@ +#pragma once + +#include "Export.hpp" + +#include + +#include + +/** + * @file + * Important definitions used throughout the library. + */ + +namespace QtNodes { +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +NODE_EDITOR_PUBLIC Q_NAMESPACE +#else +Q_NAMESPACE_EXPORT(NODE_EDITOR_PUBLIC) +#endif + + /** + * Constants used for fetching QVariant data from GraphModel. + */ + enum class NodeRole { + Type = 0, ///< Type of the current node, usually a string. + Position = 1, ///< `QPointF` positon 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 + InPortCount = 7, ///< `unsigned int` + OutPortCount = 9, ///< `unsigned int` + Widget = 10, ///< Optional `QWidget*` or `nullptr` + Id = 11 ///< Return node ID + }; +Q_ENUM_NS(NodeRole) + +/** + * Specific flags regulating node features and appeaarence. + */ +enum NodeFlag { + NoFlags = 0x0, ///< Default NodeFlag + Resizable = 0x1, ///< Lets the node be resizable + Locked = 0x2 +}; + +Q_DECLARE_FLAGS(NodeFlags, NodeFlag) +Q_FLAG_NS(NodeFlags) +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. +}; +Q_ENUM_NS(PortRole) + +/** + * Defines how many connections are possible to attach to ports. The + * values are fetched using PortRole::ConnectionPolicy. + */ +enum class ConnectionPolicy { + One, ///< Just one connection for each port. + Many, ///< Any number of connections possible for the port. +}; +Q_ENUM_NS(ConnectionPolicy) + +/** + * Used for distinguishing input and output node ports. + */ +enum class PortType { + In = 0, ///< Input node port (from the left). + Out = 1, ///< Output node port (from the right). + None = 2 +}; +Q_ENUM_NS(PortType) + +using PortCount = unsigned int; + +/// ports are consecutively numbered starting from zero. +using PortIndex = unsigned int; + +static constexpr PortIndex InvalidPortIndex = std::numeric_limits::max(); + +/// Unique Id associated with each node in the GraphModel. +using NodeId = unsigned int; + +static constexpr NodeId InvalidNodeId = std::numeric_limits::max(); + +/** + * A unique connection identificator that stores + * out `NodeId`, out `PortIndex`, in `NodeId`, in `PortIndex` + */ +struct ConnectionId +{ + NodeId outNodeId; + PortIndex outPortIndex; + NodeId inNodeId; + PortIndex inPortIndex; +}; + +inline bool operator==(ConnectionId const &a, ConnectionId const &b) +{ + 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) +{ + return !(a == b); +} + +inline void invertConnection(ConnectionId &id) +{ + std::swap(id.outNodeId, id.inNodeId); + std::swap(id.outPortIndex, id.inPortIndex); +} + +} // namespace QtNodes diff --git a/story-editor/nodeeditor/include/QtNodes/internal/Export.hpp b/story-editor/nodeeditor/include/QtNodes/internal/Export.hpp new file mode 100644 index 0000000..41e6e3d --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/internal/Export.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include "Compiler.hpp" +#include "OperatingSystem.hpp" + +#ifdef NODE_EDITOR_PLATFORM_WINDOWS +#define NODE_EDITOR_EXPORT __declspec(dllexport) +#define NODE_EDITOR_IMPORT __declspec(dllimport) +#define NODE_EDITOR_LOCAL +#elif NODE_EDITOR_COMPILER_GNU_VERSION_MAJOR >= 4 || defined(NODE_EDITOR_COMPILER_CLANG) +#define NODE_EDITOR_EXPORT __attribute__((visibility("default"))) +#define NODE_EDITOR_IMPORT __attribute__((visibility("default"))) +#define NODE_EDITOR_LOCAL __attribute__((visibility("hidden"))) +#else +#define NODE_EDITOR_EXPORT +#define NODE_EDITOR_IMPORT +#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 +#else +#define NODE_EDITOR_PUBLIC NODE_EDITOR_IMPORT +#endif +#define NODE_EDITOR_PRIVATE NODE_EDITOR_LOCAL +#elif !defined(NODE_EDITOR_SHARED) && defined(NODE_EDITOR_STATIC) +#define NODE_EDITOR_PUBLIC +#define NODE_EDITOR_PRIVATE +#elif defined(NODE_EDITOR_SHARED) && defined(NODE_EDITOR_STATIC) +#ifdef NODE_EDITOR_EXPORTS +#error "Cannot build as shared and static simultaneously." +#else +#error "Cannot link against shared and static simultaneously." +#endif +#else +#ifdef NODE_EDITOR_EXPORTS +#error "Choose whether to build as shared or static." +#else +#error "Choose whether to link against shared or static." +#endif +#endif diff --git a/story-editor/nodeeditor/include/QtNodes/internal/GraphicsView.hpp b/story-editor/nodeeditor/include/QtNodes/internal/GraphicsView.hpp new file mode 100644 index 0000000..5206812 --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/internal/GraphicsView.hpp @@ -0,0 +1,97 @@ +#pragma once + +#include + +#include "Export.hpp" + +namespace QtNodes { + +class BasicGraphicsScene; + +/** + * @brief A central view able to render objects from `BasicGraphicsScene`. + */ +class NODE_EDITOR_PUBLIC GraphicsView : public QGraphicsView +{ + Q_OBJECT +public: + struct ScaleRange + { + double minimum = 0; + double maximum = 0; + }; + +public: + GraphicsView(QWidget *parent = Q_NULLPTR); + GraphicsView(BasicGraphicsScene *scene, QWidget *parent = Q_NULLPTR); + + GraphicsView(const GraphicsView &) = delete; + GraphicsView operator=(const GraphicsView &) = delete; + + QAction *clearSelectionAction() const; + + QAction *deleteSelectionAction() const; + + void setScene(BasicGraphicsScene *scene); + + void centerScene(); + + /// @brief max=0/min=0 indicates infinite zoom in/out + void setScaleRange(double minimum = 0, double maximum = 0); + + void setScaleRange(ScaleRange range); + + double getScale() const; + +public Q_SLOTS: + void scaleUp(); + + void scaleDown(); + + void setupScale(double scale); + + void onDeleteSelectedObjects(); + + void onDuplicateSelectedObjects(); + + void onCopySelectedObjects(); + + void onPasteObjects(); + +Q_SIGNALS: + void scaleChanged(double scale); + +protected: + void contextMenuEvent(QContextMenuEvent *event) override; + + void wheelEvent(QWheelEvent *event) override; + + void keyPressEvent(QKeyEvent *event) override; + + void keyReleaseEvent(QKeyEvent *event) override; + + void mousePressEvent(QMouseEvent *event) override; + + void mouseMoveEvent(QMouseEvent *event) override; + + void drawBackground(QPainter *painter, const QRectF &r) override; + + void showEvent(QShowEvent *event) override; + +protected: + BasicGraphicsScene *nodeScene(); + + /// Computes scene position for pasting the copied/duplicated node groups. + QPointF scenePastePosition(); + +private: + QAction *_clearSelectionAction = nullptr; + QAction *_deleteSelectionAction = nullptr; + QAction *_duplicateSelectionAction = nullptr; + QAction *_copySelectionAction = nullptr; + QAction *_pasteAction = nullptr; + + QPointF _clickPos; + ScaleRange _scaleRange; +}; +} // namespace QtNodes diff --git a/story-editor/nodeeditor/include/QtNodes/internal/GraphicsViewStyle.hpp b/story-editor/nodeeditor/include/QtNodes/internal/GraphicsViewStyle.hpp new file mode 100644 index 0000000..32f51b9 --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/internal/GraphicsViewStyle.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include + +#include "Export.hpp" +#include "Style.hpp" + +namespace QtNodes { + +class NODE_EDITOR_PUBLIC GraphicsViewStyle : public Style +{ +public: + GraphicsViewStyle(); + + GraphicsViewStyle(QString jsonText); + + ~GraphicsViewStyle() = default; + +public: + static void setStyle(QString jsonText); + +private: + void loadJson(QJsonObject const &json) override; + + QJsonObject toJson() const override; + +public: + QColor BackgroundColor; + QColor FineGridColor; + QColor CoarseGridColor; +}; +} // namespace QtNodes diff --git a/story-editor/nodeeditor/include/QtNodes/internal/NodeConnectionInteraction.hpp b/story-editor/nodeeditor/include/QtNodes/internal/NodeConnectionInteraction.hpp new file mode 100644 index 0000000..aaefedf --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/internal/NodeConnectionInteraction.hpp @@ -0,0 +1,68 @@ +#pragma once + +#include + +#include + +#include "Definitions.hpp" + +namespace QtNodes { + +class ConnectionGraphicsObject; +class NodeGraphicsObject; +class BasicGraphicsScene; + +/// Class wraps conecting and disconnecting checks. +/** + * An instance should be created on the stack and destroyed + * automatically when the operation is completed + */ +class NodeConnectionInteraction +{ +public: + NodeConnectionInteraction(NodeGraphicsObject &ngo, + ConnectionGraphicsObject &cgo, + BasicGraphicsScene &scene); + + /** + * Can connect when following conditions are met: + * 1. Connection 'requires' a port. + * 2. Connection loose end is above the node port. + * 3. Source and target `nodeId`s are different. + * 4. GraphModel permits connection. + */ + bool canConnect(PortIndex *portIndex) const; + + /// Creates a new connectino if possible. + /** + * 1. Check conditions from 'canConnect'. + * 2. Creates new connection with `GraphModel::addConnection`. + * 3. Adjust connection geometry. + */ + bool tryConnect() const; + + /** + * 1. Delete connection with `GraphModel::deleteConnection`. + * 2. Create a "draft" connection with incomplete `ConnectionId`. + * 3. Repaint both previously connected nodes. + */ + bool disconnect(PortType portToDisconnect) const; + +private: + PortType connectionRequiredPort() const; + + QPointF connectionEndScenePosition(PortType) const; + + QPointF nodePortScenePosition(PortType portType, PortIndex portIndex) const; + + PortIndex nodePortIndexUnderScenePoint(PortType portType, QPointF const &p) const; + +private: + NodeGraphicsObject &_ngo; + + ConnectionGraphicsObject &_cgo; + + BasicGraphicsScene &_scene; +}; + +} // namespace QtNodes diff --git a/story-editor/nodeeditor/include/QtNodes/internal/NodeData.hpp b/story-editor/nodeeditor/include/QtNodes/internal/NodeData.hpp new file mode 100644 index 0000000..1ef7480 --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/internal/NodeData.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include + +#include +#include + +#include "Export.hpp" + +namespace QtNodes { + +/** + * `id` represents an internal unique data type for the given port. + * `name` is a normal text description. + */ +struct NODE_EDITOR_PUBLIC NodeDataType +{ + QString id; + QString name; +}; + +/** + * Class represents data transferred between nodes. + * @param type is used for comparing the types + * The actual data is stored in subtypes + */ +class NODE_EDITOR_PUBLIC NodeData +{ +public: + virtual ~NodeData() = default; + + virtual bool sameType(NodeData const &nodeData) const + { + return (this->type().id == nodeData.type().id); + } + + /// Type for inner use + virtual NodeDataType type() const = 0; +}; + +} // namespace QtNodes +Q_DECLARE_METATYPE(QtNodes::NodeDataType) +Q_DECLARE_METATYPE(std::shared_ptr) diff --git a/story-editor/nodeeditor/include/QtNodes/internal/NodeDelegateModel.hpp b/story-editor/nodeeditor/include/QtNodes/internal/NodeDelegateModel.hpp new file mode 100644 index 0000000..6301164 --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/internal/NodeDelegateModel.hpp @@ -0,0 +1,133 @@ +#pragma once + +#include + +#include + +#include "Definitions.hpp" +#include "Export.hpp" +#include "NodeData.hpp" +#include "NodeStyle.hpp" +#include "Serializable.hpp" + +namespace QtNodes { + +class StyleCollection; + +/** + * The class wraps Node-specific data operations and propagates it to + * the nesting DataFlowGraphModel which is a subclass of + * AbstractGraphModel. + * This class is the same what has been called NodeDataModel before v3. + */ +class NODE_EDITOR_PUBLIC NodeDelegateModel : public QObject, public Serializable +{ + Q_OBJECT + +public: + NodeDelegateModel(); + + virtual ~NodeDelegateModel() = default; + + /// It is possible to hide caption in GUI + virtual bool captionVisible() const { return true; } + + /// Caption is used in GUI + virtual QString caption() const = 0; + + /// It is possible to hide port caption in GUI + virtual bool portCaptionVisible(PortType, PortIndex) const { return false; } + + /// Port caption is used in GUI to label individual ports + virtual QString portCaption(PortType, PortIndex) const { return QString(); } + + /// Name makes this model unique + virtual QString name() const = 0; + +public: + QJsonObject save() const override; + + void load(QJsonObject const &) override; + +public: + virtual unsigned int nPorts(PortType portType) const = 0; + + virtual NodeDataType dataType(PortType portType, PortIndex portIndex) const = 0; + +public: + virtual ConnectionPolicy portConnectionPolicy(PortType, PortIndex) const; + + NodeStyle const &nodeStyle() const; + + void setNodeStyle(NodeStyle const &style); + +public: + virtual void setInData(std::shared_ptr nodeData, PortIndex const portIndex) = 0; + + virtual std::shared_ptr outData(PortIndex const port) = 0; + + /** + * It is recommented to preform a lazy initialization for the + * embedded widget and create it inside this function, not in the + * constructor of the current model. + * + * Our Model Registry is able to shortly instantiate models in order + * to call the non-static `Model::name()`. If the embedded widget is + * allocated in the constructor but not actually embedded into some + * QGraphicsProxyWidget, we'll gonna have a dangling pointer. + */ + virtual QWidget *embeddedWidget() = 0; + + virtual bool resizable() const { return false; } + +public Q_SLOTS: + + virtual void inputConnectionCreated(ConnectionId const &) {} + + virtual void inputConnectionDeleted(ConnectionId const &) {} + + virtual void outputConnectionCreated(ConnectionId const &) {} + + virtual void outputConnectionDeleted(ConnectionId const &) {} + +Q_SIGNALS: + + /// Triggers the updates in the nodes downstream. + void dataUpdated(PortIndex const index); + + /// Triggers the propagation of the empty data downstream. + void dataInvalidated(PortIndex const index); + + void computingStarted(); + + void computingFinished(); + + void embeddedWidgetSizeUpdated(); + + /// Call this function before deleting the data associated with ports. + /** + * The function notifies the Graph Model and makes it remove and recompute the + * affected connection addresses. + */ + void portsAboutToBeDeleted(PortType const portType, PortIndex const first, PortIndex const last); + + /// Call this function when data and port moditications are finished. + void portsDeleted(); + + /// Call this function before inserting the data associated with ports. + /** + * The function notifies the Graph Model and makes it recompute the affected + * connection addresses. + */ + void portsAboutToBeInserted(PortType const portType, + PortIndex const first, + PortIndex const last); + + /// Call this function when data and port moditications are finished. + void portsInserted(); + +private: + NodeStyle _nodeStyle; +}; + +} // namespace QtNodes diff --git a/story-editor/nodeeditor/include/QtNodes/internal/NodeDelegateModelRegistry.hpp b/story-editor/nodeeditor/include/QtNodes/internal/NodeDelegateModelRegistry.hpp new file mode 100644 index 0000000..4f230a4 --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/internal/NodeDelegateModelRegistry.hpp @@ -0,0 +1,171 @@ +#pragma once + +#include "Export.hpp" +#include "NodeData.hpp" +#include "NodeDelegateModel.hpp" +#include "QStringStdHash.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace QtNodes { + +/// Class uses map for storing models (name, model) +class NODE_EDITOR_PUBLIC NodeDelegateModelRegistry +{ +public: + using RegistryItemPtr = std::unique_ptr; + using RegistryItemCreator = std::function; + using RegisteredModelCreatorsMap = std::unordered_map; + using RegisteredModelsCategoryMap = std::unordered_map; + using CategoriesSet = std::set; + + //using RegisteredTypeConvertersMap = std::map; + + NodeDelegateModelRegistry() = default; + ~NodeDelegateModelRegistry() = default; + + NodeDelegateModelRegistry(NodeDelegateModelRegistry const &) = delete; + NodeDelegateModelRegistry(NodeDelegateModelRegistry &&) = default; + + NodeDelegateModelRegistry &operator=(NodeDelegateModelRegistry const &) = delete; + + NodeDelegateModelRegistry &operator=(NodeDelegateModelRegistry &&) = default; + +public: + template + void registerModel(RegistryItemCreator creator, QString const &category = "Nodes") + { + QString const name = computeName(HasStaticMethodName{}, creator); + if (!_registeredItemCreators.count(name)) { + _registeredItemCreators[name] = std::move(creator); + _categories.insert(category); + _registeredModelsCategory[name] = category; + } + } + + template + void registerModel(QString const &category = "Nodes") + { + RegistryItemCreator creator = []() { return std::make_unique(); }; + registerModel(std::move(creator), category); + } + +#if 0 + template + void + registerModel(RegistryItemCreator creator, + QString const& category = "Nodes") + { + registerModel(std::move(creator), category); + } + + + template + void + registerModel(ModelCreator&& creator, QString const& category = "Nodes") + { + using ModelType = compute_model_type_t; + registerModel(std::forward(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); + + RegisteredModelCreatorsMap const ®isteredModelCreators() const; + + RegisteredModelsCategoryMap const ®isteredModelsCategoryAssociation() const; + + CategoriesSet const &categories() const; + +#if 0 + TypeConverter + getTypeConverter(NodeDataType const& d1, + NodeDataType const& d2) const; +#endif + +private: + RegisteredModelsCategoryMap _registeredModelsCategory; + + CategoriesSet _categories; + + 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 + // method: `virtual QString name() const;` + template + struct HasStaticMethodName : std::false_type + {}; + + template + struct HasStaticMethodName< + T, + typename std::enable_if::value>::type> + : std::true_type + {}; + + template + static QString computeName(std::true_type, RegistryItemCreator const &) + { + return ModelType::Name(); + } + + template + static QString computeName(std::false_type, RegistryItemCreator const &creator) + { + return creator()->name(); + } + + template + struct UnwrapUniquePtr + { + // Assert always fires, but the compiler doesn't know this: + static_assert(!std::is_same::value, + "The ModelCreator must return a std::unique_ptr, where T " + "inherits from NodeDelegateModel"); + }; + + template + struct UnwrapUniquePtr> + { + static_assert(std::is_base_of::value, + "The ModelCreator must return a std::unique_ptr, where T " + "inherits from NodeDelegateModel"); + using type = T; + }; + + template + using compute_model_type_t = typename UnwrapUniquePtr::type; +}; + +} // namespace QtNodes diff --git a/story-editor/nodeeditor/include/QtNodes/internal/NodeGraphicsObject.hpp b/story-editor/nodeeditor/include/QtNodes/internal/NodeGraphicsObject.hpp new file mode 100644 index 0000000..ef042f7 --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/internal/NodeGraphicsObject.hpp @@ -0,0 +1,91 @@ +#pragma once + +#include +#include + +#include "NodeState.hpp" + +class QGraphicsProxyWidget; + +namespace QtNodes { + +class BasicGraphicsScene; +class AbstractGraphModel; + +class NodeGraphicsObject : public QGraphicsObject +{ + Q_OBJECT +public: + // Needed for qgraphicsitem_cast + enum { Type = UserType + 1 }; + + int type() const override { return Type; } + +public: + NodeGraphicsObject(BasicGraphicsScene &scene, NodeId node); + + ~NodeGraphicsObject() override = default; + +public: + AbstractGraphModel &graphModel() const; + + BasicGraphicsScene *nodeScene() const; + + NodeId nodeId() { return _nodeId; } + + NodeId nodeId() const { return _nodeId; } + + NodeState &nodeState() { return _nodeState; } + + NodeState const &nodeState() const { return _nodeState; } + + QRectF boundingRect() const override; + + void setGeometryChanged(); + + /// Visits all attached connections and corrects + /// their corresponding end points. + void moveConnections() const; + + /// Repaints the node once with reacting ports. + void reactToConnection(ConnectionGraphicsObject const *cgo); + +protected: + void paint(QPainter *painter, + QStyleOptionGraphicsItem const *option, + QWidget *widget = 0) override; + + QVariant itemChange(GraphicsItemChange change, const QVariant &value) override; + + void mousePressEvent(QGraphicsSceneMouseEvent *event) override; + + void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; + + void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; + + void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override; + + void hoverLeaveEvent(QGraphicsSceneHoverEvent *event) override; + + void hoverMoveEvent(QGraphicsSceneHoverEvent *) override; + + void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override; + + void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override; + +private: + void embedQWidget(); + + void setLockedState(); + +private: + NodeId _nodeId; + + AbstractGraphModel &_graphModel; + + NodeState _nodeState; + + // either nullptr or owned by parent QGraphicsItem + QGraphicsProxyWidget *_proxyWidget; +}; +} // namespace QtNodes diff --git a/story-editor/nodeeditor/include/QtNodes/internal/NodeState.hpp b/story-editor/nodeeditor/include/QtNodes/internal/NodeState.hpp new file mode 100644 index 0000000..1894021 --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/internal/NodeState.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include +#include + +#include +#include +#include + +#include "Export.hpp" + +#include "Definitions.hpp" +#include "NodeData.hpp" + +namespace QtNodes { + +class ConnectionGraphicsObject; +class NodeGraphicsObject; + +/// Stores bool for hovering connections and resizing flag. +class NODE_EDITOR_PUBLIC NodeState +{ +public: + NodeState(NodeGraphicsObject &ngo); + +public: + bool hovered() const { return _hovered; } + + void setHovered(bool hovered = true) { _hovered = hovered; } + + void setResizing(bool resizing); + + bool resizing() const; + + ConnectionGraphicsObject const *connectionForReaction() const; + + void storeConnectionForReaction(ConnectionGraphicsObject const *cgo); + + void resetConnectionForReaction(); + +private: + NodeGraphicsObject &_ngo; + + bool _hovered; + + bool _resizing; + + // QPointer tracks the QObject inside and is automatically cleared + // when the object is destroyed. + QPointer _connectionForReaction; +}; +} // namespace QtNodes diff --git a/story-editor/nodeeditor/include/QtNodes/internal/NodeStyle.hpp b/story-editor/nodeeditor/include/QtNodes/internal/NodeStyle.hpp new file mode 100644 index 0000000..5eca749 --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/internal/NodeStyle.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include + +#include "Export.hpp" +#include "Style.hpp" + +namespace QtNodes { + +class NODE_EDITOR_PUBLIC NodeStyle : public Style +{ +public: + NodeStyle(); + + NodeStyle(QString jsonText); + + NodeStyle(QJsonObject const &json); + + virtual ~NodeStyle() = default; + +public: + static void setNodeStyle(QString jsonText); + +public: + void loadJson(QJsonObject const &json) override; + + QJsonObject toJson() const override; + +public: + QColor NormalBoundaryColor; + QColor SelectedBoundaryColor; + QColor GradientColor0; + QColor GradientColor1; + QColor GradientColor2; + QColor GradientColor3; + QColor ShadowColor; + QColor FontColor; + QColor FontColorFaded; + + QColor ConnectionPointColor; + QColor FilledConnectionPointColor; + + QColor WarningColor; + QColor ErrorColor; + + float PenWidth; + float HoveredPenWidth; + + float ConnectionPointDiameter; + + float Opacity; +}; +} // namespace QtNodes diff --git a/story-editor/nodeeditor/include/QtNodes/internal/OperatingSystem.hpp b/story-editor/nodeeditor/include/QtNodes/internal/OperatingSystem.hpp new file mode 100644 index 0000000..8264db4 --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/internal/OperatingSystem.hpp @@ -0,0 +1,49 @@ +#pragma once + +#if defined(__CYGWIN__) || defined(__CYGWIN32__) +#define NODE_EDITOR_PLATFORM "Cygwin" +#define NODE_EDITOR_PLATFORM_CYGWIN +#define NODE_EDITOR_PLATFORM_UNIX +#define NODE_EDITOR_PLATFORM_WINDOWS +#elif defined(_WIN16) || defined(_WIN32) || defined(_WIN64) || defined(__WIN32__) \ + || defined(__TOS_WIN__) || defined(__WINDOWS__) +#define NODE_EDITOR_PLATFORM "Windows" +#define NODE_EDITOR_PLATFORM_WINDOWS +#elif defined(macintosh) || defined(Macintosh) || defined(__TOS_MACOS__) \ + || (defined(__APPLE__) && defined(__MACH__)) +#define NODE_EDITOR_PLATFORM "Mac" +#define NODE_EDITOR_PLATFORM_MAC +#define NODE_EDITOR_PLATFORM_UNIX +#elif defined(linux) || defined(__linux) || defined(__linux__) || defined(__TOS_LINUX__) +#define NODE_EDITOR_PLATFORM "Linux" +#define NODE_EDITOR_PLATFORM_LINUX +#define NODE_EDITOR_PLATFORM_UNIX +#elif defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__bsdi__) \ + || defined(__DragonFly__) +#define NODE_EDITOR_PLATFORM "BSD" +#define NODE_EDITOR_PLATFORM_BSD +#define NODE_EDITOR_PLATFORM_UNIX +#elif defined(sun) || defined(__sun) +#define NODE_EDITOR_PLATFORM "Solaris" +#define NODE_EDITOR_PLATFORM_SOLARIS +#define NODE_EDITOR_PLATFORM_UNIX +#elif defined(_AIX) || defined(__TOS_AIX__) +#define NODE_EDITOR_PLATFORM "AIX" +#define NODE_EDITOR_PLATFORM_AIX +#define NODE_EDITOR_PLATFORM_UNIX +#elif defined(hpux) || defined(_hpux) || defined(__hpux) +#define NODE_EDITOR_PLATFORM "HPUX" +#define NODE_EDITOR_PLATFORM_HPUX +#define NODE_EDITOR_PLATFORM_UNIX +#elif defined(__QNX__) +#define NODE_EDITOR_PLATFORM "QNX" +#define NODE_EDITOR_PLATFORM_QNX +#define NODE_EDITOR_PLATFORM_UNIX +#elif defined(unix) || defined(__unix) || defined(__unix__) +#define NODE_EDITOR_PLATFORM "Unix" +#define NODE_EDITOR_PLATFORM_UNIX +#endif + +#ifndef NODE_EDITOR_PLATFORM +#error "Current platform is not supported." +#endif diff --git a/story-editor/nodeeditor/include/QtNodes/internal/QStringStdHash.hpp b/story-editor/nodeeditor/include/QtNodes/internal/QStringStdHash.hpp new file mode 100644 index 0000000..617d9b5 --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/internal/QStringStdHash.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include + +#if (QT_VERSION < QT_VERSION_CHECK(5, 14, 0)) + +// As of 5.14 there is a specialization std::hash + +#include + +#include +#include + +namespace std { +template<> +struct hash +{ + inline std::size_t operator()(QString const &s) const { return qHash(s); } +}; +} // namespace std + +#endif diff --git a/story-editor/nodeeditor/include/QtNodes/internal/QUuidStdHash.hpp b/story-editor/nodeeditor/include/QtNodes/internal/QUuidStdHash.hpp new file mode 100644 index 0000000..224bc9f --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/internal/QUuidStdHash.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include + +#include +#include + +namespace std { +template<> +struct hash +{ + inline std::size_t operator()(QUuid const &uid) const { return qHash(uid); } +}; +} // namespace std diff --git a/story-editor/nodeeditor/include/QtNodes/internal/Serializable.hpp b/story-editor/nodeeditor/include/QtNodes/internal/Serializable.hpp new file mode 100644 index 0000000..71137d9 --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/internal/Serializable.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include + +namespace QtNodes { + +class Serializable +{ +public: + virtual ~Serializable() = default; + + virtual QJsonObject save() const { return {}; } + + virtual void load(QJsonObject const & /*p*/) {} +}; +} // namespace QtNodes diff --git a/story-editor/nodeeditor/include/QtNodes/internal/Style.hpp b/story-editor/nodeeditor/include/QtNodes/internal/Style.hpp new file mode 100644 index 0000000..f878083 --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/internal/Style.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace QtNodes { + +class Style // : public QObject +{ + //Q_OBJECT + +public: + virtual ~Style() = default; + +public: + virtual void loadJson(QJsonObject const &json) = 0; + + virtual QJsonObject toJson() const = 0; + + /// Loads from utf-8 byte array. + virtual void loadJsonFromByteArray(QByteArray const &byteArray) + { + auto json = QJsonDocument::fromJson(byteArray).object(); + + loadJson(json); + } + + virtual void loadJsonText(QString jsonText) { loadJsonFromByteArray(jsonText.toUtf8()); } + + virtual void loadJsonFile(QString fileName) + { + QFile file(fileName); + + if (!file.open(QIODevice::ReadOnly)) { + qWarning() << "Couldn't open file " << fileName; + + return; + } + + loadJsonFromByteArray(file.readAll()); + } +}; + +} // namespace QtNodes diff --git a/story-editor/nodeeditor/include/QtNodes/internal/StyleCollection.hpp b/story-editor/nodeeditor/include/QtNodes/internal/StyleCollection.hpp new file mode 100644 index 0000000..92a1a40 --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/internal/StyleCollection.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include "Export.hpp" + +#include "ConnectionStyle.hpp" +#include "GraphicsViewStyle.hpp" +#include "NodeStyle.hpp" + +namespace QtNodes { + +class NODE_EDITOR_PUBLIC StyleCollection +{ +public: + static NodeStyle const &nodeStyle(); + + static ConnectionStyle const &connectionStyle(); + + static GraphicsViewStyle const &flowViewStyle(); + +public: + static void setNodeStyle(NodeStyle); + + static void setConnectionStyle(ConnectionStyle); + + static void setGraphicsViewStyle(GraphicsViewStyle); + +private: + StyleCollection() = default; + + StyleCollection(StyleCollection const &) = delete; + + StyleCollection &operator=(StyleCollection const &) = delete; + + static StyleCollection &instance(); + +private: + NodeStyle _nodeStyle; + + ConnectionStyle _connectionStyle; + + GraphicsViewStyle _flowViewStyle; +}; +} // namespace QtNodes diff --git a/story-editor/nodeeditor/include/QtNodes/internal/UndoCommands.hpp b/story-editor/nodeeditor/include/QtNodes/internal/UndoCommands.hpp new file mode 100644 index 0000000..2202617 --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/internal/UndoCommands.hpp @@ -0,0 +1,123 @@ +#pragma once + +#include "Definitions.hpp" + +#include +#include +#include + +#include + +namespace QtNodes { + +class BasicGraphicsScene; + +class CreateCommand : public QUndoCommand +{ +public: + CreateCommand(BasicGraphicsScene *scene, QString const name, QPointF const &mouseScenePos); + + void undo() override; + void redo() override; + +private: + BasicGraphicsScene *_scene; + NodeId _nodeId; + QJsonObject _sceneJson; +}; + +/** + * Selected scene objects are serialized and then removed from the scene. + * The deleted elements could be restored in `undo`. + */ +class DeleteCommand : public QUndoCommand +{ +public: + DeleteCommand(BasicGraphicsScene *scene); + + void undo() override; + void redo() override; + +private: + BasicGraphicsScene *_scene; + QJsonObject _sceneJson; +}; + +class CopyCommand : public QUndoCommand +{ +public: + CopyCommand(BasicGraphicsScene *scene); +}; + +class PasteCommand : public QUndoCommand +{ +public: + PasteCommand(BasicGraphicsScene *scene, QPointF const &mouseScenePos); + + void undo() override; + void redo() override; + +private: + QJsonObject takeSceneJsonFromClipboard(); + QJsonObject makeNewNodeIdsInScene(QJsonObject const &sceneJson); + +private: + BasicGraphicsScene *_scene; + QPointF const &_mouseScenePos; + QJsonObject _newSceneJson; +}; + +class DisconnectCommand : public QUndoCommand +{ +public: + DisconnectCommand(BasicGraphicsScene *scene, ConnectionId const); + + void undo() override; + void redo() override; + +private: + BasicGraphicsScene *_scene; + + ConnectionId _connId; +}; + +class ConnectCommand : public QUndoCommand +{ +public: + ConnectCommand(BasicGraphicsScene *scene, ConnectionId const); + + void undo() override; + void redo() override; + +private: + BasicGraphicsScene *_scene; + + ConnectionId _connId; +}; + +class MoveNodeCommand : public QUndoCommand +{ +public: + MoveNodeCommand(BasicGraphicsScene *scene, QPointF const &diff); + + void undo() override; + void redo() override; + + /** + * A command ID is used in command compression. It must be an integer unique to + * this command's class, or -1 if the command doesn't support compression. + */ + int id() const override; + + /** + * Several sequential movements could be merged into one command. + */ + bool mergeWith(QUndoCommand const *c) override; + +private: + BasicGraphicsScene *_scene; + std::unordered_set _selectedNodes; + QPointF _diff; +}; + +} // namespace QtNodes diff --git a/story-editor/nodeeditor/include/QtNodes/internal/locateNode.hpp b/story-editor/nodeeditor/include/QtNodes/internal/locateNode.hpp new file mode 100644 index 0000000..cf41f83 --- /dev/null +++ b/story-editor/nodeeditor/include/QtNodes/internal/locateNode.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include +#include + +class QGraphicsScene; + +namespace QtNodes { + +class NodeGraphicsObject; + +NodeGraphicsObject *locateNodeAt(QPointF scenePoint, + QGraphicsScene &scene, + QTransform const &viewTransform); + +} // namespace QtNodes diff --git a/story-editor/nodeeditor/resources/DefaultStyle.json b/story-editor/nodeeditor/resources/DefaultStyle.json new file mode 100644 index 0000000..da8dfe8 --- /dev/null +++ b/story-editor/nodeeditor/resources/DefaultStyle.json @@ -0,0 +1,42 @@ +{ + "GraphicsViewStyle": { + "BackgroundColor": [53, 53, 53], + "FineGridColor": [60, 60, 60], + "CoarseGridColor": [25, 25, 25] + }, + "NodeStyle": { + "NormalBoundaryColor": [255, 255, 255], + "SelectedBoundaryColor": [255, 165, 0], + "GradientColor0": "gray", + "GradientColor1": [80, 80, 80], + "GradientColor2": [64, 64, 64], + "GradientColor3": [58, 58, 58], + "ShadowColor": [20, 20, 20], + "FontColor" : "white", + "FontColorFaded" : "gray", + "ConnectionPointColor": [169, 169, 169], + "FilledConnectionPointColor": "cyan", + "ErrorColor": "red", + "WarningColor": [128, 128, 0], + + "PenWidth": 1.0, + "HoveredPenWidth": 1.5, + + "ConnectionPointDiameter": 8.0, + + "Opacity": 0.8 + }, + "ConnectionStyle": { + "ConstructionColor": "gray", + "NormalColor": "darkcyan", + "SelectedColor": [100, 100, 100], + "SelectedHaloColor": "orange", + "HoveredColor": "lightcyan", + + "LineWidth": 3.0, + "ConstructionLineWidth": 2.0, + "PointDiameter": 10.0, + + "UseDataDefinedColors": false + } +} diff --git a/story-editor/nodeeditor/resources/resources.qrc b/story-editor/nodeeditor/resources/resources.qrc new file mode 100644 index 0000000..a0b5ef8 --- /dev/null +++ b/story-editor/nodeeditor/resources/resources.qrc @@ -0,0 +1,5 @@ + + + DefaultStyle.json + + diff --git a/story-editor/nodeeditor/src/AbstractGraphModel.cpp b/story-editor/nodeeditor/src/AbstractGraphModel.cpp new file mode 100644 index 0000000..10709b7 --- /dev/null +++ b/story-editor/nodeeditor/src/AbstractGraphModel.cpp @@ -0,0 +1,105 @@ +#include "AbstractGraphModel.hpp" + +#include + +namespace QtNodes { + +void AbstractGraphModel::portsAboutToBeDeleted(NodeId const nodeId, + PortType const portType, + PortIndex const first, + PortIndex const last) +{ + _shiftedByDynamicPortsConnections.clear(); + + auto portCountRole = portType == PortType::In ? NodeRole::InPortCount : NodeRole::OutPortCount; + + unsigned int portCount = nodeData(nodeId, portCountRole).toUInt(); + + if (first > portCount - 1) + return; + + if (last < first) + return; + + auto clampedLast = std::min(last, portCount - 1); + + for (PortIndex portIndex = first; portIndex <= clampedLast; ++portIndex) { + std::unordered_set conns = connections(nodeId, portType, portIndex); + + for (auto connectionId : conns) { + deleteConnection(connectionId); + } + } + + std::size_t const nRemovedPorts = clampedLast - first + 1; + + for (PortIndex portIndex = clampedLast + 1; portIndex < portCount; ++portIndex) { + std::unordered_set conns = connections(nodeId, portType, portIndex); + + for (auto connectionId : conns) { + // Erases the information about the port on one side; + auto c = makeIncompleteConnectionId(connectionId, portType); + + c = makeCompleteConnectionId(c, nodeId, portIndex - nRemovedPorts); + + _shiftedByDynamicPortsConnections.push_back(c); + + deleteConnection(connectionId); + } + } +} + +void AbstractGraphModel::portsDeleted() +{ + for (auto const connectionId : _shiftedByDynamicPortsConnections) { + addConnection(connectionId); + } + + _shiftedByDynamicPortsConnections.clear(); +} + +void AbstractGraphModel::portsAboutToBeInserted(NodeId const nodeId, + PortType const portType, + PortIndex const first, + PortIndex const last) +{ + _shiftedByDynamicPortsConnections.clear(); + + auto portCountRole = portType == PortType::In ? NodeRole::InPortCount : NodeRole::OutPortCount; + + unsigned int portCount = nodeData(nodeId, portCountRole).toUInt(); + + if (first > portCount) + return; + + if (last < first) + return; + + std::size_t const nNewPorts = last - first + 1; + + for (PortIndex portIndex = first; portIndex < portCount; ++portIndex) { + std::unordered_set conns = connections(nodeId, portType, portIndex); + + for (auto connectionId : conns) { + // Erases the information about the port on one side; + auto c = makeIncompleteConnectionId(connectionId, portType); + + c = makeCompleteConnectionId(c, nodeId, portIndex + nNewPorts); + + _shiftedByDynamicPortsConnections.push_back(c); + + deleteConnection(connectionId); + } + } +} + +void AbstractGraphModel::portsInserted() +{ + for (auto const connectionId : _shiftedByDynamicPortsConnections) { + addConnection(connectionId); + } + + _shiftedByDynamicPortsConnections.clear(); +} + +} // namespace QtNodes diff --git a/story-editor/nodeeditor/src/AbstractNodeGeometry.cpp b/story-editor/nodeeditor/src/AbstractNodeGeometry.cpp new file mode 100644 index 0000000..08c8181 --- /dev/null +++ b/story-editor/nodeeditor/src/AbstractNodeGeometry.cpp @@ -0,0 +1,81 @@ +#include "AbstractNodeGeometry.hpp" + +#include "AbstractGraphModel.hpp" +#include "StyleCollection.hpp" + +#include + +#include + +namespace QtNodes { + +AbstractNodeGeometry::AbstractNodeGeometry(AbstractGraphModel &graphModel, double marginsRatio) + : _graphModel(graphModel) + , _marginsRatio(marginsRatio) +{ + // +} + +QRectF AbstractNodeGeometry::boundingRect(NodeId const nodeId) const +{ + QSize s = size(nodeId); + + int widthMargin = s.width() * _marginsRatio; + int heightMargin = s.height() * _marginsRatio; + + QMargins margins(widthMargin, heightMargin, widthMargin, heightMargin); + + QRectF r(QPointF(0, 0), s); + + return r.marginsAdded(margins); +} + +void AbstractNodeGeometry::setMarginsRatio(double marginsRatio) +{ + _marginsRatio = marginsRatio; +} + +QPointF AbstractNodeGeometry::portScenePosition(NodeId const nodeId, + PortType const portType, + PortIndex const index, + QTransform const &t) const +{ + QPointF result = portPosition(nodeId, portType, index); + + return t.map(result); +} + +PortIndex AbstractNodeGeometry::checkPortHit(NodeId const nodeId, + PortType const portType, + QPointF const nodePoint) const +{ + auto const &nodeStyle = StyleCollection::nodeStyle(); + + PortIndex result = InvalidPortIndex; + + if (portType == PortType::None) + return result; + + double const tolerance = 2.0 * nodeStyle.ConnectionPointDiameter; + + size_t const n = _graphModel.nodeData(nodeId, + (portType == PortType::Out) + ? NodeRole::OutPortCount + : NodeRole::InPortCount); + + for (unsigned int portIndex = 0; portIndex < n; ++portIndex) { + auto pp = portPosition(nodeId, portType, portIndex); + + QPointF p = pp - nodePoint; + auto distance = std::sqrt(QPointF::dotProduct(p, p)); + + if (distance < tolerance) { + result = portIndex; + break; + } + } + + return result; +} + +} // namespace QtNodes diff --git a/story-editor/nodeeditor/src/BasicGraphicsScene.cpp b/story-editor/nodeeditor/src/BasicGraphicsScene.cpp new file mode 100644 index 0000000..1c36716 --- /dev/null +++ b/story-editor/nodeeditor/src/BasicGraphicsScene.cpp @@ -0,0 +1,308 @@ +#include "BasicGraphicsScene.hpp" + +#include "AbstractNodeGeometry.hpp" +#include "ConnectionGraphicsObject.hpp" +#include "ConnectionIdUtils.hpp" +#include "DefaultHorizontalNodeGeometry.hpp" +#include "DefaultNodePainter.hpp" +#include "DefaultVerticalNodeGeometry.hpp" +#include "GraphicsView.hpp" +#include "NodeGraphicsObject.hpp" + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace QtNodes { + +BasicGraphicsScene::BasicGraphicsScene(AbstractGraphModel &graphModel, QObject *parent) + : QGraphicsScene(parent) + , _graphModel(graphModel) + , _nodeGeometry(std::make_unique(_graphModel)) + , _nodePainter(std::make_unique()) + , _nodeDrag(false) + , _undoStack(new QUndoStack(this)) + , _orientation(Qt::Horizontal) +{ + setItemIndexMethod(QGraphicsScene::NoIndex); + + connect(&_graphModel, + &AbstractGraphModel::connectionCreated, + this, + &BasicGraphicsScene::onConnectionCreated); + + connect(&_graphModel, + &AbstractGraphModel::connectionDeleted, + this, + &BasicGraphicsScene::onConnectionDeleted); + + connect(&_graphModel, + &AbstractGraphModel::nodeCreated, + this, + &BasicGraphicsScene::onNodeCreated); + + connect(&_graphModel, + &AbstractGraphModel::nodeDeleted, + this, + &BasicGraphicsScene::onNodeDeleted); + + connect(&_graphModel, + &AbstractGraphModel::nodePositionUpdated, + this, + &BasicGraphicsScene::onNodePositionUpdated); + + connect(&_graphModel, + &AbstractGraphModel::nodeUpdated, + this, + &BasicGraphicsScene::onNodeUpdated); + + connect(this, &BasicGraphicsScene::nodeClicked, this, &BasicGraphicsScene::onNodeClicked); + + connect(&_graphModel, &AbstractGraphModel::modelReset, this, &BasicGraphicsScene::onModelReset); + + traverseGraphAndPopulateGraphicsObjects(); +} + +BasicGraphicsScene::~BasicGraphicsScene() = default; + +AbstractGraphModel const &BasicGraphicsScene::graphModel() const +{ + return _graphModel; +} + +AbstractGraphModel &BasicGraphicsScene::graphModel() +{ + return _graphModel; +} + +AbstractNodeGeometry &BasicGraphicsScene::nodeGeometry() +{ + return *_nodeGeometry; +} + +AbstractNodePainter &BasicGraphicsScene::nodePainter() +{ + return *_nodePainter; +} + +void BasicGraphicsScene::setNodePainter(std::unique_ptr newPainter) +{ + _nodePainter = std::move(newPainter); +} + +QUndoStack &BasicGraphicsScene::undoStack() +{ + return *_undoStack; +} + +void BasicGraphicsScene::setDropShadowEffect(bool enable) +{ + _dropShadowEffect = enable; +} + +bool BasicGraphicsScene::isDropShadowEffectEnabled() const +{ + return _dropShadowEffect; +} + +std::unique_ptr const &BasicGraphicsScene::makeDraftConnection( + ConnectionId const incompleteConnectionId) +{ + _draftConnection = std::make_unique(*this, incompleteConnectionId); + + _draftConnection->grabMouse(); + + return _draftConnection; +} + +void BasicGraphicsScene::resetDraftConnection() +{ + _draftConnection.reset(); +} + +void BasicGraphicsScene::clearScene() +{ + auto const &allNodeIds = graphModel().allNodeIds(); + + for (auto nodeId : allNodeIds) { + graphModel().deleteNode(nodeId); + } +} + +NodeGraphicsObject *BasicGraphicsScene::nodeGraphicsObject(NodeId nodeId) +{ + NodeGraphicsObject *ngo = nullptr; + auto it = _nodeGraphicsObjects.find(nodeId); + if (it != _nodeGraphicsObjects.end()) { + ngo = it->second.get(); + } + + return ngo; +} + +ConnectionGraphicsObject *BasicGraphicsScene::connectionGraphicsObject(ConnectionId connectionId) +{ + ConnectionGraphicsObject *cgo = nullptr; + auto it = _connectionGraphicsObjects.find(connectionId); + if (it != _connectionGraphicsObjects.end()) { + cgo = it->second.get(); + } + + return cgo; +} + +void BasicGraphicsScene::setOrientation(Qt::Orientation const orientation) +{ + if (_orientation != orientation) { + _orientation = orientation; + + switch (_orientation) { + case Qt::Horizontal: + _nodeGeometry = std::make_unique(_graphModel); + break; + + case Qt::Vertical: + _nodeGeometry = std::make_unique(_graphModel); + break; + } + + onModelReset(); + } +} + +QMenu *BasicGraphicsScene::createSceneMenu(QPointF const scenePos) +{ + Q_UNUSED(scenePos); + return nullptr; +} + +void BasicGraphicsScene::traverseGraphAndPopulateGraphicsObjects() +{ + auto allNodeIds = _graphModel.allNodeIds(); + + // First create all the nodes. + for (NodeId const nodeId : allNodeIds) { + _nodeGraphicsObjects[nodeId] = std::make_unique(*this, nodeId); + } + + // Then for each node check output connections and insert them. + for (NodeId const nodeId : allNodeIds) { + unsigned int nOutPorts = _graphModel.nodeData(nodeId, NodeRole::OutPortCount); + + for (PortIndex index = 0; index < nOutPorts; ++index) { + auto const &outConnectionIds = _graphModel.connections(nodeId, PortType::Out, index); + + for (auto cid : outConnectionIds) { + _connectionGraphicsObjects[cid] = std::make_unique(*this, + cid); + } + } + } +} + +void BasicGraphicsScene::updateAttachedNodes(ConnectionId const connectionId, + PortType const portType) +{ + auto node = nodeGraphicsObject(getNodeId(portType, connectionId)); + + if (node) { + node->update(); + } +} + +void BasicGraphicsScene::onConnectionDeleted(ConnectionId const connectionId) +{ + auto it = _connectionGraphicsObjects.find(connectionId); + if (it != _connectionGraphicsObjects.end()) { + _connectionGraphicsObjects.erase(it); + } + + // TODO: do we need it? + if (_draftConnection && _draftConnection->connectionId() == connectionId) { + _draftConnection.reset(); + } + + updateAttachedNodes(connectionId, PortType::Out); + updateAttachedNodes(connectionId, PortType::In); +} + +void BasicGraphicsScene::onConnectionCreated(ConnectionId const connectionId) +{ + _connectionGraphicsObjects[connectionId] + = std::make_unique(*this, connectionId); + + updateAttachedNodes(connectionId, PortType::Out); + updateAttachedNodes(connectionId, PortType::In); +} + +void BasicGraphicsScene::onNodeDeleted(NodeId const nodeId) +{ + auto it = _nodeGraphicsObjects.find(nodeId); + if (it != _nodeGraphicsObjects.end()) { + _nodeGraphicsObjects.erase(it); + } +} + +void BasicGraphicsScene::onNodeCreated(NodeId const nodeId) +{ + _nodeGraphicsObjects[nodeId] = std::make_unique(*this, nodeId); +} + +void BasicGraphicsScene::onNodePositionUpdated(NodeId const nodeId) +{ + auto node = nodeGraphicsObject(nodeId); + if (node) { + node->setPos(_graphModel.nodeData(nodeId, NodeRole::Position).value()); + node->update(); + _nodeDrag = true; + } +} + +void BasicGraphicsScene::onNodeUpdated(NodeId const nodeId) +{ + auto node = nodeGraphicsObject(nodeId); + + if (node) { + node->setGeometryChanged(); + + _nodeGeometry->recomputeSize(nodeId); + + node->update(); + node->moveConnections(); + } +} + +void BasicGraphicsScene::onNodeClicked(NodeId const nodeId) +{ + if (_nodeDrag) + Q_EMIT nodeMoved(nodeId, _graphModel.nodeData(nodeId, NodeRole::Position).value()); + _nodeDrag = false; +} + +void BasicGraphicsScene::onModelReset() +{ + _connectionGraphicsObjects.clear(); + _nodeGraphicsObjects.clear(); + + clear(); + + traverseGraphAndPopulateGraphicsObjects(); +} + +} // namespace QtNodes diff --git a/story-editor/nodeeditor/src/ConnectionGraphicsObject.cpp b/story-editor/nodeeditor/src/ConnectionGraphicsObject.cpp new file mode 100644 index 0000000..6a871c0 --- /dev/null +++ b/story-editor/nodeeditor/src/ConnectionGraphicsObject.cpp @@ -0,0 +1,380 @@ +#include "ConnectionGraphicsObject.hpp" + +#include "AbstractGraphModel.hpp" +#include "AbstractNodeGeometry.hpp" +#include "BasicGraphicsScene.hpp" +#include "ConnectionIdUtils.hpp" +#include "ConnectionPainter.hpp" +#include "ConnectionState.hpp" +#include "ConnectionStyle.hpp" +#include "NodeConnectionInteraction.hpp" +#include "NodeGraphicsObject.hpp" +#include "StyleCollection.hpp" +#include "locateNode.hpp" + +#include +#include +#include +#include +#include + +#include + +#include + +namespace QtNodes { + +ConnectionGraphicsObject::ConnectionGraphicsObject(BasicGraphicsScene &scene, + ConnectionId const connectionId) + : _connectionId(connectionId) + , _graphModel(scene.graphModel()) + , _connectionState(*this) + , _out{0, 0} + , _in{0, 0} +{ + scene.addItem(this); + + setFlag(QGraphicsItem::ItemIsMovable, true); + setFlag(QGraphicsItem::ItemIsFocusable, true); + setFlag(QGraphicsItem::ItemIsSelectable, true); + + setAcceptHoverEvents(true); + + //addGraphicsEffect(); + + setZValue(-1.0); + + initializePosition(); +} + +void ConnectionGraphicsObject::initializePosition() +{ + // This function is only called when the ConnectionGraphicsObject + // is newly created. At this moment both end coordinates are (0, 0) + // in Connection G.O. coordinates. The position of the whole + // Connection G. O. in scene coordinate system is also (0, 0). + // By moving the whole object to the Node Port position + // we position both connection ends correctly. + + if (_connectionState.requiredPort() != PortType::None) { + PortType attachedPort = oppositePort(_connectionState.requiredPort()); + + PortIndex portIndex = getPortIndex(attachedPort, _connectionId); + NodeId nodeId = getNodeId(attachedPort, _connectionId); + + NodeGraphicsObject *ngo = nodeScene()->nodeGraphicsObject(nodeId); + + if (ngo) { + QTransform nodeSceneTransform = ngo->sceneTransform(); + + AbstractNodeGeometry &geometry = nodeScene()->nodeGeometry(); + + QPointF pos = geometry.portScenePosition(nodeId, + attachedPort, + portIndex, + nodeSceneTransform); + + this->setPos(pos); + } + } + + move(); +} + +AbstractGraphModel &ConnectionGraphicsObject::graphModel() const +{ + return _graphModel; +} + +BasicGraphicsScene *ConnectionGraphicsObject::nodeScene() const +{ + return dynamic_cast(scene()); +} + +ConnectionId const &ConnectionGraphicsObject::connectionId() const +{ + return _connectionId; +} + +QRectF ConnectionGraphicsObject::boundingRect() const +{ + auto points = pointsC1C2(); + + // `normalized()` fixes inverted rects. + QRectF basicRect = QRectF(_out, _in).normalized(); + + QRectF c1c2Rect = QRectF(points.first, points.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); + + return commonRect; +} + +QPainterPath ConnectionGraphicsObject::shape() const +{ +#ifdef DEBUG_DRAWING + + //QPainterPath path; + + //path.addRect(boundingRect()); + //return path; + +#else + return ConnectionPainter::getPainterStroke(*this); +#endif +} + +QPointF const &ConnectionGraphicsObject::endPoint(PortType portType) const +{ + Q_ASSERT(portType != PortType::None); + + return (portType == PortType::Out ? _out : _in); +} + +void ConnectionGraphicsObject::setEndPoint(PortType portType, QPointF const &point) +{ + if (portType == PortType::In) + _in = point; + else + _out = point; +} + +void ConnectionGraphicsObject::move() +{ + auto moveEnd = [this](ConnectionId cId, PortType portType) { + NodeId nodeId = getNodeId(portType, cId); + + if (nodeId == InvalidNodeId) + return; + + NodeGraphicsObject *ngo = nodeScene()->nodeGraphicsObject(nodeId); + + if (ngo) { + AbstractNodeGeometry &geometry = nodeScene()->nodeGeometry(); + + QPointF scenePos = geometry.portScenePosition(nodeId, + portType, + getPortIndex(portType, cId), + ngo->sceneTransform()); + + QPointF connectionPos = sceneTransform().inverted().map(scenePos); + + setEndPoint(portType, connectionPos); + } + }; + + moveEnd(_connectionId, PortType::Out); + moveEnd(_connectionId, PortType::In); + + prepareGeometryChange(); + + update(); +} + +ConnectionState const &ConnectionGraphicsObject::connectionState() const +{ + return _connectionState; +} + +ConnectionState &ConnectionGraphicsObject::connectionState() +{ + return _connectionState; +} + +void ConnectionGraphicsObject::paint(QPainter *painter, + QStyleOptionGraphicsItem const *option, + QWidget *) +{ + if (!scene()) + return; + + painter->setClipRect(option->exposedRect); + + ConnectionPainter::paint(painter, *this); +} + +void ConnectionGraphicsObject::mousePressEvent(QGraphicsSceneMouseEvent *event) +{ + QGraphicsItem::mousePressEvent(event); +} + +void ConnectionGraphicsObject::mouseMoveEvent(QGraphicsSceneMouseEvent *event) +{ + prepareGeometryChange(); + + auto view = static_cast(event->widget()); + auto ngo = locateNodeAt(event->scenePos(), *nodeScene(), view->transform()); + if (ngo) { + ngo->reactToConnection(this); + + _connectionState.setLastHoveredNode(ngo->nodeId()); + } else { + _connectionState.resetLastHoveredNode(); + } + + //------------------- + + auto requiredPort = _connectionState.requiredPort(); + + if (requiredPort != PortType::None) { + setEndPoint(requiredPort, event->pos()); + } + + //------------------- + + update(); + + event->accept(); +} + +void ConnectionGraphicsObject::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) +{ + QGraphicsItem::mouseReleaseEvent(event); + + ungrabMouse(); + event->accept(); + + auto view = static_cast(event->widget()); + + Q_ASSERT(view); + + auto ngo = locateNodeAt(event->scenePos(), *nodeScene(), view->transform()); + + bool wasConnected = false; + + if (ngo) { + NodeConnectionInteraction interaction(*ngo, *this, *nodeScene()); + + wasConnected = interaction.tryConnect(); + } + + // If connection attempt was unsuccessful + if (!wasConnected) { + // Resulting unique_ptr is not used and automatically deleted. + nodeScene()->resetDraftConnection(); + } +} + +void ConnectionGraphicsObject::hoverEnterEvent(QGraphicsSceneHoverEvent *event) +{ + _connectionState.setHovered(true); + + update(); + + // Signal + nodeScene()->connectionHovered(connectionId(), event->screenPos()); + + event->accept(); +} + +void ConnectionGraphicsObject::hoverLeaveEvent(QGraphicsSceneHoverEvent *event) +{ + _connectionState.setHovered(false); + + update(); + + // Signal + nodeScene()->connectionHoverLeft(connectionId()); + + event->accept(); +} + +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)); +} + +std::pair ConnectionGraphicsObject::pointsC1C2Horizontal() 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; + + double yDistance = _in.y() - _out.y(); + + double verticalOffset = qMin(defaultOffset, std::abs(yDistance)); + + double horizontalOffset = 0; + + double ratioY = 0.5; + + if (yDistance <= 0) { + double xDistance = _in.x() - _out.x() + 20; + + double vector = xDistance < 0 ? -1.0 : 1.0; + + horizontalOffset = qMin(defaultOffset, std::abs(xDistance)) * vector; + + ratioY = 1.0; + } + + verticalOffset *= ratioY; + + QPointF c1(_out.x() + horizontalOffset, _out.y() + verticalOffset); + + QPointF c2(_in.x() - horizontalOffset, _in.y() - verticalOffset); + + return std::make_pair(c1, c2); +} + +} // namespace QtNodes diff --git a/story-editor/nodeeditor/src/ConnectionPainter.cpp b/story-editor/nodeeditor/src/ConnectionPainter.cpp new file mode 100644 index 0000000..97002ef --- /dev/null +++ b/story-editor/nodeeditor/src/ConnectionPainter.cpp @@ -0,0 +1,254 @@ +#include "ConnectionPainter.hpp" + +#include + +#include "AbstractGraphModel.hpp" +#include "ConnectionGraphicsObject.hpp" +#include "ConnectionState.hpp" +#include "Definitions.hpp" +#include "NodeData.hpp" +#include "StyleCollection.hpp" + +namespace QtNodes { + +static QPainterPath cubicPath(ConnectionGraphicsObject const &connection) +{ + QPointF const &in = connection.endPoint(PortType::In); + QPointF const &out = connection.endPoint(PortType::Out); + + auto const c1c2 = connection.pointsC1C2(); + + // cubic spline + QPainterPath cubic(out); + + cubic.cubicTo(c1c2.first, c1c2.second, in); + + return cubic; +} + +QPainterPath ConnectionPainter::getPainterStroke(ConnectionGraphicsObject const &connection) +{ + auto cubic = cubicPath(connection); + + QPointF const &out = connection.endPoint(PortType::Out); + QPainterPath result(out); + + unsigned 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); +} + +#ifdef NODE_DEBUG_DRAWING +static void debugDrawing(QPainter *painter, ConnectionGraphicsObject const &cgo) +{ + Q_UNUSED(painter); + + { + QPointF const &in = cgo.endPoint(PortType::In); + QPointF const &out = cgo.endPoint(PortType::Out); + + auto const points = cgo.pointsC1C2(); + + painter->setPen(Qt::red); + painter->setBrush(Qt::red); + + painter->drawLine(QLineF(out, points.first)); + painter->drawLine(QLineF(points.first, points.second)); + painter->drawLine(QLineF(points.second, in)); + painter->drawEllipse(points.first, 3, 3); + painter->drawEllipse(points.second, 3, 3); + + painter->setBrush(Qt::NoBrush); + painter->drawPath(cubicPath(cgo)); + } + + { + painter->setPen(Qt::yellow); + painter->drawRect(cgo.boundingRect()); + } +} + +#endif + +static void drawSketchLine(QPainter *painter, ConnectionGraphicsObject const &cgo) +{ + ConnectionState const &state = cgo.connectionState(); + + if (state.requiresPort()) { + auto const &connectionStyle = QtNodes::StyleCollection::connectionStyle(); + + QPen pen; + pen.setWidth(connectionStyle.constructionLineWidth()); + pen.setColor(connectionStyle.constructionColor()); + pen.setStyle(Qt::DashLine); + + painter->setPen(pen); + painter->setBrush(Qt::NoBrush); + + auto cubic = cubicPath(cgo); + + // cubic spline + painter->drawPath(cubic); + } +} + +static void drawHoveredOrSelected(QPainter *painter, ConnectionGraphicsObject const &cgo) +{ + bool const hovered = cgo.connectionState().hovered(); + bool const selected = cgo.isSelected(); + + // drawn as a fat background + if (hovered || selected) { + auto const &connectionStyle = QtNodes::StyleCollection::connectionStyle(); + + double const lineWidth = connectionStyle.lineWidth(); + + QPen pen; + pen.setWidth(2 * lineWidth); + pen.setColor(selected ? connectionStyle.selectedHaloColor() + : connectionStyle.hoveredColor()); + + painter->setPen(pen); + painter->setBrush(Qt::NoBrush); + + // cubic spline + auto const cubic = cubicPath(cgo); + painter->drawPath(cubic); + } +} + +static void drawNormalLine(QPainter *painter, ConnectionGraphicsObject const &cgo) +{ + ConnectionState const &state = cgo.connectionState(); + + if (state.requiresPort()) + return; + + // colors + + auto const &connectionStyle = QtNodes::StyleCollection::connectionStyle(); + + QColor normalColorOut = connectionStyle.normalColor(); + QColor normalColorIn = connectionStyle.normalColor(); + QColor selectedColor = connectionStyle.selectedColor(); + + bool useGradientColor = false; + + AbstractGraphModel const &graphModel = cgo.graphModel(); + + if (connectionStyle.useDataDefinedColors()) { + using QtNodes::PortType; + + auto const cId = cgo.connectionId(); + + auto dataTypeOut = graphModel + .portData(cId.outNodeId, + PortType::Out, + cId.outPortIndex, + PortRole::DataType) + .value(); + + auto dataTypeIn + = graphModel.portData(cId.inNodeId, PortType::In, cId.inPortIndex, PortRole::DataType) + .value(); + + useGradientColor = (dataTypeOut.id != dataTypeIn.id); + + normalColorOut = connectionStyle.normalColor(dataTypeOut.id); + normalColorIn = connectionStyle.normalColor(dataTypeIn.id); + selectedColor = normalColorOut.darker(200); + } + + // geometry + + double const lineWidth = connectionStyle.lineWidth(); + + // draw normal line + QPen p; + + p.setWidth(lineWidth); + + bool const selected = cgo.isSelected(); + + auto cubic = cubicPath(cgo); + if (useGradientColor) { + painter->setBrush(Qt::NoBrush); + + QColor cOut = normalColorOut; + if (selected) + cOut = cOut.darker(200); + p.setColor(cOut); + painter->setPen(p); + + unsigned int const 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) { + QColor cIn = normalColorIn; + if (selected) + cIn = cIn.darker(200); + + p.setColor(cIn); + painter->setPen(p); + } + painter->drawLine(cubic.pointAtPercent(ratioPrev), cubic.pointAtPercent(ratio)); + } + + { + 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); + } + } else { + p.setColor(normalColorOut); + + if (selected) { + p.setColor(selectedColor); + } + + painter->setPen(p); + painter->setBrush(Qt::NoBrush); + + painter->drawPath(cubic); + } +} + +void ConnectionPainter::paint(QPainter *painter, ConnectionGraphicsObject const &cgo) +{ + drawHoveredOrSelected(painter, cgo); + + drawSketchLine(painter, cgo); + + drawNormalLine(painter, cgo); + +#ifdef NODE_DEBUG_DRAWING + debugDrawing(painter, cgo); +#endif + + // draw end points + auto const &connectionStyle = QtNodes::StyleCollection::connectionStyle(); + + double const pointDiameter = connectionStyle.pointDiameter(); + + painter->setPen(connectionStyle.constructionColor()); + painter->setBrush(connectionStyle.constructionColor()); + double const pointRadius = pointDiameter / 2.0; + painter->drawEllipse(cgo.out(), pointRadius, pointRadius); + painter->drawEllipse(cgo.in(), pointRadius, pointRadius); +} + +} // namespace QtNodes diff --git a/story-editor/nodeeditor/src/ConnectionState.cpp b/story-editor/nodeeditor/src/ConnectionState.cpp new file mode 100644 index 0000000..52ec9b5 --- /dev/null +++ b/story-editor/nodeeditor/src/ConnectionState.cpp @@ -0,0 +1,66 @@ +#include "ConnectionState.hpp" + +#include +#include + +#include "BasicGraphicsScene.hpp" +#include "ConnectionGraphicsObject.hpp" +#include "NodeGraphicsObject.hpp" + +namespace QtNodes { + +ConnectionState::~ConnectionState() +{ + //resetLastHoveredNode(); +} + +PortType ConnectionState::requiredPort() const +{ + PortType t = PortType::None; + + if (_cgo.connectionId().outNodeId == InvalidNodeId) { + t = PortType::Out; + } else if (_cgo.connectionId().inNodeId == InvalidNodeId) { + t = PortType::In; + } + + return t; +} + +bool ConnectionState::requiresPort() const +{ + const ConnectionId &id = _cgo.connectionId(); + return id.outNodeId == InvalidNodeId || id.inNodeId == InvalidNodeId; +} + +bool ConnectionState::hovered() const +{ + return _hovered; +} + +void ConnectionState::setHovered(bool hovered) +{ + _hovered = hovered; +} + +void ConnectionState::setLastHoveredNode(NodeId const nodeId) +{ + _lastHoveredNode = nodeId; +} + +NodeId ConnectionState::lastHoveredNode() const +{ + return _lastHoveredNode; +} + +void ConnectionState::resetLastHoveredNode() +{ + if (_lastHoveredNode != InvalidNodeId) { + auto ngo = _cgo.nodeScene()->nodeGraphicsObject(_lastHoveredNode); + ngo->update(); + } + + _lastHoveredNode = InvalidNodeId; +} + +} // namespace QtNodes diff --git a/story-editor/nodeeditor/src/ConnectionStyle.cpp b/story-editor/nodeeditor/src/ConnectionStyle.cpp new file mode 100644 index 0000000..8126589 --- /dev/null +++ b/story-editor/nodeeditor/src/ConnectionStyle.cpp @@ -0,0 +1,205 @@ +#include "ConnectionStyle.hpp" + +#include "StyleCollection.hpp" + +#include +#include +#include + +#include + +#include + +using QtNodes::ConnectionStyle; + +inline void initResources() +{ + Q_INIT_RESOURCE(resources); +} + +ConnectionStyle::ConnectionStyle() +{ + // Explicit resources inialization for preventing the static initialization + // order fiasco: https://isocpp.org/wiki/faq/ctors#static-init-order + initResources(); + + // This configuration is stored inside the compiled unit and is loaded statically + loadJsonFile(":DefaultStyle.json"); +} + +ConnectionStyle::ConnectionStyle(QString jsonText) +{ + loadJsonFile(":DefaultStyle.json"); + loadJsonText(jsonText); +} + +void ConnectionStyle::setConnectionStyle(QString jsonText) +{ + ConnectionStyle style(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(); + + 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); + + CONNECTION_STYLE_READ_FLOAT(obj, LineWidth); + CONNECTION_STYLE_READ_FLOAT(obj, ConstructionLineWidth); + CONNECTION_STYLE_READ_FLOAT(obj, PointDiameter); + + CONNECTION_STYLE_READ_BOOL(obj, 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); + + CONNECTION_STYLE_WRITE_FLOAT(obj, LineWidth); + CONNECTION_STYLE_WRITE_FLOAT(obj, ConstructionLineWidth); + CONNECTION_STYLE_WRITE_FLOAT(obj, PointDiameter); + + CONNECTION_STYLE_WRITE_BOOL(obj, UseDataDefinedColors); + + QJsonObject root; + root["ConnectionStyle"] = obj; + + return root; +} + +QColor ConnectionStyle::constructionColor() const +{ + return ConstructionColor; +} + +QColor ConnectionStyle::normalColor() const +{ + return NormalColor; +} + +QColor ConnectionStyle::normalColor(QString typeId) const +{ + std::size_t hash = qHash(typeId); + + std::size_t const hue_range = 0xFF; + + std::mt19937 gen(static_cast(hash)); + std::uniform_int_distribution distrib(0, hue_range); + + int hue = distrib(gen); + int sat = 120 + hash % 129; + + return QColor::fromHsl(hue, sat, 160); +} + +QColor ConnectionStyle::selectedColor() const +{ + return SelectedColor; +} + +QColor ConnectionStyle::selectedHaloColor() const +{ + return SelectedHaloColor; +} + +QColor ConnectionStyle::hoveredColor() const +{ + return HoveredColor; +} + +float ConnectionStyle::lineWidth() const +{ + return LineWidth; +} + +float ConnectionStyle::constructionLineWidth() const +{ + return ConstructionLineWidth; +} + +float ConnectionStyle::pointDiameter() const +{ + return PointDiameter; +} + +bool ConnectionStyle::useDataDefinedColors() const +{ + return UseDataDefinedColors; +} diff --git a/story-editor/nodeeditor/src/DataFlowGraphModel.cpp b/story-editor/nodeeditor/src/DataFlowGraphModel.cpp new file mode 100644 index 0000000..fdf46e8 --- /dev/null +++ b/story-editor/nodeeditor/src/DataFlowGraphModel.cpp @@ -0,0 +1,536 @@ +#include "DataFlowGraphModel.hpp" +#include "ConnectionIdHash.hpp" + +#include + +#include + +namespace QtNodes { + +DataFlowGraphModel::DataFlowGraphModel(std::shared_ptr registry) + : _registry(std::move(registry)) + , _nextNodeId{0} +{} + +std::unordered_set DataFlowGraphModel::allNodeIds() const +{ + std::unordered_set nodeIds; + for_each(_models.begin(), _models.end(), [&nodeIds](auto const &p) { nodeIds.insert(p.first); }); + + return nodeIds; +} + +std::unordered_set DataFlowGraphModel::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; +} + +std::unordered_set DataFlowGraphModel::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; +} + +bool DataFlowGraphModel::connectionExists(ConnectionId const connectionId) const +{ + return (_connectivity.find(connectionId) != _connectivity.end()); +} + +NodeId DataFlowGraphModel::addNode(QString const nodeType) +{ + std::unique_ptr model = _registry->create(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); + + _models[newId] = std::move(model); + + Q_EMIT nodeCreated(newId); + + return newId; + } + + return InvalidNodeId; +} + +bool DataFlowGraphModel::connectionPossible(ConnectionId const connectionId) const +{ + auto getDataType = [&](PortType const portType) { + return portData(getNodeId(portType, connectionId), + portType, + getPortIndex(portType, connectionId), + PortRole::DataType) + .value(); + }; + + 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 policy = portData(nodeId, portType, portIndex, PortRole::ConnectionPolicyRole) + .value(); + + return connected.empty() || (policy == ConnectionPolicy::Many); + }; + + return getDataType(PortType::Out).id == getDataType(PortType::In).id + && portVacant(PortType::Out) && portVacant(PortType::In); +} + +void DataFlowGraphModel::addConnection(ConnectionId const connectionId) +{ + _connectivity.insert(connectionId); + + sendConnectionCreation(connectionId); + + QVariant const portDataToPropagate = portData(connectionId.outNodeId, + PortType::Out, + connectionId.outPortIndex, + PortRole::Data); + + setPortData(connectionId.inNodeId, + PortType::In, + connectionId.inPortIndex, + portDataToPropagate, + PortRole::Data); +} + +void DataFlowGraphModel::sendConnectionCreation(ConnectionId const connectionId) +{ + Q_EMIT connectionCreated(connectionId); + + auto iti = _models.find(connectionId.inNodeId); + auto ito = _models.find(connectionId.outNodeId); + if (iti != _models.end() && ito != _models.end()) { + auto &modeli = iti->second; + auto &modelo = ito->second; + modeli->inputConnectionCreated(connectionId); + modelo->outputConnectionCreated(connectionId); + } +} + +void DataFlowGraphModel::sendConnectionDeletion(ConnectionId const connectionId) +{ + Q_EMIT connectionDeleted(connectionId); + + auto iti = _models.find(connectionId.inNodeId); + auto ito = _models.find(connectionId.outNodeId); + if (iti != _models.end() && ito != _models.end()) { + auto &modeli = iti->second; + auto &modelo = ito->second; + modeli->inputConnectionDeleted(connectionId); + modelo->outputConnectionDeleted(connectionId); + } +} + +bool DataFlowGraphModel::nodeExists(NodeId const nodeId) const +{ + return (_models.find(nodeId) != _models.end()); +} + +QVariant DataFlowGraphModel::nodeData(NodeId nodeId, NodeRole role) const +{ + QVariant result; + + auto it = _models.find(nodeId); + if (it == _models.end()) + return result; + + auto &model = it->second; + + switch (role) { + case NodeRole::Id: + break; + case NodeRole::Type: + result = model->name(); + break; + + case NodeRole::Position: + result = _nodeGeometryData[nodeId].pos; + break; + + case NodeRole::Size: + result = _nodeGeometryData[nodeId].size; + break; + + case NodeRole::CaptionVisible: + result = model->captionVisible(); + break; + + case NodeRole::Caption: + result = model->caption(); + break; + + case NodeRole::Style: { + auto style = StyleCollection::nodeStyle(); + result = style.toJson().toVariantMap(); + } break; + + case NodeRole::InternalData: { + QJsonObject nodeJson; + + nodeJson["internal-data"] = _models.at(nodeId)->save(); + + result = nodeJson.toVariantMap(); + break; + } + + case NodeRole::InPortCount: + result = model->nPorts(PortType::In); + break; + + case NodeRole::OutPortCount: + result = model->nPorts(PortType::Out); + break; + + case NodeRole::Widget: { + auto w = model->embeddedWidget(); + result = QVariant::fromValue(w); + } break; + } + + return result; +} + +NodeFlags DataFlowGraphModel::nodeFlags(NodeId nodeId) const +{ + auto it = _models.find(nodeId); + + if (it != _models.end() && it->second->resizable()) + return NodeFlag::Resizable; + + return NodeFlag::NoFlags; +} + +bool DataFlowGraphModel::setNodeData(NodeId nodeId, NodeRole role, QVariant value) +{ + Q_UNUSED(nodeId); + Q_UNUSED(role); + Q_UNUSED(value); + + bool result = false; + + switch (role) { + case NodeRole::Id: + break; + case NodeRole::Type: + break; + case NodeRole::Position: { + _nodeGeometryData[nodeId].pos = value.value(); + + Q_EMIT nodePositionUpdated(nodeId); + + result = true; + } break; + + case NodeRole::Size: { + _nodeGeometryData[nodeId].size = value.value(); + result = true; + } break; + + case NodeRole::CaptionVisible: + break; + + case NodeRole::Caption: + break; + + case NodeRole::Style: + break; + + case NodeRole::InternalData: + break; + + case NodeRole::InPortCount: + break; + + case NodeRole::OutPortCount: + break; + + case NodeRole::Widget: + break; + } + + return result; +} + +QVariant DataFlowGraphModel::portData(NodeId nodeId, + PortType portType, + PortIndex portIndex, + PortRole role) const +{ + QVariant result; + + auto it = _models.find(nodeId); + if (it == _models.end()) + return result; + + auto &model = it->second; + + switch (role) { + case PortRole::Data: + if (portType == PortType::Out) + result = QVariant::fromValue(model->outData(portIndex)); + break; + + case PortRole::DataType: + result = QVariant::fromValue(model->dataType(portType, portIndex)); + break; + + case PortRole::ConnectionPolicyRole: + result = QVariant::fromValue(model->portConnectionPolicy(portType, portIndex)); + break; + + case PortRole::CaptionVisible: + result = model->portCaptionVisible(portType, portIndex); + break; + + case PortRole::Caption: + result = model->portCaption(portType, portIndex); + + break; + } + + return result; +} + +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; + + auto &model = it->second; + + switch (role) { + case PortRole::Data: + if (portType == PortType::In) { + model->setInData(value.value>(), portIndex); + + // Triggers repainting on the scene. + Q_EMIT inPortDataWasSet(nodeId, portType, portIndex); + } + break; + + default: + break; + } + + return false; +} + +bool DataFlowGraphModel::deleteConnection(ConnectionId const connectionId) +{ + bool disconnected = false; + + auto it = _connectivity.find(connectionId); + + if (it != _connectivity.end()) { + disconnected = true; + + _connectivity.erase(it); + } + + if (disconnected) { + sendConnectionDeletion(connectionId); + + propagateEmptyDataTo(getNodeId(PortType::In, connectionId), + getPortIndex(PortType::In, connectionId)); + } + + return disconnected; +} + +bool DataFlowGraphModel::deleteNode(NodeId const nodeId) +{ + // Delete connections to this node first. + auto connectionIds = allConnectionIds(nodeId); + for (auto &cId : connectionIds) { + deleteConnection(cId); + } + + _nodeGeometryData.erase(nodeId); + _models.erase(nodeId); + + Q_EMIT nodeDeleted(nodeId); + + return true; +} + +QJsonObject DataFlowGraphModel::saveNode(NodeId const nodeId) const +{ + QJsonObject nodeJson; + + nodeJson["id"] = static_cast(nodeId); + + nodeJson["internal-data"] = _models.at(nodeId)->save(); + + { + QPointF const pos = nodeData(nodeId, NodeRole::Position).value(); + + QJsonObject posJson; + posJson["x"] = pos.x(); + posJson["y"] = pos.y(); + nodeJson["position"] = posJson; + } + + return nodeJson; +} + +QJsonObject DataFlowGraphModel::save() const +{ + QJsonObject sceneJson; + + QJsonArray nodesJsonArray; + for (auto const nodeId : allNodeIds()) { + nodesJsonArray.append(saveNode(nodeId)); + } + sceneJson["nodes"] = nodesJsonArray; + + QJsonArray connJsonArray; + for (auto const &cid : _connectivity) { + connJsonArray.append(toJson(cid)); + } + sceneJson["connections"] = connJsonArray; + + return sceneJson; +} + +void DataFlowGraphModel::loadNode(QJsonObject const &nodeJson) +{ + // Possibility of the id clash when reading it from json and not generating a + // new value. + // 1. When restoring a scene from a file. + // Conflict is not possible because the scene must be cleared by the time of + // 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(); + + _nextNodeId = std::max(_nextNodeId, restoredNodeId + 1); + + QJsonObject const internalDataJson = nodeJson["internal-data"].toObject(); + + QString delegateModelName = internalDataJson["model-name"].toString(); + + std::unique_ptr model = _registry->create(delegateModelName); + + if (model) { + connect(model.get(), + &NodeDelegateModel::dataUpdated, + [restoredNodeId, this](PortIndex const portIndex) { + onOutPortDataUpdated(restoredNodeId, portIndex); + }); + + _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); + } else { + throw std::logic_error(std::string("No registered model with name ") + + delegateModelName.toLocal8Bit().data()); + } +} + +void DataFlowGraphModel::load(QJsonObject const &jsonDocument) +{ + QJsonArray nodesJsonArray = jsonDocument["nodes"].toArray(); + + for (QJsonValueRef nodeJson : nodesJsonArray) { + loadNode(nodeJson.toObject()); + } + + QJsonArray connectionJsonArray = jsonDocument["connections"].toArray(); + + for (QJsonValueRef connection : connectionJsonArray) { + QJsonObject connJson = connection.toObject(); + + ConnectionId connId = fromJson(connJson); + + // Restore the connection + addConnection(connId); + } +} + +void DataFlowGraphModel::onOutPortDataUpdated(NodeId const nodeId, PortIndex const portIndex) +{ + std::unordered_set const &connected = connections(nodeId, + PortType::Out, + portIndex); + + QVariant const portDataToPropagate = portData(nodeId, PortType::Out, portIndex, PortRole::Data); + + for (auto const &cn : connected) { + setPortData(cn.inNodeId, PortType::In, cn.inPortIndex, portDataToPropagate, PortRole::Data); + } +} + +void DataFlowGraphModel::propagateEmptyDataTo(NodeId const nodeId, PortIndex const portIndex) +{ + QVariant emptyData{}; + + setPortData(nodeId, PortType::In, portIndex, emptyData, PortRole::Data); +} + +} // namespace QtNodes diff --git a/story-editor/nodeeditor/src/DataFlowGraphicsScene.cpp b/story-editor/nodeeditor/src/DataFlowGraphicsScene.cpp new file mode 100644 index 0000000..506f722 --- /dev/null +++ b/story-editor/nodeeditor/src/DataFlowGraphicsScene.cpp @@ -0,0 +1,189 @@ +#include "DataFlowGraphicsScene.hpp" + +#include "ConnectionGraphicsObject.hpp" +#include "GraphicsView.hpp" +#include "NodeDelegateModelRegistry.hpp" +#include "NodeGraphicsObject.hpp" +#include "UndoCommands.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace QtNodes { + +DataFlowGraphicsScene::DataFlowGraphicsScene(DataFlowGraphModel &graphModel, QObject *parent) + : BasicGraphicsScene(graphModel, parent) + , _graphModel(graphModel) +{ + connect(&_graphModel, + &DataFlowGraphModel::inPortDataWasSet, + [this](NodeId const nodeId, PortType const, PortIndex const) { onNodeUpdated(nodeId); }); +} + +// TODO constructor for an empyt scene? + +std::vector DataFlowGraphicsScene::selectedNodes() const +{ + QList graphicsItems = selectedItems(); + + std::vector result; + result.reserve(graphicsItems.size()); + + for (QGraphicsItem *item : graphicsItems) { + auto ngo = qgraphicsitem_cast(item); + + if (ngo != nullptr) { + result.push_back(ngo->nodeId()); + } + } + + return result; +} + +QMenu *DataFlowGraphicsScene::createSceneMenu(QPointF const scenePos) +{ + QMenu *modelMenu = new QMenu(); + + // Add filterbox to the context menu + auto *txtBox = new QLineEdit(modelMenu); + txtBox->setPlaceholderText(QStringLiteral("Filter")); + txtBox->setClearButtonEnabled(true); + + auto *txtBoxAction = new QWidgetAction(modelMenu); + txtBoxAction->setDefaultWidget(txtBox); + + // 1. + modelMenu->addAction(txtBoxAction); + + // Add result treeview to the context menu + QTreeWidget *treeView = new QTreeWidget(modelMenu); + treeView->header()->close(); + + auto *treeViewAction = new QWidgetAction(modelMenu); + treeViewAction->setDefaultWidget(treeView); + + // 2. + modelMenu->addAction(treeViewAction); + + auto registry = _graphModel.dataModelRegistry(); + + for (auto const &cat : registry->categories()) { + auto item = new QTreeWidgetItem(treeView); + item->setText(0, cat); + item->setFlags(item->flags() & ~Qt::ItemIsSelectable); + } + + for (auto const &assoc : registry->registeredModelsCategoryAssociation()) { + QList parent = treeView->findItems(assoc.second, Qt::MatchExactly); + + if (parent.count() <= 0) + continue; + + auto item = new QTreeWidgetItem(parent.first()); + item->setText(0, assoc.first); + } + + treeView->expandAll(); + + connect(treeView, + &QTreeWidget::itemClicked, + [this, modelMenu, scenePos](QTreeWidgetItem *item, int) { + if (!(item->flags() & (Qt::ItemIsSelectable))) { + return; + } + + this->undoStack().push(new CreateCommand(this, item->text(0), scenePos)); + + modelMenu->close(); + }); + + //Setup filtering + connect(txtBox, &QLineEdit::textChanged, [treeView](const QString &text) { + QTreeWidgetItemIterator categoryIt(treeView, QTreeWidgetItemIterator::HasChildren); + while (*categoryIt) + (*categoryIt++)->setHidden(true); + QTreeWidgetItemIterator it(treeView, QTreeWidgetItemIterator::NoChildren); + while (*it) { + auto modelName = (*it)->text(0); + const bool match = (modelName.contains(text, Qt::CaseInsensitive)); + (*it)->setHidden(!match); + if (match) { + QTreeWidgetItem *parent = (*it)->parent(); + while (parent) { + parent->setHidden(false); + parent = parent->parent(); + } + } + ++it; + } + }); + + // make sure the text box gets focus so the user doesn't have to click on it + txtBox->setFocus(); + + // QMenu's instance auto-destruction + modelMenu->setAttribute(Qt::WA_DeleteOnClose); + + return modelMenu; +} + +void DataFlowGraphicsScene::save() const +{ + QString fileName = QFileDialog::getSaveFileName(nullptr, + tr("Open Flow Scene"), + QDir::homePath(), + tr("Flow Scene Files (*.flow)")); + + if (!fileName.isEmpty()) { + if (!fileName.endsWith("flow", Qt::CaseInsensitive)) + fileName += ".flow"; + + QFile file(fileName); + if (file.open(QIODevice::WriteOnly)) { + file.write(QJsonDocument(_graphModel.save()).toJson()); + } + } +} + +void DataFlowGraphicsScene::load() +{ + QString fileName = QFileDialog::getOpenFileName(nullptr, + tr("Open Flow Scene"), + QDir::homePath(), + tr("Flow Scene Files (*.flow)")); + + if (!QFileInfo::exists(fileName)) + return; + + QFile file(fileName); + + if (!file.open(QIODevice::ReadOnly)) + return; + + clearScene(); + + QByteArray const wholeFile = file.readAll(); + + _graphModel.load(QJsonDocument::fromJson(wholeFile).object()); + + Q_EMIT sceneLoaded(); +} + +} // namespace QtNodes diff --git a/story-editor/nodeeditor/src/DefaultHorizontalNodeGeometry.cpp b/story-editor/nodeeditor/src/DefaultHorizontalNodeGeometry.cpp new file mode 100644 index 0000000..cf1e80d --- /dev/null +++ b/story-editor/nodeeditor/src/DefaultHorizontalNodeGeometry.cpp @@ -0,0 +1,239 @@ +#include "DefaultHorizontalNodeGeometry.hpp" + +#include "AbstractGraphModel.hpp" +#include "NodeData.hpp" + +#include +#include +#include + +namespace QtNodes { + +DefaultHorizontalNodeGeometry::DefaultHorizontalNodeGeometry(AbstractGraphModel &graphModel) + : AbstractNodeGeometry(graphModel) + , _portSize(20) + , _portSpasing(10) + , _fontMetrics(QFont()) + , _boldFontMetrics(QFont()) +{ + QFont f({ "Arial", 10 }); + f.setBold(true); + _boldFontMetrics = QFontMetrics(f); + + _portSize = _fontMetrics.height(); +} + +QSize DefaultHorizontalNodeGeometry::size(NodeId const nodeId) const +{ + return _graphModel.nodeData(nodeId, NodeRole::Size); +} + +void DefaultHorizontalNodeGeometry::recomputeSize(NodeId const nodeId) const +{ + unsigned int height = maxVerticalPortsExtent(nodeId); + + if (auto w = _graphModel.nodeData(nodeId, NodeRole::Widget)) { + height = std::max(height, static_cast(w->height())); + } + + QRectF const capRect = captionRect(nodeId); + + height += capRect.height(); + + height += _portSpasing; // space above caption + height += _portSpasing; // space below caption + + unsigned int inPortWidth = maxPortsTextAdvance(nodeId, PortType::In); + unsigned int outPortWidth = maxPortsTextAdvance(nodeId, PortType::Out); + + unsigned int width = inPortWidth + outPortWidth + 4 * _portSpasing; + + if (auto w = _graphModel.nodeData(nodeId, NodeRole::Widget)) { + width += w->width(); + } + + width = std::max(width, static_cast(capRect.width()) + 2 * _portSpasing); + + QSize size(width, height); + + _graphModel.setNodeData(nodeId, NodeRole::Size, size); +} + +QPointF DefaultHorizontalNodeGeometry::portPosition(NodeId const nodeId, + PortType const portType, + PortIndex const portIndex) const +{ + unsigned int const step = _portSize + _portSpasing; + + QPointF result; + + double totalHeight = 15.0; + + totalHeight += captionRect(nodeId).height(); + totalHeight += _portSpasing; + + totalHeight += step * portIndex; + totalHeight += step / 2.0; + + QSize size = _graphModel.nodeData(nodeId, NodeRole::Size); + + switch (portType) { + case PortType::In: { + double x = 0.0; + + result = QPointF(x, totalHeight); + break; + } + + case PortType::Out: { + double x = size.width(); + + result = QPointF(x, totalHeight); + break; + } + + default: + break; + } + + return result; +} + +QPointF DefaultHorizontalNodeGeometry::portTextPosition(NodeId const nodeId, + PortType const portType, + PortIndex const portIndex) const +{ + QPointF p = portPosition(nodeId, portType, portIndex); + + QRectF rect = portTextRect(nodeId, portType, portIndex); + + p.setY(p.y() + rect.height() / 4.0); + + QSize size = _graphModel.nodeData(nodeId, NodeRole::Size); + + switch (portType) { + case PortType::In: + p.setX(_portSpasing); + break; + + case PortType::Out: + p.setX(size.width() - _portSpasing - rect.width()); + break; + + default: + break; + } + + 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()); +} + +QPointF DefaultHorizontalNodeGeometry::widgetPosition(NodeId const nodeId) const +{ + QSize size = _graphModel.nodeData(nodeId, NodeRole::Size); + + unsigned int captionHeight = captionRect(nodeId).height(); + + if (auto w = _graphModel.nodeData(nodeId, NodeRole::Widget)) { + // 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), + captionHeight); + } else { + return QPointF(2.0 * _portSpasing + maxPortsTextAdvance(nodeId, PortType::In), + (captionHeight + size.height() - w->height()) / 2.0); + } + } + return QPointF(); +} + +QRect DefaultHorizontalNodeGeometry::resizeHandleRect(NodeId const nodeId) const +{ + QSize size = _graphModel.nodeData(nodeId, NodeRole::Size); + + 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); +} + +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; +} + +} // namespace QtNodes diff --git a/story-editor/nodeeditor/src/DefaultNodePainter.cpp b/story-editor/nodeeditor/src/DefaultNodePainter.cpp new file mode 100644 index 0000000..afe0cc2 --- /dev/null +++ b/story-editor/nodeeditor/src/DefaultNodePainter.cpp @@ -0,0 +1,284 @@ +#include "DefaultNodePainter.hpp" + +#include + +#include + +#include "AbstractGraphModel.hpp" +#include "AbstractNodeGeometry.hpp" +#include "BasicGraphicsScene.hpp" +#include "ConnectionGraphicsObject.hpp" +#include "ConnectionIdUtils.hpp" +#include "NodeGraphicsObject.hpp" +#include "NodeState.hpp" +#include "StyleCollection.hpp" + +namespace QtNodes { + +void DefaultNodePainter::paint(QPainter *painter, NodeGraphicsObject &ngo) const +{ + // TODO? + //AbstractNodeGeometry & geometry = ngo.nodeScene()->nodeGeometry(); + //geometry.recomputeSizeIfFontChanged(painter->font()); + + drawNodeRect(painter, ngo); + + drawConnectionPoints(painter, ngo); + + drawFilledConnectionPoints(painter, ngo); + + drawNodeCaption(painter, ngo); + + drawEntryLabels(painter, ngo); + + drawResizeRect(painter, ngo); +} + +void DefaultNodePainter::drawNodeRect(QPainter *painter, NodeGraphicsObject &ngo) const +{ + AbstractGraphModel &model = ngo.graphModel(); + + NodeId const nodeId = ngo.nodeId(); + + AbstractNodeGeometry &geometry = ngo.nodeScene()->nodeGeometry(); + + QSize size = geometry.size(nodeId); + + QJsonDocument json = QJsonDocument::fromVariant(model.nodeData(nodeId, NodeRole::Style)); + + NodeStyle nodeStyle(json.object()); + + auto color = ngo.isSelected() ? nodeStyle.SelectedBoundaryColor : nodeStyle.NormalBoundaryColor; + +// if (ngo.nodeState().hovered()) { +// QPen p(color, nodeStyle.HoveredPenWidth); +// painter->setPen(p); +// } else { +// QPen p(color, nodeStyle.PenWidth); +// painter->setPen(p); +// } + QPen pen = painter->pen(); + pen.setBrush(color); + pen.setWidth(2); + painter->setPen(pen); + painter->setBrush(nodeStyle.GradientColor0); + + QRectF boundary(0, 0, size.width(), size.height()); + painter->drawRect(boundary); +} + +void DefaultNodePainter::drawConnectionPoints(QPainter *painter, NodeGraphicsObject &ngo) 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; + auto reducedDiameter = diameter * 0.6; + + for (PortType portType : {PortType::Out, PortType::In}) { + size_t const n = model + .nodeData(nodeId, + (portType == PortType::Out) ? NodeRole::OutPortCount + : NodeRole::InPortCount) + .toUInt(); + + for (PortIndex portIndex = 0; portIndex < n; ++portIndex) { + QPointF p = geometry.portPosition(nodeId, portType, portIndex); + + auto const &dataType = model.portData(nodeId, portType, portIndex, PortRole::DataType) + .value(); + + double r = 1.0; + + NodeState const &state = ngo.nodeState(); + + if (auto const *cgo = state.connectionForReaction()) { + PortType requiredPort = cgo->connectionState().requiredPort(); + + if (requiredPort == portType) { + ConnectionId possibleConnectionId = makeCompleteConnectionId(cgo->connectionId(), + nodeId, + portIndex); + + bool const possible = model.connectionPossible(possibleConnectionId); + + auto cp = cgo->sceneTransform().map(cgo->endPoint(requiredPort)); + cp = ngo.sceneTransform().inverted().map(cp); + + auto diff = cp - p; + double dist = std::sqrt(QPointF::dotProduct(diff, diff)); + + if (possible) { + double const thres = 40.0; + r = (dist < thres) ? (2.0 - dist / thres) : 1.0; + } else { + double const thres = 80.0; + r = (dist < thres) ? (dist / thres) : 1.0; + } + } + } + + if (connectionStyle.useDataDefinedColors()) { + painter->setBrush(connectionStyle.normalColor(dataType.id)); + } else { + painter->setBrush(nodeStyle.ConnectionPointColor); + } + + painter->drawEllipse(p, reducedDiameter * r, reducedDiameter * r); + } + } + + if (ngo.nodeState().connectionForReaction()) { + ngo.nodeState().resetConnectionForReaction(); + } +} + +void DefaultNodePainter::drawFilledConnectionPoints(QPainter *painter, NodeGraphicsObject &ngo) 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; + + for (PortType portType : {PortType::Out, PortType::In}) { + size_t const n = model + .nodeData(nodeId, + (portType == PortType::Out) ? NodeRole::OutPortCount + : NodeRole::InPortCount) + .toUInt(); + + for (PortIndex portIndex = 0; portIndex < n; ++portIndex) { + QPointF p = geometry.portPosition(nodeId, portType, portIndex); + + auto const &connected = model.connections(nodeId, portType, portIndex); + + if (!connected.empty()) { + auto const &dataType = model + .portData(nodeId, portType, portIndex, PortRole::DataType) + .value(); + + auto const &connectionStyle = StyleCollection::connectionStyle(); + if (connectionStyle.useDataDefinedColors()) { + QColor const c = connectionStyle.normalColor(dataType.id); + painter->setPen(c); + painter->setBrush(c); + } else { + painter->setPen(nodeStyle.FilledConnectionPointColor); + painter->setBrush(nodeStyle.FilledConnectionPointColor); + } + + painter->drawEllipse(p, diameter * 0.4, diameter * 0.4); + } + } + } +} + +void DefaultNodePainter::drawNodeCaption(QPainter *painter, NodeGraphicsObject &ngo) const +{ + AbstractGraphModel &model = ngo.graphModel(); + NodeId const nodeId = ngo.nodeId(); + AbstractNodeGeometry &geometry = ngo.nodeScene()->nodeGeometry(); + + if (!model.nodeData(nodeId, NodeRole::CaptionVisible).toBool()) + return; + + QString const name = model.nodeData(nodeId, NodeRole::Caption).toString(); + + QFont f({ "Arial", 10 }); + f.setBold(true); + + QPointF position = geometry.captionPosition(nodeId); + + QJsonDocument json = QJsonDocument::fromVariant(model.nodeData(nodeId, NodeRole::Style)); + NodeStyle nodeStyle(json.object()); + + + painter->setBrush(QBrush("#f7aa1b")); + QFontMetrics metrics(f); + auto fontRect = metrics.boundingRect(name); + + QSize sizeH = geometry.size(nodeId); + QRectF titleRect; + + int w = sizeH.width(); +// titleRect.setX(2); +// titleRect.setY(2); + titleRect.setWidth(w); + titleRect.setHeight(fontRect.height() + position.ry()); + QPen pen = painter->pen(); + pen.setWidth(0); + painter->setPen(pen); + painter->drawRect(titleRect); + + painter->setFont(f); + painter->setPen(Qt::black); + painter->drawText(position, name); + + f.setBold(false); + painter->setFont(f); +} + +void DefaultNodePainter::drawEntryLabels(QPainter *painter, NodeGraphicsObject &ngo) 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()); + + for (PortType portType : {PortType::Out, PortType::In}) { + unsigned int n = model.nodeData(nodeId, + (portType == PortType::Out) + ? NodeRole::OutPortCount + : NodeRole::InPortCount); + + for (PortIndex portIndex = 0; portIndex < n; ++portIndex) { + auto const &connected = model.connections(nodeId, portType, portIndex); + + QPointF p = geometry.portTextPosition(nodeId, portType, portIndex); + + if (connected.empty()) + painter->setPen(nodeStyle.FontColorFaded); + else + painter->setPen(nodeStyle.FontColor); + + QString s; + + if (model.portData(nodeId, portType, portIndex, PortRole::CaptionVisible)) { + s = model.portData(nodeId, portType, portIndex, PortRole::Caption); + } else { + auto portData = model.portData(nodeId, portType, portIndex, PortRole::DataType); + + s = portData.value().name; + } + + painter->drawText(p, s); + } + } +} + +void DefaultNodePainter::drawResizeRect(QPainter *painter, NodeGraphicsObject &ngo) const +{ + AbstractGraphModel &model = ngo.graphModel(); + NodeId const nodeId = ngo.nodeId(); + AbstractNodeGeometry &geometry = ngo.nodeScene()->nodeGeometry(); + + if (model.nodeFlags(nodeId) & NodeFlag::Resizable) { + painter->setBrush(Qt::gray); + + painter->drawEllipse(geometry.resizeHandleRect(nodeId)); + } +} + +} // namespace QtNodes diff --git a/story-editor/nodeeditor/src/DefaultVerticalNodeGeometry.cpp b/story-editor/nodeeditor/src/DefaultVerticalNodeGeometry.cpp new file mode 100644 index 0000000..4c3c0bd --- /dev/null +++ b/story-editor/nodeeditor/src/DefaultVerticalNodeGeometry.cpp @@ -0,0 +1,300 @@ +#include "DefaultVerticalNodeGeometry.hpp" + +#include "AbstractGraphModel.hpp" +#include "NodeData.hpp" + +#include +#include +#include + +namespace QtNodes { + +DefaultVerticalNodeGeometry::DefaultVerticalNodeGeometry(AbstractGraphModel &graphModel) + : AbstractNodeGeometry(graphModel) + , _portSize(20) + , _portSpasing(10) + , _fontMetrics(QFont()) + , _boldFontMetrics(QFont()) +{ + QFont f({ "Arial", 10 }); + f.setBold(true); + _boldFontMetrics = QFontMetrics(f); + + _portSize = _fontMetrics.height(); +} + +QSize DefaultVerticalNodeGeometry::size(NodeId const nodeId) const +{ + return _graphModel.nodeData(nodeId, NodeRole::Size); +} + +void DefaultVerticalNodeGeometry::recomputeSize(NodeId const nodeId) const +{ + unsigned int height = _portSpasing; // maxHorizontalPortsExtent(nodeId); + + if (auto w = _graphModel.nodeData(nodeId, NodeRole::Widget)) { + height = std::max(height, static_cast(w->height())); + } + + QRectF const capRect = captionRect(nodeId); + + height += capRect.height(); + + height += _portSpasing; + height += _portSpasing; + + PortCount nInPorts = _graphModel.nodeData(nodeId, NodeRole::InPortCount); + PortCount nOutPorts = _graphModel.nodeData(nodeId, NodeRole::OutPortCount); + + // Adding double step (top and bottom) to reserve space for port captions. + + height += portCaptionsHeight(nodeId, PortType::In); + height += portCaptionsHeight(nodeId, PortType::Out); + + unsigned int inPortWidth = maxPortsTextAdvance(nodeId, PortType::In); + unsigned int outPortWidth = maxPortsTextAdvance(nodeId, PortType::Out); + + unsigned int totalInPortsWidth = nInPorts > 0 + ? inPortWidth * nInPorts + _portSpasing * (nInPorts - 1) + : 0; + + unsigned int totalOutPortsWidth = nOutPorts > 0 ? outPortWidth * nOutPorts + + _portSpasing * (nOutPorts - 1) + : 0; + + unsigned int width = std::max(totalInPortsWidth, totalOutPortsWidth); + + if (auto w = _graphModel.nodeData(nodeId, NodeRole::Widget)) { + width = std::max(width, static_cast(w->width())); + } + + width = std::max(width, static_cast(capRect.width())); + + width += _portSpasing; + width += _portSpasing; + + QSize size(width, height); + + _graphModel.setNodeData(nodeId, NodeRole::Size, size); +} + +QPointF DefaultVerticalNodeGeometry::portPosition(NodeId const nodeId, + PortType const portType, + PortIndex const portIndex) const +{ + QPointF result; + + QSize size = _graphModel.nodeData(nodeId, NodeRole::Size); + + switch (portType) { + case PortType::In: { + unsigned int inPortWidth = maxPortsTextAdvance(nodeId, PortType::In) + _portSpasing; + + PortCount nInPorts = _graphModel.nodeData(nodeId, NodeRole::InPortCount); + + double x = (size.width() - (nInPorts - 1) * inPortWidth) / 2.0 + portIndex * inPortWidth; + + double y = 0.0; + + result = QPointF(x, y); + + break; + } + + case PortType::Out: { + unsigned int outPortWidth = maxPortsTextAdvance(nodeId, PortType::Out) + _portSpasing; + PortCount nOutPorts = _graphModel.nodeData(nodeId, NodeRole::OutPortCount); + + double x = (size.width() - (nOutPorts - 1) * outPortWidth) / 2.0 + portIndex * outPortWidth; + + double y = size.height(); + + result = QPointF(x, y); + + break; + } + + default: + break; + } + + return result; +} + +QPointF DefaultVerticalNodeGeometry::portTextPosition(NodeId const nodeId, + PortType const portType, + PortIndex const portIndex) const +{ + QPointF p = portPosition(nodeId, portType, portIndex); + + QRectF rect = portTextRect(nodeId, portType, portIndex); + + p.setX(p.x() - rect.width() / 2.0); + + QSize size = _graphModel.nodeData(nodeId, NodeRole::Size); + + switch (portType) { + case PortType::In: + p.setY(5.0 + rect.height()); + break; + + case PortType::Out: + p.setY(size.height() - 5.0); + break; + + default: + break; + } + + 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; + + auto rect = captionRect(nodeId); + + return QPointF(0.5 * (size.width() - rect.width()), step + rect.height()); +} + +QPointF DefaultVerticalNodeGeometry::widgetPosition(NodeId const nodeId) const +{ + QSize size = _graphModel.nodeData(nodeId, NodeRole::Size); + + unsigned int captionHeight = captionRect(nodeId).height(); + + if (auto w = _graphModel.nodeData(nodeId, NodeRole::Widget)) { + // 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); + } else { + return QPointF(_portSpasing + maxPortsTextAdvance(nodeId, PortType::In), + (captionHeight + size.height() - w->height()) / 2.0); + } + } + return QPointF(); +} + +QRect DefaultVerticalNodeGeometry::resizeHandleRect(NodeId const nodeId) const +{ + QSize size = _graphModel.nodeData(nodeId, NodeRole::Size); + + unsigned int rectSize = 7; + + 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; +} + +unsigned int DefaultVerticalNodeGeometry::portCaptionsHeight(NodeId const nodeId, + PortType const portType) const +{ + unsigned int h = 0; + + switch (portType) { + case PortType::In: { + 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; + break; + } + } + break; + } + + case PortType::Out: { + 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; + break; + } + } + break; + } + + default: + break; + } + + return h; +} + +} // namespace QtNodes diff --git a/story-editor/nodeeditor/src/Definitions.cpp b/story-editor/nodeeditor/src/Definitions.cpp new file mode 100644 index 0000000..dc8baa3 --- /dev/null +++ b/story-editor/nodeeditor/src/Definitions.cpp @@ -0,0 +1 @@ +#include "Definitions.hpp" diff --git a/story-editor/nodeeditor/src/GraphicsView.cpp b/story-editor/nodeeditor/src/GraphicsView.cpp new file mode 100644 index 0000000..1ffa8f5 --- /dev/null +++ b/story-editor/nodeeditor/src/GraphicsView.cpp @@ -0,0 +1,405 @@ +#include "GraphicsView.hpp" + +#include "BasicGraphicsScene.hpp" +#include "ConnectionGraphicsObject.hpp" +#include "NodeGraphicsObject.hpp" +#include "StyleCollection.hpp" +#include "UndoCommands.hpp" + +#include + +#include +#include + +#include + +#include +#include +#include + +#include +#include + +#include +#include + +using QtNodes::BasicGraphicsScene; +using QtNodes::GraphicsView; + +GraphicsView::GraphicsView(QWidget *parent) + : QGraphicsView(parent) + , _clearSelectionAction(Q_NULLPTR) + , _deleteSelectionAction(Q_NULLPTR) + , _duplicateSelectionAction(Q_NULLPTR) + , _copySelectionAction(Q_NULLPTR) + , _pasteAction(Q_NULLPTR) +{ + setDragMode(QGraphicsView::ScrollHandDrag); + setRenderHint(QPainter::Antialiasing); + + auto const &flowViewStyle = StyleCollection::flowViewStyle(); + + setBackgroundBrush(flowViewStyle.BackgroundColor); + + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + + setTransformationAnchor(QGraphicsView::AnchorUnderMouse); + + setCacheMode(QGraphicsView::CacheBackground); + setViewportUpdateMode(QGraphicsView::BoundingRectViewportUpdate); + + setScaleRange(0.3, 2); + + // Sets the scene rect to its maximum possible ranges to avoid autu scene range + // re-calculation when expanding the all QGraphicsItems common rect. + int maxSize = 32767; + setSceneRect(-maxSize, -maxSize, (maxSize * 2), (maxSize * 2)); +} + +GraphicsView::GraphicsView(BasicGraphicsScene *scene, QWidget *parent) + : GraphicsView(parent) +{ + setScene(scene); +} + +QAction *GraphicsView::clearSelectionAction() const +{ + return _clearSelectionAction; +} + +QAction *GraphicsView::deleteSelectionAction() const +{ + return _deleteSelectionAction; +} + +void GraphicsView::setScene(BasicGraphicsScene *scene) +{ + QGraphicsView::setScene(scene); + + { + // setup actions + delete _clearSelectionAction; + _clearSelectionAction = new QAction(QStringLiteral("Clear Selection"), this); + _clearSelectionAction->setShortcut(Qt::Key_Escape); + + connect(_clearSelectionAction, &QAction::triggered, scene, &QGraphicsScene::clearSelection); + + addAction(_clearSelectionAction); + } + + { + delete _deleteSelectionAction; + _deleteSelectionAction = new QAction(QStringLiteral("Delete Selection"), this); + _deleteSelectionAction->setShortcutContext(Qt::ShortcutContext::WidgetShortcut); + _deleteSelectionAction->setShortcut(QKeySequence(QKeySequence::Delete)); + connect(_deleteSelectionAction, + &QAction::triggered, + this, + &GraphicsView::onDeleteSelectedObjects); + + addAction(_deleteSelectionAction); + } + + { + delete _duplicateSelectionAction; + _duplicateSelectionAction = new QAction(QStringLiteral("Duplicate Selection"), this); + _duplicateSelectionAction->setShortcutContext(Qt::ShortcutContext::WidgetShortcut); + _duplicateSelectionAction->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_D)); + connect(_duplicateSelectionAction, + &QAction::triggered, + this, + &GraphicsView::onDuplicateSelectedObjects); + + addAction(_duplicateSelectionAction); + } + + { + delete _copySelectionAction; + _copySelectionAction = new QAction(QStringLiteral("Copy Selection"), this); + _copySelectionAction->setShortcutContext(Qt::ShortcutContext::WidgetShortcut); + _copySelectionAction->setShortcut(QKeySequence(QKeySequence::Copy)); + connect(_copySelectionAction, + &QAction::triggered, + this, + &GraphicsView::onCopySelectedObjects); + + addAction(_copySelectionAction); + } + + { + delete _pasteAction; + _pasteAction = new QAction(QStringLiteral("Copy Selection"), this); + _pasteAction->setShortcutContext(Qt::ShortcutContext::WidgetShortcut); + _pasteAction->setShortcut(QKeySequence(QKeySequence::Paste)); + connect(_pasteAction, &QAction::triggered, this, &GraphicsView::onPasteObjects); + + addAction(_pasteAction); + } + + auto undoAction = scene->undoStack().createUndoAction(this, tr("&Undo")); + undoAction->setShortcuts(QKeySequence::Undo); + addAction(undoAction); + + auto redoAction = scene->undoStack().createRedoAction(this, tr("&Redo")); + redoAction->setShortcuts(QKeySequence::Redo); + addAction(redoAction); +} + +void GraphicsView::centerScene() +{ + if (scene()) { + scene()->setSceneRect(QRectF()); + + QRectF sceneRect = scene()->sceneRect(); + + if (sceneRect.width() > this->rect().width() || sceneRect.height() > this->rect().height()) { + fitInView(sceneRect, Qt::KeepAspectRatio); + } + + centerOn(sceneRect.center()); + } +} + +void GraphicsView::contextMenuEvent(QContextMenuEvent *event) +{ + if (itemAt(event->pos())) { + QGraphicsView::contextMenuEvent(event); + return; + } + + auto const scenePos = mapToScene(event->pos()); + + QMenu *menu = nodeScene()->createSceneMenu(scenePos); + + if (menu) { + menu->exec(event->globalPos()); + } +} + +void GraphicsView::wheelEvent(QWheelEvent *event) +{ + QPoint delta = event->angleDelta(); + + if (delta.y() == 0) { + event->ignore(); + return; + } + + double const d = delta.y() / std::abs(delta.y()); + + if (d > 0.0) + scaleUp(); + else + scaleDown(); +} + +double GraphicsView::getScale() const +{ + return transform().m11(); +} + +void GraphicsView::setScaleRange(double minimum, double maximum) +{ + if (maximum < minimum) + std::swap(minimum, maximum); + minimum = std::max(0.0, minimum); + maximum = std::max(0.0, maximum); + + _scaleRange = {minimum, maximum}; + + setupScale(transform().m11()); +} + +void GraphicsView::setScaleRange(ScaleRange range) +{ + setScaleRange(range.minimum, range.maximum); +} + +void GraphicsView::scaleUp() +{ + double const step = 1.2; + double const factor = std::pow(step, 1.0); + + if (_scaleRange.maximum > 0) { + QTransform t = transform(); + t.scale(factor, factor); + if (t.m11() >= _scaleRange.maximum) { + setupScale(t.m11()); + return; + } + } + + scale(factor, factor); + Q_EMIT scaleChanged(transform().m11()); +} + +void GraphicsView::scaleDown() +{ + double const step = 1.2; + double const factor = std::pow(step, -1.0); + + if (_scaleRange.minimum > 0) { + QTransform t = transform(); + t.scale(factor, factor); + if (t.m11() <= _scaleRange.minimum) { + setupScale(t.m11()); + return; + } + } + + scale(factor, factor); + Q_EMIT scaleChanged(transform().m11()); +} + +void GraphicsView::setupScale(double scale) +{ + scale = std::max(_scaleRange.minimum, std::min(_scaleRange.maximum, scale)); + + if (scale <= 0) + return; + + if (scale == transform().m11()) + return; + + QTransform matrix; + matrix.scale(scale, scale); + setTransform(matrix, false); + + Q_EMIT scaleChanged(scale); +} + +void GraphicsView::onDeleteSelectedObjects() +{ + nodeScene()->undoStack().push(new DeleteCommand(nodeScene())); +} + +void GraphicsView::onDuplicateSelectedObjects() +{ + QPointF const pastePosition = scenePastePosition(); + + nodeScene()->undoStack().push(new CopyCommand(nodeScene())); + nodeScene()->undoStack().push(new PasteCommand(nodeScene(), pastePosition)); +} + +void GraphicsView::onCopySelectedObjects() +{ + nodeScene()->undoStack().push(new CopyCommand(nodeScene())); +} + +void GraphicsView::onPasteObjects() +{ + QPointF const pastePosition = scenePastePosition(); + nodeScene()->undoStack().push(new PasteCommand(nodeScene(), pastePosition)); +} + +void GraphicsView::keyPressEvent(QKeyEvent *event) +{ + switch (event->key()) { + case Qt::Key_Shift: + setDragMode(QGraphicsView::RubberBandDrag); + break; + + default: + break; + } + + QGraphicsView::keyPressEvent(event); +} + +void GraphicsView::keyReleaseEvent(QKeyEvent *event) +{ + switch (event->key()) { + case Qt::Key_Shift: + setDragMode(QGraphicsView::ScrollHandDrag); + break; + + default: + break; + } + QGraphicsView::keyReleaseEvent(event); +} + +void GraphicsView::mousePressEvent(QMouseEvent *event) +{ + QGraphicsView::mousePressEvent(event); + if (event->button() == Qt::LeftButton) { + _clickPos = mapToScene(event->pos()); + } +} + +void GraphicsView::mouseMoveEvent(QMouseEvent *event) +{ + QGraphicsView::mouseMoveEvent(event); + if (scene()->mouseGrabberItem() == nullptr && event->buttons() == Qt::LeftButton) { + // Make sure shift is not being pressed + if ((event->modifiers() & Qt::ShiftModifier) == 0) { + QPointF difference = _clickPos - mapToScene(event->pos()); + setSceneRect(sceneRect().translated(difference.x(), difference.y())); + } + } +} + +void GraphicsView::drawBackground(QPainter *painter, const QRectF &r) +{ + QGraphicsView::drawBackground(painter, r); + + auto drawGrid = [&](double gridStep) { + QRect windowRect = rect(); + QPointF tl = mapToScene(windowRect.topLeft()); + QPointF br = mapToScene(windowRect.bottomRight()); + + double left = std::floor(tl.x() / gridStep - 0.5); + double right = std::floor(br.x() / gridStep + 1.0); + double bottom = std::floor(tl.y() / gridStep - 0.5); + double top = std::floor(br.y() / gridStep + 1.0); + + // vertical lines + for (int xi = int(left); xi <= int(right); ++xi) { + QLineF line(xi * gridStep, bottom * gridStep, xi * gridStep, 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); + painter->drawLine(line); + } + }; + + auto const &flowViewStyle = StyleCollection::flowViewStyle(); + + QPen pfine(flowViewStyle.FineGridColor, 1.0); + + painter->setPen(pfine); + drawGrid(15); + + QPen p(flowViewStyle.CoarseGridColor, 1.0); + + painter->setPen(p); + drawGrid(150); +} + +void GraphicsView::showEvent(QShowEvent *event) +{ + QGraphicsView::showEvent(event); + + centerScene(); +} + +BasicGraphicsScene *GraphicsView::nodeScene() +{ + return dynamic_cast(scene()); +} + +QPointF GraphicsView::scenePastePosition() +{ + QPoint origin = mapFromGlobal(QCursor::pos()); + + QRect const viewRect = rect(); + if (!viewRect.contains(origin)) + origin = viewRect.center(); + + return mapToScene(origin); +} diff --git a/story-editor/nodeeditor/src/GraphicsViewStyle.cpp b/story-editor/nodeeditor/src/GraphicsViewStyle.cpp new file mode 100644 index 0000000..ae02a2c --- /dev/null +++ b/story-editor/nodeeditor/src/GraphicsViewStyle.cpp @@ -0,0 +1,94 @@ +#include "GraphicsViewStyle.hpp" + +#include +#include +#include +#include + +#include "StyleCollection.hpp" + +using QtNodes::GraphicsViewStyle; + +inline void initResources() +{ + Q_INIT_RESOURCE(resources); +} + +GraphicsViewStyle::GraphicsViewStyle() +{ + // Explicit resources inialization for preventing the static initialization + // order fiasco: https://isocpp.org/wiki/faq/ctors#static-init-order + initResources(); + + // This configuration is stored inside the compiled unit and is loaded statically + loadJsonFile(":DefaultStyle.json"); +} + +GraphicsViewStyle::GraphicsViewStyle(QString jsonText) +{ + loadJsonText(jsonText); +} + +void GraphicsViewStyle::setStyle(QString jsonText) +{ + GraphicsViewStyle style(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 = nodeStyleValues.toObject(); + + FLOW_VIEW_STYLE_READ_COLOR(obj, BackgroundColor); + FLOW_VIEW_STYLE_READ_COLOR(obj, FineGridColor); + FLOW_VIEW_STYLE_READ_COLOR(obj, 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); + + QJsonObject root; + root["GraphicsViewStyle"] = obj; + + return root; +} diff --git a/story-editor/nodeeditor/src/NodeConnectionInteraction.cpp b/story-editor/nodeeditor/src/NodeConnectionInteraction.cpp new file mode 100644 index 0000000..9af05fc --- /dev/null +++ b/story-editor/nodeeditor/src/NodeConnectionInteraction.cpp @@ -0,0 +1,152 @@ +#include "NodeConnectionInteraction.hpp" + +#include "AbstractNodeGeometry.hpp" +#include "BasicGraphicsScene.hpp" +#include "ConnectionGraphicsObject.hpp" +#include "ConnectionIdUtils.hpp" +#include "NodeGraphicsObject.hpp" +#include "UndoCommands.hpp" + +#include + +#include + +namespace QtNodes { + +NodeConnectionInteraction::NodeConnectionInteraction(NodeGraphicsObject &ngo, + ConnectionGraphicsObject &cgo, + BasicGraphicsScene &scene) + : _ngo(ngo) + , _cgo(cgo) + , _scene(scene) +{} + +bool NodeConnectionInteraction::canConnect(PortIndex *portIndex) const +{ + // 1. Connection requires a port. + + PortType requiredPort = _cgo.connectionState().requiredPort(); + + if (requiredPort == PortType::None) { + return false; + } + + NodeId connectedNodeId = getNodeId(oppositePort(requiredPort), _cgo.connectionId()); + + // 2. Forbid connecting the node to itself. + + if (_ngo.nodeId() == connectedNodeId) + return false; + + // 3. Connection loose end is above the node port. + + QPointF connectionPoint = _cgo.sceneTransform().map(_cgo.endPoint(requiredPort)); + + *portIndex = nodePortIndexUnderScenePoint(requiredPort, connectionPoint); + + if (*portIndex == InvalidPortIndex) { + return false; + } + + // 4. Model allows connection. + + AbstractGraphModel &model = _ngo.nodeScene()->graphModel(); + + ConnectionId connectionId = makeCompleteConnectionId(_cgo.connectionId(), // incomplete + _ngo.nodeId(), // missing node id + *portIndex); // missing port index + + return model.connectionPossible(connectionId); +} + +bool NodeConnectionInteraction::tryConnect() const +{ + // 1. Check conditions from 'canConnect'. + + PortIndex targetPortIndex = InvalidPortIndex; + if (!canConnect(&targetPortIndex)) { + return false; + } + + // 2. Create new connection. + + ConnectionId incompleteConnectionId = _cgo.connectionId(); + + ConnectionId newConnectionId = makeCompleteConnectionId(incompleteConnectionId, + _ngo.nodeId(), + targetPortIndex); + + _ngo.nodeScene()->resetDraftConnection(); + + _ngo.nodeScene()->undoStack().push(new ConnectCommand(_ngo.nodeScene(), newConnectionId)); + + return true; +} + +bool NodeConnectionInteraction::disconnect(PortType portToDisconnect) const +{ + ConnectionId connectionId = _cgo.connectionId(); + + _scene.undoStack().push(new DisconnectCommand(&_scene, connectionId)); + + AbstractNodeGeometry &geometry = _scene.nodeGeometry(); + + QPointF scenePos = geometry.portScenePosition(_ngo.nodeId(), + portToDisconnect, + getPortIndex(portToDisconnect, connectionId), + _ngo.sceneTransform()); + + // Converted to "draft" connection with the new incomplete id. + ConnectionId incompleteConnectionId = makeIncompleteConnectionId(connectionId, portToDisconnect); + + // Grabs the mouse + auto const &draftConnection = _scene.makeDraftConnection(incompleteConnectionId); + + QPointF const looseEndPos = draftConnection->mapFromScene(scenePos); + draftConnection->setEndPoint(portToDisconnect, looseEndPos); + + // Repaint connection points. + NodeId connectedNodeId = getNodeId(oppositePort(portToDisconnect), connectionId); + _scene.nodeGraphicsObject(connectedNodeId)->update(); + + NodeId disconnectedNodeId = getNodeId(portToDisconnect, connectionId); + _scene.nodeGraphicsObject(disconnectedNodeId)->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 +{ + AbstractNodeGeometry &geometry = _scene.nodeGeometry(); + + QTransform sceneTransform = _ngo.sceneTransform(); + + QPointF nodePoint = sceneTransform.inverted().map(scenePoint); + + return geometry.checkPortHit(_ngo.nodeId(), portType, nodePoint); +} + +} // namespace QtNodes diff --git a/story-editor/nodeeditor/src/NodeDelegateModel.cpp b/story-editor/nodeeditor/src/NodeDelegateModel.cpp new file mode 100644 index 0000000..94e47ad --- /dev/null +++ b/story-editor/nodeeditor/src/NodeDelegateModel.cpp @@ -0,0 +1,54 @@ +#include "NodeDelegateModel.hpp" + +#include "StyleCollection.hpp" + +namespace QtNodes { + +NodeDelegateModel::NodeDelegateModel() + : _nodeStyle(StyleCollection::nodeStyle()) +{ + // Derived classes can initialize specific style here +} + +QJsonObject NodeDelegateModel::save() const +{ + QJsonObject modelJson; + + modelJson["model-name"] = name(); + + return modelJson; +} + +void NodeDelegateModel::load(QJsonObject const &) +{ + // +} + +ConnectionPolicy NodeDelegateModel::portConnectionPolicy(PortType portType, PortIndex) const +{ + auto result = ConnectionPolicy::One; + switch (portType) { + case PortType::In: + result = ConnectionPolicy::One; + break; + case PortType::Out: + result = ConnectionPolicy::Many; + break; + case PortType::None: + break; + } + + return result; +} + +NodeStyle const &NodeDelegateModel::nodeStyle() const +{ + return _nodeStyle; +} + +void NodeDelegateModel::setNodeStyle(NodeStyle const &style) +{ + _nodeStyle = style; +} + +} // namespace QtNodes diff --git a/story-editor/nodeeditor/src/NodeDelegateModelRegistry.cpp b/story-editor/nodeeditor/src/NodeDelegateModelRegistry.cpp new file mode 100644 index 0000000..11a1f84 --- /dev/null +++ b/story-editor/nodeeditor/src/NodeDelegateModelRegistry.cpp @@ -0,0 +1,36 @@ +#include "NodeDelegateModelRegistry.hpp" + +#include +#include + +using QtNodes::NodeDataType; +using QtNodes::NodeDelegateModel; +using QtNodes::NodeDelegateModelRegistry; + +std::unique_ptr NodeDelegateModelRegistry::create(QString const &modelName) +{ + auto it = _registeredItemCreators.find(modelName); + + if (it != _registeredItemCreators.end()) { + return it->second(); + } + + return nullptr; +} + +NodeDelegateModelRegistry::RegisteredModelCreatorsMap const & +NodeDelegateModelRegistry::registeredModelCreators() const +{ + return _registeredItemCreators; +} + +NodeDelegateModelRegistry::RegisteredModelsCategoryMap const & +NodeDelegateModelRegistry::registeredModelsCategoryAssociation() const +{ + return _registeredModelsCategory; +} + +NodeDelegateModelRegistry::CategoriesSet const &NodeDelegateModelRegistry::categories() const +{ + return _categories; +} diff --git a/story-editor/nodeeditor/src/NodeGraphicsObject.cpp b/story-editor/nodeeditor/src/NodeGraphicsObject.cpp new file mode 100644 index 0000000..a25a4aa --- /dev/null +++ b/story-editor/nodeeditor/src/NodeGraphicsObject.cpp @@ -0,0 +1,364 @@ +#include "NodeGraphicsObject.hpp" + +#include +#include + +#include +#include + +#include "AbstractGraphModel.hpp" +#include "AbstractNodeGeometry.hpp" +#include "AbstractNodePainter.hpp" +#include "BasicGraphicsScene.hpp" +#include "ConnectionGraphicsObject.hpp" +#include "ConnectionIdUtils.hpp" +#include "NodeConnectionInteraction.hpp" +#include "StyleCollection.hpp" +#include "UndoCommands.hpp" + +namespace QtNodes { + +NodeGraphicsObject::NodeGraphicsObject(BasicGraphicsScene &scene, NodeId nodeId) + : _nodeId(nodeId) + , _graphModel(scene.graphModel()) + , _nodeState(*this) + , _proxyWidget(nullptr) +{ + scene.addItem(this); + + setFlag(QGraphicsItem::ItemDoesntPropagateOpacityToChildren, true); + setFlag(QGraphicsItem::ItemIsFocusable, true); + + setLockedState(); + + setCacheMode(QGraphicsItem::DeviceCoordinateCache); + + QJsonObject nodeStyleJson = _graphModel.nodeData(_nodeId, NodeRole::Style).toJsonObject(); + + NodeStyle nodeStyle(nodeStyleJson); + + if (scene.isDropShadowEffectEnabled()) + { + auto effect = new QGraphicsDropShadowEffect; + effect->setOffset(4, 4); + effect->setBlurRadius(20); + effect->setColor(nodeStyle.ShadowColor); + + setGraphicsEffect(effect); + } + + setOpacity(nodeStyle.Opacity); + + setAcceptHoverEvents(true); + + setZValue(0); + + embedQWidget(); + + nodeScene()->nodeGeometry().recomputeSize(_nodeId); + + QPointF const pos = _graphModel.nodeData(_nodeId, NodeRole::Position); + + setPos(pos); + + connect(&_graphModel, &AbstractGraphModel::nodeFlagsUpdated, [this](NodeId const nodeId) { + if (_nodeId == nodeId) + setLockedState(); + }); +} + +AbstractGraphModel &NodeGraphicsObject::graphModel() const +{ + return _graphModel; +} + +BasicGraphicsScene *NodeGraphicsObject::nodeScene() const +{ + return dynamic_cast(scene()); +} + +void NodeGraphicsObject::embedQWidget() +{ + AbstractNodeGeometry &geometry = nodeScene()->nodeGeometry(); + geometry.recomputeSize(_nodeId); + + if (auto w = _graphModel.nodeData(_nodeId, NodeRole::Widget).value()) { + _proxyWidget = new QGraphicsProxyWidget(this); + + _proxyWidget->setWidget(w); + + _proxyWidget->setPreferredWidth(5); + + geometry.recomputeSize(_nodeId); + + if (w->sizePolicy().verticalPolicy() & QSizePolicy::ExpandFlag) { + unsigned int widgetHeight = geometry.size(_nodeId).height() + - geometry.captionRect(_nodeId).height(); + + // If the widget wants to use as much vertical space as possible, set + // it to have the geom's equivalentWidgetHeight. + _proxyWidget->setMinimumHeight(widgetHeight); + } + + _proxyWidget->setPos(geometry.widgetPosition(_nodeId)); + + //update(); + + _proxyWidget->setOpacity(1.0); + _proxyWidget->setFlag(QGraphicsItem::ItemIgnoresParentOpacity); + } +} + +void NodeGraphicsObject::setLockedState() +{ + NodeFlags flags = _graphModel.nodeFlags(_nodeId); + + bool const locked = flags.testFlag(NodeFlag::Locked); + + setFlag(QGraphicsItem::ItemIsMovable, !locked); + setFlag(QGraphicsItem::ItemIsSelectable, !locked); + setFlag(QGraphicsItem::ItemSendsScenePositionChanges, !locked); +} + +QRectF NodeGraphicsObject::boundingRect() const +{ + AbstractNodeGeometry &geometry = nodeScene()->nodeGeometry(); + return geometry.boundingRect(_nodeId); + //return NodeGeometry(_nodeId, _graphModel, nodeScene()).boundingRect(); +} + +void NodeGraphicsObject::setGeometryChanged() +{ + prepareGeometryChange(); +} + +void NodeGraphicsObject::moveConnections() const +{ + auto const &connected = _graphModel.allConnectionIds(_nodeId); + + for (auto &cnId : connected) { + auto cgo = nodeScene()->connectionGraphicsObject(cnId); + + if (cgo) + cgo->move(); + } +} + +void NodeGraphicsObject::reactToConnection(ConnectionGraphicsObject const *cgo) +{ + _nodeState.storeConnectionForReaction(cgo); + + update(); +} + +void NodeGraphicsObject::paint(QPainter *painter, QStyleOptionGraphicsItem const *option, QWidget *) +{ + painter->setClipRect(option->exposedRect); + + nodeScene()->nodePainter().paint(painter, *this); +} + +QVariant NodeGraphicsObject::itemChange(GraphicsItemChange change, const QVariant &value) +{ + if (change == ItemScenePositionHasChanged && scene()) { + moveConnections(); + } + + return QGraphicsObject::itemChange(change, value); +} + +void NodeGraphicsObject::mousePressEvent(QGraphicsSceneMouseEvent *event) +{ + //if (_nodeState.locked()) + //return; + + AbstractNodeGeometry &geometry = nodeScene()->nodeGeometry(); + + for (PortType portToCheck : {PortType::In, PortType::Out}) { + QPointF nodeCoord = sceneTransform().inverted().map(event->scenePos()); + + PortIndex const portIndex = geometry.checkPortHit(_nodeId, portToCheck, nodeCoord); + + if (portIndex == InvalidPortIndex) + continue; + + auto const &connected = _graphModel.connections(_nodeId, portToCheck, portIndex); + + // Start dragging existing connection. + if (!connected.empty() && portToCheck == PortType::In) { + auto const &cnId = *connected.begin(); + + // Need ConnectionGraphicsObject + + NodeConnectionInteraction interaction(*this, + *nodeScene()->connectionGraphicsObject(cnId), + *nodeScene()); + + if (_graphModel.detachPossible(cnId)) + interaction.disconnect(portToCheck); + } else // initialize new Connection + { + if (portToCheck == PortType::Out) { + auto const outPolicy = _graphModel + .portData(_nodeId, + portToCheck, + portIndex, + PortRole::ConnectionPolicyRole) + .value(); + + if (!connected.empty() && outPolicy == ConnectionPolicy::One) { + for (auto &cnId : connected) { + _graphModel.deleteConnection(cnId); + } + } + } // if port == out + + ConnectionId const incompleteConnectionId = makeIncompleteConnectionId(_nodeId, + portToCheck, + portIndex); + + nodeScene()->makeDraftConnection(incompleteConnectionId); + } + } + + if (_graphModel.nodeFlags(_nodeId) & NodeFlag::Resizable) { + auto pos = event->pos(); + bool const hit = geometry.resizeHandleRect(_nodeId).contains(QPoint(pos.x(), pos.y())); + _nodeState.setResizing(hit); + } + + QGraphicsObject::mousePressEvent(event); + + if (isSelected()) { + Q_EMIT nodeScene()->nodeSelected(_nodeId); + } +} + +void NodeGraphicsObject::mouseMoveEvent(QGraphicsSceneMouseEvent *event) +{ + // Deselect all other items after this one is selected. + // Unless we press a CTRL button to add the item to the selected group before + // starting moving. + if (!isSelected()) { + if (!event->modifiers().testFlag(Qt::ControlModifier)) + scene()->clearSelection(); + + setSelected(true); + } + + if (_nodeState.resizing()) { + auto diff = event->pos() - event->lastPos(); + + if (auto w = _graphModel.nodeData(_nodeId, NodeRole::Widget)) { + prepareGeometryChange(); + + auto oldSize = w->size(); + + oldSize += QSize(diff.x(), diff.y()); + + w->resize(oldSize); + + AbstractNodeGeometry &geometry = nodeScene()->nodeGeometry(); + + // Passes the new size to the model. + geometry.recomputeSize(_nodeId); + + update(); + + moveConnections(); + + event->accept(); + } + } else { + auto diff = event->pos() - event->lastPos(); + + nodeScene()->undoStack().push(new MoveNodeCommand(nodeScene(), diff)); + + event->accept(); + } + + QRectF r = nodeScene()->sceneRect(); + + r = r.united(mapToScene(boundingRect()).boundingRect()); + + nodeScene()->setSceneRect(r); +} + +void NodeGraphicsObject::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) +{ + _nodeState.setResizing(false); + + QGraphicsObject::mouseReleaseEvent(event); + + // position connections precisely after fast node move + moveConnections(); + + nodeScene()->nodeClicked(_nodeId); +} + +void NodeGraphicsObject::hoverEnterEvent(QGraphicsSceneHoverEvent *event) +{ + // bring all the colliding nodes to background + QList overlapItems = collidingItems(); + + for (QGraphicsItem *item : overlapItems) { + if (item->zValue() > 0.0) { + item->setZValue(0.0); + } + } + + // bring this node forward + setZValue(1.0); + + _nodeState.setHovered(true); + + update(); + + Q_EMIT nodeScene()->nodeHovered(_nodeId, event->screenPos()); + + event->accept(); +} + +void NodeGraphicsObject::hoverLeaveEvent(QGraphicsSceneHoverEvent *event) +{ + _nodeState.setHovered(false); + + setZValue(0.0); + + update(); + + Q_EMIT nodeScene()->nodeHoverLeft(_nodeId); + + event->accept(); +} + +void NodeGraphicsObject::hoverMoveEvent(QGraphicsSceneHoverEvent *event) +{ + auto pos = event->pos(); + + //NodeGeometry geometry(_nodeId, _graphModel, nodeScene()); + AbstractNodeGeometry &geometry = nodeScene()->nodeGeometry(); + + if ((_graphModel.nodeFlags(_nodeId) | NodeFlag::Resizable) + && geometry.resizeHandleRect(_nodeId).contains(QPoint(pos.x(), pos.y()))) { + setCursor(QCursor(Qt::SizeFDiagCursor)); + } else { + setCursor(QCursor()); + } + + event->accept(); +} + +void NodeGraphicsObject::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) +{ + QGraphicsItem::mouseDoubleClickEvent(event); + + Q_EMIT nodeScene()->nodeDoubleClicked(_nodeId); +} + +void NodeGraphicsObject::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) +{ + Q_EMIT nodeScene()->nodeContextMenu(_nodeId, mapToScene(event->pos())); +} + +} // namespace QtNodes diff --git a/story-editor/nodeeditor/src/NodeState.cpp b/story-editor/nodeeditor/src/NodeState.cpp new file mode 100644 index 0000000..f6ed43e --- /dev/null +++ b/story-editor/nodeeditor/src/NodeState.cpp @@ -0,0 +1,42 @@ +#include "NodeState.hpp" + +#include "ConnectionGraphicsObject.hpp" +#include "NodeGraphicsObject.hpp" + +namespace QtNodes { + +NodeState::NodeState(NodeGraphicsObject &ngo) + : _ngo(ngo) + , _hovered(false) + , _resizing(false) + , _connectionForReaction{nullptr} +{ + Q_UNUSED(_ngo); +} + +void NodeState::setResizing(bool resizing) +{ + _resizing = resizing; +} + +bool NodeState::resizing() const +{ + return _resizing; +} + +ConnectionGraphicsObject const *NodeState::connectionForReaction() const +{ + return _connectionForReaction.data(); +} + +void NodeState::storeConnectionForReaction(ConnectionGraphicsObject const *cgo) +{ + _connectionForReaction = cgo; +} + +void NodeState::resetConnectionForReaction() +{ + _connectionForReaction.clear(); +} + +} // namespace QtNodes diff --git a/story-editor/nodeeditor/src/NodeStyle.cpp b/story-editor/nodeeditor/src/NodeStyle.cpp new file mode 100644 index 0000000..a82bf8f --- /dev/null +++ b/story-editor/nodeeditor/src/NodeStyle.cpp @@ -0,0 +1,146 @@ +#include "NodeStyle.hpp" + +#include + +#include +#include +#include + +#include + +#include "StyleCollection.hpp" + +using QtNodes::NodeStyle; + +inline void initResources() +{ + Q_INIT_RESOURCE(resources); +} + +NodeStyle::NodeStyle() +{ + // Explicit resources inialization for preventing the static initialization + // order fiasco: https://isocpp.org/wiki/faq/ctors#static-init-order + initResources(); + + // This configuration is stored inside the compiled unit and is loaded statically + loadJsonFile(":DefaultStyle.json"); +} + +NodeStyle::NodeStyle(QString jsonText) +{ + loadJsonText(jsonText); +} + +NodeStyle::NodeStyle(QJsonObject const &json) +{ + loadJson(json); +} + +void NodeStyle::setNodeStyle(QString jsonText) +{ + NodeStyle style(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; \ + } + +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_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 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_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); + + QJsonObject root; + root["NodeStyle"] = obj; + + return root; +} diff --git a/story-editor/nodeeditor/src/StyleCollection.cpp b/story-editor/nodeeditor/src/StyleCollection.cpp new file mode 100644 index 0000000..c695728 --- /dev/null +++ b/story-editor/nodeeditor/src/StyleCollection.cpp @@ -0,0 +1,43 @@ +#include "StyleCollection.hpp" + +using QtNodes::ConnectionStyle; +using QtNodes::GraphicsViewStyle; +using QtNodes::NodeStyle; +using QtNodes::StyleCollection; + +NodeStyle const &StyleCollection::nodeStyle() +{ + return instance()._nodeStyle; +} + +ConnectionStyle const &StyleCollection::connectionStyle() +{ + return instance()._connectionStyle; +} + +GraphicsViewStyle const &StyleCollection::flowViewStyle() +{ + return instance()._flowViewStyle; +} + +void StyleCollection::setNodeStyle(NodeStyle nodeStyle) +{ + instance()._nodeStyle = nodeStyle; +} + +void StyleCollection::setConnectionStyle(ConnectionStyle connectionStyle) +{ + instance()._connectionStyle = connectionStyle; +} + +void StyleCollection::setGraphicsViewStyle(GraphicsViewStyle flowViewStyle) +{ + instance()._flowViewStyle = flowViewStyle; +} + +StyleCollection &StyleCollection::instance() +{ + static StyleCollection collection; + + return collection; +} diff --git a/story-editor/nodeeditor/src/UndoCommands.cpp b/story-editor/nodeeditor/src/UndoCommands.cpp new file mode 100644 index 0000000..ea92812 --- /dev/null +++ b/story-editor/nodeeditor/src/UndoCommands.cpp @@ -0,0 +1,464 @@ +#include "UndoCommands.hpp" + +#include "BasicGraphicsScene.hpp" +#include "ConnectionGraphicsObject.hpp" +#include "ConnectionIdUtils.hpp" +#include "Definitions.hpp" +#include "NodeGraphicsObject.hpp" + +#include +#include +#include +#include +#include +#include + +#include + +namespace QtNodes { + +static QJsonObject serializeSelectedItems(BasicGraphicsScene *scene) +{ + QJsonObject serializedScene; + std::unordered_set selectedNodes; + + QJsonArray nodesJsonArray; + + for (QGraphicsItem *item : scene->selectedItems()) { + if (auto n = qgraphicsitem_cast(item)) { + + QJsonObject obj; + obj["id"] = static_cast(n->nodeId()); + nodesJsonArray.append(obj); + + selectedNodes.insert(n->nodeId()); + } + } + + QJsonArray connJsonArray; + + 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)); + } + } + } + + serializedScene["nodes"] = nodesJsonArray; + serializedScene["connections"] = connJsonArray; + + return serializedScene; +} + +static void insertSerializedItems(QJsonObject const &json, BasicGraphicsScene *scene) +{ + AbstractGraphModel &graphModel = scene->graphModel(); + + QJsonArray const &nodesJsonArray = json["nodes"].toArray(); + + for (QJsonValue node : nodesJsonArray) { + QJsonObject obj = node.toObject(); + + graphModel.loadNode(obj); + + auto id = obj["id"].toInt(); + scene->nodeGraphicsObject(id)->setZValue(1.0); + scene->nodeGraphicsObject(id)->setSelected(true); + } + + QJsonArray const &connJsonArray = json["connections"].toArray(); + + for (QJsonValue connection : connJsonArray) { + QJsonObject connJson = connection.toObject(); + + ConnectionId connId = fromJson(connJson); + + // Restore the connection + graphModel.addConnection(connId); + + scene->connectionGraphicsObject(connId)->setSelected(true); + } +} + +static void deleteSerializedItems(QJsonObject &sceneJson, AbstractGraphModel &graphModel) +{ + QJsonArray connectionJsonArray = sceneJson["connections"].toArray(); + + for (QJsonValueRef connection : connectionJsonArray) { + QJsonObject connJson = connection.toObject(); + + ConnectionId connId = fromJson(connJson); + + graphModel.deleteConnection(connId); + } + + QJsonArray nodesJsonArray = sceneJson["nodes"].toArray(); + + for (QJsonValueRef node : nodesJsonArray) { + QJsonObject nodeJson = node.toObject(); + graphModel.deleteNode(nodeJson["id"].toInt()); + } +} + +static QPointF computeAverageNodePosition(QJsonObject const &sceneJson) +{ + QPointF averagePos(0, 0); + + QJsonArray nodesJsonArray = sceneJson["nodes"].toArray(); + + for (QJsonValueRef node : nodesJsonArray) { + QJsonObject nodeJson = node.toObject(); + + averagePos += QPointF(nodeJson["position"].toObject()["x"].toDouble(), + nodeJson["position"].toObject()["y"].toDouble()); + } + + averagePos /= static_cast(nodesJsonArray.size()); + + return averagePos; +} + +//------------------------------------- + +CreateCommand::CreateCommand(BasicGraphicsScene *scene, + QString const name, + QPointF const &mouseScenePos) + : _scene(scene) + , _sceneJson(QJsonObject()) +{ + _nodeId = _scene->graphModel().addNode(name); + if (_nodeId != InvalidNodeId) { + _scene->graphModel().setNodeData(_nodeId, NodeRole::Position, mouseScenePos); + } else { + setObsolete(true); + } +} + +void CreateCommand::undo() +{ + QJsonArray nodesJsonArray; + QJsonObject obj; + obj["id"] = static_cast(_scene->graphModel().nodeData(_nodeId, NodeRole::Id).toInt()); + nodesJsonArray.append(obj); + _sceneJson["nodes"] = nodesJsonArray; + _scene->graphModel().deleteNode(_nodeId); +} + +void CreateCommand::redo() +{ + if (_sceneJson.empty() || _sceneJson["nodes"].toArray().empty()) + return; + + insertSerializedItems(_sceneJson, _scene); +} + +//------------------------------------- + +DeleteCommand::DeleteCommand(BasicGraphicsScene *scene) + : _scene(scene) +{ + auto &graphModel = _scene->graphModel(); + + QJsonArray connJsonArray; + // Delete the selected connections first, ensuring that they won't be + // automatically deleted when selected nodes are deleted (deleting a + // node deletes some connections as well) + for (QGraphicsItem *item : _scene->selectedItems()) { + if (auto c = qgraphicsitem_cast(item)) { + auto const &cid = c->connectionId(); + + connJsonArray.append(toJson(cid)); + } + } + + QJsonArray nodesJsonArray; + // Delete the nodes; this will delete many of the connections. + // Selected connections were already deleted prior to this loop, + for (QGraphicsItem *item : _scene->selectedItems()) { + if (auto n = qgraphicsitem_cast(item)) { + // saving connections attached to the selected nodes + for (auto const &cid : graphModel.allConnectionIds(n->nodeId())) { + connJsonArray.append(toJson(cid)); + } + QJsonObject obj; + obj["id"] = static_cast(n->nodeId()); + nodesJsonArray.append(obj); + } + } + + // If nothing is deleted, cancel this operation + if (connJsonArray.isEmpty() && nodesJsonArray.isEmpty()) + setObsolete(true); + + _sceneJson["nodes"] = nodesJsonArray; + _sceneJson["connections"] = connJsonArray; +} + +void DeleteCommand::undo() +{ + insertSerializedItems(_sceneJson, _scene); +} + +void DeleteCommand::redo() +{ + deleteSerializedItems(_sceneJson, _scene->graphModel()); +} + +//------------------------------------- + +void offsetNodeGroup(QJsonObject &sceneJson, QPointF const &diff) +{ + QJsonArray nodesJsonArray = sceneJson["nodes"].toArray(); + + QJsonArray newNodesJsonArray; + for (QJsonValueRef node : nodesJsonArray) { + QJsonObject obj = node.toObject(); + + QPointF oldPos(obj["position"].toObject()["x"].toDouble(), + obj["position"].toObject()["y"].toDouble()); + + oldPos += diff; + + QJsonObject posJson; + posJson["x"] = oldPos.x(); + posJson["y"] = oldPos.y(); + obj["position"] = posJson; + + newNodesJsonArray.append(obj); + } + + sceneJson["nodes"] = newNodesJsonArray; +} + +//------------------------------------- + +CopyCommand::CopyCommand(BasicGraphicsScene *scene) +{ + QJsonObject sceneJson = serializeSelectedItems(scene); + + if (sceneJson.empty() || sceneJson["nodes"].toArray().empty()) { + setObsolete(true); + return; + } + + QClipboard *clipboard = QApplication::clipboard(); + + QByteArray const data = QJsonDocument(sceneJson).toJson(); + + QMimeData *mimeData = new QMimeData(); + mimeData->setData("application/qt-nodes-graph", data); + mimeData->setText(data); + + clipboard->setMimeData(mimeData); + + // Copy command does not have any effective redo/undo operations. + // It copies the data to the clipboard and could be immediately removed + // from the stack. + setObsolete(true); +} + +//------------------------------------- + +PasteCommand::PasteCommand(BasicGraphicsScene *scene, QPointF const &mouseScenePos) + : _scene(scene) + , _mouseScenePos(mouseScenePos) +{ + _newSceneJson = takeSceneJsonFromClipboard(); + + if (_newSceneJson.empty() || _newSceneJson["nodes"].toArray().empty()) { + setObsolete(true); + return; + } + + _newSceneJson = makeNewNodeIdsInScene(_newSceneJson); + + QPointF averagePos = computeAverageNodePosition(_newSceneJson); + + offsetNodeGroup(_newSceneJson, _mouseScenePos - averagePos); +} + +void PasteCommand::undo() +{ + deleteSerializedItems(_newSceneJson, _scene->graphModel()); +} + +void PasteCommand::redo() +{ + _scene->clearSelection(); + + // Ignore if pasted in content does not generate nodes. + try { + insertSerializedItems(_newSceneJson, _scene); + } catch (...) { + // If the paste does not work, delete all selected nodes and connections + // `deleteNode(...)` implicitly removed connections + auto &graphModel = _scene->graphModel(); + + QJsonArray nodesJsonArray; + for (QGraphicsItem *item : _scene->selectedItems()) { + if (auto n = qgraphicsitem_cast(item)) { + graphModel.deleteNode(n->nodeId()); + } + } + + setObsolete(true); + } +} + +QJsonObject PasteCommand::takeSceneJsonFromClipboard() +{ + QClipboard const *clipboard = QApplication::clipboard(); + QMimeData const *mimeData = clipboard->mimeData(); + + QJsonDocument json; + if (mimeData->hasFormat("application/qt-nodes-graph")) { + json = QJsonDocument::fromJson(mimeData->data("application/qt-nodes-graph")); + } else if (mimeData->hasText()) { + json = QJsonDocument::fromJson(mimeData->text().toUtf8()); + } + + return json.object(); +} + +QJsonObject PasteCommand::makeNewNodeIdsInScene(QJsonObject const &sceneJson) +{ + AbstractGraphModel &graphModel = _scene->graphModel(); + + std::unordered_map mapNodeIds; + + QJsonArray nodesJsonArray = sceneJson["nodes"].toArray(); + + QJsonArray newNodesJsonArray; + for (QJsonValueRef node : nodesJsonArray) { + QJsonObject nodeJson = node.toObject(); + + NodeId oldNodeId = nodeJson["id"].toInt(); + + NodeId newNodeId = graphModel.newNodeId(); + + mapNodeIds[oldNodeId] = newNodeId; + + // Replace NodeId in json + nodeJson["id"] = static_cast(newNodeId); + + newNodesJsonArray.append(nodeJson); + } + + QJsonArray connectionJsonArray = sceneJson["connections"].toArray(); + + QJsonArray newConnJsonArray; + for (QJsonValueRef connection : connectionJsonArray) { + QJsonObject connJson = connection.toObject(); + + ConnectionId connId = fromJson(connJson); + + ConnectionId newConnId{mapNodeIds[connId.outNodeId], + connId.outPortIndex, + mapNodeIds[connId.inNodeId], + connId.inPortIndex}; + + newConnJsonArray.append(toJson(newConnId)); + } + + QJsonObject newSceneJson; + + newSceneJson["nodes"] = newNodesJsonArray; + newSceneJson["connections"] = newConnJsonArray; + + return newSceneJson; +} + +//------------------------------------- + +DisconnectCommand::DisconnectCommand(BasicGraphicsScene *scene, ConnectionId const connId) + : _scene(scene) + , _connId(connId) +{ + // +} + +void DisconnectCommand::undo() +{ + _scene->graphModel().addConnection(_connId); +} + +void DisconnectCommand::redo() +{ + _scene->graphModel().deleteConnection(_connId); +} + +//------ + +ConnectCommand::ConnectCommand(BasicGraphicsScene *scene, ConnectionId const connId) + : _scene(scene) + , _connId(connId) +{ + // +} + +void ConnectCommand::undo() +{ + _scene->graphModel().deleteConnection(_connId); +} + +void ConnectCommand::redo() +{ + _scene->graphModel().addConnection(_connId); +} + +//------ + +MoveNodeCommand::MoveNodeCommand(BasicGraphicsScene *scene, QPointF const &diff) + : _scene(scene) + , _diff(diff) +{ + _selectedNodes.clear(); + for (QGraphicsItem *item : _scene->selectedItems()) { + if (auto n = qgraphicsitem_cast(item)) { + _selectedNodes.insert(n->nodeId()); + } + } +} + +void MoveNodeCommand::undo() +{ + for (auto nodeId : _selectedNodes) { + auto oldPos = _scene->graphModel().nodeData(nodeId, NodeRole::Position).value(); + + oldPos -= _diff; + + _scene->graphModel().setNodeData(nodeId, NodeRole::Position, oldPos); + } +} + +void MoveNodeCommand::redo() +{ + for (auto nodeId : _selectedNodes) { + auto oldPos = _scene->graphModel().nodeData(nodeId, NodeRole::Position).value(); + + oldPos += _diff; + + _scene->graphModel().setNodeData(nodeId, NodeRole::Position, oldPos); + } +} + +int MoveNodeCommand::id() const +{ + return static_cast(typeid(MoveNodeCommand).hash_code()); +} + +bool MoveNodeCommand::mergeWith(QUndoCommand const *c) +{ + auto mc = static_cast(c); + + if (_selectedNodes == mc->_selectedNodes) { + _diff += mc->_diff; + return true; + } + return false; +} + +} // namespace QtNodes diff --git a/story-editor/nodeeditor/src/locateNode.cpp b/story-editor/nodeeditor/src/locateNode.cpp new file mode 100644 index 0000000..ddba099 --- /dev/null +++ b/story-editor/nodeeditor/src/locateNode.cpp @@ -0,0 +1,42 @@ +#include "locateNode.hpp" + +#include + +#include +#include + +#include "NodeGraphicsObject.hpp" + +namespace QtNodes { + +NodeGraphicsObject *locateNodeAt(QPointF scenePoint, + QGraphicsScene &scene, + QTransform const &viewTransform) +{ + // items under cursor + QList items = scene.items(scenePoint, + Qt::IntersectsItemShape, + Qt::DescendingOrder, + viewTransform); + + // items convertable to NodeGraphicsObject + std::vector filteredItems; + + std::copy_if(items.begin(), + items.end(), + std::back_inserter(filteredItems), + [](QGraphicsItem *item) { + return (qgraphicsitem_cast(item) != nullptr); + }); + + NodeGraphicsObject *node = nullptr; + + if (!filteredItems.empty()) { + QGraphicsItem *graphicsItem = filteredItems.front(); + node = dynamic_cast(graphicsItem); + } + + return node; +} + +} // namespace QtNodes