/* * Copyright (C) by Klaas Freitag * * 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 #include #include "issueswidget.h" #include "configfile.h" #include "syncresult.h" #include "syncengine.h" #include "logger.h" #include "theme.h" #include "folderman.h" #include "syncfileitem.h" #include "folder.h" #include "openfilemanager.h" #include "activityitemdelegate.h" #include "protocolwidget.h" #include "accountstate.h" #include "account.h" #include "accountmanager.h" #include "common/syncjournalfilerecord.h" #include "elidedlabel.h" #include "ui_issueswidget.h" #include 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 pathsWithIssuesKey(const ProtocolItem::ExtraData &data) { return qMakePair(data.folderName, data.path); } IssuesWidget::IssuesWidget(QWidget *parent) : QWidget(parent) , _ui(new Ui::IssuesWidget) { _ui->setupUi(this); connect(ProgressDispatcher::instance(), &ProgressDispatcher::progressInfo, this, &IssuesWidget::slotProgressInfo); 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(&QComboBox::currentIndexChanged), this, &IssuesWidget::slotRefreshIssues); connect(_ui->filterAccount, static_cast(&QComboBox::currentIndexChanged), this, &IssuesWidget::slotUpdateFolderFilters); connect(_ui->filterFolder, static_cast(&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); // Adjust copyToClipboard() when making changes here! QStringList header; header << tr("Time"); header << tr("File"); header << tr("Folder"); header << tr("Issue"); int timestampColumnExtra = 0; #ifdef Q_OS_WIN timestampColumnExtra = 20; // font metrics are broken on Windows, see #4721 #endif _ui->_treeWidget->setHeaderLabels(header); int timestampColumnWidth = ActivityItemDelegate::rowHeight() // icon + _ui->_treeWidget->fontMetrics().boundingRect(ProtocolItem::timeString(QDateTime::currentDateTime())).width() + timestampColumnExtra; _ui->_treeWidget->setColumnWidth(0, timestampColumnWidth); _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 _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); }); _ui->_conflictHelp->hide(); _ui->_conflictHelp->setText( tr("There were conflicts. Check the documentation on how to resolve them.") .arg(Theme::instance()->conflictHelpUrl())); } 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 &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) { // Wipe all non-persistent entries - as well as the persistent ones // in cases where a local discovery was done. auto f = FolderMan::instance()->folder(folder); if (!f) return; const auto &engine = f->syncEngine(); const auto style = engine.lastLocalDiscoveryStyle(); cleanItems([&](QTreeWidgetItem *item) { if (ProtocolItem::extraData(item).folderName != folder) return false; if (style == LocalDiscoveryStyle::FilesystemOnly) return true; if (!persistsUntilLocalDiscovery(item)) return true; // Definitely wipe the entry if the file no longer exists if (!QFileInfo(f->path() + ProtocolItem::extraData(item).path).exists()) return true; auto path = QFileInfo(ProtocolItem::extraData(item).path).dir().path(); if (path == ".") path.clear(); return engine.shouldDiscoverLocally(path); }); } if (progress.status() == ProgressInfo::Done) { // 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); } } emit ProgressDispatcher::instance()->folderConflicts(folder, conflicts); _ui->_conflictHelp->setHidden(Theme::instance()->conflictHelpUrl().isEmpty() || conflicts.isEmpty()); } } void IssuesWidget::slotItemCompleted(const QString &folder, const SyncFileItemPtr &item) { if (!item->showInIssuesTab()) return; QTreeWidgetItem *line = ProtocolItem::create(folder, *item); if (!line) return; addItem(line); } void IssuesWidget::slotRefreshIssues() { 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)); } _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()) _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(); } 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(); // 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(); } 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 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->setData(0, Qt::SizeHintRole, QSize(0, ActivityItemDelegate::rowHeight())); 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); } void IssuesWidget::addErrorWidget(QTreeWidgetItem *item, const QString &message, ErrorCategory category) { QWidget *widget = nullptr; if (category == ErrorCategory::InsufficientRemoteStorage) { widget = new QWidget; auto layout = new QHBoxLayout; widget->setLayout(layout); auto label = new ElidedLabel(message, widget); label->setElideMode(Qt::ElideMiddle); layout->addWidget(label); auto button = new QPushButton("Retry all uploads", widget); button->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Expanding); auto folderAlias = ProtocolItem::extraData(item).folderName; connect(button, &QPushButton::clicked, this, [this, folderAlias]() { retryInsufficentRemoteStorageErrors(folderAlias); }); layout->addWidget(button); } if (widget) { item->setText(3, QString()); } _ui->_treeWidget->setItemWidget(item, 3, widget); } void IssuesWidget::retryInsufficentRemoteStorageErrors(const QString &folderAlias) { auto folderman = FolderMan::instance(); auto folder = folderman->folder(folderAlias); if (!folder) return; folder->journalDb()->wipeErrorBlacklistCategory(SyncJournalErrorBlacklistRecord::InsufficientRemoteStorage); folderman->scheduleFolderNext(folder); } }