From a79cd3b5cf058e9597131b78b3c905de329b1883 Mon Sep 17 00:00:00 2001 From: Erik Verbruggen Date: Fri, 24 Sep 2021 16:36:41 +0200 Subject: [PATCH] Allow to filter issue table by issue type Fixes: #9000 --- src/gui/activitywidget.cpp | 9 +- src/gui/activitywidget.ui | 52 +++++--- src/gui/issueswidget.cpp | 158 ++++++++++++++++++++++++- src/gui/issueswidget.h | 9 +- src/gui/issueswidget.ui | 40 +++++-- src/gui/models/expandingheaderview.cpp | 8 +- src/gui/models/expandingheaderview.h | 1 + src/gui/models/models.cpp | 23 ++-- src/gui/models/models.h | 4 +- src/gui/models/protocolitemmodel.cpp | 2 +- src/gui/protocolwidget.cpp | 20 +++- src/gui/protocolwidget.h | 3 +- src/gui/protocolwidget.ui | 40 +++++-- src/libsync/localdiscoverytracker.cpp | 2 + src/libsync/owncloudpropagator.cpp | 2 + src/libsync/syncfileitem.cpp | 4 +- src/libsync/syncfileitem.h | 6 +- 17 files changed, 324 insertions(+), 59 deletions(-) diff --git a/src/gui/activitywidget.cpp b/src/gui/activitywidget.cpp index 360de23d116..ffc45582751 100644 --- a/src/gui/activitywidget.cpp +++ b/src/gui/activitywidget.cpp @@ -69,6 +69,10 @@ ActivityWidget::ActivityWidget(QWidget *parent) header->setSectionResizeMode(QHeaderView::Interactive); header->setSortIndicator(static_cast(ActivityListModel::ActivityRole::PointInTime), Qt::DescendingOrder); + connect(_ui->_filterButton, &QAbstractButton::clicked, this, [this] { + ProtocolWidget::showFilterMenu(_ui->_filterButton, _sortModel); + }); + _ui->_notifyLabel->hide(); _ui->_notifyScroll->hide(); @@ -102,9 +106,8 @@ ActivityWidget::ActivityWidget(QWidget *parent) connect(_ui->_activityList, &QListView::customContextMenuRequested, this, &ActivityWidget::slotItemContextMenu); header->setContextMenuPolicy(Qt::CustomContextMenu); connect(header, &QListView::customContextMenuRequested, header, [header, this] { - auto menu = Models::displayFilterDialog(AccountManager::instance()->accountNames(), _sortModel, static_cast(ActivityListModel::ActivityRole::Account), Qt::DisplayRole, this); - menu->addSeparator(); - menu->addAction(tr("Reset column sizes"), this, [header] { header->resizeColumns(true); }); + auto menu = ProtocolWidget::showFilterMenu(header, _sortModel); + header->addResetActionToMenu(menu); }); connect(&_removeTimer, &QTimer::timeout, this, &ActivityWidget::slotCheckToCleanWidgets); diff --git a/src/gui/activitywidget.ui b/src/gui/activitywidget.ui index 0ebbbc9205f..0ba5f4f2be9 100644 --- a/src/gui/activitywidget.ui +++ b/src/gui/activitywidget.ui @@ -60,20 +60,44 @@ - - - - 0 - 0 - - - - TextLabel - - - Qt::RichText - - + + + + + + 0 + 0 + + + + TextLabel + + + Qt::RichText + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Filter + + + + diff --git a/src/gui/issueswidget.cpp b/src/gui/issueswidget.cpp index b463187dffb..41a7b006a85 100644 --- a/src/gui/issueswidget.cpp +++ b/src/gui/issueswidget.cpp @@ -24,7 +24,6 @@ #include "folderman.h" #include "syncfileitem.h" #include "folder.h" -#include "models/expandingheaderview.h" #include "models/models.h" #include "openfilemanager.h" #include "protocolwidget.h" @@ -50,6 +49,60 @@ bool persistsUntilLocalDiscovery(const OCC::ProtocolItem &data) } namespace OCC { +class SyncFileItemStatusSetSortFilterProxyModel : public QSortFilterProxyModel +{ +public: + using StatusSet = std::array; + + explicit SyncFileItemStatusSetSortFilterProxyModel(QObject *parent = nullptr) + : QSortFilterProxyModel(parent) + { + resetFilter(); + } + + ~SyncFileItemStatusSetSortFilterProxyModel() override + { + } + + StatusSet filter() const + { + return _filter; + } + + void setFilter(const StatusSet &newFilter) + { + if (_filter != newFilter) { + _filter = newFilter; + invalidateFilter(); + } + } + + void resetFilter() + { + StatusSet defaultFilter; + defaultFilter.fill(true); + defaultFilter[SyncFileItem::NoStatus] = false; + defaultFilter[SyncFileItem::Success] = false; + setFilter(defaultFilter); + } + + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override + { + QModelIndex idx = sourceModel()->index(sourceRow, filterKeyColumn(), sourceParent); + + bool ok = false; + int sourceData = sourceModel()->data(idx, filterRole()).toInt(&ok); + if (!ok) { + return false; + } + + return _filter[static_cast(sourceData)]; + } + +private: + StatusSet _filter; +}; + /** * If more issues are reported than this they will not show up * to avoid performance issues around sorting this many issues. @@ -90,7 +143,12 @@ IssuesWidget::IssuesWidget(QWidget *parent) _model = new ProtocolItemModel(20000, true, this); _sortModel = new QSortFilterProxyModel(this); _sortModel->setSourceModel(_model); - _ui->_tableView->setModel(_sortModel); + _statusSortModel = new SyncFileItemStatusSetSortFilterProxyModel(this); + _statusSortModel->setSourceModel(_sortModel); + _statusSortModel->setSortRole(Qt::DisplayRole); // Sorting should be done based on the text in the column cells, but... + _statusSortModel->setFilterRole(Models::UnderlyingDataRole); // ... filtering should be done on the underlying enum value. + _statusSortModel->setFilterKeyColumn(static_cast(ProtocolItemModel::ProtocolItemRole::Status)); + _ui->_tableView->setModel(_statusSortModel); auto header = new ExpandingHeaderView(QStringLiteral("ActivityErrorListHeaderV2"), _ui->_tableView); _ui->_tableView->setHorizontalHeader(header); @@ -100,8 +158,13 @@ IssuesWidget::IssuesWidget(QWidget *parent) connect(_ui->_tableView, &QTreeView::customContextMenuRequested, this, &IssuesWidget::slotItemContextMenu); _ui->_tableView->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu); - connect(header, &QHeaderView::customContextMenuRequested, header, [header, this] { - ProtocolWidget::showHeaderContextMenu(header, _sortModel); + connect(header, &QHeaderView::customContextMenuRequested, [this, header]() { + auto menu = showFilterMenu(header); + menu->addAction(tr("Reset column sizes"), header, [header] { header->resizeColumns(true); }); + }); + + connect(_ui->_filterButton, &QAbstractButton::clicked, this, [this] { + showFilterMenu(_ui->_filterButton); }); _ui->_tooManyIssuesWarning->hide(); @@ -131,6 +194,33 @@ IssuesWidget::~IssuesWidget() delete _ui; } +QMenu *IssuesWidget::showFilterMenu(QWidget *parent) +{ + auto menu = new QMenu(parent); + menu->setAttribute(Qt::WA_DeleteOnClose); + + auto accountFilterReset = Models::addFilterMenuItems(menu, AccountManager::instance()->accountNames(), _sortModel, static_cast(ProtocolItemModel::ProtocolItemRole::Account), tr("Account"), Qt::DisplayRole); + menu->addSeparator(); + auto statusFilterReset = addStatusFilter(menu); + menu->addSeparator(); + addResetFiltersAction(menu, { accountFilterReset, statusFilterReset }); + + QTimer::singleShot(0, menu, [menu] { + menu->popup(QCursor::pos()); + }); + + return menu; +} + +void IssuesWidget::addResetFiltersAction(QMenu *menu, const QList> &resetFunctions) +{ + menu->addAction(QCoreApplication::translate("OCC::Models", "Reset Filters"), [resetFunctions]() { + for (const auto &reset : resetFunctions) { + reset(); + } + }); +} + void IssuesWidget::slotProgressInfo(const QString &folder, const ProgressInfo &progress) { if (progress.status() == ProgressInfo::Reconcile) { @@ -195,9 +285,69 @@ void IssuesWidget::slotItemContextMenu() { auto rows = _ui->_tableView->selectionModel()->selectedRows(); for (int i = 0; i < rows.size(); ++i) { + rows[i] = _statusSortModel->mapToSource(rows[i]); rows[i] = _sortModel->mapToSource(rows[i]); } ProtocolWidget::showContextMenu(this, _model, rows); } +std::function IssuesWidget::addStatusFilter(QMenu *menu) +{ + menu->addAction(QCoreApplication::translate("OCC::Models", "Status Filter:"))->setEnabled(false); + + // Use a QActionGroup to contain all status filter items, so we can find them back easily to reset. + auto statusFilterGroup = new QActionGroup(menu); + statusFilterGroup->setExclusive(false); + + const auto initialFilter = _statusSortModel->filter(); + + { // Add all errors under 1 action: + const std::array ErrorStatusItems = { + SyncFileItem::Status::FatalError, + SyncFileItem::Status::NormalError, + SyncFileItem::Status::SoftError, + SyncFileItem::Status::DetailError + }; + + auto action = menu->addAction(SyncFileItem::statusEnumDisplayName(SyncFileItem::NormalError), [this, ErrorStatusItems](bool checked) { + auto currentFilter = _statusSortModel->filter(); + for (const auto &item : ErrorStatusItems) { + currentFilter[item] = checked; + } + _statusSortModel->setFilter(currentFilter); + }); + action->setCheckable(true); + action->setChecked(initialFilter[ErrorStatusItems[0]]); + statusFilterGroup->addAction(action); + } + + // Add the other non-error items: + const std::array OtherStatusItems = { + SyncFileItem::Status::Conflict, + SyncFileItem::Status::FileIgnored, + SyncFileItem::Status::Restoration, + SyncFileItem::Status::BlacklistedError, + SyncFileItem::Status::Excluded + }; + for (const auto &item : OtherStatusItems) { + auto action = menu->addAction(SyncFileItem::statusEnumDisplayName(item), [this, item](bool checked) { + auto currentFilter = _statusSortModel->filter(); + currentFilter[item] = checked; + _statusSortModel->setFilter(currentFilter); + }); + action->setCheckable(true); + action->setChecked(initialFilter[item]); + statusFilterGroup->addAction(action); + } + + menu->addSeparator(); + + // Add action to reset all filters at once: + return [statusFilterGroup, this]() { + for (QAction *action : statusFilterGroup->actions()) { + action->setChecked(true); + } + _statusSortModel->resetFilter(); + }; +} } diff --git a/src/gui/issueswidget.h b/src/gui/issueswidget.h index 3fa6153ed2c..cda9830ed52 100644 --- a/src/gui/issueswidget.h +++ b/src/gui/issueswidget.h @@ -20,9 +20,10 @@ #include #include +#include "models/expandingheaderview.h" #include "models/protocolitemmodel.h" -#include "progressdispatcher.h" #include "owncloudgui.h" +#include "progressdispatcher.h" #include "ui_issueswidget.h" @@ -30,6 +31,7 @@ class QSortFilterProxyModel; namespace OCC { class SyncResult; +class SyncFileItemStatusSetSortFilterProxyModel; namespace Ui { class ProtocolWidget; @@ -55,11 +57,16 @@ public slots: void issueCountUpdated(int); private slots: + QMenu *showFilterMenu(QWidget *parent); void slotItemContextMenu(); private: + static void addResetFiltersAction(QMenu *menu, const QList> &resetFunctions); + std::function addStatusFilter(QMenu *menu); + ProtocolItemModel *_model; QSortFilterProxyModel *_sortModel; + SyncFileItemStatusSetSortFilterProxyModel *_statusSortModel; Ui::IssuesWidget *_ui; }; diff --git a/src/gui/issueswidget.ui b/src/gui/issueswidget.ui index 7f6bab06200..db29c21ca8c 100644 --- a/src/gui/issueswidget.ui +++ b/src/gui/issueswidget.ui @@ -15,14 +15,38 @@ - - - List of issues - - - Qt::PlainText - - + + + + + List of issues + + + Qt::PlainText + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Filter + + + + diff --git a/src/gui/models/expandingheaderview.cpp b/src/gui/models/expandingheaderview.cpp index e1f945fc490..3457d3f63d8 100644 --- a/src/gui/models/expandingheaderview.cpp +++ b/src/gui/models/expandingheaderview.cpp @@ -16,9 +16,10 @@ #include "configfile.h" -#include #include #include +#include +#include using namespace OCC; @@ -71,3 +72,8 @@ void ExpandingHeaderView::resizeColumns(bool reset) } resizeSection(_expandingColumn, availableWidth); } + +void ExpandingHeaderView::addResetActionToMenu(QMenu *menu) +{ + menu->addAction(tr("Reset column sizes"), this, [this] { resizeColumns(true); }); +} diff --git a/src/gui/models/expandingheaderview.h b/src/gui/models/expandingheaderview.h index 9ed5c74cb93..41029c6a8ba 100644 --- a/src/gui/models/expandingheaderview.h +++ b/src/gui/models/expandingheaderview.h @@ -27,6 +27,7 @@ class ExpandingHeaderView : public QHeaderView void setExpandingColumn(int newExpandingColumn); void resizeColumns(bool reset = false); + void addResetActionToMenu(QMenu *menu); protected: void resizeEvent(QResizeEvent *event) override; diff --git a/src/gui/models/models.cpp b/src/gui/models/models.cpp index e549aaaf8d6..dc0b41987af 100644 --- a/src/gui/models/models.cpp +++ b/src/gui/models/models.cpp @@ -77,16 +77,16 @@ QString OCC::Models::formatSelection(const QModelIndexList &items, int dataRole) return out; } -QMenu *OCC::Models::displayFilterDialog(const QStringList &candidates, QSortFilterProxyModel *model, int column, int role, QWidget *parent) +std::function OCC::Models::addFilterMenuItems(QMenu *menu, const QStringList &candidates, QSortFilterProxyModel *model, int column, const QString &columnName, int role) { - auto menu = new QMenu(parent); - menu->setAttribute(Qt::WA_DeleteOnClose); - menu->addAction(qApp->translate("OCC::Models", "Filter by")); - menu->addSeparator(); + menu->addAction(qApp->translate("OCC::Models", "%1 Filter:").arg(columnName))->setEnabled(false); + + auto filterGroup = new QActionGroup(menu); + filterGroup->setExclusive(true); const auto currentFilter = model->filterRegExp().pattern(); auto addAction = [=](const QString &s, const QString &filter) { - auto action = menu->addAction(s, parent, [=]() { + auto action = menu->addAction(s, menu, [=]() { model->setFilterRole(role); model->setFilterKeyColumn(column); model->setFilterFixedString(filter); @@ -95,10 +95,12 @@ QMenu *OCC::Models::displayFilterDialog(const QStringList &candidates, QSortFilt if (currentFilter == filter) { action->setChecked(true); } + filterGroup->addAction(action); + return action; }; - addAction(qApp->translate("OCC::Models", "No filter"), QString()); + auto noFilter = addAction(QApplication::translate("OCC::Models", "No filter"), QString()); for (const auto &c : candidates) { addAction(c, c); @@ -106,5 +108,10 @@ QMenu *OCC::Models::displayFilterDialog(const QStringList &candidates, QSortFilt QTimer::singleShot(0, menu, [menu] { menu->popup(QCursor::pos()); }); - return menu; + + auto resetFunction = [noFilter]() { + noFilter->setChecked(true); + noFilter->trigger(); + }; + return resetFunction; } diff --git a/src/gui/models/models.h b/src/gui/models/models.h index b96519fe2f6..9b302171ff4 100644 --- a/src/gui/models/models.h +++ b/src/gui/models/models.h @@ -33,9 +33,7 @@ namespace Models { */ QString formatSelection(const QModelIndexList &items, int dataRole = Qt::DisplayRole); - - QMenu *displayFilterDialog(const QStringList &candidates, QSortFilterProxyModel *model, int column, int role, QWidget *parent = nullptr); - + std::function addFilterMenuItems(QMenu *menu, const QStringList &candidates, QSortFilterProxyModel *model, int column, const QString &columnName, int role); /** * Returns a vector with indices diff --git a/src/gui/models/protocolitemmodel.cpp b/src/gui/models/protocolitemmodel.cpp index 042d51c727f..cb431a43947 100644 --- a/src/gui/models/protocolitemmodel.cpp +++ b/src/gui/models/protocolitemmodel.cpp @@ -107,7 +107,7 @@ QVariant ProtocolItemModel::data(const QModelIndex &index, int role) const case ProtocolItemRole::Account: return item.folder()->accountState()->account()->displayName(); case ProtocolItemRole::Status: - return Utility::enumName(item.status()); + return item.status(); case ProtocolItemRole::ColumnCount: Q_UNREACHABLE(); break; diff --git a/src/gui/protocolwidget.cpp b/src/gui/protocolwidget.cpp index a1925ee3cae..73b33b31403 100644 --- a/src/gui/protocolwidget.cpp +++ b/src/gui/protocolwidget.cpp @@ -48,6 +48,8 @@ ProtocolWidget::ProtocolWidget(QWidget *parent) connect(_ui->_tableView, &QTreeWidget::customContextMenuRequested, this, &ProtocolWidget::slotItemContextMenu); + // Build the model-view "stack": + // _model <- _sortModel <- _statusSortModel <- _tableView _model = new ProtocolItemModel(2000, false, this); _sortModel = new QSortFilterProxyModel(this); _sortModel->setSourceModel(_model); @@ -62,7 +64,12 @@ ProtocolWidget::ProtocolWidget(QWidget *parent) header->hideSection(static_cast(ProtocolItemModel::ProtocolItemRole::Status)); header->setContextMenuPolicy(Qt::CustomContextMenu); connect(header, &QHeaderView::customContextMenuRequested, header, [header, this] { - showHeaderContextMenu(header, _sortModel); + auto menu = showFilterMenu(header, _sortModel); + header->addResetActionToMenu(menu); + }); + + connect(_ui->_filterButton, &QAbstractButton::clicked, this, [this] { + showFilterMenu(_ui->_filterButton, _sortModel); }); connect(FolderMan::instance(), &FolderMan::folderRemoved, this, [this](Folder *f) { @@ -77,11 +84,16 @@ ProtocolWidget::~ProtocolWidget() delete _ui; } -void ProtocolWidget::showHeaderContextMenu(ExpandingHeaderView *header, QSortFilterProxyModel *model) +QMenu *ProtocolWidget::showFilterMenu(QWidget *parent, QSortFilterProxyModel *model) { - auto menu = Models::displayFilterDialog(AccountManager::instance()->accountNames(), model, static_cast(ProtocolItemModel::ProtocolItemRole::Account), Qt::DisplayRole, header); + auto menu = new QMenu(parent); + menu->setAttribute(Qt::WA_DeleteOnClose); + Models::addFilterMenuItems(menu, AccountManager::instance()->accountNames(), model, static_cast(ProtocolItemModel::ProtocolItemRole::Account), tr("Account"), Qt::DisplayRole); menu->addSeparator(); - menu->addAction(tr("Reset column sizes"), header, [header] { header->resizeColumns(true); }); + QTimer::singleShot(0, menu, [menu] { + menu->popup(QCursor::pos()); + }); + return menu; } void ProtocolWidget::showContextMenu(QWidget *parent, ProtocolItemModel *model, const QModelIndexList &items) diff --git a/src/gui/protocolwidget.h b/src/gui/protocolwidget.h index 3c6a563f9ae..ac6e111eb8d 100644 --- a/src/gui/protocolwidget.h +++ b/src/gui/protocolwidget.h @@ -48,9 +48,8 @@ class ProtocolWidget : public QWidget explicit ProtocolWidget(QWidget *parent = nullptr); ~ProtocolWidget() override; - static void showHeaderContextMenu(ExpandingHeaderView *header, QSortFilterProxyModel *model); static void showContextMenu(QWidget *parent, ProtocolItemModel *model, const QModelIndexList &items); - + static QMenu *showFilterMenu(QWidget *parent, QSortFilterProxyModel *model); public slots: void slotItemCompleted(const QString &folder, const SyncFileItemPtr &item); diff --git a/src/gui/protocolwidget.ui b/src/gui/protocolwidget.ui index cdd97d3d73b..9c95ad8cc57 100644 --- a/src/gui/protocolwidget.ui +++ b/src/gui/protocolwidget.ui @@ -15,14 +15,38 @@ - - - Local sync protocol - - - Qt::PlainText - - + + + + + Local sync protocol + + + Qt::PlainText + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Filter + + + + diff --git a/src/libsync/localdiscoverytracker.cpp b/src/libsync/localdiscoverytracker.cpp index 98d518c2aed..44cb909cab7 100644 --- a/src/libsync/localdiscoverytracker.cpp +++ b/src/libsync/localdiscoverytracker.cpp @@ -75,6 +75,8 @@ void LocalDiscoveryTracker::slotItemCompleted(const SyncFileItemPtr &item) qCDebug(lcLocalDiscoveryTracker) << "wiped successful item" << item->_file; if (!item->_renameTarget.isEmpty() && _previousLocalDiscoveryPaths.erase(item->_renameTarget)) qCDebug(lcLocalDiscoveryTracker) << "wiped successful item" << item->_renameTarget; + } else if (item->_status == SyncFileItem::StatusCount) { + Q_UNREACHABLE(); } else { _localDiscoveryPaths.insert(item->_file); qCDebug(lcLocalDiscoveryTracker) << "inserted error item" << item->_file; diff --git a/src/libsync/owncloudpropagator.cpp b/src/libsync/owncloudpropagator.cpp index 5d59da95a0a..a7e3dc005f8 100644 --- a/src/libsync/owncloudpropagator.cpp +++ b/src/libsync/owncloudpropagator.cpp @@ -287,6 +287,8 @@ void PropagateItemJob::done(SyncFileItem::Status statusArg, const QString &error case SyncFileItem::Excluded: // nothing break; + case SyncFileItem::StatusCount: + Q_UNREACHABLE(); } if (_item->hasErrorStatus()) diff --git a/src/libsync/syncfileitem.cpp b/src/libsync/syncfileitem.cpp index ab7458b8de9..8018432f29b 100644 --- a/src/libsync/syncfileitem.cpp +++ b/src/libsync/syncfileitem.cpp @@ -91,7 +91,7 @@ QString SyncFileItem::statusEnumDisplayName(Status s) case OCC::SyncFileItem::Conflict: return QCoreApplication::translate("SyncFileItem::Status", "Conflict"); case OCC::SyncFileItem::FileIgnored: - return QCoreApplication::translate("SyncFileItem::Status", "Error Ignored"); + return QCoreApplication::translate("SyncFileItem::Status", "File Ignored"); case OCC::SyncFileItem::Restoration: return QCoreApplication::translate("SyncFileItem::Status", "Restored"); case OCC::SyncFileItem::DetailError: @@ -100,6 +100,8 @@ QString SyncFileItem::statusEnumDisplayName(Status s) return QCoreApplication::translate("SyncFileItem::Status", "Blacklisted"); case OCC::SyncFileItem::Excluded: return QCoreApplication::translate("SyncFileItem::Status", "Excluded"); + case OCC::SyncFileItem::StatusCount: + Q_UNREACHABLE(); } Q_UNREACHABLE(); } diff --git a/src/libsync/syncfileitem.h b/src/libsync/syncfileitem.h index 971d75bdef4..5437e7bb823 100644 --- a/src/libsync/syncfileitem.h +++ b/src/libsync/syncfileitem.h @@ -91,7 +91,11 @@ class OWNCLOUDSYNC_EXPORT SyncFileItem /** * The file is excluded by the ignore list */ - Excluded + Excluded, + + /** For use in an array or vector for the number of items in this enum. + */ + StatusCount }; Q_ENUM(Status)