diff --git a/CMakeLists.txt b/CMakeLists.txt index 4e2ee748..3ceb2cfc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.5.0) -project(QuickQanava VERSION 0.11.0 LANGUAGES CXX) +project(QuickQanava VERSION 2.1.0 LANGUAGES CXX) set_property(GLOBAL PROPERTY USE_FOLDERS ON) add_compile_definitions(QT_DISABLE_DEPRECATED_BEFORE=0x050F00) diff --git a/QuickContainers/include/qcmAdapter.h b/QuickContainers/include/qcmAdapter.h index c51d0bf4..9d4f9735 100644 --- a/QuickContainers/include/qcmAdapter.h +++ b/QuickContainers/include/qcmAdapter.h @@ -78,7 +78,7 @@ struct adapter< QList, T > { inline static int indexOf(const QList& c, const T& t) { return c.indexOf(t); } }; -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) || defined(__clang__) template struct adapter { inline static void reserve(QVector& c, std::size_t size) { c.reserve(static_cast(size)); } diff --git a/quickqanava.pro b/quickqanava.pro index 90046b68..e44af5f5 100644 --- a/quickqanava.pro +++ b/quickqanava.pro @@ -12,17 +12,19 @@ test-style.subdir = samples/style test-dataflow.subdir = samples/dataflow test-topology.subdir = samples/topology test-cpp.subdir = samples/cpp +test-tools.subdir = samples/tools # Uncomment to activate samples projects: #SUBDIRS += test-nodes #SUBDIRS += test-edges -SUBDIRS += test-connector +#SUBDIRS += test-connector #SUBDIRS += test-groups #SUBDIRS += test-selection #SUBDIRS += test-style -SUBDIRS += test-topology +#SUBDIRS += test-topology #SUBDIRS += test-dataflow #SUBDIRS += test-cpp +SUBDIRS += test-tools # Theses ones are test projects, not sample: #SUBDIRS += test-resizer diff --git a/samples/tools/qtquickcontrols2.conf b/samples/tools/qtquickcontrols2.conf new file mode 100644 index 00000000..59f04183 --- /dev/null +++ b/samples/tools/qtquickcontrols2.conf @@ -0,0 +1,9 @@ +[Material] +Primary=#03A9F4 +Accent=#03A9F4 +Theme=Light +Variant=Dense + +[Universal] +Accent=#41cd52 +Theme=Light diff --git a/samples/tools/tools.cpp b/samples/tools/tools.cpp new file mode 100644 index 00000000..618bc39f --- /dev/null +++ b/samples/tools/tools.cpp @@ -0,0 +1,59 @@ +/* + Copyright (c) 2008-2022, 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 tools.cpp +// \author benoit@destrat.io +// \date 2022 08 10 +//----------------------------------------------------------------------------- + +// Qt headers +#include +#include +#include + +// QuickQanava headers +#include "../../src/QuickQanava.h" + +using namespace qan; + +int main(int argc, char** argv) +{ +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); +#endif + QApplication app(argc, argv); + app.setQuitOnLastWindowClosed(true); + QQuickStyle::setStyle("Material"); + QQmlApplicationEngine engine; + engine.addPluginPath(QStringLiteral("../../src")); // Necessary only for development when plugin is not installed to QTDIR/qml + QuickQanava::initialize(&engine); + engine.load(QUrl("qrc:/tools.qml")); + return app.exec(); +} + diff --git a/samples/tools/tools.pro b/samples/tools/tools.pro new file mode 100644 index 00000000..6314d513 --- /dev/null +++ b/samples/tools/tools.pro @@ -0,0 +1,14 @@ +TEMPLATE = app +TARGET = test-tools +CONFIG += qt warn_on thread c++14 +QT += widgets core gui qml quick quickcontrols2 + +include(../../src/quickqanava.pri) + +RESOURCES += ./tools.qrc + +SOURCES += ./tools.cpp + +HEADERS += ./tools.qml + +OTHER_FILES += tools.qml diff --git a/samples/tools/tools.qml b/samples/tools/tools.qml new file mode 100644 index 00000000..fd0ddb94 --- /dev/null +++ b/samples/tools/tools.qml @@ -0,0 +1,196 @@ +/* + Copyright (c) 2008-2022, 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.Window 2.2 +import QtQuick 2.13 + +import QtQuick.Controls 2.13 + +import QtQuick.Layouts 1.3 +import QtQuick.Controls.Material 2.1 +import QtQuick.Shapes 1.0 + +import QuickQanava 2.0 as Qan +import "qrc:/QuickQanava" as Qan + +ApplicationWindow { + id: window + visible: true + width: 1280 + height: 720 // MPEG - 2 HD 720p - 1280 x 720 16:9 + title: "Tools test" + Pane { + anchors.fill: parent + padding: 0 + } + + function centerItem(item) { + if (!item || !window.contentItem) + return + var windowCenter = Qt.point( + (window.contentItem.width - item.width) / 2., + (window.contentItem.height - item.height) / 2.) + var graphNodeCenter = window.contentItem.mapToItem( + graphView.containerItem, windowCenter.x, windowCenter.y) + item.x = graphNodeCenter.x + item.y = graphNodeCenter.y + } + + Qan.GraphView { + id: graphView + anchors.fill: parent + graph: topology + navigable: true + resizeHandlerColor: Material.accent + gridThickColor: Material.theme === Material.Dark ? "#4e4e4e" : "#c1c1c1" + + Qan.Graph { + id: topology + objectName: "graph" + anchors.fill: parent + clip: true + connectorEnabled: true + selectionColor: Material.accent + connectorColor: Material.accent + connectorEdgeColor: Material.accent + onConnectorEdgeInserted: edge => { + //if (edge) + // edge.label = "My edge" + } + property Component faceNodeComponent: Qt.createComponent("qrc:/FaceNode.qml") + + Component.onCompleted: { + var n1 = topology.insertNode() + n1.label = "n1" + + var n2 = topology.insertNode() + n2.label = "n2" + n2.item.x = 150 + n2.item.y = 55 + + graphView.centerOnPosition(Qt.point(0, 0)); + } + } // Qan.Graph: graph + } + + Qan.GraphPreview { + id: graphPreview + source: graphView + viewWindowColor: Material.accent + anchors.right: graphView.right + anchors.bottom: graphView.bottom + anchors.rightMargin: 8 + anchors.bottomMargin: 8 + width: previewMenu.mediumPreview.width + height: previewMenu.mediumPreview.height + Menu { + id: previewMenu + readonly property size smallPreview: Qt.size(150, 85) + readonly property size mediumPreview: Qt.size(250, 141) + readonly property size largePreview: Qt.size(350, 198) + MenuItem { + text: "Hide preview" + onTriggered: graphPreview.visible = false + } + MenuSeparator { } + MenuItem { + text: qsTr('Small') + checkable: true + checked: graphPreview.width === previewMenu.smallPreview.width && + graphPreview.height === previewMenu.smallPreview.height + onTriggered: { + graphPreview.width = previewMenu.smallPreview.width + graphPreview.height = previewMenu.smallPreview.height + } + } + MenuItem { + text: qsTr('Medium') + checkable: true + checked: graphPreview.width === previewMenu.mediumPreview.width && + graphPreview.height === previewMenu.mediumPreview.height + onTriggered: { + graphPreview.width = previewMenu.mediumPreview.width + graphPreview.height = previewMenu.mediumPreview.height + } + } + MenuItem { + text: qsTr('Large') + checkable: true + checked: graphPreview.width === previewMenu.largePreview.width && + graphPreview.height === previewMenu.largePreview.height + onTriggered: { + graphPreview.width = previewMenu.largePreview.width + graphPreview.height = previewMenu.largePreview.height + } + } + } + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.RightButton + onClicked: previewMenu.open(mouse.x, mouse.y) + } + } + + Qan.HeatMapPreview { + id: heatMapPreview + anchors.left: graphView.left + anchors.bottom: graphView.bottom + source: graphView + viewWindowColor: Material.accent + Menu { + id: heatMapMenu + MenuItem { + text: qsTr("Clear heat map") + onClicked: heatMapPreview.clearHeatMap() + } + MenuItem { + text: qsTr("Increase preview size") + onTriggered: { + heatMapPreview.width *= 1.15 + heatMapPreview.height *= 1.15 + } + } + MenuItem { + text: qsTr("Decrease preview size") + onTriggered: { + heatMapPreview.width *= Math.max(50, heatMapPreview.width * 0.85) + heatMapPreview.height *= Math.max(50, heatMapPreview.height * 0.85) + } + } + } + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.RightButton; preventStealing: true + onClicked: { + if (mouse.button === Qt.RightButton) { + heatMapMenu.x = mouse.x + heatMapMenu.y = mouse.y + heatMapMenu.open() + } + } + } + } // Qan.HeatMapPreview +} // ApplicationWindow diff --git a/samples/tools/tools.qrc b/samples/tools/tools.qrc new file mode 100644 index 00000000..bb4ff266 --- /dev/null +++ b/samples/tools/tools.qrc @@ -0,0 +1,6 @@ + + + tools.qml + qtquickcontrols2.conf + + diff --git a/samples/topology/EdgesListView.qml b/samples/topology/EdgesListView.qml index 9e3fd2a9..8c6e2ea1 100644 --- a/samples/topology/EdgesListView.qml +++ b/samples/topology/EdgesListView.qml @@ -1,5 +1,5 @@ /* - Copyright (c) 2020, Benoit AUTHEMAN All rights reserved. + Copyright (c) 2008-2022, 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: diff --git a/samples/topology/NodesListView.qml b/samples/topology/NodesListView.qml index 40e9025c..87629a69 100644 --- a/samples/topology/NodesListView.qml +++ b/samples/topology/NodesListView.qml @@ -1,5 +1,5 @@ /* - Copyright (c) 2020, Benoit AUTHEMAN All rights reserved. + Copyright (c) 2008-2022, 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: diff --git a/samples/topology/ScreenshotPopup.qml b/samples/topology/ScreenshotPopup.qml index 857767b7..b939e374 100644 --- a/samples/topology/ScreenshotPopup.qml +++ b/samples/topology/ScreenshotPopup.qml @@ -1,5 +1,5 @@ /* - Copyright (c) 2020, Benoit AUTHEMAN All rights reserved. + Copyright (c) 2008-2022, 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: diff --git a/samples/topology/qanFaceNode.cpp b/samples/topology/qanFaceNode.cpp index 32c9d39c..ad8af1b1 100644 --- a/samples/topology/qanFaceNode.cpp +++ b/samples/topology/qanFaceNode.cpp @@ -1,5 +1,5 @@ /* - Copyright (c) 2020, Benoit AUTHEMAN All rights reserved. + Copyright (c) 2008-2022, 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: diff --git a/samples/topology/qanFaceNode.h b/samples/topology/qanFaceNode.h index e39b67a8..07f9eb99 100644 --- a/samples/topology/qanFaceNode.h +++ b/samples/topology/qanFaceNode.h @@ -1,5 +1,5 @@ /* - Copyright (c) 2020, Benoit AUTHEMAN All rights reserved. + Copyright (c) 2008-2022, 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: diff --git a/samples/topology/topology.cpp b/samples/topology/topology.cpp index c04fcfcf..23896f77 100644 --- a/samples/topology/topology.cpp +++ b/samples/topology/topology.cpp @@ -1,5 +1,5 @@ /* - Copyright (c) 2020, Benoit AUTHEMAN All rights reserved. + Copyright (c) 2008-2022, 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: diff --git a/samples/topology/topology.qml b/samples/topology/topology.qml index 1b119ce2..96fbd3d1 100644 --- a/samples/topology/topology.qml +++ b/samples/topology/topology.qml @@ -1,5 +1,5 @@ /* - Copyright (c) 2020, Benoit AUTHEMAN All rights reserved. + Copyright (c) 2008-2022, 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: diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 58472233..7fb73067 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -22,6 +22,7 @@ set(qan_source_files qanSelectable.cpp qanStyle.cpp qanStyleManager.cpp + qanAnalysisTimeHeatMap.cpp qanUtils.cpp ) @@ -49,6 +50,7 @@ set (qan_header_files qanSelectable.h qanStyle.h qanStyleManager.h + qanAnalysisTimeHeatMap.cpp qanUtils.h QuickQanava.h gtpo/container_adapter.h diff --git a/src/GraphPreview.qml b/src/GraphPreview.qml index 1333125a..ce36046a 100644 --- a/src/GraphPreview.qml +++ b/src/GraphPreview.qml @@ -23,9 +23,9 @@ (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 2.7 import QtQuick.Controls 2.13 -//import QtGraphicalEffects 1.0 import QuickQanava 2.0 as Qan import "qrc:/QuickQanava" as Qan @@ -40,37 +40,44 @@ Control { width: 200 height: 113 - property alias source: navigablePreview.source - property alias visibleWindowColor: navigablePreview.visibleWindowColor + //! Source Qan.GraphView that should be previewed. + property var source: undefined + + property alias viewWindowColor: navigablePreview.viewWindowColor // Preview background panel opacity (default to 0.9). - property alias previewOpactity: previewBackground.opacity + property alias previewOpactity: previewBackground.opacity - // PUBLIC ///////////////////////////////////////////////////////////////// + //! Initial (and minimum) scene rect (should usually fit your initial screen size). + property alias initialRect: navigablePreview.initialRect + + // PRIVATE //////////////////////////////////////////////////////////////// padding: 0 property real previewSize: 0.15 - property real graphRatio: graphView.containerItem.childrenRect.width / graphView.containerItem.childrenRect.height - property real previewRatio: graphView.width / graphView.height + property real graphRatio: source ? (source.containerItem.childrenRect.width / + source.containerItem.childrenRect.height) : + 1. + property real previewRatio: source ? (source.width / source.height) : 1.0 onGraphRatioChanged: updateNavigablePreviewSize() onPreviewRatioChanged: updateNavigablePreviewSize() + onSourceChanged: updateNavigablePreviewSize() function updateNavigablePreviewSize() { + // Update the navigable preview width/height such that it's aspect ratio + // is correct but fit in this graph preview size. // Algorithm: // 1. Compute navigable preview height (nph) given graph width (gw) and graphRatio. // 2. If navigable preview height (nph) < preview height (ph), then use graphRatio to // generate nph. // 3. Else compute navigable preview width using previewRatio and fix nph to ph. - + if (!source) + return const pw = graphPreview.width const ph = graphPreview.height - const gw = graphView.containerItem.childrenRect.width - const gh = graphView.containerItem.childrenRect.height - //console.error('') - //console.error('graphRatio=' + graphRatio + ' previewRatio=' + previewRatio) - //console.error('pw=' + pw + ' ph=' + ph) - //console.error('gw=' + gw + ' gh=' + gh) + const gw = source.containerItem.childrenRect.width + const gh = source.containerItem.childrenRect.height // 1. let sw = pw / gw @@ -95,8 +102,8 @@ Control { } opacity: 0.8 - hoverEnabled: true; ToolTip.visible: hovered; ToolTip.delay: 1500 - ToolTip.text: qsTr("Show parts of image that have actually been viewed with more than 100% zoom") + hoverEnabled: true + z: 3 // Avoid tooltips beeing generated on top of preview Qan.RectangularGlow { anchors.fill: parent @@ -115,12 +122,14 @@ Control { Label { x: 4 y: 2 - text: (graphView.zoom * 100).toFixed(1) + "%" + text: source ? ((source.zoom * 100).toFixed(1) + "%") : + '' font.pixelSize: 11 } Qan.NavigablePreview { id: navigablePreview anchors.centerIn: parent + source: graphPreview.source } // Qan.NavigablePreview } } // Control graph preview diff --git a/src/HeatMapPreview.qml b/src/HeatMapPreview.qml new file mode 100644 index 00000000..c2fb1c35 --- /dev/null +++ b/src/HeatMapPreview.qml @@ -0,0 +1,146 @@ +/* + Copyright (c) 2008-2022, 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 2.7 +import QtQuick.Controls 2.13 + +import QuickQanava 2.0 as Qan +import "qrc:/QuickQanava" as Qan + +/*! \brief Visual graph preview. + * + */ +Control { + id: heatMapPreview + + // PUBLIC ///////////////////////////////////////////////////////////////// + width: 200 + height: 113 + + //! Source Qan.GraphView that should be previewed. + property var source: undefined + + property alias viewWindowColor: navigablePreview.viewWindowColor + + //! Preview background panel opacity (default to 0.9). + property alias previewOpactity: previewBackground.opacity + + //! Initial (and minimum) scene rect (should usually fit your initial screen size). + property alias initialRect: navigablePreview.initialRect + + //! Clear actual heat map. + function clearHeatMap() { analysisTimeHeatMap.clearHeatMap() } + + // PRIVATE //////////////////////////////////////////////////////////////// + padding: 0 + + property real previewSize: 0.15 + property real graphRatio: source ? (source.containerItem.childrenRect.width / + source.containerItem.childrenRect.height) : + 1. + property real previewRatio: source ? (source.width / source.height) : 1.0 + onGraphRatioChanged: updateNavigablePreviewSize() + onPreviewRatioChanged: updateNavigablePreviewSize() + onSourceChanged: updateNavigablePreviewSize() + + function updateNavigablePreviewSize() { + // Update the navigable preview width/height such that it's aspect ratio + // is correct but fit in this graph preview size. + // Algorithm: + // 1. Compute navigable preview height (nph) given graph width (gw) and graphRatio. + // 2. If navigable preview height (nph) < preview height (ph), then use graphRatio to + // generate nph. + // 3. Else compute navigable preview width using previewRatio and fix nph to ph. + if (!source) + return + const pw = heatMapPreview.width + const ph = heatMapPreview.height + + const gw = source.containerItem.childrenRect.width + const gh = source.containerItem.childrenRect.height + + // 1. + let sw = pw / gw + let sh = ph / gh + let npw = 0. + let nph = pw * graphRatio + if (nph < ph) { + // 2. + npw = pw + } else { + // 3. + nph = ph + npw = ph * graphRatio + } + // If npw is larger than actual preview width (pw), scale nph + if (npw > pw) + nph = nph * (pw / npw) + + // Secure with boundary Check + navigablePreview.width = Math.min(npw, pw) + navigablePreview.height = Math.min(nph, ph) + } + + opacity: 0.8 + hoverEnabled: true; ToolTip.visible: hovered; ToolTip.delay: 1500 + ToolTip.text: qsTr("Show parts of image that have actually been viewed with more than 100% zoom") + z: 3 // Avoid tooltips beeing generated on top of preview + Qan.RectangularGlow { + anchors.fill: parent + cached: true + glowRadius: 8 + cornerRadius: 8 + spread: 0.5 + color: "lightgrey" + } + Pane { + id: previewBackground + anchors.fill: parent + opacity: 0.9 + padding: 1 + clip: true + Label { + x: 4 + y: 2 + text: source ? ((source.zoom * 100).toFixed(1) + "%") : + '' + font.pixelSize: 11 + } + Qan.NavigablePreview { + id: navigablePreview + anchors.centerIn: parent + source: heatMapPreview.source + + Qan.AnalysisTimeHeatMap { + id: analysisTimeHeatMap + parent: navigablePreview.overlay + anchors.fill: parent + source: navigablePreview + visible: true + } + } // Qan.NavigablePreview + } +} // Control graph preview diff --git a/src/NavigablePreview.qml b/src/NavigablePreview.qml index c3b46a83..2dba8ad6 100644 --- a/src/NavigablePreview.qml +++ b/src/NavigablePreview.qml @@ -38,46 +38,36 @@ Qan.AbstractNavigablePreview { // PUBLIC ///////////////////////////////////////////////////////////////// //! Overlay item could be used to display a user defined item (for example an heat map image) between the background and the current visible window rectangle. - property var overlay : overlayItem + property var overlay: overlayItem //! Color for the visible window rect border (default to red). - property color visibleWindowColor: Qt.rgba(1, 0, 0, 1) + property color viewWindowColor: Qt.rgba(1, 0, 0, 1) //! Show or hide the target navigable content as a background image (default to true). property alias backgroundPreviewVisible: sourcePreview.visible - // PRIVATE //////////////////////////////////////////////////////////////// - function updatePreviewSourceRect(rect) { - if (!source) - return - if (preview.source && // Manually update shader effect source source rect - preview.source.containerItem && - sourcePreview.sourceItem === preview.source.containerItem ) { - var cr = preview.source.containerItem.childrenRect - if (cr.width > 0 && cr.height > 0) - sourcePreview.sourceRect = cr - } - } + //! Initial (and minimum) scene rect (should usually fit your initial screen size). + property rect initialRect: Qt.rect(-1280 / 2., -720 / 2., + 1280, 720) + // PRIVATE //////////////////////////////////////////////////////////////// onSourceChanged: { if (source && source.containerItem) { resetVisibleWindow() // Monitor source changes - source.containerItem.onWidthChanged.connect(updateVisibleWindow) - source.containerItem.onHeightChanged.connect(updateVisibleWindow) - source.containerItem.onScaleChanged.connect(updateVisibleWindow) - source.containerItem.onXChanged.connect(updateVisibleWindow) - source.containerItem.onYChanged.connect(updateVisibleWindow) - source.containerItem.onChildrenRectChanged.connect(updatePreviewSourceRect) + 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) + source.containerItem.onChildrenRectChanged.connect(updatePreview) sourcePreview.sourceItem = source.containerItem - var cr = preview.source.containerItem.childrenRect - if (cr.width > 0 && cr.height > 0) - sourcePreview.sourceRect = cr } else sourcePreview.sourceItem = undefined + updatePreview() updateVisibleWindow() } @@ -85,22 +75,51 @@ Qan.AbstractNavigablePreview { id: sourcePreview anchors.fill: parent anchors.margins: 0 - live: true; recursive: false - sourceItem: source.containerItem + live: true + recursive: false textureSize: Qt.size(width, height) } - // Reset visibleWindow rect to preview dimension (taking rectangle border into account) + function updatePreview() { + if (!source) + return + const r = computeSourceRect() + if (r && + r.width > 0. && + r.height > 0) { + sourcePreview.sourceRect = r + viewWindow.visible = true + updateVisibleWindow(r) + } else + viewWindow.visible = false + } + + function computeSourceRect(rect) { + if (!source) + return undefined + if (!preview.source || + !preview.source.containerItem || + sourcePreview.sourceItem !== preview.source.containerItem) + return undefined + + // Scene rect is union of initial rect and children rect. + let cr = preview.source.containerItem.childrenRect + let r = preview.rectUnion(cr, preview.initialRect) + return r + } + + // Reset viewWindow rect to preview dimension (taking rectangle border into account) function resetVisibleWindow() { - const border = visibleWindow.border.width - const border2 = visibleWindow.border.width * 2 - visibleWindow.x = border - visibleWindow.y = border - visibleWindow.width = preview.width - border2 - visibleWindow.height = preview.height - border2 + const border = viewWindow.border.width + const border2 = border * 2 + viewWindow.x = border + viewWindow.y = border + viewWindow.width = preview.width - border2 + viewWindow.height = preview.height - border2 } - function updateVisibleWindow() { + function updateVisibleWindow(r) { + // r is previewed rect in source.containerItem Cs if (!preview) return if (!source) { // Reset the window when source is invalid @@ -112,44 +131,57 @@ Qan.AbstractNavigablePreview { preview.resetVisibleWindow() return } - var containerItemCr = containerItem.childrenRect - if (containerItemCr.width < preview.source.width && // If scene size is stricly inferior to preview size - containerItemCr.height < preview.source.height) { // reset the preview window - //preview.resetVisibleWindow() - //return - containerItemCr.width = preview.source.width - containerItemCr.height = preview.source.height - } - if (containerItemCr.width < 0.01 || // Do not update without a valid children rect - containerItemCr.height < 0.01) { - preview.resetVisibleWindow() + if (!r) return - } - if (containerItemCr.width < containerItemCr.width || // Reset the visible window is the whole containerItem content - containerItemCr.height < containerItemCr.height) { // is smaller than graph view - preview.resetVisibleWindow() - return - } - if (containerItemCr.width < preview.width && // If scene size is stricly inferior to preview size - containerItemCr.height < preview.height) { // reset the preview window - preview.resetVisibleWindow() - return - } - const border = visibleWindow.border.width - const border2 = border * 2. - var windowTopLeft = source.mapToItem(containerItem, 0, 0) - var windowBottomRight = source.mapToItem(containerItem, source.width, source.height) - var previewXRatio = preview.width / containerItemCr.width - var previewYRatio = preview.height / containerItemCr.height - - visibleWindow.x = previewXRatio * (windowTopLeft.x - containerItemCr.x) + border - visibleWindow.y = previewYRatio * (windowTopLeft.y - containerItemCr.y) + border - visibleWindow.width = previewXRatio * Math.abs(windowBottomRight.x - windowTopLeft.x) - border2 - visibleWindow.height = previewYRatio * Math.abs(windowBottomRight.y - windowTopLeft.y) - border2 + //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 + // viewR is window rect in content rect Cs + // Window is viewR in preview Cs + + const border = viewWindow.border.width + const border2 = border * 2. - visibleWindowChanged(Qt.rect(visibleWindow.x / preview.width, visibleWindow.y / preview.height, - visibleWindow.width / preview.width, visibleWindow.height / preview.height), + // map r to preview + // map viewR to preview + // Apply scaling from r to preview + const viewR = preview.source.mapToItem(preview.source.containerItem, + Qt.rect(0, 0, + preview.source.width, + preview.source.height)) + var previewXRatio = preview.width / r.width + var previewYRatio = preview.height / r.height + + 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 + + visibleWindowChanged(Qt.rect(viewWindow.x / preview.width, + viewWindow.y / preview.height, + viewWindow.width / preview.width, + viewWindow.height / preview.height), source.zoom); } Item { @@ -157,26 +189,27 @@ Qan.AbstractNavigablePreview { anchors.fill: parent; anchors.margins: 0 } Rectangle { - id: visibleWindow + id: viewWindow color: Qt.rgba(0, 0, 0, 0) - smooth: true; antialiasing: true - border.color: visibleWindowColor; + smooth: true + antialiasing: true + border.color: viewWindowColor border.width: 2 } // Not active on 20201027 /*MouseArea { - id: visibleWindowDragger + id: viewWindowDragger anchors.fill: parent drag.onActiveChanged: { - console.debug("dragging to:" + visibleWindow.x + ":" + visibleWindow.y ); + console.debug("dragging to:" + viewWindow.x + ":" + viewWindow.y ); if ( source ) { } } - drag.target: visibleWindow + 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 - visibleWindow.width) - drag.minimumY: 0; drag.maximumY: Math.max(0, preview.height - visibleWindow.height) + 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 enabled: true @@ -185,5 +218,4 @@ Qan.AbstractNavigablePreview { onPressed : { } }*/ -} - +} // Qan.AbstractNavigablePreview: preview diff --git a/src/QuickQanava.h b/src/QuickQanava.h index ef2371a3..b408150b 100755 --- a/src/QuickQanava.h +++ b/src/QuickQanava.h @@ -58,6 +58,7 @@ #include "./qanStyleManager.h" #include "./qanBottomRightResizer.h" #include "./qanNavigablePreview.h" +#include "./qanAnalysisTimeHeatMap.h" struct QuickQanava { static void initialize(QQmlEngine* engine) { @@ -88,6 +89,7 @@ struct QuickQanava { qmlRegisterType("QuickQanava", 2, 0, "AbstractGraphView"); qmlRegisterType("QuickQanava", 2, 0, "Navigable"); qmlRegisterType("QuickQanava", 2, 0, "AbstractNavigablePreview"); + qmlRegisterType("QuickQanava", 2, 0, "AnalysisTimeHeatMap"); qmlRegisterType("QuickQanava", 2, 0, "AbstractGrid"); qmlRegisterType("QuickQanava", 2, 0, "OrthoGrid"); diff --git a/src/QuickQanava_plugin.qrc b/src/QuickQanava_plugin.qrc index 738d7dae..26e83fc7 100644 --- a/src/QuickQanava_plugin.qrc +++ b/src/QuickQanava_plugin.qrc @@ -5,6 +5,8 @@ Edge.qml EdgeTemplate.qml GraphView.qml + GraphPreview.qml + HeatMapPreview.qml Node.qml Port.qml VerticalDock.qml @@ -23,6 +25,7 @@ StyleListView.qml VisualConnector.qml LabelEditor.qml + HeatMapPreview.qml qmldir_plugin diff --git a/src/QuickQanava_static.qrc b/src/QuickQanava_static.qrc index dc7af533..ee363f7f 100644 --- a/src/QuickQanava_static.qrc +++ b/src/QuickQanava_static.qrc @@ -2,6 +2,7 @@ NavigablePreview.qml GraphPreview.qml + HeatMapPreview.qml LineGrid.qml Edge.qml EdgeTemplate.qml @@ -26,6 +27,7 @@ StyleListView.qml VisualConnector.qml LabelEditor.qml + HeatMapPreview.qml qmldir_static diff --git a/src/qanAbstractDraggableCtrl.h b/src/qanAbstractDraggableCtrl.h index 9b02a45c..1c3c1e36 100644 --- a/src/qanAbstractDraggableCtrl.h +++ b/src/qanAbstractDraggableCtrl.h @@ -32,15 +32,11 @@ // \date 2017 06 30 //----------------------------------------------------------------------------- -#ifndef qanAbstractDraggableCtrl_h -#define qanAbstractDraggableCtrl_h +#pragma once // Qt headers #include -// QuickQanava headers -// Nil - namespace qan { // ::qan /*! \brief Generic logic for dragging either qan::Node or qan::Group visual items. @@ -53,12 +49,10 @@ class AbstractDraggableCtrl virtual ~AbstractDraggableCtrl() = default; //! \c dragInitialMousePos in window coordinate system. - virtual void beginDragMove( const QPointF& dragInitialMousePos, bool dragSelection = true ) = 0; + virtual void beginDragMove(const QPointF& dragInitialMousePos, bool dragSelection = true) = 0; //! \c delta in scene coordinate system. - virtual void dragMove( const QPointF& delta, bool dragSelection = true ) = 0; - virtual void endDragMove( bool dragSelection = true ) = 0; + virtual void dragMove(const QPointF& delta, bool dragSelection = true) = 0; + virtual void endDragMove(bool dragSelection = true) = 0; }; } // ::qan - -#endif // qanAbstractDraggableCtrl_h diff --git a/src/qanAnalysisTimeHeatMap.cpp b/src/qanAnalysisTimeHeatMap.cpp new file mode 100755 index 00000000..6700b249 --- /dev/null +++ b/src/qanAnalysisTimeHeatMap.cpp @@ -0,0 +1,191 @@ +/* + Copyright (c) 2008-2022, 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. +*/ + +//----------------------------------------------------------------------------- +// \file qanAnalysisTimeHeatMap.cpp +// \author benoit@destrat.io +// \date 2017 06 04 +//----------------------------------------------------------------------------- + +// Qt headers +#include + +// Topoi++ headers +#include "./qanAnalysisTimeHeatMap.h" + +namespace qan { // ::qan + +/* AnalysisTimeHeatMap Object Management *///---------------------------------- +AnalysisTimeHeatMap::AnalysisTimeHeatMap(QQuickItem* parent) : + QQuickPaintedItem{parent}, + _image{QImage{{0,0}, QImage::Format_ARGB32_Premultiplied}} +{ +} + +void AnalysisTimeHeatMap::paint(QPainter* painter) +{ + if (!_image.isNull()) { + // Algorithm: + // 1. Compute source image ratio: image.height = imageRatio * image.width + // 2. Compute xScale ratio such that: item.width = xScale * image.width + // 3. Compute candidate height in item CS with xScale + // 3.1 If it is < item.height, center image vertically + // 3.2 Otherwise, compute yScale, center image horizontally + + QRectF drawRect{}; + QSizeF imageSize{static_cast(_image.width()), + static_cast(_image.height())}; // Copy image size to FP + + // 1. + qreal imageRatio = imageSize.height() / imageSize.width(); + + // 2. + qreal xScale = width() / imageSize.width(); + + // 3. + const auto candidateHeight = imageSize.height() * xScale; + if (candidateHeight > height()) { // Avoid an <= FP test + // 3.2 + const auto scaledWidth = height() / imageRatio; + drawRect.setTopLeft({ (width() - scaledWidth) / 2., 0. }); + drawRect.setSize({ scaledWidth, height() }); + } else { + // 3.1 + drawRect.setTopLeft({0., (height() - candidateHeight) / 2. }); // Center height + drawRect.setSize({ width(), candidateHeight }); + } + painter->drawImage(drawRect, _image); + } +} + +void AnalysisTimeHeatMap::setImage(QImage image) noexcept +{ + _image = image; + emit imageChanged(); + update(); +} +//----------------------------------------------------------------------------- + +/* Heatmap Generation Management *///------------------------------------------ +void AnalysisTimeHeatMap::setSource(qan::NavigablePreview* source) noexcept +{ + if (source != _source) { + if (_source) // Disconnect previous source from this heatmap generator + disconnect(_source.data(), 0, this, 0); + _source = source; + if (_source) + connect(_source.data(), &qan::NavigablePreview::visibleWindowChanged, + this, &AnalysisTimeHeatMap::onVisibleWindowChanged); + emit sourceChanged(); + } +} + +void AnalysisTimeHeatMap::setColor(QColor color) noexcept +{ + if (color != _color) { + _color = color; + // Update background image color (do not change alpha channel) + QColor c{color}; + auto& heatMap = getImage(); + for (int x = 0; x < heatMap.width(); x++) + for (int y = 0; y < heatMap.width(); x++) { + c.setAlpha(heatMap.pixelColor(x, y).alpha()); // Preserve original alpha value + heatMap.setPixelColor(x, y, c); + } + emit colorChanged(); + update(); + } +} + +void AnalysisTimeHeatMap::onVisibleWindowChanged(QRectF visibleWindowRect, qreal navigableZoom) +{ + if (!isVisible()) + return; + if (navigableZoom > 1.0001 && + visibleWindowRect.isValid()) { + auto& heatMap = getImage(); + const QSizeF heatMapSize{static_cast(heatMap.width()), + static_cast(heatMap.height())}; + QRect scaledVisibleWindowRect{QRectF{ visibleWindowRect.x() * heatMapSize.width(), + visibleWindowRect.y() * heatMapSize.height(), + visibleWindowRect.width() * heatMapSize.width(), + visibleWindowRect.height() * heatMapSize.height() }.toRect()}; + const auto heatMapRect = QRect{{0,0}, heatMap.size()}; + //qDebug() << "\tvisibleWindowRect=" << visibleWindowRect; + //qDebug() << "\tscaledVisibleWindowRect=" << scaledVisibleWindowRect; + //qDebug() << "\theatMapSize=" << heatMapSize; + + QColor c{_color}; + if (heatMapRect.intersects(scaledVisibleWindowRect)) { // Update pixels under visibleWindowRect + const auto updateRect = heatMapRect.intersected(scaledVisibleWindowRect); + for (int x = updateRect.left(); x < updateRect.right(); x++) + for (int y = updateRect.top(); y < updateRect.bottom(); y++) { + const auto alpha = heatMap.pixelColor(x,y).alpha(); + c.setAlpha(qMax(25, qMin(255, alpha+1))); + heatMap.setPixelColor(x, y, c); + } + update(); + } + } +} + +void AnalysisTimeHeatMap::clearHeatMap() noexcept +{ + auto& heatMap = getImage(); + QColor c{_color}; c.setAlpha(0); + QImage clearedHeatMap{heatMap}; + clearedHeatMap.fill(c); + heatMap = clearedHeatMap; + _maximumAnalysisDuration = -1; + update(); +} + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +void AnalysisTimeHeatMap::geometryChanged(const QRectF& newGeometry, const QRectF& oldGeometry) +#else +void AnalysisTimeHeatMap::geometryChange(const QRectF& newGeometry, const QRectF& oldGeometry) +#endif +{ + if (!newGeometry.size().toSize().isEmpty() && + newGeometry.toRect() != oldGeometry.toRect()) { + auto& heatMap = getImage(); + if (heatMap.isNull()) { + heatMap = QImage{newGeometry.size().toSize(), QImage::Format_ARGB32_Premultiplied}; + QColor c{_color}; c.setAlpha(0); + heatMap.fill(c); + } else { + if (newGeometry.size().width() > oldGeometry.size().width() || + newGeometry.size().height() > oldGeometry.size().height()) { + QImage scaled = heatMap.scaled(newGeometry.size().toSize()); // It should detach... + setImage(scaled); + } // Otherwise, do not reduce image size, it will be scaled at display + } + update(); + } +} +//----------------------------------------------------------------------------- + +} // ::qan diff --git a/src/qanAnalysisTimeHeatMap.h b/src/qanAnalysisTimeHeatMap.h new file mode 100755 index 00000000..0fddfa5c --- /dev/null +++ b/src/qanAnalysisTimeHeatMap.h @@ -0,0 +1,141 @@ +/* + Copyright (c) 2008-2022, 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. +*/ + +//----------------------------------------------------------------------------- +// \file qanAnalysisTimeHeatMap.cpp +// \author benoit@destrat.io +// \date 2017 06 04 (refactored / opensourced 2022 08 10) +//----------------------------------------------------------------------------- + +#pragma once + +// Qt headers +#include +#include + +// QuickQanava headers +#include "./qanNavigablePreview.h" + +namespace qan { // ::qan + +/*! \brief Draw an heat map from a qan::NavigablePreview, highlight parts of the image where the analyst has spent the more viewing time. + * + * The longer an analyst is viewing part of navigable, the hottest the heatmap will be at that location. Only + * zoom > 100% will be taken into account for heat generation, viewing a navigable at lower zoom will not affect + * heatmap, a navigable part is not considered analyzed until it has been viewed at full resolution. + * + * Call resetHeatMap() to remove all heat informations and clear the maximum analysis duration. + * + * \note AnalysisTimeHeatMap can be resized on the fly, resolution of the heatmap is mapped to item size. + * + * Internals: Analysis time heat map catch \c source qan::NavigablePreview::visibleWindowChanged() signal and + * draw more heat on the image part covered by actual visible window. + * + * \nosubgrouping + */ +class AnalysisTimeHeatMap : public QQuickPaintedItem +{ + /*! \name AnalysisTimeHeatMap Object Management *///----------------------- + //@{ + Q_OBJECT +public: + explicit AnalysisTimeHeatMap(QQuickItem* parent = nullptr); + virtual ~AnalysisTimeHeatMap() = default; + AnalysisTimeHeatMap(const AnalysisTimeHeatMap&) = delete; + +public: + virtual void paint(QPainter* painter) override; + +public: + Q_PROPERTY(QImage image READ getImage WRITE setImage NOTIFY imageChanged) + inline const QImage& getImage() const noexcept { return _image; } + inline QImage& getImage() noexcept { return _image; } + void setImage(QImage image) noexcept; +private: + QImage _image; +signals: + void imageChanged(); + //@} + //------------------------------------------------------------------------- + + /*! \name Heatmap Generation Management *///------------------------------- + //@{ +public: + /*! \brief Source qan::NavigablePreview, source qan::NavigablePreview::visibleWindowChanged() signal is caught. + * + * \warning Can be nullptr. + */ + Q_PROPERTY(qan::NavigablePreview* source READ getSource WRITE setSource NOTIFY sourceChanged FINAL) + //! \copydoc source + inline qan::NavigablePreview* getSource() const noexcept { return _source.data(); } + //! \copydoc source + void setSource(qan::NavigablePreview* source) noexcept; +private: + //! \copydoc source + QPointer _source; +signals: + //! \copydoc source + void sourceChanged(); + +public: + //! Color used to highlight analyzed areas (alpha component is ignored, default to green). + Q_PROPERTY( QColor color READ getColor WRITE setColor NOTIFY colorChanged FINAL ) + //! \copydoc color + inline QColor getColor() const noexcept { return _color; } + //! \warning Changing color dynamically is quite costly (0(width*height)). + void setColor(QColor color) noexcept; +private: + //! \copydoc color + QColor _color{0,255,0}; +signals: + //! \copydoc color + void colorChanged(); + +protected slots: + //! Update heatmap when \c source qan::NavigablePreview::visibleWindowChanged() signal is emitted. + void onVisibleWindowChanged(QRectF visibleWindowRect, qreal navigableZoom); + + //! Clear all heatmap content (also clear maximum analysis duration information). + Q_INVOKABLE void clearHeatMap() noexcept; + +private: + //! Maximum analysis time on a heatmap pixel in ms. + qint64 _maximumAnalysisDuration = -1; + +protected: +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + //! Resize the internal heatmap image. + virtual void geometryChanged(const QRectF& newGeometry, const QRectF& oldGeometry) override; +#else + virtual void geometryChange(const QRectF& newGeometry, const QRectF& oldGeometry) override; +#endif + //@} + //------------------------------------------------------------------------- +}; + +} // ::qan + +QML_DECLARE_TYPE(qan::AnalysisTimeHeatMap) diff --git a/src/qanBottomRightResizer.cpp b/src/qanBottomRightResizer.cpp index 7534fed0..66b90748 100644 --- a/src/qanBottomRightResizer.cpp +++ b/src/qanBottomRightResizer.cpp @@ -351,10 +351,15 @@ bool BottomRightResizer::eventFilter(QObject *item, QEvent *event) break; } case QEvent::MouseMove: { - QMouseEvent* me = static_cast( event ); - if ( me->buttons() | Qt::LeftButton && - !_dragInitialPos.isNull() && - !_targetInitialSize.isEmpty() ) { + QMouseEvent* me = static_cast(event); +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + const auto mePos = me->windowPos(); +#else + const auto mePos = me->scenePosition(); +#endif + if (me->buttons() | Qt::LeftButton && + !_dragInitialPos.isNull() && + !_targetInitialSize.isEmpty()) { // Inspired by void QQuickMouseArea::mouseMoveEvent(QMouseEvent *event) // https://code.woboq.org/qt5/qtdeclarative/src/quick/items/qquickmousearea.cpp.html#47curLocalPos // Coordinate mapping in qt quick is even more a nightmare than with graphics view... @@ -363,10 +368,11 @@ bool BottomRightResizer::eventFilter(QObject *item, QEvent *event) QPointF curLocalPos; if ( parentItem() != nullptr ) { startLocalPos = parentItem()->mapFromScene( _dragInitialPos ); - curLocalPos = parentItem()->mapFromScene( me->windowPos() ); + //curLocalPos = parentItem()->mapFromScene( me->windowPos() ); + curLocalPos = parentItem()->mapFromScene(mePos); } else { startLocalPos = _dragInitialPos; - curLocalPos = me->windowPos(); + curLocalPos = mePos; } const QPointF delta{curLocalPos - startLocalPos}; if (_target) { diff --git a/src/qanGraphView.h b/src/qanGraphView.h index f39f0f24..c942554e 100644 --- a/src/qanGraphView.h +++ b/src/qanGraphView.h @@ -34,9 +34,6 @@ #pragma once -// GTpo headers -//#include - // QuickQanava headers #include "./qanGraph.h" #include "./qanGroup.h" @@ -87,20 +84,20 @@ class GraphView : public qan::Navigable void rightClicked(QPointF pos); - void nodeClicked( qan::Node* node, QPointF pos ); - void nodeRightClicked( qan::Node* node, QPointF pos ); - void nodeDoubleClicked( qan::Node* node, QPointF pos ); + void nodeClicked(qan::Node* node, QPointF pos); + void nodeRightClicked(qan::Node* node, QPointF pos); + void nodeDoubleClicked(qan::Node* node, QPointF pos); - void portClicked( qan::PortItem* port, QPointF pos ); - void portRightClicked( qan::PortItem* port, QPointF pos ); + void portClicked(qan::PortItem* port, QPointF pos); + void portRightClicked(qan::PortItem* port, QPointF pos); - void edgeClicked( qan::Edge* edge, QPointF pos ); - void edgeRightClicked( qan::Edge* edge, QPointF pos ); - void edgeDoubleClicked( qan::Edge* edge, QPointF pos ); + void edgeClicked(qan::Edge* edge, QPointF pos); + void edgeRightClicked(qan::Edge* edge, QPointF pos); + void edgeDoubleClicked(qan::Edge* edge, QPointF pos); - void groupClicked( qan::Group* group, QPointF pos ); - void groupRightClicked( qan::Group* group, QPointF pos ); - void groupDoubleClicked( qan::Group* group, QPointF pos ); + void groupClicked(qan::Group* group, QPointF pos); + void groupRightClicked(qan::Group* group, QPointF pos); + void groupDoubleClicked(qan::Group* group, QPointF pos); //@} //------------------------------------------------------------------------- diff --git a/src/qanNavigable.cpp b/src/qanNavigable.cpp index 0a8f569e..f061f5f0 100644 --- a/src/qanNavigable.cpp +++ b/src/qanNavigable.cpp @@ -272,9 +272,9 @@ void Navigable::setDragActive(bool dragActive) noexcept } #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) -void Navigable::geometryChanged( const QRectF& newGeometry, const QRectF& oldGeometry ) +void Navigable::geometryChanged(const QRectF& newGeometry, const QRectF& oldGeometry) #else -void Navigable::geometryChange( const QRectF& newGeometry, const QRectF& oldGeometry ) +void Navigable::geometryChange(const QRectF& newGeometry, const QRectF& oldGeometry) #endif { if (getNavigable()) { diff --git a/src/qanNavigablePreview.cpp b/src/qanNavigablePreview.cpp index 3a7d1719..da98023e 100644 --- a/src/qanNavigablePreview.cpp +++ b/src/qanNavigablePreview.cpp @@ -41,7 +41,7 @@ namespace qan { // ::qan /* NavigablePreview Object Management *///------------------------------------- -NavigablePreview::NavigablePreview( QQuickItem* parent ) : +NavigablePreview::NavigablePreview(QQuickItem* parent) : QQuickItem{parent} { setFlag(QQuickItem::ItemHasContents); @@ -49,13 +49,15 @@ NavigablePreview::NavigablePreview( QQuickItem* parent ) : //----------------------------------------------------------------------------- /* Preview Management *///----------------------------------------------------- -void NavigablePreview::setSource( qan::Navigable* source ) noexcept +void NavigablePreview::setSource(qan::Navigable* source) noexcept { - if ( source != _source ) { + if (source != _source) { _source = source; emit sourceChanged(); } } + +QRectF NavigablePreview::rectUnion(QRectF a, QRectF b) const { return a.united(b); } //----------------------------------------------------------------------------- } // ::qan diff --git a/src/qanNavigablePreview.h b/src/qanNavigablePreview.h index 0d4b342c..fe6eeeb0 100644 --- a/src/qanNavigablePreview.h +++ b/src/qanNavigablePreview.h @@ -32,8 +32,7 @@ // \date 2017 06 02 //----------------------------------------------------------------------------- -#ifndef canNavigablePreview_h -#define canNavigablePreview_h +#pragma once // Qt headers #include @@ -53,8 +52,8 @@ class NavigablePreview : public QQuickItem //@{ Q_OBJECT public: - explicit NavigablePreview( QQuickItem* parent = nullptr ); - virtual ~NavigablePreview() { /* Nil */ } + explicit NavigablePreview(QQuickItem* parent = nullptr); + virtual ~NavigablePreview() = default; NavigablePreview(const NavigablePreview&) = delete; //@} //------------------------------------------------------------------------- @@ -66,7 +65,7 @@ Q_OBJECT * * \warning Can be nullptr. */ - Q_PROPERTY( qan::Navigable* source READ getSource WRITE setSource NOTIFY sourceChanged FINAL ) + Q_PROPERTY(qan::Navigable* source READ getSource WRITE setSource NOTIFY sourceChanged FINAL) //! \copydoc source inline qan::Navigable* getSource() const noexcept { return _source.data(); } //! \copydoc source @@ -78,6 +77,10 @@ Q_OBJECT //! \copydoc source void sourceChanged(); +protected: + //! Return union of rects \c a and \c b. + Q_INVOKABLE QRectF rectUnion(QRectF a, QRectF b) const; + signals: /*! \brief Emitted whenever the preview visible window position or size change. * @@ -93,7 +96,4 @@ Q_OBJECT } // ::qan -QML_DECLARE_TYPE( qan::NavigablePreview ) - -#endif // qanNavigablePreview_h - +QML_DECLARE_TYPE(qan::NavigablePreview) diff --git a/src/qanSelectable.cpp b/src/qanSelectable.cpp index c58f1dfe..94e59174 100644 --- a/src/qanSelectable.cpp +++ b/src/qanSelectable.cpp @@ -42,8 +42,8 @@ namespace qan { // ::qan Selectable::Selectable() { /* Nil */ } Selectable::~Selectable() { - if ( _selectionItem && // Delete selection item if it has Cpp ownership - QQmlEngine::objectOwnership(_selectionItem.data()) == QQmlEngine::CppOwnership ) + if (_selectionItem && // Delete selection item if it has Cpp ownership + QQmlEngine::objectOwnership(_selectionItem.data()) == QQmlEngine::CppOwnership) _selectionItem->deleteLater(); } @@ -51,8 +51,8 @@ void Selectable::configure(QQuickItem* target, qan::Graph* graph) { _target = target; _graph = graph; - if ( _selectionItem ) - _selectionItem->setParentItem( _target ); + if (_selectionItem) + _selectionItem->setParentItem(_target); } //----------------------------------------------------------------------------- diff --git a/src/qanSelectable.h b/src/qanSelectable.h index 3b5ff3e0..c69a3554 100644 --- a/src/qanSelectable.h +++ b/src/qanSelectable.h @@ -42,9 +42,6 @@ #include #include -// QuickQanava headers -/* Nil */ - namespace qan { // ::qan class Graph; diff --git a/src/qmldir_plugin b/src/qmldir_plugin index e9a3bb91..c8f1ba29 100644 --- a/src/qmldir_plugin +++ b/src/qmldir_plugin @@ -7,6 +7,8 @@ Edge 2.0 Edge.qml EdgeTemplate 2.0 EdgeTemplate.qml Node 2.0 Node.qml GraphView 2.0 GraphView.qml +GraphPreview 2.0 GraphPreview.qml +HeatMapPreview 2.0 HeatMapPreview.qml Group 2.0 Group.qml RectNodeTemplate 2.0 RectNodeTemplate.qml RectSolidBackground 2.0 RectSolidBackground.qml diff --git a/src/quickqanava.pri b/src/quickqanava.pri index 32f73d4a..9a368380 100644 --- a/src/quickqanava.pri +++ b/src/quickqanava.pri @@ -11,6 +11,7 @@ DEFINES += QUICKQANAVA_STATIC # use QML module (calling QuickQanava::i DEPENDPATH += $$PWD INCLUDEPATH += $$PWD RESOURCES += $$PWD/QuickQanava_static.qrc + RESOURCES += $$PWD/GraphicalEffects5/QuickQanavaGraphicalEffects.qrc HEADERS += $$PWD/QuickQanava.h \ @@ -36,6 +37,7 @@ HEADERS += $$PWD/QuickQanava.h \ $$PWD/qanStyleManager.h \ $$PWD/qanNavigable.h \ $$PWD/qanNavigablePreview.h \ + $$PWD/qanAnalysisTimeHeatMap.h \ $$PWD/qanGrid.h \ $$PWD/qanLineGrid.h \ $$PWD/qanBottomRightResizer.h \ @@ -69,6 +71,7 @@ SOURCES += $$PWD/qanGraphView.cpp \ $$PWD/qanStyleManager.cpp \ $$PWD/qanNavigable.cpp \ $$PWD/qanNavigablePreview.cpp \ + $$PWD/qanAnalysisTimeHeatMap.cpp\ $$PWD/qanGrid.cpp \ $$PWD/qanLineGrid.cpp \ $$PWD/qanBottomRightResizer.cpp @@ -76,6 +79,7 @@ SOURCES += $$PWD/qanGraphView.cpp \ OTHER_FILES += $$PWD/QuickQanava \ $$PWD/NavigablePreview.qml \ $$PWD/GraphPreview.qml \ + $$PWD/HeatMapPreview.qml \ $$PWD/LineGrid.qml \ $$PWD/GraphView.qml \ $$PWD/Graph.qml \