delete submodule

This commit is contained in:
Anthony Rabine 2023-05-28 14:50:48 +02:00
parent ceda217885
commit 41255f2cec
85 changed files with 8013 additions and 5 deletions

3
.gitmodules vendored
View file

@ -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

@ -1 +0,0 @@
Subproject commit bf458f2e4adef93ef75eeac40b20d50ae9c50726

View file

@ -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
$<INSTALL_INTERFACE:include>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
PRIVATE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include/QtNodes/internal>
)
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
$<$<CXX_COMPILER_ID:MSVC>:/W4 /wd4127 /EHsc /utf-8>
$<$<CXX_COMPILER_ID:GNU>:-Wall -Wextra>
$<$<CXX_COMPILER_ID:AppleClang>:-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
$<$<CXX_COMPILER_ID:Clang>:-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}
)

View file

@ -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.

View file

@ -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 <https://en.wikipedia.org/wiki/Dataflow_programming>`_ .
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 <https://qtnodes.readthedocs.io/>`_
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=<vcpkg_dir>/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 <https://github.com/facontidavide>`_
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 <https://github.com/GENIVI/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 <https://github.com/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 <https://github.com/fredakilla/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

View file

@ -0,0 +1 @@
#include "internal/AbstractGraphModel.hpp"

View file

@ -0,0 +1 @@
#include "internal/AbstractNodePainter.hpp"

View file

@ -0,0 +1 @@
#include "internal/BasicGraphicsScene.hpp"

View file

@ -0,0 +1 @@
#include "internal/ConnectionIdUtils.hpp"

View file

@ -0,0 +1 @@
#include "internal/ConnectionStyle.hpp"

View file

@ -0,0 +1 @@
#include "internal/DataFlowGraphModel.hpp"

View file

@ -0,0 +1 @@
#include "internal/DataFlowGraphicsScene.hpp"

View file

@ -0,0 +1 @@
#include "internal/DefaultNodePainter.hpp"

View file

@ -0,0 +1 @@
#include "internal/Definitions.hpp"

View file

@ -0,0 +1 @@
#include "internal/GraphicsView.hpp"

View file

@ -0,0 +1 @@
#include "internal/GraphicsViewStyle.hpp"

View file

@ -0,0 +1 @@
#include "internal/NodeData.hpp"

View file

@ -0,0 +1 @@
#include "internal/NodeDelegateModel.hpp"

View file

@ -0,0 +1 @@
#include "internal/NodeDelegateModelRegistry.hpp"

View file

@ -0,0 +1,2 @@
#include "internal/NodeGeometry.hpp"

View file

@ -0,0 +1 @@
#include "internal/NodeState.hpp"

View file

@ -0,0 +1 @@
#include "internal/NodeStyle.hpp"

View file

@ -0,0 +1 @@
#include "internal/StyleCollection.hpp"

View file

@ -0,0 +1 @@
#include "internal/UndoCommands.hpp"

View file

@ -0,0 +1,249 @@
#pragma once
#include "Export.hpp"
#include <unordered_map>
#include <unordered_set>
#include <QtCore/QJsonObject>
#include <QtCore/QObject>
#include <QtCore/QVariant>
#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<NodeId> allNodeIds() const = 0;
/**
* A collection of all input and output connections for the given `nodeId`.
*/
virtual std::unordered_set<ConnectionId> 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<ConnectionId> 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<typename T>
T nodeData(NodeId nodeId, NodeRole role) const
{
return nodeData(nodeId, role).value<T>();
}
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<typename T>
T portData(NodeId nodeId, PortType portType, PortIndex index, PortRole role) const
{
return portData(nodeId, portType, index, role).value<T>();
}
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<ConnectionId> _shiftedByDynamicPortsConnections;
};
} // namespace QtNodes

View file

@ -0,0 +1,82 @@
#pragma once
#include "Definitions.hpp"
#include "Export.hpp"
#include <QRectF>
#include <QSize>
#include <QTransform>
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

View file

@ -0,0 +1,29 @@
#pragma once
#include <QPainter>
#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

View file

@ -0,0 +1,184 @@
#pragma once
#include <QtCore/QUuid>
#include <QtWidgets/QGraphicsScene>
#include <QtWidgets/QMenu>
#include <functional>
#include <memory>
#include <tuple>
#include <unordered_map>
#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<AbstractNodePainter> 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<ConnectionGraphicsObject> 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<NodeGraphicsObject>;
using UniqueConnectionGraphicsObject = std::unique_ptr<ConnectionGraphicsObject>;
std::unordered_map<NodeId, UniqueNodeGraphicsObject> _nodeGraphicsObjects;
std::unordered_map<ConnectionId, UniqueConnectionGraphicsObject> _connectionGraphicsObjects;
std::unique_ptr<ConnectionGraphicsObject> _draftConnection;
std::unique_ptr<AbstractNodeGeometry> _nodeGeometry;
std::unique_ptr<AbstractNodePainter> _nodePainter;
bool _nodeDrag;
QUndoStack *_undoStack;
Qt::Orientation _orientation;
bool _dropShadowEffect{false};
};
} // namespace QtNodes

View file

@ -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

View file

@ -0,0 +1,96 @@
#pragma once
#include <utility>
#include <QtCore/QUuid>
#include <QtWidgets/QGraphicsObject>
#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<QPointF, QPointF> 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<QPointF, QPointF> pointsC1C2Horizontal() const;
std::pair<QPointF, QPointF> pointsC1C2Vertical() const;
private:
ConnectionId _connectionId;
AbstractGraphModel &_graphModel;
ConnectionState _connectionState;
mutable QPointF _out;
mutable QPointF _in;
};
} // namespace QtNodes

View file

@ -0,0 +1,55 @@
#pragma once
#include <functional>
#include "Definitions.hpp"
inline void hash_combine(std::size_t &seed)
{
Q_UNUSED(seed);
}
template<typename T, typename... Rest>
inline void hash_combine(std::size_t &seed, const T &v, Rest... rest)
{
std::hash<T> hasher;
seed ^= hasher(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
hash_combine(seed, rest...);
}
namespace std {
template<>
struct hash<QtNodes::ConnectionId>
{
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<std::pair<QtNodes::NodeId, QtNodes::PortIndex>>
{
inline std::size_t operator()(std::pair<QtNodes::NodeId, QtNodes::PortIndex> const &nodePort) const
{
std::size_t h = 0;
hash_combine(h, nodePort.first, nodePort.second);
return h;
}
};
template<>
struct hash<std::tuple<QtNodes::NodeId, QtNodes::PortType, QtNodes::PortIndex>>
{
using Key = std::tuple<QtNodes::NodeId, QtNodes::PortType, QtNodes::PortIndex>;
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

View file

@ -0,0 +1,151 @@
#pragma once
#include "Definitions.hpp"
#include <QJsonObject>
#include <iostream>
#include <string>
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<qint64>(connId.outNodeId);
connJson["outPortIndex"] = static_cast<qint64>(connId.outPortIndex);
connJson["intNodeId"] = static_cast<qint64>(connId.inNodeId);
connJson["inPortIndex"] = static_cast<qint64>(connId.inPortIndex);
return connJson;
}
inline ConnectionId fromJson(QJsonObject const &connJson)
{
ConnectionId connId{static_cast<NodeId>(connJson["outNodeId"].toInt(InvalidNodeId)),
static_cast<PortIndex>(connJson["outPortIndex"].toInt(InvalidPortIndex)),
static_cast<NodeId>(connJson["intNodeId"].toInt(InvalidNodeId)),
static_cast<PortIndex>(connJson["inPortIndex"].toInt(InvalidPortIndex))};
return connId;
}
} // namespace QtNodes

View file

@ -0,0 +1,21 @@
#pragma once
#include <QtGui/QPainter>
#include <QtGui/QPainterPath>
#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

View file

@ -0,0 +1,60 @@
#pragma once
#include <QtCore/QUuid>
#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

View file

@ -0,0 +1,54 @@
#pragma once
#include <QtGui/QColor>
#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

View file

@ -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 <QJsonObject>
#include <memory>
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<NodeDelegateModelRegistry> registry);
std::shared_ptr<NodeDelegateModelRegistry> dataModelRegistry() { return _registry; }
public:
std::unordered_set<NodeId> allNodeIds() const override;
std::unordered_set<ConnectionId> allConnectionIds(NodeId const nodeId) const override;
std::unordered_set<ConnectionId> 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<typename NodeDelegateModelType>
NodeDelegateModelType *delegateModel(NodeId const nodeId)
{
auto it = _models.find(nodeId);
if (it == _models.end())
return nullptr;
auto model = dynamic_cast<NodeDelegateModelType *>(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<NodeDelegateModelRegistry> _registry;
NodeId _nextNodeId;
std::unordered_map<NodeId, std::unique_ptr<NodeDelegateModel>> _models;
std::unordered_set<ConnectionId> _connectivity;
mutable std::unordered_map<NodeId, NodeGeometryData> _nodeGeometryData;
};
} // namespace QtNodes

View file

@ -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<NodeId> 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

View file

@ -0,0 +1,58 @@
#pragma once
#include "AbstractNodeGeometry.hpp"
#include <QtGui/QFontMetrics>
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

View file

@ -0,0 +1,34 @@
#pragma once
#include <QtGui/QPainter>
#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

View file

@ -0,0 +1,60 @@
#pragma once
#include "AbstractNodeGeometry.hpp"
#include <QtGui/QFontMetrics>
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

View file

@ -0,0 +1,125 @@
#pragma once
#include "Export.hpp"
#include <QtCore/QMetaObject>
#include <limits>
/**
* @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<NodeData>`.
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<PortIndex>::max();
/// Unique Id associated with each node in the GraphModel.
using NodeId = unsigned int;
static constexpr NodeId InvalidNodeId = std::numeric_limits<NodeId>::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

View file

@ -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

View file

@ -0,0 +1,97 @@
#pragma once
#include <QtWidgets/QGraphicsView>
#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

View file

@ -0,0 +1,32 @@
#pragma once
#include <QtGui/QColor>
#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

View file

@ -0,0 +1,68 @@
#pragma once
#include <memory>
#include <QtCore/QPointF>
#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

View file

@ -0,0 +1,43 @@
#pragma once
#include <memory>
#include <QtCore/QObject>
#include <QtCore/QString>
#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<QtNodes::NodeData>)

View file

@ -0,0 +1,133 @@
#pragma once
#include <memory>
#include <QtWidgets/QWidget>
#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> nodeData, PortIndex const portIndex) = 0;
virtual std::shared_ptr<NodeData> 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

View file

@ -0,0 +1,171 @@
#pragma once
#include "Export.hpp"
#include "NodeData.hpp"
#include "NodeDelegateModel.hpp"
#include "QStringStdHash.hpp"
#include <QtCore/QString>
#include <functional>
#include <memory>
#include <set>
#include <type_traits>
#include <unordered_map>
#include <utility>
#include <vector>
namespace QtNodes {
/// Class uses map for storing models (name, model)
class NODE_EDITOR_PUBLIC NodeDelegateModelRegistry
{
public:
using RegistryItemPtr = std::unique_ptr<NodeDelegateModel>;
using RegistryItemCreator = std::function<RegistryItemPtr()>;
using RegisteredModelCreatorsMap = std::unordered_map<QString, RegistryItemCreator>;
using RegisteredModelsCategoryMap = std::unordered_map<QString, QString>;
using CategoriesSet = std::set<QString>;
//using RegisteredTypeConvertersMap = std::map<TypeConverterId, TypeConverter>;
NodeDelegateModelRegistry() = default;
~NodeDelegateModelRegistry() = default;
NodeDelegateModelRegistry(NodeDelegateModelRegistry const &) = delete;
NodeDelegateModelRegistry(NodeDelegateModelRegistry &&) = default;
NodeDelegateModelRegistry &operator=(NodeDelegateModelRegistry const &) = delete;
NodeDelegateModelRegistry &operator=(NodeDelegateModelRegistry &&) = default;
public:
template<typename ModelType>
void registerModel(RegistryItemCreator creator, QString const &category = "Nodes")
{
QString const name = computeName<ModelType>(HasStaticMethodName<ModelType>{}, creator);
if (!_registeredItemCreators.count(name)) {
_registeredItemCreators[name] = std::move(creator);
_categories.insert(category);
_registeredModelsCategory[name] = category;
}
}
template<typename ModelType>
void registerModel(QString const &category = "Nodes")
{
RegistryItemCreator creator = []() { return std::make_unique<ModelType>(); };
registerModel<ModelType>(std::move(creator), category);
}
#if 0
template<typename ModelType>
void
registerModel(RegistryItemCreator creator,
QString const& category = "Nodes")
{
registerModel<ModelType>(std::move(creator), category);
}
template <typename ModelCreator>
void
registerModel(ModelCreator&& creator, QString const& category = "Nodes")
{
using ModelType = compute_model_type_t<decltype(creator())>;
registerModel<ModelType>(std::forward<ModelCreator>(creator), category);
}
template <typename ModelCreator>
void
registerModel(QString const& category, ModelCreator&& creator)
{
registerModel(std::forward<ModelCreator>(creator), category);
}
void
registerTypeConverter(TypeConverterId const& id,
TypeConverter typeConverter)
{
_registeredTypeConverters[id] = std::move(typeConverter);
}
#endif
std::unique_ptr<NodeDelegateModel> create(QString const &modelName);
RegisteredModelCreatorsMap const &registeredModelCreators() const;
RegisteredModelsCategoryMap const &registeredModelsCategoryAssociation() 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<typename T, typename = void>
struct HasStaticMethodName : std::false_type
{};
template<typename T>
struct HasStaticMethodName<
T,
typename std::enable_if<std::is_same<decltype(T::Name()), QString>::value>::type>
: std::true_type
{};
template<typename ModelType>
static QString computeName(std::true_type, RegistryItemCreator const &)
{
return ModelType::Name();
}
template<typename ModelType>
static QString computeName(std::false_type, RegistryItemCreator const &creator)
{
return creator()->name();
}
template<typename T>
struct UnwrapUniquePtr
{
// Assert always fires, but the compiler doesn't know this:
static_assert(!std::is_same<T, T>::value,
"The ModelCreator must return a std::unique_ptr<T>, where T "
"inherits from NodeDelegateModel");
};
template<typename T>
struct UnwrapUniquePtr<std::unique_ptr<T>>
{
static_assert(std::is_base_of<NodeDelegateModel, T>::value,
"The ModelCreator must return a std::unique_ptr<T>, where T "
"inherits from NodeDelegateModel");
using type = T;
};
template<typename CreatorResult>
using compute_model_type_t = typename UnwrapUniquePtr<CreatorResult>::type;
};
} // namespace QtNodes

View file

@ -0,0 +1,91 @@
#pragma once
#include <QtCore/QUuid>
#include <QtWidgets/QGraphicsObject>
#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

View file

@ -0,0 +1,52 @@
#pragma once
#include <unordered_map>
#include <vector>
#include <QtCore/QPointF>
#include <QtCore/QPointer>
#include <QtCore/QUuid>
#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<ConnectionGraphicsObject const> _connectionForReaction;
};
} // namespace QtNodes

View file

@ -0,0 +1,53 @@
#pragma once
#include <QtGui/QColor>
#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

View file

@ -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

View file

@ -0,0 +1,22 @@
#pragma once
#include <QtGlobal>
#if (QT_VERSION < QT_VERSION_CHECK(5, 14, 0))
// As of 5.14 there is a specialization std::hash<QString>
#include <functional>
#include <QtCore/QString>
#include <QtCore/QVariant>
namespace std {
template<>
struct hash<QString>
{
inline std::size_t operator()(QString const &s) const { return qHash(s); }
};
} // namespace std
#endif

View file

@ -0,0 +1,14 @@
#pragma once
#include <functional>
#include <QtCore/QUuid>
#include <QtCore/QVariant>
namespace std {
template<>
struct hash<QUuid>
{
inline std::size_t operator()(QUuid const &uid) const { return qHash(uid); }
};
} // namespace std

View file

@ -0,0 +1,16 @@
#pragma once
#include <QtCore/QJsonObject>
namespace QtNodes {
class Serializable
{
public:
virtual ~Serializable() = default;
virtual QJsonObject save() const { return {}; }
virtual void load(QJsonObject const & /*p*/) {}
};
} // namespace QtNodes

View file

@ -0,0 +1,48 @@
#pragma once
#include <QtCore/QDebug>
#include <QtCore/QFile>
#include <QtCore/QJsonDocument>
#include <QtCore/QJsonObject>
#include <QtCore/QObject>
#include <QtCore/QString>
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

View file

@ -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

View file

@ -0,0 +1,123 @@
#pragma once
#include "Definitions.hpp"
#include <QUndoCommand>
#include <QtCore/QJsonObject>
#include <QtCore/QPointF>
#include <unordered_set>
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<NodeId> _selectedNodes;
QPointF _diff;
};
} // namespace QtNodes

View file

@ -0,0 +1,16 @@
#pragma once
#include <QtCore/QPointF>
#include <QtGui/QTransform>
class QGraphicsScene;
namespace QtNodes {
class NodeGraphicsObject;
NodeGraphicsObject *locateNodeAt(QPointF scenePoint,
QGraphicsScene &scene,
QTransform const &viewTransform);
} // namespace QtNodes

View file

@ -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
}
}

View file

@ -0,0 +1,5 @@
<RCC version="1.0">
<qresource>
<file>DefaultStyle.json</file>
</qresource>
</RCC>

View file

@ -0,0 +1,105 @@
#include "AbstractGraphModel.hpp"
#include <QtNodes/ConnectionIdUtils>
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<ConnectionId> 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<ConnectionId> 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<ConnectionId> 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

View file

@ -0,0 +1,81 @@
#include "AbstractNodeGeometry.hpp"
#include "AbstractGraphModel.hpp"
#include "StyleCollection.hpp"
#include <QMargins>
#include <cmath>
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<unsigned int>(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

View file

@ -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 <QUndoStack>
#include <QtWidgets/QFileDialog>
#include <QtWidgets/QGraphicsSceneMoveEvent>
#include <QtCore/QBuffer>
#include <QtCore/QByteArray>
#include <QtCore/QDataStream>
#include <QtCore/QFile>
#include <QtCore/QJsonArray>
#include <QtCore/QJsonDocument>
#include <QtCore/QJsonObject>
#include <QtCore/QtGlobal>
#include <iostream>
#include <stdexcept>
#include <unordered_set>
#include <utility>
#include <queue>
namespace QtNodes {
BasicGraphicsScene::BasicGraphicsScene(AbstractGraphModel &graphModel, QObject *parent)
: QGraphicsScene(parent)
, _graphModel(graphModel)
, _nodeGeometry(std::make_unique<DefaultHorizontalNodeGeometry>(_graphModel))
, _nodePainter(std::make_unique<DefaultNodePainter>())
, _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<AbstractNodePainter> 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<ConnectionGraphicsObject> const &BasicGraphicsScene::makeDraftConnection(
ConnectionId const incompleteConnectionId)
{
_draftConnection = std::make_unique<ConnectionGraphicsObject>(*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<DefaultHorizontalNodeGeometry>(_graphModel);
break;
case Qt::Vertical:
_nodeGeometry = std::make_unique<DefaultVerticalNodeGeometry>(_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<NodeGraphicsObject>(*this, nodeId);
}
// Then for each node check output connections and insert them.
for (NodeId const nodeId : allNodeIds) {
unsigned int nOutPorts = _graphModel.nodeData<PortCount>(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<ConnectionGraphicsObject>(*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<ConnectionGraphicsObject>(*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<NodeGraphicsObject>(*this, nodeId);
}
void BasicGraphicsScene::onNodePositionUpdated(NodeId const nodeId)
{
auto node = nodeGraphicsObject(nodeId);
if (node) {
node->setPos(_graphModel.nodeData(nodeId, NodeRole::Position).value<QPointF>());
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<QPointF>());
_nodeDrag = false;
}
void BasicGraphicsScene::onModelReset()
{
_connectionGraphicsObjects.clear();
_nodeGraphicsObjects.clear();
clear();
traverseGraphAndPopulateGraphicsObjects();
}
} // namespace QtNodes

View file

@ -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 <QtWidgets/QGraphicsBlurEffect>
#include <QtWidgets/QGraphicsDropShadowEffect>
#include <QtWidgets/QGraphicsSceneMouseEvent>
#include <QtWidgets/QGraphicsView>
#include <QtWidgets/QStyleOptionGraphicsItem>
#include <QtCore/QDebug>
#include <stdexcept>
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<BasicGraphicsScene *>(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<QGraphicsView *>(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<QGraphicsView *>(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<QPointF, QPointF> 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<QPointF, QPointF> 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<QPointF, QPointF> 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

View file

@ -0,0 +1,254 @@
#include "ConnectionPainter.hpp"
#include <QtGui/QIcon>
#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<NodeDataType>();
auto dataTypeIn
= graphModel.portData(cId.inNodeId, PortType::In, cId.inPortIndex, PortRole::DataType)
.value<NodeDataType>();
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

View file

@ -0,0 +1,66 @@
#include "ConnectionState.hpp"
#include <QtCore/QDebug>
#include <QtCore/QPointF>
#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

View file

@ -0,0 +1,205 @@
#include "ConnectionStyle.hpp"
#include "StyleCollection.hpp"
#include <QtCore/QJsonArray>
#include <QtCore/QJsonObject>
#include <QtCore/QJsonValueRef>
#include <QDebug>
#include <random>
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<int> 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<unsigned int>(hash));
std::uniform_int_distribution<int> 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;
}

View file

@ -0,0 +1,536 @@
#include "DataFlowGraphModel.hpp"
#include "ConnectionIdHash.hpp"
#include <QJsonArray>
#include <stdexcept>
namespace QtNodes {
DataFlowGraphModel::DataFlowGraphModel(std::shared_ptr<NodeDelegateModelRegistry> registry)
: _registry(std::move(registry))
, _nextNodeId{0}
{}
std::unordered_set<NodeId> DataFlowGraphModel::allNodeIds() const
{
std::unordered_set<NodeId> nodeIds;
for_each(_models.begin(), _models.end(), [&nodeIds](auto const &p) { nodeIds.insert(p.first); });
return nodeIds;
}
std::unordered_set<ConnectionId> DataFlowGraphModel::allConnectionIds(NodeId const nodeId) const
{
std::unordered_set<ConnectionId> 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<ConnectionId> DataFlowGraphModel::connections(NodeId nodeId,
PortType portType,
PortIndex portIndex) const
{
std::unordered_set<ConnectionId> 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<NodeDelegateModel> 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<NodeDataType>();
};
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<ConnectionPolicy>();
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<QPointF>();
Q_EMIT nodePositionUpdated(nodeId);
result = true;
} break;
case NodeRole::Size: {
_nodeGeometryData[nodeId].size = value.value<QSize>();
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<std::shared_ptr<NodeData>>(), 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<qint64>(nodeId);
nodeJson["internal-data"] = _models.at(nodeId)->save();
{
QPointF const pos = nodeData(nodeId, NodeRole::Position).value<QPointF>();
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<NodeDelegateModel> 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<ConnectionId> 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

View file

@ -0,0 +1,189 @@
#include "DataFlowGraphicsScene.hpp"
#include "ConnectionGraphicsObject.hpp"
#include "GraphicsView.hpp"
#include "NodeDelegateModelRegistry.hpp"
#include "NodeGraphicsObject.hpp"
#include "UndoCommands.hpp"
#include <QtWidgets/QFileDialog>
#include <QtWidgets/QGraphicsSceneMoveEvent>
#include <QtWidgets/QHeaderView>
#include <QtWidgets/QLineEdit>
#include <QtWidgets/QTreeWidget>
#include <QtWidgets/QWidgetAction>
#include <QtCore/QBuffer>
#include <QtCore/QByteArray>
#include <QtCore/QDataStream>
#include <QtCore/QDebug>
#include <QtCore/QFile>
#include <QtCore/QJsonArray>
#include <QtCore/QJsonDocument>
#include <QtCore/QJsonObject>
#include <QtCore/QtGlobal>
#include <stdexcept>
#include <utility>
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<NodeId> DataFlowGraphicsScene::selectedNodes() const
{
QList<QGraphicsItem *> graphicsItems = selectedItems();
std::vector<NodeId> result;
result.reserve(graphicsItems.size());
for (QGraphicsItem *item : graphicsItems) {
auto ngo = qgraphicsitem_cast<NodeGraphicsObject *>(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<QTreeWidgetItem *> 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

View file

@ -0,0 +1,239 @@
#include "DefaultHorizontalNodeGeometry.hpp"
#include "AbstractGraphModel.hpp"
#include "NodeData.hpp"
#include <QPoint>
#include <QRect>
#include <QWidget>
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<QSize>(nodeId, NodeRole::Size);
}
void DefaultHorizontalNodeGeometry::recomputeSize(NodeId const nodeId) const
{
unsigned int height = maxVerticalPortsExtent(nodeId);
if (auto w = _graphModel.nodeData<QWidget *>(nodeId, NodeRole::Widget)) {
height = std::max(height, static_cast<unsigned int>(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<QWidget *>(nodeId, NodeRole::Widget)) {
width += w->width();
}
width = std::max(width, static_cast<unsigned int>(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<QSize>(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<QSize>(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<bool>(nodeId, NodeRole::CaptionVisible))
return QRect();
QString name = _graphModel.nodeData<QString>(nodeId, NodeRole::Caption);
return _boldFontMetrics.boundingRect(name);
}
QPointF DefaultHorizontalNodeGeometry::captionPosition(NodeId const nodeId) const
{
QSize size = _graphModel.nodeData<QSize>(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<QSize>(nodeId, NodeRole::Size);
unsigned int captionHeight = captionRect(nodeId).height();
if (auto w = _graphModel.nodeData<QWidget *>(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<QSize>(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<bool>(nodeId, portType, portIndex, PortRole::CaptionVisible)) {
s = _graphModel.portData<QString>(nodeId, portType, portIndex, PortRole::Caption);
} else {
auto portData = _graphModel.portData(nodeId, portType, portIndex, PortRole::DataType);
s = portData.value<NodeDataType>().name;
}
return _fontMetrics.boundingRect(s);
}
unsigned int DefaultHorizontalNodeGeometry::maxVerticalPortsExtent(NodeId const nodeId) const
{
PortCount nInPorts = _graphModel.nodeData<PortCount>(nodeId, NodeRole::InPortCount);
PortCount nOutPorts = _graphModel.nodeData<PortCount>(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<bool>(nodeId, portType, portIndex, PortRole::CaptionVisible)) {
name = _graphModel.portData<QString>(nodeId, portType, portIndex, PortRole::Caption);
} else {
NodeDataType portData = _graphModel.portData<NodeDataType>(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

View file

@ -0,0 +1,284 @@
#include "DefaultNodePainter.hpp"
#include <cmath>
#include <QtCore/QMargins>
#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<NodeDataType>();
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<NodeDataType>();
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<unsigned int>(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<bool>(nodeId, portType, portIndex, PortRole::CaptionVisible)) {
s = model.portData<QString>(nodeId, portType, portIndex, PortRole::Caption);
} else {
auto portData = model.portData(nodeId, portType, portIndex, PortRole::DataType);
s = portData.value<NodeDataType>().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

View file

@ -0,0 +1,300 @@
#include "DefaultVerticalNodeGeometry.hpp"
#include "AbstractGraphModel.hpp"
#include "NodeData.hpp"
#include <QPoint>
#include <QRect>
#include <QWidget>
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<QSize>(nodeId, NodeRole::Size);
}
void DefaultVerticalNodeGeometry::recomputeSize(NodeId const nodeId) const
{
unsigned int height = _portSpasing; // maxHorizontalPortsExtent(nodeId);
if (auto w = _graphModel.nodeData<QWidget *>(nodeId, NodeRole::Widget)) {
height = std::max(height, static_cast<unsigned int>(w->height()));
}
QRectF const capRect = captionRect(nodeId);
height += capRect.height();
height += _portSpasing;
height += _portSpasing;
PortCount nInPorts = _graphModel.nodeData<PortCount>(nodeId, NodeRole::InPortCount);
PortCount nOutPorts = _graphModel.nodeData<PortCount>(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<QWidget *>(nodeId, NodeRole::Widget)) {
width = std::max(width, static_cast<unsigned int>(w->width()));
}
width = std::max(width, static_cast<unsigned int>(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<QSize>(nodeId, NodeRole::Size);
switch (portType) {
case PortType::In: {
unsigned int inPortWidth = maxPortsTextAdvance(nodeId, PortType::In) + _portSpasing;
PortCount nInPorts = _graphModel.nodeData<PortCount>(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<PortCount>(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<QSize>(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<bool>(nodeId, NodeRole::CaptionVisible))
return QRect();
QString name = _graphModel.nodeData<QString>(nodeId, NodeRole::Caption);
return _boldFontMetrics.boundingRect(name);
}
QPointF DefaultVerticalNodeGeometry::captionPosition(NodeId const nodeId) const
{
QSize size = _graphModel.nodeData<QSize>(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<QSize>(nodeId, NodeRole::Size);
unsigned int captionHeight = captionRect(nodeId).height();
if (auto w = _graphModel.nodeData<QWidget *>(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<QSize>(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<bool>(nodeId, portType, portIndex, PortRole::CaptionVisible)) {
s = _graphModel.portData<QString>(nodeId, portType, portIndex, PortRole::Caption);
} else {
auto portData = _graphModel.portData(nodeId, portType, portIndex, PortRole::DataType);
s = portData.value<NodeDataType>().name;
}
return _fontMetrics.boundingRect(s);
}
unsigned int DefaultVerticalNodeGeometry::maxHorizontalPortsExtent(NodeId const nodeId) const
{
PortCount nInPorts = _graphModel.nodeData<PortCount>(nodeId, NodeRole::InPortCount);
PortCount nOutPorts = _graphModel.nodeData<PortCount>(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<bool>(nodeId, portType, portIndex, PortRole::CaptionVisible)) {
name = _graphModel.portData<QString>(nodeId, portType, portIndex, PortRole::Caption);
} else {
NodeDataType portData = _graphModel.portData<NodeDataType>(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<PortCount>(nodeId, NodeRole::InPortCount);
for (PortIndex i = 0; i < nInPorts; ++i) {
if (_graphModel.portData<bool>(nodeId, PortType::In, i, PortRole::CaptionVisible)) {
h += _portSpasing;
break;
}
}
break;
}
case PortType::Out: {
PortCount nOutPorts = _graphModel.nodeData<PortCount>(nodeId, NodeRole::OutPortCount);
for (PortIndex i = 0; i < nOutPorts; ++i) {
if (_graphModel.portData<bool>(nodeId, PortType::Out, i, PortRole::CaptionVisible)) {
h += _portSpasing;
break;
}
}
break;
}
default:
break;
}
return h;
}
} // namespace QtNodes

View file

@ -0,0 +1 @@
#include "Definitions.hpp"

View file

@ -0,0 +1,405 @@
#include "GraphicsView.hpp"
#include "BasicGraphicsScene.hpp"
#include "ConnectionGraphicsObject.hpp"
#include "NodeGraphicsObject.hpp"
#include "StyleCollection.hpp"
#include "UndoCommands.hpp"
#include <QtWidgets/QGraphicsScene>
#include <QtGui/QBrush>
#include <QtGui/QPen>
#include <QtWidgets/QMenu>
#include <QtCore/QDebug>
#include <QtCore/QPointF>
#include <QtCore/QRectF>
#include <QtOpenGL>
#include <QtWidgets>
#include <cmath>
#include <iostream>
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<BasicGraphicsScene *>(scene());
}
QPointF GraphicsView::scenePastePosition()
{
QPoint origin = mapFromGlobal(QCursor::pos());
QRect const viewRect = rect();
if (!viewRect.contains(origin))
origin = viewRect.center();
return mapToScene(origin);
}

View file

@ -0,0 +1,94 @@
#include "GraphicsViewStyle.hpp"
#include <QtCore/QFile>
#include <QtCore/QJsonArray>
#include <QtCore/QJsonObject>
#include <QtCore/QJsonValueRef>
#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<int> 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;
}

View file

@ -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 <QtCore/QDebug>
#include <QUndoStack>
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

View file

@ -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

View file

@ -0,0 +1,36 @@
#include "NodeDelegateModelRegistry.hpp"
#include <QtCore/QFile>
#include <QtWidgets/QMessageBox>
using QtNodes::NodeDataType;
using QtNodes::NodeDelegateModel;
using QtNodes::NodeDelegateModelRegistry;
std::unique_ptr<NodeDelegateModel> 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;
}

View file

@ -0,0 +1,364 @@
#include "NodeGraphicsObject.hpp"
#include <cstdlib>
#include <iostream>
#include <QtWidgets/QGraphicsEffect>
#include <QtWidgets/QtWidgets>
#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<QPointF>(_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<BasicGraphicsScene *>(scene());
}
void NodeGraphicsObject::embedQWidget()
{
AbstractNodeGeometry &geometry = nodeScene()->nodeGeometry();
geometry.recomputeSize(_nodeId);
if (auto w = _graphModel.nodeData(_nodeId, NodeRole::Widget).value<QWidget *>()) {
_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<ConnectionPolicy>();
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<QWidget *>(_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<QGraphicsItem *> 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

View file

@ -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

View file

@ -0,0 +1,146 @@
#include "NodeStyle.hpp"
#include <iostream>
#include <QtCore/QJsonArray>
#include <QtCore/QJsonObject>
#include <QtCore/QJsonValueRef>
#include <QtCore/QDebug>
#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<int> 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;
}

View file

@ -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;
}

View file

@ -0,0 +1,464 @@
#include "UndoCommands.hpp"
#include "BasicGraphicsScene.hpp"
#include "ConnectionGraphicsObject.hpp"
#include "ConnectionIdUtils.hpp"
#include "Definitions.hpp"
#include "NodeGraphicsObject.hpp"
#include <QtCore/QJsonArray>
#include <QtCore/QJsonDocument>
#include <QtCore/QMimeData>
#include <QtGui/QClipboard>
#include <QtWidgets/QApplication>
#include <QtWidgets/QGraphicsObject>
#include <typeinfo>
namespace QtNodes {
static QJsonObject serializeSelectedItems(BasicGraphicsScene *scene)
{
QJsonObject serializedScene;
std::unordered_set<NodeId> selectedNodes;
QJsonArray nodesJsonArray;
for (QGraphicsItem *item : scene->selectedItems()) {
if (auto n = qgraphicsitem_cast<NodeGraphicsObject *>(item)) {
QJsonObject obj;
obj["id"] = static_cast<int>(n->nodeId());
nodesJsonArray.append(obj);
selectedNodes.insert(n->nodeId());
}
}
QJsonArray connJsonArray;
for (QGraphicsItem *item : scene->selectedItems()) {
if (auto c = qgraphicsitem_cast<ConnectionGraphicsObject *>(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<double>(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<int>(_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<ConnectionGraphicsObject *>(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<NodeGraphicsObject *>(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<int>(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<NodeGraphicsObject *>(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<NodeId, NodeId> 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<qint64>(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<NodeGraphicsObject *>(item)) {
_selectedNodes.insert(n->nodeId());
}
}
}
void MoveNodeCommand::undo()
{
for (auto nodeId : _selectedNodes) {
auto oldPos = _scene->graphModel().nodeData(nodeId, NodeRole::Position).value<QPointF>();
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<QPointF>();
oldPos += _diff;
_scene->graphModel().setNodeData(nodeId, NodeRole::Position, oldPos);
}
}
int MoveNodeCommand::id() const
{
return static_cast<int>(typeid(MoveNodeCommand).hash_code());
}
bool MoveNodeCommand::mergeWith(QUndoCommand const *c)
{
auto mc = static_cast<MoveNodeCommand const *>(c);
if (_selectedNodes == mc->_selectedNodes) {
_diff += mc->_diff;
return true;
}
return false;
}
} // namespace QtNodes

View file

@ -0,0 +1,42 @@
#include "locateNode.hpp"
#include <vector>
#include <QtCore/QList>
#include <QtWidgets/QGraphicsScene>
#include "NodeGraphicsObject.hpp"
namespace QtNodes {
NodeGraphicsObject *locateNodeAt(QPointF scenePoint,
QGraphicsScene &scene,
QTransform const &viewTransform)
{
// items under cursor
QList<QGraphicsItem *> items = scene.items(scenePoint,
Qt::IntersectsItemShape,
Qt::DescendingOrder,
viewTransform);
// items convertable to NodeGraphicsObject
std::vector<QGraphicsItem *> filteredItems;
std::copy_if(items.begin(),
items.end(),
std::back_inserter(filteredItems),
[](QGraphicsItem *item) {
return (qgraphicsitem_cast<NodeGraphicsObject *>(item) != nullptr);
});
NodeGraphicsObject *node = nullptr;
if (!filteredItems.empty()) {
QGraphicsItem *graphicsItem = filteredItems.front();
node = dynamic_cast<NodeGraphicsObject *>(graphicsItem);
}
return node;
}
} // namespace QtNodes