diff --git a/samples/layouts/layouts.qml b/samples/layouts/layouts.qml index c10a5718..98a45b52 100644 --- a/samples/layouts/layouts.qml +++ b/samples/layouts/layouts.qml @@ -43,42 +43,55 @@ ApplicationWindow { navigable : true resizeHandlerColor: "#03a9f4" gridThickColor: Material.theme === Material.Dark ? "#4e4e4e" : "#c1c1c1" + property var treeRoot: undefined graph: Qan.Graph { parent: graphView id: graph Component.onCompleted: { let n1 = graph.insertNode() - n1.label = "n1"; n1.item.x=15; n1.item.y= 25 + id: graphView + graphView.treeRoot = n1 + n1.label = "n1"; n1.item.x = 15; n1.item.y = 25 n1.item.ratio = 0.4 let n11 = graph.insertNode() - n11.label = "n11"; n11.item.x=15; n11.item.y= 125 - let n12 = graph.insertNode() - n12.label = "n12"; n12.item.x=125; n12.item.y= 125 + n11.label = "n11"; n11.item.x = 115; n11.item.y = 100 + let n111 = graph.insertNode() + n111.label = "n111"; n111.item.x = 215; n111.item.y = 170 + let n1111 = graph.insertNode() + n1111.label = "n1111"; n1111.item.x = 315; n1111.item.y = 240 + let n12 = graph.insertNode() + n12.label = "n12"; n12.item.x = 115; n12.item.y= 310 let n121 = graph.insertNode() - n121.label = "n121"; n121.item.x=125; n121.item.y= 225 + n121.label = "n121"; n121.item.x = 215; n121.item.y = 380 let n122 = graph.insertNode() - n122.label = "n122"; n122.item.x=225; n122.item.y= 225 - - let n1211 = graph.insertNode() - n1211.label = "n1211"; n1211.item.x=125; n1211.item.y= 225 + n122.label = "n122"; n122.item.x = 215; n122.item.y = 450 let n13 = graph.insertNode() - n13.label = "n13"; n13.item.x=225; n13.item.y= 125 + n13.label = "n13"; n13.item.x = 115; n13.item.y = 520 + let n131 = graph.insertNode() + n131.label = "n131"; n131.item.x = 225; n131.item.y = 590 + + graph.insertEdge(n1, n11) + graph.insertEdge(n1, n12) + graph.insertEdge(n1, n13) - graph.insertEdge(n1, n11); - graph.insertEdge(n1, n12); - graph.insertEdge(n1, n13); - graph.insertEdge(n12, n121); - graph.insertEdge(n12, n122); - graph.insertEdge(n121, n1211); + graph.insertEdge(n11, n111) + graph.insertEdge(n111, n1111) - orgTreeLayout.layout(n1); + graph.insertEdge(n12, n121) + graph.insertEdge(n12, n122) + + graph.insertEdge(n13, n131) } Qan.OrgTreeLayout { id: orgTreeLayout } + Qan.RandomLayout { + id: randomLayout + layoutRect: Qt.rect(100, 100, 1000, 1000) + } } // Qan.Graph Menu { // Context menu demonstration id: contextMenu @@ -97,6 +110,49 @@ ApplicationWindow { contextMenu.y = pos.y contextMenu.open() } + Pane { + anchors.top: parent.top + anchors.topMargin: 10 + anchors.horizontalCenter: parent.horizontalCenter + width: 470 + height: 50 + padding: 2 + RowLayout { + anchors.fill: parent + Label { + text: "Apply OrgTree:" + } + Button { + text: 'Random' + Material.roundedScale: Material.SmallScale + onClicked: randomLayout.layout(graphView.treeRoot) + } + Button { + text: 'Mixed' + Material.roundedScale: Material.SmallScale + onClicked: { + orgTreeLayout.layoutOrientation = Qan.OrgTreeLayout.Mixed + orgTreeLayout.layout(graphView.treeRoot); + } + } + Button { + text: 'Vertical' + Material.roundedScale: Material.SmallScale + onClicked: { + orgTreeLayout.layoutOrientation = Qan.OrgTreeLayout.Vertical + orgTreeLayout.layout(graphView.treeRoot); + } + } + Button { + text: 'Horizontal' + Material.roundedScale: Material.SmallScale + onClicked: { + orgTreeLayout.layoutOrientation = Qan.OrgTreeLayout.Horizontal + orgTreeLayout.layout(graphView.treeRoot); + } + } + } + } } // Qan.GraphView } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index da55f564..e7cc55de 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -30,7 +30,7 @@ set(qan_source_files qanTableCell.cpp qanTableBorder.cpp qanTableGroupItem.cpp - qanTreeLayout.cpp + qanTreeLayouts.cpp ) set (qan_header_files @@ -65,7 +65,7 @@ set (qan_header_files qanTableCell.h qanTableBorder.h qanTableGroupItem.h - qanTreeLayout.cpp + qanTreeLayouts.h QuickQanava.h gtpo/container_adapter.h gtpo/edge.h diff --git a/src/QuickQanava.h b/src/QuickQanava.h index 25d34813..96ac680f 100755 --- a/src/QuickQanava.h +++ b/src/QuickQanava.h @@ -63,7 +63,7 @@ #include "./qanBottomResizer.h" #include "./qanNavigablePreview.h" #include "./qanAnalysisTimeHeatMap.h" -#include "./qanTreeLayout.h" +#include "./qanTreeLayouts.h" struct QuickQanava { static void initialize(QQmlEngine* engine) { @@ -118,6 +118,7 @@ struct QuickQanava { qmlRegisterType("QuickQanava", 2, 0, "RightResizer"); qmlRegisterType("QuickQanava", 2, 0, "BottomResizer"); + qmlRegisterType("QuickQanava", 2, 0, "RandomLayout"); qmlRegisterType("QuickQanava", 2, 0, "OrgTreeLayout"); } // initialize() }; diff --git a/src/RectGlowEffect.qml b/src/RectGlowEffect.qml index bce4226a..949bbff3 100644 --- a/src/RectGlowEffect.qml +++ b/src/RectGlowEffect.qml @@ -66,12 +66,11 @@ Item { anchors.centerIn: parent width: border.width + (glowRadius * 2) height: border.height + (glowRadius * 2) - blurEnabled: glowEffect.visible && - glowEffect.style !== undefined ? style.effectEnabled : false + blurEnabled: glowEffect.visible && (glowEffect.style?.effectEnabled || false) blurMax: 30 blur: 1. colorization: 1.0 - colorizationColor: glowColor + colorizationColor: style?.effectColor ?? Qt.rgba(0.7, 0.7, 0.7, 0.7) maskEnabled: true && glowEffect.visible maskThresholdMin: 0.29 // Should be just below border.color diff --git a/src/RectGroupTemplate.qml b/src/RectGroupTemplate.qml index ae66d70f..47fb4d51 100644 --- a/src/RectGroupTemplate.qml +++ b/src/RectGroupTemplate.qml @@ -127,6 +127,7 @@ Item { exclusiveSignals: TapHandler.DoubleTap onTapped: labelEditor.visible = true } + MouseArea { anchors.fill: parent; cursorShape: Qt.SizeAllCursor; acceptedButtons: Qt.NoButton } } } // labelEditor Item } // RowLayout: collapser + label diff --git a/src/qanGraph.h b/src/qanGraph.h index 3e97c6f9..9398e376 100644 --- a/src/qanGraph.h +++ b/src/qanGraph.h @@ -1073,7 +1073,7 @@ class Graph : public gtpo::graph */ std::vector collectDfs(const qan::Node& node, bool collectGroup = false) const noexcept; - //! \copydoc collectDfs() + //! Collect all out nodes of \c nodes using DFS, return an unordered set of subnodes (nodes in node are _not_ in returned set). auto collectSubNodes(const QVector nodes, bool collectGroup = false) const noexcept -> std::unordered_set; private: diff --git a/src/qanTreeLayout.cpp b/src/qanTreeLayouts.cpp similarity index 53% rename from src/qanTreeLayout.cpp rename to src/qanTreeLayouts.cpp index f4abb909..2f5ab981 100644 --- a/src/qanTreeLayout.cpp +++ b/src/qanTreeLayouts.cpp @@ -43,7 +43,7 @@ #include // QuickQanava headers -#include "./qanTreeLayout.h" +#include "./qanTreeLayouts.h" namespace qan { // ::qan @@ -142,6 +142,57 @@ void NaiveTreeLayout::layout(qan::Node* root) noexcept //----------------------------------------------------------------------------- +/* OrgTreeLayout Object Management *///---------------------------------------- +RandomLayout::RandomLayout(QObject* parent) noexcept : + QObject{parent} +{ +} +RandomLayout::~RandomLayout() { } + +bool RandomLayout::setLayoutRect(QRectF layoutRect) noexcept +{ + _layoutRect = layoutRect; + emit layoutRectChanged(); + return true; +} +const QRectF RandomLayout::getLayoutRect() const noexcept { return _layoutRect; } + +void RandomLayout::layout(qan::Node& root) noexcept +{ + // In nodes, out nodes, adjacent nodes ? + const auto graph = root.getGraph(); + if (graph == nullptr) + return; + if (root.getItem() == nullptr) + return; + + // Generate a 1000x1000 layout rect centered on root if the user has not specified one + const auto rootPosition = root.getItem()->position(); + const auto layoutRect = _layoutRect.isEmpty() ? QRectF{rootPosition.x() - 500, rootPosition.y() - 500, 1000., 1000.} : + _layoutRect; + + auto outNodes = graph->collectSubNodes(QVector{&root}, false); + outNodes.insert(&root); + for (auto n : outNodes) { + auto node = const_cast(n); + if (node->getItem() == nullptr) + continue; + const auto nodeBr = node->getItem()->boundingRect(); + qreal maxX = layoutRect.width() - nodeBr.width(); // Generate and set random x and y positions + qreal maxY = layoutRect.height() - nodeBr.height(); // within available layoutRect area + node->getItem()->setX(QRandomGenerator::global()->bounded(maxX) + layoutRect.left()); + node->getItem()->setY(QRandomGenerator::global()->bounded(maxY) + layoutRect.top()); + } +} + +void RandomLayout::layout(qan::Node* root) noexcept +{ + if (root != nullptr) + layout(*root); +} +//----------------------------------------------------------------------------- + + /* OrgTreeLayout Object Management *///---------------------------------------- OrgTreeLayout::OrgTreeLayout(QObject* parent) noexcept : QObject{parent} @@ -149,9 +200,22 @@ OrgTreeLayout::OrgTreeLayout(QObject* parent) noexcept : } OrgTreeLayout::~OrgTreeLayout() { } +bool OrgTreeLayout::setLayoutOrientation(OrgTreeLayout::LayoutOrientation layoutOrientation) noexcept { + if (_layoutOrientation != layoutOrientation) { + _layoutOrientation = layoutOrientation; + emit layoutOrientationChanged(); + return true; + } + return false; +} +OrgTreeLayout::LayoutOrientation OrgTreeLayout::getLayoutOrientation() noexcept { return _layoutOrientation; } +const OrgTreeLayout::LayoutOrientation OrgTreeLayout::getLayoutOrientation() const noexcept { return _layoutOrientation; } + + void OrgTreeLayout::layout(qan::Node& root, qreal xSpacing, qreal ySpacing) noexcept { - // FIXME #228: Variant / naive Reingold-Tilford algorithm + // Note: Recursive variant of Reingold-Tilford algorithm with naive shifting (ie shifting + // based on the less space efficient sub tree bounding rect intersection...) // Pre-condition: root must be a tree subgraph, this is not enforced in this algorithm, // any circuit will lead to intinite recursion... @@ -159,28 +223,75 @@ void OrgTreeLayout::layout(qan::Node& root, qreal xSpacing, qreal ySpacing) n // Algorithm: // Traverse graph DFS aligning child nodes vertically // At a given level: `shift` next node according to previous node sub-tree BR - auto layout_rec = [xSpacing, ySpacing](auto&& self, auto& childNodes, QRectF br) -> QRectF { - //qWarning() << "layout_rec(): br=" << br; + auto layoutVert_rec = [xSpacing, ySpacing](auto&& self, auto& childNodes, QRectF br) -> QRectF { const auto x = br.right() + xSpacing; for (auto child: childNodes) { - //qWarning() << "layout_rec(): child.label=" << child->getLabel() << " br=" << br; child->getItem()->setX(x); child->getItem()->setY(br.bottom() + ySpacing); + // Take into account this level maximum width + br = br.united(child->getItem()->boundingRect().translated(child->getItem()->position())); + const auto childBr = self(self, child->get_out_nodes(), br); + br.setBottom(childBr.bottom()); // Note: Do not take full child BR into account to avoid x drifting + } + return br; + }; + + auto layoutHoriz_rec = [xSpacing, ySpacing](auto&& self, auto& childNodes, QRectF br) -> QRectF { + const auto y = br.bottom() + ySpacing; + for (auto child: childNodes) { + child->getItem()->setX(br.right() + xSpacing); + child->getItem()->setY(y); + // Take into account this level maximum width br = br.united(child->getItem()->boundingRect().translated(child->getItem()->position())); - const auto prevBr = self(self, child->get_out_nodes(), br); - br = br.united(prevBr); + const auto childBr = self(self, child->get_out_nodes(), br); + br.setRight(childBr.right()); // Note: Do not take full child BR into account to avoid x drifting + } + return br; + }; + + auto layoutMixed_rec = [xSpacing, ySpacing, layoutHoriz_rec](auto&& self, auto& childNodes, QRectF br) -> QRectF { + auto childsAreLeafs = true; + for (const auto child: childNodes) + if (child->get_out_nodes().size() != 0) { + childsAreLeafs = false; + break; + } + if (childsAreLeafs) + return layoutHoriz_rec(self, childNodes, br); + else { + const auto x = br.right() + xSpacing; + for (auto child: childNodes) { + child->getItem()->setX(x); + child->getItem()->setY(br.bottom() + ySpacing); + // Take into account this level maximum width + br = br.united(child->getItem()->boundingRect().translated(child->getItem()->position())); + const auto childBr = self(self, child->get_out_nodes(), br); + br.setBottom(childBr.bottom()); // Note: Do not take full child BR into account to avoid x drifting + } } return br; }; - //qWarning() << "root.bottomRight=" << root.getItem()->boundingRect().bottomRight(); - // Note: QQuickItem boundingRect is in item local CS, translate to scene CS. - layout_rec(layout_rec, root.get_out_nodes(), - root.getItem()->boundingRect().translated(root.getItem()->position())); + + // Note: QQuickItem boundingRect() is in item local CS, translate to "scene" CS. + switch (getLayoutOrientation()) { + case LayoutOrientation::Undefined: return; + case LayoutOrientation::Vertical: + layoutVert_rec(layoutVert_rec, root.get_out_nodes(), + root.getItem()->boundingRect().translated(root.getItem()->position())); + break; + case LayoutOrientation::Horizontal: + layoutHoriz_rec(layoutHoriz_rec, root.get_out_nodes(), + root.getItem()->boundingRect().translated(root.getItem()->position())); + break; + case LayoutOrientation::Mixed: + layoutMixed_rec(layoutMixed_rec, root.get_out_nodes(), + root.getItem()->boundingRect().translated(root.getItem()->position())); + break; + } } void OrgTreeLayout::layout(qan::Node* root, qreal xSpacing, qreal ySpacing) noexcept { - //qWarning() << "qan::OrgTreeLayout::layout(): root=" << root; if (root != nullptr) layout(*root, xSpacing, ySpacing); } diff --git a/src/qanTreeLayout.h b/src/qanTreeLayouts.h similarity index 57% rename from src/qanTreeLayout.h rename to src/qanTreeLayouts.h index 2d02c4de..2fb33615 100644 --- a/src/qanTreeLayout.h +++ b/src/qanTreeLayouts.h @@ -74,7 +74,57 @@ class NaiveTreeLayout : public QObject }; -/*! \brief +/*! \brief Layout nodes randomly inside a bounding rect. + * \nosubgrouping + */ +class RandomLayout : public QObject +{ + Q_OBJECT + /*! \name RandomLayout Object Management *///----------------------------- + //@{ +public: + explicit RandomLayout(QObject* parent = nullptr) noexcept; + virtual ~RandomLayout() override; + RandomLayout(const RandomLayout&) = delete; + RandomLayout& operator=(const RandomLayout&) = delete; + RandomLayout(RandomLayout&&) = delete; + RandomLayout& operator=(RandomLayout&&) = delete; + +public: + //! \copydoc getLayoutRect() + Q_PROPERTY(QRectF layoutRect READ getLayoutRect WRITE setLayoutRect NOTIFY layoutRectChanged FINAL) + //! \copydoc getLayoutRect() + bool setLayoutRect(QRectF layoutRect) noexcept; + //! \copydoc getLayoutRect() + const QRectF getLayoutRect() const noexcept; +protected: + //! \copydoc getLayoutRect() + QRectF _layoutRect = QRectF{}; +signals: + //! \copydoc getLayoutRect() + void layoutRectChanged(); + +public: + /*! \brief Apply a random layout fitting nodes positions inside \c layoutRect. + * If \c layoutRect is empty, generate a 1000x1000 default rect around \c root position. + */ + void layout(qan::Node& root) noexcept; + + //! QML invokable version of layout(). + Q_INVOKABLE void layout(qan::Node* root) noexcept; + //@} + //------------------------------------------------------------------------- +}; + + +/*! \brief Org chart naive recursive variant of Reingold-Tilford algorithm with shifting. + * + * This algorithm layout tree in an "Org chart" fashion using no space optimization, + * respecting node ordering and working for n-ary trees. + * + * \note This layout does not enforces that the input graph is a tree, laying out + * a non-tree graph might lead to infinite recursion. + * * \nosubgrouping */ class OrgTreeLayout : public QObject @@ -90,6 +140,35 @@ class OrgTreeLayout : public QObject OrgTreeLayout(OrgTreeLayout&&) = delete; OrgTreeLayout& operator=(OrgTreeLayout&&) = delete; +public: + //! Define layout orentation, modify before a call to layout(), default to Vertical. + enum class LayoutOrientation : unsigned int { + //! Undefined. + Undefined = 0, + //! Vertical tree layout. + Vertical = 2, + //! Horizontal tree layout. + Horizontal = 4, + //! Mixed t0ree layout (ie vertical, but horizontal for leaf nodes). + Mixed = 8 + }; + Q_ENUM(LayoutOrientation) + + //! \copydoc LayoutOrientation + Q_PROPERTY(LayoutOrientation layoutOrientation READ getLayoutOrientation WRITE setLayoutOrientation NOTIFY layoutOrientationChanged FINAL) + //! \copydoc LayoutOrientation + bool setLayoutOrientation(LayoutOrientation layoutOrientation) noexcept; + //! \copydoc LayoutOrientation + LayoutOrientation getLayoutOrientation() noexcept; + //! \copydoc LayoutOrientation + const LayoutOrientation getLayoutOrientation() const noexcept; +protected: + //! \copydoc LayoutOrientation + LayoutOrientation _layoutOrientation = LayoutOrientation::Vertical; +signals: + //! \copydoc LayoutOrientation + void layoutOrientationChanged(); + public: /*! \brief Apply a vertical "organisational chart tree layout algorithm" to subgraph \c root. * @@ -102,10 +181,10 @@ class OrgTreeLayout : public QObject * running this algorithm on a non tree subgraph might lead to inifinite recursions or * invalid layouts. */ - void layout(qan::Node& root, qreal xSpacing = 35., qreal ySpacing = 25.) noexcept; + void layout(qan::Node& root, qreal xSpacing = 25., qreal ySpacing = 25.) noexcept; //! QML invokable version of layout(). - Q_INVOKABLE void layout(qan::Node* root, qreal xSpacing = 35., qreal ySpacing = 25.) noexcept; + Q_INVOKABLE void layout(qan::Node* root, qreal xSpacing = 25., qreal ySpacing = 25.) noexcept; //@} //------------------------------------------------------------------------- };