From b757effd8dd3c7516dce3d687bc88d96d49d17c7 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Tue, 7 Nov 2017 16:12:07 +0100 Subject: [PATCH] Protocol: Introduce context menu with "open in browser" #6121 To do this conveniently a bunch of functionality that's common to IssueWidget and ProtocolWidget is moved to ProtocolItem. Also the convenience function to asynchronously retrieve the private link url is moved from the socket api to the network jobs. --- src/gui/issueswidget.cpp | 22 +++-- src/gui/issueswidget.h | 1 + src/gui/protocolwidget.cpp | 171 +++++++++++++++++++++++------------- src/gui/protocolwidget.h | 22 +++-- src/gui/socketapi.cpp | 34 ++----- src/libsync/networkjobs.cpp | 32 +++++++ src/libsync/networkjobs.h | 18 ++++ 7 files changed, 202 insertions(+), 98 deletions(-) diff --git a/src/gui/issueswidget.cpp b/src/gui/issueswidget.cpp index c132ff92501..64c108ea25d 100644 --- a/src/gui/issueswidget.cpp +++ b/src/gui/issueswidget.cpp @@ -32,6 +32,7 @@ #include "common/syncjournalfilerecord.h" #include "elidedlabel.h" + #include "ui_issueswidget.h" #include @@ -54,6 +55,9 @@ IssuesWidget::IssuesWidget(QWidget *parent) 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); @@ -85,7 +89,7 @@ IssuesWidget::IssuesWidget(QWidget *parent) _ui->_treeWidget->setHeaderLabels(header); int timestampColumnWidth = ActivityItemDelegate::rowHeight() // icon - + _ui->_treeWidget->fontMetrics().width(ProtocolWidget::timeString(QDateTime::currentDateTime())) + + _ui->_treeWidget->fontMetrics().width(ProtocolItem::timeString(QDateTime::currentDateTime())) + timestampColumnExtra; _ui->_treeWidget->setColumnWidth(0, timestampColumnWidth); _ui->_treeWidget->setColumnWidth(1, 180); @@ -198,7 +202,7 @@ void IssuesWidget::slotItemCompleted(const QString &folder, const SyncFileItemPt { if (!item->hasErrorStatus()) return; - QTreeWidgetItem *line = ProtocolWidget::createCompletedTreewidgetItem(folder, *item); + QTreeWidgetItem *line = ProtocolItem::create(folder, *item); if (!line) return; addItem(line); @@ -233,6 +237,14 @@ void IssuesWidget::slotAccountRemoved(AccountState *account) updateAccountChoiceVisibility(); } +void IssuesWidget::slotItemContextMenu(const QPoint &pos) +{ + auto item = _ui->_treeWidget->itemAt(pos); + if (!item) + return; + ProtocolItem::openContextMenu(item, this); +} + void IssuesWidget::updateAccountChoiceVisibility() { bool visible = _ui->filterAccount->count() > 2; @@ -373,8 +385,8 @@ void IssuesWidget::addError(const QString &folderAlias, const QString &message, QStringList columns; QDateTime timestamp = QDateTime::currentDateTime(); - const QString timeStr = ProtocolWidget::timeString(timestamp); - const QString longTimeStr = ProtocolWidget::timeString(timestamp, QLocale::LongFormat); + const QString timeStr = ProtocolItem::timeString(timestamp); + const QString longTimeStr = ProtocolItem::timeString(timestamp, QLocale::LongFormat); columns << timeStr; columns << ""; // no "File" entry @@ -383,7 +395,7 @@ void IssuesWidget::addError(const QString &folderAlias, const QString &message, QIcon icon = Theme::instance()->syncStateIcon(SyncResult::Error); - QTreeWidgetItem *twitem = new SortedTreeWidgetItem(columns); + QTreeWidgetItem *twitem = new ProtocolItem(columns); twitem->setData(0, Qt::SizeHintRole, QSize(0, ActivityItemDelegate::rowHeight())); twitem->setData(0, Qt::UserRole, timestamp); twitem->setIcon(0, icon); diff --git a/src/gui/issueswidget.h b/src/gui/issueswidget.h index 4accd89b220..9f295b8165f 100644 --- a/src/gui/issueswidget.h +++ b/src/gui/issueswidget.h @@ -68,6 +68,7 @@ private slots: void slotUpdateFolderFilters(); void slotAccountAdded(AccountState *account); void slotAccountRemoved(AccountState *account); + void slotItemContextMenu(const QPoint &pos); private: void updateAccountChoiceVisibility(); diff --git a/src/gui/protocolwidget.cpp b/src/gui/protocolwidget.cpp index e2715256361..29a44de6c09 100644 --- a/src/gui/protocolwidget.cpp +++ b/src/gui/protocolwidget.cpp @@ -25,6 +25,8 @@ #include "folder.h" #include "openfilemanager.h" #include "activityitemdelegate.h" +#include "guiutility.h" +#include "accountstate.h" #include "ui_protocolwidget.h" @@ -32,7 +34,106 @@ namespace OCC { -bool SortedTreeWidgetItem::operator<(const QTreeWidgetItem &other) const +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 *ProtocolItem::create(const QString &folder, const SyncFileItem &item) +{ + auto f = FolderMan::instance()->folder(folder); + if (!f) { + return 0; + } + + 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 << f->shortGuiLocalPath(); + + // If the error string is set, it's prefered because it is a useful user message. + QString message = item._errorString; + 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->setData(0, Qt::SizeHintRole, QSize(0, ActivityItemDelegate::rowHeight())); + twitem->setData(0, Qt::UserRole, timestamp); + twitem->setIcon(0, icon); + twitem->setToolTip(0, longTimeStr); + twitem->setToolTip(1, item._file); + twitem->setData(2, Qt::UserRole, folder); + twitem->setToolTip(3, message); + twitem->setData(3, Qt::UserRole, item._status); + return twitem; +} + +SyncJournalFileRecord ProtocolItem::syncJournalRecord(QTreeWidgetItem *item) +{ + SyncJournalFileRecord rec; + auto f = folder(item); + if (!f) + return rec; + f->journalDb()->getFileRecord(item->toolTip(1), &rec); + return rec; +} + +Folder *ProtocolItem::folder(QTreeWidgetItem *item) +{ + return FolderMan::instance()->folder(item->data(2, Qt::UserRole).toString()); +} + +void ProtocolItem::openContextMenu(QTreeWidgetItem *item, QWidget *parent) +{ + auto f = ProtocolItem::folder(item); + if (!f) + return; + auto rec = ProtocolItem::syncJournalRecord(item); + // rec might not be valid + + QObject *noParent = nullptr; + QAction openInBrowser(ProtocolWidget::tr("Open in browser"), noParent); + QObject::connect(&openInBrowser, &QAction::triggered, parent, [&]() { + fetchPrivateLinkUrl(f->accountState()->account(), rec._path, + rec.numericFileId(), parent, [parent](const QString &url) { + Utility::openBrowser(url, parent); + }); + }); + + QMenu menu; + if (rec.isValid()) + menu.addAction(&openInBrowser); + + menu.exec(QCursor::pos()); +} + +bool ProtocolItem::operator<(const QTreeWidgetItem &other) const { int column = treeWidget()->sortColumn(); if (column != 0) { @@ -56,6 +157,9 @@ ProtocolWidget::ProtocolWidget(QWidget *parent) connect(_ui->_treeWidget, &QTreeWidget::itemActivated, this, &ProtocolWidget::slotOpenFile); + _ui->_treeWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(_ui->_treeWidget, &QTreeWidget::customContextMenuRequested, this, &ProtocolWidget::slotItemContextMenu); + // Adjust copyToClipboard() when making changes here! QStringList header; header << tr("Time"); @@ -71,7 +175,7 @@ ProtocolWidget::ProtocolWidget(QWidget *parent) _ui->_treeWidget->setHeaderLabels(header); int timestampColumnWidth = - _ui->_treeWidget->fontMetrics().width(timeString(QDateTime::currentDateTime())) + _ui->_treeWidget->fontMetrics().width(ProtocolItem::timeString(QDateTime::currentDateTime())) + timestampColumnExtra; _ui->_treeWidget->setColumnWidth(0, timestampColumnWidth); _ui->_treeWidget->setColumnWidth(1, 180); @@ -119,14 +223,12 @@ void ProtocolWidget::hideEvent(QHideEvent *ev) QWidget::hideEvent(ev); } - -QString ProtocolWidget::timeString(QDateTime dt, QLocale::FormatType format) +void ProtocolWidget::slotItemContextMenu(const QPoint &pos) { - 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); + auto item = _ui->_treeWidget->itemAt(pos); + if (!item) + return; + ProtocolItem::openContextMenu(item, this); } void ProtocolWidget::slotOpenFile(QTreeWidgetItem *item, int) @@ -144,60 +246,11 @@ void ProtocolWidget::slotOpenFile(QTreeWidgetItem *item, int) } } -QTreeWidgetItem *ProtocolWidget::createCompletedTreewidgetItem(const QString &folder, const SyncFileItem &item) -{ - auto f = FolderMan::instance()->folder(folder); - if (!f) { - return 0; - } - - 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 << f->shortGuiLocalPath(); - - // If the error string is set, it's prefered because it is a useful user message. - QString message = item._errorString; - 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); - } - - QTreeWidgetItem *twitem = new SortedTreeWidgetItem(columns); - twitem->setData(0, Qt::SizeHintRole, QSize(0, ActivityItemDelegate::rowHeight())); - twitem->setData(0, Qt::UserRole, timestamp); - twitem->setIcon(0, icon); - twitem->setToolTip(0, longTimeStr); - twitem->setToolTip(1, item._file); - twitem->setData(2, Qt::UserRole, folder); - twitem->setToolTip(3, message); - twitem->setData(3, Qt::UserRole, item._status); - return twitem; -} - void ProtocolWidget::slotItemCompleted(const QString &folder, const SyncFileItemPtr &item) { if (item->hasErrorStatus()) return; - QTreeWidgetItem *line = createCompletedTreewidgetItem(folder, *item); + QTreeWidgetItem *line = ProtocolItem::create(folder, *item); if (line) { // Limit the number of items int itemCnt = _ui->_treeWidget->topLevelItemCount(); diff --git a/src/gui/protocolwidget.h b/src/gui/protocolwidget.h index 77bb641b464..878396ca6c2 100644 --- a/src/gui/protocolwidget.h +++ b/src/gui/protocolwidget.h @@ -35,16 +35,25 @@ namespace Ui { class Application; /** - * A QTreeWidgetItem with special sorting. + * The items used in the protocol and issue QTreeWidget * - * It allows items for global entries to be moved to the top if the + * Special sorting: It allows items for global entries to be moved to the top if the * sorting section is the "Time" column. */ -class SortedTreeWidgetItem : public QTreeWidgetItem +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); + + static SyncJournalFileRecord syncJournalRecord(QTreeWidgetItem *item); + static Folder *folder(QTreeWidgetItem *item); + + static void openContextMenu(QTreeWidgetItem *item, QWidget *parent); + private: bool operator<(const QTreeWidgetItem &other) const override; }; @@ -63,10 +72,6 @@ class ProtocolWidget : public QWidget void storeSyncActivity(QTextStream &ts); - // Shared with IssueWidget - static QTreeWidgetItem *createCompletedTreewidgetItem(const QString &folder, const SyncFileItem &item); - static QString timeString(QDateTime dt, QLocale::FormatType format = QLocale::NarrowFormat); - public slots: void slotItemCompleted(const QString &folder, const SyncFileItemPtr &item); void slotOpenFile(QTreeWidgetItem *item, int); @@ -75,6 +80,9 @@ public slots: void showEvent(QShowEvent *); void hideEvent(QHideEvent *); +private slots: + void slotItemContextMenu(const QPoint &pos); + signals: void copyToClipboard(); diff --git a/src/gui/socketapi.cpp b/src/gui/socketapi.cpp index 647d24e79e3..61ab4a66ad9 100644 --- a/src/gui/socketapi.cpp +++ b/src/gui/socketapi.cpp @@ -492,7 +492,7 @@ void SocketApi::command_SHARE_MENU_TITLE(const QString &, SocketListener *listen } // Fetches the private link url asynchronously and then calls the target slot -void fetchPrivateLinkUrl(const QString &localFile, SocketApi *target, void (SocketApi::*targetFun)(const QString &url) const) +static void fetchPrivateLinkUrlHelper(const QString &localFile, SocketApi *target, void (SocketApi::*targetFun)(const QString &url) const) { Folder *shareFolder = FolderMan::instance()->folderForPath(localFile); if (!shareFolder) { @@ -503,45 +503,25 @@ void fetchPrivateLinkUrl(const QString &localFile, SocketApi *target, void (Sock const QString localFileClean = QDir::cleanPath(localFile); const QString file = localFileClean.mid(shareFolder->cleanPath().length() + 1); + auto account = shareFolder->accountState()->account(); + // Generate private link ourselves: used as a fallback SyncJournalFileRecord rec; if (!shareFolder->journalDb()->getFileRecord(file, &rec) || !rec.isValid()) return; - const QString oldUrl = - shareFolder->accountState()->account()->deprecatedPrivateLinkUrl(rec.numericFileId()).toString(QUrl::FullyEncoded); - - // If the server doesn't have the property, use the old url directly. - if (!shareFolder->accountState()->account()->capabilities().privateLinkPropertyAvailable()) { - (target->*targetFun)(oldUrl); - return; - } - - // Retrieve the new link by PROPFIND - PropfindJob *job = new PropfindJob(shareFolder->accountState()->account(), file, target); - job->setProperties(QList() << "http://owncloud.org/ns:privatelink"); - job->setTimeout(10 * 1000); - QObject::connect(job, &PropfindJob::result, target, [=](const QVariantMap &result) { - auto privateLinkUrl = result["privatelink"].toString(); - if (!privateLinkUrl.isEmpty()) { - (target->*targetFun)(privateLinkUrl); - } else { - (target->*targetFun)(oldUrl); - } - }); - QObject::connect(job, &PropfindJob::finishedWithError, target, [=](QNetworkReply *) { - (target->*targetFun)(oldUrl); + fetchPrivateLinkUrl(account, file, rec.numericFileId(), target, [=](const QString &url) { + (target->*targetFun)(url); }); - job->start(); } void SocketApi::command_COPY_PRIVATE_LINK(const QString &localFile, SocketListener *) { - fetchPrivateLinkUrl(localFile, this, &SocketApi::copyPrivateLinkToClipboard); + fetchPrivateLinkUrlHelper(localFile, this, &SocketApi::copyPrivateLinkToClipboard); } void SocketApi::command_EMAIL_PRIVATE_LINK(const QString &localFile, SocketListener *) { - fetchPrivateLinkUrl(localFile, this, &SocketApi::emailPrivateLink); + fetchPrivateLinkUrlHelper(localFile, this, &SocketApi::emailPrivateLink); } void SocketApi::copyPrivateLinkToClipboard(const QString &link) const diff --git a/src/libsync/networkjobs.cpp b/src/libsync/networkjobs.cpp index 19041f1046d..310f0a55646 100644 --- a/src/libsync/networkjobs.cpp +++ b/src/libsync/networkjobs.cpp @@ -911,4 +911,36 @@ bool SimpleNetworkJob::finished() return true; } +void fetchPrivateLinkUrl(AccountPtr account, const QString &remotePath, + const QByteArray &numericFileId, QObject *target, + std::function targetFun) +{ + const QString oldUrl = account->deprecatedPrivateLinkUrl(numericFileId).toString(QUrl::FullyEncoded); + + // If the server doesn't have the property, use the old url directly. + if (!account->capabilities().privateLinkPropertyAvailable()) { + QTimer::singleShot(0, target, [=]() { + targetFun(oldUrl); + }); + return; + } + + // Retrieve the new link by PROPFIND + PropfindJob *job = new PropfindJob(account, remotePath, target); + job->setProperties(QList() << "http://owncloud.org/ns:privatelink"); + job->setTimeout(10 * 1000); + QObject::connect(job, &PropfindJob::result, target, [=](const QVariantMap &result) { + auto privateLinkUrl = result["privatelink"].toString(); + if (!privateLinkUrl.isEmpty()) { + targetFun(privateLinkUrl); + } else { + targetFun(oldUrl); + } + }); + QObject::connect(job, &PropfindJob::finishedWithError, target, [=](QNetworkReply *) { + targetFun(oldUrl); + }); + job->start(); +} + } // namespace OCC diff --git a/src/libsync/networkjobs.h b/src/libsync/networkjobs.h index 2965d5ae382..ebae65de915 100644 --- a/src/libsync/networkjobs.h +++ b/src/libsync/networkjobs.h @@ -18,6 +18,8 @@ #include "abstractnetworkjob.h" +#include + class QUrl; class QJsonObject; @@ -399,6 +401,22 @@ private slots: bool finished() Q_DECL_OVERRIDE; }; +/** + * @brief Runs a PROPFIND to figure out the private link url + * + * The numericFileId is used only to build the deprecatedPrivateLinkUrl + * locally as a fallback. If it's empty and the PROPFIND fails, targetFun + * will be called with an empty string. + * + * The job and signal connections are parented to the target QObject. + * + * Note: targetFun is guaranteed to be called only through the event + * loop and never directly. + */ +void fetchPrivateLinkUrl(AccountPtr account, const QString &remotePath, + const QByteArray &numericFileId, QObject *target, + std::function targetFun); + } // namespace OCC #endif // NETWORKJOBS_H