diff --git a/CMakeLists.txt b/CMakeLists.txt index dbd7a903..ca5d9018 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,6 +24,7 @@ if (${QUICK_QANAVA_BUILD_SAMPLES}) add_subdirectory(samples/groups) #add_subdirectory(samples/navigable) add_subdirectory(samples/nodes) + add_subdirectory(samples/layouts) #add_subdirectory(samples/selection) #add_subdirectory(samples/style) #add_subdirectory(samples/topology) diff --git a/samples/layouts/CMakeLists.txt b/samples/layouts/CMakeLists.txt new file mode 100644 index 00000000..453c4792 --- /dev/null +++ b/samples/layouts/CMakeLists.txt @@ -0,0 +1,38 @@ +cmake_minimum_required(VERSION 3.16) + +project(QuickQanava_sample_layouts VERSION 2.5.0 LANGUAGES CXX) + +# Require C++17 +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +# Configure Qt +find_package(Qt6 REQUIRED COMPONENTS Core Quick Qml Quick QuickControls2) + +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set_property(DIRECTORY APPEND PROPERTY COMPILE_DEFINITIONS $<$:QT_QML_DEBUG>) +set(CMAKE_INCLUDE_CURRENT_DIR ON) +include_directories(${CMAKE_CURRENT_SOURCE_DIR} "../../src") + +if(NOT TARGET QuickQanava) + add_subdirectory(../../ quickqanava_build) # Use ../../src as quickqanava_build subdirectory + # see https://stackoverflow.com/questions/50408169/cmake-error-add-subdirectory-not-given-a-binary-directory/50408555 +endif() + +set(source_files + layouts.cpp +) + +set (header_files +) + +add_executable(sample_layouts ${source_files} ${header_files} layouts.qrc) +target_include_directories(sample_layouts PUBLIC QuickQanava Qt${QT_VERSION_MAJOR}::QuickControls2) +target_link_libraries(sample_layouts + QuickQanava + Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Gui + Qt${QT_VERSION_MAJOR}::QuickControls2 +) diff --git a/samples/layouts/default.qml b/samples/layouts/default.qml new file mode 100644 index 00000000..28d55fd5 --- /dev/null +++ b/samples/layouts/default.qml @@ -0,0 +1,157 @@ +/* + Copyright (c) 2008-2024, Benoit AUTHEMAN All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the author or Destrat.io nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL AUTHOR BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +import QtQuick +import QtQuick.Controls.Material +import QtQuick.Layouts + +import QuickQanava 2.0 as Qan +import "qrc:/QuickQanava" as Qan + +Qan.GraphView { + id: graphView + anchors.fill: parent + navigable : true + resizeHandlerColor: "#03a9f4" // SAMPLE: Set resize handler color to blue for 'resizable' nodes + gridThickColor: Material.theme === Material.Dark ? "#4e4e4e" : "#c1c1c1" + PinchHandler { + target: null + onActiveScaleChanged: { + console.error('centroid.position=' + centroid.position) + console.error('activeScale=' + activeScale) + var p = centroid.position + var f = activeScale > 1.0 ? 1. : -1. + graphView.zoomOn(p, graphView.zoom + (f * 0.03)) + } + } + graph: Qan.Graph { + parent: graphView + id: graph + Component.onCompleted: { + var n1 = graph.insertNode() + n1.label = "Hello World"; n1.item.x=15; n1.item.y= 25 + n1.item.ratio = 0.4 + var n2 = graph.insertNode() + n2.label = "Node 2"; n2.item.x=15; n2.item.y= 125 + + var e = graph.insertEdge(n1, n2); + //defaultEdgeStyle.lineType = Qan.EdgeStyle.Curved + } + onNodeClicked: (node) => { + notifyUser( "Node " + node.label + " clicked" ) + nodeEditor.node = node + } + onNodeRightClicked: (node) => { notifyUser( "Node " + node.label + " right clicked" ) } + onNodeDoubleClicked: (node) => { notifyUser( "Node " + node.label + " double clicked" ) } + onNodeMoved: (node) => { notifyUser("Node " + node.label + " moved") } + } // Qan.Graph + + Menu { // Context menu demonstration + id: contextMenu + property var node: undefined + MenuItem { + text: "Insert Node" + onClicked: { + let n = graph.insertNode() + n.label = 'New Node' + n.item.x = contextMenu.x + n.item.y = contextMenu.y + } + } + MenuItem { + text: "Remove node" + enabled: contextMenu.node !== undefined + onClicked: { + graph.removeNode(contextMenu.node) + contextMenu.node = undefined + } + } + onClosed: { // Clean internal state when context menu us closed + contextMenu.node = undefined + } + } // Menu + + onRightClicked: function(pos) { + contextMenu.x = pos.x + contextMenu.y = pos.y + contextMenu.open() + } + + ToolTip { id: toolTip; timeout: 2500 } + function notifyUser(message) { toolTip.text=message; toolTip.open() } + Label { + anchors.left: parent.left; anchors.leftMargin: 15 + anchors.bottom: parent.bottom; anchors.bottomMargin: 15 + text: "Use CTRL+Click to select multiples nodes" + } + Pane { + id: nodeEditor + property var node: undefined + onNodeChanged: nodeItem = node ? node.item : undefined + property var nodeItem: undefined + anchors.bottom: parent.bottom; anchors.bottomMargin: 15 + anchors.right: parent.right; anchors.rightMargin: 15 + padding: 0 + Frame { + ColumnLayout { + Label { + text: nodeEditor.node ? "Editing node " + nodeEditor.node.label + "": "Select a node..." + } + CheckBox { + text: "Draggable" + enabled: nodeEditor.nodeItem !== undefined + checked: nodeEditor.nodeItem ? nodeEditor.nodeItem.draggable : false + onClicked: nodeEditor.nodeItem.draggable = checked + } + CheckBox { + text: "Resizable" + enabled: nodeEditor.nodeItem !== undefined + checked: nodeEditor.nodeItem ? nodeEditor.nodeItem.resizable : false + onClicked: nodeEditor.nodeItem.resizable = checked + } + CheckBox { + text: "Selected (read-only)" + enabled: false + checked: nodeEditor.nodeItem ? nodeEditor.nodeItem.selected : false + } + CheckBox { + text: "Selectable" + enabled: nodeEditor.nodeItem != null + checked: nodeEditor.nodeItem ? nodeEditor.nodeItem.selectable : false + onClicked: nodeEditor.nodeItem.selectable = checked + } + Label { text: "style.backRadius" } + Slider { + from: 0.; to: 15.0; + value: defaultNodeStyle.backRadius + stepSize: 1.0 + onMoved: defaultNodeStyle.backRadius = value + } + } + } + } +} // Qan.GraphView + diff --git a/samples/layouts/layouts.cpp b/samples/layouts/layouts.cpp new file mode 100644 index 00000000..38c1f068 --- /dev/null +++ b/samples/layouts/layouts.cpp @@ -0,0 +1,57 @@ +/* + Copyright (c) 2008-2024, Benoit AUTHEMAN All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the author or Destrat.io nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL AUTHOR BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +//----------------------------------------------------------------------------- +// This file is a part of the QuickQanava software library. +// +// \file layouts.cpp +// \author benoit@destrat.io +// \date 20240813 +//----------------------------------------------------------------------------- + +// Qt headers +#include +#include +#include + +// QuickQanava headers +#include + +//----------------------------------------------------------------------------- +int main( int argc, char** argv ) +{ + QGuiApplication app(argc, argv); + QQuickStyle::setStyle("Material"); + QQmlApplicationEngine* engine = new QQmlApplicationEngine(); + engine->addPluginPath(QStringLiteral("../../src")); // Necessary only for development when plugin is not installed to QTDIR/qml + QuickQanava::initialize(engine); + engine->load(QUrl("qrc:/layouts.qml")); + const auto status = app.exec(); + delete engine; + return status; +} +//----------------------------------------------------------------------------- + diff --git a/samples/layouts/layouts.qml b/samples/layouts/layouts.qml new file mode 100644 index 00000000..c10a5718 --- /dev/null +++ b/samples/layouts/layouts.qml @@ -0,0 +1,102 @@ +/* + Copyright (c) 2008-2024, Benoit AUTHEMAN All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the author or Destrat.io nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL AUTHOR BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +import QtQuick +import QtQuick.Controls.Material +import QtQuick.Layouts + +import QuickQanava 2.0 as Qan +import "qrc:/QuickQanava" as Qan + +ApplicationWindow { + id: window + visible: true + width: 1280; height: 720 + title: "Layouts Sample" + + Qan.GraphView { + id: graphView + anchors.fill: parent + navigable : true + resizeHandlerColor: "#03a9f4" + gridThickColor: Material.theme === Material.Dark ? "#4e4e4e" : "#c1c1c1" + graph: Qan.Graph { + parent: graphView + id: graph + Component.onCompleted: { + let n1 = graph.insertNode() + 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 + + let n121 = graph.insertNode() + n121.label = "n121"; n121.item.x=125; n121.item.y= 225 + 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 + + let n13 = graph.insertNode() + n13.label = "n13"; n13.item.x=225; n13.item.y= 125 + + graph.insertEdge(n1, n11); + graph.insertEdge(n1, n12); + graph.insertEdge(n1, n13); + graph.insertEdge(n12, n121); + graph.insertEdge(n12, n122); + graph.insertEdge(n121, n1211); + + orgTreeLayout.layout(n1); + } + Qan.OrgTreeLayout { + id: orgTreeLayout + } + } // Qan.Graph + Menu { // Context menu demonstration + id: contextMenu + MenuItem { + text: "Insert Node" + onClicked: { + let n = graph.insertNode() + n.label = 'New Node' + n.item.x = contextMenu.x + n.item.y = contextMenu.y + } + } + } // Menu + onRightClicked: function(pos) { + contextMenu.x = pos.x + contextMenu.y = pos.y + contextMenu.open() + } + } // Qan.GraphView +} + diff --git a/samples/layouts/layouts.qrc b/samples/layouts/layouts.qrc new file mode 100644 index 00000000..6d7876a0 --- /dev/null +++ b/samples/layouts/layouts.qrc @@ -0,0 +1,6 @@ + + + layouts.qml + qtquickcontrols2.conf + + diff --git a/samples/layouts/qtquickcontrols2.conf b/samples/layouts/qtquickcontrols2.conf new file mode 100644 index 00000000..59f04183 --- /dev/null +++ b/samples/layouts/qtquickcontrols2.conf @@ -0,0 +1,9 @@ +[Material] +Primary=#03A9F4 +Accent=#03A9F4 +Theme=Light +Variant=Dense + +[Universal] +Accent=#41cd52 +Theme=Light diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index bed8a5b4..da55f564 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -30,6 +30,7 @@ set(qan_source_files qanTableCell.cpp qanTableBorder.cpp qanTableGroupItem.cpp + qanTreeLayout.cpp ) set (qan_header_files @@ -64,6 +65,7 @@ set (qan_header_files qanTableCell.h qanTableBorder.h qanTableGroupItem.h + qanTreeLayout.cpp QuickQanava.h gtpo/container_adapter.h gtpo/edge.h diff --git a/src/QuickQanava.h b/src/QuickQanava.h index 3a090dc1..25d34813 100755 --- a/src/QuickQanava.h +++ b/src/QuickQanava.h @@ -63,6 +63,7 @@ #include "./qanBottomResizer.h" #include "./qanNavigablePreview.h" #include "./qanAnalysisTimeHeatMap.h" +#include "./qanTreeLayout.h" struct QuickQanava { static void initialize(QQmlEngine* engine) { @@ -116,6 +117,8 @@ struct QuickQanava { qmlRegisterType("QuickQanava", 2, 0, "BottomRightResizer"); qmlRegisterType("QuickQanava", 2, 0, "RightResizer"); qmlRegisterType("QuickQanava", 2, 0, "BottomResizer"); + + qmlRegisterType("QuickQanava", 2, 0, "OrgTreeLayout"); } // initialize() }; diff --git a/src/qanTreeLayout.cpp b/src/qanTreeLayout.cpp new file mode 100644 index 00000000..f4abb909 --- /dev/null +++ b/src/qanTreeLayout.cpp @@ -0,0 +1,189 @@ +/* + Copyright (c) 2008-2024, Benoit AUTHEMAN All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the author or Destrat.io nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL AUTHOR BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +//----------------------------------------------------------------------------- +// This file is a part of the QuickQanava software library. +// +// \file qanOrgTreeLayout.h +// \author benoit@destrat.io +// \date 2024 08 13 +//----------------------------------------------------------------------------- + +// Std headers +#include +#include + +// Qt headers +#include +#include +#include +#include + +// QuickQanava headers +#include "./qanTreeLayout.h" + + +namespace qan { // ::qan + +/* NaiveTreeLayout Object Management *///-------------------------------------- +NaiveTreeLayout::NaiveTreeLayout(QObject* parent) noexcept : + QObject{parent} +{ +} +NaiveTreeLayout::~NaiveTreeLayout() { } + +void NaiveTreeLayout::layout(qan::Node& root) noexcept +{ + // Pre-condition: root must be a tree subgraph, this is not enforced in this methodod. + + // Algorithm: + // 1. Use BFS to generate an array of nodes "level by level". + // 2. Layout the tree bottom up; for every level, bottom up: + // 2.1 Layout the node at y(level) position. + // 2.2 For mid level, shift nodes x position to align right subgraph on it's previous + // node subgraph (magic happen here: use a shifting heuristic !) + // 2.3 Align node on it's sub graph (according to input configuration align left or center) + // 3. Shift the tree to align root to it's original position. + + const auto collectBFS = [](qan::Node* root) -> std::vector> { + std::vector> r; + if (root == nullptr) + return r; + + // <-- hand tuned ChatGPT code + std::queue> nodeLevelqueue; + std::unordered_set visited; + + nodeLevelqueue.push({root, 0}); + visited.insert(root); + + while (!nodeLevelqueue.empty()) { + auto [current, level] = nodeLevelqueue.front(); + nodeLevelqueue.pop(); + + if (r.size() <= level) + r.resize(level + 1); // Resize() initialize new items + r[level].push_back(current); + for (auto child : current->get_out_nodes()) { // Enqueue unvisited children with their level + if (visited.find(child) == visited.end()) { + nodeLevelqueue.push({child, level + 1}); + visited.insert(child); + } + } + } + // <-- ChatGPT code + return r; + }; + + // 1. BFS + const auto levels = collectBFS(&root); + + // Debug + int l = 0; + for (const auto& level: levels) { + std::cerr << l++ << ": "; + for (const auto node: level) + std::cerr << node->getLabel().toStdString() << "\t"; + std::cerr << std::endl; + } + + // 2. + if (levels.size() <= 1) // Can't layout a tree with less than 2 levels + return; + const double xSpacing = 25.; + const double ySpacing = 125.; + for (int level = levels.size() - 1; level >= 0; level--) { + auto nodes = levels[level]; + + // 2.1 + const double y = level * ySpacing; // FIXME, be smarter on shift here... + + // 2.2 + double x = 0.; + for (const auto node: nodes) { + node->getItem()->setX(x); + node->getItem()->setY(y); + x += node->getItem()->getBoundingShape().boundingRect().width() + xSpacing; + } + } + + // FIXME centering in another pass... +} + +void NaiveTreeLayout::layout(qan::Node* root) noexcept +{ + qWarning() << "qan::NaiveTreeLayout::layout(): root=" << root; + if (root != nullptr) + layout(*root); +} +//----------------------------------------------------------------------------- + + +/* OrgTreeLayout Object Management *///---------------------------------------- +OrgTreeLayout::OrgTreeLayout(QObject* parent) noexcept : + QObject{parent} +{ +} +OrgTreeLayout::~OrgTreeLayout() { } + +void OrgTreeLayout::layout(qan::Node& root, qreal xSpacing, qreal ySpacing) noexcept +{ + // FIXME #228: Variant / naive Reingold-Tilford algorithm + + // Pre-condition: root must be a tree subgraph, this is not enforced in this algorithm, + // any circuit will lead to intinite recursion... + + // 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; + 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); + br = br.united(child->getItem()->boundingRect().translated(child->getItem()->position())); + const auto prevBr = self(self, child->get_out_nodes(), br); + br = br.united(prevBr); + } + 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())); +} + +void OrgTreeLayout::layout(qan::Node* root, qreal xSpacing, qreal ySpacing) noexcept +{ + //qWarning() << "qan::OrgTreeLayout::layout(): root=" << root; + if (root != nullptr) + layout(*root, xSpacing, ySpacing); +} +//----------------------------------------------------------------------------- + +} // ::qan diff --git a/src/qanTreeLayout.h b/src/qanTreeLayout.h new file mode 100644 index 00000000..2d02c4de --- /dev/null +++ b/src/qanTreeLayout.h @@ -0,0 +1,116 @@ +/* + Copyright (c) 2008-2024, Benoit AUTHEMAN All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the author or Destrat.io nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL AUTHOR BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +//----------------------------------------------------------------------------- +// This file is a part of the QuickQanava software library. +// +// \file qanOrgTreeLayout.h +// \author benoit@destrat.io +// \date 2024 08 13 +//----------------------------------------------------------------------------- + +#pragma once + +// Qt headers +#include +#include +#include +#include +#include + +// QuickQanava headers +#include "./qanGraph.h" + + +namespace qan { // ::qan + +/*! \brief + * \nosubgrouping + */ +class NaiveTreeLayout : public QObject +{ + Q_OBJECT + /*! \name NaiveTreeLayout Object Management *///--------------------------- + //@{ +public: + explicit NaiveTreeLayout(QObject* parent = nullptr) noexcept; + virtual ~NaiveTreeLayout() override; + NaiveTreeLayout(const NaiveTreeLayout&) = delete; + NaiveTreeLayout& operator=(const NaiveTreeLayout&) = delete; + NaiveTreeLayout(NaiveTreeLayout&&) = delete; + NaiveTreeLayout& operator=(NaiveTreeLayout&&) = delete; + +public: + // FIXME #228 + void layout(qan::Node& root) noexcept; + + //! QML invokable version of layout(). + Q_INVOKABLE void layout(qan::Node* root) noexcept; + //@} + //------------------------------------------------------------------------- +}; + + +/*! \brief + * \nosubgrouping + */ +class OrgTreeLayout : public QObject +{ + Q_OBJECT + /*! \name OrgTreeLayout Object Management *///----------------------------- + //@{ +public: + explicit OrgTreeLayout(QObject* parent = nullptr) noexcept; + virtual ~OrgTreeLayout() override; + OrgTreeLayout(const OrgTreeLayout&) = delete; + OrgTreeLayout& operator=(const OrgTreeLayout&) = delete; + OrgTreeLayout(OrgTreeLayout&&) = delete; + OrgTreeLayout& operator=(OrgTreeLayout&&) = delete; + +public: + /*! \brief Apply a vertical "organisational chart tree layout algorithm" to subgraph \c root. + * + * OrgChart layout _will preserve_ node orders. + * + * This naive implementation is recursive and not "space optimal" while it run in O(n), + * n beeing the number of nodes in root "tree subgraph". + * + * \note \c root must be a tree subgraph, this method will not enforce this condition, + * 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; + + //! QML invokable version of layout(). + Q_INVOKABLE void layout(qan::Node* root, qreal xSpacing = 35., qreal ySpacing = 25.) noexcept; + //@} + //------------------------------------------------------------------------- +}; + +} // ::qan + +QML_DECLARE_TYPE(qan::OrgTreeLayout) +