diff --git a/CMakeLists.txt b/CMakeLists.txt index 39453a5..638ece1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -63,10 +63,14 @@ set(Actions src/ScalarSourceAction.cpp src/SelectionAction.h src/SelectionAction.cpp + src/SelectionRestrictionAction.h + src/SelectionRestrictionAction.cpp src/SettingsAction.h src/SettingsAction.cpp src/SubsetAction.h src/SubsetAction.cpp + src/ZOrderingAction.h + src/ZOrderingAction.cpp src/ExportAction.h src/ExportAction.cpp src/DatasetsAction.h diff --git a/src/MiscellaneousAction.cpp b/src/MiscellaneousAction.cpp index 3222798..5747a22 100644 --- a/src/MiscellaneousAction.cpp +++ b/src/MiscellaneousAction.cpp @@ -10,15 +10,13 @@ const QColor MiscellaneousAction::DEFAULT_BACKGROUND_COLOR = qRgb(255, 255, 255) MiscellaneousAction::MiscellaneousAction(QObject* parent, const QString& title) : VerticalGroupAction(parent, title), _scatterplotPlugin(dynamic_cast(parent->parent())), - _backgroundColorAction(this, "Background color"), - _randomizedDepthAction(this, "Randomized depth", true) + _backgroundColorAction(this, "Background color") { setIconByName("cog"); setLabelSizingType(LabelSizingType::Auto); setConfigurationFlag(WidgetAction::ConfigurationFlag::ForceCollapsedInGroup); addAction(&_backgroundColorAction); - addAction(&_randomizedDepthAction); _backgroundColorAction.setColor(DEFAULT_BACKGROUND_COLOR); @@ -32,15 +30,6 @@ MiscellaneousAction::MiscellaneousAction(QObject* parent, const QString& title) updateBackgroundColor(); - const auto updateRandomizedDepth = [this]() -> void { - _scatterplotPlugin->getScatterplotWidget().setRandomizedDepthEnabled(_randomizedDepthAction.isChecked()); - }; - - connect(&_randomizedDepthAction, &ToggleAction::toggled, this, [this, updateRandomizedDepth](bool toggled) { - updateRandomizedDepth(); - }); - - updateRandomizedDepth(); } QMenu* MiscellaneousAction::getContextMenu() @@ -85,7 +74,6 @@ void MiscellaneousAction::fromVariantMap(const QVariantMap& variantMap) GroupAction::fromVariantMap(variantMap); _backgroundColorAction.fromParentVariantMap(variantMap); - _randomizedDepthAction.fromParentVariantMap(variantMap); } QVariantMap MiscellaneousAction::toVariantMap() const @@ -93,7 +81,6 @@ QVariantMap MiscellaneousAction::toVariantMap() const auto variantMap = GroupAction::toVariantMap(); _backgroundColorAction.insertIntoVariantMap(variantMap); - _randomizedDepthAction.insertIntoVariantMap(variantMap); return variantMap; -} \ No newline at end of file +} diff --git a/src/MiscellaneousAction.h b/src/MiscellaneousAction.h index 59189a4..2c7dfb8 100644 --- a/src/MiscellaneousAction.h +++ b/src/MiscellaneousAction.h @@ -2,7 +2,6 @@ #include #include -#include using namespace mv::gui; @@ -66,12 +65,10 @@ class MiscellaneousAction : public VerticalGroupAction public: // Action getters ColorAction& getBackgroundColorAction() { return _backgroundColorAction; } - ToggleAction& getRandomizedDepthAction() { return _randomizedDepthAction; } private: ScatterplotPlugin* _scatterplotPlugin; /** Pointer to scatter plot plugin */ ColorAction _backgroundColorAction; /** Color action for setting the background color action */ - ToggleAction _randomizedDepthAction; /** whether the z-order of each point is to be randomized or not */ static const QColor DEFAULT_BACKGROUND_COLOR; @@ -80,4 +77,4 @@ class MiscellaneousAction : public VerticalGroupAction Q_DECLARE_METATYPE(MiscellaneousAction) -inline const auto miscellaneousActionMetaTypeId = qRegisterMetaType("MiscellaneousAction"); \ No newline at end of file +inline const auto miscellaneousActionMetaTypeId = qRegisterMetaType("MiscellaneousAction"); diff --git a/src/ScatterplotPlugin.cpp b/src/ScatterplotPlugin.cpp index 2f18488..4c246a2 100644 --- a/src/ScatterplotPlugin.cpp +++ b/src/ScatterplotPlugin.cpp @@ -87,6 +87,7 @@ ScatterplotPlugin::ScatterplotPlugin(const PluginFactory* factory) : _primaryToolbarAction->addAction(&_settingsAction->getDatasetsAction()); _primaryToolbarAction->addAction(&_settingsAction->getRenderModeAction(), 3, GroupAction::Horizontal); _primaryToolbarAction->addAction(&_settingsAction->getPositionAction(), 1, GroupAction::Horizontal); + _primaryToolbarAction->addAction(&_settingsAction->getZOrderingAction(), 1, GroupAction::Horizontal); _primaryToolbarAction->addAction(&_settingsAction->getPlotAction(), 2, GroupAction::Horizontal); _primaryToolbarAction->addAction(&_settingsAction->getColoringAction()); _primaryToolbarAction->addAction(&_settingsAction->getSubsetAction()); @@ -457,6 +458,78 @@ void ScatterplotPlugin::createSubset(const bool& fromSourceData /*= false*/, con subset->getDataHierarchyItem().select(); } +void ScatterplotPlugin::selectAllEligiblePoints() +{ + if (!_positionDataset.isValid()) + return; + + std::vector globalIndices; + _positionDataset->getGlobalIndices(globalIndices); + + std::vector eligibleIndices; + eligibleIndices.reserve(globalIndices.size()); + + for (std::uint32_t localIndex = 0; localIndex < globalIndices.size(); ++localIndex) { + if (!_scatterPlotWidget->isSelectionExcluded(localIndex)) + eligibleIndices.push_back(globalIndices[localIndex]); + } + + _positionDataset->setSelectionIndices(eligibleIndices); + events().notifyDatasetDataSelectionChanged(_positionDataset->getSourceDataset()); +} + +void ScatterplotPlugin::invertEligiblePointSelection() +{ + if (!_positionDataset.isValid()) + return; + + const auto selection = _positionDataset->getSelection(); + + std::vector selected; + _positionDataset->selectedLocalIndices(selection->indices, selected); + + std::vector globalIndices; + _positionDataset->getGlobalIndices(globalIndices); + + std::vector invertedIndices; + invertedIndices.reserve(globalIndices.size()); + + for (std::uint32_t localIndex = 0; localIndex < globalIndices.size(); ++localIndex) { + if (!_scatterPlotWidget->isSelectionExcluded(localIndex) && (localIndex >= selected.size() || !selected[localIndex])) + invertedIndices.push_back(globalIndices[localIndex]); + } + + _positionDataset->setSelectionIndices(invertedIndices); + events().notifyDatasetDataSelectionChanged(_positionDataset->getSourceDataset()); +} + +void ScatterplotPlugin::filterSelectionExcludedIndices(std::vector& globalIndices) const +{ + if (!_positionDataset.isValid()) { + globalIndices.clear(); + return; + } + + std::vector selected; + _positionDataset->selectedLocalIndices(globalIndices, selected); + + std::vector localGlobalIndices; + _positionDataset->getGlobalIndices(localGlobalIndices); + + globalIndices.clear(); + globalIndices.reserve(localGlobalIndices.size()); + + for (std::uint32_t localIndex = 0; localIndex < localGlobalIndices.size(); ++localIndex) { + if (localIndex < selected.size() && selected[localIndex] && !_scatterPlotWidget->isSelectionExcluded(localIndex)) + globalIndices.push_back(localGlobalIndices[localIndex]); + } +} + +void ScatterplotPlugin::refreshSelection() +{ + updateSelection(); +} + void ScatterplotPlugin::selectPoints() { if (getSettingsAction().getSelectionAction().getFreezeSelectionAction().isChecked()) @@ -494,6 +567,9 @@ void ScatterplotPlugin::selectPoints() // Go over all points in the dataset to see if they are selected for (std::uint32_t localPointIndex = 0; localPointIndex < _positions.size(); localPointIndex++) { + if (_scatterPlotWidget->isSelectionExcluded(localPointIndex)) + continue; + const auto& point = _positions[localPointIndex]; // Compute the offset of the point in the world space @@ -568,6 +644,8 @@ void ScatterplotPlugin::selectPoints() } } + filterSelectionExcludedIndices(targetSelectionIndices); + auto& navigationAction = navigator.getNavigationAction(); navigationAction.getZoomSelectionAction().setEnabled(!targetSelectionIndices.empty() && navigationAction.isNavigationActive()); @@ -608,6 +686,9 @@ void ScatterplotPlugin::samplePoints() // Go over all points in the dataset to see if they should be sampled for (std::uint32_t localPointIndex = 0; localPointIndex < _positions.size(); localPointIndex++) { + if (_scatterPlotWidget->isSelectionExcluded(localPointIndex)) + continue; + // Compute the offset of the point in the world space const auto pointOffsetWorld = QPointF(_positions[localPointIndex].x - zoomRectangleWorld.left(), _positions[localPointIndex].y - zoomRectangleWorld.top()); @@ -989,6 +1070,7 @@ void ScatterplotPlugin::updateData() // Pass the 2D points to the scatter plot widget _scatterPlotWidget->setData(&_positions); + _settingsAction->getZOrderingAction().updateScatterplotWidget(); updateSelection(); } @@ -996,6 +1078,7 @@ void ScatterplotPlugin::updateData() _numPoints = 0; _positions.clear(); _scatterPlotWidget->setData(&_positions); + _settingsAction->getZOrderingAction().updateScatterplotWidget(); } } @@ -1029,14 +1112,16 @@ void ScatterplotPlugin::updateSelection() sampledPoints.reserve(_positions.size()); - for (auto selectionIndex : selection->indices) - sampledPoints.push_back(selectionIndex); + for (std::uint32_t localIndex = 0; localIndex < selected.size(); ++localIndex) { + if (selected[localIndex] && !_scatterPlotWidget->isSelectionExcluded(localIndex)) + sampledPoints.push_back(localIndex); + } std::int32_t numberOfPoints = 0; QVariantList localPointIndices, globalPointIndices; - const auto numberOfSelectedPoints = selection->indices.size(); + const auto numberOfSelectedPoints = sampledPoints.size(); localPointIndices.reserve(static_cast(numberOfSelectedPoints)); globalPointIndices.reserve(static_cast(numberOfSelectedPoints)); @@ -1072,6 +1157,8 @@ void ScatterplotPlugin::updateSelection() { "RenderMode", _settingsAction->getRenderModeAction().getCurrentText() } }); } + + updateHeadsUpDisplay(); } void ScatterplotPlugin::updateHeadsUpDisplay() @@ -1101,6 +1188,16 @@ void ScatterplotPlugin::updateHeadsUpDisplay() //qDebug() << "ScatterplotPlugin::updateHeadsUpDisplay: point size dataset: " << pointPlotAction.getSizeAction().getCurrentDataset().isValid() << ", opacity dataset: " << pointPlotAction.getOpacityAction().getCurrentDataset().isValid(); addMetaDataToHeadsUpDisplay("Size", pointPlotAction.getSizeAction().getCurrentDataset(), datasetsItem); addMetaDataToHeadsUpDisplay("Opacity", pointPlotAction.getOpacityAction().getCurrentDataset(), datasetsItem); + + const auto selectionItem = getHeadsUpDisplayAction().addHeadsUpDisplayItem("Selection", "", ""); + const auto numberOfSelectedPoints = _scatterPlotWidget->getNumberOfEffectivelySelectedPoints(); + const auto numberOfSelectablePoints = _scatterPlotWidget->getNumberOfSelectablePoints(); + + getHeadsUpDisplayAction().addHeadsUpDisplayItem( + "Selected:", + QString("%1 of %2 selectable points").arg(numberOfSelectedPoints).arg(numberOfSelectablePoints), + "", + selectionItem); } else { getHeadsUpDisplayAction().addHeadsUpDisplayItem("No datasets loaded", "", ""); } diff --git a/src/ScatterplotPlugin.h b/src/ScatterplotPlugin.h index ef32af8..12a5338 100644 --- a/src/ScatterplotPlugin.h +++ b/src/ScatterplotPlugin.h @@ -44,6 +44,9 @@ class ScatterplotPlugin : public ViewPlugin public: void createSubset(const bool& fromSourceData = false, const QString& name = ""); + void selectAllEligiblePoints(); + void invertEligiblePointSelection(); + void refreshSelection(); public: // Dimension picking void setXDimension(const std::int32_t& dimensionIndex); @@ -111,6 +114,7 @@ class ScatterplotPlugin : public ViewPlugin void updateData(); void updateSelection(); void updateHeadsUpDisplayTextColor(); + void filterSelectionExcludedIndices(std::vector& globalIndices) const; public: @@ -184,4 +188,4 @@ class ScatterplotPluginFactory : public ViewPluginFactory * @return URL of the GitHub repository (or readme markdown URL if set) */ QUrl getRepositoryUrl() const override; -}; \ No newline at end of file +}; diff --git a/src/ScatterplotWidget.cpp b/src/ScatterplotWidget.cpp index 837ed7c..8535092 100644 --- a/src/ScatterplotWidget.cpp +++ b/src/ScatterplotWidget.cpp @@ -286,6 +286,10 @@ void ScatterplotWidget::setData(const std::vector* points) _pointRenderer.setData(*points); _densityRenderer.setData(points); + _selectionExcludedIndices.clear(); + _selectionExclusionMask.assign(points->size(), 0); + updateEffectiveHighlights(); + switch (_renderMode) { case ScatterplotWidget::SCATTERPLOT: @@ -320,11 +324,84 @@ void ScatterplotWidget::setBackgroundColor(QColor color) void ScatterplotWidget::setHighlights(const std::vector& highlights, const std::int32_t& numSelectedPoints) { - _pointRenderer.setHighlights(highlights, numSelectedPoints); + Q_UNUSED(numSelectedPoints); + + _selectionHighlights = highlights; + updateEffectiveHighlights(); + + update(); +} + +void ScatterplotWidget::setSelectionExcludedIndices(const std::vector& excludedIndices) +{ + _selectionExcludedIndices.clear(); + _selectionExcludedIndices.reserve(excludedIndices.size()); + _selectionExclusionMask.assign(_pointRenderer.getGpuPoints().getPositions().size(), 0); + + for (const auto index : excludedIndices) { + if (index < _selectionExclusionMask.size() && _selectionExclusionMask[index] == 0) { + _selectionExcludedIndices.push_back(index); + _selectionExclusionMask[index] = 1; + } + } + + updateEffectiveHighlights(); + + update(); +} + +void ScatterplotWidget::clearSelectionExcludedIndices() +{ + _selectionExcludedIndices.clear(); + _selectionExclusionMask.assign(_pointRenderer.getGpuPoints().getPositions().size(), 0); + updateEffectiveHighlights(); update(); } +const std::vector& ScatterplotWidget::getSelectionExcludedIndices() const +{ + return _selectionExcludedIndices; +} + +bool ScatterplotWidget::isSelectionExcluded(std::uint32_t localPointIndex) const +{ + return localPointIndex < _selectionExclusionMask.size() && _selectionExclusionMask[localPointIndex] != 0; +} + +std::uint32_t ScatterplotWidget::getNumberOfSelectablePoints() const +{ + return static_cast(_selectionExclusionMask.size() - _selectionExcludedIndices.size()); +} + +std::uint32_t ScatterplotWidget::getNumberOfEffectivelySelectedPoints() const +{ + std::uint32_t numberOfSelectedPoints = 0; + + for (std::uint32_t index = 0; index < _selectionHighlights.size(); ++index) { + if (_selectionHighlights[index] != 0 && !isSelectionExcluded(index)) + ++numberOfSelectedPoints; + } + + return numberOfSelectedPoints; +} + +void ScatterplotWidget::updateEffectiveHighlights() +{ + auto effectiveHighlights = _selectionHighlights; + std::int32_t numberOfEffectiveHighlights = 0; + + for (std::size_t index = 0; index < effectiveHighlights.size(); ++index) { + if (isSelectionExcluded(static_cast(index))) + effectiveHighlights[index] = -1; + + if (effectiveHighlights[index] > 0) + ++numberOfEffectiveHighlights; + } + + _pointRenderer.setHighlights(effectiveHighlights, numberOfEffectiveHighlights); +} + void ScatterplotWidget::setScalars(const std::vector& scalars) { _pointRenderer.setColorChannelScalars(scalars); @@ -613,6 +690,25 @@ void ScatterplotWidget::setRandomizedDepthEnabled(bool randomizedDepth) update(); } +PointZOrderMode ScatterplotWidget::getZOrderMode() const +{ + return _pointRenderer.getZOrderMode(); +} + +void ScatterplotWidget::setZOrderMode(PointZOrderMode zOrderMode) +{ + _pointRenderer.setZOrderMode(zOrderMode); + + update(); +} + +void ScatterplotWidget::setZOrderScalars(const std::vector& zOrderScalars) +{ + _pointRenderer.setZOrderChannelScalars(zOrderScalars); + + update(); +} + bool ScatterplotWidget::getRandomizedDepthEnabled() const { return _pointRenderer.getRandomizedDepthEnabled(); @@ -679,8 +775,10 @@ void ScatterplotWidget::paintGL() // Reset the blending function glEnable(GL_BLEND); - if (getRandomizedDepthEnabled()) + if (_renderMode == SCATTERPLOT && getZOrderMode() != PointZOrderMode::InsertionOrder) glEnable(GL_DEPTH_TEST); + else + glDisable(GL_DEPTH_TEST); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); diff --git a/src/ScatterplotWidget.h b/src/ScatterplotWidget.h index 4441bd6..818c22e 100644 --- a/src/ScatterplotWidget.h +++ b/src/ScatterplotWidget.h @@ -80,6 +80,17 @@ class ScatterplotWidget : public QOpenGLWidget, protected QOpenGLFunctions_3_3_C void setHighlights(const std::vector& highlights, const std::int32_t& numSelectedPoints); void setScalars(const std::vector& scalars); + /** + * Exclude local point indices from selection in this scatterplot. + * Exclusions are applied to both interactive and externally supplied selections. + */ + void setSelectionExcludedIndices(const std::vector& excludedIndices); + void clearSelectionExcludedIndices(); + const std::vector& getSelectionExcludedIndices() const; + bool isSelectionExcluded(std::uint32_t localPointIndex) const; + std::uint32_t getNumberOfSelectablePoints() const; + std::uint32_t getNumberOfEffectivelySelectedPoints() const; + /** Set the second color scalar channel (used for 2D and RGB coloring) */ void setScalars2(const std::vector& scalars); @@ -104,6 +115,13 @@ class ScatterplotWidget : public QOpenGLWidget, protected QOpenGLFunctions_3_3_C */ void setPointOpacityScalars(const std::vector& pointOpacityScalars); + /** Get/set how point depth is determined. */ + PointZOrderMode getZOrderMode() const; + void setZOrderMode(PointZOrderMode zOrderMode); + + /** Set the scalar channel used by data-driven z ordering. */ + void setZOrderScalars(const std::vector& zOrderScalars); + void setScalarEffect(PointEffect effect); void setPointScaling(PointScaling scalingMode); @@ -203,16 +221,13 @@ class ScatterplotWidget : public QOpenGLWidget, protected QOpenGLFunctions_3_3_C /** * Set whether the selection outline halo is enabled or not - * @param randomizedDepth Boolean determining whether the selection outline halo is enabled or not - */ - void setRandomizedDepthEnabled(bool randomizedDepth); - - /** - * Set whether the z-order of each point is to be randomized or not - * @param selectionOutlineHaloEnabled Boolean determining whether the z-order of each point is to be randomized or not + * @param selectionOutlineHaloEnabled Boolean determining whether the selection outline halo is enabled or not */ void setSelectionOutlineHaloEnabled(bool selectionOutlineHaloEnabled); + /** Compatibility wrapper for selecting randomized or insertion-order depth. */ + void setRandomizedDepthEnabled(bool randomizedDepth); + /** * Get whether the z-order of each point is to be randomized or not * @return Boolean determining whether the z-order of each point is to be randomized or not @@ -295,6 +310,9 @@ public slots: private slots: void updatePixelRatio(); +private: + void updateEffectiveHighlights(); + protected: PointRenderer _pointRenderer; /** For rendering point data as points */ DensityRenderer _densityRenderer; /** For rendering point data as a density plot */ @@ -311,6 +329,9 @@ private slots: PixelSelectionTool _samplerPixelSelectionTool; /** 2D pixel selection tool */ float _pixelRatio; /** Current pixel ratio */ bool _weightDensity; /** Use point scalar sizes to weight density */ + std::vector _selectionHighlights; /** Selection highlights before local exclusions */ + std::vector _selectionExcludedIndices; /** Local point indices excluded from selection */ + std::vector _selectionExclusionMask; /** Mask of points excluded from selection */ mv::plugin::ViewPlugin* _parentPlugin = nullptr; diff --git a/src/SelectionAction.cpp b/src/SelectionAction.cpp index 0135b40..72b6aa8 100644 --- a/src/SelectionAction.cpp +++ b/src/SelectionAction.cpp @@ -15,7 +15,8 @@ SelectionAction::SelectionAction(QObject* parent, const QString& title) : _outlineScaleAction(this, "Scale", 100.0f, 500.0f, 200.0f, 1), _outlineOpacityAction(this, "Opacity", 0.0f, 100.0f, 100.0f, 1), _outlineHaloEnabledAction(this, "Halo"), - _freezeSelectionAction(this, "Freeze selection") + _freezeSelectionAction(this, "Freeze selection"), + _selectionRestrictionAction(this, "Selection restriction") { setIconByName("mouse-pointer"); @@ -36,6 +37,7 @@ SelectionAction::SelectionAction(QObject* parent, const QString& title) : addAction(&getOutlineOpacityAction()); addAction(&getOutlineHaloEnabledAction()); addAction(&getFreezeSelectionAction()); + addAction(&_selectionRestrictionAction); _pixelSelectionAction.getOverlayColorAction().setText("Color"); @@ -88,9 +90,11 @@ void SelectionAction::initialize(ScatterplotPlugin* scatterplotPlugin) _outlineHaloEnabledAction.setChecked(scatterplotPlugin->getScatterplotWidget().getSelectionOutlineHaloEnabled()); _outlineOverrideColorAction.setChecked(scatterplotPlugin->getScatterplotWidget().getSelectionOutlineOverrideColor()); + _selectionRestrictionAction.initialize(scatterplotPlugin); + connect(&_pixelSelectionAction.getSelectAllAction(), &QAction::triggered, [this, scatterplotPlugin]() { if (scatterplotPlugin->getPositionDataset().isValid()) - scatterplotPlugin->getPositionDataset()->selectAll(); + scatterplotPlugin->selectAllEligiblePoints(); }); connect(&_pixelSelectionAction.getClearSelectionAction(), &QAction::triggered, this, [this, scatterplotPlugin]() { @@ -100,7 +104,7 @@ void SelectionAction::initialize(ScatterplotPlugin* scatterplotPlugin) connect(&_pixelSelectionAction.getInvertSelectionAction(), &QAction::triggered, this, [this, scatterplotPlugin]() { if (scatterplotPlugin->getPositionDataset().isValid()) - scatterplotPlugin->getPositionDataset()->selectInvert(); + scatterplotPlugin->invertEligiblePointSelection(); }); connect(&_outlineScaleAction, &DecimalAction::valueChanged, this, [this, scatterplotPlugin](float value) { @@ -153,6 +157,7 @@ void SelectionAction::connectToPublicAction(WidgetAction* publicAction, bool rec actions().connectPrivateActionToPublicAction(&_outlineOpacityAction, &publicSelectionAction->getOutlineOpacityAction(), recursive); actions().connectPrivateActionToPublicAction(&_outlineHaloEnabledAction, &publicSelectionAction->getOutlineHaloEnabledAction(), recursive); actions().connectPrivateActionToPublicAction(&_freezeSelectionAction, &publicSelectionAction->getFreezeSelectionAction(), recursive); + actions().connectPrivateActionToPublicAction(&_selectionRestrictionAction, &publicSelectionAction->getSelectionRestrictionAction(), recursive); } GroupAction::connectToPublicAction(publicAction, recursive); @@ -171,6 +176,7 @@ void SelectionAction::disconnectFromPublicAction(bool recursive) actions().disconnectPrivateActionFromPublicAction(&_outlineOpacityAction, recursive); actions().disconnectPrivateActionFromPublicAction(&_outlineHaloEnabledAction, recursive); actions().disconnectPrivateActionFromPublicAction(&_freezeSelectionAction, recursive); + actions().disconnectPrivateActionFromPublicAction(&_selectionRestrictionAction, recursive); } GroupAction::disconnectFromPublicAction(recursive); @@ -188,6 +194,7 @@ void SelectionAction::fromVariantMap(const QVariantMap& variantMap) _outlineOpacityAction.fromParentVariantMap(variantMap); _outlineHaloEnabledAction.fromParentVariantMap(variantMap); _freezeSelectionAction.fromParentVariantMap(variantMap); + _selectionRestrictionAction.fromParentVariantMap(variantMap, true); } QVariantMap SelectionAction::toVariantMap() const @@ -202,6 +209,7 @@ QVariantMap SelectionAction::toVariantMap() const _outlineOpacityAction.insertIntoVariantMap(variantMap); _outlineHaloEnabledAction.insertIntoVariantMap(variantMap); _freezeSelectionAction.insertIntoVariantMap(variantMap); + _selectionRestrictionAction.insertIntoVariantMap(variantMap); return variantMap; -} \ No newline at end of file +} diff --git a/src/SelectionAction.h b/src/SelectionAction.h index d564c5e..0f3f574 100644 --- a/src/SelectionAction.h +++ b/src/SelectionAction.h @@ -3,6 +3,8 @@ #include #include +#include "SelectionRestrictionAction.h" + class ScatterplotPlugin; using namespace mv::gui; @@ -65,6 +67,7 @@ class SelectionAction : public GroupAction DecimalAction& getOutlineOpacityAction() { return _outlineOpacityAction; } ToggleAction& getOutlineHaloEnabledAction() { return _outlineHaloEnabledAction; } ToggleAction& getFreezeSelectionAction() { return _freezeSelectionAction; } + SelectionRestrictionAction& getSelectionRestrictionAction() { return _selectionRestrictionAction; } private: PixelSelectionAction _pixelSelectionAction; /** Pixel selection action */ @@ -75,10 +78,11 @@ class SelectionAction : public GroupAction DecimalAction _outlineOpacityAction; /** Selection outline opacity action */ ToggleAction _outlineHaloEnabledAction; /** Selection outline halo enabled action */ ToggleAction _freezeSelectionAction; /** Freeze selection action */ + SelectionRestrictionAction _selectionRestrictionAction; /** Dimension-based selection restriction */ friend class mv::AbstractActionsManager; }; Q_DECLARE_METATYPE(SelectionAction) -inline const auto selectionActionMetaTypeId = qRegisterMetaType("SelectionAction"); \ No newline at end of file +inline const auto selectionActionMetaTypeId = qRegisterMetaType("SelectionAction"); diff --git a/src/SelectionRestrictionAction.cpp b/src/SelectionRestrictionAction.cpp new file mode 100644 index 0000000..0d78f00 --- /dev/null +++ b/src/SelectionRestrictionAction.cpp @@ -0,0 +1,248 @@ +#include "SelectionRestrictionAction.h" + +#include "ScatterplotPlugin.h" +#include "ScatterplotWidget.h" +#include "SelectionAction.h" +#include "SettingsAction.h" + +#include +#include +#include + +SelectionRestrictionAction::SelectionRestrictionAction(QObject* parent, const QString& title) : + VerticalGroupAction(parent, title), + _enabledAction(this, "Restrict selection by dimension", false), + _dimensionPickerAction(this, "Dimension"), + _rangeAction(this, "Selectable range", util::NumericalRange(0.0f, 1.0f), util::NumericalRange(0.0f, 1.0f), 3) +{ + setIconByName("filter"); + setLabelSizingType(LabelSizingType::Auto); + setConfigurationFlag(WidgetAction::ConfigurationFlag::ForceCollapsedInGroup); + setShowLabels(false); + + addAction(&_enabledAction); + addAction(&_dimensionPickerAction); + addAction(&_rangeAction); + + _enabledAction.setToolTip("Only allow points within a dimension value range to be selected"); + _dimensionPickerAction.setToolTip("Dimension whose values determine whether points are selectable"); + _rangeAction.setToolTip("Inclusive range of dimension values for selectable points"); +} + +void SelectionRestrictionAction::initialize(ScatterplotPlugin* scatterplotPlugin) +{ + Q_ASSERT(scatterplotPlugin != nullptr); + + if (scatterplotPlugin == nullptr) + return; + + _scatterplotPlugin = scatterplotPlugin; + + connect(&_enabledAction, &ToggleAction::toggled, this, [this]() { + updateActionsReadOnly(); + updateSelectionExclusions(); + }); + + connect(&_dimensionPickerAction, &DimensionPickerAction::currentDimensionIndexChanged, this, [this]() { + updateDimensionValues(true); + }); + + connect(&_rangeAction, &DecimalRangeAction::rangeChanged, this, [this]() { + if (!_updatingRange) + updateSelectionExclusions(); + }); + + connect(&_scatterplotPlugin->getPositionDataset(), &Dataset::changed, this, &SelectionRestrictionAction::updateDataset); + connect(&_scatterplotPlugin->getPositionDataset(), &Dataset::dataDimensionsChanged, this, &SelectionRestrictionAction::updateDataset); + connect(&_scatterplotPlugin->getPositionDataset(), &Dataset::dataChanged, this, [this]() { + updateDimensionValues(false); + }); + + updateDataset(); +} + +void SelectionRestrictionAction::updateDataset() +{ + if (_scatterplotPlugin == nullptr) + return; + + auto& positionDataset = _scatterplotPlugin->getPositionDataset(); + + if (!positionDataset.isValid()) { + _dimensionPickerAction.setPointsDataset(Dataset()); + _dimensionValues.clear(); + updateActionsReadOnly(); + updateSelectionExclusions(); + return; + } + + auto dimensionIndex = _dimensionPickerAction.getCurrentDimensionIndex(); + const auto numberOfDimensions = static_cast(positionDataset->getNumDimensions()); + + if (dimensionIndex < 0 || dimensionIndex >= numberOfDimensions) + dimensionIndex = 0; + + _dimensionPickerAction.setPointsDataset(positionDataset); + _dimensionPickerAction.setCurrentDimensionIndex(dimensionIndex); + updateDimensionValues(true); +} + +void SelectionRestrictionAction::updateDimensionValues(bool resetRange) +{ + _dimensionValues.clear(); + + if (_scatterplotPlugin == nullptr) + return; + + auto& positionDataset = _scatterplotPlugin->getPositionDataset(); + const auto dimensionIndex = _dimensionPickerAction.getCurrentDimensionIndex(); + + if (positionDataset.isValid() && dimensionIndex >= 0 && dimensionIndex < static_cast(positionDataset->getNumDimensions())) + positionDataset->extractDataForDimension(_dimensionValues, dimensionIndex); + + auto minimum = std::numeric_limits::max(); + auto maximum = std::numeric_limits::lowest(); + + for (const auto value : _dimensionValues) { + if (!std::isfinite(value)) + continue; + + minimum = std::min(minimum, value); + maximum = std::max(maximum, value); + } + + if (minimum > maximum) { + minimum = 0.0f; + maximum = 1.0f; + } + + const auto previousMinimum = _rangeAction.getMinimum(); + const auto previousMaximum = _rangeAction.getMaximum(); + + _updatingRange = true; + + if (minimum > _rangeAction.getLimitsMaximum()) { + _rangeAction.setLimitsMaximum(maximum); + _rangeAction.setLimitsMinimum(minimum); + } + else { + _rangeAction.setLimitsMinimum(minimum); + _rangeAction.setLimitsMaximum(maximum); + } + + if (resetRange) { + _rangeAction.setRange(util::NumericalRange(minimum, maximum)); + } + else { + const auto rangeMinimum = std::clamp(previousMinimum, minimum, maximum); + const auto rangeMaximum = std::clamp(previousMaximum, rangeMinimum, maximum); + + _rangeAction.setRange(util::NumericalRange(rangeMinimum, rangeMaximum)); + } + + _updatingRange = false; + + updateActionsReadOnly(); + updateSelectionExclusions(); +} + +void SelectionRestrictionAction::updateActionsReadOnly() +{ + const auto restrictionAvailable = _scatterplotPlugin != nullptr && + _scatterplotPlugin->getPositionDataset().isValid() && + !_dimensionValues.empty(); + + setEnabled(_scatterplotPlugin != nullptr && _scatterplotPlugin->getPositionDataset().isValid()); + _enabledAction.setEnabled(restrictionAvailable); + _dimensionPickerAction.setEnabled(restrictionAvailable); + _rangeAction.setEnabled(restrictionAvailable && _enabledAction.isChecked()); +} + +void SelectionRestrictionAction::updateSelectionExclusions() +{ + if (_scatterplotPlugin == nullptr) + return; + + auto& scatterplotWidget = _scatterplotPlugin->getScatterplotWidget(); + auto& selectAllAction = dynamic_cast(parent())->getPixelSelectionAction().getSelectAllAction(); + const auto restrictionActive = _enabledAction.isEnabled() && _enabledAction.isChecked(); + + if (!restrictionActive) { + selectAllAction.setText("Select all"); + selectAllAction.setToolTip("Select all points"); + scatterplotWidget.clearSelectionExcludedIndices(); + _scatterplotPlugin->refreshSelection(); + return; + } + + std::vector excludedIndices; + excludedIndices.reserve(_dimensionValues.size()); + + const auto minimum = _rangeAction.getMinimum(); + const auto maximum = _rangeAction.getMaximum(); + + for (std::uint32_t index = 0; index < _dimensionValues.size(); ++index) { + const auto value = _dimensionValues[index]; + + if (!std::isfinite(value) || value < minimum || value > maximum) + excludedIndices.push_back(index); + } + + scatterplotWidget.setSelectionExcludedIndices(excludedIndices); + selectAllAction.setText("Select all selectable points"); + selectAllAction.setToolTip(QString("Select all points within the dimension range (%1 excluded)").arg(excludedIndices.size())); + _scatterplotPlugin->refreshSelection(); +} + +void SelectionRestrictionAction::connectToPublicAction(WidgetAction* publicAction, bool recursive) +{ + auto publicRestrictionAction = dynamic_cast(publicAction); + + Q_ASSERT(publicRestrictionAction != nullptr); + + if (publicRestrictionAction == nullptr) + return; + + if (recursive) { + actions().connectPrivateActionToPublicAction(&_enabledAction, &publicRestrictionAction->getEnabledAction(), recursive); + actions().connectPrivateActionToPublicAction(&_dimensionPickerAction, &publicRestrictionAction->getDimensionPickerAction(), recursive); + actions().connectPrivateActionToPublicAction(&_rangeAction, &publicRestrictionAction->getRangeAction(), recursive); + } + + GroupAction::connectToPublicAction(publicAction, recursive); +} + +void SelectionRestrictionAction::disconnectFromPublicAction(bool recursive) +{ + if (!isConnected()) + return; + + if (recursive) { + actions().disconnectPrivateActionFromPublicAction(&_enabledAction, recursive); + actions().disconnectPrivateActionFromPublicAction(&_dimensionPickerAction, recursive); + actions().disconnectPrivateActionFromPublicAction(&_rangeAction, recursive); + } + + GroupAction::disconnectFromPublicAction(recursive); +} + +void SelectionRestrictionAction::fromVariantMap(const QVariantMap& variantMap) +{ + GroupAction::fromVariantMap(variantMap); + + _dimensionPickerAction.fromParentVariantMap(variantMap); + _rangeAction.fromParentVariantMap(variantMap); + _enabledAction.fromParentVariantMap(variantMap); + updateSelectionExclusions(); +} + +QVariantMap SelectionRestrictionAction::toVariantMap() const +{ + auto variantMap = GroupAction::toVariantMap(); + + _enabledAction.insertIntoVariantMap(variantMap); + _dimensionPickerAction.insertIntoVariantMap(variantMap); + _rangeAction.insertIntoVariantMap(variantMap); + + return variantMap; +} diff --git a/src/SelectionRestrictionAction.h b/src/SelectionRestrictionAction.h new file mode 100644 index 0000000..75631a7 --- /dev/null +++ b/src/SelectionRestrictionAction.h @@ -0,0 +1,54 @@ +#pragma once + +#include +#include +#include + +#include + +class ScatterplotPlugin; + +using namespace mv::gui; + +/** Action for restricting selection eligibility to a dimension value range. */ +class SelectionRestrictionAction : public VerticalGroupAction +{ + Q_OBJECT + +public: + Q_INVOKABLE SelectionRestrictionAction(QObject* parent, const QString& title); + + void initialize(ScatterplotPlugin* scatterplotPlugin); + void updateSelectionExclusions(); + +protected: // Linking + void connectToPublicAction(WidgetAction* publicAction, bool recursive) override; + void disconnectFromPublicAction(bool recursive) override; + +public: // Serialization + void fromVariantMap(const QVariantMap& variantMap) override; + QVariantMap toVariantMap() const override; + +public: // Action getters + ToggleAction& getEnabledAction() { return _enabledAction; } + DimensionPickerAction& getDimensionPickerAction() { return _dimensionPickerAction; } + DecimalRangeAction& getRangeAction() { return _rangeAction; } + +private: + void updateDataset(); + void updateDimensionValues(bool resetRange); + void updateActionsReadOnly(); + + ScatterplotPlugin* _scatterplotPlugin = nullptr; + ToggleAction _enabledAction; + DimensionPickerAction _dimensionPickerAction; + DecimalRangeAction _rangeAction; + std::vector _dimensionValues; + bool _updatingRange = false; + + friend class mv::AbstractActionsManager; +}; + +Q_DECLARE_METATYPE(SelectionRestrictionAction) + +inline const auto selectionRestrictionActionMetaTypeId = qRegisterMetaType("SelectionRestrictionAction"); diff --git a/src/SettingsAction.cpp b/src/SettingsAction.cpp index f0739dc..c446a1b 100644 --- a/src/SettingsAction.cpp +++ b/src/SettingsAction.cpp @@ -15,11 +15,12 @@ SettingsAction::SettingsAction(QObject* parent, const QString& title) : _scatterplotPlugin(dynamic_cast(parent)), _renderModeAction(this, "Render Mode"), _positionAction(this, "Position"), + _selectionAction(this, "Selection"), + _zOrderingAction(this, "Z ordering"), _plotAction(this, "Plot"), _coloringAction(this, "Coloring"), _subsetAction(this, "Subset"), _clusteringAction(this, "Clustering"), - _selectionAction(this, "Selection"), _exportAction(this, "Export"), _miscellaneousAction(this, "Miscellaneous"), _datasetsAction(this, "Datasets") @@ -27,9 +28,10 @@ SettingsAction::SettingsAction(QObject* parent, const QString& title) : setConnectionPermissionsToForceNone(); _renderModeAction.initialize(_scatterplotPlugin); + _selectionAction.initialize(_scatterplotPlugin); + _zOrderingAction.initialize(_scatterplotPlugin); _plotAction.initialize(_scatterplotPlugin); _subsetAction.initialize(_scatterplotPlugin); - _selectionAction.initialize(_scatterplotPlugin); _exportAction.initialize(_scatterplotPlugin); const auto updateEnabled = [this]() { @@ -38,6 +40,7 @@ SettingsAction::SettingsAction(QObject* parent, const QString& title) : _plotAction.setEnabled(enabled); _positionAction.setEnabled(enabled); _coloringAction.setEnabled(enabled); + _zOrderingAction.setEnabled(enabled); }; updateEnabled(); @@ -53,6 +56,7 @@ QMenu* SettingsAction::getContextMenu() menu->addMenu(_plotAction.getContextMenu()); menu->addSeparator(); menu->addMenu(_positionAction.getContextMenu()); + menu->addMenu(_zOrderingAction.getContextMenu()); menu->addMenu(_coloringAction.getContextMenu()); menu->addSeparator(); menu->addMenu(_subsetAction.getContextMenu()); @@ -66,16 +70,31 @@ void SettingsAction::fromVariantMap(const QVariantMap& variantMap) { WidgetAction::fromVariantMap(variantMap); + const auto containsZOrderingSettings = variantMap.contains("Z ordering"); + _datasetsAction.fromParentVariantMap(variantMap); _plotAction.fromParentVariantMap(variantMap); _positionAction.fromParentVariantMap(variantMap); + _selectionAction.fromParentVariantMap(variantMap); + _zOrderingAction.fromParentVariantMap(variantMap, true); _coloringAction.fromParentVariantMap(variantMap); _subsetAction.fromParentVariantMap(variantMap, true); _clusteringAction.fromParentVariantMap(variantMap, true); _renderModeAction.fromParentVariantMap(variantMap); - _selectionAction.fromParentVariantMap(variantMap); _miscellaneousAction.fromParentVariantMap(variantMap); + // Migrate projects saved before z ordering became a dedicated action. + if (!containsZOrderingSettings) { + const auto miscellaneousMap = variantMap.value("Miscellaneous").toMap(); + const auto randomizedDepthMap = miscellaneousMap.value("Randomized depth").toMap(); + + if (!randomizedDepthMap.isEmpty()) { + const auto mode = randomizedDepthMap.value("Value").toBool() ? ZOrderingAction::Mode::Randomized : ZOrderingAction::Mode::InsertionOrder; + + _zOrderingAction.getModeAction().setCurrentIndex(static_cast(mode)); + } + } + if (variantMap.contains("PointRendererNavigation")) _scatterplotPlugin->getScatterplotWidget().getPointRendererNavigator().getNavigationAction().fromVariantMap(variantMap["PointRendererNavigation"].toMap()); @@ -91,6 +110,7 @@ QVariantMap SettingsAction::toVariantMap() const _renderModeAction.insertIntoVariantMap(variantMap); _plotAction.insertIntoVariantMap(variantMap); _positionAction.insertIntoVariantMap(variantMap); + _zOrderingAction.insertIntoVariantMap(variantMap); _coloringAction.insertIntoVariantMap(variantMap); _subsetAction.insertIntoVariantMap(variantMap); _clusteringAction.insertIntoVariantMap(variantMap); diff --git a/src/SettingsAction.h b/src/SettingsAction.h index a6b0ba9..f066cd7 100644 --- a/src/SettingsAction.h +++ b/src/SettingsAction.h @@ -12,6 +12,7 @@ #include "RenderModeAction.h" #include "SelectionAction.h" #include "SubsetAction.h" +#include "ZOrderingAction.h" using namespace mv::gui; @@ -59,6 +60,7 @@ class SettingsAction : public GroupAction RenderModeAction& getRenderModeAction() { return _renderModeAction; } PositionAction& getPositionAction() { return _positionAction; } + ZOrderingAction& getZOrderingAction() { return _zOrderingAction; } PlotAction& getPlotAction() { return _plotAction; } ColoringAction& getColoringAction() { return _coloringAction; } SubsetAction& getSubsetAction() { return _subsetAction; } @@ -72,11 +74,12 @@ class SettingsAction : public GroupAction ScatterplotPlugin* _scatterplotPlugin; /** Pointer to scatter plot plugin */ RenderModeAction _renderModeAction; /** Action for configuring render mode */ PositionAction _positionAction; /** Action for configuring point positions */ + SelectionAction _selectionAction; /** Action for selecting points */ + ZOrderingAction _zOrderingAction; /** Action for configuring point z ordering */ PlotAction _plotAction; /** Action for configuring plot settings */ ColoringAction _coloringAction; /** Action for configuring point coloring */ SubsetAction _subsetAction; /** Action for creating subset(s) */ ClusteringAction _clusteringAction; /** Action for creating clusters */ - SelectionAction _selectionAction; /** Action for selecting points */ ExportAction _exportAction; /** Action for exporting */ MiscellaneousAction _miscellaneousAction; /** Action for miscellaneous settings */ DatasetsAction _datasetsAction; /** Action for picking dataset(s) */ diff --git a/src/ZOrderingAction.cpp b/src/ZOrderingAction.cpp new file mode 100644 index 0000000..60acfd7 --- /dev/null +++ b/src/ZOrderingAction.cpp @@ -0,0 +1,308 @@ +#include "ZOrderingAction.h" + +#include "ScatterplotPlugin.h" +#include "ScatterplotWidget.h" +#include "SelectionAction.h" +#include "SelectionRestrictionAction.h" +#include "SettingsAction.h" + +#include +#include + +ZOrderingAction::ZOrderingAction(QObject* parent, const QString& title) : + VerticalGroupAction(parent, title), + _modeAction(this, "Mode", { "Insertion order", "Dimension", "Randomized" }), + _dimensionPickerAction(this, "Dimension"), + _restrictSelectionByZOrderAction(this, "Restrict selection by Z-order dimension", false) +{ + setIconByName("sort"); + setLabelSizingType(LabelSizingType::Auto); + setConfigurationFlag(WidgetAction::ConfigurationFlag::ForceCollapsedInGroup); + + addAction(&_modeAction, OptionAction::HorizontalButtons); + addAction(&_dimensionPickerAction); + addAction(&_restrictSelectionByZOrderAction); + + auto& selectionRangeAction = dynamic_cast(parent)->getSelectionAction().getSelectionRestrictionAction().getRangeAction(); + + addAction(&selectionRangeAction, -1, [this, &selectionRangeAction](WidgetAction*, QWidget* widget) { + const auto updateReadOnly = [this, &selectionRangeAction, widget]() { + widget->setEnabled( + selectionRangeAction.isEnabled() && + _restrictSelectionByZOrderAction.isEnabled() && + _restrictSelectionByZOrderAction.isChecked()); + }; + + connect(&_restrictSelectionByZOrderAction, &ToggleAction::toggled, widget, updateReadOnly); + connect(&_restrictSelectionByZOrderAction, &QAction::enabledChanged, widget, updateReadOnly); + connect(&selectionRangeAction, &QAction::enabledChanged, widget, updateReadOnly); + + updateReadOnly(); + }); + + _modeAction.setToolTip("Choose how overlapping points are ordered"); + _dimensionPickerAction.setToolTip("Dimension whose numerical values determine point depth"); + _restrictSelectionByZOrderAction.setToolTip("Restrict selection using the current Z-order dimension and the selectable range below"); + _dimensionPickerAction.setEnabled(false); + _restrictSelectionByZOrderAction.setEnabled(false); +} + +void ZOrderingAction::initialize(ScatterplotPlugin* scatterplotPlugin) +{ + Q_ASSERT(scatterplotPlugin != nullptr); + + if (scatterplotPlugin == nullptr) + return; + + _scatterplotPlugin = scatterplotPlugin; + + auto& selectionRestrictionAction = dynamic_cast(parent())->getSelectionAction().getSelectionRestrictionAction(); + + const auto updateDataset = [this]() { + auto& positionDataset = _scatterplotPlugin->getPositionDataset(); + + if (!positionDataset.isValid()) { + _dimensionPickerAction.setPointsDataset(Dataset()); + _zOrderScalars.clear(); + updateScatterplotWidget(); + return; + } + + auto dimensionIndex = static_cast(_dimensionPickerAction.getCurrentDimensionIndex()); + const auto numberOfDimensions = static_cast(positionDataset->getNumDimensions()); + + if (dimensionIndex < 0 || dimensionIndex >= numberOfDimensions) + dimensionIndex = 0; + + _dimensionPickerAction.setPointsDataset(positionDataset); + _dimensionPickerAction.setCurrentDimensionIndex(dimensionIndex); + + updateZOrderScalars(); + updateScatterplotWidget(); + }; + + connect(&_modeAction, &OptionAction::currentIndexChanged, this, [this]() { + updateScatterplotWidget(); + }); + + connect(&_dimensionPickerAction, &DimensionPickerAction::currentDimensionIndexChanged, this, [this, selectionRestriction = &selectionRestrictionAction]() { + updateZOrderScalars(); + + if (_restrictSelectionByZOrderAction.isChecked() && !_updatingCoupledRestriction) { + _updatingCoupledRestriction = true; + { + selectionRestriction->getDimensionPickerAction().setCurrentDimensionIndex(_dimensionPickerAction.getCurrentDimensionIndex()); + } + _updatingCoupledRestriction = false; + } + + updateScatterplotWidget(); + }); + + connect(&_restrictSelectionByZOrderAction, &ToggleAction::toggled, this, [this, selectionRestriction = &selectionRestrictionAction](bool checked) { + if (_updatingCoupledRestriction) + return; + + const auto restrictionAvailable = _scatterplotPlugin->getPositionDataset().isValid() && + static_cast(_modeAction.getCurrentIndex()) == Mode::Dimension; + + if (checked && !restrictionAvailable) { + _updatingCoupledRestriction = true; + { + _restrictSelectionByZOrderAction.setChecked(false); + } + _updatingCoupledRestriction = false; + + return; + } + + _updatingCoupledRestriction = true; + + if (checked) { + selectionRestriction->getDimensionPickerAction().setCurrentDimensionIndex(_dimensionPickerAction.getCurrentDimensionIndex()); + selectionRestriction->getEnabledAction().setChecked(true); + } else { + selectionRestriction->getEnabledAction().setChecked(false); + } + + _updatingCoupledRestriction = false; + }); + + connect(&selectionRestrictionAction.getEnabledAction(), &ToggleAction::toggled, this, [this, selectionRestriction = &selectionRestrictionAction](bool checked) { + if (_updatingCoupledRestriction) + return; + + const auto restrictionAvailable = _scatterplotPlugin->getPositionDataset().isValid() && + static_cast(_modeAction.getCurrentIndex()) == Mode::Dimension; + + _updatingCoupledRestriction = true; + + if (checked && restrictionAvailable) { + const QSignalBlocker dimensionBlocker(&_dimensionPickerAction); + _dimensionPickerAction.setCurrentDimensionIndex(selectionRestriction->getDimensionPickerAction().getCurrentDimensionIndex()); + updateZOrderScalars(); + } + + _restrictSelectionByZOrderAction.setChecked(checked && restrictionAvailable); + + _updatingCoupledRestriction = false; + }); + + connect(&selectionRestrictionAction.getDimensionPickerAction(), &DimensionPickerAction::currentDimensionIndexChanged, this, [this, selectionRestriction = &selectionRestrictionAction](std::int32_t dimensionIndex) { + if (_updatingCoupledRestriction || !selectionRestriction->getEnabledAction().isChecked() || + static_cast(_modeAction.getCurrentIndex()) != Mode::Dimension) + return; + + _updatingCoupledRestriction = true; + + { + const QSignalBlocker dimensionBlocker(&_dimensionPickerAction); + _dimensionPickerAction.setCurrentDimensionIndex(dimensionIndex); + } + + _restrictSelectionByZOrderAction.setChecked(true); + + updateZOrderScalars(); + _updatingCoupledRestriction = false; + }); + + connect(&_scatterplotPlugin->getPositionDataset(), &Dataset::changed, this, updateDataset); + connect(&_scatterplotPlugin->getPositionDataset(), &Dataset::dataDimensionsChanged, this, updateDataset); + connect(&_scatterplotPlugin->getPositionDataset(), &Dataset::dataChanged, this, [this]() { + updateZOrderScalars(); + updateScatterplotWidget(); + }); + + _modeAction.setCurrentIndex(static_cast(Mode::InsertionOrder)); + updateDataset(); +} + +void ZOrderingAction::updateScatterplotWidget() +{ + if (_scatterplotPlugin == nullptr) + return; + + const auto mode = static_cast(_modeAction.getCurrentIndex()); + const auto hasDataset = _scatterplotPlugin->getPositionDataset().isValid(); + + setEnabled(hasDataset); + + _dimensionPickerAction.setEnabled(hasDataset && mode == Mode::Dimension); + _restrictSelectionByZOrderAction.setEnabled(hasDataset && mode == Mode::Dimension); + + auto& selectionRestriction = dynamic_cast(parent())->getSelectionAction().getSelectionRestrictionAction(); + const auto restrictionCoupled = hasDataset && mode == Mode::Dimension && selectionRestriction.getEnabledAction().isChecked(); + + _updatingCoupledRestriction = true; + + if (restrictionCoupled) { + const QSignalBlocker dimensionBlocker(&_dimensionPickerAction); + _dimensionPickerAction.setCurrentDimensionIndex(selectionRestriction.getDimensionPickerAction().getCurrentDimensionIndex()); + } + + _restrictSelectionByZOrderAction.setChecked(restrictionCoupled); + + _updatingCoupledRestriction = false; + + switch (mode) { + case Mode::InsertionOrder: + _scatterplotPlugin->getScatterplotWidget().setZOrderMode(PointZOrderMode::InsertionOrder); + break; + + case Mode::Dimension: + _scatterplotPlugin->getScatterplotWidget().setZOrderMode(PointZOrderMode::Dimension); + break; + + case Mode::Randomized: + _scatterplotPlugin->getScatterplotWidget().setZOrderMode(PointZOrderMode::Randomized); + break; + } + + if (mode == Mode::Dimension && hasDataset) + updateZOrderScalars(); +} + +void ZOrderingAction::updateZOrderScalars() +{ + _zOrderScalars.clear(); + + if (_scatterplotPlugin == nullptr) + return; + + auto& positionDataset = _scatterplotPlugin->getPositionDataset(); + const auto dimensionIndex = static_cast(_dimensionPickerAction.getCurrentDimensionIndex()); + + if (positionDataset.isValid() && dimensionIndex >= 0 && dimensionIndex < static_cast(positionDataset->getNumDimensions())) + positionDataset->extractDataForDimension(_zOrderScalars, dimensionIndex); + + _scatterplotPlugin->getScatterplotWidget().setZOrderScalars(_zOrderScalars); +} + +QMenu* ZOrderingAction::getContextMenu(QWidget* parent) +{ + auto menu = new QMenu("Z ordering", parent); + + menu->addAction(&_modeAction); + menu->addAction(&_dimensionPickerAction); + + if (_scatterplotPlugin != nullptr) { + auto& selectionRestriction = dynamic_cast(this->parent())->getSelectionAction().getSelectionRestrictionAction(); + + menu->addSeparator(); + menu->addAction(&_restrictSelectionByZOrderAction); + menu->addAction(&selectionRestriction.getRangeAction()); + } + + return menu; +} + +void ZOrderingAction::connectToPublicAction(WidgetAction* publicAction, bool recursive) +{ + auto publicZOrderingAction = dynamic_cast(publicAction); + + Q_ASSERT(publicZOrderingAction != nullptr); + + if (publicZOrderingAction == nullptr) + return; + + if (recursive) { + actions().connectPrivateActionToPublicAction(&_modeAction, &publicZOrderingAction->getModeAction(), recursive); + actions().connectPrivateActionToPublicAction(&_dimensionPickerAction, &publicZOrderingAction->getDimensionPickerAction(), recursive); + actions().connectPrivateActionToPublicAction(&_restrictSelectionByZOrderAction, &publicZOrderingAction->getRestrictSelectionByZOrderAction(), recursive); + } + + GroupAction::connectToPublicAction(publicAction, recursive); +} + +void ZOrderingAction::disconnectFromPublicAction(bool recursive) +{ + if (!isConnected()) + return; + + if (recursive) { + actions().disconnectPrivateActionFromPublicAction(&_modeAction, recursive); + actions().disconnectPrivateActionFromPublicAction(&_dimensionPickerAction, recursive); + actions().disconnectPrivateActionFromPublicAction(&_restrictSelectionByZOrderAction, recursive); + } + + GroupAction::disconnectFromPublicAction(recursive); +} + +void ZOrderingAction::fromVariantMap(const QVariantMap& variantMap) +{ + GroupAction::fromVariantMap(variantMap); + + _modeAction.fromParentVariantMap(variantMap); + _dimensionPickerAction.fromParentVariantMap(variantMap); + updateScatterplotWidget(); +} + +QVariantMap ZOrderingAction::toVariantMap() const +{ + auto variantMap = GroupAction::toVariantMap(); + + _modeAction.insertIntoVariantMap(variantMap); + _dimensionPickerAction.insertIntoVariantMap(variantMap); + + return variantMap; +} diff --git a/src/ZOrderingAction.h b/src/ZOrderingAction.h new file mode 100644 index 0000000..2beadd2 --- /dev/null +++ b/src/ZOrderingAction.h @@ -0,0 +1,59 @@ +#pragma once + +#include +#include +#include + +#include + +using namespace mv::gui; + +class QMenu; +class ScatterplotPlugin; + +/** Action for choosing how overlapping points are ordered along the z-axis. */ +class ZOrderingAction : public VerticalGroupAction +{ +public: + enum class Mode { + InsertionOrder, + Dimension, + Randomized + }; + + Q_INVOKABLE ZOrderingAction(QObject* parent, const QString& title); + + void initialize(ScatterplotPlugin* scatterplotPlugin); + void updateScatterplotWidget(); + + QMenu* getContextMenu(QWidget* parent = nullptr) override; + +protected: // Linking + void connectToPublicAction(WidgetAction* publicAction, bool recursive) override; + void disconnectFromPublicAction(bool recursive) override; + +public: // Serialization + void fromVariantMap(const QVariantMap& variantMap) override; + QVariantMap toVariantMap() const override; + +public: // Action getters + OptionAction& getModeAction() { return _modeAction; } + DimensionPickerAction& getDimensionPickerAction() { return _dimensionPickerAction; } + ToggleAction& getRestrictSelectionByZOrderAction() { return _restrictSelectionByZOrderAction; } + +private: + void updateZOrderScalars(); + + ScatterplotPlugin* _scatterplotPlugin = nullptr; + OptionAction _modeAction; + DimensionPickerAction _dimensionPickerAction; + ToggleAction _restrictSelectionByZOrderAction; + std::vector _zOrderScalars; + bool _updatingCoupledRestriction = false; + + friend class mv::AbstractActionsManager; +}; + +Q_DECLARE_METATYPE(ZOrderingAction) + +inline const auto zOrderingActionMetaTypeId = qRegisterMetaType("ZOrderingAction");