diff --git a/CMakeLists.txt b/CMakeLists.txt index 754613b..fe1180e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -78,8 +78,6 @@ set(Actions src/ExportAction.cpp src/DatasetsAction.h src/DatasetsAction.cpp - src/NavigationAction.h - src/NavigationAction.cpp ) set(Models diff --git a/src/NavigationAction.cpp b/src/NavigationAction.cpp deleted file mode 100644 index 3406e5a..0000000 --- a/src/NavigationAction.cpp +++ /dev/null @@ -1,102 +0,0 @@ -#include "NavigationAction.h" -#include "ScatterplotWidget.h" - -#include -#include - -#include - -using namespace mv; -using namespace mv::gui; - -NavigationAction::NavigationAction(QObject* parent, const QString& title) : - HorizontalGroupAction(parent, title), - _scatterplotWidget(nullptr), - _zoomRectangleAction(this, "Zoom Rectangle"), - _zoomDataExtentsAction(this, "Zoom to data extents"), - _freezeZoomAction(this, "Freeze zoom") -{ - setIconByName("image"); - setShowLabels(false); - - addAction(&_zoomRectangleAction); - addAction(&_zoomDataExtentsAction); - addAction(&_freezeZoomAction); - - _zoomRectangleAction.setToolTip("Extents of the current view"); - _zoomRectangleAction.setIcon(combineIcons(StyledIcon("expand"), StyledIcon("ellipsis-h"))); - _zoomRectangleAction.setIconByName("vector-square"); - _zoomRectangleAction.setConfigurationFlag(WidgetAction::ConfigurationFlag::ForceCollapsedInGroup); - - _zoomDataExtentsAction.setToolTip("Zoom to the extents of the data (Alt + O)"); - _zoomDataExtentsAction.setIconByName("expand"); - _zoomDataExtentsAction.setDefaultWidgetFlags(TriggerAction::Icon); - _zoomDataExtentsAction.setConnectionPermissionsToForceNone(true); - _zoomDataExtentsAction.setShortcutContext(Qt::WidgetWithChildrenShortcut); - _zoomDataExtentsAction.setShortcut(QKeySequence("Alt+O")); - - _freezeZoomAction.setToolTip("Freeze the zoom extents"); - _freezeZoomAction.setConnectionPermissionsToForceNone(true); -} - -void NavigationAction::initialize(ScatterplotWidget* scatterplotWidget) -{ - Q_ASSERT(scatterplotWidget != nullptr); - - if (scatterplotWidget == nullptr) - return; - - _scatterplotWidget = scatterplotWidget; - - _scatterplotWidget->addAction(&_zoomDataExtentsAction); - - connect(&_zoomDataExtentsAction, &TriggerAction::triggered, _scatterplotWidget, &ScatterplotWidget::resetView); -} - -void NavigationAction::connectToPublicAction(WidgetAction* publicAction, bool recursive) -{ - auto publicNavigationAction = dynamic_cast(publicAction); - - Q_ASSERT(publicNavigationAction != nullptr); - - if (publicNavigationAction == nullptr) - return; - - if (recursive) { - actions().connectPrivateActionToPublicAction(&_zoomRectangleAction, &publicNavigationAction->getZoomRectangleAction(), recursive); - } - - HorizontalGroupAction::connectToPublicAction(publicAction, recursive); -} - -void NavigationAction::disconnectFromPublicAction(bool recursive) -{ - if (!isConnected()) - return; - - if (recursive) { - actions().disconnectPrivateActionFromPublicAction(&_zoomRectangleAction, recursive); - } - - HorizontalGroupAction::disconnectFromPublicAction(recursive); -} - -void NavigationAction::fromVariantMap(const QVariantMap& variantMap) -{ - HorizontalGroupAction::fromVariantMap(variantMap); - - _zoomRectangleAction.fromParentVariantMap(variantMap); - _zoomDataExtentsAction.fromParentVariantMap(variantMap); - _freezeZoomAction.fromParentVariantMap(variantMap); -} - -QVariantMap NavigationAction::toVariantMap() const -{ - auto variantMap = HorizontalGroupAction::toVariantMap(); - - _zoomRectangleAction.insertIntoVariantMap(variantMap); - _zoomDataExtentsAction.insertIntoVariantMap(variantMap); - _freezeZoomAction.insertIntoVariantMap(variantMap); - - return variantMap; -} diff --git a/src/NavigationAction.h b/src/NavigationAction.h deleted file mode 100644 index d5d1e55..0000000 --- a/src/NavigationAction.h +++ /dev/null @@ -1,83 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -using namespace mv::gui; - -class ScatterplotWidget; - -/** - * Navigation action class - * - * Action class for navigating - * - * @author Thomas Kroes - */ -class NavigationAction : public HorizontalGroupAction -{ -public: - - /** - * Construct with \p parent and \p title - * @param parent Pointer to parent object - * @param title Title of the action - */ - Q_INVOKABLE NavigationAction(QObject* parent, const QString& title); - - /** - * Initialize the navigation action with pointer to owning \p scatterplotWidget - * @param scatterplotWidget Pointer to scatterplot widget - */ - void initialize(ScatterplotWidget* scatterplotWidget); - -protected: // Linking - - /** - * Connect this action to a public action - * @param publicAction Pointer to public action to connect to - * @param recursive Whether to also connect descendant child actions - */ - void connectToPublicAction(WidgetAction* publicAction, bool recursive) override; - - /** - * Disconnect this action from its public action - * @param recursive Whether to also disconnect descendant child actions - */ - void disconnectFromPublicAction(bool recursive) override; - -public: // Serialization - - /** - * Load widget action from variant map - * @param Variant map representation of the widget action - */ - void fromVariantMap(const QVariantMap& variantMap) override; - - /** - * Save widget action to variant map - * @return Variant map representation of the widget action - */ - - QVariantMap toVariantMap() const override; - -public: // Action getters - - DecimalRectangleAction& getZoomRectangleAction() { return _zoomRectangleAction; } - TriggerAction& getZoomDataExtentsAction() { return _zoomDataExtentsAction; } - ToggleAction& getFreezeZoomAction() { return _freezeZoomAction; } - -private: - ScatterplotWidget* _scatterplotWidget; /** Pointer to owning scatterplot widget */ - DecimalRectangleAction _zoomRectangleAction; /** Rectangle action for setting the current zoom bounds */ - TriggerAction _zoomDataExtentsAction; /** Trigger action to zoom to data extents */ - ToggleAction _freezeZoomAction; /** Action for toggling the zoom rectangle freeze */ - - friend class mv::AbstractActionsManager; -}; - -Q_DECLARE_METATYPE(NavigationAction) - -inline const auto navigationActionMetaTypeId = qRegisterMetaType("NavigationAction"); \ No newline at end of file diff --git a/src/ScatterplotPlugin.cpp b/src/ScatterplotPlugin.cpp index 1c7bab9..3cf788a 100644 --- a/src/ScatterplotPlugin.cpp +++ b/src/ScatterplotPlugin.cpp @@ -47,8 +47,7 @@ ScatterplotPlugin::ScatterplotPlugin(const PluginFactory* factory) : _scatterPlotWidget(new ScatterplotWidget(this)), _numPoints(0), _settingsAction(this, "Settings"), - _primaryToolbarAction(this, "Primary Toolbar"), - _secondaryToolbarAction(this, "Secondary Toolbar") + _primaryToolbarAction(this, "Primary Toolbar") { setObjectName("Scatterplot"); @@ -56,22 +55,28 @@ ScatterplotPlugin::ScatterplotPlugin(const PluginFactory* factory) : shortcuts.add({ QKeySequence(Qt::Key_R), "Selection", "Rectangle (default)" }); shortcuts.add({ QKeySequence(Qt::Key_L), "Selection", "Lasso" }); + shortcuts.add({ QKeySequence(Qt::Key_P), "Selection", "Polygon" }); shortcuts.add({ QKeySequence(Qt::Key_B), "Selection", "Circular brush (mouse wheel adjusts the radius)" }); shortcuts.add({ QKeySequence(Qt::SHIFT), "Selection", "Add to selection" }); shortcuts.add({ QKeySequence(Qt::CTRL), "Selection", "Remove from selection" }); + shortcuts.add({ QKeySequence(Qt::Key_A), "Selection", "Select all" }); + shortcuts.add({ QKeySequence(Qt::Key_E), "Selection", "Clear selection" }); + shortcuts.add({ QKeySequence(Qt::Key_I), "Selection", "Invert selection" }); shortcuts.add({ QKeySequence(Qt::Key_S), "Render", "Scatter mode (default)" }); shortcuts.add({ QKeySequence(Qt::Key_D), "Render", "Density mode" }); shortcuts.add({ QKeySequence(Qt::Key_C), "Render", "Contour mode" }); + shortcuts.add({ QKeySequence(Qt::Key_Plus), "Navigation", "Zoom in by 10%" }); + shortcuts.add({ QKeySequence(Qt::Key_Minus), "Navigation", "Zoom out by 10%" }); shortcuts.add({ QKeySequence(Qt::ALT), "Navigation", "Pan (LMB down)" }); shortcuts.add({ QKeySequence(Qt::ALT), "Navigation", "Zoom (mouse wheel)" }); shortcuts.add({ QKeySequence(Qt::Key_O), "Navigation", "Original view" }); + shortcuts.add({ QKeySequence(Qt::Key_H), "Navigation", "Zoom to selection" }); + shortcuts.add({ QKeySequence(Qt::Key_F), "Navigation", "Zoom to window" }); _dropWidget = new DropWidget(_scatterPlotWidget); - _scatterPlotWidget->getNavigationAction().setParent(this); - getWidget().setFocusPolicy(Qt::ClickFocus); _primaryToolbarAction.addAction(&_settingsAction.getDatasetsAction()); @@ -105,11 +110,7 @@ ScatterplotPlugin::ScatterplotPlugin(const PluginFactory* factory) : connect(_scatterPlotWidget, &ScatterplotWidget::renderModeChanged, this, updateReadOnly); connect(&_positionDataset, &Dataset::changed, this, updateReadOnly); - _secondaryToolbarAction.addAction(&_settingsAction.getColoringAction().getColorMap1DAction(), 1); - _secondaryToolbarAction.addAction(focusSelectionAction, 2); - //_secondaryToolbarAction.addAction(&_settingsAction.getExportAction()); - _secondaryToolbarAction.addAction(&_settingsAction.getMiscellaneousAction()); - _secondaryToolbarAction.addAction(&_scatterPlotWidget->getNavigationAction()); + //_secondaryToolbarAction.addAction(&_settingsAction.getMiscellaneousAction()); connect(_scatterPlotWidget, &ScatterplotWidget::customContextMenuRequested, this, [this](const QPoint& point) { if (!_positionDataset.isValid()) @@ -252,11 +253,32 @@ void ScatterplotPlugin::init() layout->setSpacing(0); layout->addWidget(_primaryToolbarAction.createWidget(&getWidget())); layout->addWidget(_scatterPlotWidget, 100); - layout->addWidget(_secondaryToolbarAction.createWidget(&getWidget())); + + auto navigationWidget = new QWidget(); + auto navigationLayout = new QHBoxLayout(); + + navigationLayout->setContentsMargins(4, 4, 4, 4); + + navigationLayout->addStretch(1); + { + auto renderersNavigationGroupAction = new HorizontalGroupAction(this, "Navigation"); + + renderersNavigationGroupAction->setShowLabels(false); + + renderersNavigationGroupAction->addAction(const_cast(&_scatterPlotWidget->getPointRendererNavigator().getNavigationAction())); + + _scatterPlotWidget->getPointRendererNavigator().getNavigationAction().setParent(&_settingsAction); + + navigationLayout->addWidget(renderersNavigationGroupAction->createWidget(&getWidget())); + } + navigationLayout->addStretch(1); + + navigationWidget->setLayout(navigationLayout); + + layout->addWidget(navigationWidget); getWidget().setLayout(layout); - // Update the data when the scatter plot widget is initialized connect(_scatterPlotWidget, &ScatterplotWidget::initialized, this, &ScatterplotPlugin::updateData); connect(&_scatterPlotWidget->getPixelSelectionTool(), &PixelSelectionTool::areaChanged, [this]() { @@ -282,6 +304,11 @@ void ScatterplotPlugin::init() getLearningCenterAction().getViewPluginOverlayWidget()->setTargetWidget(_scatterPlotWidget); + connect(&getScatterplotWidget().getPointRendererNavigator().getNavigationAction().getZoomSelectionAction(), &TriggerAction::triggered, this, [this]() -> void { + if (_selectionBoundaries.isValid()) + _scatterPlotWidget->getPointRendererNavigator().setZoomRectangleWorld(_selectionBoundaries); + }); + #ifdef VIEW_SAMPLING_HTML getSamplerAction().setHtmlViewGeneratorFunction([this](const ViewPluginSamplerAction::SampleContext& toolTipContext) -> QString { QStringList localPointIndices, globalPointIndices; @@ -357,10 +384,13 @@ void ScatterplotPlugin::createSubset(const bool& fromSourceData /*= false*/, con void ScatterplotPlugin::selectPoints() { + if (getSettingsAction().getSelectionAction().getFreezeSelectionAction().isChecked()) + return; + auto& pixelSelectionTool = _scatterPlotWidget->getPixelSelectionTool(); // Only proceed with a valid points position dataset and when the pixel selection tool is active - if (!_positionDataset.isValid() || !pixelSelectionTool.isActive() || _scatterPlotWidget->isNavigating() || !pixelSelectionTool.isEnabled()) + if (!_positionDataset.isValid() || !pixelSelectionTool.isActive() || _scatterPlotWidget->_pointRenderer.getNavigator().isNavigating() || !pixelSelectionTool.isEnabled()) return; auto selectionAreaImage = pixelSelectionTool.getAreaPixmap().toImage(); @@ -374,27 +404,49 @@ void ScatterplotPlugin::selectPoints() _positionDataset->getGlobalIndices(localGlobalIndices); - auto& zoomRectangleAction = _scatterPlotWidget->getNavigationAction().getZoomRectangleAction(); + auto& pointRenderer = _scatterPlotWidget->_pointRenderer; + auto& navigator = pointRenderer.getNavigator(); - const auto width = selectionAreaImage.width(); - const auto height = selectionAreaImage.height(); - const auto size = width < height ? width : height; - const auto uvOffset = QPoint((selectionAreaImage.width() - size) / 2.0f, (selectionAreaImage.height() - size) / 2.0f); + const auto zoomRectangleWorld = navigator.getZoomRectangleWorld(); + const auto screenRectangle = QRect(QPoint(), pointRenderer.getRenderSize()); - QPointF uvNormalized = {}; - QPoint uv = {}; + float boundaries[4]{ + std::numeric_limits::max(), + std::numeric_limits::lowest(), + std::numeric_limits::max(), + std::numeric_limits::lowest() + }; + // Go over all points in the dataset to see if they are selected for (std::uint32_t localPointIndex = 0; localPointIndex < _positions.size(); localPointIndex++) { - uvNormalized = QPointF((_positions[localPointIndex].x - zoomRectangleAction.getLeft()) / zoomRectangleAction.getWidth(), (zoomRectangleAction.getTop() - _positions[localPointIndex].y) / zoomRectangleAction.getHeight()); - uv = uvOffset + QPoint(uvNormalized.x() * size, uvNormalized.y() * size); + const auto& point = _positions[localPointIndex]; - if (uv.x() >= selectionAreaImage.width() || uv.x() < 0 || uv.y() >= selectionAreaImage.height() || uv.y() < 0) - continue; + // Compute the offset of the point in the world space + const auto pointOffsetWorld = QPointF(point.x - zoomRectangleWorld.left(), point.y - zoomRectangleWorld.top()); + + // Normalize it + const auto pointOffsetWorldNormalized = QPointF(pointOffsetWorld.x() / zoomRectangleWorld.width(), pointOffsetWorld.y() / zoomRectangleWorld.height()); + + // Convert it to screen space + const auto pointOffsetScreen = QPoint(pointOffsetWorldNormalized.x() * screenRectangle.width(), screenRectangle.height() - pointOffsetWorldNormalized.y() * screenRectangle.height()); + + // Continue to next point if the point is outside the screen + if (!screenRectangle.contains(pointOffsetScreen)) + continue; + + // If the corresponding pixel is not transparent, add the point to the selection + if (selectionAreaImage.pixelColor(pointOffsetScreen).alpha() > 0) { + targetSelectionIndices.push_back(localGlobalIndices[localPointIndex]); - if (selectionAreaImage.pixelColor(uv).alpha() > 0) - targetSelectionIndices.push_back(localGlobalIndices[localPointIndex]); + boundaries[0] = std::min(boundaries[0], point.x); + boundaries[1] = std::max(boundaries[1], point.x); + boundaries[2] = std::min(boundaries[2], point.y); + boundaries[3] = std::max(boundaries[3], point.y); + } } + _selectionBoundaries = QRectF(boundaries[0], boundaries[2], boundaries[1] - boundaries[0], boundaries[3] - boundaries[2]); + switch (const auto selectionModifier = pixelSelectionTool.isAborted() ? PixelSelectionModifierType::Subtract : pixelSelectionTool.getModifier()) { case PixelSelectionModifierType::Replace: @@ -441,6 +493,10 @@ void ScatterplotPlugin::selectPoints() } } + auto& navigationAction = _scatterPlotWidget->getPointRendererNavigator().getNavigationAction(); + + navigationAction.getZoomSelectionAction().setEnabled(!targetSelectionIndices.empty() && !navigationAction.getFreezeNavigation().isChecked()); + _positionDataset->setSelectionIndices(targetSelectionIndices); events().notifyDatasetDataSelectionChanged(_positionDataset->getSourceDataset()); @@ -450,7 +506,7 @@ void ScatterplotPlugin::samplePoints() { auto& samplerPixelSelectionTool = _scatterPlotWidget->getSamplerPixelSelectionTool(); - if (!_positionDataset.isValid() || !samplerPixelSelectionTool.isActive() || _scatterPlotWidget->isNavigating() || !samplerPixelSelectionTool.isEnabled()) + if (!_positionDataset.isValid() || _scatterPlotWidget->_pointRenderer.getNavigator().isNavigating() || !samplerPixelSelectionTool.isActive()) return; auto selectionAreaImage = samplerPixelSelectionTool.getAreaPixmap().toImage(); @@ -462,31 +518,37 @@ void ScatterplotPlugin::samplePoints() std::vector localGlobalIndices; _positionDataset->getGlobalIndices(localGlobalIndices); - - auto& zoomRectangleAction = _scatterPlotWidget->getNavigationAction().getZoomRectangleAction(); - - const auto width = selectionAreaImage.width(); - const auto height = selectionAreaImage.height(); - const auto size = width < height ? width : height; - const auto uvOffset = QPoint((selectionAreaImage.width() - size) / 2.0f, (selectionAreaImage.height() - size) / 2.0f); - - QPointF pointUvNormalized; - - QPoint pointUv, mouseUv = _scatterPlotWidget->mapFromGlobal(QCursor::pos()); std::vector focusHighlights(_positions.size()); std::vector> sampledPoints; + auto& pointRenderer = _scatterPlotWidget->_pointRenderer; + auto& navigator = pointRenderer.getNavigator(); + + const auto zoomRectangleWorld = navigator.getZoomRectangleWorld(); + const auto screenRectangle = QRect(QPoint(), pointRenderer.getRenderSize()); + const auto mousePositionWorld = pointRenderer.getScreenPointToWorldPosition(pointRenderer.getNavigator().getViewMatrix(), _scatterPlotWidget->mapFromGlobal(QCursor::pos())); + + // Go over all points in the dataset to see if they should be sampled for (std::uint32_t localPointIndex = 0; localPointIndex < _positions.size(); localPointIndex++) { - pointUvNormalized = QPointF((_positions[localPointIndex].x - zoomRectangleAction.getLeft()) / zoomRectangleAction.getWidth(), (zoomRectangleAction.getTop() - _positions[localPointIndex].y) / zoomRectangleAction.getHeight()); - pointUv = uvOffset + QPoint(pointUvNormalized.x() * size, pointUvNormalized.y() * size); - if (pointUv.x() >= selectionAreaImage.width() || pointUv.x() < 0 || pointUv.y() >= selectionAreaImage.height() || pointUv.y() < 0) + // Compute the offset of the point in the world space + const auto pointOffsetWorld = QPointF(_positions[localPointIndex].x - zoomRectangleWorld.left(), _positions[localPointIndex].y - zoomRectangleWorld.top()); + + // Normalize it + const auto pointOffsetWorldNormalized = QPointF(pointOffsetWorld.x() / zoomRectangleWorld.width(), pointOffsetWorld.y() / zoomRectangleWorld.height()); + + // Convert it to screen space + const auto pointOffsetScreen = QPoint(pointOffsetWorldNormalized.x() * screenRectangle.width(), screenRectangle.height() - pointOffsetWorldNormalized.y() * screenRectangle.height()); + + // Continue to next point if the point is outside the screen + if (!screenRectangle.contains(pointOffsetScreen)) continue; - if (selectionAreaImage.pixelColor(pointUv).alpha() > 0) { - const auto sample = std::pair((QVector2D(mouseUv) - QVector2D(pointUv)).length(), localPointIndex); + // If the corresponding pixel is not transparent, add the point to the selection + if (selectionAreaImage.pixelColor(pointOffsetScreen).alpha() > 0) { + const auto sample = std::pair((QVector2D(_positions[localPointIndex].x, _positions[localPointIndex].y) - mousePositionWorld.toVector2D()).length(), localPointIndex); sampledPoints.emplace_back(sample); } @@ -569,7 +631,10 @@ void ScatterplotPlugin::positionDatasetChanged() _positionSourceDataset = _positionDataset->getSourceDataset(); _numPoints = _positionDataset->getNumPoints(); - + + _scatterPlotWidget->getPointRendererNavigator().resetView(true); + _scatterPlotWidget->getDensityRendererNavigator().resetView(true); + updateData(); } @@ -789,10 +854,16 @@ void ScatterplotPlugin::fromVariantMap(const QVariantMap& variantMap) variantMapMustContain(variantMap, "Settings"); _primaryToolbarAction.fromParentVariantMap(variantMap); - _secondaryToolbarAction.fromParentVariantMap(variantMap); _settingsAction.fromParentVariantMap(variantMap); - - _scatterPlotWidget->getNavigationAction().fromParentVariantMap(variantMap); + + auto& pointRenderer = const_cast(_scatterPlotWidget->getPointRenderer()); + + if (pointRenderer.getNavigator().getNavigationAction().getSerializationCountFrom() == 0) { + qDebug() << "Resetting view"; + pointRenderer.getNavigator().resetView(true); + + _scatterPlotWidget->update(); + } } QVariantMap ScatterplotPlugin::toVariantMap() const @@ -800,11 +871,8 @@ QVariantMap ScatterplotPlugin::toVariantMap() const QVariantMap variantMap = ViewPlugin::toVariantMap(); _primaryToolbarAction.insertIntoVariantMap(variantMap); - _secondaryToolbarAction.insertIntoVariantMap(variantMap); _settingsAction.insertIntoVariantMap(variantMap); - _scatterPlotWidget->getNavigationAction().insertIntoVariantMap(variantMap); - return variantMap; } @@ -901,5 +969,5 @@ PluginTriggerActions ScatterplotPluginFactory::getPluginTriggerActions(const mv: QUrl ScatterplotPluginFactory::getRepositoryUrl() const { - return QUrl("https://github.com/ManiVaultStudio/Scatterplot"); + return { "https://github.com/ManiVaultStudio/Scatterplot" }; } diff --git a/src/ScatterplotPlugin.h b/src/ScatterplotPlugin.h index ba10fd8..23b8250 100644 --- a/src/ScatterplotPlugin.h +++ b/src/ScatterplotPlugin.h @@ -111,15 +111,13 @@ class ScatterplotPlugin : public ViewPlugin private: mv::gui::DropWidget* _dropWidget; /** Widget for dropping datasets */ ScatterplotWidget* _scatterPlotWidget; /** The visualization widget */ - Dataset _positionDataset; /** Smart pointer to points dataset for point position */ Dataset _positionSourceDataset; /** Smart pointer to source of the points dataset for point position (if any) */ std::vector _positions; /** Point positions */ unsigned int _numPoints; /** Number of point positions */ - SettingsAction _settingsAction; /** Group action for all settings */ HorizontalToolbarAction _primaryToolbarAction; /** Horizontal toolbar for primary content */ - HorizontalToolbarAction _secondaryToolbarAction; /** Secondary toolbar for secondary content */ + QRectF _selectionBoundaries; /** Boundaries of the selection */ static const std::int32_t LAZY_UPDATE_INTERVAL = 2; diff --git a/src/ScatterplotWidget.cpp b/src/ScatterplotWidget.cpp index f95a50c..5ad29d3 100644 --- a/src/ScatterplotWidget.cpp +++ b/src/ScatterplotWidget.cpp @@ -4,11 +4,9 @@ #include -#include +#include #include -#include -#include #include #include #include @@ -16,9 +14,7 @@ #include #include -#include - -#include +#include using namespace mv; @@ -38,54 +34,33 @@ namespace return bounds; } - - void translateBounds(Bounds& b, float x, float y) - { - b.setLeft(b.getLeft() + x); - b.setRight(b.getRight() + x); - b.setBottom(b.getBottom() + y); - b.setTop(b.getTop() + y); - } } ScatterplotWidget::ScatterplotWidget(mv::plugin::ViewPlugin* parentPlugin) : - QOpenGLWidget(), - _pointRenderer(), - _densityRenderer(DensityRenderer::RenderMode::DENSITY), + _densityRenderer(DensityRenderer::RenderMode::DENSITY, this), + _pointRenderer(this), _isInitialized(false), _renderMode(SCATTERPLOT), _backgroundColor(255, 255, 255, 255), _coloringMode(ColoringMode::Constant), - _widgetSizeInfo(), _dataRectangleAction(this, "Data rectangle"), - _navigationAction(this, "Navigation"), - _colorMapImage(), _pixelSelectionTool(this), _samplerPixelSelectionTool(this), _pixelRatio(1.0), - _mousePositions(), - _isNavigating(false), _weightDensity(false), _parentPlugin(parentPlugin) { setContextMenuPolicy(Qt::CustomContextMenu); setAcceptDrops(true); setMouseTracking(true); - setFocusPolicy(Qt::ClickFocus); + //setFocusPolicy(Qt::ClickFocus); grabGesture(Qt::PinchGesture); - //setAttribute(Qt::WA_TranslucentBackground); installEventFilter(this); - _navigationAction.initialize(this); - _pixelSelectionTool.setEnabled(true); _pixelSelectionTool.setMainColor(QColor(Qt::black)); _pixelSelectionTool.setFixedBrushRadiusModifier(Qt::AltModifier); - _samplerPixelSelectionTool.setEnabled(true); - _samplerPixelSelectionTool.setMainColor(QColor(Qt::black)); - _samplerPixelSelectionTool.setFixedBrushRadiusModifier(Qt::AltModifier); - connect(&_pixelSelectionTool, &PixelSelectionTool::shapeChanged, [this]() { if (isInitialized()) update(); @@ -134,18 +109,27 @@ ScatterplotWidget::ScatterplotWidget(mv::plugin::ViewPlugin* parentPlugin) : QObject::connect(winHandle, &QWindow::screenChanged, this, &ScatterplotWidget::updatePixelRatio, Qt::UniqueConnection); }); - connect(&_navigationAction.getZoomRectangleAction(), &DecimalRectangleAction::rectangleChanged, this, [this]() -> void { - auto& zoomRectangleAction = _navigationAction.getZoomRectangleAction(); + connect(&_pointRenderer.getNavigator(), &Navigator2D::isNavigatingChanged, this, [this](bool isNavigating) -> void { + _pixelSelectionTool.setEnabled(!isNavigating); - const auto zoomBounds = zoomRectangleAction.getBounds(); + if (isNavigating) { + _samplerPixelSelectionTool.setEnabled(false); + } + else if (_parentPlugin) { + _samplerPixelSelectionTool.setEnabled(_parentPlugin->getSamplerAction().getEnabledAction().isChecked()); + } + }); - _pointRenderer.setViewBounds(zoomBounds); - _densityRenderer.setBounds(zoomBounds); + connect(&getPointRendererNavigator(), &Navigator2D::zoomRectangleWorldChanged, this, [this]() -> void { update(); }); + connect(&getDensityRendererNavigator(), &Navigator2D::zoomRectangleWorldChanged, this, [this]() -> void { update(); }); - _navigationAction.getZoomDataExtentsAction().setEnabled(zoomBounds != _dataRectangleAction.getBounds()); + _samplerPixelSelectionTool.setEnabled(true); + _samplerPixelSelectionTool.setMainColor(QColor(Qt::black)); + _samplerPixelSelectionTool.setFixedBrushRadiusModifier(Qt::AltModifier); - update(); - }); + getPointRendererNavigator().setEnabled(true); + + _densityRenderer.setCustomNavigator(&getPointRendererNavigator()); } bool ScatterplotWidget::event(QEvent* event) @@ -153,152 +137,51 @@ bool ScatterplotWidget::event(QEvent* event) if (!event) return QOpenGLWidget::event(event); + // Need this to receive key release events in the two-dimensional renderer + if (event->type() == QEvent::ShortcutOverride) { + const auto keyEvent = dynamic_cast(event); + + if (keyEvent->key() == Qt::Key_Alt) { + event->accept(); + + return QOpenGLWidget::event(event); + } + } + auto setIsNavigating = [this](bool isNavigating) -> void { - _isNavigating = isNavigating; - _pixelSelectionTool.setEnabled(!isNavigating); + _pixelSelectionTool.setEnabled(getRenderMode() == RenderMode::SCATTERPLOT && !isNavigating); + if (isNavigating) { _samplerPixelSelectionTool.setEnabled(false); } - else if (_parentPlugin) { // reset to UI-setting + else if (_parentPlugin) { _samplerPixelSelectionTool.setEnabled(_parentPlugin->getSamplerAction().getEnabledAction().isChecked()); } + }; - }; + if (event->type() == QEvent::KeyPress) { + const auto keyEvent = dynamic_cast(event); - // Set navigation flag on Alt press/release - if (event->type() == QEvent::KeyRelease) { - if (const auto* keyEvent = static_cast(event)) { - if (keyEvent->key() == Qt::Key_Alt) { - setIsNavigating(false); - } - } + if (keyEvent->key() == Qt::Key_Alt) { + setIsNavigating(true); - } - else if (event->type() == QEvent::KeyPress) { - if (const auto* keyEvent = static_cast(event)) { - if (keyEvent->key() == Qt::Key_Alt) { - setIsNavigating(true); - } + return QOpenGLWidget::event(event); } - } - // Interactions when Alt is pressed - if (isInitialized() && QGuiApplication::keyboardModifiers() == Qt::AltModifier) { - - switch (event->type()) - { - case QEvent::Wheel: - { - // Scroll to zoom - if (auto* wheelEvent = static_cast(event)) - zoomAround(wheelEvent->position().toPoint(), wheelEvent->angleDelta().x() / 1200.f); - - break; - } - - case QEvent::MouseButtonPress: - { - if (const auto* mouseEvent = static_cast(event)) - { - if(mouseEvent->button() == Qt::MiddleButton) - resetView(); - - // Navigation - if (mouseEvent->buttons() == Qt::LeftButton) - { - setIsNavigating(true); - setCursor(Qt::ClosedHandCursor); - _mousePositions << mouseEvent->pos(); - update(); - } - } - - break; - } - - case QEvent::MouseButtonRelease: - { - setIsNavigating(false); - setCursor(Qt::ArrowCursor); - _mousePositions.clear(); - update(); - - break; - } - - case QEvent::MouseMove: - { - if (const auto* mouseEvent = static_cast(event)) - { - _mousePositions << mouseEvent->pos(); - - if (mouseEvent->buttons() == Qt::LeftButton && _mousePositions.size() >= 2) - { - const auto& previousMousePosition = _mousePositions[_mousePositions.size() - 2]; - const auto& currentMousePosition = _mousePositions[_mousePositions.size() - 1]; - const auto panVector = currentMousePosition - previousMousePosition; - - panBy(panVector); - } - } + if (event->type() == QEvent::KeyRelease) { + const auto keyEvent = dynamic_cast(event); - break; - } + if (keyEvent->key() == Qt::Key_Alt) { + setIsNavigating(false); + return QOpenGLWidget::event(event); } - } return QOpenGLWidget::event(event); } -void ScatterplotWidget::resetView() -{ - _navigationAction.getZoomRectangleAction().setBounds(_dataRectangleAction.getBounds()); -} - -void ScatterplotWidget::panBy(const QPointF& to) -{ - auto& zoomRectangleAction = _navigationAction.getZoomRectangleAction(); - - const auto moveBy = QPointF(to.x() / _widgetSizeInfo.width * zoomRectangleAction.getWidth() * _widgetSizeInfo.ratioWidth * -1.f, - to.y() / _widgetSizeInfo.height * zoomRectangleAction.getHeight() * _widgetSizeInfo.ratioHeight); - - zoomRectangleAction.translateBy({ moveBy.x(), moveBy.y() }); - - update(); -} - -void ScatterplotWidget::zoomAround(const QPointF& at, float factor) -{ - auto& zoomRectangleAction = _navigationAction.getZoomRectangleAction(); - - // the widget might have a different aspect ratio than the square opengl viewport - const auto offsetBounds = QPointF(zoomRectangleAction.getWidth() * (0.5f * (1 - _widgetSizeInfo.ratioWidth)), - zoomRectangleAction.getHeight() * (0.5f * (1 - _widgetSizeInfo.ratioHeight)) * -1.f); - - const auto originBounds = QPointF(zoomRectangleAction.getLeft(), zoomRectangleAction.getTop()); - - // translate mouse point in widget to mouse point in bounds coordinates - const auto atTransformed = QPointF(at.x() / _widgetSizeInfo.width * zoomRectangleAction.getWidth() * _widgetSizeInfo.ratioWidth, - at.y() / _widgetSizeInfo.height * zoomRectangleAction.getHeight() * _widgetSizeInfo.ratioHeight * -1.f); - - const auto atInBounds = originBounds + offsetBounds + atTransformed; - - // ensure mouse position is the same after zooming - const auto currentBoundCenter = zoomRectangleAction.getCenter(); - - float moveMouseX = (atInBounds.x() - currentBoundCenter.first) * factor; - float moveMouseY = (atInBounds.y() - currentBoundCenter.second) * factor; - - // zoom and move view - zoomRectangleAction.translateBy({ moveMouseX, moveMouseY }); - zoomRectangleAction.expandBy(-1.f * factor); - - update(); -} - bool ScatterplotWidget::isInitialized() const { return _isInitialized; @@ -321,18 +204,17 @@ void ScatterplotWidget::setRenderMode(const RenderMode& renderMode) switch (_renderMode) { case ScatterplotWidget::SCATTERPLOT: - break; + { + break; + } case ScatterplotWidget::DENSITY: - computeDensity(); - break; - case ScatterplotWidget::LANDSCAPE: - computeDensity(); - break; + { + computeDensity(); - default: - break; + break; + } } update(); @@ -366,9 +248,9 @@ PixelSelectionTool& ScatterplotWidget::getSamplerPixelSelectionTool() void ScatterplotWidget::computeDensity() { emit densityComputationStarted(); - - _densityRenderer.computeDensity(); - + { + _densityRenderer.computeDensity(); + } emit densityComputationEnded(); update(); @@ -381,45 +263,40 @@ void ScatterplotWidget::setData(const std::vector* points) { auto dataBounds = getDataBounds(*points); - // pass un-adjusted data bounds to renderer for 2D colormapping - _pointRenderer.setDataBounds(dataBounds); - - // Adjust data points for projection matrix creation (add a little white space around data) - dataBounds.ensureMinimumSize(1e-07f, 1e-07f); - dataBounds.makeSquare(); - dataBounds.expand(0.1f); - - const auto shouldSetBounds = (mv::projects().isOpeningProject() || mv::projects().isImportingProject()) ? false : !_navigationAction.getFreezeZoomAction().isChecked(); + const auto dataBoundsRect = QRectF(QPointF(dataBounds.getLeft(), dataBounds.getBottom()), QSizeF(dataBounds.getWidth(), dataBounds.getHeight())); - if (shouldSetBounds) - _pointRenderer.setViewBounds(dataBounds); - - _densityRenderer.setBounds(dataBounds); + _pointRenderer.setDataBounds(dataBoundsRect); + _densityRenderer.setDataBounds(dataBoundsRect); _dataRectangleAction.setBounds(dataBounds); - if (shouldSetBounds) - _navigationAction.getZoomRectangleAction().setBounds(dataBounds); + auto densityDataBounds = dataBounds; + + //densityDataBounds.ensureMinimumSize(1e-07f, 1e-07f); + //densityDataBounds.makeSquare(); + //densityDataBounds.expand(0.1f); + _densityRenderer.setDensityComputationDataBounds(QRectF(QPointF(densityDataBounds.getLeft(), densityDataBounds.getBottom()), QSizeF(densityDataBounds.getWidth(), densityDataBounds.getHeight()))); + _pointRenderer.setData(*points); _densityRenderer.setData(points); switch (_renderMode) { case ScatterplotWidget::SCATTERPLOT: + { + _pointRenderer.getNavigator().resetView(); break; + } case ScatterplotWidget::DENSITY: case ScatterplotWidget::LANDSCAPE: { + _densityRenderer.getNavigator().resetView(); _densityRenderer.computeDensity(); break; } - - default: - break; } - // _pointRenderer.setSelectionOutlineColor(Vector3f(1, 0, 0)); update(); } @@ -522,7 +399,7 @@ mv::Vector3f ScatterplotWidget::getColorMapRange() const break; } - return Vector3f(); + return {}; } void ScatterplotWidget::setColorMapRange(const float& min, const float& max) @@ -756,21 +633,6 @@ void ScatterplotWidget::initializeGL() void ScatterplotWidget::resizeGL(int w, int h) { - _widgetSizeInfo.width = static_cast(w); - _widgetSizeInfo.height = static_cast(h); - _widgetSizeInfo.minWH = _widgetSizeInfo.width < _widgetSizeInfo.height ? _widgetSizeInfo.width : _widgetSizeInfo.height; - _widgetSizeInfo.ratioWidth = _widgetSizeInfo.width / _widgetSizeInfo.minWH; - _widgetSizeInfo.ratioHeight = _widgetSizeInfo.height / _widgetSizeInfo.minWH; - - // we need this here as we do not have the screen yet to get the actual devicePixelRatio when the view is created - _pixelRatio = devicePixelRatio(); - - // Pixel ratio tells us how many pixels map to a point - // That is needed as macOS calculates in points and we do in pixels - // On macOS high dpi displays pixel ration is 2 - w *= _pixelRatio; - h *= _pixelRatio; - _pointRenderer.resize(QSize(w, h)); _densityRenderer.resize(QSize(w, h)); } diff --git a/src/ScatterplotWidget.h b/src/ScatterplotWidget.h index 1b9545c..cf0618a 100644 --- a/src/ScatterplotWidget.h +++ b/src/ScatterplotWidget.h @@ -1,7 +1,5 @@ #pragma once -#include "NavigationAction.h" - #include #include @@ -25,14 +23,6 @@ namespace mv::plugin class ViewPlugin; } -struct widgetSizeInfo { - float width; - float height; - float minWH; - float ratioWidth; - float ratioHeight; -}; - class ScatterplotWidget : public QOpenGLWidget, protected QOpenGLFunctions_3_3_Core { Q_OBJECT @@ -123,25 +113,6 @@ class ScatterplotWidget : public QOpenGLWidget, protected QOpenGLFunctions_3_3_C return _dataRectangleAction.getBounds(); } - /* - mv::Bounds getZoomBounds() const { - return _zoomBounds; - } - - void setZoomBounds(const mv::Bounds& newBounds) { - _zoomBounds = newBounds; - _pointRenderer.setBounds(_zoomBounds); - emit zoomBoundsChanged(_zoomBounds); - update(); - } - */ - - NavigationAction& getNavigationAction() { return _navigationAction; } - - bool isNavigating() const { - return _isNavigating; - } - mv::Vector3f getColorMapRange() const; void setColorMapRange(const float& min, const float& max); @@ -258,10 +229,6 @@ class ScatterplotWidget : public QOpenGLWidget, protected QOpenGLFunctions_3_3_C bool event(QEvent* event) override; - void zoomAround(const QPointF& at, float factor); - void panBy(const QPointF& to); - void resetView(); - public: // Const access to renderers const PointRenderer& getPointRenderer() const { @@ -277,6 +244,20 @@ class ScatterplotWidget : public QOpenGLWidget, protected QOpenGLFunctions_3_3_C /** Assign a color map image to the point and density renderers */ void setColorMap(const QImage& colorMapImage); +public: // Navigators + + /** + * Get the navigator for the point renderer + * @return Reference to the navigator + */ + mv::Navigator2D& getPointRendererNavigator() { return _pointRenderer.getNavigator(); } + + /** + * Get the navigator for the density renderer + * @return Reference to the navigator + */ + mv::Navigator2D& getDensityRendererNavigator() { return _densityRenderer.getNavigator(); } + signals: void initialized(); void created(); @@ -308,25 +289,24 @@ public slots: private slots: void updatePixelRatio(); -private: +protected: PointRenderer _pointRenderer; /** For rendering point data as points */ DensityRenderer _densityRenderer; /** For rendering point data as a density plot */ + +private: bool _isInitialized; /** Boolean determining whether the widget it properly initialized or not */ RenderMode _renderMode; /** Current render mode */ QColor _backgroundColor; /** Background color */ ColoringMode _coloringMode; /** Type of point/density coloring */ - widgetSizeInfo _widgetSizeInfo; /** Info about size of the scatterplot widget */ DecimalRectangleAction _dataRectangleAction; /** Rectangle action for the bounds of the loaded data */ - NavigationAction _navigationAction; /** All navigation-related actions are grouped in this action */ QImage _colorMapImage; /** 1D/2D color map image */ PixelSelectionTool _pixelSelectionTool; /** 2D pixel selection tool */ PixelSelectionTool _samplerPixelSelectionTool; /** 2D pixel selection tool */ float _pixelRatio; /** Current pixel ratio */ - QVector _mousePositions; /** Recorded mouse positions */ - bool _isNavigating; /** Boolean determining whether view navigation is currently taking place or not */ bool _weightDensity; /** Use point scalar sizes to weight density */ mv::plugin::ViewPlugin* _parentPlugin = nullptr; + friend class ScatterplotPlugin; friend class NavigationAction; }; diff --git a/src/SelectionAction.cpp b/src/SelectionAction.cpp index 3aca6e6..1e71d63 100644 --- a/src/SelectionAction.cpp +++ b/src/SelectionAction.cpp @@ -4,9 +4,6 @@ #include -#include -#include - using namespace mv::gui; SelectionAction::SelectionAction(QObject* parent, const QString& title) : @@ -17,7 +14,8 @@ SelectionAction::SelectionAction(QObject* parent, const QString& title) : _outlineOverrideColorAction(this, "Custom color", true), _outlineScaleAction(this, "Scale", 100.0f, 500.0f, 200.0f, 1), _outlineOpacityAction(this, "Opacity", 0.0f, 100.0f, 100.0f, 1), - _outlineHaloEnabledAction(this, "Halo") + _outlineHaloEnabledAction(this, "Halo"), + _freezeSelectionAction(this, "Freeze selection") { setIconByName("mouse-pointer"); @@ -35,6 +33,7 @@ SelectionAction::SelectionAction(QObject* parent, const QString& title) : addAction(&getOutlineScaleAction()); addAction(&getOutlineOpacityAction()); addAction(&getOutlineHaloEnabledAction()); + addAction(&getFreezeSelectionAction()); _pixelSelectionAction.getOverlayColorAction().setText("Color"); @@ -77,6 +76,8 @@ void SelectionAction::initialize(ScatterplotPlugin* scatterplotPlugin) PixelSelectionType::Sample }); + _samplerPixelSelectionAction.setShortcutsEnabled(false); + _displayModeAction.setCurrentIndex(static_cast(scatterplotPlugin->getScatterplotWidget().getSelectionDisplayMode())); _outlineScaleAction.setValue(100.0f * scatterplotPlugin->getScatterplotWidget().getSelectionOutlineScale()); _outlineOpacityAction.setValue(100.0f * scatterplotPlugin->getScatterplotWidget().getSelectionOutlineOpacity()); @@ -148,6 +149,7 @@ void SelectionAction::connectToPublicAction(WidgetAction* publicAction, bool rec actions().connectPrivateActionToPublicAction(&_outlineScaleAction, &publicSelectionAction->getOutlineScaleAction(), recursive); actions().connectPrivateActionToPublicAction(&_outlineOpacityAction, &publicSelectionAction->getOutlineOpacityAction(), recursive); actions().connectPrivateActionToPublicAction(&_outlineHaloEnabledAction, &publicSelectionAction->getOutlineHaloEnabledAction(), recursive); + actions().connectPrivateActionToPublicAction(&_freezeSelectionAction, &publicSelectionAction->getFreezeSelectionAction(), recursive); } GroupAction::connectToPublicAction(publicAction, recursive); @@ -165,6 +167,7 @@ void SelectionAction::disconnectFromPublicAction(bool recursive) actions().disconnectPrivateActionFromPublicAction(&_outlineScaleAction, recursive); actions().disconnectPrivateActionFromPublicAction(&_outlineOpacityAction, recursive); actions().disconnectPrivateActionFromPublicAction(&_outlineHaloEnabledAction, recursive); + actions().disconnectPrivateActionFromPublicAction(&_freezeSelectionAction, recursive); } GroupAction::disconnectFromPublicAction(recursive); @@ -181,6 +184,7 @@ void SelectionAction::fromVariantMap(const QVariantMap& variantMap) _outlineScaleAction.fromParentVariantMap(variantMap); _outlineOpacityAction.fromParentVariantMap(variantMap); _outlineHaloEnabledAction.fromParentVariantMap(variantMap); + _freezeSelectionAction.fromParentVariantMap(variantMap); } QVariantMap SelectionAction::toVariantMap() const @@ -194,6 +198,7 @@ QVariantMap SelectionAction::toVariantMap() const _outlineScaleAction.insertIntoVariantMap(variantMap); _outlineOpacityAction.insertIntoVariantMap(variantMap); _outlineHaloEnabledAction.insertIntoVariantMap(variantMap); + _freezeSelectionAction.insertIntoVariantMap(variantMap); return variantMap; } \ No newline at end of file diff --git a/src/SelectionAction.h b/src/SelectionAction.h index 708a454..d564c5e 100644 --- a/src/SelectionAction.h +++ b/src/SelectionAction.h @@ -64,6 +64,7 @@ class SelectionAction : public GroupAction DecimalAction& getOutlineScaleAction() { return _outlineScaleAction; } DecimalAction& getOutlineOpacityAction() { return _outlineOpacityAction; } ToggleAction& getOutlineHaloEnabledAction() { return _outlineHaloEnabledAction; } + ToggleAction& getFreezeSelectionAction() { return _freezeSelectionAction; } private: PixelSelectionAction _pixelSelectionAction; /** Pixel selection action */ @@ -73,6 +74,7 @@ class SelectionAction : public GroupAction DecimalAction _outlineScaleAction; /** Selection outline scale action */ DecimalAction _outlineOpacityAction; /** Selection outline opacity action */ ToggleAction _outlineHaloEnabledAction; /** Selection outline halo enabled action */ + ToggleAction _freezeSelectionAction; /** Freeze selection action */ friend class mv::AbstractActionsManager; }; diff --git a/src/SettingsAction.cpp b/src/SettingsAction.cpp index ef66b79..740da78 100644 --- a/src/SettingsAction.cpp +++ b/src/SettingsAction.cpp @@ -6,6 +6,8 @@ #include +#include "ScatterplotWidget.h" + using namespace mv::gui; SettingsAction::SettingsAction(QObject* parent, const QString& title) : @@ -71,6 +73,12 @@ void SettingsAction::fromVariantMap(const QVariantMap& variantMap) _renderModeAction.fromParentVariantMap(variantMap); _selectionAction.fromParentVariantMap(variantMap); _miscellaneousAction.fromParentVariantMap(variantMap); + + if (variantMap.contains("PointRendererNavigation")) + _scatterplotPlugin->getScatterplotWidget().getPointRendererNavigator().getNavigationAction().fromVariantMap(variantMap["PointRendererNavigation"].toMap()); + + if (variantMap.contains("DensityRendererNavigation")) + _scatterplotPlugin->getScatterplotWidget().getDensityRendererNavigator().getNavigationAction().fromVariantMap(variantMap["DensityRendererNavigation"].toMap()); } QVariantMap SettingsAction::toVariantMap() const @@ -85,5 +93,8 @@ QVariantMap SettingsAction::toVariantMap() const _selectionAction.insertIntoVariantMap(variantMap); _miscellaneousAction.insertIntoVariantMap(variantMap); + variantMap["PointRendererNavigation"] = _scatterplotPlugin->getScatterplotWidget().getPointRendererNavigator().getNavigationAction().toVariantMap(); + variantMap["DensityRendererNavigation"] = _scatterplotPlugin->getScatterplotWidget().getDensityRendererNavigator().getNavigationAction().toVariantMap(); + return variantMap; }