diff --git a/.github/workflows/build-linux-qt5.yml b/.github/workflows/build-linux-qt5.yml index 8f55e175..e658158e 100644 --- a/.github/workflows/build-linux-qt5.yml +++ b/.github/workflows/build-linux-qt5.yml @@ -1,8 +1,8 @@ -name: Linux build develop +name: Linux Qt5 QMake g++ on: push: - branches: [master] + branches: [master, develop] jobs: test-build: @@ -20,7 +20,7 @@ jobs: modules: qtcore qttools qtgui qtquickcontrols2 version: 5.15.2 target: desktop - #setup-python: false + setup-python: false - name: Create Build Dir run: mkdir build diff --git a/.github/workflows/build-win64-qt5.yml b/.github/workflows/build-win64-qt5.yml new file mode 100644 index 00000000..ebd938e5 --- /dev/null +++ b/.github/workflows/build-win64-qt5.yml @@ -0,0 +1,49 @@ +name: Win64 Qt5 QMake msvc2019 + +on: + push: + branches: [master, develop] + +jobs: + test-build: + runs-on: windows-2019 + timeout-minutes: 120 + steps: + - name: Clone QuickQanava + uses: actions/checkout@v2 + with: + ref: master + + - name: Install Qt + uses: jurplel/install-qt-action@v3 + with: + version: '5.15.*' + host: 'windows' + target: 'desktop' + arch: 'win64_msvc2019_64' + dir: '${{ github.workspace }}/' + install-deps: 'true' + modules: 'qtwebengine' + cache: 'false' + cache-key-prefix: 'install-qt-action' + setup-python: 'false' + + - name: Create Build Dir + run: mkdir build + working-directory: ${{runner.workspace}} + + - uses: ilammy/msvc-dev-cmd@v1 + + - name: Configure qmake + run: | + QT_SELECT=5 qmake CONFIG+=release -o Makefile ${GITHUB_WORKSPACE}/quickqanava.pro + working-directory: ${{runner.workspace}}/build + shell: bash + + - name: Build Project + run: | + echo "NMAKE..." + set CL=/MP & rem "Enabling multi-threading (max. available)" + nmake + shell: cmd + working-directory: ${{runner.workspace}}/build diff --git a/.github/workflows/build-win64-qt6.yml b/.github/workflows/build-win64-qt6.yml index 7ae06fcd..0b1a05b9 100644 --- a/.github/workflows/build-win64-qt6.yml +++ b/.github/workflows/build-win64-qt6.yml @@ -1,4 +1,4 @@ -name: Win64 build master +name: Win64 Qt6 CMake msvc2019 on: push: diff --git a/CHANGELOG.md b/CHANGELOG.md index 01343cd1..f47c25f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # CHANGELOG +## 20221215 v2.2.0: +- #183: Add a `multipleSelectionEnabled` property to `qan::Graph` to enable or disable multiple selection. + +## 20221204 v2.2.0: +- #161: Add graph dynamic graph preview (move, center on, zoom on). + ## 20221011 v2.2.0: - #169: Create qan::RightResizer and qan::BottomResizer. - By default, nodes and groups are resizable from their right and bottom borders. diff --git a/README.md b/README.md index b1471a47..2e82b590 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,12 @@ # QuickQanava -![Build linux Qt5.15](https://github.com/cneben/QuickQanava/actions/workflows/build-linux-qt5.yml/badge.svg) -![Build win64 Qt6.4](https://github.com/cneben/QuickQanava/actions/workflows/build-win64-qt6.yml/badge.svg) +![Linux Qt5 g++ qmake](https://github.com/cneben/QuickQanava/actions/workflows/build-linux-qt5.yml/badge.svg) +![Win64 Qt5 msvc qmake](https://github.com/cneben/QuickQanava/actions/workflows/build-win64-qt5.yml/badge.svg) +![Win64 Qt6 msvc CMake](https://github.com/cneben/QuickQanava/actions/workflows/build-win64-qt6.yml/badge.svg) [![Documentation](https://img.shields.io/badge/docs-mkdocs-blue.svg)](http://cneben.github.io/QuickQanava/) [![License](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause) -![](https://img.shields.io/badge/version-2.0.0-blue.svg) +![](https://img.shields.io/badge/version-2.1.0-blue.svg) [![Twitter URL](https://img.shields.io/twitter/url/https/twitter.com/fold_left.svg?style=social&label=Follow%20%40QuickQanava)](https://twitter.com/QuickQanava) ![](https://github.com/cneben/QuickQanava/blob/master/doc/web/docs/images/home.png) diff --git a/doc/web/docs/index.md b/doc/web/docs/index.md index 8038a1d8..963c3eb5 100644 --- a/doc/web/docs/index.md +++ b/doc/web/docs/index.md @@ -6,16 +6,18 @@ weight: 0 --- ![home](images/home.png) -[![Build Status](https://travis-ci.org/cneben/QuickQanava.svg?branch=master)](https://travis-ci.org/cneben/QuickQanava) (Linux/g++6/Qt5.12.1 - OSX/Clang/Qt5.12.1) -[![Build status](https://ci.appveyor.com/api/projects/status/ghpiaqqew63er8ea?svg=true)](https://ci.appveyor.com/project/cneben/quickqanava) (Windows MSVC 2015 x64/Qt5.10.1) +![Linux Qt5 g++ qmake](https://github.com/cneben/QuickQanava/actions/workflows/build-linux-qt5.yml/badge.svg) (Linux, g++, Qt5.15, qmake) + +![Win64 Qt5 msvc qmake](https://github.com/cneben/QuickQanava/actions/workflows/build-win64-qt5.yml/badge.svg) (Windows, msvc2019, Qt5.15, qmake) + +![Win64 Qt6 msvc CMake](https://github.com/cneben/QuickQanava/actions/workflows/build-win64-qt6.yml/badge.svg) (Windows, msvc2019, Qt6.4, CMake) [![Documentation](https://img.shields.io/badge/docs-doxygen-blue.svg)](http://www.destrat.io/quickqanava/doc) [![License](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause) [![Twitter URL](https://img.shields.io/twitter/url/https/twitter.com/fold_left.svg?style=social&label=Follow%20%40QuickQanava)](https://twitter.com/QuickQanava) - -!!! warning "QuickQanava is alpha, interface may change before 1.0.0 release, but QuickQanava is already used extensively in production code." +!!! warning "QuickQanava is still alpha, but QuickQanava is already used extensively in production code." !!! note "QuickQanava is licensed under BSD-3, specific features or support is available on demand: benoit@destrat.io" @@ -25,7 +27,7 @@ weight: 0 QuickQanava main repository is hosted on GitHub: https://github.com/cneben/quickqanava -QuickQanava is primarily developed with Qt >= 5.13 with MSVC2015 and g++7. minimal required Qt version is **Qt 5.10**. +QuickQanava is primarily developed on Linux and Qt 5.15. minimal required Qt version is **Qt 5.10**. + Project homepage: [http://cneben.github.io/QuickQanava/index.html](http://cneben.github.io/QuickQanava/index.html) @@ -42,14 +44,3 @@ Please refer to [Installation](installation.md) manual and [Graph](graph.md), [N ![styles](samples/topology.png) -## Roadmap - - - **pre v2.0:** - - [X] Add full support for groups inside group (ie subgraphs). - - [X] Update geometry creation interface and delegate management. - - [X] Rewrite CMake configuration, add install step, use QML plugins. - - **v2.1.x:** - - [ ] Add support for direct visual dragging of port items. - - [ ] Add "snap to grid" support. - - diff --git a/doc/web/docs/utilities.md b/doc/web/docs/utilities.md index 4bb51e22..84d84368 100644 --- a/doc/web/docs/utilities.md +++ b/doc/web/docs/utilities.md @@ -5,7 +5,7 @@ BottomRightResizer: ------------------ -Qan.BottomRightResizer add a "resize handler" ont the bottom right of a target QML Item. Bottom right resizer component is automatically initialized in the QuickQanava::initialize method, it has no dependencies on QuickQanava and could be used in an isolated project just by copying its source code: fqlBotomRightRizer.h and fqlBotomRightRizer.cpp with a call to `#!js qmlRegisterType< fql::BottomRightResizer >( "YourModule", 1, 0, "BottomRightResizer" );` +Qan.BottomRightResizer add a "resize handler" ont the bottom right of a target QML Item. Bottom right resizer component is automatically initialized in the QuickQanava::initialize method, it has no dependencies on QuickQanava and could be used in an isolated project just by copying its source code: `qanBottomRightResizer.h` and `qanBottomRightResizer.cpp` with a call to `#!js qmlRegisterType( "YourModule", 1, 0, "BottomRightResizer");` ![BottomRightResizer](utilities/utilities-resizer.png) @@ -13,18 +13,20 @@ Qan.BottomRightResizer add a "resize handler" ont the bottom right of a target Q ~~~~~~~~~~~~~{.cpp} // From c++: -qmlRegisterType< fql::BottomRightResizer >( "YourModule", 1, 0, "BottomRightResizer" ); +qmlRegisterType("YourModule", 1, 0, "BottomRightResizer"); // From QML: -import YourModule 1.0 as Fql +import YourModule 1.0 as Qan Item { id: targetItem - Fql.BottomRightResizer { target: targetItem } + Qan.BottomRightResizer { target: targetItem } } ~~~~~~~~~~~~~ -Resizer not necessarilly has to be in *target* (host) sibling, `#!js Fql.BottomRightResizer` could be defined outside of target item hierarchy, for example to avoid corrupting the target `childrenRect` property. It is however more efficient to use the resizer as a target child (most common case). +Resizer not necessarilly has to be in *target* (host) sibling, `#!js Qan.BottomRightResizer` could be defined outside of target item hierarchy, for example to avoid corrupting the target `childrenRect` property. It is however more efficient to use the resizer as a target child (most common case). + +Right and bottom resizer are also available, see: `qan::RightResizer` and `qan::BottomResizer`. Navigable: ------------------ diff --git a/quickqanava.pro b/quickqanava.pro index ee6b293e..de023499 100644 --- a/quickqanava.pro +++ b/quickqanava.pro @@ -16,13 +16,13 @@ test-tools.subdir = samples/tools # Uncomment to activate samples projects: SUBDIRS += test-nodes -#SUBDIRS += test-edges -#SUBDIRS += test-connector +SUBDIRS += test-edges +SUBDIRS += test-connector SUBDIRS += test-groups -#SUBDIRS += test-selection +SUBDIRS += test-selection #SUBDIRS += test-style -#SUBDIRS += test-topology -#SUBDIRS += test-dataflow +SUBDIRS += test-topology +SUBDIRS += test-dataflow #SUBDIRS += test-cpp #SUBDIRS += test-tools diff --git a/samples/dataflow/TintNode.qml b/samples/dataflow/TintNode.qml index ae56a46d..7ced1b25 100644 --- a/samples/dataflow/TintNode.qml +++ b/samples/dataflow/TintNode.qml @@ -35,6 +35,7 @@ import QtQuick 2.7 import QtQuick.Controls 2.0 import QtQuick.Layouts 1.3 +import QtGraphicalEffects 1.15 import QuickQanava 2.0 as Qan import "qrc:/QuickQanava" as Qan diff --git a/samples/groups/groups.qml b/samples/groups/groups.qml index 66f15697..2f655bab 100644 --- a/samples/groups/groups.qml +++ b/samples/groups/groups.qml @@ -301,5 +301,15 @@ ApplicationWindow { } } // Control groupEditor } // Qan.GraphView -} + Qan.GraphPreview { + id: graphPreview + source: graphView + viewWindowColor: Material.accent + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: graphView.bottom + anchors.bottomMargin: 8 + width: 350 + height: 198 + } // Qan.GraphPreview +} // ApplicationWindow: window diff --git a/samples/selection/selection.qml b/samples/selection/selection.qml index b84506b7..b8679d04 100644 --- a/samples/selection/selection.qml +++ b/samples/selection/selection.qml @@ -175,6 +175,11 @@ ApplicationWindow { } } } + Switch { + text: "Multiple selection enabled" + checked: topology.multipleSelectionEnabled + onClicked: topology.multipleSelectionEnabled = checked + } RowLayout { Layout.margins: 2 Label { text:"Policy:" } diff --git a/src/GraphPreview.qml b/src/GraphPreview.qml index ce36046a..a742e037 100644 --- a/src/GraphPreview.qml +++ b/src/GraphPreview.qml @@ -38,7 +38,7 @@ Control { // PUBLIC ///////////////////////////////////////////////////////////////// width: 200 - height: 113 + height: 135 //! Source Qan.GraphView that should be previewed. property var source: undefined @@ -54,7 +54,6 @@ Control { // PRIVATE //////////////////////////////////////////////////////////////// padding: 0 - property real previewSize: 0.15 property real graphRatio: source ? (source.containerItem.childrenRect.width / source.containerItem.childrenRect.height) : 1. @@ -62,6 +61,8 @@ Control { onGraphRatioChanged: updateNavigablePreviewSize() onPreviewRatioChanged: updateNavigablePreviewSize() onSourceChanged: updateNavigablePreviewSize() + onWidthChanged: updateNavigablePreviewSize() + onHeightChanged: updateNavigablePreviewSize() function updateNavigablePreviewSize() { // Update the navigable preview width/height such that it's aspect ratio diff --git a/src/GraphView.qml b/src/GraphView.qml index 2a74cc00..99e35a8d 100644 --- a/src/GraphView.qml +++ b/src/GraphView.qml @@ -292,7 +292,8 @@ Qan.AbstractGraphView { // Do not show resizers when group is collapsed groupRightResizer.visible = groupBottomResizer.visible = groupResizer.visible = Qt.binding(() => { // Resizer is visible : - return group && group.item && // If group and group.item are valid + return group && ! group.locked && + group.item && // If group and group.item are valid group.item.visible && (!group.item.collapsed) && // And if group is not collapsed group.item.resizable; // And if group is resizeable diff --git a/src/HeatMapPreview.qml b/src/HeatMapPreview.qml index c2fb1c35..766982b4 100644 --- a/src/HeatMapPreview.qml +++ b/src/HeatMapPreview.qml @@ -57,7 +57,6 @@ Control { // PRIVATE //////////////////////////////////////////////////////////////// padding: 0 - property real previewSize: 0.15 property real graphRatio: source ? (source.containerItem.childrenRect.width / source.containerItem.childrenRect.height) : 1. diff --git a/src/NavigablePreview.qml b/src/NavigablePreview.qml index 2dba8ad6..9ab61209 100644 --- a/src/NavigablePreview.qml +++ b/src/NavigablePreview.qml @@ -60,9 +60,12 @@ Qan.AbstractNavigablePreview { source.containerItem.onWidthChanged.connect(updatePreview) source.containerItem.onHeightChanged.connect(updatePreview) source.containerItem.onScaleChanged.connect(updatePreview) - source.containerItem.onXChanged.connect(updatePreview) - source.containerItem.onYChanged.connect(updatePreview) + // Note 20221204: Do not connect on containerItem on(X/Y)Changed(), + // wait for user initiated onContainerItemModified since dragging + // view window also modify container x/y source.containerItem.onChildrenRectChanged.connect(updatePreview) + // Emitted only on user initiaited navitable view changes. + source.containerItemModified.connect(updatePreview) sourcePreview.sourceItem = source.containerItem } else @@ -134,28 +137,7 @@ Qan.AbstractNavigablePreview { if (!r) return - //if (r.width < preview.source.width && // If scene size is stricly inferior to preview size - // r.height < preview.source.height) { // reset the preview window - // r.width = preview.source.width - // r.height = preview.source.height - //} - //if (r.width < 0.01 || // Do not update without a valid children rect - // r.height < 0.01) { - // preview.resetVisibleWindow() - // return - //} - //if (r.width < r.width || // Reset the visible window is the whole containerItem content - // r.height < r.height) { // is smaller than graph view - // preview.resetVisibleWindow() - // return - //} - //if (r.width < preview.width && // If scene size is stricly inferior to preview size - // r.height < preview.height) { // reset the preview window - // preview.resetVisibleWindow() - // return - //} - - // r is content rect + // r is content rect in scene Cs // viewR is window rect in content rect Cs // Window is viewR in preview Cs @@ -175,8 +157,9 @@ Qan.AbstractNavigablePreview { viewWindow.visible = true viewWindow.x = (previewXRatio * (viewR.x - r.x)) + border viewWindow.y = (previewYRatio * (viewR.y - r.y)) + border - viewWindow.width = (previewXRatio * viewR.width) - border2 - viewWindow.height = (previewYRatio * viewR.height) - border2 + // Set a minimum view window size to avoid users beeing lost in large graphs + viewWindow.width = Math.max(12, (previewXRatio * viewR.width) - border2) + viewWindow.height = Math.max(9, (previewYRatio * viewR.height) - border2) visibleWindowChanged(Qt.rect(viewWindow.x / preview.width, viewWindow.y / preview.height, @@ -184,38 +167,120 @@ Qan.AbstractNavigablePreview { viewWindow.height / preview.height), source.zoom); } + + // Map from preview Cs to graph Cs + function mapFromPreview(p) { + if (!p) + return; + // preview window origin is (r.x, r.y) + let r = sourcePreview.sourceRect + var previewXRatio = r.width / preview.width + var previewYRatio = r.height / preview.height + let sourceP = Qt.point((p.x * previewXRatio) + r.x, + (p.y * previewYRatio) + r.y) + return sourceP + } + Item { id: overlayItem anchors.fill: parent; anchors.margins: 0 } Rectangle { id: viewWindow + z: 1 color: Qt.rgba(0, 0, 0, 0) smooth: true antialiasing: true border.color: viewWindowColor border.width: 2 - } - // Not active on 20201027 - /*MouseArea { - id: viewWindowDragger - anchors.fill: parent - drag.onActiveChanged: { - console.debug("dragging to:" + viewWindow.x + ":" + viewWindow.y ); - if ( source ) { + onXChanged: viewWindowDragged() + onYChanged: viewWindowDragged() + function viewWindowDragged() { + if (source && + (viewWindowController.pressedButtons & Qt.LeftButton || + viewWindowController.active)) { + // Convert viewWindow coordinate to source graph view CCS + let sceneP = mapFromPreview(Qt.point(viewWindow.x, viewWindow.y)) + source.moveTo(sceneP) } } - drag.target: viewWindow - drag.threshold: 1. // Note 20170311: Avoid a nasty delay between mouse position and dragged item position - // Do not allow dragging outside preview area - drag.minimumX: 0; drag.maximumX: Math.max(0, preview.width - viewWindow.width) - drag.minimumY: 0; drag.maximumY: Math.max(0, preview.height - viewWindow.height) - acceptedButtons: Qt.LeftButton | Qt.RightButton - hoverEnabled: true + MouseArea { // Manage dragging of "view window" + id: viewWindowController + anchors.fill: parent + drag.target: viewWindow + drag.threshold: 1. + drag.minimumX: 0 // Do not allow dragging outside preview area + drag.minimumY: 0 + drag.maximumX: Math.max(0, preview.width - viewWindow.width) + drag.maximumY: Math.max(0, preview.height - viewWindow.height) + acceptedButtons: Qt.LeftButton + enabled: true + cursorShape: Qt.SizeAllCursor + // Note 20221221: Surprisingly, onClicked and onDoubleClicked are activated without interferences + // while dragging is enabled. Activate zoom on click and double click just as in navigation controller, + // usefull when scene is fully de-zommed and view window take the full control space. + Timer { // See Note 20221204 + id: viewWindowTimer + interval: 100 + running: false + property point p: Qt.point(0, 0) + onTriggered: { + let sceneP = mapFromPreview(Qt.point(p.x, p.y)) + source.centerOnPosition(sceneP) + updatePreview() + } + } + onClicked: { + let p = mapToItem(preview, Qt.point(mouse.x, mouse.y)) + viewWindowTimer.p = p + viewWindowTimer.start() + mouse.accepted = true + } + onDoubleClicked: { + viewWindowTimer.stop() + let p = mapToItem(preview, Qt.point(mouse.x, mouse.y)) + let sceneP = mapFromPreview(p) + source.centerOnPosition(sceneP) + source.zoom = 1.0 + mouse.accepted = true + updatePreview() + } + } + } // Rectangle: viewWindow + MouseArea { // Manage move on click and zoom on double click + id: navigationController + anchors.fill: parent + z: 0 + acceptedButtons: Qt.LeftButton enabled: true - onReleased: { + cursorShape: Qt.CrossCursor + // Note 20221204: Hack for onClicked/onDoubleClicked(), see: + // https://forum.qt.io/topic/103829/providing-precedence-for-ondoubleclicked-over-onclicked-in-qml/2 + // onClicked trigger a centerOnPosition(), hidding this mouse area + // with viewWindow, thus not triggering any double click + Timer { + id: timer + interval: 100 + running: false + property point p: Qt.point(0, 0) + onTriggered: { + let sceneP = mapFromPreview(Qt.point(p.x, p.y)) + source.centerOnPosition(sceneP) + updatePreview() + } + } + onClicked: { + timer.p = Qt.point(mouse.x, mouse.y) + timer.start() + mouse.accepted = true } - onPressed : { + onDoubleClicked: { + timer.stop() + let sceneP = mapFromPreview(Qt.point(mouse.x, mouse.y)) + source.centerOnPosition(sceneP) + source.zoom = 1.0 + mouse.accepted = true + updatePreview() } - }*/ + } } // Qan.AbstractNavigablePreview: preview diff --git a/src/qanDraggableCtrl.cpp b/src/qanDraggableCtrl.cpp index eb417392..a24d7859 100644 --- a/src/qanDraggableCtrl.cpp +++ b/src/qanDraggableCtrl.cpp @@ -248,22 +248,23 @@ void DraggableCtrl::dragMove(const QPointF& delta, bool dragSelection) // Eventually, propose a node group drop after move if (!movedInsideGroup && _targetItem->getDroppable()) { - qan::Group* group = graph->groupAt( _targetItem->mapToItem(graphContainerItem, QPointF{0., 0.}), - { _targetItem->width(), _targetItem->height() }, - _targetItem /* except _targetItem */ ); + qan::Group* group = graph->groupAt(_targetItem->mapToItem(graphContainerItem, QPointF{0., 0.}), + {_targetItem->width(), _targetItem->height()}, + _targetItem /* except _targetItem */); if (group != nullptr && + !group->getLocked() && group->getItem() != nullptr && static_cast(group->getItem()) != static_cast(_targetItem.data())) { // Do not drop a group in itself group->itemProposeNodeDrop(); - if ( _lastProposedGroup && // When a node is already beeing proposed in a group (ie _lastProposedGroup is non nullptr), it - _lastProposedGroup->getItem() != nullptr && // might also end up beeing dragged over a sub group of _lastProposedGroup, so - group != _lastProposedGroup ) // notify endProposeNodeDrop() for last proposed group + if (_lastProposedGroup && // When a node is already beeing proposed in a group (ie _lastProposedGroup is non nullptr), it + _lastProposedGroup->getItem() != nullptr && // might also end up beeing dragged over a sub group of _lastProposedGroup, so + group != _lastProposedGroup) // notify endProposeNodeDrop() for last proposed group _lastProposedGroup->itemEndProposeNodeDrop(); _lastProposedGroup = group; - } else if ( group == nullptr && - _lastProposedGroup != nullptr && - _lastProposedGroup->getItem() != nullptr ) { + } else if (group == nullptr && + _lastProposedGroup != nullptr && + _lastProposedGroup->getItem() != nullptr) { _lastProposedGroup->itemEndProposeNodeDrop(); _lastProposedGroup = nullptr; } @@ -272,7 +273,7 @@ void DraggableCtrl::dragMove(const QPointF& delta, bool dragSelection) void DraggableCtrl::endDragMove(bool dragSelection) { - _dragLastPos = QPointF{ 0., 0. }; // Invalid all cached coordinates when drag ends + _dragLastPos = QPointF{0., 0.}; // Invalid all cached coordinates when drag ends _lastProposedGroup = nullptr; // PRECONDITIONS: @@ -295,8 +296,9 @@ void DraggableCtrl::endDragMove(bool dragSelection) qan::Group* group = graph->groupAt(targetContainerPos, { _targetItem->width(), _targetItem->height() }, _targetItem); if (group != nullptr && static_cast(group->getItem()) != static_cast(_targetItem.data())) { // Do not drop a group in itself - if (group->getGroupItem() != nullptr && // Do not allow grouping a node in a collapsed - !group->getGroupItem()->getCollapsed()) { // group item + if (group->getGroupItem() != nullptr && // Do not allow grouping a node in a collapsed + !group->getGroupItem()->getCollapsed() && // or locked group item + !group->getLocked() ) { graph->groupNode(group, _target.data()); nodeGrouped = true; } diff --git a/src/qanGraph.cpp b/src/qanGraph.cpp index acf26336..2c5d2fa2 100644 --- a/src/qanGraph.cpp +++ b/src/qanGraph.cpp @@ -264,32 +264,32 @@ void Graph::setConnectorSource(qan::Node* sourceNode) noexcept } } -void Graph::setConnectorEdgeColor( QColor connectorEdgeColor ) noexcept +void Graph::setConnectorEdgeColor(QColor connectorEdgeColor) noexcept { - if ( connectorEdgeColor != _connectorEdgeColor ) { + if (connectorEdgeColor != _connectorEdgeColor) { _connectorEdgeColor = connectorEdgeColor; - if ( _connector ) - _connector->setProperty( "edgeColor", connectorEdgeColor ); + if (_connector) + _connector->setProperty("edgeColor", connectorEdgeColor); emit connectorEdgeColorChanged(); } } -void Graph::setConnectorColor( QColor connectorColor ) noexcept +void Graph::setConnectorColor(QColor connectorColor) noexcept { - if ( connectorColor != _connectorColor ) { + if (connectorColor != _connectorColor) { _connectorColor = connectorColor; - if ( _connector ) - _connector->setProperty( "connectorColor", connectorColor ); + if (_connector) + _connector->setProperty("connectorColor", connectorColor); emit connectorColorChanged(); } } -void Graph::setConnectorCreateDefaultEdge( bool connectorCreateDefaultEdge ) noexcept +void Graph::setConnectorCreateDefaultEdge(bool connectorCreateDefaultEdge) noexcept { - if ( connectorCreateDefaultEdge != _connectorCreateDefaultEdge ) { + if (connectorCreateDefaultEdge != _connectorCreateDefaultEdge) { _connectorCreateDefaultEdge = connectorCreateDefaultEdge; - if ( _connector ) - _connector->setProperty( "createDefaultEdge", connectorCreateDefaultEdge ); + if (_connector) + _connector->setProperty("createDefaultEdge", connectorCreateDefaultEdge); emit connectorCreateDefaultEdgeChanged(); } } @@ -344,8 +344,8 @@ void Graph::setEdgeDelegate(QQmlComponent* edgeDelegate) noexcept void Graph::setEdgeDelegate(std::unique_ptr edgeDelegate) noexcept { - if ( edgeDelegate && - edgeDelegate != _edgeDelegate ) { + if (edgeDelegate && + edgeDelegate != _edgeDelegate) { _edgeDelegate = std::move(edgeDelegate); emit edgeDelegateChanged(); } @@ -353,10 +353,10 @@ void Graph::setEdgeDelegate(std::unique_ptr edgeDelegate) noex void Graph::setGroupDelegate(QQmlComponent* groupDelegate) noexcept { - if ( groupDelegate != nullptr ) { - if ( groupDelegate != _groupDelegate.get() ) { + if (groupDelegate != nullptr) { + if (groupDelegate != _groupDelegate.get()) { _groupDelegate.reset(groupDelegate); - QQmlEngine::setObjectOwnership( groupDelegate, QQmlEngine::CppOwnership ); + QQmlEngine::setObjectOwnership(groupDelegate, QQmlEngine::CppOwnership); emit groupDelegateChanged(); } } @@ -1065,7 +1065,7 @@ bool qan::Graph::ungroupNode(qan::Node* node, qan::Group* group, bool transfo /* Selection Management *///--------------------------------------------------- -void Graph::setSelectionPolicy(SelectionPolicy selectionPolicy) noexcept +void Graph::setSelectionPolicy(SelectionPolicy selectionPolicy) { if (selectionPolicy == _selectionPolicy) // Binding loop protection return; @@ -1075,6 +1075,18 @@ void Graph::setSelectionPolicy(SelectionPolicy selectionPolicy) noexcept emit selectionPolicyChanged(); } +auto Graph::setMultipleSelectionEnabled(bool multipleSelectionEnabled) -> bool +{ + if (multipleSelectionEnabled != _multipleSelectionEnabled) { + _multipleSelectionEnabled = multipleSelectionEnabled; + if (multipleSelectionEnabled == false) + clearSelection(); + emit multipleSelectionEnabledChanged(); + return true; + } + return false; +} + void Graph::setSelectionColor(QColor selectionColor) noexcept { if (selectionColor != _selectionColor) { @@ -1149,6 +1161,8 @@ bool selectPrimitive(Primitive_t& primitive, } } if (primitiveSelected) { + if (!graph.getMultipleSelectionEnabled()) + graph.clearSelection(); graph.addToSelection(primitive); return true; } diff --git a/src/qanGraph.h b/src/qanGraph.h index 1fbfc26d..98d2c8a7 100644 --- a/src/qanGraph.h +++ b/src/qanGraph.h @@ -139,11 +139,11 @@ class Graph : public gtpo::graph //! \sa containerItem inline QQuickItem* getContainerItem() noexcept { return _containerItem.data(); } inline const QQuickItem* getContainerItem() const noexcept { return _containerItem.data(); } - void setContainerItem( QQuickItem* containerItem ); + void setContainerItem(QQuickItem* containerItem); signals: void containerItemChanged(); private: - QPointer< QQuickItem > _containerItem{nullptr}; + QPointer _containerItem; //@} //------------------------------------------------------------------------- @@ -613,7 +613,11 @@ class Graph : public gtpo::graph * \li \c SelectOnClick (default): Node is selected when clicked, multiple selection is allowed with CTRL. * \li \c SelectOnCtrlClick: Node is selected only when CTRL is pressed, multiple selection is allowed with CTRL. */ - enum SelectionPolicy { NoSelection, SelectOnClick, SelectOnCtrlClick }; + enum class SelectionPolicy : int { + NoSelection = 1, + SelectOnClick = 2, + SelectOnCtrlClick = 4 + }; Q_ENUM(SelectionPolicy) public: @@ -622,13 +626,26 @@ class Graph : public gtpo::graph * \warning setting NoSeleciton will clear the actual \c selectedNodes model. */ Q_PROPERTY(SelectionPolicy selectionPolicy READ getSelectionPolicy WRITE setSelectionPolicy NOTIFY selectionPolicyChanged FINAL) - void setSelectionPolicy(SelectionPolicy selectionPolicy) noexcept; - inline SelectionPolicy getSelectionPolicy() const noexcept { return _selectionPolicy; } + void setSelectionPolicy(SelectionPolicy selectionPolicy); + SelectionPolicy getSelectionPolicy() const { return _selectionPolicy; } private: SelectionPolicy _selectionPolicy = SelectionPolicy::SelectOnClick; signals: void selectionPolicyChanged(); +public: + /*! \brief Enable mutliple selection (default to true ie multiple selection with CTRL is enabled). + * + * \warning setting \c multipleSelectionEnabled to \c false will clear the actual \c selectedNodes model. + */ + Q_PROPERTY(bool multipleSelectionEnabled READ getMultipleSelectionEnabled WRITE setMultipleSelectionEnabled NOTIFY multipleSelectionEnabledChanged FINAL) + auto setMultipleSelectionEnabled(bool multipleSelectionEnabled) -> bool; + auto getMultipleSelectionEnabled() const -> bool { return _multipleSelectionEnabled; } +private: + bool _multipleSelectionEnabled = true; +signals: + void multipleSelectionEnabledChanged(); + public: //! Color for the node selection hilgither component (default to dodgerblue). diff --git a/src/qanNavigable.cpp b/src/qanNavigable.cpp index c0744bec..86d68959 100644 --- a/src/qanNavigable.cpp +++ b/src/qanNavigable.cpp @@ -32,9 +32,6 @@ // \date 2015 07 19 //----------------------------------------------------------------------------- -// Qt headers -// Nil - // QuickQanava headers #include "./qanNavigable.h" @@ -110,19 +107,27 @@ void Navigable::centerOnPosition(QPointF position) // ALGORITHM: // 1. Compute a translation vector to move from current view center to the target position. // Take the zoom into account to scale the translation. - if (_containerItem == nullptr) return; - QPointF navigableCenter{width() / 2., height() / 2.}; - QPointF navigableCenterContainerCs = mapToItem(_containerItem, navigableCenter); - QPointF translation{navigableCenterContainerCs - position}; + const QPointF navigableCenter{width() / 2., height() / 2.}; + const QPointF navigableCenterContainerCs = mapToItem(_containerItem, navigableCenter); + const QPointF translation{navigableCenterContainerCs - position}; const qreal zoom = _containerItem->scale(); _containerItem->setPosition(_containerItem->position() + (translation * zoom)); updateGrid(); } +void Navigable::moveTo(QPointF position) +{ + if (_containerItem == nullptr) + return; + const qreal zoom = _containerItem->scale(); + _containerItem->setPosition(-position * zoom); + updateGrid(); +} + void Navigable::fitContentInView(qreal forceWidth, qreal forceHeight) { QRectF content = _containerItem->childrenRect(); @@ -186,15 +191,15 @@ void Navigable::zoomOn(QPointF center, qreal zoom) { // Get center coordinates in container CS, it is our // zoom application point - qreal containerCenterX = center.x() - _containerItem->x(); - qreal containerCenterY = center.y() - _containerItem->y(); - qreal lastZoom = _zoom; + const qreal containerCenterX = center.x() - _containerItem->x(); + const qreal containerCenterY = center.y() - _containerItem->y(); + const qreal lastZoom = _zoom; // Don't apply modification if new zoom is not valid (probably because it is not in zoomMin, zoomMax range) - if ( isValidZoom( zoom ) ) { + if (isValidZoom(zoom)) { // Get center coordinate in container CS with the new zoom - qreal oldZoomX = containerCenterX * ( zoom / lastZoom ); - qreal oldZoomY = containerCenterY * ( zoom / lastZoom ); + qreal oldZoomX = containerCenterX * (zoom / lastZoom); + qreal oldZoomY = containerCenterY * (zoom / lastZoom); // Compute a container position correction to have a fixed "zoom // application point" @@ -202,9 +207,9 @@ void Navigable::zoomOn(QPointF center, qreal zoom) qreal zoomCorrectionY = containerCenterY - oldZoomY; // Correct container position and set the appropriate scaling - _containerItem->setX( _containerItem->x() + zoomCorrectionX ); - _containerItem->setY( _containerItem->y() + zoomCorrectionY ); - _containerItem->setScale( zoom ); + _containerItem->setX(_containerItem->x() + zoomCorrectionX); + _containerItem->setY(_containerItem->y() + zoomCorrectionY); + _containerItem->setScale(zoom); _zoom = zoom; _zoomModified = true; _panModified = true; @@ -228,7 +233,7 @@ bool Navigable::isValidZoom(qreal zoom) const void Navigable::setZoomOrigin(QQuickItem::TransformOrigin zoomOrigin) { - if ( zoomOrigin != _zoomOrigin ) { + if (zoomOrigin != _zoomOrigin) { _zoomOrigin = zoomOrigin; emit zoomOriginChanged(); } @@ -244,9 +249,9 @@ void Navigable::setZoomMax(qreal zoomMax) void Navigable::setZoomMin(qreal zoomMin) { - if ( qFuzzyCompare( 1. + zoomMin - _zoomMin, 1.0 ) ) + if (qFuzzyCompare(1. + zoomMin - _zoomMin, 1.0)) return; - if ( zoomMin < 0.01 ) + if (zoomMin < 0.01) return; _zoomMin = zoomMin; emit zoomMinChanged(); @@ -268,35 +273,35 @@ void Navigable::geometryChange(const QRectF& newGeometry, const QRectF& oldGe { if (getNavigable()) { // Apply fitContentInView if auto fitting is set to true and the user has not applyed a custom zoom or pan - if ( _autoFitMode == AutoFit && - ( !_panModified || !_zoomModified ) ) { + if (_autoFitMode == AutoFit && + (!_panModified || !_zoomModified)) { fitContentInView(); } // In AutoFit mode, try centering the content when the view is resized and the content // size is less than the view size (it is no fitting but centering...) - if ( _autoFitMode == AutoFit ) { + if (_autoFitMode == AutoFit) { bool centerWidth = false; bool centerHeight = false; // Container item children Br mapped in root CS. - QRectF contentBr = mapRectFromItem( _containerItem, _containerItem->childrenRect( ) ); - if ( newGeometry.contains( contentBr ) ) { + const QRectF contentBr = mapRectFromItem(_containerItem, _containerItem->childrenRect()); + if (newGeometry.contains(contentBr)) { centerWidth = true; centerHeight = true; } else { - if ( contentBr.top() > newGeometry.top() && - contentBr.bottom() < newGeometry.bottom() ) + if (contentBr.top() > newGeometry.top() && + contentBr.bottom() < newGeometry.bottom()) centerHeight = true; - if ( contentBr.left() > newGeometry.left() && - contentBr.right() < newGeometry.right() ) + if (contentBr.left() > newGeometry.left() && + contentBr.right() < newGeometry.right()) centerWidth = true; } if (centerWidth) { - qreal cx = ( newGeometry.width() - contentBr.width() ) / 2.; - _containerItem->setX( cx ); + const qreal cx = (newGeometry.width() - contentBr.width()) / 2.; + _containerItem->setX(cx); } if (centerHeight) { - qreal cy = ( newGeometry.height() - contentBr.height() ) / 2.; - _containerItem->setY( cy ); + const qreal cy = (newGeometry.height() - contentBr.height()) / 2.; + _containerItem->setY(cy); } } @@ -308,21 +313,21 @@ void Navigable::geometryChange(const QRectF& newGeometry, const QRectF& oldGe bool anchorLeft = false; // Container item children Br mapped in root CS. - QRectF contentBr = mapRectFromItem( _containerItem, _containerItem->childrenRect() ); - if ( contentBr.width() > newGeometry.width() && - contentBr.right() < newGeometry.right() ) { + const QRectF contentBr = mapRectFromItem(_containerItem, _containerItem->childrenRect()); + if (contentBr.width() > newGeometry.width() && + contentBr.right() < newGeometry.right()) { anchorRight = true; } - if ( contentBr.width() > newGeometry.width() && - contentBr.left() > newGeometry.left() ) { + if (contentBr.width() > newGeometry.width() && + contentBr.left() > newGeometry.left()) { anchorLeft = true; } - if ( anchorRight ) { - qreal xd = newGeometry.right() - contentBr.right(); - _containerItem->setX( _containerItem->x() + xd ); - } else if ( anchorLeft ) { // Right anchoring has priority over left anchoring... - qreal xd = newGeometry.left( ) - contentBr.left( ); - _containerItem->setX( _containerItem->x() + xd ); + if (anchorRight) { + const qreal xd = newGeometry.right() - contentBr.right(); + _containerItem->setX(_containerItem->x() + xd); + } else if (anchorLeft) { // Right anchoring has priority over left anchoring... + const qreal xd = newGeometry.left() - contentBr.left(); + _containerItem->setX(_containerItem->x() + xd); } } @@ -408,7 +413,7 @@ void Navigable::mousePressEvent(QMouseEvent* event) event->ignore(); } -void Navigable::mouseReleaseEvent( QMouseEvent* event ) +void Navigable::mouseReleaseEvent(QMouseEvent* event) { if (getNavigable()) { if (event->button() == Qt::LeftButton && @@ -479,10 +484,10 @@ void Navigable::selectionRectEnd() { } /* Grid Management *///-------------------------------------------------------- void Navigable::setGrid(qan::Grid* grid) noexcept { - if ( grid != _grid ) { - if ( _grid ) { // Hide previous grid - disconnect(_grid, nullptr, - this, nullptr); // Disconnect every update signals from grid to this navigable + if (grid != _grid) { + if (_grid) { // Hide previous grid + disconnect(_grid, nullptr, + this, nullptr); // Disconnect every update signals from grid to this navigable } _grid = grid; // Configure new grid if (_grid) { @@ -502,13 +507,13 @@ void Navigable::setGrid(qan::Grid* grid) noexcept void Navigable::updateGrid() noexcept { - if ( _grid && - _containerItem != nullptr ) { + if (_grid && + _containerItem != nullptr) { // Generate a view rect to update grid - QRectF viewRect{ _containerItem->mapFromItem(this, {0.,0.}), - _containerItem->mapFromItem(this, {width(), height()}) }; + QRectF viewRect{_containerItem->mapFromItem(this, {0.,0.}), + _containerItem->mapFromItem(this, {width(), height()})}; if (!viewRect.isEmpty()) - _grid->updateGrid(viewRect, *_containerItem, *this ); + _grid->updateGrid(viewRect, *_containerItem, *this); } } //----------------------------------------------------------------------------- diff --git a/src/qanNavigable.h b/src/qanNavigable.h index c45851d6..88f54248 100644 --- a/src/qanNavigable.h +++ b/src/qanNavigable.h @@ -156,6 +156,9 @@ Q_OBJECT //! Center the view on a given position Q_INVOKABLE void centerOnPosition(QPointF position); + //! Move to \c position (position will be be at top left corner). + Q_INVOKABLE void moveTo(QPointF position); + /*! Fit the area content (\c containerItem children) in view and update current zoom level. * * Area content will be fitted in view even if current AutoFitMode is NoAutoFit. @@ -235,7 +238,7 @@ Q_OBJECT * \note To avoid QML binding loops, this setter is protected against setting the same value multiple times. * \sa zoom */ - Q_INVOKABLE void setZoom(qreal zoom); + Q_INVOKABLE void setZoom(qreal zoom); //! Set area current zoom centered on a given \c center point. Q_INVOKABLE void zoomOn(QPointF center, qreal zoom); //! Return true if zoom is valid (ie it is different from the actual zoom and in the (minZoom, maxZoom) range. diff --git a/src/qanNavigablePreview.cpp b/src/qanNavigablePreview.cpp index da98023e..b569e109 100644 --- a/src/qanNavigablePreview.cpp +++ b/src/qanNavigablePreview.cpp @@ -32,9 +32,6 @@ // \date 2017 06 02 //----------------------------------------------------------------------------- -// Qt headers -// Nil - // QuickQanava headers #include "./qanNavigablePreview.h"