diff --git a/ManiVault/cmake/CMakeMvSourcesPublic.cmake b/ManiVault/cmake/CMakeMvSourcesPublic.cmake index 87cab48b8..ce0cfb537 100644 --- a/ManiVault/cmake/CMakeMvSourcesPublic.cmake +++ b/ManiVault/cmake/CMakeMvSourcesPublic.cmake @@ -427,6 +427,7 @@ set(PUBLIC_WIDGETS_INTERNAL_HEADERS src/widgets/ViewPluginShortcutsDialog.h src/widgets/ViewPluginLearningCenterOverlayWidget.h src/widgets/IconLabel.h + src/widgets/MultiSelectComboBox.h ) set(PUBLIC_WIDGETS_INTERNAL_SOURCES @@ -446,6 +447,7 @@ set(PUBLIC_WIDGETS_INTERNAL_SOURCES src/widgets/ViewPluginShortcutsDialog.cpp src/widgets/ViewPluginLearningCenterOverlayWidget.cpp src/widgets/IconLabel.cpp + src/widgets/MultiSelectComboBox.cpp ) set(PUBLIC_WIDGETS_INTERNAL_FILES diff --git a/ManiVault/src/MiscellaneousSettingsAction.cpp b/ManiVault/src/MiscellaneousSettingsAction.cpp index f1e381b13..138e0142b 100644 --- a/ManiVault/src/MiscellaneousSettingsAction.cpp +++ b/ManiVault/src/MiscellaneousSettingsAction.cpp @@ -37,6 +37,7 @@ MiscellaneousSettingsAction::MiscellaneousSettingsAction(QObject* parent) : addAction(&_keepDescendantsAfterRemovalAction); addAction(&_statusBarVisibleAction); addAction(&_statusBarOptionsAction); + addAction(&_showSimplifiedGuidsAction); } void MiscellaneousSettingsAction::updateStatusBarOptionsAction() diff --git a/ManiVault/src/actions/OptionsAction.cpp b/ManiVault/src/actions/OptionsAction.cpp index ce9ec0b9f..6a155ccbf 100644 --- a/ManiVault/src/actions/OptionsAction.cpp +++ b/ManiVault/src/actions/OptionsAction.cpp @@ -277,7 +277,8 @@ QVariantMap OptionsAction::toVariantMap() const OptionsAction::ComboBoxWidget::ComboBoxWidget(QWidget* parent, OptionsAction* optionsAction, const std::int32_t& widgetFlags) : QWidget(parent), - _optionsAction(optionsAction) + _optionsAction(optionsAction), + _preventPopupClose(false) { auto comboBoxCheckableTableView = new CheckableTableView(this); @@ -303,6 +304,8 @@ OptionsAction::ComboBoxWidget::ComboBoxWidget(QWidget* parent, OptionsAction* op _comboBox.installEventFilter(this); _comboBox.lineEdit()->installEventFilter(this); + _comboBox.init(); + _layout.setContentsMargins(0, 0, 0, 0); _layout.addWidget(&_comboBox); diff --git a/ManiVault/src/actions/OptionsAction.h b/ManiVault/src/actions/OptionsAction.h index 141deb20b..e2ee38af8 100644 --- a/ManiVault/src/actions/OptionsAction.h +++ b/ManiVault/src/actions/OptionsAction.h @@ -12,6 +12,7 @@ #include "models/CheckableStringListModel.h" #include "widgets/FlowLayout.h" +#include "widgets/MultiSelectComboBox.h" #include #include @@ -128,11 +129,12 @@ class CORE_EXPORT OptionsAction : public WidgetAction void updateCurrentText() const; protected: - OptionsAction* _optionsAction; /** Pointer to owning options action */ - QHBoxLayout _layout; /** Horizontal layout */ - QComboBox _comboBox; /** Combobox for selecting options */ - QCompleter _completer; /** For inline searching */ - + OptionsAction* _optionsAction; /** Pointer to owning options action */ + QHBoxLayout _layout; /** Horizontal layout */ + MultiSelectComboBox _comboBox; /** Combobox for selecting options */ + QCompleter _completer; /** For inline searching */ + bool _preventPopupClose; /** Prevent the popup from closing when we do not want it */ + friend class OptionsAction; }; diff --git a/ManiVault/src/actions/RecentFilesAction.cpp b/ManiVault/src/actions/RecentFilesAction.cpp index 012323e3f..1ca6048e6 100644 --- a/ManiVault/src/actions/RecentFilesAction.cpp +++ b/ManiVault/src/actions/RecentFilesAction.cpp @@ -18,9 +18,6 @@ namespace mv::gui { RecentFilesAction::RecentFilesAction(QObject* parent, const QString& settingsKey /*= ""*/, const QString& fileType /*= ""*/, const QString& shortcutPrefix /*= ""*/, const QIcon& icon /*= QIcon()*/) : WidgetAction(parent, "Recent Files"), - _settingsKey(), - _fileType(), - _icon(), _model(this), _filterModel(this), _editAction(this, "Edit...") diff --git a/ManiVault/src/plugins/DataHierarchyPlugin/src/DataHierarchyWidget.cpp b/ManiVault/src/plugins/DataHierarchyPlugin/src/DataHierarchyWidget.cpp index 15ec6f3a2..c9366ddde 100644 --- a/ManiVault/src/plugins/DataHierarchyPlugin/src/DataHierarchyWidget.cpp +++ b/ManiVault/src/plugins/DataHierarchyPlugin/src/DataHierarchyWidget.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include @@ -70,7 +71,7 @@ class ProgressItemDelegateEditorWidget : public QWidget private: /** Updates the editor widget visibility based on the dataset task status */ - void updateEditorWidgetVisibility() { + void updateEditorWidgetVisibility() const { const auto datasetTaskStatus = _progressItem->getDatasetTask().getStatus(); if (datasetTaskStatus == Task::Status::Running || datasetTaskStatus == Task::Status::RunningIndeterminate || datasetTaskStatus == Task::Status::Finished) @@ -80,7 +81,7 @@ class ProgressItemDelegateEditorWidget : public QWidget } /** Updates the editor widget read-only state based on the dataset task status */ - void updateEditorWidgetReadOnly() { + void updateEditorWidgetReadOnly() const { _progressEditorWidget->setEnabled(!_progressItem->getDataset()->isLocked()); } @@ -102,7 +103,7 @@ class ItemDelegate : public QStyledItemDelegate { /** * Construct with owning parent \p dataHierarchyWidget - * @param parent Pointer to owning parent data hierarchy widget + * @param dataHierarchyWidget Pointer to owning parent data hierarchy widget */ explicit ItemDelegate(DataHierarchyWidget* dataHierarchyWidget) : QStyledItemDelegate(dataHierarchyWidget), @@ -121,12 +122,12 @@ class ItemDelegate : public QStyledItemDelegate { * @param index Model index to create the editor for * @return Pointer to widget if progress column, nullptr otherwise */ - QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const { + QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const override { if (static_cast(index.column()) != AbstractDataHierarchyModel::Column::Progress) return QStyledItemDelegate::createEditor(parent, option, index); const auto sourceModelIndex = _dataHierarchyWidget->getFilterModel().mapToSource(index); - const auto progressItem = static_cast(_dataHierarchyWidget->getTreeModel().itemFromIndex(sourceModelIndex)); + const auto progressItem = dynamic_cast(_dataHierarchyWidget->getTreeModel().itemFromIndex(sourceModelIndex)); return new ProgressItemDelegateEditorWidget(progressItem, parent); } @@ -136,7 +137,7 @@ class ItemDelegate : public QStyledItemDelegate { * @param option Style option * @param index Model index of the cell for which the geometry changed */ - void updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex& index) const { + void updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex& index) const override { Q_UNUSED(index) if (editor == nullptr) @@ -156,7 +157,7 @@ class ItemDelegate : public QStyledItemDelegate { { QStyledItemDelegate::initStyleOption(option, index); - auto item = static_cast(_dataHierarchyWidget->getTreeModel().itemFromIndex(_dataHierarchyWidget->getFilterModel().mapToSource(index))); + auto item = dynamic_cast(_dataHierarchyWidget->getTreeModel().itemFromIndex(_dataHierarchyWidget->getFilterModel().mapToSource(index))); if (item->getDataset()->isLocked())// || index.column() >= static_cast(AbstractDataHierarchyModel::Column::IsGroup)) option->state &= ~QStyle::State_Enabled; @@ -230,7 +231,7 @@ DataHierarchyWidget::DataHierarchyWidget(QWidget* parent) : connect(expandAllAction, &TriggerAction::triggered, this, [this]() -> void { _hierarchyWidget.getExpandAllAction().trigger(); - }); + }); auto& treeView = _hierarchyWidget.getTreeView(); @@ -427,6 +428,17 @@ DataHierarchyWidget::DataHierarchyWidget(QWidget* parent) : }); } + connect(&_hierarchyWidget.getTreeView(), &QTreeView::clicked, this, [this](const QModelIndex& index) -> void { + if (index.column() != static_cast(AbstractDataHierarchyModel::Column::DatasetId)) + return; + + const auto datasetId = _treeModel.getItem(_filterModel.mapToSource(index))->getDataset()->getId(); + const auto datasetIdLog = _treeModel.getItem(_filterModel.mapToSource(index))->getDataset()->getId(mv::settings().getMiscellaneousSettings().getShowSimplifiedGuidsAction().isChecked()); + + QGuiApplication::clipboard()->setText(datasetId); + + qDebug() << "Dataset identifier" << datasetIdLog << "copied to clipboard"; + }); } QModelIndex DataHierarchyWidget::getModelIndexByDataset(const Dataset& dataset) const @@ -434,7 +446,7 @@ QModelIndex DataHierarchyWidget::getModelIndexByDataset(const Dataset(AbstractDataHierarchyModel::Column::DatasetId), QModelIndex()), Qt::EditRole, dataset->getId(), 1, Qt::MatchFlag::MatchRecursive); if (modelIndices.isEmpty()) - throw new std::runtime_error(QString("'%1' not found in the data hierarchy model").arg(dataset->text()).toLatin1()); + throw std::runtime_error(QString("'%1' not found in the data hierarchy model").arg(dataset->text()).toLatin1()); return modelIndices.first(); } diff --git a/ManiVault/src/plugins/DataHierarchyPlugin/src/DataHierarchyWidgetContextMenu.cpp b/ManiVault/src/plugins/DataHierarchyPlugin/src/DataHierarchyWidgetContextMenu.cpp index e1fb329ea..e27343d83 100644 --- a/ManiVault/src/plugins/DataHierarchyPlugin/src/DataHierarchyWidgetContextMenu.cpp +++ b/ManiVault/src/plugins/DataHierarchyPlugin/src/DataHierarchyWidgetContextMenu.cpp @@ -15,8 +15,7 @@ #include #include #include - -#include +#include using namespace mv; using namespace mv::util; @@ -72,13 +71,41 @@ DataHierarchyWidgetContextMenu::DataHierarchyWidgetContextMenu(QWidget* parent, addAction(removeDatasetsAction); } + + if (!_selectedDatasets.isEmpty()) { + addSeparator(); + + const auto copySelectedDatasetIdsToClipboard = [this]() -> void { + QStringList datasetIds, datasetIdsLog; + + for (const auto& selectedDataset : _selectedDatasets) { + datasetIds << selectedDataset->getId(); + datasetIdsLog << selectedDataset->getId(mv::settings().getMiscellaneousSettings().getShowSimplifiedGuidsAction().isChecked()); + } + + QGuiApplication::clipboard()->setText(datasetIds.join("\n")); + + if (datasetIdsLog.count() > 1) + qDebug() << "Dataset identifiers" << datasetIdsLog.join(", ") << "copied to clipboard"; + else + qDebug() << "Dataset identifier" << datasetIdsLog.join(", ") << "copied to clipboard"; + }; + + auto copyAction = new QAction(QString("Copy dataset ID%1").arg(_selectedDatasets.count() > 1 ? "'s" : "")); + + copyAction->setIcon(Application::getIconFont("FontAwesome").getIcon("barcode")); + + connect(copyAction, &QAction::triggered, copyAction, copySelectedDatasetIdsToClipboard); + + addAction(copyAction); + } } void DataHierarchyWidgetContextMenu::addMenusForPluginType(plugin::Type pluginType) { QMap menus; - for (auto pluginTriggerAction : Application::core()->getPluginManager().getPluginTriggerActions(pluginType, _selectedDatasets)) { + for (const auto& pluginTriggerAction : Application::core()->getPluginManager().getPluginTriggerActions(pluginType, _selectedDatasets)) { const auto titleSegments = pluginTriggerAction->getMenuLocation().split("/"); QString menuPath, previousMenuPath = titleSegments.first(); @@ -311,3 +338,4 @@ QMenu* DataHierarchyWidgetContextMenu::getUnhideMenu() return unhideMenu; } + diff --git a/ManiVault/src/private/FileMenu.cpp b/ManiVault/src/private/FileMenu.cpp index 68ce2e353..9959a1e5b 100644 --- a/ManiVault/src/private/FileMenu.cpp +++ b/ManiVault/src/private/FileMenu.cpp @@ -22,7 +22,7 @@ FileMenu::FileMenu(QWidget* parent /*= nullptr*/) : setToolTip("File operations"); // Quit is by default in the app menu on macOS - if(QOperatingSystemVersion::currentType() != QOperatingSystemVersion::MacOS) { + if (QOperatingSystemVersion::currentType() != QOperatingSystemVersion::MacOS) { _exitApplictionAction.setShortcut(QKeySequence("Alt+F4")); _exitApplictionAction.setShortcutContext(Qt::ApplicationShortcut); @@ -38,6 +38,14 @@ FileMenu::FileMenu(QWidget* parent /*= nullptr*/) : populate(); } +void FileMenu::showEvent(QShowEvent* event) +{ + QMenu::showEvent(event); + + if (QOperatingSystemVersion::currentType() == QOperatingSystemVersion::Windows) + populate(); +} + void FileMenu::populate() { clear(); diff --git a/ManiVault/src/private/FileMenu.h b/ManiVault/src/private/FileMenu.h index 65d9bdf45..5498b4d3c 100644 --- a/ManiVault/src/private/FileMenu.h +++ b/ManiVault/src/private/FileMenu.h @@ -24,7 +24,13 @@ class FileMenu : public QMenu * @param parent Pointer to parent widget */ FileMenu(QWidget *parent = nullptr); - + + /** + * + * @param event + */ + void showEvent(QShowEvent* event) override; + private: /** diff --git a/ManiVault/src/util/Serializable.cpp b/ManiVault/src/util/Serializable.cpp index ff8dcaa95..ca9f229dc 100644 --- a/ManiVault/src/util/Serializable.cpp +++ b/ManiVault/src/util/Serializable.cpp @@ -32,7 +32,7 @@ Serializable::Serializable(const QString& name /*= ""*/) : QString Serializable::getId(bool truncated /*= false*/) const { if (truncated) - return _id.left(8); + return _id.right(8); return _id; } diff --git a/ManiVault/src/widgets/MultiSelectComboBox.cpp b/ManiVault/src/widgets/MultiSelectComboBox.cpp new file mode 100644 index 000000000..3731f1c46 --- /dev/null +++ b/ManiVault/src/widgets/MultiSelectComboBox.cpp @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// A corresponding LICENSE file is located in the root directory of this source tree +// Copyright (C) 2023 BioVault (Biomedical Visual Analytics Unit LUMC - TU Delft) + +#include "MultiSelectComboBox.h" + +#include +#include + +namespace mv::gui { + +MultiSelectComboBox::MultiSelectComboBox(QWidget* parent) : + QComboBox(parent), + _preventHidePopup(true) +{ + setEditable(true); +} + +void MultiSelectComboBox::init() +{ + // Install an event filter to prevent automated popup hide + view()->installEventFilter(this); + + // Install an event filter to hide the popup when clicked outside of it + view()->parentWidget()->installEventFilter(this); +} + +void MultiSelectComboBox::hidePopup() +{ + if (_preventHidePopup) + return; + + QComboBox::hidePopup(); + + _preventHidePopup = true; +} + +bool MultiSelectComboBox::eventFilter(QObject* watched, QEvent* event) +{ + if (event->type() == QEvent::MouseButtonPress) { + const auto mouseEvent = dynamic_cast(event); + + if (watched == view()) { + const auto index = view()->indexAt(mouseEvent->pos()); + + if (index.isValid()) { + toggleItemCheckState(index); + return true; + } + } + + if (watched == view()->parentWidget()) { + _preventHidePopup = false; + hidePopup(); + } + } + + return QComboBox::eventFilter(watched, event); +} + +void MultiSelectComboBox::toggleItemCheckState(const QModelIndex& index) const +{ + if (index.isValid()) + model()->setData(index, !model()->data(index, Qt::CheckStateRole).toBool(), Qt::CheckStateRole); +} + +} diff --git a/ManiVault/src/widgets/MultiSelectComboBox.h b/ManiVault/src/widgets/MultiSelectComboBox.h new file mode 100644 index 000000000..b004e9149 --- /dev/null +++ b/ManiVault/src/widgets/MultiSelectComboBox.h @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// A corresponding LICENSE file is located in the root directory of this source tree +// Copyright (C) 2023 BioVault (Biomedical Visual Analytics Unit LUMC - TU Delft) + +#pragma once + +#include "ManiVaultGlobals.h" + +#include + +namespace mv::gui { + +/** + * Multi-select combobox widget class + * + * Allows to select multiple items during a single popup sessions. + * + * @author Thomas Kroes + */ +class CORE_EXPORT MultiSelectComboBox : public QComboBox { + + Q_OBJECT + +public: + + + /** + * Construct with pointer to \p parent widget + * @param parent Pointer to parent widget + */ + explicit MultiSelectComboBox(QWidget* parent = nullptr); + + /** Call this post QComboBox::setView() to setup connections */ + void init(); + +protected: + + /** Override to customize popup hide behaviour */ + void hidePopup() override; + + + /** + * Called when \p event happens for \p watched object + * @param watched Pointer to watched object + * @param event Pointer to event that occurred + * @return Whether the event was handled or not + */ + bool eventFilter(QObject* watched, QEvent* event) override; + +private: + + + /** + * Toggle the check state of \p index + * @param index Index of which to toggle the check state + */ + void toggleItemCheckState(const QModelIndex& index) const; + +private: + bool _preventHidePopup = false; /** Whether to prevent the popup from closing or not */ +}; + +}