diff options
-rw-r--r-- | changelog/unreleased/8158 | 7 | ||||
-rw-r--r-- | src/gui/CMakeLists.txt | 3 | ||||
-rw-r--r-- | src/gui/activitylistmodel.cpp | 7 | ||||
-rw-r--r-- | src/gui/activitylistmodel.h | 2 | ||||
-rw-r--r-- | src/gui/activitywidget.cpp | 50 | ||||
-rw-r--r-- | src/gui/activitywidget.h | 4 | ||||
-rw-r--r-- | src/gui/folderman.cpp | 4 | ||||
-rw-r--r-- | src/gui/issueswidget.cpp | 440 | ||||
-rw-r--r-- | src/gui/issueswidget.h | 40 | ||||
-rw-r--r-- | src/gui/issueswidget.ui | 141 | ||||
-rw-r--r-- | src/gui/models.cpp | 49 | ||||
-rw-r--r-- | src/gui/models.h | 35 | ||||
-rw-r--r-- | src/gui/protocolitem.cpp | 82 | ||||
-rw-r--r-- | src/gui/protocolitem.h | 65 | ||||
-rw-r--r-- | src/gui/protocolitemmodel.cpp | 217 | ||||
-rw-r--r-- | src/gui/protocolitemmodel.h | 77 | ||||
-rw-r--r-- | src/gui/protocolwidget.cpp | 319 | ||||
-rw-r--r-- | src/gui/protocolwidget.h | 62 | ||||
-rw-r--r-- | src/gui/protocolwidget.ui | 40 | ||||
-rw-r--r-- | src/gui/settingsdialog.cpp | 4 | ||||
-rw-r--r-- | src/gui/settingsdialog.h | 2 | ||||
-rw-r--r-- | test/modeltests/CMakeLists.txt | 1 | ||||
-rw-r--r-- | test/modeltests/testactivitymodel.cpp | 2 | ||||
-rw-r--r-- | test/modeltests/testprotocolmodel.cpp | 48 |
24 files changed, 767 insertions, 934 deletions
diff --git a/changelog/unreleased/8158 b/changelog/unreleased/8158 index 286a9960f..4d5d01d6b 100644 --- a/changelog/unreleased/8158 +++ b/changelog/unreleased/8158 @@ -1,7 +1,8 @@ -Enhancement: We reworked the server activity table +Enhancement: We reworked the tables -We redone the server activity table, its now behaves as a proper table, -is sortable, supports right to left layouts and overall behave more smooth. +We reworked all the tables in the application to unify +their behaviour and improve their performance. https://github.com/owncloud/client/issues/8158 https://github.com/owncloud/client/issues/4336 +https://github.com/owncloud/client/issues/8528 diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index f0490de9a..7f0e5ada3 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -49,6 +49,7 @@ set(client_SRCS ignorelisteditor.cpp lockwatcher.cpp logbrowser.cpp + models.cpp networksettings.cpp ocsjob.cpp ocssharejob.cpp @@ -57,6 +58,8 @@ set(client_SRCS owncloudgui.cpp owncloudsetupwizard.cpp protocolwidget.cpp + protocolitem.cpp + protocolitemmodel.cpp issueswidget.cpp activitydata.cpp activitylistmodel.cpp diff --git a/src/gui/activitylistmodel.cpp b/src/gui/activitylistmodel.cpp index 238269856..ccc03acd0 100644 --- a/src/gui/activitylistmodel.cpp +++ b/src/gui/activitylistmodel.cpp @@ -22,9 +22,10 @@ #include "account.h" #include "accountstate.h" #include "accountmanager.h" -#include "folderman.h" #include "accessmanager.h" +#include "folderman.h" #include "guiutility.h" +#include "models.h" #include "activitydata.h" #include "activitylistmodel.h" @@ -52,7 +53,7 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const } const auto column = static_cast<ActivityRole>(index.column()); switch (role) { - case UnderlyingDataRole: + case Models::UnderlyingDataRole: Q_FALLTHROUGH(); case Qt::DisplayRole: switch (column) { @@ -61,7 +62,7 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const case ActivityRole::Text: return a.subject(); case ActivityRole::PointInTime: - if (role == UnderlyingDataRole) { + if (role == Models::UnderlyingDataRole) { return a.dateTime(); } else { return Utility::timeAgoInWords(a.dateTime()); diff --git a/src/gui/activitylistmodel.h b/src/gui/activitylistmodel.h index 014cae296..8c5b2422f 100644 --- a/src/gui/activitylistmodel.h +++ b/src/gui/activitylistmodel.h @@ -38,8 +38,6 @@ class ActivityListModel : public QAbstractTableModel { Q_OBJECT public: - // TODO: Move to a common namespace - static constexpr int UnderlyingDataRole = Qt::UserRole + 100; enum class ActivityRole { Text, Account, diff --git a/src/gui/activitywidget.cpp b/src/gui/activitywidget.cpp index 94e326d3c..e5e49d085 100644 --- a/src/gui/activitywidget.cpp +++ b/src/gui/activitywidget.cpp @@ -17,7 +17,7 @@ #include "activitylistmodel.h" #include "activitywidget.h" -#include "configfile.h"> +#include "configfile.h" #include "syncresult.h" #include "logger.h" #include "theme.h" @@ -29,6 +29,7 @@ #include "account.h" #include "accountstate.h" #include "accountmanager.h" +#include "models.h" #include "protocolwidget.h" #include "issueswidget.h" #include "QProgressIndicator.h" @@ -55,18 +56,13 @@ ActivityWidget::ActivityWidget(QWidget *parent) { _ui->setupUi(this); -// Adjust copyToClipboard() when making changes here! -#if defined(Q_OS_MAC) - _ui->_activityList->setMinimumWidth(400); -#endif - _model = new ActivityListModel(this); auto sortModel = new QSortFilterProxyModel(this); sortModel->setSourceModel(_model); _ui->_activityList->setModel(sortModel); - sortModel->setSortRole(ActivityListModel::UnderlyingDataRole); + sortModel->setSortRole(Models::UnderlyingDataRole); _ui->_activityList->hideColumn(static_cast<int>(ActivityListModel::ActivityRole::Path)); - _ui->_activityList->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); + _ui->_activityList->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive); _ui->_activityList->horizontalHeader()->setSectionResizeMode(static_cast<int>(ActivityListModel::ActivityRole::Text), QHeaderView::Stretch); _ui->_activityList->horizontalHeader()->setSortIndicator(static_cast<int>(ActivityListModel::ActivityRole::PointInTime), Qt::DescendingOrder); @@ -108,10 +104,6 @@ ActivityWidget::ActivityWidget(QWidget *parent) } }); - _copyBtn = _ui->_dialogButtonBox->addButton(tr("Copy"), QDialogButtonBox::ActionRole); - _copyBtn->setToolTip(tr("Copy the activity list to the clipboard.")); - connect(_copyBtn, &QAbstractButton::clicked, this, &ActivityWidget::copyToClipboard); - connect(_model, &QAbstractItemModel::modelReset, this, &ActivityWidget::dataChanged); connect(_ui->_activityList, &QListView::activated, this, &ActivityWidget::slotOpenFile); @@ -524,22 +516,18 @@ ActivitySettings::ActivitySettings(QWidget *parent) hbox->addWidget(_tab); _activityWidget = new ActivityWidget(this); _activityTabId = _tab->addTab(_activityWidget, Theme::instance()->applicationIcon(), tr("Server Activity")); - connect(_activityWidget, &ActivityWidget::copyToClipboard, this, &ActivitySettings::slotCopyToClipboard); connect(_activityWidget, &ActivityWidget::hideActivityTab, this, &ActivitySettings::setActivityTabHidden); connect(_activityWidget, &ActivityWidget::guiLog, this, &ActivitySettings::guiLog); connect(_activityWidget, &ActivityWidget::newNotification, this, &ActivitySettings::slotShowActivityTab); _protocolWidget = new ProtocolWidget(this); _protocolTabId = _tab->addTab(_protocolWidget, Theme::instance()->syncStateIcon(SyncResult::Success), tr("Sync Protocol")); - connect(_protocolWidget, &ProtocolWidget::copyToClipboard, this, &ActivitySettings::slotCopyToClipboard); _issuesWidget = new IssuesWidget(this); _syncIssueTabId = _tab->addTab(_issuesWidget, Theme::instance()->syncStateIcon(SyncResult::Problem), QString()); slotShowIssueItemCount(0); // to display the label. connect(_issuesWidget, &IssuesWidget::issueCountUpdated, this, &ActivitySettings::slotShowIssueItemCount); - connect(_issuesWidget, &IssuesWidget::copyToClipboard, - this, &ActivitySettings::slotCopyToClipboard); // Add a progress indicator to spin if the acitivity list is updated. _progressIndicator = new QProgressIndicator(this); @@ -594,39 +582,11 @@ void ActivitySettings::slotShowActivityTab() } } -void ActivitySettings::slotShowIssuesTab(const QString &folderAlias) +void ActivitySettings::slotShowIssuesTab() { if (_syncIssueTabId == -1) return; _tab->setCurrentIndex(_syncIssueTabId); - - _issuesWidget->showFolderErrors(folderAlias); -} - -void ActivitySettings::slotCopyToClipboard() -{ - QString text; - QTextStream ts(&text); - - int idx = _tab->currentIndex(); - QString message; - - if (idx == _activityTabId) { - // the activity widget - _activityWidget->storeActivityList(ts); - message = tr("The server activity list has been copied to the clipboard."); - } else if (idx == _protocolTabId) { - // the protocol widget - _protocolWidget->storeSyncActivity(ts); - message = tr("The sync activity list has been copied to the clipboard."); - } else if (idx == _syncIssueTabId) { - // issues Widget - message = tr("The list of unsynced items has been copied to the clipboard."); - _issuesWidget->storeSyncIssues(ts); - } - - QApplication::clipboard()->setText(text); - emit guiLog(tr("Copied to clipboard"), message); } void ActivitySettings::slotRemoveAccount(AccountState *ptr) diff --git a/src/gui/activitywidget.h b/src/gui/activitywidget.h index 4aa903c4c..4e85747f3 100644 --- a/src/gui/activitywidget.h +++ b/src/gui/activitywidget.h @@ -80,7 +80,6 @@ public slots: signals: void guiLog(const QString &, const QString &); - void copyToClipboard(); void dataChanged(); void hideActivityTab(bool); void newNotification(); @@ -138,10 +137,9 @@ public slots: void setNotificationRefreshInterval(std::chrono::milliseconds interval); - void slotShowIssuesTab(const QString &folderAlias); + void slotShowIssuesTab(); private slots: - void slotCopyToClipboard(); void setActivityTabHidden(bool hidden); void slotRegularNotificationCheck(); void slotShowIssueItemCount(int cnt); diff --git a/src/gui/folderman.cpp b/src/gui/folderman.cpp index f351c59db..520b3b6db 100644 --- a/src/gui/folderman.cpp +++ b/src/gui/folderman.cpp @@ -536,9 +536,7 @@ void FolderMan::slotFolderCanSyncChanged() Folder *FolderMan::folder(const QString &alias) { if (!alias.isEmpty()) { - if (_folderMap.contains(alias)) { - return _folderMap[alias]; - } + return _folderMap.value(alias); } return nullptr; } diff --git a/src/gui/issueswidget.cpp b/src/gui/issueswidget.cpp index c9a972beb..e6707ddcd 100644 --- a/src/gui/issueswidget.cpp +++ b/src/gui/issueswidget.cpp @@ -24,6 +24,7 @@ #include "folderman.h" #include "syncfileitem.h" #include "folder.h" +#include "models.h" #include "openfilemanager.h" #include "protocolwidget.h" #include "accountstate.h" @@ -37,18 +38,20 @@ #include <climits> +namespace { +bool persistsUntilLocalDiscovery(const OCC::ProtocolItem &data) +{ + return data.status() == OCC::SyncFileItem::Conflict + || (data.status() == OCC::SyncFileItem::FileIgnored && data.direction() == OCC::SyncFileItem::Up); +} + +} namespace OCC { /** * If more issues are reported than this they will not show up * to avoid performance issues around sorting this many issues. */ -static const int maxIssueCount = 50000; - -static QPair<QString, QString> pathsWithIssuesKey(const ProtocolItem::ExtraData &data) -{ - return qMakePair(data.folderName, data.path); -} IssuesWidget::IssuesWidget(QWidget *parent) : QWidget(parent) @@ -61,59 +64,41 @@ IssuesWidget::IssuesWidget(QWidget *parent) connect(ProgressDispatcher::instance(), &ProgressDispatcher::itemCompleted, this, &IssuesWidget::slotItemCompleted); connect(ProgressDispatcher::instance(), &ProgressDispatcher::syncError, - this, &IssuesWidget::addError); - - connect(_ui->_treeWidget, &QTreeWidget::itemActivated, this, &IssuesWidget::slotOpenFile); - connect(_ui->copyIssuesButton, &QAbstractButton::clicked, this, &IssuesWidget::copyToClipboard); - - _ui->_treeWidget->setContextMenuPolicy(Qt::CustomContextMenu); - connect(_ui->_treeWidget, &QTreeWidget::customContextMenuRequested, this, &IssuesWidget::slotItemContextMenu); - - connect(_ui->showIgnores, &QAbstractButton::toggled, this, &IssuesWidget::slotRefreshIssues); - connect(_ui->showWarnings, &QAbstractButton::toggled, this, &IssuesWidget::slotRefreshIssues); - connect(_ui->filterAccount, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &IssuesWidget::slotRefreshIssues); - connect(_ui->filterAccount, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &IssuesWidget::slotUpdateFolderFilters); - connect(_ui->filterFolder, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &IssuesWidget::slotRefreshIssues); - for (auto account : AccountManager::instance()->accounts()) { - slotAccountAdded(account.data()); - } - connect(AccountManager::instance(), &AccountManager::accountAdded, - this, &IssuesWidget::slotAccountAdded); - connect(AccountManager::instance(), &AccountManager::accountRemoved, - this, &IssuesWidget::slotAccountRemoved); - connect(FolderMan::instance(), &FolderMan::folderListChanged, - this, &IssuesWidget::slotUpdateFolderFilters); + this, [this](const QString &folderAlias, const QString &message, ErrorCategory) { + auto item = SyncFileItemPtr::create(); + item->_status = SyncFileItem::NormalError; + item->_errorString = message; + item->_responseTimeStamp = QDateTime::currentDateTime().toString(Qt::RFC2822Date).toUtf8(); + _model->addProtocolItem(ProtocolItem { folderAlias, item }); + }); + _model = new ProtocolItemModel(this); + _sortModel = new QSortFilterProxyModel(this); + _sortModel->setSourceModel(_model); + _ui->_tableView->setModel(_sortModel); + connect(_ui->_tableView, &QTreeView::customContextMenuRequested, this, &IssuesWidget::slotItemContextMenu); - // Adjust copyToClipboard() when making changes here! - QStringList header; - header << tr("Time"); - header << tr("File"); - header << tr("Folder"); - header << tr("Issue"); + _ui->_tableView->horizontalHeader()->setObjectName(QStringLiteral("ActivityErrorListHeaderV2")); + _ui->_tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive); + _ui->_tableView->horizontalHeader()->setSectionResizeMode(static_cast<int>(ProtocolItemModel::ProtocolItemRole::Action), QHeaderView::Stretch); + _ui->_tableView->horizontalHeader()->setSortIndicator(static_cast<int>(ProtocolItemModel::ProtocolItemRole::Time), Qt::DescendingOrder); - int timestampColumnExtra = 0; -#ifdef Q_OS_WIN - timestampColumnExtra = 20; // font metrics are broken on Windows, see #4721 -#endif + ConfigFile cfg; + cfg.restoreGeometryHeader(_ui->_tableView->horizontalHeader()); - _ui->_treeWidget->setHeaderLabels(header); - _ui->_treeWidget->setColumnWidth(1, 180); - _ui->_treeWidget->setColumnCount(4); - _ui->_treeWidget->setRootIsDecorated(false); - _ui->_treeWidget->setTextElideMode(Qt::ElideMiddle); - _ui->_treeWidget->header()->setObjectName("ActivityErrorListHeader"); -#if defined(Q_OS_MAC) - _ui->_treeWidget->setMinimumWidth(400); -#endif + connect(qApp, &QApplication::aboutToQuit, this, [this] { + ConfigFile cfg; + cfg.saveGeometryHeader(_ui->_tableView->horizontalHeader()); + }); - _reenableSorting.setInterval(5000); - connect(&_reenableSorting, &QTimer::timeout, this, - [this]() { _ui->_treeWidget->setSortingEnabled(true); }); _ui->_tooManyIssuesWarning->hide(); - connect(this, &IssuesWidget::issueCountUpdated, this, - [this](int count) { _ui->_tooManyIssuesWarning->setVisible(count >= maxIssueCount); }); + connect(_model, &ProtocolItemModel::rowsInserted, this, [this] { + _ui->_tooManyIssuesWarning->setVisible(_model->isModelFull()); + }); + connect(_model, &ProtocolItemModel::modelReset, this, [this] { + _ui->_tooManyIssuesWarning->setVisible(_model->isModelFull()); + }); _ui->_conflictHelp->hide(); _ui->_conflictHelp->setText( @@ -126,115 +111,6 @@ IssuesWidget::~IssuesWidget() delete _ui; } -void IssuesWidget::showEvent(QShowEvent *ev) -{ - ConfigFile cfg; - cfg.restoreGeometryHeader(_ui->_treeWidget->header()); - - // Sorting by section was newly enabled. But if we restore the header - // from a state where sorting was disabled, both of these flags will be - // false and sorting will be impossible! - _ui->_treeWidget->header()->setSectionsClickable(true); - _ui->_treeWidget->header()->setSortIndicatorShown(true); - - // Switch back to "first important, then by time" ordering - _ui->_treeWidget->sortByColumn(0, Qt::DescendingOrder); - - QWidget::showEvent(ev); -} - -void IssuesWidget::hideEvent(QHideEvent *ev) -{ - ConfigFile cfg; - cfg.saveGeometryHeader(_ui->_treeWidget->header()); - QWidget::hideEvent(ev); -} - -static bool persistsUntilLocalDiscovery(QTreeWidgetItem *item) -{ - const auto data = ProtocolItem::extraData(item); - return data.status == SyncFileItem::Conflict - || (data.status == SyncFileItem::FileIgnored && data.direction == SyncFileItem::Up); -} - -void IssuesWidget::cleanItems(const std::function<bool(QTreeWidgetItem *)> &shouldDelete) -{ - _ui->_treeWidget->setSortingEnabled(false); - - // The issue list is a state, clear it and let the next sync fill it - // with ignored files and propagation errors. - int itemCnt = _ui->_treeWidget->topLevelItemCount(); - for (int cnt = itemCnt - 1; cnt >= 0; cnt--) { - QTreeWidgetItem *item = _ui->_treeWidget->topLevelItem(cnt); - if (shouldDelete(item)) { - _pathsWithIssues.remove(pathsWithIssuesKey(ProtocolItem::extraData(item))); - delete item; - } - } - - _ui->_treeWidget->setSortingEnabled(true); - - // update the tabtext - emit(issueCountUpdated(_ui->_treeWidget->topLevelItemCount())); -} - -void IssuesWidget::addItem(QTreeWidgetItem *item) -{ - if (!item) - return; - - int count = _ui->_treeWidget->topLevelItemCount(); - if (count >= maxIssueCount) { - delete item; - return; - } - - _ui->_treeWidget->setSortingEnabled(false); - _reenableSorting.start(); - - // Insert item specific errors behind the others - int insertLoc = 0; - if (!item->text(1).isEmpty()) { - for (int i = 0; i < count; ++i) { - if (_ui->_treeWidget->topLevelItem(i)->text(1).isEmpty()) { - insertLoc = i + 1; - } else { - break; - } - } - } - - // Wipe any existing message for the same folder and path - auto newData = ProtocolItem::extraData(item); - if (_pathsWithIssues.contains(pathsWithIssuesKey(newData))) { - for (int i = 0; i < count; ++i) { - auto otherItem = _ui->_treeWidget->topLevelItem(i); - auto otherData = ProtocolItem::extraData(otherItem); - if (otherData.path == newData.path && otherData.folderName == newData.folderName) { - delete otherItem; - break; - } - } - } - - _ui->_treeWidget->insertTopLevelItem(insertLoc, item); - _pathsWithIssues.insert(pathsWithIssuesKey(newData)); - item->setHidden(!shouldBeVisible(item, currentAccountFilter(), currentFolderFilter())); - emit issueCountUpdated(_ui->_treeWidget->topLevelItemCount()); -} - -void IssuesWidget::slotOpenFile(QTreeWidgetItem *item, int) -{ - QString fileName = item->text(1); - if (Folder *folder = ProtocolItem::folder(item)) { - // folder->path() always comes back with trailing path - QString fullPath = folder->path() + fileName; - if (QFile(fullPath).exists()) { - showInFileManager(fullPath); - } - } -} - void IssuesWidget::slotProgressInfo(const QString &folder, const ProgressInfo &progress) { if (progress.status() == ProgressInfo::Reconcile) { @@ -245,20 +121,23 @@ void IssuesWidget::slotProgressInfo(const QString &folder, const ProgressInfo &p return; const auto &engine = f->syncEngine(); const auto style = engine.lastLocalDiscoveryStyle(); - cleanItems([&](QTreeWidgetItem *item) { - if (ProtocolItem::extraData(item).folderName != folder) + _model->remove([&](const ProtocolItem &item) { + if (item.folderName() != folder) { return false; - if (style == LocalDiscoveryStyle::FilesystemOnly) + } + if (style == LocalDiscoveryStyle::FilesystemOnly) { return true; - if (!persistsUntilLocalDiscovery(item)) + } + if (!persistsUntilLocalDiscovery(item)) { return true; - + } // Definitely wipe the entry if the file no longer exists - if (!QFileInfo(f->path() + ProtocolItem::extraData(item).path).exists()) + if (!QFileInfo::exists(f->path() + item.path())) { return true; + } - auto path = QFileInfo(ProtocolItem::extraData(item).path).dir().path(); - if (path == ".") + auto path = QFileInfo(item.path()).dir().path(); + if (path == QLatin1Char('.')) path.clear(); return engine.shouldDiscoverLocally(path); @@ -268,13 +147,10 @@ void IssuesWidget::slotProgressInfo(const QString &folder, const ProgressInfo &p // We keep track very well of pending conflicts. // Inform other components about them. QStringList conflicts; - auto tree = _ui->_treeWidget; - for (int i = 0; i < tree->topLevelItemCount(); ++i) { - auto item = tree->topLevelItem(i); - auto data = ProtocolItem::extraData(item); - if (data.folderName == folder - && data.status == SyncFileItem::Conflict) { - conflicts.append(data.path); + for (const auto &data : _model->rawData()) { + if (data.folderName() == folder + && data.status() == SyncFileItem::Conflict) { + conflicts.append(data.path()); } } emit ProgressDispatcher::instance()->folderConflicts(folder, conflicts); @@ -287,215 +163,20 @@ void IssuesWidget::slotItemCompleted(const QString &folder, const SyncFileItemPt { if (!item->showInIssuesTab()) return; - QTreeWidgetItem *line = ProtocolItem::create(folder, *item); - if (!line) - return; - addItem(line); + _model->addProtocolItem(ProtocolItem { folder, item }); } -void IssuesWidget::slotRefreshIssues() +void IssuesWidget::slotItemContextMenu() { - auto tree = _ui->_treeWidget; - auto filterFolderAlias = currentFolderFilter(); - auto filterAccount = currentAccountFilter(); - - for (int i = 0; i < tree->topLevelItemCount(); ++i) { - auto item = tree->topLevelItem(i); - item->setHidden(!shouldBeVisible(item, filterAccount, filterFolderAlias)); + auto rows = _ui->_tableView->selectionModel()->selectedRows(); + for (int i = 0; i < rows.size(); ++i) { + rows[i] = _sortModel->mapToSource(rows[i]); } - - _ui->_treeWidget->setColumnHidden(2, !filterFolderAlias.isEmpty()); -} - -void IssuesWidget::slotAccountAdded(AccountState *account) -{ - _ui->filterAccount->addItem(account->account()->displayName(), QVariant::fromValue(account)); - updateAccountChoiceVisibility(); -} - -void IssuesWidget::slotAccountRemoved(AccountState *account) -{ - for (int i = _ui->filterAccount->count() - 1; i >= 0; --i) { - if (account == _ui->filterAccount->itemData(i).value<AccountState *>()) - _ui->filterAccount->removeItem(i); - } - updateAccountChoiceVisibility(); -} - -void IssuesWidget::slotItemContextMenu(const QPoint &pos) -{ - auto item = _ui->_treeWidget->itemAt(pos); - if (!item) - return; - auto globalPos = _ui->_treeWidget->viewport()->mapToGlobal(pos); - ProtocolItem::openContextMenu(globalPos, item, this); -} - -void IssuesWidget::updateAccountChoiceVisibility() -{ - bool visible = _ui->filterAccount->count() > 2; - _ui->filterAccount->setVisible(visible); - _ui->accountLabel->setVisible(visible); - slotUpdateFolderFilters(); -} - -AccountState *IssuesWidget::currentAccountFilter() const -{ - return _ui->filterAccount->currentData().value<AccountState *>(); -} - -QString IssuesWidget::currentFolderFilter() const -{ - return _ui->filterFolder->currentData().toString(); -} - -bool IssuesWidget::shouldBeVisible(QTreeWidgetItem *item, AccountState *filterAccount, - const QString &filterFolderAlias) const -{ - bool visible = true; - auto data = ProtocolItem::extraData(item); - auto status = data.status; - visible &= (_ui->showIgnores->isChecked() || status != SyncFileItem::FileIgnored); - visible &= (_ui->showWarnings->isChecked() - || (status != SyncFileItem::SoftError - && status != SyncFileItem::Restoration)); - - const auto &folderalias = data.folderName; - if (filterAccount) { - auto folder = FolderMan::instance()->folder(folderalias); - visible &= folder && folder->accountState() == filterAccount; - } - visible &= (filterFolderAlias.isEmpty() || filterFolderAlias == folderalias); - - return visible; -} - -void IssuesWidget::slotUpdateFolderFilters() -{ - auto account = _ui->filterAccount->currentData().value<AccountState *>(); - - // If there is no account selector, show folders for the single - // available account - if (_ui->filterAccount->isHidden() && _ui->filterAccount->count() > 1) { - account = _ui->filterAccount->itemData(1).value<AccountState *>(); - } - - if (!account) { - _ui->filterFolder->setCurrentIndex(0); - } - _ui->filterFolder->setEnabled(account != nullptr); - - for (int i = _ui->filterFolder->count() - 1; i >= 1; --i) { - _ui->filterFolder->removeItem(i); - } - - // Find all selectable folders while figuring out if we need a folder - // selector in the first place - bool anyAccountHasMultipleFolders = false; - QSet<AccountState *> accountsWithFolders; - for (auto folder : FolderMan::instance()->map().values()) { - if (accountsWithFolders.contains(folder->accountState())) - anyAccountHasMultipleFolders = true; - accountsWithFolders.insert(folder->accountState()); - - if (folder->accountState() != account) - continue; - _ui->filterFolder->addItem(folder->shortGuiLocalPath(), folder->alias()); - } - - // If we don't need the combo box, hide it. - _ui->filterFolder->setVisible(anyAccountHasMultipleFolders); - _ui->folderLabel->setVisible(anyAccountHasMultipleFolders); - - // If there's no choice, select the only folder and disable - if (_ui->filterFolder->count() == 2 && anyAccountHasMultipleFolders) { - _ui->filterFolder->setCurrentIndex(1); - _ui->filterFolder->setEnabled(false); - } -} - -void IssuesWidget::storeSyncIssues(QTextStream &ts) -{ - int topLevelItems = _ui->_treeWidget->topLevelItemCount(); - - for (int i = 0; i < topLevelItems; i++) { - QTreeWidgetItem *child = _ui->_treeWidget->topLevelItem(i); - if (child->isHidden()) - continue; - ts << right - // time stamp - << qSetFieldWidth(20) - << child->data(0, Qt::DisplayRole).toString() - // separator - << qSetFieldWidth(0) << "," - - // file name - << qSetFieldWidth(64) - << child->data(1, Qt::DisplayRole).toString() - // separator - << qSetFieldWidth(0) << "," - - // folder - << qSetFieldWidth(30) - << child->data(2, Qt::DisplayRole).toString() - // separator - << qSetFieldWidth(0) << "," - - // action - << qSetFieldWidth(15) - << child->data(3, Qt::DisplayRole).toString() - << qSetFieldWidth(0) - << endl; - } -} - -void IssuesWidget::showFolderErrors(const QString &folderAlias) -{ - auto folder = FolderMan::instance()->folder(folderAlias); - if (!folder) - return; - - _ui->filterAccount->setCurrentIndex( - qMax(0, _ui->filterAccount->findData(QVariant::fromValue(folder->accountState())))); - _ui->filterFolder->setCurrentIndex( - qMax(0, _ui->filterFolder->findData(folderAlias))); - _ui->showIgnores->setChecked(false); - _ui->showWarnings->setChecked(false); -} - -void IssuesWidget::addError(const QString &folderAlias, const QString &message, - ErrorCategory category) -{ - auto folder = FolderMan::instance()->folder(folderAlias); - if (!folder) - return; - - QStringList columns; - QDateTime timestamp = QDateTime::currentDateTime(); - const QString timeStr = ProtocolItem::timeString(timestamp); - const QString longTimeStr = ProtocolItem::timeString(timestamp, QLocale::LongFormat); - - columns << timeStr; - columns << ""; // no "File" entry - columns << folder->shortGuiLocalPath(); - columns << message; - - QIcon icon = Theme::instance()->syncStateIcon(SyncResult::Error); - - QTreeWidgetItem *twitem = new ProtocolItem(columns); - twitem->setIcon(0, icon); - twitem->setToolTip(0, longTimeStr); - twitem->setToolTip(3, message); - ProtocolItem::ExtraData data; - data.timestamp = timestamp; - data.folderName = folderAlias; - data.status = SyncFileItem::NormalError; - ProtocolItem::setExtraData(twitem, data); - - addItem(twitem); - addErrorWidget(twitem, message, category); + ProtocolWidget::showContextMenu(this, _model, rows); } +// TODO: needs porting +#if 0 void IssuesWidget::addErrorWidget(QTreeWidgetItem *item, const QString &message, ErrorCategory category) { QWidget *widget = nullptr; @@ -532,4 +213,5 @@ void IssuesWidget::retryInsufficentRemoteStorageErrors(const QString &folderAlia folder->journalDb()->wipeErrorBlacklistCategory(SyncJournalErrorBlacklistRecord::InsufficientRemoteStorage); folderman->scheduleFolderNext(folder); } +#endif } diff --git a/src/gui/issueswidget.h b/src/gui/issueswidget.h index c5c2874fd..0f68a7184 100644 --- a/src/gui/issueswidget.h +++ b/src/gui/issueswidget.h @@ -20,12 +20,13 @@ #include <QLocale> #include <QTimer> +#include "protocolitemmodel.h" #include "progressdispatcher.h" #include "owncloudgui.h" #include "ui_issueswidget.h" -class QPushButton; +class QSortFilterProxyModel; namespace OCC { class SyncResult; @@ -46,50 +47,23 @@ public: explicit IssuesWidget(QWidget *parent = nullptr); ~IssuesWidget() override; - void storeSyncIssues(QTextStream &ts); - void showFolderErrors(const QString &folderAlias); - public slots: - void addError(const QString &folderAlias, const QString &message, ErrorCategory category); + // void addError(const QString &folderAlias, const QString &message, ErrorCategory category); void slotProgressInfo(const QString &folder, const ProgressInfo &progress); void slotItemCompleted(const QString &folder, const SyncFileItemPtr &item); - void slotOpenFile(QTreeWidgetItem *item, int); - -protected: - void showEvent(QShowEvent *) override; - void hideEvent(QHideEvent *) override; signals: - void copyToClipboard(); void issueCountUpdated(int); private slots: - void slotRefreshIssues(); - void slotUpdateFolderFilters(); - void slotAccountAdded(AccountState *account); - void slotAccountRemoved(AccountState *account); - void slotItemContextMenu(const QPoint &pos); + void slotItemContextMenu(); private: - void updateAccountChoiceVisibility(); - AccountState *currentAccountFilter() const; - QString currentFolderFilter() const; - bool shouldBeVisible(QTreeWidgetItem *item, AccountState *filterAccount, - const QString &filterFolderAlias) const; - void cleanItems(const std::function<bool(QTreeWidgetItem *)> &shouldDelete); - void addItem(QTreeWidgetItem *item); - - /// Add the special error widget for the category, if any - void addErrorWidget(QTreeWidgetItem *item, const QString &message, ErrorCategory category); - /// Wipes all insufficient remote storgage blacklist entries - void retryInsufficentRemoteStorageErrors(const QString &folderAlias); - - /// Each insert disables sorting, this timer reenables it - QTimer _reenableSorting; + // void retryInsufficentRemoteStorageErrors(const QString &folderAlias); - /// Optimization: keep track of all folder/paths pairs that have an associated issue - QSet<QPair<QString, QString>> _pathsWithIssues; + ProtocolItemModel *_model; + QSortFilterProxyModel *_sortModel; Ui::IssuesWidget *_ui; }; diff --git a/src/gui/issueswidget.ui b/src/gui/issueswidget.ui index 73c0ca5d5..7f6bab062 100644 --- a/src/gui/issueswidget.ui +++ b/src/gui/issueswidget.ui @@ -25,110 +25,32 @@ </widget> </item> <item> - <layout class="QHBoxLayout" name="horizontalLayout_3"> - <item> - <layout class="QFormLayout" name="accountFolderLayout"> - <item row="0" column="0"> - <widget class="QLabel" name="accountLabel"> - <property name="text"> - <string>Account</string> - </property> - </widget> - </item> - <item row="0" column="1"> - <widget class="QComboBox" name="filterAccount"> - <item> - <property name="text"> - <string><no filter></string> - </property> - </item> - </widget> - </item> - <item row="1" column="0"> - <widget class="QLabel" name="folderLabel"> - <property name="text"> - <string>Folder</string> - </property> - </widget> - </item> - <item row="1" column="1"> - <widget class="QComboBox" name="filterFolder"> - <property name="enabled"> - <bool>false</bool> - </property> - <item> - <property name="text"> - <string><no filter></string> - </property> - </item> - </widget> - </item> - </layout> - </item> - <item> - <layout class="QFormLayout" name="formLayout_2"> - <item row="0" column="1"> - <widget class="QCheckBox" name="showWarnings"> - <property name="text"> - <string>Show warnings</string> - </property> - <property name="checked"> - <bool>true</bool> - </property> - </widget> - </item> - <item row="1" column="1"> - <widget class="QCheckBox" name="showIgnores"> - <property name="text"> - <string>Show ignored files</string> - </property> - <property name="checked"> - <bool>true</bool> - </property> - </widget> - </item> - </layout> - </item> - </layout> - </item> - <item> - <widget class="QTreeWidget" name="_treeWidget"> + <widget class="QTableView" name="_tableView"> + <property name="contextMenuPolicy"> + <enum>Qt::CustomContextMenu</enum> + </property> <property name="alternatingRowColors"> <bool>true</bool> </property> - <property name="rootIsDecorated"> + <property name="selectionBehavior"> + <enum>QAbstractItemView::SelectRows</enum> + </property> + <property name="showGrid"> <bool>false</bool> </property> <property name="sortingEnabled"> <bool>true</bool> </property> - <property name="columnCount"> - <number>4</number> - </property> - <column> - <property name="text"> - <string notr="true">1</string> - </property> - </column> - <column> - <property name="text"> - <string notr="true">2</string> - </property> - </column> - <column> - <property name="text"> - <string notr="true">3</string> - </property> - </column> - <column> - <property name="text"> - <string notr="true">4</string> - </property> - </column> + <attribute name="horizontalHeaderShowSortIndicator" stdset="0"> + <bool>true</bool> + </attribute> + <attribute name="verticalHeaderVisible"> + <bool>false</bool> + </attribute> </widget> </item> <item> - <layout class="QHBoxLayout" name="horizontalLayout_2" stretch="1,0"> + <layout class="QHBoxLayout" name="horizontalLayout_2" stretch="1"> <item> <layout class="QVBoxLayout" name="verticalLayout_2"> <item> @@ -156,41 +78,8 @@ </item> </layout> </item> - <item> - <layout class="QVBoxLayout" name="verticalLayout_3"> - <item> - <spacer name="verticalSpacer"> - <property name="orientation"> - <enum>Qt::Vertical</enum> - </property> - <property name="sizeType"> - <enum>QSizePolicy::Minimum</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>20</width> - <height>0</height> - </size> - </property> - </spacer> - </item> - <item> - <widget class="QPushButton" name="copyIssuesButton"> - <property name="toolTip"> - <string>Copy the issues list to the clipboard.</string> - </property> - <property name="text"> - <string>Copy</string> - </property> - </widget> - </item> - </layout> - </item> </layout> </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout"/> - </item> </layout> </widget> <resources/> diff --git a/src/gui/models.cpp b/src/gui/models.cpp new file mode 100644 index 000000000..2c0c37c5a --- /dev/null +++ b/src/gui/models.cpp @@ -0,0 +1,49 @@ +/* + * Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ +#include "models.h" + +#include <QApplication> +#include <QItemSelectionRange> +#include <QTextStream> + +QString OCC::Models::formatSelection(const QModelIndexList &items) +{ + if (items.isEmpty()) { + return {}; + } + const auto columns = items.first().model()->columnCount(); + QString out; + QTextStream stream(&out); + + for (int c = 0; c < columns; ++c) { + const auto width = items.first().model()->headerData(c, Qt::Horizontal, StringFormatWidthRole).toInt(); + Q_ASSERT(width); + stream << right + << qSetFieldWidth(width) + << items.first().model()->headerData(c, Qt::Horizontal).toString() + << qSetFieldWidth(0) << ","; + } + stream << endl; + for (const auto &index : items) { + for (int c = 0; c < columns; ++c) { + const auto &child = index.siblingAtColumn(c); + stream << right + << qSetFieldWidth(child.model()->headerData(c, Qt::Horizontal, StringFormatWidthRole).toInt()) + << child.data(Qt::DisplayRole).toString() + << qSetFieldWidth(0) << ","; + } + stream << endl; + } + return out; +} diff --git a/src/gui/models.h b/src/gui/models.h new file mode 100644 index 000000000..8cca50725 --- /dev/null +++ b/src/gui/models.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ +#pragma once + +#include <QModelIndexList> +#include <QString> +#include <QtGlobal> + +namespace OCC { + +namespace Models { + enum DataRoles { + UnderlyingDataRole = Qt::UserRole + 100, + StringFormatWidthRole // The width for a cvs formated column + }; + + /** + * Returns a cvs representation of a table + */ + QString formatSelection(const QModelIndexList &items); + + +} +} diff --git a/src/gui/protocolitem.cpp b/src/gui/protocolitem.cpp new file mode 100644 index 000000000..769236005 --- /dev/null +++ b/src/gui/protocolitem.cpp @@ -0,0 +1,82 @@ +/* + * Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ +#include "protocolitem.h" + +#include "progressdispatcher.h" + +#include <QApplication> +#include <QFileInfo> +#include <QMenu> +#include <QPointer> + +using namespace OCC; + +ProtocolItem::ProtocolItem(const QString &folder, const SyncFileItemPtr &item) + : _path(item->destination()) + , _folderName(folder) + , _size(item->_size) + , _status(item->_status) + , _direction(item->_direction) + , _message(item->_errorString) + , _sizeIsRelevant(ProgressInfo::isSizeDependent(*item)) +{ + if (!item->_responseTimeStamp.isEmpty()) { + _timestamp = QDateTime::fromString(QString::fromUtf8(item->_responseTimeStamp), Qt::RFC2822Date); + } else { + _timestamp = QDateTime::currentDateTime(); + } + if (_message.isEmpty()) { + _message = Progress::asResultString(*item); + } +} + +QString ProtocolItem::path() const +{ + return _path; +} + +QString ProtocolItem::folderName() const +{ + return _folderName; +} + +QDateTime ProtocolItem::timestamp() const +{ + return _timestamp; +} + +qint64 ProtocolItem::size() const +{ + return _size; +} + +SyncFileItem::Status ProtocolItem::status() const +{ + return _status; +} + +SyncFileItem::Direction ProtocolItem::direction() const +{ + return _direction; +} + +QString ProtocolItem::message() const +{ + return _message; +} + +bool ProtocolItem::isSizeRelevant() const +{ + return _sizeIsRelevant; +} diff --git a/src/gui/protocolitem.h b/src/gui/protocolitem.h new file mode 100644 index 000000000..84ab99150 --- /dev/null +++ b/src/gui/protocolitem.h @@ -0,0 +1,65 @@ +/* + * Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ +#pragma once + +#include "csync/csync.h" +#include "libsync/syncfileitem.h" + +namespace OCC { + +class ProtocolItem +{ + Q_GADGET +public: + ProtocolItem() = default; + explicit ProtocolItem(const QString &folder, const SyncFileItemPtr &item); + + QString path() const; + + QString folderName() const; + + QDateTime timestamp() const; + + qint64 size() const; + + SyncFileItem::Status status() const; + + SyncFileItem::Direction direction() const; + + QString message() const; + + bool isSizeRelevant() const; + +private: + QString _path; + QString _folderName; + QDateTime _timestamp; + qint64 _size; + SyncFileItem::Status _status BITFIELD(4); + SyncFileItem::Direction _direction BITFIELD(3); + + QString _message; + bool _sizeIsRelevant; + + /** + * The creation id + */ + qulonglong _id = [] { + static qulonglong count = 0; + return ++count; + }(); + friend class ProtocolItemModel; +}; + +} diff --git a/src/gui/protocolitemmodel.cpp b/src/gui/protocolitemmodel.cpp new file mode 100644 index 000000000..84cf5c4ef --- /dev/null +++ b/src/gui/protocolitemmodel.cpp @@ -0,0 +1,217 @@ +/* + * Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ +#include "protocolitemmodel.h" + +#include "account.h" +#include "accountstate.h" +#include "models.h" +#include "gui/folderman.h" + +#include "theme.h" + +#include <QIcon> + +namespace { +auto getFolder(const OCC::ProtocolItem &item) +{ + auto f = OCC::FolderMan::instance()->folder(item.folderName()); + OC_ASSERT(f); + return f; +} +} + +using namespace OCC; + +ProtocolItemModel::ProtocolItemModel(QObject *parent, bool issueMode, int maxLogSize) + : QAbstractTableModel(parent) + , _issueMode(issueMode) + , _maxLogSize(maxLogSize) +{ + _data.reserve(maxLogSize); +} + +int ProtocolItemModel::rowCount(const QModelIndex &parent) const +{ + Q_ASSERT(checkIndex(parent)); + if (parent.isValid()) { + return 0; + } + return actualSize(); +} + +int ProtocolItemModel::columnCount(const QModelIndex &parent) const +{ + Q_ASSERT(checkIndex(parent)); + if (parent.isValid()) { + return 0; + } + return static_cast<int>(ProtocolItemRole::ColumnCount); +} + +QVariant ProtocolItemModel::data(const QModelIndex &index, int role) const +{ + Q_ASSERT(checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid)); + + const auto column = static_cast<ProtocolItemRole>(index.column()); + const auto &item = protocolItem(index); + switch (role) { + case Qt::DisplayRole: + switch (column) { + case ProtocolItemRole::Time: + return item.timestamp(); + case ProtocolItemRole::Folder: + return getFolder(item)->shortGuiLocalPath(); + case ProtocolItemRole::Action: + return item.message(); + case ProtocolItemRole::Size: + return item.isSizeRelevant() ? Utility::octetsToString(item.size()) : QVariant(); + case ProtocolItemRole::File: + return Utility::fileNameForGuiUse(item.path()); + case ProtocolItemRole::Account: + return getFolder(item)->accountState()->account()->displayName(); + case ProtocolItemRole::ColumnCount: + Q_UNREACHABLE(); + break; + } + case Qt::DecorationRole: + if (column == ProtocolItemRole::Action) { + const auto status = item.status(); + if (status == SyncFileItem::NormalError + || status == SyncFileItem::FatalError + || status == SyncFileItem::DetailError + || status == SyncFileItem::BlacklistedError) { + return Theme::instance()->syncStateIcon(SyncResult::Error); + } else if (Progress::isWarningKind(status)) { + return Theme::instance()->syncStateIcon(SyncResult::Problem); + } else { + return {}; + // TODO: display icon on success? + // return Theme::instance()->syncStateIcon(SyncResult::Success); + } + } + case Models::UnderlyingDataRole: + switch (column) { + case ProtocolItemRole::Time: + return item.timestamp(); + case ProtocolItemRole::Folder: + return item.folderName(); + case ProtocolItemRole::Action: + return item.message(); + case ProtocolItemRole::Size: + return item.size(); + case ProtocolItemRole::File: + return item.path(); + case ProtocolItemRole::Account: + return getFolder(item)->accountState()->account()->displayName(); + case ProtocolItemRole::ColumnCount: + Q_UNREACHABLE(); + break; + } + } + return {}; +} + +QVariant ProtocolItemModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Horizontal) { + const auto actionRole = static_cast<ProtocolItemRole>(section); + switch (role) { + case Qt::DisplayRole: + switch (actionRole) { + case ProtocolItemRole::Time: + return tr("Time"); + case ProtocolItemRole::File: + return tr("File"); + case ProtocolItemRole::Folder: + return tr("Folder"); + case ProtocolItemRole::Action: + return _issueMode ? tr("Issues") : tr("Action"); + case ProtocolItemRole::Size: + return tr("Size"); + case ProtocolItemRole::Account: + return tr("Account"); + case ProtocolItemRole::ColumnCount: + Q_UNREACHABLE(); + break; + }; + + case Models::StringFormatWidthRole: + // TODO: fine tune + switch (actionRole) { + case ProtocolItemRole::Time: + return 20; + case ProtocolItemRole::Folder: + return 30; + case ProtocolItemRole::Action: + return 15; + case ProtocolItemRole::Size: + return 6; + case ProtocolItemRole::File: + return 64; + case ProtocolItemRole::Account: + return 20; + case ProtocolItemRole::ColumnCount: + Q_UNREACHABLE(); + break; + } + }; + } + return QAbstractTableModel::headerData(section, orientation, role); +} + +void ProtocolItemModel::addProtocolItem(const ProtocolItem &&item) +{ + Q_ASSERT(actualSize() == _data.size()); + if (_data.size() >= _maxLogSize) { + beginRemoveRows(QModelIndex(), 0, 0); + _start++; + endRemoveRows(); + } else { + _data.push_back({}); + } + // _data.size() might differ + const auto size = actualSize(); + beginInsertRows(QModelIndex(), size, size); + _data[convertToIndex(size)] = std::move(item); + _end++; + endInsertRows(); +} + +const ProtocolItem &ProtocolItemModel::protocolItem(const QModelIndex &index) const +{ + Q_ASSERT(checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid)); + return _data.at(convertToIndex(index.row())); +} + +const std::vector<ProtocolItem> &ProtocolItemModel::rawData() const +{ + return _data; +} + +void ProtocolItemModel::remove(const std::function<bool(const ProtocolItem &)> &filter) +{ + if (_data.empty()) { + return; + } + const auto first = protocolItem(index(0, 0)); + beginResetModel(); + _data.erase(std::remove_if(_data.begin(), _data.end(), filter), _data.end()); + // find start again + _start = std::distance(_data.cbegin(), std::find_if(_data.cbegin(), _data.cend(), [&first](const ProtocolItem &pi) { + return pi._id <= first._id; + })); + _end = _start + _data.size(); + endResetModel(); + _data.reserve(_maxLogSize); +} diff --git a/src/gui/protocolitemmodel.h b/src/gui/protocolitemmodel.h new file mode 100644 index 000000000..c2ea8be15 --- /dev/null +++ b/src/gui/protocolitemmodel.h @@ -0,0 +1,77 @@ +/* + * Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ +#pragma once + +#include <QAbstractTableModel> + +#include "protocolitem.h" + + +namespace OCC { +class ProtocolItemModel : public QAbstractTableModel +{ + Q_OBJECT +public: + enum class ProtocolItemRole { + Action, + File, + Folder, + Size, + Account, + Time, + + ColumnCount + }; + /** + * @brief ProtocolItemModel + * @param parent + * @param issueMode Whether we are tracking all synced items or issues + */ + ProtocolItemModel(QObject *parent = nullptr, bool issueMode = false, int maxLogSize=2000); + + int rowCount(const QModelIndex &parent = {}) const override; + int columnCount(const QModelIndex &parent = {}) const override; + QVariant data(const QModelIndex &index, int role) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + + void addProtocolItem(const ProtocolItem &&item); + const ProtocolItem &protocolItem(const QModelIndex &index) const; + + const std::vector<ProtocolItem> &rawData() const; + void remove(const std::function<bool(const ProtocolItem &)> &filter); + + bool isModelFull() const + { + return _data.size() == _maxLogSize; + } + +private: + bool _issueMode; + int _maxLogSize; + std::vector<ProtocolItem> _data; + + qulonglong _start = 0; + qulonglong _end = 0; + + constexpr qulonglong actualSize() const + { + return _end - _start; + } + constexpr int convertToIndex(qulonglong i) const + { + return (i + _start) % _maxLogSize; + } +}; + +} diff --git a/src/gui/protocolwidget.cpp b/src/gui/protocolwidget.cpp index 4d962d2e5..dc7e3fe62 100644 --- a/src/gui/protocolwidget.cpp +++ b/src/gui/protocolwidget.cpp @@ -12,166 +12,32 @@ * for more details. */ +#include <QCursor> #include <QtGui> #include <QtWidgets> +#include "activitylistmodel.h" #include "protocolwidget.h" #include "configfile.h" #include "syncresult.h" #include "logger.h" #include "theme.h" #include "folderman.h" -#include "syncfileitem.h" #include "folder.h" +#include "models.h" #include "openfilemanager.h" #include "guiutility.h" #include "accountstate.h" +#include "syncfileitem.h" +#include "activitylistmodel.h" #include "ui_protocolwidget.h" #include <climits> -Q_DECLARE_METATYPE(OCC::ProtocolItem::ExtraData) namespace OCC { -QString ProtocolItem::timeString(QDateTime dt, QLocale::FormatType format) -{ - const QLocale loc = QLocale::system(); - QString dtFormat = loc.dateTimeFormat(format); - static const QRegExp re("(HH|H|hh|h):mm(?!:s)"); - dtFormat.replace(re, "\\1:mm:ss"); - return loc.toString(dt, dtFormat); -} - -ProtocolItem::ExtraData ProtocolItem::extraData(const QTreeWidgetItem *item) -{ - return item->data(0, Qt::UserRole).value<ExtraData>(); -} - -void ProtocolItem::setExtraData(QTreeWidgetItem *item, const ExtraData &data) -{ - item->setData(0, Qt::UserRole, QVariant::fromValue(data)); -} - -ProtocolItem *ProtocolItem::create(const QString &folderName, const SyncFileItem &item) -{ - auto folder = FolderMan::instance()->folder(folderName); - - QStringList columns; - QDateTime timestamp = QDateTime::currentDateTime(); - const QString timeStr = timeString(timestamp); - const QString longTimeStr = timeString(timestamp, QLocale::LongFormat); - - columns << timeStr; - columns << Utility::fileNameForGuiUse(item._originalFile); - columns << (folder ? folder->shortGuiLocalPath() : QDir::toNativeSeparators(folderName)); - - // If the error string is set, it's prefered because it is a useful user message. - QString message = item._errorString; - if (message.isEmpty()) { - message = item._messageString; - } - if (message.isEmpty()) { - message = Progress::asResultString(item); - } - columns << message; - - QIcon icon; - if (item._status == SyncFileItem::NormalError - || item._status == SyncFileItem::FatalError - || item._status == SyncFileItem::DetailError - || item._status == SyncFileItem::BlacklistedError) { - icon = Theme::instance()->syncStateIcon(SyncResult::Error); - } else if (Progress::isWarningKind(item._status)) { - icon = Theme::instance()->syncStateIcon(SyncResult::Problem); - } - - if (ProgressInfo::isSizeDependent(item)) { - columns << Utility::octetsToString(item._size); - } - - ProtocolItem *twitem = new ProtocolItem(columns); - // Warning: The data and tooltips on the columns define an implicit - // interface and can only be changed with care. - twitem->setIcon(0, icon); - twitem->setToolTip(0, longTimeStr); - twitem->setToolTip(1, item.destination()); - twitem->setToolTip(3, message); - ProtocolItem::ExtraData data; - data.timestamp = timestamp; - data.path = item.destination(); - data.folderName = folderName; - data.status = item._status; - data.size = item._size; - data.direction = item._direction; - ProtocolItem::setExtraData(twitem, data); - return twitem; -} - -SyncJournalFileRecord ProtocolItem::syncJournalRecord(QTreeWidgetItem *item) -{ - SyncJournalFileRecord rec; - auto f = folder(item); - if (!f) - return rec; - f->journalDb()->getFileRecord(extraData(item).path, &rec); - return rec; -} - -Folder *ProtocolItem::folder(QTreeWidgetItem *item) -{ - return FolderMan::instance()->folder(extraData(item).folderName); -} - -void ProtocolItem::openContextMenu(QPoint globalPos, QTreeWidgetItem *item, QWidget *parent) -{ - auto f = folder(item); - if (!f) - return; - AccountPtr account = f->accountState()->account(); - auto rec = syncJournalRecord(item); - // rec might not be valid - - auto menu = new QMenu(parent); - - if (rec.isValid()) { - // "Open in Browser" action - auto openInBrowser = menu->addAction(ProtocolWidget::tr("Open in browser")); - QObject::connect(openInBrowser, &QAction::triggered, parent, [parent, account, rec]() { - fetchPrivateLinkUrl(account, rec._path, parent, - [parent](const QString &url) { - Utility::openBrowser(url, parent); - }); - }); - } - - // More actions will be conditionally added to the context menu here later - - if (menu->actions().isEmpty()) { - delete menu; - return; - } - - menu->setAttribute(Qt::WA_DeleteOnClose); - menu->popup(globalPos); -} - -bool ProtocolItem::operator<(const QTreeWidgetItem &other) const -{ - int column = treeWidget()->sortColumn(); - if (column == 0) { - // Items with empty "File" column are larger than others, - // otherwise sort by time (this uses lexicographic ordering) - return std::forward_as_tuple(text(1).isEmpty(), extraData(this).timestamp) - < std::forward_as_tuple(other.text(1).isEmpty(), extraData(&other).timestamp); - } else if (column == 4) { - return extraData(this).size < extraData(&other).size; - } - - return QTreeWidgetItem::operator<(other); -} - ProtocolWidget::ProtocolWidget(QWidget *parent) : QWidget(parent) , _ui(new Ui::ProtocolWidget) @@ -181,37 +47,28 @@ ProtocolWidget::ProtocolWidget(QWidget *parent) connect(ProgressDispatcher::instance(), &ProgressDispatcher::itemCompleted, this, &ProtocolWidget::slotItemCompleted); - connect(_ui->_treeWidget, &QTreeWidget::itemActivated, this, &ProtocolWidget::slotOpenFile); + connect(_ui->_tableView, &QTreeWidget::customContextMenuRequested, this, &ProtocolWidget::slotItemContextMenu); - _ui->_treeWidget->setContextMenuPolicy(Qt::CustomContextMenu); - connect(_ui->_treeWidget, &QTreeWidget::customContextMenuRequested, this, &ProtocolWidget::slotItemContextMenu); + _model = new ProtocolItemModel(this); + _sortModel = new QSortFilterProxyModel(this); + _sortModel->setSourceModel(_model); + _sortModel->setSortRole(Models::UnderlyingDataRole); + _ui->_tableView->setModel(_sortModel); - // Adjust copyToClipboard() when making changes here! - QStringList header; - header << tr("Time"); - header << tr("File"); - header << tr("Folder"); - header << tr("Action"); - header << tr("Size"); + _ui->_tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive); + _ui->_tableView->horizontalHeader()->setSectionResizeMode(static_cast<int>(ProtocolItemModel::ProtocolItemRole::File), QHeaderView::Stretch); + _ui->_tableView->horizontalHeader()->setSortIndicator(static_cast<int>(ProtocolItemModel::ProtocolItemRole::Time), Qt::DescendingOrder); - _ui->_treeWidget->setHeaderLabels(header); - int timestampColumnWidth = - _ui->_treeWidget->fontMetrics().boundingRect(ProtocolItem::timeString(QDateTime::currentDateTime())).width(); - _ui->_treeWidget->setColumnWidth(0, timestampColumnWidth); - _ui->_treeWidget->setColumnWidth(1, 180); - _ui->_treeWidget->setColumnCount(5); - _ui->_treeWidget->setRootIsDecorated(false); - _ui->_treeWidget->setTextElideMode(Qt::ElideMiddle); - _ui->_treeWidget->header()->setObjectName("ActivityListHeader"); -#if defined(Q_OS_MAC) - _ui->_treeWidget->setMinimumWidth(400); -#endif - _ui->_headerLabel->setText(tr("Local sync protocol")); + _ui->_tableView->horizontalHeader()->setObjectName(QStringLiteral("ActivityListHeaderV2")); + ConfigFile cfg; + cfg.restoreGeometryHeader(_ui->_tableView->horizontalHeader()); + + connect(qApp, &QApplication::aboutToQuit, this, [this] { + ConfigFile cfg; + cfg.saveGeometryHeader(_ui->_tableView->horizontalHeader()); + }); - QPushButton *copyBtn = _ui->_dialogButtonBox->addButton(tr("Copy"), QDialogButtonBox::ActionRole); - copyBtn->setToolTip(tr("Copy the activity list to the clipboard.")); - copyBtn->setEnabled(true); - connect(copyBtn, &QAbstractButton::clicked, this, &ProtocolWidget::copyToClipboard); + _ui->_headerLabel->setText(tr("Local sync protocol")); } ProtocolWidget::~ProtocolWidget() @@ -219,104 +76,62 @@ ProtocolWidget::~ProtocolWidget() delete _ui; } -void ProtocolWidget::showEvent(QShowEvent *ev) -{ - ConfigFile cfg; - cfg.restoreGeometryHeader(_ui->_treeWidget->header()); - - // Sorting by section was newly enabled. But if we restore the header - // from a state where sorting was disabled, both of these flags will be - // false and sorting will be impossible! - _ui->_treeWidget->header()->setSectionsClickable(true); - _ui->_treeWidget->header()->setSortIndicatorShown(true); - - // Switch back to "by time" ordering - _ui->_treeWidget->sortByColumn(0, Qt::DescendingOrder); - - QWidget::showEvent(ev); -} - -void ProtocolWidget::hideEvent(QHideEvent *ev) +void ProtocolWidget::showContextMenu(QWidget *parent, ProtocolItemModel *model, const QModelIndexList &items) { - ConfigFile cfg; - cfg.saveGeometryHeader(_ui->_treeWidget->header()); - QWidget::hideEvent(ev); -} + auto menu = new QMenu(parent); + menu->setAttribute(Qt::WA_DeleteOnClose); -void ProtocolWidget::slotItemContextMenu(const QPoint &pos) -{ - auto item = _ui->_treeWidget->itemAt(pos); - if (!item) - return; - auto globalPos = _ui->_treeWidget->viewport()->mapToGlobal(pos); - ProtocolItem::openContextMenu(globalPos, item, this); + menu->addAction(tr("Copy to clipboard"), parent, [text = Models::formatSelection(items)] { + QApplication::clipboard()->setText(text); + }); + + if (items.size() == 1) { + const auto &data = model->protocolItem(items.first()); + auto folder = FolderMan::instance()->folder(data.folderName()); + OC_ASSERT(folder); + if (!folder) + return; + + { + const QString localPath = folder->path() + data.path(); + if (QFileInfo::exists(localPath)) { + menu->addAction(tr("Show in file browser"), parent, [localPath] { + if (QFileInfo::exists(localPath)) { + showInFileManager(localPath); + } + }); + } + } + // "Open in Browser" action + { + fetchPrivateLinkUrl(folder->accountState()->account(), folder->remotePathTrailingSlash() + data.path(), parent, [parent, menu = QPointer<QMenu>(menu)](const QString &url) { + // as fetchPrivateLinkUrl is async we need to check the menu still exists + if (menu) { + menu->addAction(tr("Show in web browser"), parent, [url, parent] { + Utility::openBrowser(url, parent); + }); + } + }); + } + } + menu->popup(QCursor::pos()); } -void ProtocolWidget::slotOpenFile(QTreeWidgetItem *item, int) +void ProtocolWidget::slotItemContextMenu() { - QString fileName = item->text(1); - if (Folder *folder = ProtocolItem::folder(item)) { - // folder->path() always comes back with trailing path - QString fullPath = folder->path() + fileName; - if (QFile(fullPath).exists()) { - showInFileManager(fullPath); - } + QModelIndexList list; + auto rows = _ui->_tableView->selectionModel()->selectedRows(); + for (int i = 0; i < rows.size(); ++i) { + rows[i] = _sortModel->mapToSource(rows[i]); } + showContextMenu(this, _model, rows); } void ProtocolWidget::slotItemCompleted(const QString &folder, const SyncFileItemPtr &item) { if (!item->showInProtocolTab()) return; - QTreeWidgetItem *line = ProtocolItem::create(folder, *item); - if (line) { - // Limit the number of items - int itemCnt = _ui->_treeWidget->topLevelItemCount(); - while (itemCnt > 2000) { - delete _ui->_treeWidget->takeTopLevelItem(itemCnt - 1); - itemCnt--; - } - _ui->_treeWidget->insertTopLevelItem(0, line); - } -} - -void ProtocolWidget::storeSyncActivity(QTextStream &ts) -{ - int topLevelItems = _ui->_treeWidget->topLevelItemCount(); - - for (int i = 0; i < topLevelItems; i++) { - QTreeWidgetItem *child = _ui->_treeWidget->topLevelItem(i); - ts << right - // time stamp - << qSetFieldWidth(20) - << child->data(0, Qt::DisplayRole).toString() - // separator - << qSetFieldWidth(0) << "," - - // file name - << qSetFieldWidth(64) - << child->data(1, Qt::DisplayRole).toString() - // separator - << qSetFieldWidth(0) << "," - - // folder - << qSetFieldWidth(30) - << child->data(2, Qt::DisplayRole).toString() - // separator - << qSetFieldWidth(0) << "," - - // action - << qSetFieldWidth(15) - << child->data(3, Qt::DisplayRole).toString() - // separator - << qSetFieldWidth(0) << "," - - // size - << qSetFieldWidth(10) - << child->data(4, Qt::DisplayRole).toString() - << qSetFieldWidth(0) - << endl; - } + _model->addProtocolItem(ProtocolItem { folder, item }); } } diff --git a/src/gui/protocolwidget.h b/src/gui/protocolwidget.h index 79ff35868..da3de2c60 100644 --- a/src/gui/protocolwidget.h +++ b/src/gui/protocolwidget.h @@ -21,13 +21,15 @@ #include "progressdispatcher.h" #include "owncloudgui.h" +#include "protocolitemmodel.h" +#include "protocolitem.h" #include "ui_protocolwidget.h" class QPushButton; +class QSortFilterProxyModel; namespace OCC { -class SyncResult; namespace Ui { class ProtocolWidget; @@ -35,49 +37,6 @@ namespace Ui { class Application; /** - * The items used in the protocol and issue QTreeWidget - * - * Special sorting: It allows items for global entries to be moved to the top if the - * sorting section is the "Time" column. - */ -class ProtocolItem : public QTreeWidgetItem -{ -public: - using QTreeWidgetItem::QTreeWidgetItem; - - // Shared with IssueWidget - static ProtocolItem *create(const QString &folder, const SyncFileItem &item); - static QString timeString(QDateTime dt, QLocale::FormatType format = QLocale::NarrowFormat); - - struct ExtraData - { - ExtraData() - : status(SyncFileItem::NoStatus) - , direction(SyncFileItem::None) - { - } - - QString path; - QString folderName; - QDateTime timestamp; - qint64 size = 0; - SyncFileItem::Status status BITFIELD(4); - SyncFileItem::Direction direction BITFIELD(3); - }; - - static ExtraData extraData(const QTreeWidgetItem *item); - static void setExtraData(QTreeWidgetItem *item, const ExtraData &data); - - static SyncJournalFileRecord syncJournalRecord(QTreeWidgetItem *item); - static Folder *folder(QTreeWidgetItem *item); - - static void openContextMenu(QPoint globalPos, QTreeWidgetItem *item, QWidget *parent); - -private: - bool operator<(const QTreeWidgetItem &other) const override; -}; - -/** * @brief The ProtocolWidget class * @ingroup gui */ @@ -88,23 +47,18 @@ public: explicit ProtocolWidget(QWidget *parent = nullptr); ~ProtocolWidget() override; - void storeSyncActivity(QTextStream &ts); + static void showContextMenu(QWidget *parent, ProtocolItemModel *model, const QModelIndexList &items); + public slots: void slotItemCompleted(const QString &folder, const SyncFileItemPtr &item); - void slotOpenFile(QTreeWidgetItem *item, int); - -protected: - void showEvent(QShowEvent *) override; - void hideEvent(QHideEvent *) override; private slots: - void slotItemContextMenu(const QPoint &pos); - -signals: - void copyToClipboard(); + void slotItemContextMenu(); private: + ProtocolItemModel *_model; + QSortFilterProxyModel *_sortModel; Ui::ProtocolWidget *_ui; }; } diff --git a/src/gui/protocolwidget.ui b/src/gui/protocolwidget.ui index 3867ec7a9..7941d7418 100644 --- a/src/gui/protocolwidget.ui +++ b/src/gui/protocolwidget.ui @@ -25,42 +25,28 @@ </widget> </item> <item> - <widget class="QTreeWidget" name="_treeWidget"> + <widget class="QTableView" name="_tableView"> + <property name="contextMenuPolicy"> + <enum>Qt::CustomContextMenu</enum> + </property> <property name="alternatingRowColors"> <bool>true</bool> </property> - <property name="rootIsDecorated"> - <bool>false</bool> + <property name="selectionBehavior"> + <enum>QAbstractItemView::SelectRows</enum> </property> - <property name="uniformRowHeights"> - <bool>true</bool> + <property name="showGrid"> + <bool>false</bool> </property> <property name="sortingEnabled"> <bool>true</bool> </property> - <property name="columnCount"> - <number>4</number> + <property name="wordWrap"> + <bool>false</bool> </property> - <column> - <property name="text"> - <string notr="true">1</string> - </property> - </column> - <column> - <property name="text"> - <string notr="true">2</string> - </property> - </column> - <column> - <property name="text"> - <string notr="true">3</string> - </property> - </column> - <column> - <property name="text"> - <string notr="true">4</string> - </property> - </column> + <attribute name="verticalHeaderVisible"> + <bool>false</bool> + </attribute> </widget> </item> <item> diff --git a/src/gui/settingsdialog.cpp b/src/gui/settingsdialog.cpp index c2bf31357..4402d9576 100644 --- a/src/gui/settingsdialog.cpp +++ b/src/gui/settingsdialog.cpp @@ -327,12 +327,12 @@ void SettingsDialog::showActivityPage() } } -void SettingsDialog::showIssuesList(const QString &folderAlias) +void SettingsDialog::showIssuesList() { if (!_activityAction) return; _activityAction->trigger(); - _activitySettings->slotShowIssuesTab(folderAlias); + _activitySettings->slotShowIssuesTab(); } void SettingsDialog::accountAdded(AccountState *s) diff --git a/src/gui/settingsdialog.h b/src/gui/settingsdialog.h index a9fdd4239..f977ffe6d 100644 --- a/src/gui/settingsdialog.h +++ b/src/gui/settingsdialog.h @@ -59,7 +59,7 @@ public: public slots: void showFirstPage(); void showActivityPage(); - void showIssuesList(const QString &folderAlias); + void showIssuesList(); void slotSwitchPage(QAction *action); void slotRefreshActivity(AccountState *accountState); void slotRefreshActivityAccountStateSender(); diff --git a/test/modeltests/CMakeLists.txt b/test/modeltests/CMakeLists.txt index c7f4bc693..e2e5d5934 100644 --- a/test/modeltests/CMakeLists.txt +++ b/test/modeltests/CMakeLists.txt @@ -1 +1,2 @@ owncloud_add_test(ActivityModel) +owncloud_add_test(ProtocolModel) diff --git a/test/modeltests/testactivitymodel.cpp b/test/modeltests/testactivitymodel.cpp index 63cec088b..066a9c9b8 100644 --- a/test/modeltests/testactivitymodel.cpp +++ b/test/modeltests/testactivitymodel.cpp @@ -23,7 +23,7 @@ private Q_SLOTS: { auto model = new ActivityListModel(this); - auto tester = new QAbstractItemModelTester(model, this); + new QAbstractItemModelTester(model, this); auto manager = AccountManager::instance(); diff --git a/test/modeltests/testprotocolmodel.cpp b/test/modeltests/testprotocolmodel.cpp new file mode 100644 index 000000000..2462827cc --- /dev/null +++ b/test/modeltests/testprotocolmodel.cpp @@ -0,0 +1,48 @@ + +/* + * This software is in the public domain, furnished "as is", without technical + * support, and with no warranty, express or implied, as to its usefulness for + * any purpose. + * + */ + +#include "gui/protocolitemmodel.h" + +#include <QTest> +#include <QAbstractItemModelTester> + +namespace OCC { + +class TestProtocolModel : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void testInsertAndRemove() + { + // no need to test with 20000 lines + const auto TestBacklogSize = 111; + auto model = new ProtocolItemModel(this, false, TestBacklogSize); + + new QAbstractItemModelTester(model, this); + + // populate with dummy data + auto item = SyncFileItemPtr::create(); + for (int i = 0; i < TestBacklogSize * 1.1; ++i) { + model->addProtocolItem({ ProtocolItem(QStringLiteral("foo") + QString::number(i), item) }); + } + + const auto oldSize = model->rowCount(); + QCOMPARE(oldSize, TestBacklogSize); + // pick one from the middle + const auto toBeRemoved = model->protocolItem(model->index(TestBacklogSize / 2, 0)); + model->remove([&toBeRemoved](const ProtocolItem &pi) { + return pi.folderName() == toBeRemoved.folderName(); + }); + QCOMPARE(oldSize - 1, model->rowCount()); + } +}; +} + +QTEST_GUILESS_MAIN(OCC::TestProtocolModel) +#include "testprotocolmodel.moc" |