├── CHANGELOG.rst ├── .gitignore ├── doc └── example_editor01.png ├── examples └── simple-data-forwarding │ ├── test_qobjects.cpp │ ├── test_qobjects.hpp │ ├── CMakeLists.txt │ ├── mainwindow.hpp │ ├── main.cpp │ └── mainwindow.cpp ├── src ├── graphicsnodedefs.hpp ├── graphicsnodescene.hpp ├── graphicsnode_p.h ├── graphicsbezieredge_p.h ├── qnodeview.h ├── CMakeLists.txt ├── qtypecoloriserproxy.h ├── graphicsbezieredge.hpp ├── qnodewidget.h ├── qtypecoloriserproxy.cpp ├── graphicsnodeview.hpp ├── qnodeview.cpp ├── qmodeldatalistdecoder.h ├── graphicsnodescene.cpp ├── graphicsnodesocket.hpp ├── graphicsnodesocket_p.h ├── graphicsnode.hpp ├── qmultimodeltree.h ├── qnodeeditorsocketmodel.h ├── qreactiveproxymodel.h ├── qobjectmodel.h ├── qmodeldatalistdecoder.cpp ├── qnodewidget.cpp ├── graphicsbezieredge.cpp ├── graphicsnodesocket.cpp ├── graphicsnodeview.cpp ├── qmultimodeltree.cpp ├── qobjectmodel.cpp ├── qreactiveproxymodel.cpp ├── graphicsnode.cpp └── qnodeeditorsocketmodel.cpp ├── TODO.rst ├── .travis.yml ├── CMakeLists.txt ├── README.rst └── LICENSE /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | -------------------------------------------------------------------------------- /doc/example_editor01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nwaniek/qt5-node-editor/HEAD/doc/example_editor01.png -------------------------------------------------------------------------------- /examples/simple-data-forwarding/test_qobjects.cpp: -------------------------------------------------------------------------------- 1 | #include "test_qobjects.hpp" 2 | 3 | QString testnode1::getTXT() const 4 | { 5 | return m_txt; 6 | } 7 | 8 | void testnode1::setTXT(QString arg) 9 | { 10 | if (m_txt == arg) 11 | return; 12 | 13 | m_txt = arg; 14 | emit txtChanged(arg); 15 | } 16 | -------------------------------------------------------------------------------- /src/graphicsnodedefs.hpp: -------------------------------------------------------------------------------- 1 | /* See LICENSE file for copyright and license details. */ 2 | 3 | #ifndef __GRAPHICSNODEDEFS_HPP__49761BBD_1BA5_49AC_8C23_88079EED41F1 4 | #define __GRAPHICSNODEDEFS_HPP__49761BBD_1BA5_49AC_8C23_88079EED41F1 5 | 6 | #include 7 | 8 | enum GraphicsNodeItemTypes { 9 | TypeNode = QGraphicsItem::UserType + 1, 10 | TypeBezierEdge = QGraphicsItem::UserType + 2, 11 | TypeSocket = QGraphicsItem::UserType + 3 12 | }; 13 | 14 | #endif /* __GRAPHICSNODEDEFS_HPP__49761BBD_1BA5_49AC_8C23_88079EED41F1 */ 15 | 16 | -------------------------------------------------------------------------------- /examples/simple-data-forwarding/test_qobjects.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __TEST_QOBJECTS_HPP__AF779D98_80FC_4E47_BAC8_C919FE80A131 2 | #define __TEST_QOBJECTS_HPP__AF779D98_80FC_4E47_BAC8_C919FE80A131 3 | 4 | #include 5 | 6 | class testnode1: public QObject 7 | { 8 | Q_OBJECT 9 | Q_PROPERTY(QString txt READ getTXT WRITE setTXT NOTIFY txtChanged USER true) 10 | public: 11 | QString getTXT() const; 12 | public slots: 13 | void setTXT(QString arg); 14 | signals: 15 | void txtChanged(QString arg); 16 | private: 17 | QString m_txt; 18 | }; 19 | 20 | #endif /* __TEST_QOBJECTS_HPP__AF779D98_80FC_4E47_BAC8_C919FE80A131 */ 21 | 22 | -------------------------------------------------------------------------------- /TODO.rst: -------------------------------------------------------------------------------- 1 | TODO 2 | ==== 3 | 4 | * Provide installer for library header files 5 | * Qt naming scheme of methods, variables, etc. 6 | * develop data model 7 | * moving some of the UI logic to the data model (node.hpp and node.cpp) 8 | * item ordering (bringing nodes to front, and keeping them there) 9 | * edge /node managers 10 | * have lists of all nodes, may come in handy to link with a data model 11 | 12 | 13 | DONE 14 | ==== 15 | 16 | * build as library 17 | * restructure directory (separate examples/implementation) 18 | * minimum size constraints 19 | * node titles 20 | * resizable nodes 21 | * draggable edges 22 | * other widgets embeddable on each node (setCentralWidget) 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: required 3 | language: cpp 4 | 5 | # build matrix 6 | 7 | os: 8 | - osx 9 | - linux 10 | 11 | compiler: 12 | - g++ 13 | - clang 14 | 15 | env: 16 | - BUILD_TYPE=Debug 17 | - BUILD_TYPE=Release 18 | 19 | matrix: 20 | exclude: 21 | os: osx 22 | compiler: g++ 23 | 24 | before_install: 25 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update && brew install qt cmake; fi 26 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get update -qq && sudo apt-get install cmake qtbase5-dev; fi 27 | 28 | install: 29 | - mkdir build 30 | - cd build 31 | - cmake .. -DCMAKE_BUILD_TYPE=$BUILD_TYPE 32 | - make 33 | -------------------------------------------------------------------------------- /examples/simple-data-forwarding/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 2.8.8) 2 | project(simple-data-forwarding) 3 | 4 | add_definitions(-std=c++11) 5 | set(CMAKE_BUILD_TYPE DEBUG) 6 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -pedantic") 7 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -pedantic") 8 | set(CMAKE_AUTOMOC ON) 9 | 10 | find_package(Qt5Widgets REQUIRED) 11 | 12 | set(SRC 13 | main.cpp 14 | mainwindow.cpp 15 | test_qobjects.cpp 16 | ) 17 | set(HEADERS 18 | mainwindow.hpp 19 | test_qobjects.hpp 20 | ) 21 | 22 | add_executable(${PROJECT_NAME} ${SRC} ${HEADERS_MOC} ${HEADERS}) 23 | target_link_libraries(${PROJECT_NAME} 24 | Qt5NodeEditor 25 | ) 26 | qt5_use_modules(${PROJECT_NAME} Widgets) 27 | -------------------------------------------------------------------------------- /src/graphicsnodescene.hpp: -------------------------------------------------------------------------------- 1 | /* See LICENSE file for copyright and license details. */ 2 | 3 | #ifndef __GRAPHICSNODESCENE_HPP__7F9E4C1E_8F4E_4BD2_BDF7_3D4ECEC206B5 4 | #define __GRAPHICSNODESCENE_HPP__7F9E4C1E_8F4E_4BD2_BDF7_3D4ECEC206B5 5 | 6 | #include 7 | #include 8 | 9 | 10 | class GraphicsNodeScene : public QGraphicsScene 11 | { 12 | Q_OBJECT 13 | 14 | public: 15 | GraphicsNodeScene(QObject *parent); 16 | 17 | protected: 18 | virtual void drawBackground(QPainter *painter, const QRectF &rect) override; 19 | 20 | private: 21 | QColor _color_background; 22 | QColor _color_light; 23 | QColor _color_dark; 24 | 25 | QPen _pen_light; 26 | QPen _pen_dark; 27 | 28 | QBrush _brush_background; 29 | }; 30 | 31 | #endif /* __GRAPHICSNODESCENE_HPP__7F9E4C1E_8F4E_4BD2_BDF7_3D4ECEC206B5 */ 32 | 33 | -------------------------------------------------------------------------------- /src/graphicsnode_p.h: -------------------------------------------------------------------------------- 1 | #ifndef GRAPHICS_NODE_P_H 2 | #define GRAPHICS_NODE_P_H 3 | 4 | #include 5 | 6 | class NodeGraphicsItem : public QGraphicsItem 7 | { 8 | friend class GraphicsNode; // to access the protected methods 9 | friend class GraphicsNodePrivate; // to call prepareUpdate 10 | public: 11 | NodeGraphicsItem(QGraphicsItem* parent) : QGraphicsItem(parent) {} 12 | 13 | virtual int type() const override; 14 | 15 | virtual QRectF boundingRect() const override; 16 | virtual void paint(QPainter *painter, 17 | const QStyleOptionGraphicsItem *option, 18 | QWidget *widget = 0) override; 19 | 20 | GraphicsNodePrivate* d_ptr; 21 | GraphicsNode* q_ptr; 22 | 23 | protected: 24 | virtual void mousePressEvent(QGraphicsSceneMouseEvent *event) override; 25 | virtual QVariant itemChange(GraphicsItemChange change, const QVariant &value) override; 26 | }; 27 | 28 | #endif 29 | -------------------------------------------------------------------------------- /examples/simple-data-forwarding/mainwindow.hpp: -------------------------------------------------------------------------------- 1 | /* See LICENSE file for copyright and license details. */ 2 | 3 | #ifndef __MAINWINDOW_HPP__0E042426_E73E_4382_BC86_DE7F000B57CC 4 | #define __MAINWINDOW_HPP__0E042426_E73E_4382_BC86_DE7F000B57CC 5 | 6 | #include 7 | #include 8 | 9 | class QMenu; 10 | class QAction; 11 | class QResizeEvent; 12 | class GraphicsNodeView; 13 | class GraphicsNodeScene; 14 | 15 | 16 | class MainWindow : public QMainWindow 17 | { 18 | Q_OBJECT 19 | 20 | public: 21 | MainWindow(); 22 | 23 | protected: 24 | virtual void resizeEvent(QResizeEvent *event); 25 | 26 | public slots: 27 | void onClearTriggered(); 28 | 29 | 30 | private: 31 | void addNodeViews(); 32 | void addFakeContent(); 33 | 34 | GraphicsNodeView *_view; 35 | GraphicsNodeScene *_scene; 36 | 37 | QMenu *_mnuFile; 38 | QAction *_actClear; 39 | 40 | QPainterPath _path; 41 | }; 42 | 43 | #endif /* __MAINWINDOW_HPP__0E042426_E73E_4382_BC86_DE7F000B57CC */ 44 | 45 | -------------------------------------------------------------------------------- /src/graphicsbezieredge_p.h: -------------------------------------------------------------------------------- 1 | #ifndef GRAPHICS_DIRECTED_EDGE_PRIVATE_H 2 | #define GRAPHICS_DIRECTED_EDGE_PRIVATE_H 3 | 4 | #include 5 | #include 6 | 7 | class GraphicsEdgeItem; 8 | class QNodeEditorEdgeModel; 9 | 10 | class GraphicsDirectedEdgePrivate final 11 | { 12 | public: 13 | explicit GraphicsDirectedEdgePrivate(GraphicsDirectedEdge* q) : q_ptr(q) {} 14 | 15 | //TODO support effect lazy loading 16 | #if 0 17 | QGraphicsDropShadowEffect *_effect {new QGraphicsDropShadowEffect()}; 18 | #endif 19 | 20 | QPen _pen {QColor("#00FF00")}; 21 | QPointF _start; 22 | QPointF _stop; 23 | qreal _factor; 24 | 25 | GraphicsEdgeItem *m_pGrpahicsItem {nullptr}; 26 | 27 | GraphicsDirectedEdge* q_ptr; 28 | 29 | QNodeEditorEdgeModel* m_pModel; 30 | 31 | QPersistentModelIndex m_Index; 32 | 33 | // Helpers 34 | 35 | void setStart(QPointF p); 36 | void setStop(QPointF p); 37 | }; 38 | 39 | #endif 40 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 2.8.8) 2 | project(qnodeeditor) 3 | 4 | add_definitions(-std=c++11) 5 | set(CMAKE_BUILD_TYPE DEBUG) 6 | 7 | # set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -pedantic") 8 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -pedantic") 9 | 10 | # add additional warnings when compiling with gcc or clang 11 | if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_COMPILER_IS_GNUCC OR (CMAKE_C_COMPILER_ID MATCHES "Clang") OR (CMAKE_CXX_COMPILER_ID MATCHES "Clang")) 12 | # set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wextra") 13 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wextra") 14 | endif(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_COMPILER_IS_GNUCC OR (CMAKE_C_COMPILER_ID MATCHES "Clang") OR (CMAKE_CXX_COMPILER_ID MATCHES "Clang")) 15 | 16 | set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) 17 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) 18 | 19 | # main library 20 | include_directories(src) 21 | add_subdirectory(src) 22 | 23 | # demo applications 24 | # add_subdirectory(examples/simple-data-forwarding) 25 | -------------------------------------------------------------------------------- /src/qnodeview.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "graphicsnodeview.hpp" 4 | 5 | class QAbstractItemModel; 6 | class GraphicsNodeScene; 7 | class QReactiveProxyModel; 8 | class QNodeViewPrivate; 9 | 10 | class GraphicsNode; 11 | 12 | //TODO use a QAbstractItemView as the base class... 13 | 14 | class Q_DECL_EXPORT QNodeView : public GraphicsNodeView 15 | { 16 | Q_OBJECT 17 | public: 18 | explicit QNodeView(QWidget* parent = Q_NULLPTR); 19 | virtual ~QNodeView(); 20 | 21 | void setModel(QAbstractItemModel* m); 22 | 23 | GraphicsNode* getNode(const QModelIndex& idx) const; 24 | 25 | Q_INVOKABLE QAbstractItemModel *sinkSocketModel(const QModelIndex& node) const; 26 | Q_INVOKABLE QAbstractItemModel *sourceSocketModel(const QModelIndex& node) const; 27 | 28 | GraphicsNodeScene* scene() const; //TODO remove 29 | 30 | QReactiveProxyModel* reactiveModel() const; //TODO try to find a better way 31 | 32 | QAbstractItemModel* edgeModel() const; 33 | 34 | private: 35 | QNodeViewPrivate* d_ptr; 36 | Q_DECLARE_PRIVATE(QNodeView) 37 | }; 38 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 2.8.8) 2 | project(Qt5NodeEditor) 3 | 4 | add_definitions(-std=c++11) 5 | set(CMAKE_BUILD_TYPE DEBUG) 6 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -pedantic") 7 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -pedantic") 8 | set(CMAKE_INCLUDE_CURRENT_DIR ON) 9 | set(CMAKE_AUTOMOC ON) 10 | 11 | find_package(Qt5Widgets REQUIRED) 12 | 13 | set(SRC 14 | graphicsbezieredge.cpp 15 | graphicsnode.cpp 16 | graphicsnodeview.cpp 17 | graphicsnodescene.cpp 18 | graphicsnodesocket.cpp 19 | qobjectmodel.cpp 20 | qmultimodeltree.cpp 21 | qreactiveproxymodel.cpp 22 | qnodeview.cpp 23 | qnodewidget.cpp 24 | qnodeeditorsocketmodel.cpp 25 | qmodeldatalistdecoder.cpp 26 | qtypecoloriserproxy.cpp 27 | ) 28 | set(HEADERS 29 | graphicsbezieredge.hpp 30 | graphicsnode.hpp 31 | graphicsnodeview.hpp 32 | graphicsnodescene.hpp 33 | graphicsnodesocket.hpp 34 | graphicsnodedefs.hpp 35 | ) 36 | 37 | add_library(qnodeeditor SHARED ${SRC} ${HEADERS_MOC} ${HEADERS}) 38 | qt5_use_modules(qnodeeditor Widgets) 39 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Qt5 Node Editor 2 | =============== 3 | 4 | an early version of a node editor written in Qt and C++11. may still contain 5 | lots of bugs, and some logical issues. upcomming changes will contain (but will 6 | not be limited to): 7 | 8 | * Qt naming scheme of methods, variables, etc. 9 | * develop data model. At the moment it's just VC instead of MVC 10 | * moving some of the UI logic to the data model (node.hpp and node.cpp) 11 | * restructuring the directory 12 | * providing cmake file to generate a library 13 | 14 | Hence, *be warned* that the API is not settled yet and is likely to change in 15 | future releases. 16 | 17 | Example Screenshot: 18 | 19 | .. figure:: doc/example_editor01.png 20 | 21 | 22 | 23 | How to build 24 | ------------ 25 | 26 | You need cmake (>= 2.8.8), and a recent C++ compiler. For an out-of-directory 27 | build (recommended), create a build folder, and call cmake/make in there: 28 | 29 | .. code:: 30 | 31 | $ mkdir build 32 | $ cd build 33 | $ cmake .. 34 | $ make 35 | 36 | The library will end up in lib/ of your build directory, all compiled examples 37 | in bin/. 38 | -------------------------------------------------------------------------------- /src/qtypecoloriserproxy.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | class QTypeColoriserProxyPrivate; 6 | 7 | /** 8 | * Set the background and foreground color based on a role QMetaType. 9 | */ 10 | class Q_DECL_EXPORT QTypeColoriserProxy : public QIdentityProxyModel 11 | { 12 | Q_OBJECT 13 | public: 14 | explicit QTypeColoriserProxy(QObject* parent); 15 | virtual ~QTypeColoriserProxy(); 16 | 17 | virtual QVariant data(const QModelIndex& idx, int role) const override; 18 | 19 | int baseRole() const; 20 | void setBaseRole(int role); 21 | 22 | void setForegroundRole(quint32 typeId, const QVariant& value); 23 | void setBackgroundRole(quint32 typeId, const QVariant& value); 24 | 25 | template 26 | void setForegroundRole(const QVariant& value) { 27 | setForegroundRole(qMetaTypeId(), value); 28 | } 29 | 30 | template 31 | void setBackgroundRole(const QVariant& value) { 32 | setBackgroundRole(qMetaTypeId(), value); 33 | } 34 | 35 | private: 36 | QTypeColoriserProxyPrivate* d_ptr; 37 | Q_DECLARE_PRIVATE(QTypeColoriserProxy) 38 | }; 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT/X Consortium License 2 | 3 | Copyright © 2015-2016 Nicolai Waniek 4 | Copyright © 2016 Emmanuel Lepage Vallée 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a 7 | copy of this software and associated documentation files (the 8 | "Software"), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included 15 | in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 18 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 21 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 22 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | -------------------------------------------------------------------------------- /examples/simple-data-forwarding/main.cpp: -------------------------------------------------------------------------------- 1 | /* See LICENSE file for copyright and license details. */ 2 | 3 | #include 4 | #include 5 | #include "mainwindow.hpp" 6 | 7 | int main(int argc, char *argv[]) { 8 | QApplication app(argc, argv); 9 | 10 | // set a dark color theme 11 | /* 12 | app.setStyle(QStyleFactory::create("Fusion")); 13 | QPalette darkPalette; 14 | darkPalette.setColor(QPalette::Window, QColor(53,53,53)); 15 | darkPalette.setColor(QPalette::WindowText, Qt::white); 16 | darkPalette.setColor(QPalette::Base, QColor(25,25,25)); 17 | darkPalette.setColor(QPalette::AlternateBase, QColor(53,53,53)); 18 | darkPalette.setColor(QPalette::ToolTipBase, Qt::white); 19 | darkPalette.setColor(QPalette::ToolTipText, Qt::white); 20 | darkPalette.setColor(QPalette::Text, Qt::white); 21 | darkPalette.setColor(QPalette::Button, QColor(53,53,53)); 22 | darkPalette.setColor(QPalette::ButtonText, Qt::white); 23 | darkPalette.setColor(QPalette::BrightText, Qt::red); 24 | darkPalette.setColor(QPalette::Link, QColor(42, 130, 218)); 25 | 26 | darkPalette.setColor(QPalette::Highlight, QColor(42, 130, 218)); 27 | darkPalette.setColor(QPalette::HighlightedText, Qt::black); 28 | 29 | app.setPalette(darkPalette); 30 | app.setStyleSheet("QToolTip { color: #ffffff; background-color: #2a82da; border: 1px solid white; }"); 31 | */ 32 | 33 | MainWindow win; 34 | win.show(); 35 | return app.exec(); 36 | } 37 | -------------------------------------------------------------------------------- /src/graphicsbezieredge.hpp: -------------------------------------------------------------------------------- 1 | /* See LICENSE file for copyright and license details. */ 2 | 3 | #ifndef GRAPHICS_DIRECTED_EDGE_H 4 | #define GRAPHICS_DIRECTED_EDGE_H 5 | 6 | #include 7 | #include "graphicsnodedefs.hpp" 8 | 9 | class GraphicsNodeSocket; 10 | 11 | class GraphicsDirectedEdgePrivate; 12 | 13 | class QNodeEditorEdgeModel; 14 | 15 | class GraphicsDirectedEdge : public QObject 16 | { 17 | //TODO once the point is stored in the index those 3 can go away 18 | friend class GraphicsNodeView; //To allow intermediate positions 19 | friend class GraphicsNodeSocketPrivate; //To allow intermediate positions 20 | 21 | friend class QNodeEditorSocketModelPrivate; // to notify changes 22 | 23 | Q_OBJECT 24 | public: 25 | 26 | virtual ~GraphicsDirectedEdge(); 27 | 28 | void update(); 29 | 30 | QGraphicsItem *graphicsItem() const; 31 | 32 | void setSink(const QModelIndex& idx); 33 | void setSource(const QModelIndex& idx); 34 | 35 | QModelIndex index() const; 36 | 37 | protected: 38 | // It cannot be constructed by itself or the user 39 | explicit GraphicsDirectedEdge(QNodeEditorEdgeModel* m, const QModelIndex& index, qreal factor=0.5); 40 | 41 | GraphicsDirectedEdgePrivate* d_ptr; 42 | Q_DECLARE_PRIVATE(GraphicsDirectedEdge) 43 | }; 44 | 45 | class GraphicsBezierEdge : public GraphicsDirectedEdge 46 | { 47 | friend class EdgeWrapper; // to create 48 | public: 49 | 50 | protected: 51 | explicit GraphicsBezierEdge(QNodeEditorEdgeModel* m, const QModelIndex& index, qreal factor=0.5); 52 | }; 53 | 54 | #endif /* GRAPHICS_DIRECTED_EDGE_H */ 55 | 56 | -------------------------------------------------------------------------------- /src/qnodewidget.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "qnodeview.h" 4 | 5 | #include 6 | 7 | class QNodeWidgetPrivate; 8 | 9 | class QAbstractItemModel; 10 | class GraphicsNode; 11 | 12 | class Q_DECL_EXPORT QNodeWidget : public QNodeView 13 | { 14 | Q_OBJECT 15 | public: 16 | enum class ObjectFlags { 17 | NONE = 0, 18 | USER_PROPERTIES = 1 << 0, 19 | DESIGNABLE_PROPERTIES = 1 << 1, 20 | CLASS_PROPERTIES = 1 << 2, 21 | INHERITED_PROPERTIES = 1 << 3, 22 | SIGNALS = 1 << 4, 23 | SLOTS = 1 << 5, 24 | CLASS_METHODS = 1 << 6, 25 | INHERITED_METHODS = 1 << 7 26 | }; 27 | 28 | explicit QNodeWidget(QWidget* parent = Q_NULLPTR); 29 | virtual ~QNodeWidget(); 30 | 31 | GraphicsNode* addObject( 32 | QObject* o, 33 | const QString& title = QString(), 34 | ObjectFlags f = ObjectFlags::NONE, 35 | const QVariant& uid = {} 36 | ); 37 | 38 | GraphicsNode* addModel( 39 | QAbstractItemModel* m, 40 | const QString& title = QString(), 41 | const QVariant& uid = {} 42 | ); 43 | 44 | Q_SIGNALS: 45 | // Removing 46 | void objectRemoved(QObject* o); 47 | void modelRemoved(QAbstractItemModel* m); 48 | 49 | // Renaming 50 | void objectRenamed(QObject* o, const QString& nameName, const QString& oldName); 51 | void modelRenamed(QAbstractItemModel* m, const QString& nameName, const QString& oldName); 52 | void nodeRenamed(GraphicsNode* n, const QString& nameName, const QString& oldName); 53 | void nodeRenamed(const QString& uid, const QString& nameName, const QString& oldName); 54 | 55 | private: 56 | QNodeWidgetPrivate* d_ptr; 57 | Q_DECLARE_PRIVATE(QNodeWidget) 58 | }; 59 | -------------------------------------------------------------------------------- /src/qtypecoloriserproxy.cpp: -------------------------------------------------------------------------------- 1 | #include "qtypecoloriserproxy.h" 2 | 3 | #include 4 | 5 | class QTypeColoriserProxyPrivate 6 | { 7 | public: 8 | int m_Role {Qt::EditRole}; 9 | 10 | QHash m_lBg; 11 | QHash m_lFg; 12 | }; 13 | 14 | QTypeColoriserProxy::QTypeColoriserProxy(QObject* parent) : QIdentityProxyModel(parent), 15 | d_ptr(new QTypeColoriserProxyPrivate) 16 | { 17 | } 18 | 19 | QVariant QTypeColoriserProxy::data(const QModelIndex& idx, int role) const 20 | { 21 | if (!idx.isValid()) 22 | return {}; 23 | 24 | switch(role) { 25 | case Qt::BackgroundRole: { 26 | const auto t = idx.data(d_ptr->m_Role).userType(); 27 | if (d_ptr->m_lBg.contains(t)) 28 | return d_ptr->m_lBg[t]; 29 | } 30 | break; 31 | case Qt::ForegroundRole: { 32 | const auto t = idx.data(d_ptr->m_Role).userType(); 33 | if (d_ptr->m_lFg.contains(t)) 34 | return d_ptr->m_lFg[idx.data(d_ptr->m_Role).userType()]; 35 | } 36 | break; 37 | }; 38 | 39 | return QIdentityProxyModel::data(idx, role); 40 | } 41 | 42 | QTypeColoriserProxy::~QTypeColoriserProxy() 43 | { 44 | delete d_ptr; 45 | } 46 | 47 | int QTypeColoriserProxy::baseRole() const 48 | { 49 | return d_ptr->m_Role; 50 | } 51 | 52 | void QTypeColoriserProxy::setBaseRole(int role) 53 | { 54 | d_ptr->m_Role = role; 55 | Q_EMIT dataChanged(index(0,0), index(rowCount()-1, columnCount()-1)); 56 | } 57 | 58 | void QTypeColoriserProxy::setForegroundRole(quint32 typeId, const QVariant& value) 59 | { 60 | d_ptr->m_lFg[typeId] = value; 61 | Q_EMIT dataChanged(index(0,0), index(rowCount()-1, columnCount()-1)); 62 | } 63 | 64 | void QTypeColoriserProxy::setBackgroundRole(quint32 typeId, const QVariant& value) 65 | { 66 | d_ptr->m_lBg[typeId] = value; 67 | Q_EMIT dataChanged(index(0,0), index(rowCount()-1, columnCount()-1)); 68 | } 69 | -------------------------------------------------------------------------------- /src/graphicsnodeview.hpp: -------------------------------------------------------------------------------- 1 | /* See LICENSE file for copyright and license details. */ 2 | 3 | #ifndef __GRAPHICSNODEVIEW_HPP__59C6610F_3283_42A1_9102_38A7065DB718 4 | #define __GRAPHICSNODEVIEW_HPP__59C6610F_3283_42A1_9102_38A7065DB718 5 | 6 | #include 7 | #include 8 | 9 | class QMimeData; 10 | class QResizeEvent; 11 | class GraphicsNode; 12 | class GraphicsDirectedEdge; 13 | class GraphicsNodeSocket; 14 | 15 | //HACK this is totally *not* fine to put the architecture on its head 16 | class QNodeEditorEdgeModel; 17 | class QNodeEditorSocketModel; 18 | 19 | struct EdgeDragEvent 20 | { 21 | // encode what the user is actually doing. 22 | enum drag_mode { 23 | // connect a new edge to a source or sink 24 | to_source, 25 | to_sink, 26 | }; 27 | 28 | GraphicsDirectedEdge *e; 29 | QMimeData* mimeData; 30 | drag_mode mode; 31 | }; 32 | 33 | 34 | struct NodeResizeEvent 35 | { 36 | GraphicsNode *node; 37 | qreal orig_width, orig_height; 38 | QPoint pos; 39 | }; 40 | 41 | 42 | class GraphicsNodeView : public QGraphicsView 43 | { 44 | Q_OBJECT 45 | public: 46 | explicit GraphicsNodeView(QWidget *parent = nullptr); 47 | GraphicsNodeView(QGraphicsScene *scene, QWidget *parent = nullptr); 48 | 49 | protected: 50 | virtual void wheelEvent(QWheelEvent *event); 51 | virtual void mouseMoveEvent(QMouseEvent *event); 52 | virtual void mousePressEvent(QMouseEvent *event); 53 | virtual void mouseReleaseEvent(QMouseEvent *event); 54 | virtual void resizeEvent(QResizeEvent *event); 55 | 56 | QNodeEditorSocketModel* m_pModel {nullptr}; //HACK evil workaround until the QAbstractItemView is added 57 | 58 | private: 59 | void middleMouseButtonPress(QMouseEvent *event); 60 | void leftMouseButtonPress(QMouseEvent *event); 61 | 62 | void middleMouseButtonRelease(QMouseEvent *event); 63 | void leftMouseButtonRelease(QMouseEvent *event); 64 | 65 | bool can_accept_edge(GraphicsNodeSocket *sock); 66 | GraphicsNodeSocket* socket_at(QPoint pos); 67 | 68 | private: 69 | EdgeDragEvent *_drag_event = nullptr; 70 | NodeResizeEvent *_resize_event = nullptr; 71 | 72 | }; 73 | 74 | #endif /* __GRAPHICSNODEVIEW_HPP__59C6610F_3283_42A1_9102_38A7065DB718 */ 75 | 76 | -------------------------------------------------------------------------------- /src/qnodeview.cpp: -------------------------------------------------------------------------------- 1 | #include "qnodeview.h" 2 | 3 | #include 4 | 5 | #include "graphicsnode.hpp" 6 | 7 | #include "qreactiveproxymodel.h" 8 | #include "graphicsnodescene.hpp" 9 | #include "graphicsnodesocket.hpp" 10 | 11 | #include "qnodeeditorsocketmodel.h" 12 | 13 | class QNodeViewPrivate final : public QObject 14 | { 15 | public: 16 | explicit QNodeViewPrivate(QObject* p) : QObject(p) {} 17 | 18 | QReactiveProxyModel m_Proxy {this }; 19 | QAbstractItemModel* m_pModel {Q_NULLPTR}; 20 | QVector m_lNodes { }; 21 | GraphicsNodeScene m_Scene {this }; 22 | QNodeEditorSocketModel* m_pFactory{Q_NULLPTR}; 23 | }; 24 | 25 | QNodeView::QNodeView(QWidget* parent) : GraphicsNodeView(parent), 26 | d_ptr(new QNodeViewPrivate(this)) 27 | { 28 | d_ptr->m_pFactory = new QNodeEditorSocketModel(&d_ptr->m_Proxy, &d_ptr->m_Scene); 29 | 30 | m_pModel = d_ptr->m_pFactory; //HACK to remove 31 | 32 | setScene(&d_ptr->m_Scene); 33 | } 34 | 35 | QNodeView::~QNodeView() 36 | { 37 | delete d_ptr; 38 | } 39 | 40 | GraphicsNodeScene* QNodeView::scene() const 41 | { 42 | return &d_ptr->m_Scene; 43 | } 44 | 45 | void QNodeView::setModel(QAbstractItemModel* m) 46 | { 47 | d_ptr->m_pModel = m; 48 | d_ptr->m_Proxy.setSourceModel(m); 49 | } 50 | 51 | GraphicsNode* QNodeView::getNode(const QModelIndex& idx) const 52 | { 53 | if (!idx.isValid()) 54 | return Q_NULLPTR; 55 | 56 | const auto factoryIdx = d_ptr->m_pFactory->mapFromSource( 57 | d_ptr->m_Proxy.mapFromSource(idx) 58 | ); 59 | 60 | Q_ASSERT(factoryIdx.isValid()); 61 | 62 | return d_ptr->m_pFactory->getNode(factoryIdx); 63 | } 64 | 65 | QReactiveProxyModel* QNodeView::reactiveModel() const 66 | { 67 | return &d_ptr->m_Proxy; 68 | } 69 | 70 | QAbstractItemModel *QNodeView::sinkSocketModel(const QModelIndex& node) const 71 | { 72 | return d_ptr->m_pFactory->sinkSocketModel(node); 73 | } 74 | 75 | QAbstractItemModel *QNodeView::sourceSocketModel(const QModelIndex& node) const 76 | { 77 | return d_ptr->m_pFactory->sourceSocketModel(node); 78 | } 79 | 80 | QAbstractItemModel* QNodeView::edgeModel() const 81 | { 82 | return d_ptr->m_pFactory->edgeModel(); 83 | } 84 | -------------------------------------------------------------------------------- /src/qmodeldatalistdecoder.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | class QMimeData; 7 | 8 | class QModelDataListDecoderPrivate; 9 | 10 | /* 11 | * The default model implementation adds the `application/x-qabstractitemmodeldatalist` 12 | * MIME Type. This things is totally undocumented, but some reverse engineering 13 | * of it's encoder indicate it is (as of Qt 5.6) an array of: 14 | * 15 | * Tuple> 16 | * 17 | * pushed in a `QDataStream`. This format suck, as it's not really able to 18 | * express the source `QModelIndex`. However, it does contain the QVariant 19 | * of some of its role. From them, even the invalid ones, the QMetaType id 20 | * should be pushed into the stream. It should have been easy, but there 21 | * is another problem. QVariant::load exits early when decoding a QMetaType 22 | * that cannot be encoder. While it prints the QMetaType on stderr, it doesn't 23 | * export it. This, in turn, causes another problem where the QMap will be empty 24 | * if a single element fail to be deserialized. This little class implements a 25 | * serializable Qt type that mimics the QVariant decoder to be able to extract 26 | * the correct type. 27 | * 28 | * The QVariant data is encoded as (it is stable and documented): 29 | * 30 | * * The type of the data (quint32) 31 | * * The null flag (qint8) 32 | * * The data of the specified type 33 | * 34 | * Reference: 35 | * 36 | * * http://doc.qt.io/qt-5/datastreamformat.html 37 | * * qvariant.cpp 38 | * * qabstractitemmodel.cpp 39 | */ 40 | class QModelDataListDecoder 41 | { 42 | public: 43 | explicit QModelDataListDecoder(const QMimeData* data); 44 | virtual ~QModelDataListDecoder(); 45 | 46 | bool canConvert(quint32 typeId, int role = Qt::EditRole, int row = -1, int column = -1) const; 47 | 48 | template 49 | bool canConvert(int role = Qt::EditRole) const { 50 | return canConvert(qMetaTypeId(), role); 51 | } 52 | 53 | QVariant data(int role, int row = -1, int column = -1) const; 54 | 55 | int count(); 56 | 57 | QPair firstElement() const; 58 | 59 | quint32 typeId(int role = Qt::EditRole, int row = -1, int column = -1) const; 60 | 61 | private: 62 | QModelDataListDecoderPrivate* d_ptr; 63 | }; 64 | 65 | -------------------------------------------------------------------------------- /src/graphicsnodescene.cpp: -------------------------------------------------------------------------------- 1 | /* See LICENSE file for copyright and license details. */ 2 | 3 | #include "graphicsnodescene.hpp" 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | // TODO: move to graphicsnodeview. use graphicsnodescene for management 11 | 12 | GraphicsNodeScene::GraphicsNodeScene(QObject *parent) 13 | : QGraphicsScene(parent) 14 | , _color_background(QColor("#393939")) 15 | , _color_light(QColor("#2F2F2F")) 16 | , _color_dark(QColor("#292929")) 17 | , _pen_light(QPen(_color_light)) 18 | , _pen_dark(QPen(_color_dark)) 19 | , _brush_background(_color_background) 20 | { 21 | // initialize default pen settings 22 | for (auto p : {&_pen_light, &_pen_dark}) { 23 | p->setWidth(0); 24 | } 25 | 26 | // initialize the background 27 | setBackgroundBrush(_brush_background); 28 | } 29 | 30 | 31 | /* 32 | * TODO: move the visualization into the graphicsview, and move all the GUI 33 | * logic into the graphicsnodescene 34 | */ 35 | void GraphicsNodeScene:: 36 | drawBackground(QPainter *painter, const QRectF &rect) 37 | { 38 | // call parent method 39 | QGraphicsScene::drawBackground(painter, rect); 40 | 41 | // augment the painted with grid 42 | const int gridsize = 20; 43 | auto left = static_cast(std::floor(rect.left())); 44 | auto right = static_cast(std::ceil(rect.right())); 45 | auto top = static_cast(std::floor(rect.top())); 46 | auto bottom = static_cast(std::ceil(rect.bottom())); 47 | 48 | // compute indices of lines to draw 49 | const auto first_left = left - (left % gridsize); 50 | const auto first_top = top - (top % gridsize); 51 | 52 | // compute lines to draw and 53 | std::vector lines_light; 54 | std::vector lines_dark; 55 | for (auto x = first_left; x <= right; x += gridsize) { 56 | if (x % 100 != 0) 57 | lines_light.push_back(QLine(x, top, x, bottom)); 58 | else 59 | lines_dark.push_back(QLine(x, top, x, bottom)); 60 | } 61 | for (auto y = first_top; y <= bottom; y += gridsize) { 62 | if (y % 100 != 0) 63 | lines_light.push_back(QLine(left, y, right, y)); 64 | else 65 | lines_dark.push_back(QLine(left, y, right, y)); 66 | } 67 | 68 | // draw calls 69 | painter->setPen(_pen_light); 70 | painter->drawLines(lines_light.data(), lines_light.size()); 71 | 72 | painter->setPen(_pen_dark); 73 | painter->drawLines(lines_dark.data(), lines_dark.size()); 74 | } 75 | 76 | -------------------------------------------------------------------------------- /src/graphicsnodesocket.hpp: -------------------------------------------------------------------------------- 1 | /* See LICENSE file for copyright and license details. */ 2 | 3 | #ifndef GRAPHICS_NODE_SOCKET_H 4 | #define GRAPHICS_NODE_SOCKET_H 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #include "graphicsnodedefs.hpp" 11 | 12 | class QGraphicsSceneMouseEvent; 13 | class QGraphicsSceneDragDropEvent; 14 | class GraphicsDirectedEdge; 15 | 16 | class GraphicsNode; 17 | class QNodeEditorSocketModel; 18 | 19 | class GraphicsNodeSocketPrivate; 20 | 21 | /** 22 | * visual representation of a socket. the visual representation consists of a 23 | * circle for User Interaction and a label 24 | */ 25 | class GraphicsNodeSocket : public QObject 26 | { 27 | Q_OBJECT 28 | friend class GraphicsDirectedEdge; // could be removed once the model is ready 29 | friend class GraphicsNode; // For the constructor TODO no longer needed? 30 | friend class GraphicsNodePrivate; // for notifyPositionChange //TODO remove 31 | friend class GraphicsDirectedEdgePrivate; // could be removed once the model is ready 32 | friend class GraphicsNodeView; //for the view helpers, could be removed 33 | friend class SocketWrapper; // For the constructor 34 | public: 35 | /* 36 | * the socket comes in two flavors: either as sink or as source for a 37 | * data stream 38 | */ 39 | enum class SocketType 40 | { 41 | SINK, 42 | SOURCE 43 | }; 44 | 45 | QModelIndex edge() const; 46 | void setEdge(const QModelIndex& index); 47 | 48 | QString text() const; 49 | void setText(const QString& text); 50 | 51 | SocketType socketType() const; 52 | 53 | bool isSink() const; //TODO get rid of those socketType is enough 54 | bool isSource() const; 55 | 56 | QSizeF size() const; 57 | QSizeF minimalSize() const; 58 | 59 | QGraphicsItem *graphicsItem() const; 60 | 61 | bool isConnected() const; 62 | 63 | bool isEnabled() const; 64 | 65 | QModelIndex index() const; 66 | 67 | Q_SIGNALS: 68 | void connectedTo(GraphicsNodeSocket* other); 69 | 70 | private: 71 | explicit GraphicsNodeSocket(const QModelIndex& index, SocketType socket_type, GraphicsNode *parent); 72 | 73 | GraphicsNodeSocketPrivate* d_ptr; 74 | Q_DECLARE_PRIVATE(GraphicsNodeSocket) 75 | }; 76 | Q_ENUMS(GraphicsNodeSocket::SocketType) 77 | 78 | #endif /* __GRAPHICSNODESOCKET_HPP__99275D3E_35A8_4D63_8E10_995E5DC83C8C */ 79 | 80 | -------------------------------------------------------------------------------- /src/graphicsnodesocket_p.h: -------------------------------------------------------------------------------- 1 | #ifndef GRAPHICSNODESOCKET_P_H 2 | #define GRAPHICSNODESOCKET_P_H 3 | 4 | #include "graphicsnodesocket.hpp" 5 | #include 6 | 7 | #include 8 | 9 | #define PEN_COLOR_CIRCLE QColor("#FF000000") 10 | #define PEN_COLOR_TEXT QColor("#FFFFFFFF") 11 | 12 | class SocketGraphicsItem; 13 | 14 | class GraphicsNodeSocketPrivate 15 | { 16 | public: 17 | explicit GraphicsNodeSocketPrivate(GraphicsNodeSocket* q) : q_ptr(q) {} 18 | 19 | // return the anchor position relative to the scene in which the socket 20 | // is living in 21 | QPointF sceneAnchorPos() const; //TODO move to the private class 22 | 23 | /** 24 | * determine if a point is actually within the socket circle. 25 | */ 26 | bool isInSocketCircle(const QPointF &p) const; //TODO move to the private class 27 | 28 | // Attributes 29 | GraphicsNodeSocket::SocketType _socket_type; 30 | QPen _pen_circle {PEN_COLOR_CIRCLE}; 31 | const QPen _pen_text {PEN_COLOR_TEXT}; 32 | QBrush _brush_circle; 33 | 34 | QPersistentModelIndex m_PersistentIndex; 35 | QPersistentModelIndex m_EdgeIndex; 36 | 37 | const qreal _pen_width = 1.0; 38 | const qreal _circle_radius = 6.0; 39 | const qreal _text_offset = 3.0; 40 | 41 | const qreal _min_width = 30; 42 | const qreal _min_height = 12.0; 43 | 44 | SocketGraphicsItem* m_pGraphicsItem; 45 | 46 | // Helper 47 | void drawAlignedText(QPainter *painter); 48 | 49 | GraphicsNodeSocket* q_ptr; 50 | }; 51 | 52 | class SocketGraphicsItem final : public QGraphicsItem 53 | { 54 | public: 55 | SocketGraphicsItem(QGraphicsItem* parent, GraphicsNodeSocketPrivate* d) : 56 | QGraphicsItem(parent), d_ptr(d) {} 57 | 58 | virtual QRectF boundingRect() const override; 59 | 60 | /* 61 | */ 62 | virtual void paint(QPainter *painter, 63 | const QStyleOptionGraphicsItem *option, 64 | QWidget *widget = 0) override; 65 | 66 | /** 67 | * type of the class. usefull within a QGraphicsScene to distinguish 68 | * what is really behind a pointer 69 | */ 70 | virtual int type() const override; 71 | 72 | GraphicsNodeSocketPrivate* d_ptr; 73 | 74 | protected: 75 | // event handling 76 | virtual void mousePressEvent(QGraphicsSceneMouseEvent *event) override; 77 | virtual void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; 78 | virtual void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; 79 | 80 | }; 81 | 82 | #endif 83 | -------------------------------------------------------------------------------- /src/graphicsnode.hpp: -------------------------------------------------------------------------------- 1 | /* See LICENSE file for copyright and license details. */ 2 | 3 | #ifndef GRAPHICS_NODE_H 4 | #define GRAPHICS_NODE_H 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "graphicsnodedefs.hpp" 12 | 13 | class QWidget; 14 | class QGraphicsSceneMouseEvent; 15 | class QGraphicsTextItem; 16 | class GraphicsDirectedEdge; 17 | class GraphicsNodeSocket; 18 | class QAbstractItemModel; 19 | 20 | class QNodeEditorSocketModel; 21 | 22 | class GraphicsNodePrivate; 23 | 24 | class Q_DECL_EXPORT GraphicsNode : public QObject 25 | { 26 | friend class QNodeEditorSocketModel; // To update them 27 | friend class QNodeEditorSocketModelPrivate; // For creating GraphicsNodes 28 | friend class NodeWrapper; // To delete it 29 | Q_OBJECT 30 | 31 | public: 32 | QGraphicsItem *graphicsItem() const; 33 | 34 | QSizeF size() const; 35 | QRectF rect() const; 36 | 37 | void setTitle(const QString &title); 38 | 39 | QString title() const; 40 | 41 | QBrush background() const; 42 | QPen foreground() const; 43 | 44 | void setBackground(const QBrush& brush); 45 | void setBackground(const QString& brush); 46 | void setForeground(const QPen& pen); 47 | void setForeground(const QColor& pen); 48 | void setForeground(const QString& pen); 49 | 50 | void setDecoration(const QVariant& deco); 51 | 52 | Q_INVOKABLE QAbstractItemModel *sinkModel() const; 53 | Q_INVOKABLE QAbstractItemModel *sourceModel() const; 54 | 55 | const QModelIndex socketIndex(const QString& name) const; 56 | 57 | void setSize(const qreal width, const qreal height); 58 | void setSize(const QSizeF size); 59 | void setSize(const QPointF size); 60 | 61 | void setRect(const qreal x, const qreal y, const qreal width, const qreal height); 62 | void setRect(const QRectF size); 63 | 64 | QModelIndex index() const; 65 | QAbstractItemModel* model() const; 66 | 67 | /** 68 | * set a regular QWidget as central widget 69 | */ 70 | void setCentralWidget(QWidget *widget); 71 | 72 | private: 73 | explicit GraphicsNode(QNodeEditorSocketModel* model, const QPersistentModelIndex& index, QGraphicsItem *parent = nullptr); 74 | virtual ~GraphicsNode(); 75 | 76 | void update(); 77 | void setIndex(const QModelIndex& idx);//FIXME HACK this is a workaround for a bug elsewhere 78 | 79 | GraphicsNodePrivate* d_ptr; 80 | Q_DECLARE_PRIVATE(GraphicsNode) 81 | }; 82 | 83 | Q_DECLARE_METATYPE(GraphicsNode*) 84 | 85 | 86 | #endif //GRAPHICS_NODE_H 87 | 88 | -------------------------------------------------------------------------------- /src/qmultimodeltree.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | class QMultiModelTreePrivate; 6 | 7 | /** 8 | * Create a tree model with external models as top level indices and their 9 | * content as children. 10 | * 11 | * The use case for this is either views like side panels or as a node in a 12 | * longer proxy model chain. 13 | * 14 | * Note that this code currently doesn't tree models as children. If anybody 15 | * has a solution that's both fast and reliable, please tell. 16 | */ 17 | class QMultiModelTree : public QAbstractItemModel 18 | { 19 | Q_OBJECT 20 | public: 21 | explicit QMultiModelTree(QObject* parent = Q_NULLPTR); 22 | virtual ~QMultiModelTree(); 23 | 24 | /// A role used to return the model identifier 25 | int topLevelIdentifierRole() const; 26 | void setTopLevelIdentifierRole(int role); 27 | 28 | Q_INVOKABLE QAbstractItemModel* getModel(const QModelIndex& idx) const; 29 | 30 | virtual QVariant data(const QModelIndex& idx, int role) const override; 31 | virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; 32 | virtual int rowCount(const QModelIndex& parent = {}) const override; 33 | virtual int columnCount(const QModelIndex& parent = {}) const override; 34 | virtual QModelIndex index(int row, int column, const QModelIndex& parent ={}) const override; 35 | virtual Qt::ItemFlags flags(const QModelIndex &idx) const override; 36 | virtual QModelIndex parent(const QModelIndex& idx) const override; 37 | virtual QModelIndex mapFromSource(const QModelIndex& sourceIndex) const; 38 | virtual QModelIndex mapToSource(const QModelIndex& proxyIndex) const; 39 | virtual bool canDropMimeData(const QMimeData *data, Qt::DropAction action, 40 | int row, int column, const QModelIndex &parent) const override; 41 | virtual bool dropMimeData(const QMimeData *data, Qt::DropAction action, 42 | int row, int column, const QModelIndex &parent) override; 43 | virtual QMimeData *mimeData(const QModelIndexList &indexes) const override; 44 | virtual bool removeRows(int row, int count, const QModelIndex &parent = {}) override; 45 | 46 | QModelIndex appendModel(QAbstractItemModel* model, const QVariant& id = {}); 47 | 48 | Q_SIGNALS: 49 | void modelRenamed(QAbstractItemModel* model, const QString& newName, const QString& oldName); 50 | void modelRenamed(const QModelIndex& idx, const QString& newName, const QString& oldName); 51 | 52 | public: 53 | QMultiModelTreePrivate* d_ptr; 54 | Q_DECLARE_PRIVATE(QMultiModelTree) 55 | }; 56 | -------------------------------------------------------------------------------- /src/qnodeeditorsocketmodel.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "qtypecoloriserproxy.h" 4 | #include 5 | 6 | #include 7 | 8 | class GraphicsNode; 9 | class QReactiveProxyModel; 10 | class GraphicsNodeScene; 11 | class GraphicsDirectedEdge; 12 | 13 | class QNodeEditorSocketModelPrivate; 14 | 15 | //TODO move to a subclass 16 | class QNodeEditorEdgeModel : public QIdentityProxyModel 17 | { 18 | Q_OBJECT 19 | public: 20 | explicit QNodeEditorEdgeModel(QNodeEditorSocketModelPrivate* parent = Q_NULLPTR); //TODO make private 21 | virtual ~QNodeEditorEdgeModel(); 22 | 23 | // virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; 24 | virtual QVariant data(const QModelIndex& idx, int role) const override; 25 | 26 | bool canConnect(const QModelIndex& idx1, const QModelIndex& idx2) const; 27 | bool connectSocket(const QModelIndex& idx1, const QModelIndex& idx2); 28 | 29 | GraphicsNodeScene* scene() const; 30 | 31 | QNodeEditorSocketModel* socketModel() const; 32 | 33 | private: 34 | QNodeEditorSocketModelPrivate* d_ptr; 35 | }; 36 | 37 | /** 38 | * Keeper/factory for the nodes and socket objects. This is the only place 39 | * allowed to create and destroy them. 40 | * 41 | * It might seems a little abusive to use a proxy for that, but trying to keep 42 | * consistency across all 4 "view" classes proved to be challenging and 43 | * messy. Having a central entity to do it ensure a clear and simple ownership 44 | * pyramid for each objects. 45 | * 46 | * TODO once the model refactoring is done, turn into a private class 47 | */ 48 | class QNodeEditorSocketModel : public QTypeColoriserProxy 49 | { 50 | Q_OBJECT 51 | public: 52 | 53 | explicit QNodeEditorSocketModel( 54 | QReactiveProxyModel* rmodel, 55 | GraphicsNodeScene* scene 56 | ); 57 | 58 | virtual ~QNodeEditorSocketModel(); 59 | 60 | virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; 61 | virtual void setSourceModel(QAbstractItemModel *sourceModel) override; 62 | virtual QMimeData *mimeData(const QModelIndexList &indexes) const override; 63 | virtual Qt::ItemFlags flags(const QModelIndex &idx) const override; 64 | 65 | int sourceSocketCount(const QModelIndex& idx) const; 66 | int sinkSocketCount(const QModelIndex& idx) const; 67 | 68 | QNodeEditorEdgeModel* edgeModel() const; 69 | 70 | GraphicsNodeScene* scene() const; 71 | 72 | GraphicsNode* getNode(const QModelIndex& idx, bool recursive = false); 73 | GraphicsNodeSocket* getSourceSocket(const QModelIndex& idx); 74 | GraphicsNodeSocket* getSinkSocket(const QModelIndex& idx); 75 | 76 | GraphicsDirectedEdge* getSourceEdge(const QModelIndex& idx); 77 | GraphicsDirectedEdge* getSinkEdge(const QModelIndex& idx); 78 | 79 | Q_INVOKABLE QAbstractItemModel *sinkSocketModel(const QModelIndex& node) const; 80 | Q_INVOKABLE QAbstractItemModel *sourceSocketModel(const QModelIndex& node) const; 81 | 82 | //TODO in later iterations of the API, add partial connections to the QNodeEditorEdgeModel 83 | GraphicsDirectedEdge* initiateConnectionFromSource(const QModelIndex& index, const QPointF& point); 84 | GraphicsDirectedEdge* initiateConnectionFromSink(const QModelIndex& index, const QPointF& point); 85 | 86 | private: 87 | QNodeEditorSocketModelPrivate* d_ptr; 88 | Q_DECLARE_PRIVATE(QNodeEditorSocketModel) 89 | }; 90 | -------------------------------------------------------------------------------- /examples/simple-data-forwarding/mainwindow.cpp: -------------------------------------------------------------------------------- 1 | /* See LICENSE file for copyright and license details. */ 2 | 3 | #include "mainwindow.hpp" 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | #include 26 | 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | 34 | #include "test_qobjects.hpp" 35 | 36 | 37 | MainWindow::MainWindow() 38 | : _view(nullptr) 39 | , _scene(nullptr) 40 | { 41 | _mnuFile = new QMenu(tr("File")); 42 | menuBar()->addMenu(_mnuFile); 43 | 44 | _actClear = new QAction(tr("Clear"), this); 45 | connect(_actClear, &QAction::triggered, this, &MainWindow::onClearTriggered); 46 | _mnuFile->addAction(_actClear); 47 | 48 | // create and configure scene 49 | _scene = new GraphicsNodeScene(this); 50 | _scene->setSceneRect(-32000, -32000, 64000, 64000); 51 | 52 | // view setup 53 | _view = new GraphicsNodeView(this); 54 | _view->setScene(_scene); 55 | this->setCentralWidget(_view); 56 | 57 | // add some content 58 | //addFakeContent(); 59 | addNodeViews(); 60 | } 61 | 62 | 63 | void MainWindow:: 64 | onClearTriggered() 65 | { 66 | _scene->clear(); 67 | } 68 | 69 | 70 | void MainWindow:: 71 | resizeEvent(QResizeEvent *event) 72 | { 73 | QMainWindow::resizeEvent(event); 74 | } 75 | 76 | 77 | void MainWindow:: 78 | addFakeContent() 79 | { 80 | // fill with some content 81 | QBrush greenBrush(Qt::green); 82 | QPen outlinePen(Qt::black); 83 | outlinePen.setWidth(2); 84 | 85 | // populate with a of lines 86 | auto gridColor = QColor::fromRgbF(0.4, 0.4, 0.4, 1.0); 87 | QBrush gridBrush(gridColor); 88 | QPen gridPen(gridColor); 89 | 90 | // populate with 'content' 91 | auto rect = _scene->addRect(100, 0, 80, 100, outlinePen, greenBrush); 92 | rect->setFlag(QGraphicsItem::ItemIsMovable); 93 | 94 | auto text = _scene->addText("scene01", QFont("Ubuntu Mono")); 95 | text->setDefaultTextColor(QColor::fromRgbF(1.0, 1.0, 1.0, 1.0)); 96 | text->setFlag(QGraphicsItem::ItemIsMovable); 97 | text->setPos(0, -25); 98 | 99 | auto widget1 = new QPushButton("Hello World"); 100 | auto proxy1 = _scene->addWidget(widget1); 101 | proxy1->setPos(0, 30); 102 | 103 | auto widget2 = new QTextEdit(); 104 | auto proxy2 = _scene->addWidget(widget2); 105 | proxy2->setPos(0, 60); 106 | } 107 | 108 | 109 | void MainWindow:: 110 | addNodeViews() 111 | { 112 | #if 1 113 | for (int i = 0; i < 5; i++) { 114 | auto n = new GraphicsNode(); 115 | for (int j = i; j < 5; j++) { 116 | n->setPos(i * 25, i * 25); 117 | n->add_sink("sink"); 118 | n->add_source("source"); 119 | } 120 | 121 | if (i == 4) { 122 | QTextEdit *te = new QTextEdit(); 123 | n->setCentralWidget(te); 124 | } 125 | 126 | n->setTitle(QString("GraphicsNode %1").arg(i)); 127 | 128 | _scene->addItem(n); 129 | } 130 | 131 | #else 132 | QObject* t1 = new QLineEdit(); 133 | QObjectnode* n1 = new QObjectnode(t1); 134 | _scene->addItem(n1); 135 | n1->setPos(0,0); 136 | 137 | t1 = new testnode1(); 138 | QObjectnode* n2 = new QObjectnode(t1); 139 | _scene->addItem(n2); 140 | n2->setPos(0+n1->width()*1.5,0); 141 | 142 | GraphicsDirectedEdge* e12 = new GraphicsBezierEdge(); 143 | e12->connect(n1,0,n2,0); 144 | _scene->addItem(e12); 145 | 146 | t1 = new QLineEdit(); 147 | QObjectnode* n3 = new QObjectnode(t1); 148 | _scene->addItem(n3); 149 | n3->setPos(n2->pos().x()+n2->width()*1.5,0); 150 | 151 | GraphicsDirectedEdge* e23 = new GraphicsBezierEdge(); 152 | e23->connect(n2,0,n3,0); 153 | _scene->addItem(e23); 154 | #endif 155 | } 156 | 157 | -------------------------------------------------------------------------------- /src/qreactiveproxymodel.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | class QAbstractProxyModel; 6 | 7 | class QReactiveProxyModelPrivate; 8 | 9 | /** 10 | * Listen to index changes and forward them to another index setData. 11 | * It wraps the model so the drag and drop events can be interpreted as socket 12 | * connections rather than whatever they were meant to be. 13 | * 14 | * This model also create and expose a submodel of all the active connections. 15 | * 16 | * This is used to implement reactive programming components such as 17 | * spreadsheets, pipe and filter or node based editing. 18 | * 19 | * All connections has to be from the same model. This could be fixed later. 20 | * 21 | * To avoid poluting stderr with QVariant warnings, this macro can be called 22 | * from the .cpp to "mute" warnings instead of `Q_DECLARE_METATYPE`. 23 | * 24 | * #ifndef Q_DECLARE_STREAM_METATYPE 25 | * #define Q_DECLARE_STREAM_METATYPE(T) \ 26 | * QDataStream &operator<<(QDataStream &s, const T);\ 27 | * QDataStream &operator<<(QDataStream &s, const T) { return s; }\ 28 | * QDataStream &operator>>(QDataStream &s, const T);\ 29 | * QDataStream &operator>>(QDataStream &s, const T) { return s; }\ 30 | * static int ___ = ([]()->int{\ 31 | * qRegisterMetaType(#T);\ 32 | * qRegisterMetaTypeStreamOperators(#T);\ 33 | * return 0;\ 34 | * })(); 35 | * #endif 36 | * 37 | */ 38 | class Q_DECL_EXPORT QReactiveProxyModel : public QIdentityProxyModel 39 | { 40 | Q_OBJECT 41 | public: 42 | enum ConnectionsRoles { 43 | SOURCE_INDEX = -1, 44 | DESTINATION_INDEX = -1, 45 | IS_VALID = -2, 46 | IS_USED = -3, 47 | UID = -4, 48 | }; 49 | 50 | enum ConnectionsColumns { 51 | SOURCE = 0, 52 | CONNECTION = 1, 53 | DESTINATION = 2, 54 | }; 55 | 56 | explicit QReactiveProxyModel(QObject* parent = Q_NULLPTR); 57 | virtual ~QReactiveProxyModel(); 58 | 59 | virtual bool canDropMimeData(const QMimeData *data, Qt::DropAction action, 60 | int row, int column, const QModelIndex &parent) const override; 61 | virtual bool dropMimeData(const QMimeData *data, Qt::DropAction action, 62 | int row, int column, const QModelIndex &parent) override; 63 | virtual QMimeData *mimeData(const QModelIndexList &indexes) const override; 64 | virtual Qt::DropActions supportedDropActions() const override; 65 | virtual Qt::DropActions supportedDragActions() const override; 66 | virtual void setSourceModel(QAbstractItemModel *sourceModel) override; 67 | virtual QStringList mimeTypes() const override; 68 | virtual Qt::ItemFlags flags(const QModelIndex &idx) const override; 69 | 70 | void addConnectedRole(int role); 71 | QVector connectedRoles() const; 72 | 73 | enum class ExtraRoles { 74 | SourceConnectionNotificationRole, 75 | DestinationConnectionNotificationRole, 76 | SourceDisconnectionNotificationRole, 77 | DestinationDisconnectionNotificationRole, 78 | IdentifierRole, 79 | }; 80 | 81 | int extraRole(ExtraRoles type) const; 82 | 83 | void setExtraRole(ExtraRoles type, int role); 84 | 85 | QAbstractItemModel *connectionsModel() const; 86 | 87 | QAbstractProxyModel* currentProxy() const; 88 | void setCurrentProxy(QAbstractProxyModel* proxy); 89 | 90 | bool connectIndices(const QModelIndex& source, const QModelIndex& destination); //TODO add roles 91 | bool areConnected(const QModelIndex& source, const QModelIndex& destination) const; //TODO add roles 92 | 93 | QList sendTo(const QModelIndex& source) const; 94 | QList receiveFrom(const QModelIndex& destination) const; 95 | 96 | Q_SIGNALS: 97 | void connected(const QModelIndex& source, const QModelIndex& destination); 98 | void disconnected(const QModelIndex& source, const QModelIndex& destination); 99 | 100 | private: 101 | QReactiveProxyModelPrivate* d_ptr; 102 | Q_DECLARE_PRIVATE(QReactiveProxyModel) 103 | }; 104 | -------------------------------------------------------------------------------- /src/qobjectmodel.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | class QObjectModelPrivate; 6 | 7 | /** 8 | * TODO not fully implemented yet 9 | * Automatically a list of QObject into a model. 10 | * 11 | * It supports: 12 | * 13 | * * Properties ("USER=true" ones are enabled by default) 14 | * * Signal (disabled by default) 15 | * * Slots (disabled by default) 16 | * 17 | * It also supports to dispositions: 18 | * 19 | * * Horizontal (each property has its columns, the object name is used for the 20 | * vertical header) 21 | * * Vertical (all properties are added on new rows, mixing the objects) 22 | * 23 | * It also supports introspection filters to control the content of the model 24 | */ 25 | class Q_DECL_EXPORT QObjectModel : public QAbstractItemModel 26 | { 27 | Q_OBJECT 28 | friend class PropertyChangeReceiver; // for createIndex() 29 | public: 30 | enum Capabilities : char { 31 | NONE = 0 << 0, 32 | READ = 1 << 0, 33 | WRITE = 1 << 1, 34 | NOTIFY = 1 << 2, 35 | CONST = 1 << 3, 36 | USER = 1 << 4, 37 | }; 38 | 39 | enum Role { 40 | ValueRole = Qt::UserRole+1, 41 | PropertyIdRole, 42 | PropertyNameRole, 43 | CapabilitiesRole, 44 | MetaTypeRole, 45 | TypeNameRole, 46 | }; 47 | 48 | /// Get the number of objects displayed by the model 49 | Q_PROPERTY(int objectCount READ objectCount) 50 | 51 | /** In heterogeneous mode, this model take all the properties and mix 52 | * them. When this is disabled (default), only the common properties are 53 | * added to the model (to preserve the table columns consistency) 54 | **/ 55 | Q_PROPERTY(bool heterogeneous READ isHeterogeneous WRITE setHeterogeneous) 56 | 57 | /// By default, the value is used, but it can be configured to use something else 58 | Q_PROPERTY(int displayRole READ displayRole WRITE setDisplayRole) 59 | 60 | /// Display as a list or a table 61 | Q_PROPERTY(Qt::Orientation orientation READ orientation WRITE setOrientation) 62 | 63 | /** 64 | * By default, this models enable "setData" when the property is writable. 65 | */ 66 | Q_PROPERTY(bool readOnly READ isReadOnly WRITE setReadOnly) 67 | 68 | explicit QObjectModel(QObject* parent = Q_NULLPTR); 69 | QObjectModel(const QList objs, Qt::Orientation = Qt::Horizontal, int displayRole = Qt::DisplayRole, QObject* parent = Q_NULLPTR); 70 | virtual ~QObjectModel(); 71 | 72 | virtual QVariant data(const QModelIndex& idx, int role) const override; 73 | virtual int rowCount(const QModelIndex& parent = {}) const override; 74 | virtual int columnCount(const QModelIndex& parent = {}) const override; 75 | virtual QModelIndex index(int row, int column, const QModelIndex& parent ={}) const override; 76 | virtual Qt::ItemFlags flags(const QModelIndex &idx) const override; 77 | virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; 78 | virtual QHash roleNames() const override; 79 | virtual QModelIndex parent(const QModelIndex& idx) const override; 80 | virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; 81 | 82 | bool isHeterogeneous() const; 83 | void setHeterogeneous(bool value); //TODO 84 | 85 | bool isReadOnly() const; //TODO 86 | void setReadOnly(bool value); 87 | 88 | int displayRole() const; 89 | void setDisplayRole(int role); 90 | 91 | int objectCount() const; 92 | 93 | Q_INVOKABLE QObject* getObject(const QModelIndex& idx) const; 94 | 95 | Qt::Orientation orientation() const; 96 | void setOrientation(Qt::Orientation o); 97 | 98 | /** 99 | * Add objects to be displayed in the model. 100 | */ 101 | Q_INVOKABLE void addObject(QObject* obj); 102 | Q_INVOKABLE void addObjects(const QVector& objs); 103 | Q_INVOKABLE void addObjects(const QList& objs); 104 | 105 | private: 106 | QObjectModelPrivate* d_ptr; 107 | Q_DECLARE_PRIVATE(QObjectModel) 108 | }; 109 | 110 | Q_FLAGS(QObjectModel::Capabilities) 111 | Q_ENUMS(QObjectModel::Role) 112 | -------------------------------------------------------------------------------- /src/qmodeldatalistdecoder.cpp: -------------------------------------------------------------------------------- 1 | #include "qmodeldatalistdecoder.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | // Be less strict about unserializable values that are part of the stream. 8 | class QLousyVariantDecoder 9 | { 10 | public: 11 | class QVariantExt : public QVariant { 12 | public: 13 | inline void createProxy(int typeId) { create(typeId, Q_NULLPTR); } 14 | }; 15 | 16 | quint32 metaType; 17 | qint8 isNull; 18 | QVariantExt variant ; 19 | QByteArray userType; 20 | bool isLoaded; 21 | }; 22 | 23 | class QModelDataListDecoderPrivate 24 | { 25 | public: 26 | QHash, QMap > m_Data; 27 | }; 28 | 29 | QDebug operator<<(QDebug debug, const QLousyVariantDecoder &v); 30 | QDataStream &operator<<(QDataStream &s, const QLousyVariantDecoder &self); 31 | QDataStream &operator>>(QDataStream &s, QLousyVariantDecoder &self); 32 | 33 | QDebug operator<<(QDebug debug, const QLousyVariantDecoder &v) 34 | { 35 | return debug << QStringLiteral("<<<") 36 | << QStringLiteral("Type:" ) << v.metaType << v.userType 37 | << QStringLiteral(", Is NULL:" ) << v.isNull 38 | << QStringLiteral(", Is valid:") << v.isLoaded 39 | << QStringLiteral(", Value:" ) << v.variant 40 | << QStringLiteral(">>>"); 41 | } 42 | 43 | QDataStream &operator<<(QDataStream &s, const QLousyVariantDecoder &self) 44 | { 45 | return s << self.variant; 46 | } 47 | 48 | QDataStream &operator>>(QDataStream &s, QLousyVariantDecoder &self) 49 | { 50 | // There is no Qt ways to doing this before 5.7 beside creating it by hand 51 | // Qt5.7 support transactions, but replicating the exact behavior of the 52 | // following code is longer than not using transactions. 53 | s >> self.metaType; 54 | s >> self.isNull; 55 | 56 | if (self.metaType == QVariant::UserType) { 57 | s >> self.userType; 58 | self.metaType = QMetaType::type(self.userType.constData()); 59 | 60 | if (self.metaType == QMetaType::UnknownType) { 61 | s.setStatus(QDataStream::ReadCorruptData); 62 | return s; 63 | } 64 | } 65 | else 66 | self.userType = QMetaType::typeName(self.metaType); 67 | 68 | if (!self.isNull) { 69 | self.variant.createProxy(self.metaType); 70 | 71 | void* data = const_cast(self.variant.constData()); 72 | 73 | // Ignore errors, as the way it is implemented, the field is empty, 74 | // so the streams remains valid. However dropping the data wont work. 75 | self.isLoaded = QMetaType::load(s, self.variant.type(), data); 76 | } 77 | 78 | return s; 79 | } 80 | 81 | // hack to execute code at a RANDOM moment during initialization. 82 | static auto _DUMMY = ([]()->bool { 83 | qRegisterMetaType ("QLousyVariantDecoder"); 84 | qRegisterMetaTypeStreamOperators("QLousyVariantDecoder"); 85 | return true; 86 | })(); 87 | 88 | QModelDataListDecoder::QModelDataListDecoder(const QMimeData* data) 89 | : d_ptr(new QModelDataListDecoderPrivate) 90 | { 91 | if (!data) 92 | return; 93 | 94 | // Check all payloads if one can be converted to the right QMetaType 95 | auto buf = data->data("application/x-qabstractitemmodeldatalist"); 96 | 97 | if (buf.isEmpty()) 98 | return; 99 | 100 | QDataStream s(buf); 101 | 102 | while (!s.atEnd()) { 103 | int r, c; 104 | QMap v; 105 | s >> r >> c >> v; 106 | 107 | // only add valid items 108 | if (r+1 && c+1) 109 | d_ptr->m_Data[{r, c}] = std::move(v); 110 | } 111 | } 112 | 113 | QModelDataListDecoder::~QModelDataListDecoder() 114 | { 115 | delete d_ptr; 116 | } 117 | 118 | QPair QModelDataListDecoder::firstElement() const 119 | { 120 | if (d_ptr->m_Data.isEmpty()) 121 | return {-1,-1}; 122 | 123 | return d_ptr->m_Data.begin().key(); 124 | } 125 | 126 | bool QModelDataListDecoder::canConvert(quint32 typeId, int role, int row, int col) const 127 | { 128 | auto v = data(role, row, col); 129 | 130 | if (v.isValid()) 131 | return v.canConvert(typeId); 132 | 133 | const auto pair = (row+1&&col+1) ? QPair(row, col) : firstElement(); 134 | 135 | if (!d_ptr->m_Data.contains(pair)) 136 | return false; 137 | 138 | auto info = d_ptr->m_Data[pair][role]; 139 | 140 | if (info.metaType == typeId) 141 | return true; 142 | 143 | QLousyVariantDecoder::QVariantExt var; 144 | var.createProxy(info.metaType); 145 | 146 | return var.canConvert(typeId);; 147 | } 148 | 149 | QVariant QModelDataListDecoder::data(int role, int row, int col) const 150 | { 151 | const auto pair = (row+1&&col+1) ? QPair(row, col) : firstElement(); 152 | 153 | if (!d_ptr->m_Data.contains(pair)) 154 | return {}; 155 | 156 | return d_ptr->m_Data[pair][role].variant; 157 | } 158 | 159 | quint32 QModelDataListDecoder::typeId(int role, int row, int col) const 160 | { 161 | const auto pair = (row+1&&col+1) ? QPair(row, col) : firstElement(); 162 | 163 | if (!d_ptr->m_Data.contains(pair)) 164 | return QMetaType::UnknownType; 165 | 166 | const auto data = d_ptr->m_Data[pair]; 167 | 168 | if (!data.contains(role)) 169 | return QMetaType::UnknownType; 170 | 171 | return data[role].metaType; 172 | } 173 | -------------------------------------------------------------------------------- /src/qnodewidget.cpp: -------------------------------------------------------------------------------- 1 | #include "qnodewidget.h" 2 | #include "qobjectmodel.h" 3 | #include "graphicsnode.hpp" 4 | #include "qmultimodeltree.h" 5 | 6 | #include "qreactiveproxymodel.h" 7 | 8 | #include 9 | 10 | class QNodeWidgetPrivate : public QObject 11 | { 12 | Q_OBJECT 13 | public: 14 | explicit QNodeWidgetPrivate(QObject* p) : QObject(p) {} 15 | 16 | QMultiModelTree m_Model{this}; 17 | 18 | QNodeWidget* q_ptr; 19 | 20 | public Q_SLOTS: 21 | void slotRemoveRows(const QModelIndex& parent, int first, int last); 22 | void slotModelRenamed(const QModelIndex& index, const QString& newName, const QString& oldName); 23 | }; 24 | 25 | QNodeWidget::QNodeWidget(QWidget* parent) : QNodeView(parent), 26 | d_ptr(new QNodeWidgetPrivate(this)) 27 | { 28 | d_ptr->m_Model.setTopLevelIdentifierRole(Qt::UserRole); 29 | 30 | setModel(&d_ptr->m_Model); 31 | 32 | connect(reactiveModel(), &QAbstractItemModel::rowsAboutToBeRemoved, 33 | d_ptr, &QNodeWidgetPrivate::slotRemoveRows); 34 | 35 | connect(&d_ptr->m_Model, SIGNAL(modelRenamed(QModelIndex,QString,QString)), 36 | d_ptr, SLOT(slotModelRenamed(QModelIndex,QString,QString))); 37 | } 38 | 39 | QNodeWidget::~QNodeWidget() 40 | { 41 | delete d_ptr; 42 | } 43 | 44 | /** 45 | * Also note that you can set the object parent to the QNodeWidget to have it 46 | * deleted automatically when its row is removed from the model. 47 | */ 48 | GraphicsNode* QNodeWidget::addObject(QObject* o, const QString& title, QNodeWidget::ObjectFlags f, const QVariant& uid) 49 | { 50 | Q_UNUSED(f) 51 | d_ptr->q_ptr = this; 52 | 53 | auto m = new QObjectModel({o}, Qt::Vertical, QObjectModel::Role::PropertyNameRole, this); 54 | 55 | return addModel(m, title, uid); 56 | } 57 | 58 | /** 59 | * Add a new node based on a model to the canvas. 60 | * 61 | * For the model to behave properly, it has to comply to the following thing: 62 | * 63 | * 1) Only QModelIndex with Qt::ItemIsDragEnabled flags will be added as source 64 | * 2) Only QModelIndex with Qt::ItemIsEditable and Qt::ItemIsDropEnable 65 | * flags will be added as sinks 66 | * 3) All model ::data Qt::EditRole need to set the role that changes when 67 | * dropped. It need to **always** return a typed QVariant, even if the 68 | * value is NULL. This is used to check if the connection is valid. 69 | * 4) The following roles are supported: 70 | * * Qt::DisplayRole for the label text 71 | * * Qt::TooltipRole for the socket tooltip 72 | * * Qt::BackgroundRole for the socket and edge background 73 | * * Qt::DecorationRole for the socket icon 74 | * * Qt::ForegroundRole for the label color 75 | * 76 | * Also note that mimeData() need to include the `application/x-qabstractitemmodeldatalist` 77 | * MIME type. For models that re-implement it, make sure to use the QMimeData* 78 | * returned from QAbstractItemModel::mimeData() as a base instead of creating 79 | * a blank one. 80 | * 81 | * Also note that you can set the model parent to the QNodeWidget to have it 82 | * deleted automatically when it is removed from the model. 83 | */ 84 | GraphicsNode* QNodeWidget::addModel(QAbstractItemModel* m, const QString& title, const QVariant& uid) 85 | { 86 | const auto idx = d_ptr->m_Model.appendModel(m); 87 | 88 | if (!title.isEmpty()) 89 | d_ptr->m_Model.setData(idx, title, Qt::EditRole); 90 | 91 | d_ptr->m_Model.setData(idx, uid, Qt::UserRole); 92 | 93 | Q_ASSERT(d_ptr->m_Model.data(idx, Qt::UserRole) == uid); 94 | Q_ASSERT(reactiveModel()->data(idx, Qt::UserRole) == uid); 95 | Q_ASSERT(idx.isValid()); 96 | Q_ASSERT(getNode(idx)); 97 | 98 | return getNode(idx); 99 | } 100 | 101 | void QNodeWidgetPrivate::slotRemoveRows(const QModelIndex& parent, int first, int last) 102 | { 103 | if (parent.isValid()) 104 | return; 105 | 106 | for (int i = first; i <= last; i++) { 107 | auto idx = m_Model.index(i, 0); 108 | 109 | Q_ASSERT(idx.isValid()); 110 | 111 | auto m = m_Model.getModel(idx); 112 | 113 | if (auto qom = qobject_cast(m)) { 114 | if (qom->objectCount() == 1) { 115 | auto obj = qom->getObject(qom->index(0,0)); 116 | Q_EMIT q_ptr->objectRemoved(obj); 117 | 118 | if (obj && obj->parent() == q_ptr) 119 | obj->deleteLater(); 120 | } 121 | else 122 | Q_EMIT q_ptr->modelRemoved(m); 123 | } 124 | else 125 | Q_EMIT q_ptr->modelRemoved(m); 126 | 127 | if (m->parent() == q_ptr) 128 | m->deleteLater(); 129 | } 130 | } 131 | 132 | void QNodeWidgetPrivate::slotModelRenamed(const QModelIndex& index, const QString& newName, const QString& oldName) 133 | { 134 | if ((!index.isValid()) || (index.parent().isValid())) 135 | return; 136 | 137 | if (auto node = q_ptr->getNode(index)) 138 | Q_EMIT q_ptr->nodeRenamed(node, newName, oldName); 139 | 140 | const auto uid = index.data(m_Model.topLevelIdentifierRole()).toString(); 141 | if (!uid.isEmpty()) 142 | Q_EMIT q_ptr->nodeRenamed(uid, newName, oldName); 143 | 144 | auto model = m_Model.getModel(index); 145 | 146 | if (auto qom = qobject_cast(model)) { 147 | if (qom->objectCount() == 1) { 148 | auto obj = qom->getObject(qom->index(0,0)); 149 | Q_EMIT q_ptr->objectRenamed(obj, newName, oldName); 150 | } 151 | else 152 | Q_EMIT q_ptr->modelRenamed(model, newName, oldName); 153 | } 154 | else 155 | Q_EMIT q_ptr->modelRenamed(model, newName, oldName); 156 | } 157 | 158 | #include 159 | -------------------------------------------------------------------------------- /src/graphicsbezieredge.cpp: -------------------------------------------------------------------------------- 1 | /* See LICENSE file for copyright and license details. */ 2 | 3 | #include "graphicsbezieredge.hpp" 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | #include "graphicsnode.hpp" 13 | #include "graphicsnodesocket.hpp" 14 | 15 | #include "graphicsnodesocket_p.h" 16 | #include "graphicsbezieredge_p.h" 17 | 18 | #include "qreactiveproxymodel.h" 19 | 20 | #include "qnodeeditorsocketmodel.h" 21 | 22 | class GraphicsEdgeItem : public QGraphicsPathItem 23 | { 24 | public: 25 | explicit GraphicsEdgeItem(GraphicsDirectedEdgePrivate* s) : d_ptr(s) {} 26 | 27 | virtual int type() const override; 28 | 29 | virtual void updatePath() = 0; 30 | 31 | protected: 32 | virtual void mousePressEvent(QGraphicsSceneMouseEvent *event) override; 33 | GraphicsDirectedEdgePrivate* d_ptr; 34 | }; 35 | 36 | class GraphicsBezierItem final : public GraphicsEdgeItem 37 | { 38 | public: 39 | explicit GraphicsBezierItem(GraphicsDirectedEdgePrivate* s) : 40 | GraphicsEdgeItem(s) {} 41 | 42 | virtual void paint(QPainter * painter, const QStyleOptionGraphicsItem * option, QWidget * widget = 0) override; 43 | virtual int type() const override; 44 | virtual void updatePath() override; 45 | }; 46 | 47 | GraphicsDirectedEdge:: 48 | GraphicsDirectedEdge(QNodeEditorEdgeModel* m, const QModelIndex& index, qreal factor) 49 | : QObject(), d_ptr(new GraphicsDirectedEdgePrivate(this)) 50 | { 51 | Q_ASSERT(index.isValid()); 52 | 53 | d_ptr->m_pModel = m; 54 | d_ptr->m_Index = index; 55 | d_ptr->_factor = factor; 56 | d_ptr->m_pGrpahicsItem = new GraphicsBezierItem(d_ptr); 57 | 58 | d_ptr->_pen.setWidth(2); 59 | d_ptr->m_pGrpahicsItem->setZValue(-1); 60 | 61 | #if 0 62 | d_ptr->_effect->setBlurRadius(15.0); 63 | d_ptr->_effect->setColor(QColor("#99050505")); 64 | d_ptr->m_pGrpahicsItem->setGraphicsEffect(d_ptr->_effect); 65 | #endif 66 | } 67 | 68 | void GraphicsDirectedEdge::update() 69 | { 70 | d_ptr->m_pGrpahicsItem->updatePath(); 71 | } 72 | 73 | int GraphicsBezierItem:: 74 | type() const 75 | { 76 | return GraphicsNodeItemTypes::TypeBezierEdge; 77 | } 78 | 79 | QGraphicsItem *GraphicsDirectedEdge:: 80 | graphicsItem() const 81 | { 82 | Q_ASSERT(d_ptr->m_pGrpahicsItem); 83 | return d_ptr->m_pGrpahicsItem; 84 | } 85 | 86 | 87 | GraphicsDirectedEdge:: 88 | ~GraphicsDirectedEdge() 89 | { 90 | #if 0 91 | delete d_ptr->_effect; 92 | #endif 93 | delete d_ptr->m_pGrpahicsItem; 94 | delete d_ptr; 95 | } 96 | 97 | 98 | void GraphicsEdgeItem:: 99 | mousePressEvent(QGraphicsSceneMouseEvent *event) 100 | { 101 | //FIXME currently dead code, need to be implemented 102 | QGraphicsPathItem::mousePressEvent(event); 103 | } 104 | 105 | void GraphicsDirectedEdgePrivate:: 106 | setStart(QPointF p) 107 | { 108 | _start = p; 109 | m_pGrpahicsItem->updatePath(); 110 | } 111 | 112 | 113 | void GraphicsDirectedEdgePrivate:: 114 | setStop(QPointF p) 115 | { 116 | _stop = p; 117 | m_pGrpahicsItem->updatePath(); 118 | } 119 | 120 | GraphicsBezierEdge::GraphicsBezierEdge(QNodeEditorEdgeModel* m, const QModelIndex& index, qreal factor) 121 | : GraphicsDirectedEdge(m, index, factor) 122 | { 123 | d_ptr->m_pGrpahicsItem = new GraphicsBezierItem(d_ptr); 124 | } 125 | 126 | void GraphicsBezierItem:: 127 | updatePath() 128 | { 129 | Q_ASSERT(d_ptr->m_Index.isValid()); 130 | 131 | auto srcI = d_ptr->m_pModel->index(d_ptr->m_Index.row(), 0) 132 | .data(Qt::SizeHintRole); 133 | auto sinkI = d_ptr->m_pModel->index(d_ptr->m_Index.row(), 2) 134 | .data(Qt::SizeHintRole); 135 | 136 | // compute anchor point offsets 137 | const qreal min_dist = 0.; //FIXME this is dead code? can the code below ever get negative? 138 | 139 | QPointF c1 = srcI.canConvert() ? srcI.toPointF() : d_ptr->_start; 140 | 141 | QPointF c2 = sinkI.canConvert() ? sinkI.toPointF() : d_ptr->_stop; 142 | 143 | const qreal dist = (c1.x() <= c2.x()) ? 144 | std::max(min_dist, (c2.x() - c1.x()) * d_ptr->_factor): 145 | std::max(min_dist, (c1.x() - c2.x()) * d_ptr->_factor); 146 | 147 | QPainterPath path(c1); 148 | 149 | const auto c3 = c2; 150 | 151 | c1.rx() += dist; 152 | c2.rx() -= dist; 153 | 154 | path.cubicTo(c1, c2, c3); 155 | setPath(path); 156 | 157 | //FIXME technically, all edges currently are at {0,0}, they should be at c1 158 | // This makes many "objects under cursor" call slow 159 | } 160 | 161 | int GraphicsEdgeItem:: 162 | type() const 163 | { 164 | return GraphicsNodeItemTypes::TypeBezierEdge; 165 | } 166 | 167 | void GraphicsBezierItem:: 168 | paint(QPainter * painter, const QStyleOptionGraphicsItem * opt, QWidget *w) 169 | { 170 | Q_UNUSED(opt) 171 | Q_UNUSED(w) 172 | 173 | const auto fg = d_ptr->m_Index.data(Qt::ForegroundRole); 174 | 175 | // Set the line color 176 | if (fg.canConvert()) 177 | painter->setPen(QPen(qvariant_cast(fg),d_ptr->_pen.width())); 178 | else if (fg.canConvert()) 179 | painter->setPen(qvariant_cast(fg)); 180 | else 181 | painter->setPen(d_ptr->_pen); 182 | 183 | 184 | painter->drawPath(path()); 185 | } 186 | 187 | QModelIndex GraphicsDirectedEdge::index() const 188 | { 189 | return d_ptr->m_Index; 190 | } 191 | 192 | void GraphicsDirectedEdge::setSink(const QModelIndex& idx) 193 | { 194 | const auto i = d_ptr->m_pModel->index(d_ptr->m_Index.row(), 2); 195 | d_ptr->m_pModel->setData(i, idx, QReactiveProxyModel::ConnectionsRoles::SOURCE_INDEX); 196 | } 197 | 198 | void GraphicsDirectedEdge::setSource(const QModelIndex& idx) 199 | { 200 | const auto i = d_ptr->m_pModel->index(d_ptr->m_Index.row(), 0); 201 | d_ptr->m_pModel->setData(i, idx, QReactiveProxyModel::ConnectionsRoles::DESTINATION_INDEX); 202 | } 203 | -------------------------------------------------------------------------------- /src/graphicsnodesocket.cpp: -------------------------------------------------------------------------------- 1 | /* See LICENSE file for copyright and license details. */ 2 | 3 | #include "graphicsnodesocket.hpp" 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | #include 13 | 14 | #include "graphicsbezieredge.hpp" 15 | 16 | #include "graphicsnode.hpp" 17 | 18 | #include "graphicsnodesocket_p.h" 19 | 20 | #include "qnodeeditorsocketmodel.h" 21 | 22 | #include "qreactiveproxymodel.h" 23 | 24 | #define BRUSH_COLOR_SINK QColor("#FF0077FF") 25 | #define BRUSH_COLOR_SOURCE QColor("#FFFF7700") 26 | 27 | #include "graphicsbezieredge_p.h" 28 | 29 | GraphicsNodeSocket:: 30 | GraphicsNodeSocket(const QModelIndex& index, SocketType socket_type, GraphicsNode *parent) 31 | : QObject(), d_ptr(new GraphicsNodeSocketPrivate(this)) 32 | { 33 | d_ptr->m_PersistentIndex = index; 34 | 35 | d_ptr->_socket_type = socket_type; 36 | 37 | d_ptr->_brush_circle = (socket_type == SocketType::SINK) ? 38 | BRUSH_COLOR_SINK : BRUSH_COLOR_SOURCE; 39 | 40 | d_ptr->_pen_circle.setWidth(0); 41 | 42 | d_ptr->m_pGraphicsItem = new SocketGraphicsItem(parent->graphicsItem(), d_ptr); 43 | 44 | d_ptr->m_pGraphicsItem->setAcceptDrops(true); 45 | } 46 | 47 | QGraphicsItem *GraphicsNodeSocket:: 48 | graphicsItem() const 49 | { 50 | return d_ptr->m_pGraphicsItem; 51 | } 52 | 53 | GraphicsNodeSocket::SocketType GraphicsNodeSocket:: 54 | socketType() const 55 | { 56 | return d_ptr->_socket_type; 57 | } 58 | 59 | int SocketGraphicsItem:: 60 | type() const 61 | { 62 | return GraphicsNodeItemTypes::TypeSocket; 63 | } 64 | 65 | bool GraphicsNodeSocket:: 66 | isSink() const 67 | { 68 | return d_ptr->_socket_type == SocketType::SINK; 69 | } 70 | 71 | bool GraphicsNodeSocket:: 72 | isSource() const 73 | { 74 | return d_ptr->_socket_type == SocketType::SOURCE; 75 | } 76 | 77 | 78 | QRectF SocketGraphicsItem:: 79 | boundingRect() const 80 | { 81 | const QSizeF s = d_ptr->q_ptr->size(); 82 | const qreal x = -d_ptr->_circle_radius - d_ptr->_pen_width/2; 83 | const qreal y = -s.height()/2.0 - d_ptr->_pen_width/2; 84 | 85 | return QRectF( 86 | d_ptr->_socket_type == GraphicsNodeSocket::SocketType::SINK ? 87 | x : -s.width()-x, 88 | y, 89 | s.width(), 90 | s.height() 91 | ).normalized(); 92 | } 93 | 94 | 95 | QSizeF GraphicsNodeSocket:: 96 | minimalSize() const { 97 | // Assumes the theme doesn't change 98 | static QFontMetrics fm({}); 99 | static const qreal text_height = static_cast(fm.height()); 100 | const int text_width = fm.width(text()); 101 | 102 | return { 103 | std::max( 104 | d_ptr->_min_width, 105 | d_ptr->_circle_radius*2 + d_ptr->_text_offset + text_width + d_ptr->_pen_width 106 | ), 107 | std::max(d_ptr->_min_height, text_height + d_ptr->_pen_width) 108 | }; 109 | } 110 | 111 | 112 | QSizeF GraphicsNodeSocket:: 113 | size() const 114 | { 115 | return minimalSize(); 116 | } 117 | 118 | 119 | void GraphicsNodeSocketPrivate:: 120 | drawAlignedText(QPainter *painter) 121 | { 122 | int flags = Qt::AlignVCenter; 123 | 124 | static const qreal s = 32767.0; 125 | 126 | QPointF corner; 127 | 128 | switch(_socket_type) { 129 | case GraphicsNodeSocket::SocketType::SINK: 130 | corner = { 131 | _circle_radius + _text_offset, 132 | -s 133 | }; 134 | corner.ry() += s/2.0; 135 | flags |= Qt::AlignLeft; 136 | break; 137 | case GraphicsNodeSocket::SocketType::SOURCE: 138 | corner = { 139 | -_circle_radius - _text_offset, 140 | -s 141 | }; 142 | corner.ry() += s/2.0; //TODO find out why it was done this way 143 | corner.rx() -= s; 144 | flags |= Qt::AlignRight; 145 | break; 146 | } 147 | 148 | const QRectF rect(corner, QSizeF(s, s)); 149 | 150 | auto fg = m_PersistentIndex.data(Qt::ForegroundRole); 151 | 152 | // Same color as the node 153 | if (!fg.isValid()) 154 | fg = m_PersistentIndex.parent().data(Qt::ForegroundRole); 155 | 156 | if (fg.canConvert()) 157 | painter->setPen(qvariant_cast(fg)); 158 | if (fg.canConvert()) 159 | painter->setPen(qvariant_cast(fg)); 160 | else 161 | painter->setPen(_pen_text); 162 | 163 | painter->drawText(rect, flags, m_PersistentIndex.data().toString(), 0); 164 | } 165 | 166 | 167 | QPointF GraphicsNodeSocketPrivate:: 168 | sceneAnchorPos() const 169 | { 170 | return m_pGraphicsItem->mapToScene(0,0); 171 | } 172 | 173 | 174 | void SocketGraphicsItem:: 175 | paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/) 176 | { 177 | painter->setPen(d_ptr->_pen_circle); 178 | 179 | const auto bg = d_ptr->m_PersistentIndex.data(Qt::BackgroundRole); 180 | 181 | painter->setBrush(bg.canConvert() ? qvariant_cast(bg) : d_ptr->_brush_circle); 182 | 183 | painter->drawEllipse(-d_ptr->_circle_radius, -d_ptr->_circle_radius, d_ptr->_circle_radius*2, d_ptr->_circle_radius*2); 184 | d_ptr->drawAlignedText(painter); 185 | 186 | // debug painting the bounding box 187 | #if 0 188 | QPen debugPen = QPen(QColor(Qt::red)); 189 | debugPen.setWidth(0); 190 | auto r = boundingRect(); 191 | painter->setPen(debugPen); 192 | painter->setBrush(Qt::NoBrush); 193 | painter->drawRect(r); 194 | 195 | painter->drawPoint(0,0); 196 | painter->drawLine(0,0, r.width(), 0); 197 | #endif 198 | } 199 | 200 | 201 | void SocketGraphicsItem:: 202 | mouseMoveEvent(QGraphicsSceneMouseEvent *event) 203 | { 204 | QGraphicsItem::mouseMoveEvent(event); 205 | } 206 | 207 | 208 | void SocketGraphicsItem:: 209 | mouseReleaseEvent(QGraphicsSceneMouseEvent *event) 210 | { 211 | QGraphicsItem::mouseReleaseEvent(event); 212 | } 213 | 214 | 215 | bool GraphicsNodeSocketPrivate:: 216 | isInSocketCircle(const QPointF &p) const 217 | { 218 | return p.x() >= -_circle_radius 219 | && p.x() <= _circle_radius 220 | && p.y() >= -_circle_radius 221 | && p.y() <= _circle_radius; 222 | } 223 | 224 | 225 | void SocketGraphicsItem:: 226 | mousePressEvent(QGraphicsSceneMouseEvent *event) 227 | { 228 | QGraphicsItem::mousePressEvent(event); 229 | } 230 | 231 | /** 232 | * set the edge for this socket 233 | */ 234 | void GraphicsNodeSocket:: 235 | setEdge(const QModelIndex& idx) 236 | { 237 | d_ptr->m_EdgeIndex = idx; 238 | } 239 | 240 | QModelIndex GraphicsNodeSocket:: 241 | edge() const 242 | { 243 | return d_ptr->m_EdgeIndex; 244 | } 245 | 246 | bool GraphicsNodeSocket::isConnected() const 247 | { 248 | return d_ptr->m_EdgeIndex.data( 249 | QReactiveProxyModel::ConnectionsRoles::SOURCE_INDEX 250 | ).toModelIndex().isValid(); 251 | } 252 | 253 | bool GraphicsNodeSocket::isEnabled() const 254 | { 255 | return d_ptr->m_PersistentIndex.flags() & Qt::ItemIsEnabled; 256 | } 257 | 258 | QString GraphicsNodeSocket:: 259 | text() const 260 | { 261 | return d_ptr->m_PersistentIndex.data().toString(); 262 | } 263 | 264 | void GraphicsNodeSocket:: 265 | setText(const QString& t) 266 | { 267 | if (auto m = const_cast(d_ptr->m_PersistentIndex.model())) 268 | m->setData(d_ptr->m_PersistentIndex, t, Qt::DisplayRole); 269 | } 270 | 271 | QModelIndex GraphicsNodeSocket:: 272 | index() const 273 | { 274 | return d_ptr->m_PersistentIndex; 275 | } 276 | 277 | 278 | -------------------------------------------------------------------------------- /src/graphicsnodeview.cpp: -------------------------------------------------------------------------------- 1 | /* See LICENSE file for copyright and license details. */ 2 | 3 | #include "graphicsnodeview.hpp" 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "graphicsnode.hpp" 13 | #include "graphicsnodesocket.hpp" 14 | #include "graphicsnodedefs.hpp" 15 | #include "graphicsbezieredge.hpp" 16 | 17 | #include "graphicsbezieredge_p.h" 18 | #include "graphicsnodesocket_p.h" 19 | #include "graphicsnode_p.h" 20 | 21 | #include "qnodeeditorsocketmodel.h" 22 | 23 | GraphicsNodeView::GraphicsNodeView(QWidget *parent) 24 | : GraphicsNodeView(nullptr, parent) 25 | { } 26 | 27 | 28 | GraphicsNodeView::GraphicsNodeView(QGraphicsScene *scene, QWidget *parent) 29 | : QGraphicsView(scene, parent) 30 | { 31 | setRenderHints(QPainter::Antialiasing | 32 | QPainter::TextAntialiasing | 33 | QPainter::HighQualityAntialiasing | 34 | QPainter::SmoothPixmapTransform); 35 | 36 | setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn); 37 | setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); 38 | setResizeAnchor(NoAnchor); 39 | setTransformationAnchor(AnchorUnderMouse); 40 | // setDragMode(QGraphicsView::RubberBandDrag); 41 | } 42 | 43 | 44 | void GraphicsNodeView:: 45 | resizeEvent(QResizeEvent *event) 46 | { 47 | // always have the origin in the top left corner 48 | static bool first_resize = true; 49 | if (first_resize) { 50 | // TODO: scale awareness 51 | centerOn(width()/2 - 50, height()/2 - 50); 52 | first_resize = false; 53 | } 54 | QGraphicsView::resizeEvent(event); 55 | } 56 | 57 | 58 | 59 | void GraphicsNodeView:: 60 | wheelEvent(QWheelEvent *event) { 61 | if (event->modifiers() & Qt::ControlModifier) { 62 | setTransformationAnchor(QGraphicsView::AnchorUnderMouse); 63 | double scaleFactor = 1.25; 64 | if (event->delta() < 0) { 65 | // zoom in 66 | scale(scaleFactor, scaleFactor); 67 | } 68 | else { 69 | // zoom out 70 | scale(1.0 / scaleFactor, 1.0 / scaleFactor); 71 | } 72 | event->accept(); 73 | } 74 | else { 75 | QGraphicsView::wheelEvent(event); 76 | } 77 | } 78 | 79 | 80 | void GraphicsNodeView:: 81 | mousePressEvent(QMouseEvent *event) 82 | { 83 | #pragma GCC diagnostic push 84 | #pragma GCC diagnostic ignored "-Wswitch" 85 | switch (event->button()) { 86 | case Qt::MiddleButton: 87 | middleMouseButtonPress(event); 88 | break; 89 | case Qt::LeftButton: 90 | leftMouseButtonPress(event); 91 | break; 92 | default: 93 | QGraphicsView::mousePressEvent(event); 94 | } 95 | #pragma GCC diagnostic pop 96 | } 97 | 98 | 99 | void GraphicsNodeView:: 100 | middleMouseButtonRelease(QMouseEvent *event) 101 | { 102 | QMouseEvent fakeEvent(event->type(), event->localPos(), event->screenPos(), event->windowPos(), 103 | Qt::LeftButton, event->buttons() & ~Qt::LeftButton, event->modifiers()); 104 | QGraphicsView::mouseReleaseEvent(&fakeEvent); 105 | // setDragMode(QGraphicsView::RubberBandDrag); 106 | setDragMode(QGraphicsView::NoDrag); 107 | } 108 | 109 | #include 110 | void GraphicsNodeView:: 111 | leftMouseButtonRelease(QMouseEvent *event) 112 | { 113 | if (_drag_event) { 114 | auto sock = socket_at(event->pos()); 115 | 116 | 117 | if (!sock || !can_accept_edge(sock)) { 118 | _drag_event->e->setSource({}); 119 | _drag_event->e->setSink({}); 120 | } else { 121 | switch (_drag_event->mode) { 122 | case EdgeDragEvent::to_source: 123 | _drag_event->e->setSource(sock->index()); 124 | break; 125 | case EdgeDragEvent::to_sink: 126 | _drag_event->e->setSink(sock->index()); 127 | break; 128 | } 129 | 130 | } 131 | _drag_event->mimeData->deleteLater(); 132 | delete _drag_event; 133 | _drag_event = nullptr; 134 | } else 135 | if (_resize_event) { 136 | // TODO: finally set the width/height of the node ? 137 | delete _resize_event; 138 | _resize_event = nullptr; 139 | setViewportUpdateMode(QGraphicsView::MinimalViewportUpdate); 140 | } 141 | QGraphicsView::mouseReleaseEvent(event); 142 | } 143 | 144 | 145 | void GraphicsNodeView:: 146 | mouseReleaseEvent(QMouseEvent *event) 147 | { 148 | viewport()->setCursor(Qt::ArrowCursor); 149 | 150 | #pragma GCC diagnostic push 151 | #pragma GCC diagnostic ignored "-Wswitch" 152 | switch (event->button()) { 153 | case Qt::MiddleButton: 154 | middleMouseButtonRelease(event); 155 | break; 156 | case Qt::LeftButton: 157 | leftMouseButtonRelease(event); 158 | break; 159 | default: 160 | QGraphicsView::mouseReleaseEvent(event); 161 | } 162 | #pragma GCC diagnostic pop 163 | } 164 | 165 | 166 | void GraphicsNodeView:: 167 | mouseMoveEvent(QMouseEvent *event) 168 | { 169 | // if the left mouse button was pressed and we actually have a mode and 170 | // temporary edge already set 171 | if (_drag_event && (event->buttons() & Qt::LeftButton)) { 172 | 173 | // set start/stop (ignored if the sockets are set) 174 | _drag_event->e->d_ptr->setStart(mapToScene(event->pos())); 175 | _drag_event->e->d_ptr->setStop (mapToScene(event->pos())); 176 | 177 | // update visual feedback 178 | auto sock = socket_at(event->pos()); 179 | if (!sock) { 180 | viewport()->setCursor(Qt::ClosedHandCursor); 181 | } 182 | else { 183 | if (!can_accept_edge(sock)) 184 | viewport()->setCursor(Qt::ForbiddenCursor); 185 | else { 186 | viewport()->setCursor(Qt::DragMoveCursor); 187 | } 188 | } 189 | } 190 | else if (_resize_event && (event->buttons() & Qt::LeftButton)) { 191 | QPointF size = mapToScene(event->pos()) 192 | - _resize_event->node->graphicsItem()->mapToScene(0,0); 193 | _resize_event->node->setSize(size); 194 | } 195 | else { 196 | // no button is pressed, so indicate what the user can do with 197 | // the item by changing the cursor 198 | if (event->buttons() == 0) { 199 | auto sock = socket_at(event->pos()); 200 | if (sock) { 201 | QPointF scenePos = mapToScene(event->pos()); 202 | QPointF itemPos = sock->graphicsItem()->mapFromScene(scenePos); 203 | if (sock->d_ptr->isInSocketCircle(itemPos)) 204 | viewport()->setCursor(Qt::OpenHandCursor); 205 | else 206 | viewport()->setCursor(Qt::ArrowCursor); 207 | } 208 | else 209 | viewport()->setCursor(Qt::ArrowCursor); 210 | } 211 | QGraphicsView::mouseMoveEvent(event); 212 | } 213 | } 214 | 215 | 216 | void GraphicsNodeView:: 217 | leftMouseButtonPress(QMouseEvent *event) 218 | { 219 | QGraphicsView::mousePressEvent(event); 220 | // GUI logic: if we click on a socket, we need to handle 221 | // the event appropriately 222 | QGraphicsItem *item = itemAt(event->pos()); 223 | if (!item) { 224 | QGraphicsView::mousePressEvent(event); 225 | return; 226 | } 227 | 228 | switch (item->type()) 229 | { 230 | case GraphicsNodeItemTypes::TypeSocket: { 231 | const QPointF scenePos = mapToScene(event->pos()); 232 | const QPointF itemPos = item->mapFromScene(scenePos); 233 | 234 | GraphicsNodeSocket *sock = static_cast(item)->d_ptr->q_ptr; 235 | 236 | if (sock->d_ptr->isInSocketCircle(itemPos)) { 237 | viewport()->setCursor(Qt::ClosedHandCursor); 238 | 239 | // initialize a new drag mode event 240 | _drag_event = new EdgeDragEvent(); 241 | _drag_event->mimeData = m_pModel->mimeData({sock->index()}); 242 | 243 | switch (sock->socketType()) { 244 | case GraphicsNodeSocket::SocketType::SINK: 245 | if ((_drag_event->e = m_pModel->getSinkEdge(sock->edge()))) 246 | _drag_event->mode = EdgeDragEvent::to_sink; 247 | else { 248 | _drag_event->e = m_pModel->initiateConnectionFromSink( 249 | sock->index(), scenePos 250 | ); 251 | _drag_event->mode = EdgeDragEvent::to_source; 252 | } 253 | _drag_event->e->setSink(sock->index()); 254 | break; 255 | case GraphicsNodeSocket::SocketType::SOURCE: 256 | if ((_drag_event->e = m_pModel->getSinkEdge(sock->edge()))) 257 | _drag_event->mode = EdgeDragEvent::to_source; 258 | else { 259 | _drag_event->e = m_pModel->initiateConnectionFromSource( 260 | sock->index(), scenePos 261 | ); 262 | _drag_event->mode = EdgeDragEvent::to_sink; 263 | } 264 | _drag_event->e->setSource(sock->index()); 265 | break; 266 | } 267 | 268 | // set the fallback positions, they are ignored is the socket is set 269 | _drag_event->e->d_ptr->setStop (scenePos); 270 | _drag_event->e->d_ptr->setStart(scenePos); 271 | 272 | event->ignore(); 273 | } 274 | else { 275 | QGraphicsView::mousePressEvent(event); 276 | } 277 | break; 278 | } 279 | 280 | case GraphicsNodeItemTypes::TypeNode: { 281 | QPointF scenePos = mapToScene(event->pos()); 282 | QPointF itemPos = item->mapFromScene(scenePos); 283 | GraphicsNode *node = static_cast(item)->q_ptr; 284 | 285 | if (itemPos.x() > (node->size().width() - 10) && (itemPos.y() > (node->size().height() - 10))) { 286 | setViewportUpdateMode(QGraphicsView::BoundingRectViewportUpdate); 287 | _resize_event = new NodeResizeEvent(); 288 | _resize_event->node = node; 289 | _resize_event->orig_width = node->size().width(); 290 | _resize_event->orig_height = node->size().height(); 291 | _resize_event->pos = event->pos(); 292 | event->ignore(); 293 | } 294 | else { 295 | QGraphicsView::mousePressEvent(event); 296 | } 297 | break; 298 | } 299 | 300 | default: 301 | QGraphicsView::mousePressEvent(event); 302 | } 303 | } 304 | 305 | 306 | void GraphicsNodeView:: 307 | middleMouseButtonPress(QMouseEvent *event) 308 | { 309 | QMouseEvent releaseEvent(QEvent::MouseButtonRelease, 310 | event->localPos(), event->screenPos(), event->windowPos(), 311 | Qt::LeftButton, 0, event->modifiers()); 312 | QGraphicsView::mouseReleaseEvent(&releaseEvent); 313 | setDragMode(QGraphicsView::ScrollHandDrag); 314 | QMouseEvent fakeEvent(event->type(), event->localPos(), event->screenPos(), event->windowPos(), 315 | Qt::LeftButton, 316 | event->buttons() | Qt::LeftButton, 317 | event->modifiers()); 318 | QGraphicsView::mousePressEvent(&fakeEvent); 319 | } 320 | 321 | 322 | bool GraphicsNodeView:: 323 | can_accept_edge(GraphicsNodeSocket *sock) 324 | { 325 | return sock && sock->isEnabled() && _drag_event &&( 326 | (sock->isSink () && _drag_event->mode == EdgeDragEvent::to_sink ) 327 | || (sock->isSource() && _drag_event->mode == EdgeDragEvent::to_source) 328 | ); 329 | } 330 | 331 | 332 | GraphicsNodeSocket* GraphicsNodeView:: 333 | socket_at(QPoint pos) 334 | { 335 | // figure out if we are above another socket. this requires 336 | // stepping through all items that we can actually see, and 337 | // taking the topmost one 338 | 339 | QList itms = items(pos); 340 | QGraphicsItem *item = nullptr; 341 | for (int i = itms.count() - 1; i >= 0; i--) { 342 | if (itms[i]->type() == GraphicsNodeItemTypes::TypeSocket) { 343 | item = itms[i]; 344 | break; 345 | } 346 | } 347 | 348 | if (item && item->type() == GraphicsNodeItemTypes::TypeSocket) 349 | return static_cast(item)->d_ptr->q_ptr; 350 | else { 351 | // qDebug() << "\n\nNOT FOUND"; 352 | return nullptr; 353 | } 354 | } 355 | -------------------------------------------------------------------------------- /src/qmultimodeltree.cpp: -------------------------------------------------------------------------------- 1 | #include "qmultimodeltree.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #if QT_VERSION < 0x050700 9 | //Q_FOREACH is deprecated and Qt CoW containers are detached on C++11 for loops 10 | template 11 | const T& qAsConst(const T& v) 12 | { 13 | return const_cast(v); 14 | } 15 | #endif 16 | 17 | struct InternalItem 18 | { 19 | enum class Mode { 20 | ROOT, 21 | PROXY 22 | }; 23 | 24 | int m_Index; 25 | Mode m_Mode; 26 | QAbstractItemModel* m_pModel; 27 | InternalItem* m_pParent; 28 | QVector m_lChildren; 29 | QString m_Title; 30 | QVariant m_UId, m_Bg, m_Fg, m_Deco; 31 | }; 32 | 33 | class QMultiModelTreePrivate : public QObject 34 | { 35 | public: 36 | explicit QMultiModelTreePrivate(QObject* p) : QObject(p) {} 37 | 38 | QVector m_lRows; 39 | QHash m_hModels; 40 | 41 | bool m_HasIdRole {false}; 42 | int m_IdRole {Qt::DisplayRole}; 43 | 44 | QMultiModelTree* q_ptr; 45 | 46 | public Q_SLOTS: 47 | void slotRowsInserted(const QModelIndex& parent, int first, int last); 48 | void slotDataChanged(const QModelIndex& tl, const QModelIndex& br); 49 | void slotAddRows(const QModelIndex& parent, int first, int last, QAbstractItemModel* src); 50 | }; 51 | 52 | QMultiModelTree::QMultiModelTree(QObject* parent) : QAbstractItemModel(parent), 53 | d_ptr(new QMultiModelTreePrivate(this)) 54 | { 55 | d_ptr->q_ptr = this; 56 | } 57 | 58 | QMultiModelTree::~QMultiModelTree() 59 | { 60 | d_ptr->m_hModels.clear(); 61 | while(!d_ptr->m_lRows.isEmpty()) { 62 | while(!d_ptr->m_lRows.first()->m_lChildren.isEmpty()) 63 | delete d_ptr->m_lRows.first()->m_lChildren.first(); 64 | delete d_ptr->m_lRows.first(); 65 | } 66 | 67 | delete d_ptr; 68 | } 69 | 70 | QAbstractItemModel* QMultiModelTree::getModel(const QModelIndex& _idx) const 71 | { 72 | auto idx = _idx; 73 | 74 | if (idx.parent().isValid()) 75 | idx = idx.parent(); 76 | 77 | if ((!idx.isValid()) || idx.model() != this || idx.parent().isValid()) 78 | return Q_NULLPTR; 79 | 80 | const auto i = static_cast(idx.internalPointer()); 81 | 82 | Q_ASSERT(!i->m_pParent); 83 | Q_ASSERT(i->m_pModel); 84 | 85 | return i->m_pModel; 86 | } 87 | 88 | QVariant QMultiModelTree::data(const QModelIndex& idx, int role) const 89 | { 90 | if (!idx.isValid()) 91 | return {}; 92 | 93 | const auto i = static_cast(idx.internalPointer()); 94 | 95 | if (i->m_Mode == InternalItem::Mode::PROXY) 96 | return i->m_pModel->data(mapToSource(idx), role); 97 | 98 | if (d_ptr->m_HasIdRole && d_ptr->m_IdRole == role) 99 | return i->m_UId; 100 | 101 | switch (role) { 102 | case Qt::BackgroundRole: 103 | return i->m_Bg; 104 | case Qt::ForegroundRole: 105 | return i->m_Fg; 106 | case Qt::DecorationRole: 107 | return i->m_Deco; 108 | case Qt::DisplayRole: 109 | return (!i->m_Title.isEmpty()) ? 110 | i->m_Title : i->m_pModel->objectName(); 111 | }; 112 | 113 | return {}; 114 | } 115 | 116 | bool QMultiModelTree::setData(const QModelIndex &index, const QVariant &value, int role) 117 | { 118 | if (!index.isValid()) 119 | return {}; 120 | 121 | auto i = static_cast(index.internalPointer()); 122 | 123 | switch(i->m_Mode) { 124 | case InternalItem::Mode::PROXY: 125 | return i->m_pModel->setData(mapToSource(index), value, role); 126 | case InternalItem::Mode::ROOT: 127 | if (d_ptr->m_HasIdRole && d_ptr->m_IdRole == role) { 128 | i->m_UId = value; 129 | Q_EMIT dataChanged(index, index); 130 | return true; 131 | } 132 | switch(role) { 133 | case Qt::BackgroundRole: 134 | i->m_Bg = value; 135 | Q_EMIT dataChanged(index, index); 136 | return true; 137 | case Qt::ForegroundRole: 138 | i->m_Fg = value; 139 | Q_EMIT dataChanged(index, index); 140 | return true; 141 | case Qt::DecorationRole: 142 | i->m_Deco = value; 143 | Q_EMIT dataChanged(index, index); 144 | return true; 145 | case Qt::DisplayRole: 146 | case Qt::EditRole: { 147 | const auto oldT = i->m_Title; 148 | const auto newT = value.toString(); 149 | i->m_Title = newT; 150 | Q_EMIT dataChanged(index, index); 151 | if (newT != oldT) { 152 | Q_EMIT modelRenamed(i->m_pModel, newT, oldT); 153 | Q_EMIT modelRenamed(index, newT, oldT); 154 | } 155 | return true; 156 | } 157 | break; 158 | } 159 | break; 160 | }; 161 | 162 | return false; 163 | } 164 | 165 | int QMultiModelTree::rowCount(const QModelIndex& parent) const 166 | { 167 | if (!parent.isValid()) 168 | return d_ptr->m_lRows.size(); 169 | 170 | const auto i = static_cast(parent.internalPointer()); 171 | 172 | if (i->m_Mode == InternalItem::Mode::PROXY) 173 | return 0; 174 | 175 | return i->m_pModel->rowCount(); 176 | } 177 | 178 | int QMultiModelTree::columnCount(const QModelIndex& parent) const 179 | { 180 | Q_UNUSED(parent) 181 | return 1; 182 | } 183 | 184 | QModelIndex QMultiModelTree::index(int row, int column, const QModelIndex& parent) const 185 | { 186 | if ( 187 | (!parent.isValid()) 188 | && row >= 0 189 | && row < d_ptr->m_lRows.size() 190 | && column == 0 191 | ) 192 | return createIndex(row, column, d_ptr->m_lRows[row]); 193 | 194 | if ((!parent.isValid()) || parent.model() != this) 195 | return {}; 196 | 197 | 198 | const auto i = static_cast(parent.internalPointer()); 199 | 200 | return mapFromSource(i->m_pModel->index(row, column)); 201 | } 202 | 203 | Qt::ItemFlags QMultiModelTree::flags(const QModelIndex &idx) const 204 | { 205 | const auto pidx = mapToSource(idx); 206 | return pidx.isValid() ? 207 | pidx.flags() : Qt::ItemIsEnabled | Qt::ItemIsSelectable; 208 | } 209 | 210 | QModelIndex QMultiModelTree::parent(const QModelIndex& idx) const 211 | { 212 | if (!idx.isValid()) 213 | return {}; 214 | 215 | const auto i = static_cast(idx.internalPointer()); 216 | 217 | if (i->m_Mode == InternalItem::Mode::ROOT) 218 | return {}; 219 | 220 | return createIndex(i->m_pParent->m_Index, 0, i->m_pParent); 221 | } 222 | 223 | QModelIndex QMultiModelTree::mapFromSource(const QModelIndex& sourceIndex) const 224 | { 225 | if ((!sourceIndex.isValid()) || sourceIndex.parent().isValid() || sourceIndex.column()) 226 | return {}; 227 | 228 | const auto i = d_ptr->m_hModels[sourceIndex.model()]; 229 | 230 | if ((!i) || sourceIndex.row() >= i->m_lChildren.size()) 231 | return {}; 232 | 233 | return createIndex(sourceIndex.row(), 0, i->m_lChildren[sourceIndex.row()]); 234 | } 235 | 236 | QModelIndex QMultiModelTree::mapToSource(const QModelIndex& proxyIndex) const 237 | { 238 | if ((!proxyIndex.isValid()) || proxyIndex.model() != this) 239 | return {}; 240 | 241 | if (!proxyIndex.parent().isValid()) 242 | return {}; 243 | 244 | const auto i = static_cast(proxyIndex.internalPointer()); 245 | 246 | return i->m_pModel->index(proxyIndex.row(), proxyIndex.column()); 247 | } 248 | 249 | bool QMultiModelTree::removeRows(int row, int count, const QModelIndex &parent) 250 | { 251 | if (row < 0 || count < 1) 252 | return false; 253 | 254 | auto idx = parent; 255 | 256 | while (idx.parent().isValid()) 257 | idx = idx.parent(); 258 | 259 | if (idx.isValid() && !idx.parent().isValid()) { 260 | const auto i = static_cast(idx.internalPointer()); 261 | if (i->m_pModel->removeRows(row, count, mapToSource(parent))) { 262 | beginRemoveRows(parent, row, row + count - 1); 263 | endRemoveRows(); 264 | return true; 265 | } 266 | //FIXME update the m_Index 267 | } 268 | else if (!parent.isValid() && row + count <= d_ptr->m_lRows.size()) { 269 | 270 | beginRemoveRows(parent, row, row + count - 1); 271 | for(int i = row; i < row+count; i++) { 272 | auto item = d_ptr->m_lRows[i]; 273 | d_ptr->m_hModels.remove(item->m_pModel); 274 | 275 | delete item; 276 | } 277 | 278 | d_ptr->m_lRows.remove(row, count); 279 | 280 | for (int i = row+count-1; i < rowCount(); i++) 281 | d_ptr->m_lRows[i]->m_Index = i; 282 | 283 | endRemoveRows(); 284 | 285 | return true; 286 | } 287 | 288 | return false; 289 | } 290 | 291 | void QMultiModelTreePrivate::slotAddRows(const QModelIndex& parent, int first, int last, QAbstractItemModel* src) 292 | { 293 | if (parent.isValid()) return; 294 | 295 | auto p = m_hModels[src]; 296 | Q_ASSERT(p); 297 | 298 | const auto localParent = q_ptr->index(p->m_Index, 0); 299 | 300 | q_ptr->beginInsertRows(localParent, first, last); 301 | for (int i = first; i <= last; i++) { 302 | p->m_lChildren << new InternalItem { 303 | p->m_lChildren.size(), 304 | InternalItem::Mode::PROXY, 305 | src, 306 | p, 307 | {}, 308 | QStringLiteral("N/A"), 309 | {}, {}, {}, {} 310 | }; 311 | } 312 | q_ptr->endInsertRows(); 313 | } 314 | 315 | void QMultiModelTreePrivate::slotRowsInserted(const QModelIndex& parent, int first, int last) 316 | { 317 | const auto model = qobject_cast(QObject::sender()); 318 | Q_ASSERT(model); 319 | 320 | slotAddRows(parent, first, last, model); 321 | } 322 | 323 | void QMultiModelTreePrivate::slotDataChanged(const QModelIndex& tl, const QModelIndex& br) 324 | { 325 | if (tl == br && !tl.parent().isValid()) { 326 | const auto i = q_ptr->mapFromSource(tl); 327 | 328 | Q_ASSERT((!tl.isValid()) || i.isValid()); 329 | 330 | Q_EMIT q_ptr->dataChanged(i, i); 331 | } 332 | else { 333 | Q_EMIT q_ptr->dataChanged(q_ptr->mapFromSource(tl), q_ptr->mapFromSource(br)); 334 | } 335 | } 336 | 337 | QModelIndex QMultiModelTree::appendModel(QAbstractItemModel* model, const QVariant& id) 338 | { 339 | if ((!model) || d_ptr->m_hModels[model]) return {}; 340 | 341 | beginInsertRows({}, d_ptr->m_lRows.size(), d_ptr->m_lRows.size()); 342 | d_ptr->m_hModels[model] = new InternalItem { 343 | d_ptr->m_lRows.size(), 344 | InternalItem::Mode::ROOT, 345 | model, 346 | Q_NULLPTR, 347 | {}, 348 | id.canConvert() ? id.toString() : model->objectName(), 349 | id, {}, {}, {} 350 | }; 351 | d_ptr->m_lRows << d_ptr->m_hModels[model]; 352 | endInsertRows(); 353 | 354 | d_ptr->slotAddRows({}, 0, model->rowCount(), model); 355 | 356 | //TODO connect to the model row moved/removed/reset 357 | connect(model, &QAbstractItemModel::rowsInserted, 358 | d_ptr, &QMultiModelTreePrivate::slotRowsInserted); 359 | connect(model, &QAbstractItemModel::dataChanged, 360 | d_ptr, &QMultiModelTreePrivate::slotDataChanged); 361 | 362 | return index(rowCount()-1, 0); 363 | } 364 | 365 | bool QMultiModelTree::canDropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) const 366 | { 367 | auto idx = index(row, column, parent); 368 | 369 | if (!idx.isValid()) 370 | return false; 371 | 372 | const auto i = static_cast(idx.internalPointer()); 373 | 374 | auto srcIdx = mapToSource(idx); 375 | 376 | return i->m_pModel->canDropMimeData(data, action, srcIdx.row(), srcIdx.column(), srcIdx.parent()); 377 | } 378 | 379 | bool QMultiModelTree::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) 380 | { 381 | auto idx = index(row, column, parent); 382 | 383 | if (!idx.isValid()) 384 | return false; 385 | 386 | const auto i = static_cast(idx.internalPointer()); 387 | 388 | auto srcIdx = mapToSource(idx); 389 | 390 | return i->m_pModel->dropMimeData(data, action, srcIdx.row(), srcIdx.column(), srcIdx.parent()); 391 | } 392 | 393 | QMimeData* QMultiModelTree::mimeData(const QModelIndexList &indexes) const 394 | { 395 | QModelIndexList newList; 396 | 397 | bool singleModel = true; 398 | QAbstractItemModel* srcModel = Q_NULLPTR; 399 | 400 | for (auto i : qAsConst(indexes)) { 401 | const auto srcIdx = mapToSource(i); 402 | Q_ASSERT(srcIdx.model() != this); 403 | 404 | if (!srcModel) 405 | srcModel = const_cast(srcIdx.model()); 406 | else if (srcIdx.model() && srcIdx.model() != srcModel) { 407 | singleModel = false; 408 | break; 409 | } 410 | 411 | if (i.isValid()) { 412 | Q_ASSERT(i.model() == this); 413 | newList << srcIdx; 414 | } 415 | } 416 | 417 | if (singleModel && (!newList.isEmpty()) && srcModel) 418 | return srcModel->mimeData(newList); 419 | 420 | return QAbstractItemModel::mimeData(indexes); 421 | } 422 | 423 | int QMultiModelTree::topLevelIdentifierRole() const 424 | { 425 | return d_ptr->m_IdRole; 426 | } 427 | 428 | void QMultiModelTree::setTopLevelIdentifierRole(int role) 429 | { 430 | d_ptr->m_HasIdRole = true; 431 | d_ptr->m_IdRole = role; 432 | } 433 | -------------------------------------------------------------------------------- /src/qobjectmodel.cpp: -------------------------------------------------------------------------------- 1 | #include "qobjectmodel.h" 2 | 3 | #include 4 | #include 5 | 6 | #if QT_VERSION < 0x050700 7 | //Q_FOREACH is deprecated and Qt CoW containers are detached on C++11 for loops 8 | template 9 | const T& qAsConst(const T& v) 10 | { 11 | return const_cast(v); 12 | } 13 | #endif 14 | 15 | struct InternalItem; 16 | 17 | // The reason why this isn't part of InternalItem is to reduce the number of 18 | // cache fault when displaying the model. It isn't possible to mix generic 19 | // QMetaMethod based connections with lambdas. Obviously, doing it this way 20 | // can potentially create many, many QObjects. 21 | // 22 | // One future optimization would be to have many slots per objects and keep 23 | // an internal mapping back to the right InternalItem. 24 | // 25 | // Another one would be to re-implement the low level signal emit catcher 26 | // and avoid all the QObject::connect. 27 | class PropertyChangeReceiver : public QObject 28 | { 29 | Q_OBJECT 30 | public: 31 | explicit PropertyChangeReceiver(QObjectModel* m, InternalItem* i, QObject* p, int sigIdx); 32 | 33 | InternalItem* item; 34 | QObjectModel* model; 35 | 36 | public Q_SLOTS: 37 | void changed(); 38 | void slotDestroyed(); 39 | }; 40 | 41 | 42 | class PropertyChangeReceiverFactory 43 | { 44 | static PropertyChangeReceiver* connectForId(InternalItem* item, int id); 45 | private: 46 | static QHash m_hReceivers; 47 | }; 48 | 49 | QHash PropertyChangeReceiverFactory::m_hReceivers; 50 | 51 | struct MetaPropertyColumnMapper 52 | { 53 | struct Property { 54 | const uchar flags; 55 | const int index; 56 | const int metaType; 57 | const QByteArray name; 58 | int sigIdx; 59 | }; 60 | 61 | const QMetaObject* m_pMetaObject; 62 | QVector m_lProperties {}; 63 | 64 | private: 65 | friend class QObjectModelPrivate; 66 | explicit MetaPropertyColumnMapper(const QMetaObject* m) : m_pMetaObject(m){} 67 | }; 68 | 69 | struct InternalItem 70 | { 71 | int m_Index; 72 | QObject* m_pObject; 73 | bool m_IsObjectRoot; 74 | QObjectModel* m_pModel; 75 | MetaPropertyColumnMapper::Property *m_pProp; 76 | QVector m_lColumns; 77 | 78 | InternalItem* getChild(int id) { 79 | return id ? m_lColumns[id] : this; 80 | } 81 | }; 82 | 83 | class QObjectModelPrivate final 84 | { 85 | public: 86 | bool m_IsHeterogeneous {false}; 87 | bool m_IsVertical {false}; 88 | bool m_IsReadOnly {false}; 89 | int m_DisplayRole {Qt::DisplayRole}; 90 | QVector m_lRows; 91 | static QHash m_hMapper; 92 | 93 | // Helpers 94 | void clear(); 95 | void regen(); 96 | static MetaPropertyColumnMapper* getMapper(QObject*); 97 | }; 98 | 99 | QHash QObjectModelPrivate::m_hMapper; 100 | 101 | QObjectModel::QObjectModel(QObject* parent) : QAbstractItemModel(parent), 102 | d_ptr(new QObjectModelPrivate) 103 | { 104 | } 105 | 106 | QObjectModel::QObjectModel(const QList objs, Qt::Orientation o, int dr, QObject* p) : 107 | QObjectModel(p) 108 | { 109 | d_ptr->m_IsVertical = o == Qt::Vertical; 110 | d_ptr->m_DisplayRole = dr; 111 | addObjects(objs); 112 | } 113 | 114 | 115 | QObjectModel::~QObjectModel() 116 | { 117 | d_ptr->clear(); 118 | delete d_ptr; 119 | } 120 | 121 | QHash QObjectModel::roleNames() const 122 | { 123 | static QHash ret; 124 | 125 | if (ret.isEmpty()) { 126 | ret[ ValueRole ] = "valueRole"; 127 | ret[ PropertyIdRole ] = "propertyIdRole"; 128 | ret[ PropertyNameRole ] = "propertyNameRole"; 129 | ret[ CapabilitiesRole ] = "capabilitiesRole"; 130 | ret[ MetaTypeRole ] = "metaTypeRole"; 131 | ret[ TypeNameRole ] = "typeNameRole"; 132 | } 133 | 134 | return ret; 135 | } 136 | 137 | QVariant QObjectModel::data(const QModelIndex& idx, int role) const 138 | { 139 | if (!idx.isValid()) return {}; 140 | 141 | const auto item = static_cast(idx.internalPointer()); 142 | 143 | switch (role) { 144 | case Qt::DisplayRole: 145 | return data(idx, d_ptr->m_DisplayRole); 146 | case Qt::EditRole: 147 | [[clang::fallthrough]]; 148 | case Role::ValueRole: 149 | return item->m_pObject->property(item->m_pProp->name); 150 | case Role::PropertyIdRole: 151 | return item->m_pProp->index; 152 | case Role::CapabilitiesRole: 153 | return item->m_pProp->flags; 154 | case Role::MetaTypeRole: 155 | return item->m_pProp->metaType; 156 | case Role::TypeNameRole: 157 | return QStringLiteral("TODO"); 158 | case Role::PropertyNameRole: 159 | return item->m_pProp->name; 160 | } 161 | 162 | return {}; 163 | } 164 | 165 | bool QObjectModel::setData(const QModelIndex &index, const QVariant &value, int role) 166 | { 167 | Q_UNUSED(role) 168 | if (!index.isValid()) 169 | return false; 170 | 171 | const auto item = static_cast(index.internalPointer()); 172 | 173 | if (!value.canConvert(item->m_pProp->metaType)) 174 | return false; 175 | 176 | 177 | item->m_pObject->setProperty(item->m_pProp->name, value); 178 | return true; 179 | } 180 | 181 | int QObjectModel::rowCount(const QModelIndex& parent) const 182 | { 183 | return parent.isValid() ? 0 : d_ptr->m_lRows.size(); 184 | } 185 | 186 | int QObjectModel::columnCount(const QModelIndex& parent) const 187 | { 188 | return (parent.isValid() || !d_ptr->m_lRows.size()) ? 189 | 0 : d_ptr->m_lRows[0]->m_lColumns.size(); 190 | } 191 | 192 | QModelIndex QObjectModel::index(int row, int column, const QModelIndex& parent) const 193 | { 194 | if (parent.isValid() || column < 0 || row < 0) 195 | return {}; 196 | 197 | if (row < d_ptr->m_lRows.size() && ( 198 | column == 0 || column <= d_ptr->m_lRows[row]->m_lColumns.size()-1) 199 | ) 200 | return createIndex(row, column, d_ptr->m_lRows[row]->getChild(column)); 201 | 202 | return {}; 203 | } 204 | 205 | Qt::ItemFlags QObjectModel::flags(const QModelIndex &idx) const 206 | { 207 | if (!idx.isValid()) 208 | return Qt::NoItemFlags; 209 | 210 | const auto item = static_cast(idx.internalPointer()); 211 | 212 | return Qt::ItemIsEnabled 213 | | Qt::ItemIsSelectable 214 | | Qt::ItemNeverHasChildren 215 | | ( 216 | // Ok, that's disputable as a generic rule, but for the use 217 | // case where this model was developed, it is necessary 218 | item->m_pProp->flags & Capabilities::NOTIFY ? 219 | Qt::ItemIsDragEnabled : Qt::NoItemFlags 220 | ) 221 | | ( 222 | item->m_pProp->flags & Capabilities::WRITE ? 223 | (Qt::ItemIsEditable | Qt::ItemIsDropEnabled) : Qt::NoItemFlags 224 | ); 225 | } 226 | 227 | QModelIndex QObjectModel::parent(const QModelIndex& idx) const 228 | { 229 | Q_UNUSED(idx) 230 | // This model doesn't support trees yet 231 | return {}; 232 | } 233 | 234 | QVariant QObjectModel::headerData(int section, Qt::Orientation orientation, int role) const 235 | { 236 | Q_UNUSED(section) 237 | Q_UNUSED(orientation) 238 | Q_UNUSED(role) 239 | return {}; 240 | } 241 | 242 | bool QObjectModel::isHeterogeneous() const 243 | { 244 | return d_ptr->m_IsHeterogeneous; //TODO 245 | } 246 | 247 | void QObjectModel::setHeterogeneous(bool value) 248 | { 249 | if (value == d_ptr->m_IsHeterogeneous) return; 250 | 251 | d_ptr->m_IsHeterogeneous = value; //TODO 252 | d_ptr->regen(); 253 | } 254 | 255 | Qt::Orientation QObjectModel::orientation() const 256 | { 257 | return d_ptr->m_IsVertical ? Qt::Vertical : Qt::Horizontal; 258 | } 259 | 260 | void QObjectModel::setOrientation(Qt::Orientation o) 261 | { 262 | d_ptr->m_IsVertical = o == Qt::Vertical; 263 | } 264 | 265 | int QObjectModel::displayRole() const 266 | { 267 | return d_ptr->m_DisplayRole; 268 | } 269 | 270 | void QObjectModel::setDisplayRole(int role) 271 | { 272 | d_ptr->m_DisplayRole = role; 273 | Q_EMIT dataChanged(index(0,0), index(rowCount()-1, columnCount() -1)); 274 | } 275 | 276 | bool QObjectModel::isReadOnly() const 277 | { 278 | return d_ptr->m_IsReadOnly; 279 | } 280 | 281 | void QObjectModel::setReadOnly(bool value) 282 | { 283 | //const bool changed = value != d_ptr->m_IsReadOnly; 284 | d_ptr->m_IsReadOnly = value; 285 | 286 | // Emit dataChanged so the ::flags() method is called by the view 287 | Q_EMIT dataChanged(index(0,0), index(rowCount()-1, columnCount() -1)); 288 | } 289 | 290 | void QObjectModel::addObject(QObject* obj) 291 | { 292 | if (!obj) return; 293 | 294 | auto mapper = d_ptr->getMapper(obj); 295 | 296 | if (mapper->m_lProperties.isEmpty()) return; 297 | 298 | beginInsertRows({}, d_ptr->m_lRows.size(), d_ptr->m_lRows.size()); 299 | auto item = new InternalItem { 300 | d_ptr->m_lRows.size(), 301 | obj, 302 | true, 303 | this, 304 | mapper->m_lProperties[0], 305 | {} 306 | }; 307 | endInsertRows(); 308 | 309 | if (!d_ptr->m_IsVertical) 310 | d_ptr->m_lRows << item; 311 | 312 | auto list = d_ptr->m_IsVertical ? &d_ptr->m_lRows : &item->m_lColumns; 313 | 314 | beginInsertRows({}, list->size(), list->size()+mapper->m_lProperties.size()-2); //FIXME support columns 315 | for (auto p : qAsConst(mapper->m_lProperties)) { 316 | // Add the existing item to its own column to simplify the code elsewhere 317 | auto ip = ((p==item->m_pProp) ? item : new InternalItem { 318 | d_ptr->m_lRows.size(), //FIXME this doesn't support columns 319 | obj, 320 | false, 321 | this, 322 | p, 323 | {} 324 | }); 325 | (*list) << ip; 326 | 327 | // The notify signal could be anything. It doesn't have to have an argument 328 | // with the same QMetaType as the property. Even if it has, there is no way 329 | // to know if it matches the property without trying to get it. So better 330 | // just assume the arguments is irrelevant and use the getter. Note that it 331 | // could be done using a custom qt_static_metacall, but again, it is a 332 | // little pointless to implement. 333 | if (p->sigIdx >= 0) 334 | new PropertyChangeReceiver(this, ip, obj, p->sigIdx); 335 | } 336 | endInsertRows(); 337 | } 338 | 339 | void QObjectModel::addObjects(const QVector& objs) 340 | { 341 | for (auto o : qAsConst(objs)) 342 | addObject(o); 343 | } 344 | 345 | void QObjectModel::addObjects(const QList& objs) 346 | { 347 | for (auto o : qAsConst(objs)) 348 | addObject(o); 349 | } 350 | 351 | QObject* QObjectModel::getObject(const QModelIndex& idx) const 352 | { 353 | if ((!idx.isValid()) || idx.model() != this) 354 | return Q_NULLPTR; 355 | 356 | const auto item = static_cast(idx.internalPointer()); 357 | 358 | return item->m_pObject; 359 | } 360 | 361 | int QObjectModel::objectCount() const 362 | { 363 | if (d_ptr->m_IsVertical) 364 | return d_ptr->m_lRows.isEmpty() ? 0 : 1; 365 | 366 | return d_ptr->m_lRows.size(); 367 | } 368 | 369 | void QObjectModelPrivate::clear() 370 | { 371 | QList items; 372 | 373 | for (auto i : qAsConst(m_lRows)) 374 | if (i->m_IsObjectRoot) 375 | items << i; 376 | 377 | for (auto i : qAsConst(items)) { 378 | if (i->m_lColumns.size()) 379 | for (auto ii : qAsConst(i->m_lColumns)) delete ii; 380 | delete i; 381 | } 382 | 383 | m_lRows.clear(); 384 | } 385 | 386 | void QObjectModelPrivate::regen() 387 | { 388 | // Gather all structures. This is very costly, but it should not be called 389 | // often (if ever) so lets save memory and keep only 1 index. 390 | QList rootItems; 391 | 392 | for (auto i : qAsConst(m_lRows)) 393 | if (i->m_IsObjectRoot) { 394 | i->m_Index = m_lRows.size(); 395 | rootItems << i; 396 | } 397 | 398 | m_lRows.clear(); 399 | 400 | if (m_IsVertical) { 401 | for (auto i : qAsConst(rootItems)) 402 | for (auto ii : qAsConst(i->m_lColumns)) { 403 | ii->m_Index = m_lRows.size(); 404 | m_lRows << ii; 405 | } 406 | } else 407 | m_lRows = rootItems.toVector(); 408 | } 409 | 410 | MetaPropertyColumnMapper* QObjectModelPrivate::getMapper(QObject* o) 411 | { 412 | const QMetaObject* m = o->metaObject(); 413 | auto mapper = m_hMapper[m]; 414 | if (mapper) return mapper; 415 | 416 | mapper = new MetaPropertyColumnMapper(m); 417 | 418 | // Usually, I prefer enum classes, but Qt doesn't have much magic for 419 | // them yet... 420 | typedef QObjectModel::Capabilities CAP; 421 | 422 | const int pCount = m->propertyCount(); 423 | 424 | for (int i=0; i < pCount; i++) { 425 | const auto p = m->property(i); 426 | 427 | if (!p.isUser()) //TODO remove this and add capability filters 428 | continue; 429 | 430 | mapper->m_lProperties << new MetaPropertyColumnMapper::Property { 431 | static_cast( 432 | (p.isReadable () ? CAP::READ : CAP::NONE) | 433 | (p.isWritable () ? CAP::WRITE : CAP::NONE) | 434 | (p.hasNotifySignal() ? CAP::NOTIFY : CAP::NONE) | 435 | (p.isConstant () ? CAP::CONST : CAP::NONE) | 436 | (p.isUser () ? CAP::USER : CAP::NONE) 437 | ), 438 | p.propertyIndex(), 439 | p.userType(), 440 | p.name(), 441 | p.notifySignalIndex() 442 | }; 443 | } 444 | 445 | return mapper; 446 | } 447 | 448 | // Setting the parent ensure the memory will be cleaned when `o` is destroyed. 449 | PropertyChangeReceiver::PropertyChangeReceiver(QObjectModel* m, InternalItem* i, QObject* p, int sigIdx) 450 | : QObject(p), item(i), model(m) 451 | { 452 | // Handle removing the item from this 453 | if (i->m_IsObjectRoot) 454 | connect(this, &QObject::destroyed, 455 | this, &PropertyChangeReceiver::slotDestroyed); 456 | 457 | connect(m, &QObject::destroyed, this, &QObject::deleteLater); 458 | 459 | const auto notifySignal = p->metaObject()->method(sigIdx); 460 | 461 | static int changedId = metaObject()->indexOfSlot("changed()"); 462 | 463 | const auto notifySlot = metaObject()->method(changedId); 464 | 465 | QObject::connect(p, notifySignal, this, notifySlot); 466 | } 467 | 468 | void PropertyChangeReceiver::changed() 469 | { 470 | const QModelIndex idx = model->createIndex(item->m_Index, 0, item); //FIXME this doesn't support columns 471 | 472 | Q_EMIT model->dataChanged(idx, idx); 473 | } 474 | 475 | void PropertyChangeReceiver::slotDestroyed() 476 | { 477 | //TODO emit the beginRemoveRows and delete the InternalItem 478 | } 479 | 480 | #include "qobjectmodel.moc" 481 | -------------------------------------------------------------------------------- /src/qreactiveproxymodel.cpp: -------------------------------------------------------------------------------- 1 | #include "qreactiveproxymodel.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "qmodeldatalistdecoder.h" 9 | 10 | #if QT_VERSION < 0x050700 11 | //Q_FOREACH is deprecated and Qt CoW containers are detached on C++11 for loops 12 | template 13 | const T& qAsConst(const T& v) 14 | { 15 | return const_cast(v); 16 | } 17 | #endif 18 | 19 | class ConnectedIndicesModel : public QAbstractTableModel 20 | { 21 | friend class QReactiveProxyModel; 22 | friend class QReactiveProxyModelPrivate; 23 | public: 24 | ConnectedIndicesModel(QObject* parent, QReactiveProxyModelPrivate* d); 25 | 26 | virtual QVariant data(const QModelIndex& idx, int role) const override; 27 | virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; 28 | virtual int rowCount(const QModelIndex& parent = {}) const override; 29 | virtual int columnCount(const QModelIndex& parent = {}) const override; 30 | 31 | private: 32 | QReactiveProxyModelPrivate* d_ptr; 33 | }; 34 | 35 | struct ConnectionHolder 36 | { 37 | int index; 38 | 39 | QPersistentModelIndex source; 40 | QPersistentModelIndex destination; 41 | void* sourceIP; 42 | void* destinationIP; 43 | 44 | bool isValid() const { 45 | return source.isValid() && destination.isValid(); 46 | } 47 | 48 | bool isUsed() const { 49 | return source.isValid() || destination.isValid(); 50 | } 51 | }; 52 | 53 | class QReactiveProxyModelPrivate : public QObject 54 | { 55 | public: 56 | const QString MIME_TYPE = QStringLiteral("qt-model/reactive-connection"); 57 | QVector m_lConnectedRoles; 58 | ConnectedIndicesModel* m_pConnectionModel; 59 | QHash m_hDraggedIndexCache; 60 | QVector m_lConnections; 61 | 62 | QAbstractProxyModel* m_pCurrentProxy {nullptr}; 63 | 64 | bool m_HasExtraRole[5] {false, false, false, false, false}; 65 | int m_ExtraRole [5] {0, 0, 0, 0, 0 }; 66 | 67 | // In case dataChanged() contains a single QModelIndex, use this fast path 68 | // to avoid doing a query on each connections or QModelIndex 69 | QHash m_hDirectMapping; 70 | 71 | //Helper 72 | void clear(); 73 | bool synchronize(const QModelIndex& source, const QModelIndex& destination) const; 74 | ConnectionHolder* newConnection(); 75 | 76 | void notifyConnect(const QModelIndex& source, const QModelIndex& destination) const; 77 | void notifyDisconnect(const QModelIndex& source, const QModelIndex& destination) const; 78 | void notifyConnect(const ConnectionHolder* conn) const; 79 | void notifyDisconnect(const ConnectionHolder* conn) const; 80 | 81 | QReactiveProxyModel* q_ptr; 82 | public Q_SLOTS: 83 | void slotMimeDestroyed(); 84 | void slotDataChanged(const QModelIndex& tl, const QModelIndex& br); 85 | void slotRemoveItem(const QModelIndex &parent, int first, int last); 86 | }; 87 | 88 | QReactiveProxyModel::QReactiveProxyModel(QObject* parent) : QIdentityProxyModel(parent), 89 | d_ptr(new QReactiveProxyModelPrivate) 90 | { 91 | d_ptr->q_ptr = this; 92 | d_ptr->m_pConnectionModel = new ConnectedIndicesModel(this, d_ptr); 93 | 94 | connect(this, &QAbstractItemModel::dataChanged, 95 | d_ptr, &QReactiveProxyModelPrivate::slotDataChanged); 96 | connect(this, &QAbstractItemModel::rowsAboutToBeRemoved, 97 | d_ptr, &QReactiveProxyModelPrivate::slotRemoveItem); 98 | } 99 | 100 | ConnectedIndicesModel::ConnectedIndicesModel(QObject* parent, QReactiveProxyModelPrivate* d) 101 | : QAbstractTableModel(parent), d_ptr(d) 102 | { 103 | 104 | } 105 | 106 | QReactiveProxyModel::~QReactiveProxyModel() 107 | { 108 | delete d_ptr; 109 | } 110 | 111 | QMimeData* QReactiveProxyModel::mimeData(const QModelIndexList &indexes) const 112 | { 113 | if (indexes.size() != 1) 114 | return Q_NULLPTR; 115 | 116 | const auto idx = indexes.first(); 117 | 118 | if (!idx.isValid()) 119 | return Q_NULLPTR; 120 | 121 | // Will also have whatever the source had and hopefully the 122 | // `application/x-qabstractitemmodeldatalist` MIME type from 123 | // QAbstractItemModel::mimeData()` 124 | auto md = QIdentityProxyModel::mimeData(indexes); 125 | 126 | connect(md, &QObject::destroyed, d_ptr, &QReactiveProxyModelPrivate::slotMimeDestroyed); 127 | 128 | d_ptr->m_hDraggedIndexCache[md] = idx; 129 | 130 | md->setData(d_ptr->MIME_TYPE, "valid"); 131 | 132 | return md; 133 | } 134 | 135 | bool QReactiveProxyModel::canDropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) const 136 | { 137 | const auto idx = index(row, column, parent); 138 | 139 | if ((!data) || (!idx.isValid()) || !(action & supportedDropActions())) 140 | return QIdentityProxyModel::canDropMimeData(data, action, row, column, parent); 141 | 142 | if (!data->hasFormat(d_ptr->MIME_TYPE)) 143 | return QIdentityProxyModel::canDropMimeData(data, action, row, column, parent); 144 | 145 | // Try to decode `application/x-qabstractitemmodeldatalist` 146 | QModelDataListDecoder decoder(data); 147 | 148 | return decoder.canConvert(idx.data(Qt::EditRole).userType()); 149 | } 150 | 151 | bool QReactiveProxyModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) 152 | { 153 | Q_UNUSED(action) 154 | 155 | if ((!data) || !data->hasFormat(d_ptr->MIME_TYPE)) 156 | return QIdentityProxyModel::dropMimeData(data, action, row, column, parent); 157 | 158 | const auto srcIdx = d_ptr->m_hDraggedIndexCache[data]; 159 | const auto destIdx = index(row, column, parent); 160 | 161 | if ((!srcIdx.isValid()) || !destIdx.isValid()) 162 | return QIdentityProxyModel::dropMimeData(data, action, row, column, parent); 163 | 164 | connectIndices(srcIdx, destIdx); 165 | 166 | return true; 167 | } 168 | 169 | QStringList QReactiveProxyModel::mimeTypes() const 170 | { 171 | static QStringList ret {d_ptr->MIME_TYPE}; 172 | 173 | return ret; 174 | } 175 | 176 | Qt::DropActions QReactiveProxyModel::supportedDropActions() const 177 | { 178 | return QIdentityProxyModel::supportedDropActions() | Qt::LinkAction; 179 | } 180 | 181 | Qt::DropActions QReactiveProxyModel::supportedDragActions() const 182 | { 183 | return QIdentityProxyModel::supportedDropActions() | Qt::LinkAction; 184 | } 185 | 186 | void QReactiveProxyModel::setSourceModel(QAbstractItemModel *sm) 187 | { 188 | if (sm == sourceModel()) 189 | return; 190 | 191 | d_ptr->clear(); 192 | 193 | QIdentityProxyModel::setSourceModel(sm); 194 | 195 | } 196 | 197 | Qt::ItemFlags QReactiveProxyModel::flags(const QModelIndex &idx) const 198 | { 199 | return mapToSource(idx).flags(); 200 | } 201 | 202 | void QReactiveProxyModel::addConnectedRole(int role) 203 | { 204 | if (d_ptr->m_lConnectedRoles.indexOf(role) == -1) 205 | d_ptr->m_lConnectedRoles << role; 206 | } 207 | 208 | QVector QReactiveProxyModel::connectedRoles() const 209 | { 210 | return d_ptr->m_lConnectedRoles; 211 | } 212 | 213 | QAbstractProxyModel* QReactiveProxyModel::currentProxy() const 214 | { 215 | return d_ptr->m_pCurrentProxy; 216 | } 217 | 218 | /** 219 | * Allow to set a proxy the connection model will listen to in order to be 220 | * notified of item changes. 221 | * 222 | * This is useful to avoid having to expose the internal mapping. However, it 223 | * isn't the prettyest workaround ever. 224 | */ 225 | void QReactiveProxyModel::setCurrentProxy(QAbstractProxyModel* proxy) 226 | { 227 | auto cur = d_ptr->m_pCurrentProxy ? d_ptr->m_pCurrentProxy : this; 228 | 229 | disconnect(cur, &QAbstractItemModel::dataChanged, 230 | d_ptr, &QReactiveProxyModelPrivate::slotDataChanged); 231 | 232 | d_ptr->m_pCurrentProxy = proxy; 233 | 234 | cur = d_ptr->m_pCurrentProxy ? d_ptr->m_pCurrentProxy : this; 235 | 236 | connect(cur, &QAbstractItemModel::dataChanged, 237 | d_ptr, &QReactiveProxyModelPrivate::slotDataChanged); 238 | } 239 | 240 | QAbstractItemModel* QReactiveProxyModel::connectionsModel() const 241 | { 242 | return d_ptr->m_pConnectionModel; 243 | } 244 | 245 | int QReactiveProxyModel::extraRole(ExtraRoles type) const 246 | { 247 | return d_ptr->m_ExtraRole[static_cast(type)]; 248 | } 249 | 250 | void QReactiveProxyModel::setExtraRole(ExtraRoles type, int role) 251 | { 252 | d_ptr->m_HasExtraRole[static_cast(type)] = true; 253 | d_ptr->m_ExtraRole [static_cast(type)] = role; 254 | } 255 | 256 | ConnectionHolder* QReactiveProxyModelPrivate::newConnection() 257 | { 258 | if (m_lConnections.isEmpty() || m_lConnections.last()->isUsed()) { 259 | const int id = m_lConnections.size(); 260 | 261 | auto conn = new ConnectionHolder { id, {}, {}, {}, {}, }; 262 | 263 | // Register the connection 264 | //m_pConnectionModel->beginInsertRows({}, id, id); //FIXME conflict with rowCount 265 | m_lConnections << conn; 266 | //m_pConnectionModel->endInsertRows(); 267 | } 268 | 269 | return m_lConnections.last(); 270 | } 271 | 272 | bool QReactiveProxyModel::connectIndices(const QModelIndex& srcIdx, const QModelIndex& destIdx) 273 | { 274 | if (!(srcIdx.isValid() && destIdx.isValid())) 275 | return false; 276 | 277 | if (srcIdx.model() != this || destIdx.model() != this) { 278 | qWarning() << "Trying to connect QModelIndex from the wrong model"; 279 | return false; 280 | } 281 | 282 | //TODO check if its already connected 283 | //TODO check if there is a partial connection that can be re-used 284 | 285 | auto conn = d_ptr->newConnection(); 286 | 287 | Q_ASSERT(srcIdx.model() == this); 288 | Q_ASSERT(destIdx.model() == this); 289 | 290 | conn->source = srcIdx; 291 | conn->destination = destIdx; 292 | conn->sourceIP = srcIdx.internalPointer(); 293 | conn->destinationIP = destIdx.internalPointer(); 294 | 295 | // Check if there is an internal pointer or UID, models can work without 296 | // them, better not assume they exist. It is also not possible to assume 297 | // they are valid or unique. This is checked when it is retrieved. 298 | if (srcIdx.internalPointer()) 299 | d_ptr->m_hDirectMapping[ srcIdx.internalPointer () ] = conn; //FIXME this limits the number of connections to 1 300 | 301 | if (destIdx.internalPointer()) 302 | d_ptr->m_hDirectMapping[ destIdx.internalPointer() ] = conn; //FIXME this limits the number of connections to 1 303 | 304 | // Sync the current source value into the sink 305 | d_ptr->synchronize(srcIdx, destIdx); 306 | 307 | d_ptr->notifyConnect(srcIdx, destIdx); 308 | 309 | return true; 310 | } 311 | 312 | bool QReactiveProxyModel::areConnected(const QModelIndex& source, const QModelIndex& destination) const 313 | { 314 | Q_UNUSED(source) 315 | Q_UNUSED(destination) 316 | return false; //TODO 317 | } 318 | 319 | QList QReactiveProxyModel::sendTo(const QModelIndex& source) const 320 | { 321 | Q_UNUSED(source) 322 | return {}; //TODO 323 | } 324 | 325 | QList QReactiveProxyModel::receiveFrom(const QModelIndex& destination) const 326 | { 327 | Q_UNUSED(destination) 328 | return {}; //TODO 329 | } 330 | 331 | 332 | bool ConnectedIndicesModel::setData(const QModelIndex &index, const QVariant &value, int role) 333 | { 334 | Q_ASSERT((!index.isValid()) || index.model() == this); //TODO remove 335 | 336 | if ((!index.isValid()) || index.model() != this || !value.canConvert()) 337 | return false; 338 | 339 | // This model auto add rows 340 | if (index.row() == d_ptr->m_lConnections.size()) 341 | d_ptr->newConnection(); 342 | 343 | // First, given is a random QModelIndex, it might be in an anonymous proxy. 344 | // Usually, those are rejected, but here is would void some valid use cases. 345 | auto i = value.toModelIndex(); 346 | 347 | #define M qobject_cast(i.model()) 348 | while (i.model() != d_ptr->q_ptr && M && (i = M->mapToSource(i)).isValid()); 349 | #undef M 350 | 351 | // `i` can be invalid (to disconnect) 352 | Q_ASSERT((!i.isValid()) || i.model() == d_ptr->q_ptr); 353 | 354 | const auto conn = d_ptr->m_lConnections[index.row()]; 355 | 356 | const bool wasValid = conn->isValid(); 357 | 358 | switch (role) { 359 | case QReactiveProxyModel::ConnectionsRoles::SOURCE_INDEX: // also DEST 360 | Q_ASSERT(index.column() != 1); 361 | if (index.column() == QReactiveProxyModel::ConnectionsColumns::SOURCE && i != conn->source) { 362 | d_ptr->m_hDirectMapping.remove(conn->sourceIP); 363 | 364 | if (wasValid) 365 | Q_EMIT d_ptr->q_ptr->disconnected(conn->source, conn->destination); 366 | 367 | conn->source = i; 368 | 369 | if (i.isValid()) { 370 | conn->sourceIP = i.internalPointer(); 371 | d_ptr->m_hDirectMapping[i.internalPointer()] = conn; 372 | } 373 | else 374 | conn->sourceIP = nullptr; 375 | d_ptr->synchronize(conn->source, conn->destination); 376 | 377 | if (conn->isValid()) 378 | d_ptr->notifyConnect(conn->source, conn->destination); 379 | } 380 | else if (index.column() == QReactiveProxyModel::ConnectionsColumns::DESTINATION && i != conn->destination) { 381 | d_ptr->m_hDirectMapping.remove(conn->destinationIP); 382 | 383 | if (wasValid) 384 | Q_EMIT d_ptr->q_ptr->disconnected(conn->source, conn->destination); 385 | 386 | conn->destination = i; 387 | 388 | if (i.isValid()) { 389 | conn->destinationIP = i.internalPointer(); 390 | d_ptr->m_hDirectMapping[i.internalPointer()] = conn; 391 | } 392 | else 393 | conn->destinationIP = nullptr; 394 | d_ptr->synchronize(conn->source, conn->destination); 395 | 396 | if (conn->isValid()) 397 | d_ptr->notifyConnect(conn->source, conn->destination); 398 | 399 | //FIXME this may require a beginInsertRows 400 | } 401 | 402 | Q_EMIT dataChanged(index, index); 403 | 404 | return true; 405 | } 406 | 407 | return false; 408 | } 409 | 410 | QVariant ConnectedIndicesModel::data(const QModelIndex& idx, int role) const 411 | { 412 | if ((!idx.isValid()) || d_ptr->m_lConnections.size() <= idx.row()) 413 | return {}; 414 | 415 | const auto conn = d_ptr->m_lConnections[idx.row()]; 416 | 417 | Q_ASSERT(conn); 418 | 419 | // custom role for this model 420 | switch(role) { 421 | case QReactiveProxyModel::ConnectionsRoles::IS_VALID: 422 | return conn->isValid(); 423 | case QReactiveProxyModel::ConnectionsRoles::IS_USED: 424 | return conn->isUsed(); 425 | case QReactiveProxyModel::ConnectionsRoles::SOURCE_INDEX: 426 | switch(idx.column()) { 427 | case QReactiveProxyModel::ConnectionsColumns::SOURCE: 428 | return conn->source; 429 | case QReactiveProxyModel::ConnectionsColumns::DESTINATION: 430 | return conn->destination; 431 | } 432 | break; 433 | case QReactiveProxyModel::ConnectionsRoles::UID: 434 | // Fallback on the Qt::DisplayRole is done on purpose 435 | switch(idx.column()) { 436 | case QReactiveProxyModel::ConnectionsColumns::SOURCE: 437 | return conn->source.data(d_ptr->m_ExtraRole[ 438 | (int) QReactiveProxyModel::ExtraRoles::IdentifierRole 439 | ]); 440 | case QReactiveProxyModel::ConnectionsColumns::DESTINATION: 441 | return conn->destination.data(d_ptr->m_ExtraRole[ 442 | (int) QReactiveProxyModel::ExtraRoles::IdentifierRole 443 | ]); 444 | } 445 | break; 446 | } 447 | 448 | // proxy to the source 449 | switch(idx.column()) { 450 | case QReactiveProxyModel::ConnectionsColumns::SOURCE: 451 | return conn->source.data(role); 452 | case QReactiveProxyModel::ConnectionsColumns::CONNECTION: 453 | return {}; //Eventually they will be named and have colors 454 | case QReactiveProxyModel::ConnectionsColumns::DESTINATION: 455 | return conn->destination.data(role); 456 | } 457 | 458 | return {}; 459 | } 460 | 461 | int ConnectedIndicesModel::rowCount(const QModelIndex& parent) const 462 | { 463 | const bool needNew = (!d_ptr->m_lConnections.size()) || 464 | d_ptr->m_lConnections.last()->isValid(); 465 | 466 | return parent.isValid() ? 0 : d_ptr->m_lConnections.size() + (needNew?1:0); 467 | } 468 | 469 | int ConnectedIndicesModel::columnCount(const QModelIndex& parent) const 470 | { 471 | return parent.isValid() ? 0 : 3; 472 | } 473 | 474 | void QReactiveProxyModelPrivate::clear() 475 | { 476 | while (!m_lConnections.isEmpty()) { 477 | notifyDisconnect(m_lConnections.first()); 478 | delete m_lConnections.first(); 479 | } 480 | } 481 | 482 | void QReactiveProxyModelPrivate::notifyConnect(const QModelIndex& source, const QModelIndex& destination) const 483 | { 484 | typedef QReactiveProxyModel::ExtraRoles R; 485 | 486 | Q_EMIT q_ptr->connected(source, destination); 487 | 488 | if (m_HasExtraRole[(int)R::SourceConnectionNotificationRole]) { 489 | q_ptr->setData(source, destination, m_ExtraRole[(int)R::SourceConnectionNotificationRole]); 490 | } 491 | 492 | if (m_HasExtraRole[(int)R::DestinationConnectionNotificationRole]) 493 | q_ptr->setData(source, source, m_ExtraRole[(int)R::DestinationConnectionNotificationRole]); 494 | 495 | } 496 | 497 | void QReactiveProxyModelPrivate::notifyDisconnect(const QModelIndex& source, const QModelIndex& destination) const 498 | { 499 | typedef QReactiveProxyModel::ExtraRoles R; 500 | 501 | Q_EMIT q_ptr->disconnected(source, destination); 502 | 503 | if (m_HasExtraRole[(int)R::SourceDisconnectionNotificationRole]) 504 | q_ptr->setData(source, destination, m_ExtraRole[(int)R::SourceDisconnectionNotificationRole]); 505 | 506 | if (m_HasExtraRole[(int)R::DestinationDisconnectionNotificationRole]) 507 | q_ptr->setData(source, source, m_ExtraRole[(int)R::DestinationDisconnectionNotificationRole]); 508 | } 509 | 510 | void QReactiveProxyModelPrivate::notifyConnect(const ConnectionHolder* conn) const 511 | { 512 | if (conn->isValid()) 513 | notifyConnect(conn->source, conn->destination); 514 | } 515 | 516 | void QReactiveProxyModelPrivate::notifyDisconnect(const ConnectionHolder* conn) const 517 | { 518 | if (conn->isValid()) 519 | notifyDisconnect(conn->source, conn->destination); 520 | } 521 | 522 | bool QReactiveProxyModelPrivate::synchronize(const QModelIndex& s, const QModelIndex& d) const 523 | { 524 | if ((!s.isValid()) || !d.isValid()) 525 | return false; 526 | 527 | static const QVector fallbackRole {Qt::DisplayRole}; 528 | 529 | const auto roles = m_lConnectedRoles.size() ? &m_lConnectedRoles : &(fallbackRole); 530 | 531 | for (int role : qAsConst(*roles)) { 532 | const auto md = q_ptr->mimeData({s}); 533 | 534 | q_ptr->canDropMimeData( 535 | md, Qt::LinkAction, d.row(), d.column(), d.parent() 536 | ); 537 | q_ptr->setData(d, s.data(role) , role); 538 | } 539 | 540 | return true; 541 | } 542 | 543 | void QReactiveProxyModelPrivate::slotMimeDestroyed() 544 | { 545 | // collect the garbage 546 | m_hDraggedIndexCache.remove(static_cast(QObject::sender())); 547 | } 548 | 549 | void QReactiveProxyModelPrivate::slotDataChanged(const QModelIndex& tl, const QModelIndex& br) 550 | { 551 | if (!tl.isValid()) 552 | return; 553 | 554 | // To avoid doing a foreach of the index matrix, this model ties to implement 555 | // some "hacky" optimizations to keep the overhead low. There is 3 scenarios: 556 | // 557 | // 1) There is only 1 changed item. Then use the internal pointers has hash 558 | // keys. 559 | // 2) The top_left...bottom_right is smaller than the number of connected 560 | // pairs. Then foreach the matrix 561 | // 3) The matrix is larger than the number of connections. Then foreach 562 | // the connections. And use the `parent()` and `<=` `>=` operators. 563 | // Only 1 item changed 564 | if (tl == br) { 565 | if (const auto conn = m_hDirectMapping[tl.internalPointer()]) { 566 | if (synchronize(conn->source, conn->destination)) 567 | Q_EMIT m_pConnectionModel->dataChanged( 568 | m_pConnectionModel->index(conn->index, 0), 569 | m_pConnectionModel->index(conn->index, 2) 570 | ); 571 | } 572 | } 573 | else { 574 | //TODO make this faster... 575 | 576 | for (int i = tl.row(); i <= br.row(); i++) 577 | for (int j = tl.column(); j <= br.column(); j++) { 578 | const auto idx = tl.model()->index(i, j, tl.parent()); 579 | slotDataChanged(idx, idx); 580 | } 581 | } 582 | 583 | //TODO implement scenario 3 584 | } 585 | 586 | void QReactiveProxyModelPrivate::slotRemoveItem(const QModelIndex &parent, int first, int last) 587 | { 588 | // Use a recursive scan of all children to get all connections and remove 589 | // them. 590 | for (int i = first; i <= last; i++) { 591 | const int cc = q_ptr->columnCount(parent); 592 | for (int j=0 ; j < cc; j++) { 593 | const auto idx = q_ptr->index(i, j, parent); 594 | if (auto conn = m_hDirectMapping[idx.internalPointer()]) { 595 | conn->source = QModelIndex(); 596 | conn->destination = QModelIndex(); 597 | conn->sourceIP = Q_NULLPTR; 598 | conn->destinationIP = Q_NULLPTR; 599 | 600 | Q_EMIT m_pConnectionModel->dataChanged( 601 | m_pConnectionModel->index(conn->index, 0), 602 | m_pConnectionModel->index(conn->index, 2) 603 | ); 604 | } 605 | 606 | if (const int rc = q_ptr->rowCount(idx)) 607 | slotRemoveItem(idx, 0, rc -1); 608 | } 609 | } 610 | } 611 | -------------------------------------------------------------------------------- /src/graphicsnode.cpp: -------------------------------------------------------------------------------- 1 | /* See LICENSE file for copyright and license details. */ 2 | 3 | #include "graphicsnode.hpp" 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | #include 19 | 20 | #include "graphicsnode_p.h" 21 | 22 | #include "graphicsbezieredge.hpp" 23 | #include "graphicsnodesocket.hpp" 24 | #include "graphicsnodesocket_p.h" 25 | 26 | #include "qnodeeditorsocketmodel.h" 27 | 28 | #if QT_VERSION < 0x050700 29 | //Q_FOREACH is deprecated and Qt CoW containers are detached on C++11 for loops 30 | template 31 | const T& qAsConst(const T& v) 32 | { 33 | return const_cast(v); 34 | } 35 | #endif 36 | 37 | class CloseButton : public QGraphicsTextItem 38 | { 39 | public: 40 | explicit CloseButton(NodeGraphicsItem* parent); 41 | 42 | int width() const; 43 | 44 | private: 45 | GraphicsNodePrivate* d_ptr; 46 | 47 | protected: 48 | virtual void mousePressEvent(QGraphicsSceneMouseEvent *event) override; 49 | }; 50 | 51 | class NodeTitle; 52 | 53 | class GraphicsNodePrivate final 54 | { 55 | public: 56 | explicit GraphicsNodePrivate(GraphicsNode* q) : q_ptr(q) {} 57 | 58 | QNodeEditorSocketModel* m_pModel; 59 | QPersistentModelIndex m_Index; 60 | 61 | // TODO: change pairs of sizes to QPointF, QSizeF, or quadrupels to QRectF 62 | 63 | constexpr static const qreal _hard_min_width = 150.0; 64 | constexpr static const qreal _hard_min_height = 120.0; 65 | 66 | QSizeF m_MinSize {150.0, 120.0}; 67 | 68 | constexpr static const qreal _top_margin = 30.0; 69 | constexpr static const qreal _bottom_margin = 15.0; 70 | constexpr static const qreal _item_padding = 5.0; 71 | constexpr static const qreal _lr_padding = 10.0; 72 | 73 | constexpr static const qreal _pen_width = 1.0; 74 | constexpr static const qreal _socket_size = 6.0; 75 | 76 | NodeGraphicsItem* m_pGraphicsItem; 77 | 78 | bool _changed {false}; 79 | 80 | QSizeF m_Size {150, 120}; 81 | 82 | QPen _pen_default {QColor("#7F000000")}; 83 | QPen _pen_selected {QColor("#FFFF36A7")}; 84 | QPen _pen_sources {QColor("#FF000000")}; 85 | QPen _pen_sinks {QColor("#FF000000")}; 86 | 87 | QBrush _brush_title {QColor("#E3212121")}; 88 | QBrush _brush_background {QColor("#E31a1a1a")}; 89 | QBrush m_brushSources {QColor("#FFFF7700")}; 90 | QBrush m_brushSinks {QColor("#FF0077FF")}; 91 | 92 | //TODO lazy load, add option to disable, its nice, but sllooowwww 93 | #if 0 94 | QGraphicsDropShadowEffect *_effect {new QGraphicsDropShadowEffect()}; 95 | #endif 96 | NodeTitle *_title_item {nullptr}; 97 | QGraphicsPixmapItem *_deco_item {nullptr}; 98 | CloseButton *_close_item {nullptr}; 99 | QGraphicsProxyWidget *_central_proxy {nullptr}; 100 | 101 | // Helpers 102 | void updateGeometry(); 103 | void updateSizeHints(); 104 | 105 | GraphicsNode* q_ptr; 106 | }; 107 | 108 | // Necessary for some compilers... 109 | constexpr const qreal GraphicsNodePrivate::_hard_min_width; 110 | constexpr const qreal GraphicsNodePrivate::_hard_min_height; 111 | constexpr const qreal GraphicsNodePrivate::_top_margin; 112 | constexpr const qreal GraphicsNodePrivate::_bottom_margin; 113 | constexpr const qreal GraphicsNodePrivate::_item_padding; 114 | constexpr const qreal GraphicsNodePrivate::_lr_padding; 115 | constexpr const qreal GraphicsNodePrivate::_pen_width; 116 | constexpr const qreal GraphicsNodePrivate::_socket_size; 117 | 118 | class NodeTitle : public QGraphicsTextItem 119 | { 120 | public: 121 | explicit NodeTitle(GraphicsNodePrivate* parent) : QGraphicsTextItem(parent->m_pGraphicsItem), 122 | d_ptr(parent) {} 123 | 124 | private: 125 | GraphicsNodePrivate* d_ptr; 126 | 127 | protected: 128 | virtual void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override; 129 | virtual void focusOutEvent(QFocusEvent* event) override; 130 | virtual void keyPressEvent(QKeyEvent *event) override; 131 | }; 132 | 133 | 134 | void NodeTitle::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) 135 | { 136 | Q_UNUSED(event); 137 | setTextInteractionFlags( 138 | Qt::TextEditorInteraction 139 | ); 140 | 141 | auto cur = textCursor(); 142 | cur.select(QTextCursor::Document); 143 | setTextCursor(cur); 144 | setFocus(); 145 | } 146 | 147 | void NodeTitle::focusOutEvent(QFocusEvent* event) 148 | { 149 | auto cur = textCursor(); 150 | cur.clearSelection(); 151 | setTextCursor(cur); 152 | 153 | setTextInteractionFlags( 154 | Qt::NoTextInteraction 155 | ); 156 | 157 | if (event) 158 | event->accept(); 159 | } 160 | 161 | void NodeTitle::keyPressEvent(QKeyEvent *event) 162 | { 163 | if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) { 164 | focusOutEvent(nullptr); 165 | d_ptr->q_ptr->setTitle(toPlainText()); 166 | event->accept(); 167 | } 168 | else if (event->key() == Qt::Key_Escape) { 169 | focusOutEvent(nullptr); 170 | setPlainText(d_ptr->m_Index.data().toString()); 171 | event->accept(); 172 | } 173 | 174 | QGraphicsTextItem::keyPressEvent(event); 175 | } 176 | 177 | GraphicsNode::GraphicsNode(QNodeEditorSocketModel* model, const QPersistentModelIndex& index, QGraphicsItem *parent) 178 | : QObject(nullptr), d_ptr(new GraphicsNodePrivate(this)) 179 | { 180 | d_ptr->m_pModel = model; 181 | d_ptr->m_Index = index; 182 | 183 | d_ptr->m_pGraphicsItem = new NodeGraphicsItem(parent); 184 | d_ptr->m_pGraphicsItem->d_ptr = d_ptr; 185 | d_ptr->m_pGraphicsItem->q_ptr = this; 186 | 187 | d_ptr->_deco_item = new QGraphicsPixmapItem(d_ptr->m_pGraphicsItem); 188 | d_ptr->_deco_item->setPos(2, 4); 189 | 190 | d_ptr->_title_item = new NodeTitle(d_ptr); 191 | 192 | for (auto p : { 193 | &d_ptr->_pen_default, &d_ptr->_pen_selected, 194 | &d_ptr->_pen_default, &d_ptr->_pen_selected 195 | }) 196 | p->setWidth(0); 197 | 198 | d_ptr->m_pGraphicsItem->setFlag(QGraphicsItem::ItemIsMovable); 199 | d_ptr->m_pGraphicsItem->setFlag(QGraphicsItem::ItemIsSelectable); 200 | d_ptr->m_pGraphicsItem->setFlag(QGraphicsItem::ItemSendsGeometryChanges); 201 | 202 | // The close button 203 | d_ptr->_close_item = new CloseButton(d_ptr->m_pGraphicsItem); 204 | d_ptr->_close_item->setPos(d_ptr->m_Size.width() - d_ptr->_close_item->width(), 0); 205 | 206 | d_ptr->_title_item->setDefaultTextColor(Qt::white); 207 | 208 | d_ptr->_title_item->setPos(20, 0); 209 | d_ptr->_title_item->setTextWidth( 210 | d_ptr->m_Size.width() 211 | - 2*d_ptr->_lr_padding 212 | - d_ptr->_close_item->width() 213 | - 20 //icon 214 | ); 215 | 216 | #if 0 217 | d_ptr->_effect->setBlurRadius(13.0); 218 | d_ptr->_effect->setColor(QColor("#99121212")); 219 | 220 | d_ptr->m_pGraphicsItem->setGraphicsEffect(d_ptr->_effect); 221 | #endif 222 | } 223 | 224 | 225 | void GraphicsNode:: 226 | setTitle(const QString &title) 227 | { 228 | if (auto m = const_cast(d_ptr->m_Index.model())) 229 | m->setData(d_ptr->m_Index, title, Qt::DisplayRole); 230 | 231 | d_ptr->m_pGraphicsItem->prepareGeometryChange(); 232 | d_ptr->_title_item->setPlainText(d_ptr->m_Index.data().toString()); 233 | } 234 | 235 | void GraphicsNode:: 236 | setDecoration(const QVariant& deco) 237 | { 238 | if (!deco.isValid()) 239 | return; 240 | 241 | if (deco.canConvert()) { 242 | const auto px = qvariant_cast(deco).pixmap(16,16); 243 | d_ptr->_deco_item->setPixmap(px); 244 | } 245 | else if (deco.canConvert()) { 246 | d_ptr->_deco_item->setPixmap(qvariant_cast(deco)); 247 | } 248 | } 249 | 250 | void GraphicsNode:: 251 | setBackground(const QString& brush) 252 | { 253 | if (auto m = const_cast(d_ptr->m_Index.model())) 254 | m->setData(d_ptr->m_Index, QColor(brush), Qt::BackgroundRole); 255 | } 256 | 257 | void GraphicsNode:: 258 | setBackground(const QBrush& brush) 259 | { 260 | if (auto m = const_cast(d_ptr->m_Index.model())) 261 | m->setData(d_ptr->m_Index, brush, Qt::BackgroundRole); 262 | } 263 | 264 | void GraphicsNode:: 265 | setForeground(const QString& pen) 266 | { 267 | setForeground(QColor(pen)); 268 | } 269 | 270 | void GraphicsNode:: 271 | setForeground(const QPen& pen) 272 | { 273 | if (auto m = const_cast(d_ptr->m_Index.model())) 274 | m->setData(d_ptr->m_Index, pen, Qt::ForegroundRole); 275 | 276 | //FIXME this should be removed once dataChanged really reload it 277 | const auto fgVar = d_ptr->m_Index.data(Qt::ForegroundRole); 278 | 279 | d_ptr->_title_item->setDefaultTextColor(fgVar.canConvert() ? 280 | qvariant_cast(fgVar) : Qt::white 281 | ); 282 | 283 | // Update the palette 284 | if (d_ptr->_central_proxy) { 285 | auto pal = d_ptr->_central_proxy->palette(); 286 | pal.setColor(QPalette::Foreground, pen.color()); 287 | d_ptr->_central_proxy->setPalette(pal); 288 | } 289 | } 290 | 291 | void GraphicsNode:: 292 | setForeground(const QColor& pen) 293 | { 294 | if (auto m = const_cast(d_ptr->m_Index.model())) 295 | m->setData(d_ptr->m_Index, pen, Qt::ForegroundRole); 296 | 297 | //FIXME this should be removed once dataChanged really reload it 298 | const auto fgVar = d_ptr->m_Index.data(Qt::ForegroundRole); 299 | 300 | d_ptr->_title_item->setDefaultTextColor(fgVar.canConvert() ? 301 | qvariant_cast(fgVar) : Qt::white 302 | ); 303 | 304 | // Update the palette 305 | if (d_ptr->_central_proxy) { 306 | auto pal = d_ptr->_central_proxy->palette(); 307 | pal.setColor(QPalette::Foreground, pen); 308 | d_ptr->_central_proxy->setPalette(pal); 309 | } 310 | } 311 | 312 | QString GraphicsNode:: 313 | title() const 314 | { 315 | return d_ptr->m_Index.data().toString(); 316 | } 317 | 318 | QBrush GraphicsNode:: 319 | background() const 320 | { 321 | return qvariant_cast(d_ptr->m_Index.data(Qt::BackgroundRole)); 322 | } 323 | 324 | QPen GraphicsNode:: 325 | foreground() const 326 | { 327 | return qvariant_cast(d_ptr->m_Index.data(Qt::ForegroundRole)); 328 | } 329 | 330 | QAbstractItemModel *GraphicsNode:: 331 | sinkModel() const 332 | { 333 | return d_ptr->m_pModel->sinkSocketModel(d_ptr->m_Index); 334 | } 335 | 336 | QAbstractItemModel *GraphicsNode:: 337 | sourceModel() const 338 | { 339 | return d_ptr->m_pModel->sourceSocketModel(d_ptr->m_Index); 340 | } 341 | 342 | int NodeGraphicsItem:: 343 | type() const 344 | { 345 | return GraphicsNodeItemTypes::TypeNode; 346 | } 347 | 348 | QSizeF GraphicsNode:: 349 | size() const 350 | { 351 | return d_ptr->m_Size; 352 | } 353 | 354 | QRectF GraphicsNode:: 355 | rect() const 356 | { 357 | return QRectF( 358 | d_ptr->m_pGraphicsItem->pos(), 359 | d_ptr->m_Size 360 | ); 361 | } 362 | 363 | QGraphicsItem *GraphicsNode:: 364 | graphicsItem() const 365 | { 366 | return d_ptr->m_pGraphicsItem; 367 | } 368 | 369 | GraphicsNode:: 370 | ~GraphicsNode() 371 | { 372 | Q_ASSERT(!d_ptr->m_pGraphicsItem->scene()); 373 | // The widget proxy doesn't own the widget unless specified 374 | if (d_ptr->_central_proxy) { 375 | delete d_ptr->_central_proxy; 376 | d_ptr->_central_proxy = Q_NULLPTR; 377 | 378 | // Even when removed from the scene, the prepareGeometryChange is 379 | // required to avoid a crash, don't ask me why 380 | //setSize(0,0); 381 | } 382 | 383 | delete d_ptr->_title_item; 384 | delete d_ptr->_deco_item; 385 | #if 0 386 | delete d_ptr->_effect; 387 | #endif 388 | delete d_ptr->m_pGraphicsItem; 389 | delete d_ptr; 390 | } 391 | 392 | 393 | QRectF NodeGraphicsItem:: 394 | boundingRect() const 395 | { 396 | return QRectF( 397 | -d_ptr->_pen_width/2.0 - d_ptr->_socket_size, 398 | -d_ptr->_pen_width/2.0, 399 | d_ptr->m_Size.width() + d_ptr->_pen_width/2.0 + 2.0 * d_ptr->_socket_size, 400 | d_ptr->m_Size.height() + d_ptr->_pen_width/2.0 401 | ).normalized(); 402 | } 403 | 404 | 405 | void NodeGraphicsItem:: 406 | paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) 407 | { 408 | const qreal edge_size = 10.0; 409 | const qreal title_height = 20.0; 410 | 411 | // path for the caption of this node 412 | QPainterPath path_title; 413 | path_title.setFillRule(Qt::WindingFill); 414 | path_title.addRoundedRect(QRect(0, 0, d_ptr->m_Size.width(), title_height), edge_size, edge_size); 415 | path_title.addRect(0, title_height - edge_size, edge_size, edge_size); 416 | path_title.addRect(d_ptr->m_Size.width() - edge_size, title_height - edge_size, edge_size, edge_size); 417 | painter->setPen(Qt::NoPen); 418 | painter->setBrush(d_ptr->_brush_title); 419 | painter->drawPath(path_title.simplified()); 420 | 421 | // path for the content of this node 422 | QPainterPath path_content; 423 | path_content.setFillRule(Qt::WindingFill); 424 | path_content.addRoundedRect(QRect(0, title_height, d_ptr->m_Size.width(), d_ptr->m_Size.height() - title_height), edge_size, edge_size); 425 | path_content.addRect(0, title_height, edge_size, edge_size); 426 | path_content.addRect(d_ptr->m_Size.width() - edge_size, title_height, edge_size, edge_size); 427 | painter->setPen(Qt::NoPen); 428 | 429 | const auto bgVar = d_ptr->m_Index.data(Qt::BackgroundRole); 430 | 431 | painter->setBrush(bgVar.canConvert() ? 432 | qvariant_cast(bgVar) : d_ptr->_brush_background 433 | ); 434 | 435 | painter->drawPath(path_content.simplified()); 436 | 437 | // path for the outline 438 | QPainterPath path_outline = QPainterPath(); 439 | path_outline.addRoundedRect(QRect(0, 0, d_ptr->m_Size.width(), d_ptr->m_Size.height()), edge_size, edge_size); 440 | painter->setPen(isSelected() ? d_ptr->_pen_selected : d_ptr->_pen_default); 441 | painter->setBrush(Qt::NoBrush); 442 | painter->drawPath(path_outline.simplified()); 443 | 444 | // debug bounding box 445 | #if 0 446 | QPen debugPen = QPen(QColor(Qt::red)); 447 | debugPen.setWidth(0); 448 | auto r = boundingRect(); 449 | painter->setPen(debugPen); 450 | painter->setBrush(Qt::NoBrush); 451 | painter->drawRect(r); 452 | 453 | painter->drawPoint(0,0); 454 | #endif 455 | } 456 | 457 | 458 | void NodeGraphicsItem:: 459 | mousePressEvent(QGraphicsSceneMouseEvent *event) 460 | { 461 | // TODO: ordering after selection/deselection cycle 462 | QGraphicsItem::mousePressEvent(event); 463 | 464 | setZValue(isSelected() ? 1 : 0); 465 | } 466 | 467 | 468 | void GraphicsNode:: 469 | setSize(const qreal width, const qreal height) 470 | { 471 | setSize(QPointF(width, height)); 472 | } 473 | 474 | 475 | void GraphicsNode:: 476 | setSize(const QPointF size) 477 | { 478 | setSize(QSizeF(size.x(), size.y())); 479 | } 480 | 481 | 482 | void GraphicsNode:: 483 | setSize(const QSizeF size) 484 | { 485 | d_ptr->m_Size = { 486 | std::max(d_ptr->m_MinSize.width() , size.width ()), 487 | std::max(d_ptr->m_MinSize.height(), size.height()) 488 | }; 489 | 490 | d_ptr->_changed = true; 491 | d_ptr->m_pGraphicsItem->prepareGeometryChange(); 492 | d_ptr->updateGeometry(); 493 | } 494 | 495 | void GraphicsNode:: 496 | setRect(const qreal x, const qreal y, const qreal width, const qreal height) 497 | { 498 | graphicsItem()->setPos(x, y); 499 | setSize(width, height); 500 | } 501 | 502 | void GraphicsNode:: 503 | setRect(const QRectF size) 504 | { 505 | graphicsItem()->setPos(size.topLeft()); 506 | setSize(size.size()); 507 | } 508 | 509 | QVariant NodeGraphicsItem:: 510 | itemChange(GraphicsItemChange change, const QVariant &value) 511 | { 512 | #pragma GCC diagnostic push 513 | #pragma GCC diagnostic ignored "-Wswitch" 514 | switch (change) { 515 | case QGraphicsItem::ItemSelectedChange: 516 | setZValue(value.toBool() ? 1 : 0); 517 | break; 518 | case QGraphicsItem::ItemPositionChange: 519 | case QGraphicsItem::ItemPositionHasChanged: { 520 | auto m = const_cast(d_ptr->m_Index.model()); 521 | 522 | m->setData(d_ptr->m_Index, q_ptr->rect(), Qt::SizeHintRole); 523 | } 524 | break; 525 | 526 | default: 527 | break; 528 | } 529 | #pragma GCC diagnostic pop 530 | return QGraphicsItem::itemChange(change, value); 531 | } 532 | 533 | void GraphicsNode::update() 534 | { 535 | d_ptr->_changed = true; 536 | d_ptr->m_pGraphicsItem->prepareGeometryChange(); 537 | d_ptr->updateGeometry(); 538 | } 539 | 540 | QModelIndex GraphicsNode::index() const 541 | { 542 | return d_ptr->m_Index; 543 | } 544 | 545 | void GraphicsNode::setIndex(const QModelIndex& idx) 546 | { 547 | d_ptr->m_Index = idx; 548 | } 549 | 550 | QAbstractItemModel* GraphicsNode::model() const 551 | { 552 | return d_ptr->m_pModel; 553 | } 554 | 555 | const QModelIndex GraphicsNode::socketIndex(const QString& name) const 556 | { 557 | const auto c = model()->rowCount(index()); 558 | for (int i = 0; i < c; i++) { 559 | auto idx = model()->index(i, 0, index()); 560 | if (idx.data() == name) 561 | return idx; 562 | } 563 | 564 | return {}; 565 | } 566 | 567 | void GraphicsNodePrivate:: 568 | updateGeometry() 569 | { 570 | if (!_changed) return; 571 | 572 | // compute if we have reached the minimum size 573 | updateSizeHints(); 574 | 575 | // close button 576 | _close_item->setPos(m_Size.width() - _close_item->width(), 0); 577 | 578 | // title 579 | _title_item->setTextWidth(m_Size.width() - _close_item->width()); 580 | 581 | qreal yposSink = _top_margin; 582 | qreal yposSrc = m_Size.height() - _bottom_margin; 583 | 584 | const int count = m_pModel->rowCount(m_Index); 585 | 586 | for (int i = 0; i < count; i++) { 587 | const auto idx = m_pModel->index(i, 0, m_Index); 588 | 589 | // sinks 590 | if (const auto s = m_pModel->getSinkSocket(idx)) { 591 | const auto size = s->size(); 592 | 593 | s->graphicsItem()->setPos(0, yposSink + size.height()/2.0); 594 | yposSink += size.height() + _item_padding; 595 | 596 | s->graphicsItem()->setOpacity(s->index().flags() & Qt::ItemIsEnabled ? 597 | 1.0 : 0.1 598 | ); 599 | } 600 | 601 | // sources 602 | if (const auto s = m_pModel->getSourceSocket(idx)) { 603 | const auto size = s->size(); 604 | 605 | yposSrc -= size.height(); 606 | s->graphicsItem()->setPos(m_Size.width(), yposSrc + size.height()/2.0); 607 | yposSrc -= _item_padding; 608 | 609 | s->graphicsItem()->setOpacity(s->index().flags() & Qt::ItemIsEnabled ? 610 | 1.0 : 0.1 611 | ); 612 | } 613 | } 614 | 615 | // central widget 616 | if (_central_proxy) { 617 | _central_proxy->setGeometry({ 618 | _lr_padding, 619 | yposSink, 620 | m_Size.width() - 2.0 * _lr_padding, 621 | yposSrc - yposSink 622 | }); 623 | } 624 | 625 | _changed = false; 626 | } 627 | 628 | void GraphicsNode:: 629 | setCentralWidget (QWidget *widget) 630 | { 631 | if (d_ptr->_central_proxy) 632 | delete d_ptr->_central_proxy; 633 | 634 | if (!widget) { 635 | d_ptr->m_pGraphicsItem->prepareGeometryChange(); 636 | d_ptr->updateGeometry(); 637 | } 638 | 639 | d_ptr->_central_proxy = new QGraphicsProxyWidget(d_ptr->m_pGraphicsItem); 640 | 641 | // Update the palette 642 | auto pal = d_ptr->_central_proxy->palette(); 643 | 644 | const auto fgVar = d_ptr->m_Index.data(Qt::ForegroundRole); 645 | 646 | if (fgVar.isValid()) { 647 | if(fgVar.canConvert()) 648 | pal.setColor(QPalette::Foreground, qvariant_cast(fgVar)); 649 | } 650 | 651 | pal.setColor(QPalette::Background, Qt::transparent); 652 | d_ptr->_central_proxy->setPalette(pal); 653 | 654 | d_ptr->_central_proxy->setWidget(widget); 655 | d_ptr->_changed = true; 656 | d_ptr->m_pGraphicsItem->prepareGeometryChange(); 657 | d_ptr->updateGeometry(); 658 | } 659 | 660 | 661 | void GraphicsNodePrivate:: 662 | updateSizeHints() { 663 | qreal min_width(0.0), min_height(_top_margin + _bottom_margin); 664 | 665 | const int count = m_pModel->rowCount(m_Index); 666 | 667 | // sinks 668 | for (int i = 0; i < count; i++) { 669 | if (const auto s = m_pModel->getSinkSocket(m_pModel->index(i, 0, m_Index))) { 670 | auto size = s->minimalSize(); 671 | 672 | min_height += size.height() + _item_padding; 673 | min_width = std::max(size.width(), min_width); 674 | } 675 | } 676 | 677 | // central widget 678 | if (_central_proxy) { 679 | if (const auto wgt = _central_proxy->widget()) { 680 | // only take the size hint if the value is valid, and if 681 | // the minimumSize is not set (similar to 682 | // QWidget/QLayout standard behavior 683 | const auto sh = wgt->minimumSizeHint(); 684 | const auto sz = wgt->minimumSize(); 685 | 686 | if (sh.isValid()) { 687 | min_height += (sz.height() > 0) ? sz.height() : sh.height(); 688 | 689 | min_width = std::max( 690 | qreal((sz.width() > 0) ? sz.width() : sh.width()) 691 | + 2.0*_lr_padding, 692 | min_width 693 | ); 694 | 695 | } else { 696 | min_height += sh.height(); 697 | min_width = std::max( 698 | qreal(sh.width()) + 2.0*_lr_padding, 699 | min_width 700 | ); 701 | } 702 | } 703 | } 704 | 705 | // sources 706 | for (int i = 0; i < count; i++) { 707 | if (const auto s = m_pModel->getSourceSocket(m_pModel->index(i, 0, m_Index))) { 708 | const auto size = s->minimalSize(); 709 | 710 | min_height += size.height() + _item_padding; 711 | min_width = std::max(size.width(), min_width); 712 | } 713 | } 714 | 715 | m_MinSize = { 716 | std::max(min_width, _hard_min_width ), 717 | std::max(min_height, _hard_min_height) 718 | }; 719 | 720 | // prevent the scene from being out of sync 721 | if (m_Size.width() < min_width || m_Size.height() < min_height) { 722 | m_pGraphicsItem->prepareGeometryChange(); 723 | 724 | m_Size = { 725 | std::max(min_width , m_Size.width ()), 726 | std::max(min_height, m_Size.height()) 727 | }; 728 | 729 | // Will cause the socket and edges to be updated 730 | //FIXME Q_EMIT m_pModel->dataChanged(m_Index, m_Index); 731 | } 732 | } 733 | 734 | CloseButton::CloseButton(NodeGraphicsItem* parent) : QGraphicsTextItem(parent), 735 | d_ptr(parent->d_ptr) 736 | { 737 | setDefaultTextColor(Qt::white); 738 | setHtml(QStringLiteral("")); 739 | setTextWidth(width()); 740 | } 741 | 742 | int CloseButton::width() const 743 | { 744 | static int closeWidth = 0; 745 | if (!closeWidth) 746 | closeWidth = QFontMetrics(font()).height(); 747 | 748 | return closeWidth; 749 | } 750 | 751 | void CloseButton::mousePressEvent(QGraphicsSceneMouseEvent *event) 752 | { 753 | d_ptr->m_pModel->removeRow(d_ptr->m_Index.row(), d_ptr->m_Index.parent()); 754 | event->accept(); 755 | } 756 | -------------------------------------------------------------------------------- /src/qnodeeditorsocketmodel.cpp: -------------------------------------------------------------------------------- 1 | #include "qnodeeditorsocketmodel.h" 2 | 3 | #include "graphicsnode.hpp" 4 | #include "graphicsnodescene.hpp" 5 | #include "graphicsbezieredge.hpp" 6 | #include "graphicsbezieredge_p.h" 7 | 8 | #include "qreactiveproxymodel.h" 9 | 10 | #include "qmodeldatalistdecoder.h" 11 | 12 | #include 13 | #include 14 | #include 15 | 16 | #include "qobjectmodel.h" //TODO remove 17 | 18 | #if QT_VERSION < 0x050700 19 | //Q_FOREACH is deprecated and Qt CoW containers are detached on C++11 for loops 20 | template 21 | const T& qAsConst(const T& v) 22 | { 23 | return const_cast(v); 24 | } 25 | #endif 26 | 27 | class QNodeEdgeFilterProxy; 28 | 29 | struct EdgeWrapper; 30 | struct SocketWrapper; 31 | 32 | struct NodeWrapper final 33 | { 34 | NodeWrapper(QNodeEditorSocketModel *q, const QPersistentModelIndex& i) : m_Node(q, i) {} 35 | 36 | GraphicsNode m_Node; 37 | 38 | // Keep aligned with the source row 39 | QVector m_lSourcesFromSrc {}; 40 | QVector m_lSinksFromSrc {}; 41 | 42 | QVector m_lSourcesToSrc {}; 43 | QVector m_lSinksToSrc {}; 44 | 45 | // Keep aligned with the node row 46 | QVector m_lSources {}; 47 | QVector m_lSinks {}; 48 | 49 | mutable QNodeEdgeFilterProxy *m_pSourceProxy {Q_NULLPTR}; 50 | mutable QNodeEdgeFilterProxy *m_pSinkProxy {Q_NULLPTR}; 51 | 52 | QRectF m_SceneRect; 53 | }; 54 | 55 | struct SocketWrapper final 56 | { 57 | SocketWrapper(const QModelIndex& idx, GraphicsNodeSocket::SocketType t, NodeWrapper* n) 58 | : m_Socket(idx, t, &n->m_Node), m_pNode(n) {} 59 | 60 | GraphicsNodeSocket m_Socket; 61 | 62 | NodeWrapper* m_pNode; 63 | 64 | EdgeWrapper* m_EdgeWrapper {Q_NULLPTR}; 65 | }; 66 | 67 | //TODO split the "data" and "proxy" part of this class and use it to replace 68 | // the NodeWrapper:: content. This way, getting an on demand proxy will cost 69 | // "nothing" 70 | class QNodeEdgeFilterProxy final : public QAbstractProxyModel 71 | { 72 | Q_OBJECT 73 | public: 74 | explicit QNodeEdgeFilterProxy(QNodeEditorSocketModelPrivate* d, NodeWrapper *w, GraphicsNodeSocket::SocketType t); 75 | 76 | virtual int rowCount(const QModelIndex& parent = {}) const override; 77 | virtual QModelIndex mapFromSource(const QModelIndex& sourceIndex) const override; 78 | virtual QModelIndex mapToSource(const QModelIndex& proxyIndex) const override; 79 | virtual int columnCount(const QModelIndex& parent = {}) const override; 80 | virtual QModelIndex index(int row, int column, const QModelIndex& parent ={}) const override; 81 | virtual QVariant data(const QModelIndex& idx, int role) const override; 82 | virtual QModelIndex parent(const QModelIndex& idx) const override; 83 | virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; 84 | virtual Qt::ItemFlags flags(const QModelIndex &idx) const override; 85 | 86 | // void updateExtraColumns( 87 | 88 | private: 89 | const GraphicsNodeSocket::SocketType m_Type; 90 | const NodeWrapper *m_pWrapper; 91 | 92 | // Helpers 93 | void processRow(const QModelIndex& srcIdx); 94 | 95 | QNodeEditorSocketModelPrivate* d_ptr; 96 | }; 97 | 98 | struct EdgeWrapper final 99 | { 100 | explicit EdgeWrapper(QNodeEditorEdgeModel* m, const QModelIndex& index) 101 | : m_Edge(m, index) 102 | { Q_ASSERT(index.isValid()); } 103 | 104 | SocketWrapper* m_pSource {Q_NULLPTR}; 105 | GraphicsBezierEdge m_Edge ; 106 | SocketWrapper* m_pSink {Q_NULLPTR}; 107 | bool m_IsShown { false }; 108 | int m_EdgeId {s_CurId++}; 109 | 110 | static int s_CurId; 111 | }; 112 | 113 | class QNodeEditorSocketModelPrivate final : public QObject 114 | { 115 | Q_OBJECT 116 | public: 117 | enum class State { 118 | NORMAL, 119 | DRAGGING, 120 | }; 121 | 122 | explicit QNodeEditorSocketModelPrivate(QObject* p) : QObject(p) {} 123 | 124 | QNodeEditorEdgeModel m_EdgeModel {this}; 125 | QVector m_lWrappers; 126 | QVector m_lEdges; 127 | GraphicsNodeScene* m_pScene; 128 | State m_State {State::NORMAL}; 129 | quint32 m_CurrentTypeId {QMetaType::UnknownType}; 130 | 131 | // helper 132 | GraphicsNode* insertNode(int idx); 133 | NodeWrapper* getNode(const QModelIndex& idx, bool r = false) const; 134 | 135 | void insertSockets(const QModelIndex& parent, int first, int last); 136 | void updateSockets(const QModelIndex& parent, int first, int last); 137 | 138 | GraphicsDirectedEdge* initiateConnectionFromSource( 139 | const QModelIndex& index, 140 | GraphicsNodeSocket::SocketType type, 141 | const QPointF& point 142 | ); 143 | 144 | // Use a template so the compiler can safely inline the result 145 | template< 146 | QVector NodeWrapper::* SM, 147 | QVector NodeWrapper::* S, 148 | SocketWrapper* EdgeWrapper::* E 149 | > 150 | inline SocketWrapper* getSocketCommon(const QModelIndex& idx) const; 151 | 152 | SocketWrapper* getSourceSocket(const QModelIndex& idx) const; 153 | SocketWrapper* getSinkSocket(const QModelIndex& idx) const; 154 | 155 | QNodeEditorSocketModel* q_ptr; 156 | 157 | public Q_SLOTS: 158 | void slotRowsInserted (const QModelIndex& parent, int first, int last); 159 | void slotConnectionsInserted(const QModelIndex& parent, int first, int last); 160 | void slotConnectionsChanged (const QModelIndex& tl, const QModelIndex& br ); 161 | void slotAboutRemoveItem (const QModelIndex &parent, int first, int last); 162 | void exitDraggingMode(); 163 | }; 164 | 165 | int EdgeWrapper::s_CurId = 1; 166 | 167 | QNodeEditorSocketModel::QNodeEditorSocketModel( QReactiveProxyModel* rmodel, GraphicsNodeScene* scene ) : 168 | QTypeColoriserProxy(rmodel), d_ptr(new QNodeEditorSocketModelPrivate(this)) 169 | { 170 | Q_ASSERT(rmodel); 171 | 172 | setBackgroundRole(QBrush("#ff0000")); 173 | setBackgroundRole(QBrush("#ff00ff")); 174 | setBackgroundRole(QBrush("#ffff00")); 175 | 176 | rmodel->addConnectedRole(QObjectModel::Role::ValueRole); 177 | 178 | d_ptr->q_ptr = this; 179 | d_ptr->m_pScene = scene; 180 | setSourceModel(rmodel); 181 | 182 | d_ptr->m_EdgeModel.setSourceModel(rmodel->connectionsModel()); 183 | 184 | connect(this, &QAbstractItemModel::rowsInserted, 185 | d_ptr, &QNodeEditorSocketModelPrivate::slotRowsInserted); 186 | 187 | connect(this, &QAbstractItemModel::rowsAboutToBeRemoved, 188 | d_ptr, &QNodeEditorSocketModelPrivate::slotAboutRemoveItem); 189 | 190 | connect(&d_ptr->m_EdgeModel, &QAbstractItemModel::rowsInserted, 191 | d_ptr, &QNodeEditorSocketModelPrivate::slotConnectionsInserted); 192 | 193 | connect(&d_ptr->m_EdgeModel, &QAbstractItemModel::dataChanged, 194 | d_ptr, &QNodeEditorSocketModelPrivate::slotConnectionsChanged); 195 | 196 | 197 | rmodel->setCurrentProxy(this); 198 | } 199 | 200 | QNodeEditorSocketModel::~QNodeEditorSocketModel() 201 | { 202 | while (!d_ptr->m_lEdges.isEmpty()) 203 | delete d_ptr->m_lEdges.takeLast(); 204 | 205 | while (!d_ptr->m_lWrappers.isEmpty()) 206 | delete d_ptr->m_lWrappers.takeLast(); 207 | 208 | delete d_ptr; 209 | } 210 | 211 | void QNodeEditorSocketModel::setSourceModel(QAbstractItemModel *sm) 212 | { 213 | // This models can only work with a QReactiveProxyModel (no proxies) 214 | Q_ASSERT(qobject_cast(sm)); 215 | 216 | QTypeColoriserProxy::setSourceModel(sm); 217 | 218 | //TODO clear (this can wait, it wont happen anyway) 219 | 220 | d_ptr->slotRowsInserted({}, 0, sourceModel()->rowCount() -1 ); 221 | } 222 | 223 | bool QNodeEditorSocketModel::setData(const QModelIndex &idx, const QVariant &value, int role) 224 | { 225 | if (!idx.isValid()) 226 | return false; 227 | 228 | if (role == Qt::SizeHintRole && value.canConvert() && !idx.parent().isValid()) { 229 | auto n = d_ptr->getNode(idx); 230 | Q_ASSERT(n); 231 | n->m_SceneRect = value.toRectF(); 232 | 233 | Q_EMIT dataChanged(idx, idx); 234 | 235 | // All socket position also changed 236 | const int cc = rowCount(idx); 237 | 238 | if (cc) 239 | Q_EMIT dataChanged(index(0,0,idx), index(cc -1, 0, idx)); 240 | 241 | return true; 242 | } 243 | 244 | return QTypeColoriserProxy::setData(idx, value, role); 245 | } 246 | 247 | QMimeData *QNodeEditorSocketModel::mimeData(const QModelIndexList &idxs) const 248 | { 249 | auto md = QTypeColoriserProxy::mimeData(idxs); 250 | 251 | // Assume the QMimeData exist only while the data is being dragged 252 | if (md) { 253 | const QModelDataListDecoder decoder(md); 254 | 255 | auto typeId = decoder.typeId(Qt::EditRole); 256 | 257 | if (typeId != QMetaType::UnknownType) { 258 | d_ptr->m_State = QNodeEditorSocketModelPrivate::State::DRAGGING; 259 | d_ptr->m_CurrentTypeId = typeId; 260 | 261 | connect(md, &QObject::destroyed, d_ptr, 262 | &QNodeEditorSocketModelPrivate::exitDraggingMode); 263 | 264 | for (auto n : qAsConst(d_ptr->m_lWrappers)) 265 | n->m_Node.update(); 266 | } 267 | } 268 | 269 | return md; 270 | } 271 | 272 | Qt::ItemFlags QNodeEditorSocketModel::flags(const QModelIndex &idx) const 273 | { 274 | Qt::ItemFlags f = QTypeColoriserProxy::flags(idx); 275 | 276 | // Disable everything but compatible sockets 277 | return f ^ (( 278 | d_ptr->m_State == QNodeEditorSocketModelPrivate::State::DRAGGING && 279 | (!idx.data(Qt::EditRole).canConvert(d_ptr->m_CurrentTypeId)) && 280 | f | Qt::ItemIsEnabled 281 | ) ? Qt::ItemIsEnabled : Qt::NoItemFlags);; 282 | } 283 | 284 | void QNodeEditorSocketModelPrivate::exitDraggingMode() 285 | { 286 | m_CurrentTypeId = QMetaType::UnknownType; 287 | m_State = QNodeEditorSocketModelPrivate::State::NORMAL; 288 | 289 | for (auto n : qAsConst(m_lWrappers)) 290 | n->m_Node.update(); 291 | } 292 | 293 | GraphicsNodeScene* QNodeEditorSocketModel::scene() const 294 | { 295 | return d_ptr->m_pScene; 296 | } 297 | 298 | GraphicsNodeScene* QNodeEditorEdgeModel::scene() const 299 | { 300 | return d_ptr->m_pScene; 301 | } 302 | 303 | int QNodeEditorSocketModel::sourceSocketCount(const QModelIndex& idx) const 304 | { 305 | const auto nodew = d_ptr->getNode(idx); 306 | 307 | return nodew ? nodew->m_lSources.size() : 0; 308 | } 309 | 310 | int QNodeEditorSocketModel::sinkSocketCount(const QModelIndex& idx) const 311 | { 312 | const auto nodew = d_ptr->getNode(idx); 313 | 314 | return nodew ? nodew->m_lSinks.size() : 0; 315 | } 316 | 317 | GraphicsNode* QNodeEditorSocketModel::getNode(const QModelIndex& idx, bool recursive) 318 | { 319 | if ((!idx.isValid()) || idx.model() != this) 320 | return Q_NULLPTR; 321 | 322 | const auto i = (recursive && idx.parent().isValid()) ? idx.parent() : idx; 323 | 324 | auto nodew = d_ptr->getNode(i); 325 | 326 | return nodew ? &nodew->m_Node : Q_NULLPTR; 327 | } 328 | 329 | template< 330 | QVector NodeWrapper::* SM, 331 | QVector NodeWrapper::* S, 332 | SocketWrapper* EdgeWrapper::* E 333 | > 334 | SocketWrapper* QNodeEditorSocketModelPrivate::getSocketCommon(const QModelIndex& idx) const 335 | { 336 | // Use some dark template metamagic. This could have been done otherwise 337 | 338 | if (!idx.parent().isValid()) 339 | return Q_NULLPTR; 340 | 341 | if (idx.model() == q_ptr->edgeModel()) { 342 | const EdgeWrapper* e = m_lEdges[idx.row()]; 343 | return (*e).*E; 344 | } 345 | 346 | const NodeWrapper* nodew = getNode(idx, true); 347 | 348 | if (!nodew) 349 | return Q_NULLPTR; 350 | 351 | // The -1 is because int defaults to 0 and invalid is -1 352 | const int relIdx = ((*nodew).*SM).size() > idx.row() ? 353 | ((*nodew).*SM)[idx.row()] - 1 : -1; 354 | 355 | auto ret = relIdx != -1 ? ((*nodew).*S)[relIdx] : Q_NULLPTR; 356 | 357 | // Q_ASSERT((!ret) || ret->m_Socket.index() == idx); 358 | 359 | return ret; 360 | } 361 | 362 | SocketWrapper* QNodeEditorSocketModelPrivate::getSourceSocket(const QModelIndex& idx) const 363 | { 364 | return getSocketCommon< 365 | &NodeWrapper::m_lSourcesFromSrc, &NodeWrapper::m_lSources, &EdgeWrapper::m_pSource 366 | >(idx); 367 | } 368 | 369 | SocketWrapper* QNodeEditorSocketModelPrivate::getSinkSocket(const QModelIndex& idx) const 370 | { 371 | return getSocketCommon< 372 | &NodeWrapper::m_lSinksFromSrc, &NodeWrapper::m_lSinks, &EdgeWrapper::m_pSink 373 | >(idx); 374 | } 375 | 376 | GraphicsNodeSocket* QNodeEditorSocketModel::getSourceSocket(const QModelIndex& idx) 377 | { 378 | return &d_ptr->getSocketCommon< 379 | &NodeWrapper::m_lSourcesFromSrc, &NodeWrapper::m_lSources, &EdgeWrapper::m_pSource 380 | >(idx)->m_Socket; 381 | } 382 | 383 | GraphicsNodeSocket* QNodeEditorSocketModel::getSinkSocket(const QModelIndex& idx) 384 | { 385 | return &d_ptr->getSocketCommon< 386 | &NodeWrapper::m_lSinksFromSrc, &NodeWrapper::m_lSinks, &EdgeWrapper::m_pSink 387 | >(idx)->m_Socket; 388 | } 389 | 390 | GraphicsDirectedEdge* QNodeEditorSocketModel::getSourceEdge(const QModelIndex& idx) 391 | { 392 | return idx.model() == edgeModel() ? 393 | &d_ptr->m_lEdges[idx.row()]->m_Edge : Q_NULLPTR; 394 | 395 | //FIXME support SocketModel index 396 | } 397 | 398 | GraphicsDirectedEdge* QNodeEditorSocketModel::getSinkEdge(const QModelIndex& idx) 399 | { 400 | return idx.model() == edgeModel() ? 401 | &d_ptr->m_lEdges[idx.row()]->m_Edge : Q_NULLPTR; 402 | 403 | //FIXME support SocketModel index 404 | } 405 | 406 | QNodeEditorEdgeModel* QNodeEditorSocketModel::edgeModel() const 407 | { 408 | return &d_ptr->m_EdgeModel; 409 | } 410 | 411 | GraphicsDirectedEdge* QNodeEditorSocketModelPrivate::initiateConnectionFromSource( const QModelIndex& idx, GraphicsNodeSocket::SocketType type, const QPointF& point ) 412 | { 413 | Q_UNUSED(type ); //TODO use it or delete it 414 | Q_UNUSED(point); //TODO use it or delete it 415 | 416 | if (!idx.parent().isValid()) { 417 | qWarning() << "Cannot initiate an edge from an invalid node index"; 418 | return Q_NULLPTR; 419 | } 420 | 421 | const int last = q_ptr->edgeModel()->rowCount() - 1; 422 | 423 | if (m_lEdges.size() <= last || !m_lEdges[last]) { 424 | m_lEdges.resize(std::max(m_lEdges.size(), last+1)); 425 | m_lEdges[last] = new EdgeWrapper( 426 | q_ptr->edgeModel(), 427 | q_ptr->edgeModel()->index(last, 1) 428 | ); 429 | m_pScene->addItem(m_lEdges[last]->m_Edge.graphicsItem()); 430 | } 431 | 432 | return &m_lEdges[last]->m_Edge; 433 | } 434 | 435 | GraphicsDirectedEdge* QNodeEditorSocketModel::initiateConnectionFromSource(const QModelIndex& index, const QPointF& point) 436 | { 437 | return d_ptr->initiateConnectionFromSource( 438 | index, GraphicsNodeSocket::SocketType::SOURCE, point 439 | ); 440 | } 441 | 442 | GraphicsDirectedEdge* QNodeEditorSocketModel::initiateConnectionFromSink(const QModelIndex& index, const QPointF& point) 443 | { 444 | return d_ptr->initiateConnectionFromSource( 445 | index, GraphicsNodeSocket::SocketType::SINK, point 446 | ); 447 | } 448 | 449 | QAbstractItemModel *QNodeEditorSocketModel::sinkSocketModel(const QModelIndex& node) const 450 | { 451 | auto n = d_ptr->getNode(node); 452 | 453 | if (!n) 454 | return Q_NULLPTR; 455 | 456 | if (!n->m_pSinkProxy) 457 | n->m_pSinkProxy = new QNodeEdgeFilterProxy( 458 | d_ptr, 459 | n, 460 | GraphicsNodeSocket::SocketType::SINK 461 | ); 462 | 463 | return n->m_pSinkProxy; 464 | } 465 | 466 | QAbstractItemModel *QNodeEditorSocketModel::sourceSocketModel(const QModelIndex& node) const 467 | { 468 | auto n = d_ptr->getNode(node); 469 | 470 | if (!n) 471 | return Q_NULLPTR; 472 | 473 | if (!n->m_pSourceProxy) 474 | n->m_pSourceProxy = new QNodeEdgeFilterProxy( 475 | d_ptr, 476 | n, 477 | GraphicsNodeSocket::SocketType::SOURCE 478 | ); 479 | 480 | return n->m_pSourceProxy; 481 | } 482 | 483 | void QNodeEditorSocketModelPrivate::slotRowsInserted(const QModelIndex& parent, int first, int last) 484 | { 485 | if (last < first) return; 486 | 487 | if (!parent.isValid()) { 488 | // create new nodes 489 | for (int i = first; i <= last; i++) { 490 | const auto idx = q_ptr->index(i, 0); 491 | if (idx.isValid()) { 492 | insertNode(idx.row()); 493 | slotRowsInserted(idx, 0, q_ptr->rowCount(idx)); 494 | } 495 | } 496 | } 497 | else if (!parent.parent().isValid()) 498 | insertSockets(parent, first, last); 499 | } 500 | 501 | GraphicsNode* QNodeEditorSocketModelPrivate::insertNode(int idx) 502 | { 503 | const auto idx2 = q_ptr->index(idx, 0); 504 | 505 | Q_ASSERT(idx2.isValid()); 506 | 507 | if (idx == 0 && m_lWrappers.size()) 508 | Q_ASSERT(false); 509 | 510 | auto nw = new NodeWrapper(q_ptr, idx2); 511 | 512 | m_lWrappers.insert(idx, nw); 513 | 514 | m_pScene->addItem(nw->m_Node.graphicsItem()); 515 | 516 | return &nw->m_Node; 517 | } 518 | 519 | NodeWrapper* QNodeEditorSocketModelPrivate::getNode(const QModelIndex& idx, bool r) const 520 | { 521 | // for convenience 522 | auto i = idx.model() == q_ptr->sourceModel() ? q_ptr->mapFromSource(idx) : idx; 523 | 524 | if ((!i.isValid()) || i.model() != q_ptr) 525 | return Q_NULLPTR; 526 | 527 | if (i.parent().isValid() && r) 528 | i = i.parent(); 529 | 530 | if (i.parent().isValid()) 531 | return Q_NULLPTR; 532 | 533 | Q_ASSERT(i == q_ptr->index(i.row(), i.column())); 534 | Q_ASSERT(i.row() < q_ptr->rowCount()); 535 | 536 | // This should have been taken care of already. If it isn't, either the 537 | // source model is buggy (it will cause crashes later anyway) or this 538 | // code is (and in that case, the state is already corrupted, ignoring that 539 | // will cause garbage data to be shown/serialized). 540 | Q_ASSERT(m_lWrappers.size() > i.row()); 541 | 542 | return m_lWrappers[i.row()]; 543 | } 544 | 545 | void QNodeEditorSocketModelPrivate::insertSockets(const QModelIndex& parent, int first, int last) 546 | { 547 | Q_UNUSED(first) 548 | Q_UNUSED(last) 549 | 550 | auto nodew = getNode(parent); 551 | Q_ASSERT(nodew); 552 | 553 | Q_ASSERT(parent.isValid() && (!parent.parent().isValid())); 554 | 555 | Q_ASSERT(parent.model() == q_ptr); 556 | 557 | Q_ASSERT(nodew->m_lSourcesFromSrc.size() >= first); 558 | Q_ASSERT(nodew->m_lSinksFromSrc.size() >= first); 559 | 560 | for (int i = first; i <= last; i++) { 561 | const auto idx = q_ptr->index(i, 0, parent); 562 | 563 | // It doesn't attempt to insert the socket at the correct index as 564 | // many items will be rejected 565 | 566 | constexpr static const Qt::ItemFlags sourceFlags( 567 | Qt::ItemIsDragEnabled | 568 | Qt::ItemIsSelectable 569 | ); 570 | 571 | // SOURCES 572 | if ((idx.flags() & sourceFlags) == sourceFlags) { 573 | auto s = new SocketWrapper( 574 | idx, 575 | GraphicsNodeSocket::SocketType::SOURCE, 576 | nodew 577 | ); 578 | nodew->m_lSourcesFromSrc.insert(i, nodew->m_lSources.size() + 1); 579 | nodew->m_lSources << s; 580 | nodew->m_lSourcesToSrc << i; 581 | } 582 | else 583 | nodew->m_lSourcesFromSrc.insert(i, 0); 584 | 585 | constexpr static const Qt::ItemFlags sinkFlags( 586 | Qt::ItemIsDropEnabled | 587 | Qt::ItemIsSelectable | 588 | Qt::ItemIsEditable 589 | ); 590 | 591 | // SINKS 592 | if ((idx.flags() & sinkFlags) == sinkFlags) { 593 | auto s = new SocketWrapper( 594 | idx, 595 | GraphicsNodeSocket::SocketType::SINK, 596 | nodew 597 | ); 598 | nodew->m_lSinksFromSrc.insert(i, nodew->m_lSinks.size() + 1); 599 | nodew->m_lSinks << s; 600 | nodew->m_lSinksToSrc << i; 601 | } 602 | else 603 | nodew->m_lSinksFromSrc.insert(i, 0); 604 | 605 | nodew->m_Node.update(); 606 | } 607 | 608 | Q_ASSERT(nodew->m_lSinksFromSrc.size() == nodew->m_lSourcesFromSrc.size()); 609 | } 610 | 611 | void QNodeEditorSocketModelPrivate::updateSockets(const QModelIndex& parent, int first, int last) 612 | { 613 | Q_UNUSED(parent) 614 | Q_UNUSED(first) 615 | Q_UNUSED(last) 616 | //TODO 617 | } 618 | 619 | QNodeEditorEdgeModel::QNodeEditorEdgeModel(QNodeEditorSocketModelPrivate* parent) 620 | : QIdentityProxyModel(parent), d_ptr(parent) 621 | { 622 | 623 | } 624 | 625 | QNodeEditorEdgeModel::~QNodeEditorEdgeModel() 626 | { /* Nothing is owned by the edge model*/ } 627 | 628 | bool QNodeEditorEdgeModel::canConnect(const QModelIndex& idx1, const QModelIndex& idx2) const 629 | { 630 | Q_UNUSED(idx1) 631 | Q_UNUSED(idx2) 632 | 633 | return true; //TODO 634 | } 635 | 636 | bool QNodeEditorEdgeModel::connectSocket(const QModelIndex& idx1, const QModelIndex& idx2) 637 | { 638 | if (idx1.model() != d_ptr->q_ptr || idx2.model() != d_ptr->q_ptr) 639 | return false; 640 | 641 | auto m = qobject_cast(d_ptr->q_ptr->sourceModel()); 642 | 643 | m->connectIndices( 644 | d_ptr->q_ptr->mapToSource(idx1), 645 | d_ptr->q_ptr->mapToSource(idx2) 646 | ); 647 | 648 | return true; 649 | } 650 | 651 | // bool QNodeEditorEdgeModel::setData(const QModelIndex &index, const QVariant &value, int role) 652 | // { 653 | // if (!index.isValid()) 654 | // return false; 655 | // 656 | // switch (role) { 657 | // case Qt::SizeHintRole: 658 | // break; 659 | // } 660 | // 661 | // return QIdentityProxyModel::setData(index, value, role); 662 | // } 663 | 664 | QVariant QNodeEditorEdgeModel::data(const QModelIndex& idx, int role) const 665 | { 666 | if (!idx.isValid()) 667 | return {}; 668 | 669 | Q_ASSERT(idx.model() == this); 670 | 671 | QModelIndex srcIdx; 672 | 673 | switch(idx.column()) { 674 | case QReactiveProxyModel::ConnectionsColumns::SOURCE: 675 | srcIdx = mapToSource(idx).data( 676 | QReactiveProxyModel::ConnectionsRoles::SOURCE_INDEX 677 | ).toModelIndex(); 678 | break; 679 | case QReactiveProxyModel::ConnectionsColumns::DESTINATION: 680 | srcIdx = mapToSource(idx).data( 681 | QReactiveProxyModel::ConnectionsRoles::DESTINATION_INDEX 682 | ).toModelIndex(); 683 | break; 684 | case QReactiveProxyModel::ConnectionsColumns::CONNECTION: 685 | 686 | srcIdx = mapToSource(index(idx.row(),2)).data( 687 | QReactiveProxyModel::ConnectionsRoles::DESTINATION_INDEX 688 | ).toModelIndex(); 689 | 690 | // Use the socket background as line color 691 | if (role != Qt::ForegroundRole) 692 | break; 693 | 694 | if (!srcIdx.isValid()) { 695 | srcIdx = mapToSource(index(idx.row(),0)).data( 696 | QReactiveProxyModel::ConnectionsRoles::SOURCE_INDEX 697 | ).toModelIndex(); 698 | } 699 | 700 | return d_ptr->q_ptr->mapFromSource(srcIdx).data(Qt::BackgroundRole); 701 | } 702 | 703 | if (srcIdx.model() == d_ptr->q_ptr->sourceModel()) 704 | srcIdx = d_ptr->q_ptr->mapFromSource(srcIdx); 705 | 706 | // It will crash soon if it is false, better do it now 707 | Q_ASSERT( 708 | (!srcIdx.isValid()) || 709 | srcIdx.model() == d_ptr->q_ptr || 710 | srcIdx.model() == d_ptr->q_ptr->edgeModel() 711 | ); 712 | 713 | if (srcIdx.model() == d_ptr->q_ptr && srcIdx.parent().isValid()) //TODO remove 714 | Q_ASSERT(srcIdx.parent().data() == d_ptr->q_ptr->index(srcIdx.parent().row(),0).data()); 715 | 716 | auto sock = (!idx.column()) ? 717 | d_ptr->getSourceSocket(srcIdx) : d_ptr->getSinkSocket(srcIdx); 718 | 719 | if (sock && role == Qt::SizeHintRole) 720 | return sock->m_Socket.graphicsItem()->mapToScene(0,0); 721 | 722 | return QIdentityProxyModel::data(idx, role); 723 | } 724 | 725 | void QNodeEditorSocketModelPrivate::slotConnectionsInserted(const QModelIndex& parent, int first, int last) 726 | { 727 | //FIXME dead code 728 | typedef QReactiveProxyModel::ConnectionsColumns Column; 729 | 730 | auto srcSockI = q_ptr->index(first, Column::SOURCE).data( 731 | QReactiveProxyModel::ConnectionsRoles::SOURCE_INDEX 732 | ); 733 | auto destSockI = q_ptr->index(first, Column::DESTINATION).data( 734 | QReactiveProxyModel::ConnectionsRoles::DESTINATION_INDEX 735 | ); 736 | 737 | // If this happens, then the model is buggy or there is a race condition. 738 | if ((!srcSockI.isValid()) && (!destSockI.isValid())) 739 | return; 740 | 741 | //TODO support rows removed 742 | 743 | // Avoid pointless indentation 744 | if (last > first) 745 | slotConnectionsInserted(parent, ++first, last); 746 | } 747 | 748 | void QNodeEditorSocketModelPrivate::slotConnectionsChanged(const QModelIndex& tl, const QModelIndex& br) 749 | { 750 | typedef QReactiveProxyModel::ConnectionsRoles CRole; 751 | 752 | for (int i = tl.row(); i <= br.row(); i++) { 753 | const auto src = m_EdgeModel.index(i, 0).data(CRole::SOURCE_INDEX ).toModelIndex(); 754 | const auto sink = m_EdgeModel.index(i, 2).data(CRole::DESTINATION_INDEX).toModelIndex(); 755 | 756 | // Make sure the edge exists 757 | if (m_lEdges.size()-1 < i || !m_lEdges[i]) { 758 | m_lEdges.resize(std::max(m_lEdges.size(), i+1)); 759 | m_lEdges[i] = new EdgeWrapper(&m_EdgeModel, m_EdgeModel.index(i, 1)); 760 | } 761 | 762 | auto e = m_lEdges[i]; 763 | 764 | auto oldSrc(e->m_pSource), oldSink(e->m_pSink); 765 | 766 | // Update the node mapping 767 | if ((e->m_pSource = getSourceSocket(src))) 768 | e->m_pSource->m_Socket.setEdge(m_EdgeModel.index(i, 0)); 769 | 770 | if (oldSrc != e->m_pSource) { 771 | if (oldSrc) 772 | oldSrc->m_Socket.setEdge({}); 773 | 774 | if (oldSrc && oldSrc->m_EdgeWrapper && oldSrc->m_EdgeWrapper == e) { 775 | oldSrc->m_EdgeWrapper = Q_NULLPTR; 776 | } 777 | 778 | if (e->m_pSource) { 779 | e->m_pSource->m_EdgeWrapper = e; 780 | } 781 | } 782 | 783 | if ((e->m_pSink = getSinkSocket(sink))) 784 | e->m_pSink->m_Socket.setEdge(m_EdgeModel.index(i, 2)); 785 | 786 | if (oldSink != e->m_pSink) { 787 | if (oldSink) 788 | oldSink->m_Socket.setEdge({}); 789 | 790 | if (oldSink && oldSink->m_EdgeWrapper && oldSink->m_EdgeWrapper == e) { 791 | oldSink->m_EdgeWrapper = Q_NULLPTR; 792 | } 793 | 794 | if (e->m_pSink) 795 | e->m_pSink->m_EdgeWrapper = e; 796 | } 797 | 798 | // Update the graphic item 799 | const bool isUsed = e->m_pSource || e->m_pSink; 800 | 801 | e->m_Edge.update(); 802 | 803 | if (e->m_IsShown != isUsed && isUsed) 804 | m_pScene->addItem(e->m_Edge.graphicsItem()); 805 | else if (e->m_IsShown != isUsed && !isUsed) 806 | m_pScene->removeItem(e->m_Edge.graphicsItem()); 807 | 808 | e->m_IsShown = isUsed; 809 | } 810 | } 811 | 812 | void QNodeEditorSocketModelPrivate::slotAboutRemoveItem(const QModelIndex &parent, int first, int last) 813 | { 814 | if (first < 0 || last < first) 815 | return; 816 | 817 | if (!parent.isValid() && m_lWrappers.size() > last) { 818 | 819 | for (int i = first; i <= last; i++) { 820 | const auto idx = q_ptr->index(i, 0, parent); 821 | 822 | // remove the sockets 823 | slotAboutRemoveItem(idx, 0, q_ptr->rowCount(idx) - 1); 824 | 825 | auto nw = m_lWrappers[i]; 826 | nw->m_Node.setCentralWidget(Q_NULLPTR); 827 | m_pScene->removeItem(nw->m_Node.graphicsItem()); 828 | 829 | m_lWrappers.remove(i - (i-first)); 830 | 831 | delete nw; 832 | } 833 | } 834 | else if (parent.isValid() && !parent.parent().isValid()) { 835 | auto nw = m_lWrappers[parent.row()]; 836 | Q_ASSERT(nw); 837 | 838 | QList srcToDel, sinkToDel; 839 | 840 | for (int i = first; i <= last; i++) { 841 | Q_ASSERT(parent == nw->m_Node.index()); 842 | 843 | Q_ASSERT(nw->m_lSinksFromSrc.size() == nw->m_lSourcesFromSrc.size()); 844 | Q_ASSERT(nw->m_lSourcesFromSrc.size() > q_ptr->rowCount(parent)); 845 | 846 | int sid = nw->m_lSourcesFromSrc[i] - 1; 847 | if (sid >= 0) { 848 | auto sw = nw->m_lSources[sid]; 849 | m_pScene->removeItem(sw->m_Socket.graphicsItem()); 850 | srcToDel << sid; 851 | delete sw; 852 | } 853 | 854 | sid = nw->m_lSinksFromSrc[i] - 1; 855 | 856 | if (sid >= 0) { 857 | auto sw = nw->m_lSinks[sid]; 858 | m_pScene->removeItem(sw->m_Socket.graphicsItem()); 859 | sinkToDel << sid; 860 | delete sw; 861 | } 862 | } 863 | 864 | nw->m_lSinksFromSrc.remove(first, last - first + 1); 865 | nw->m_lSourcesFromSrc.remove(first, last - first + 1); 866 | 867 | // remove from the list, this assume the sockets are ordered 868 | for (int i = 0; i < srcToDel.size(); i++) { 869 | nw->m_lSources.remove(srcToDel[i] - i); 870 | nw->m_lSourcesToSrc.remove(srcToDel[i] - i); 871 | } 872 | 873 | for (int i = 0; i < sinkToDel.size(); i++) { 874 | nw->m_lSinks.remove(sinkToDel[i] - i); 875 | nw->m_lSinksToSrc.remove(sinkToDel[i] - i); 876 | } 877 | 878 | // reload the "FromSrc" mapping 879 | int rc(q_ptr->rowCount(parent) - (last - first) - 1), csrc(0), csink(0); 880 | for (int i = 0; i < rc; i++) { 881 | const auto idx2 = q_ptr->index(i, 0, parent); 882 | 883 | nw->m_lSourcesFromSrc[i] = (csrc < nw->m_lSources.size() 884 | && nw->m_lSources[csrc]->m_Socket.index() == idx2 885 | && ++csrc) ? csrc : -1; 886 | 887 | nw->m_lSinksFromSrc[i] = (csink < nw->m_lSinks.size() 888 | && nw->m_lSinks[csink]->m_Socket.index() == idx2 889 | && ++csink) ? csink : -1; 890 | } 891 | } 892 | } 893 | 894 | QNodeEditorSocketModel* QNodeEditorEdgeModel::socketModel() const 895 | { 896 | return d_ptr->q_ptr; 897 | } 898 | 899 | QNodeEdgeFilterProxy::QNodeEdgeFilterProxy(QNodeEditorSocketModelPrivate* d, NodeWrapper *w, GraphicsNodeSocket::SocketType t) : 900 | QAbstractProxyModel(d), m_Type(t), m_pWrapper(w), d_ptr(d) 901 | { 902 | setSourceModel(d->q_ptr); 903 | } 904 | 905 | int QNodeEdgeFilterProxy::rowCount(const QModelIndex& parent) const 906 | { 907 | if (parent.isValid()) 908 | return 0; 909 | 910 | switch (m_Type) { 911 | case GraphicsNodeSocket::SocketType::SOURCE: 912 | return m_pWrapper->m_lSources.size(); 913 | case GraphicsNodeSocket::SocketType::SINK: 914 | return m_pWrapper->m_lSinks.size(); 915 | } 916 | 917 | return 0; 918 | } 919 | 920 | QModelIndex QNodeEdgeFilterProxy::mapFromSource(const QModelIndex& srcIdx) const 921 | { 922 | static const auto check = []( 923 | const QVector& l, const QModelIndex& src, const GraphicsNode* n 924 | ) -> bool { 925 | return src.isValid() 926 | && l.size() > src.row() 927 | && n->index() == src.parent() 928 | && l[src.row()]; 929 | }; 930 | 931 | switch (m_Type) { 932 | case GraphicsNodeSocket::SocketType::SOURCE: 933 | if (check(m_pWrapper->m_lSourcesFromSrc, srcIdx, &m_pWrapper->m_Node)) 934 | return createIndex(m_pWrapper->m_lSourcesFromSrc[srcIdx.row()] - 1, 0, Q_NULLPTR); 935 | break; 936 | case GraphicsNodeSocket::SocketType::SINK: 937 | if (check(m_pWrapper->m_lSinksFromSrc, srcIdx, &m_pWrapper->m_Node)) 938 | return createIndex(m_pWrapper->m_lSinksFromSrc[srcIdx.row()] - 1, 0, Q_NULLPTR); 939 | break; 940 | } 941 | 942 | return {}; 943 | } 944 | 945 | QModelIndex QNodeEdgeFilterProxy::mapToSource(const QModelIndex& proxyIndex) const 946 | { 947 | if ((!proxyIndex.isValid()) || proxyIndex.model() != this) 948 | return {}; 949 | 950 | if (proxyIndex.column()) 951 | return {}; 952 | 953 | const auto srcP = m_pWrapper->m_Node.index(); 954 | 955 | switch (m_Type) { 956 | case GraphicsNodeSocket::SocketType::SOURCE: 957 | return d_ptr->q_ptr->index(m_pWrapper->m_lSourcesToSrc[proxyIndex.row()], 0, srcP); 958 | case GraphicsNodeSocket::SocketType::SINK: 959 | return d_ptr->q_ptr->index(m_pWrapper->m_lSinksToSrc[proxyIndex.row()], 0, srcP); 960 | } 961 | 962 | return {}; 963 | } 964 | 965 | int QNodeEdgeFilterProxy::columnCount(const QModelIndex& parent) const 966 | { 967 | return parent.isValid() ? 0 : 4; 968 | } 969 | 970 | QModelIndex QNodeEdgeFilterProxy::index(int row, int column, const QModelIndex& parent) const 971 | { 972 | if (parent.isValid() || row < 0 || column < 0 || column > 3) 973 | return {}; 974 | 975 | switch(m_Type) { 976 | case GraphicsNodeSocket::SocketType::SOURCE: 977 | if (row < m_pWrapper->m_lSources.size()) 978 | return createIndex(row, column, nullptr); 979 | break; 980 | case GraphicsNodeSocket::SocketType::SINK: 981 | if (row < m_pWrapper->m_lSinks.size()) 982 | return createIndex(row, column, nullptr); 983 | break; 984 | } 985 | 986 | return {}; 987 | } 988 | 989 | QVariant QNodeEdgeFilterProxy::data(const QModelIndex& idx, int role) const 990 | { 991 | if ((!idx.isValid()) || idx.model() != this) 992 | return {}; 993 | 994 | if (!idx.column()) 995 | return QAbstractProxyModel::data(idx, role); 996 | 997 | SocketWrapper* i = Q_NULLPTR; 998 | 999 | switch (m_Type) { 1000 | case GraphicsNodeSocket::SocketType::SOURCE: 1001 | Q_ASSERT(idx.row() < m_pWrapper->m_lSources.size()); 1002 | i = m_pWrapper->m_lSources[idx.row()]; 1003 | break; 1004 | case GraphicsNodeSocket::SocketType::SINK: 1005 | Q_ASSERT(idx.row() < m_pWrapper->m_lSinks.size()); 1006 | i = m_pWrapper->m_lSinks[idx.row()]; 1007 | break; 1008 | } 1009 | 1010 | // There is nothing to display if the socket isn't connected 1011 | if ((!i) || (!i->m_EdgeWrapper)) 1012 | return {}; 1013 | 1014 | const auto edgeIdx = i->m_EdgeWrapper->m_Edge.index(); 1015 | 1016 | if (idx.column() == 1) { 1017 | if (role == Qt::DisplayRole) 1018 | return i->m_EdgeWrapper->m_EdgeId; 1019 | else 1020 | return edgeIdx.data(role); 1021 | } 1022 | 1023 | // This leaves columns 2 and 3 to handle 1024 | 1025 | if (i->m_EdgeWrapper->m_pSink && i->m_EdgeWrapper->m_pSource) { 1026 | switch (m_Type) { 1027 | case GraphicsNodeSocket::SocketType::SOURCE: 1028 | if (idx.column() == 2) 1029 | return edgeIdx.model()->index(edgeIdx.row(), 2).data(role); 1030 | else { 1031 | return i->m_EdgeWrapper->m_pSink->m_pNode->m_Node.index().data(role); 1032 | } 1033 | case GraphicsNodeSocket::SocketType::SINK: 1034 | if (idx.column() == 2) 1035 | return edgeIdx.model()->index(edgeIdx.row(), 0).data(role); 1036 | else { 1037 | return i->m_EdgeWrapper->m_pSource->m_pNode->m_Node.index().data(role); 1038 | } 1039 | } 1040 | } 1041 | 1042 | return {}; 1043 | } 1044 | 1045 | QModelIndex QNodeEdgeFilterProxy::parent(const QModelIndex& idx) const 1046 | { 1047 | Q_UNUSED(idx); 1048 | return {}; 1049 | } 1050 | 1051 | QVariant QNodeEdgeFilterProxy::headerData(int section, Qt::Orientation ori, int role) const 1052 | { 1053 | // The default one is good enough 1054 | if (ori == Qt::Vertical || role != Qt::DisplayRole) 1055 | return {}; 1056 | 1057 | static QVariant sock {QStringLiteral("Socket name")}; 1058 | static QVariant edge {QStringLiteral("Edge")}; 1059 | static QVariant other {QStringLiteral("Connected socket")}; 1060 | static QVariant node {QStringLiteral("Connected node")}; 1061 | 1062 | switch(section) { 1063 | case 0: 1064 | return sock; 1065 | case 1: 1066 | return edge; 1067 | case 2: 1068 | return other; 1069 | case 3: 1070 | return node; 1071 | }; 1072 | 1073 | return {}; 1074 | } 1075 | 1076 | Qt::ItemFlags QNodeEdgeFilterProxy::flags(const QModelIndex &idx) const 1077 | { 1078 | if (!idx.isValid()) 1079 | return Qt::NoItemFlags; 1080 | 1081 | if (!idx.column()) 1082 | return QAbstractProxyModel::flags(idx); 1083 | 1084 | return Qt::ItemIsEnabled | Qt::ItemIsSelectable; 1085 | } 1086 | 1087 | #include 1088 | --------------------------------------------------------------------------------