From 93b5d668754ce733dcd43fef590f0313a095c15e Mon Sep 17 00:00:00 2001 From: Martchus Date: Mon, 1 Apr 2024 18:57:18 +0200 Subject: [PATCH] Allow browsing remote/global files --- .../syncthingfileitemaction.cpp | 41 ++- syncthingconnector/syncthingconnection.h | 23 ++ .../syncthingconnection_requests.cpp | 83 ++++++ syncthingmodel/CMakeLists.txt | 2 + syncthingmodel/syncthingfilemodel.cpp | 280 ++++++++++++++++++ syncthingmodel/syncthingfilemodel.h | 54 ++++ syncthingmodel/syncthingicons.cpp | 4 + syncthingmodel/syncthingicons.h | 1 + syncthingmodel/syncthingmodel.cpp | 17 ++ syncthingmodel/syncthingmodel.h | 1 + syncthingwidgets/misc/otherdialogs.cpp | 51 ++++ syncthingwidgets/misc/otherdialogs.h | 5 +- tray/CMakeLists.txt | 1 + tray/gui/dirview.cpp | 7 + tray/gui/dirview.h | 1 + tray/gui/traywidget.cpp | 9 + tray/gui/traywidget.h | 1 + 17 files changed, 559 insertions(+), 22 deletions(-) create mode 100644 syncthingmodel/syncthingfilemodel.cpp create mode 100644 syncthingmodel/syncthingfilemodel.h diff --git a/fileitemactionplugin/syncthingfileitemaction.cpp b/fileitemactionplugin/syncthingfileitemaction.cpp index eb75f7a..1c1d36d 100644 --- a/fileitemactionplugin/syncthingfileitemaction.cpp +++ b/fileitemactionplugin/syncthingfileitemaction.cpp @@ -19,7 +19,6 @@ #include using namespace std; -using namespace Data; #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) K_PLUGIN_CLASS_WITH_JSON(SyncthingFileItemAction, "metadata.json"); @@ -28,13 +27,13 @@ K_PLUGIN_FACTORY(SyncthingFileItemActionFactory, registerPlugin SyncthingFileItemAction::actions(const KFileItemListProperties } struct DirStats { - explicit DirStats(const QList &dirs); + explicit DirStats(const QList &dirs); QStringList ids; bool anyPaused = false; bool allPaused = true; }; -DirStats::DirStats(const QList &dirs) +DirStats::DirStats(const QList &dirs) { ids.reserve(dirs.size()); - for (const SyncthingDir *const dir : dirs) { + for (const Data::SyncthingDir *const dir : dirs) { ids << dir->id; if (dir->paused) { anyPaused = true; @@ -124,11 +123,11 @@ QList SyncthingFileItemAction::createActions(const KFileItemListPrope } // determine relevant Syncthing dirs - QList detectedDirs; - QList containingDirs; + QList detectedDirs; + QList containingDirs; QList detectedItems; - const SyncthingDir *lastDir = nullptr; - for (const SyncthingDir &dir : dirs) { + const Data::SyncthingDir *lastDir = nullptr; + for (const Data::SyncthingDir &dir : dirs) { auto dirPath = QDir::cleanPath(dir.path); auto dirPathWithSlash = dirPath + QChar('/'); for (const QString &path : std::as_const(paths)) { @@ -177,7 +176,7 @@ QList SyncthingFileItemAction::createActions(const KFileItemListPrope actions << new QAction(QIcon::fromTheme(QStringLiteral("folder-sync")), detectedDirs.size() == 1 ? tr("Rescan \"%1\"").arg(detectedDirs.front()->displayName()) : tr("Rescan selected folders"), parent); if (connection.isConnected() && !detectedDirsStats.allPaused) { - for (const SyncthingDir *dir : std::as_const(detectedDirs)) { + for (const Data::SyncthingDir *dir : std::as_const(detectedDirs)) { connect(actions.back(), &QAction::triggered, bind(&SyncthingFileItemActionStaticData::rescanDir, &data, dir->id, QString())); containingDirs.removeAll(dir); } @@ -195,8 +194,8 @@ QList SyncthingFileItemAction::createActions(const KFileItemListPrope } if (connection.isConnected()) { connect(actions.back(), &QAction::triggered, - bind(detectedDirsStats.anyPaused ? &SyncthingConnection::resumeDirectories : &SyncthingConnection::pauseDirectories, &connection, - detectedDirsStats.ids)); + bind(detectedDirsStats.anyPaused ? &Data::SyncthingConnection::resumeDirectories : &Data::SyncthingConnection::pauseDirectories, + &connection, detectedDirsStats.ids)); } else { actions.back()->setEnabled(false); } @@ -208,7 +207,7 @@ QList SyncthingFileItemAction::createActions(const KFileItemListPrope actions << new QAction(QIcon::fromTheme(QStringLiteral("folder-sync")), containingDirs.size() == 1 ? tr("Rescan \"%1\"").arg(containingDirs.front()->displayName()) : tr("Rescan containing folders"), parent); if (connection.isConnected() && !containingDirsStats.allPaused) { - for (const SyncthingDir *dir : std::as_const(containingDirs)) { + for (const Data::SyncthingDir *dir : std::as_const(containingDirs)) { connect(actions.back(), &QAction::triggered, bind(&SyncthingFileItemActionStaticData::rescanDir, &data, dir->id, QString())); } } else { @@ -226,8 +225,8 @@ QList SyncthingFileItemAction::createActions(const KFileItemListPrope } if (connection.isConnected()) { connect(actions.back(), &QAction::triggered, - bind(containingDirsStats.anyPaused ? &SyncthingConnection::resumeDirectories : &SyncthingConnection::pauseDirectories, &connection, - containingDirsStats.ids)); + bind(containingDirsStats.anyPaused ? &Data::SyncthingConnection::resumeDirectories : &Data::SyncthingConnection::pauseDirectories, + &connection, containingDirsStats.ids)); } else { actions.back()->setEnabled(false); } @@ -236,10 +235,10 @@ QList SyncthingFileItemAction::createActions(const KFileItemListPrope // add actions to show further information about directory if the selection is only about one particular Syncthing dir if (lastDir && detectedDirs.size() + containingDirs.size() == 1) { auto *statusActions = new SyncthingDirActions(*lastDir, &data, parent); - connect(&connection, &SyncthingConnection::newDirs, statusActions, - static_cast &)>(&SyncthingDirActions::updateStatus)); - connect(&connection, &SyncthingConnection::dirStatusChanged, statusActions, - static_cast(&SyncthingDirActions::updateStatus)); + connect(&connection, &Data::SyncthingConnection::newDirs, statusActions, + static_cast &)>(&SyncthingDirActions::updateStatus)); + connect(&connection, &Data::SyncthingConnection::dirStatusChanged, statusActions, + static_cast(&SyncthingDirActions::updateStatus)); actions << *statusActions; } diff --git a/syncthingconnector/syncthingconnection.h b/syncthingconnector/syncthingconnection.h index 51bd6c9..f5aec99 100644 --- a/syncthingconnector/syncthingconnection.h +++ b/syncthingconnector/syncthingconnection.h @@ -53,6 +53,21 @@ struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingLogEntry { QString message; }; +enum class SyncthingItemType { Unknown, File, Directory }; + +struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingItem { + QString name; + CppUtilities::DateTime modificationTime; + std::size_t size = std::size_t(); + SyncthingItemType type = SyncthingItemType::Unknown; + std::vector children; + SyncthingItem *parent = nullptr; // not populated but might be set as needed (take care in case the pointer gets invalidated) + std::size_t index = std::size_t(); + int level = 0; // the level of nesting, does *not* include levels of the prefix + bool childrenPopulated = false; // populated depending on requested level + bool checked = false; // not populated but might be set to flag an item for some mass-action +}; + class LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingConnection : public QObject { friend ConnectionTests; friend MiscTests; @@ -240,6 +255,11 @@ public Q_SLOTS: void postConfigFromJsonObject(const QJsonObject &rawConfig); void postConfigFromByteArray(const QByteArray &rawConfig); +public: + // methods to GET or POST information from/to Syncthing (non-slots) + QMetaObject::Connection browse( + const QString &dirId, const QString &prefix, int level, std::function &&)> &&callback); + Q_SIGNALS: void newConfig(const QJsonObject &rawConfig); void newDirs(const std::vector &dirs); @@ -352,6 +372,9 @@ private Q_SLOTS: void recalculateStatus(); private: + // handler to evaluate results from request...() methods + void readBrowse(const QString &dirId, int levels, std::function &&)> &&callback); + // internal helper methods struct Reply { QNetworkReply *reply; diff --git a/syncthingconnector/syncthingconnection_requests.cpp b/syncthingconnector/syncthingconnection_requests.cpp index f914871..6a068ad 100644 --- a/syncthingconnector/syncthingconnection_requests.cpp +++ b/syncthingconnector/syncthingconnection_requests.cpp @@ -1582,6 +1582,89 @@ void SyncthingConnection::readRevert() } } +/*! + * \brief Lists items in the directory with the specified \a dirId down to \a levels (or fully if \a levels is 0) as of \a prefix. + * \sa https://docs.syncthing.net/rest/db-browse-get.html + * \remarks + * In contrast to other functions, this one uses a \a callback to return results (instead of a signal). This makes it easier to + * consume results of a specific request. Errors are still reported via the error() signal so there's no extra error handling + * required. Note that \a callback is *not* invoked in the error case. + */ +QMetaObject::Connection SyncthingConnection::browse( + const QString &dirId, const QString &prefix, int levels, std::function &&)> &&callback) +{ + auto query = QUrlQuery(); + query.addQueryItem(QStringLiteral("folder"), formatQueryItem(dirId)); + if (!prefix.isEmpty()) { + query.addQueryItem(QStringLiteral("prefix"), formatQueryItem(prefix)); + } + if (levels > 0) { + query.addQueryItem(QStringLiteral("levels"), QString::number(levels)); + } + return QObject::connect( + requestData(QStringLiteral("db/browse"), query), &QNetworkReply::finished, this, + [this, id = dirId, l = levels, cb = std::move(callback)]() mutable { readBrowse(id, l, std::move(cb)); }, Qt::QueuedConnection); +} + +/// \cond +static void readSyncthingItems(const QJsonArray &array, std::vector &into, int level, int levels) +{ + into.reserve(static_cast(array.size())); + for (const auto &jsonItem : array) { + if (!jsonItem.isObject()) { + continue; + } + const auto jsonItemObj = jsonItem.toObject(); + const auto type = jsonItemObj.value(QLatin1String("type")).toString(); + const auto index = into.size(); + const auto children = jsonItemObj.value(QLatin1String("children")); + auto &item = into.emplace_back(); + item.name = jsonItemObj.value(QLatin1String("name")).toString(); + item.modificationTime = CppUtilities::DateTime::fromIsoStringGmt(jsonItemObj.value(QLatin1String("modTime")).toString().toUtf8().data()); + item.size = static_cast(jsonItemObj.value(QLatin1String("size")).toInteger()); + item.index = index; + item.level = level; + if (type == QLatin1String("FILE_INFO_TYPE_FILE")) { + item.type = SyncthingItemType::File; + } else if (type == QLatin1String("FILE_INFO_TYPE_DIRECTORY")) { + item.type = SyncthingItemType::Directory; + } + readSyncthingItems(children.toArray(), item.children, level + 1, levels); + item.childrenPopulated = !levels || level < levels; + } +} +/// \endcond + +/*! + * \brief Reads the response of browse() and reports results via the specified \a callback or emits error() in case of an error. + */ +void SyncthingConnection::readBrowse(const QString &dirId, int levels, std::function &&)> &&callback) +{ + auto const [reply, response] = prepareReply(); + if (!reply) { + return; + } + auto items = std::vector(); + switch (reply->error()) { + case QNetworkReply::NoError: { + auto jsonError = QJsonParseError(); + const auto replyDoc = QJsonDocument::fromJson(response, &jsonError); + if (jsonError.error != QJsonParseError::NoError) { + emit error(tr("Unable to parse response for browsing \"%1\": ").arg(dirId) + jsonError.errorString(), SyncthingErrorCategory::Parsing, + QNetworkReply::NoError); + return; + } + readSyncthingItems(replyDoc.array(), items, 0, levels); + if (callback) { + callback(std::move(items)); + } + break; + } + default: + emitError(tr("Unable to browse \"%1\": ").arg(dirId), SyncthingErrorCategory::SpecificRequest, reply); + } +} + // post config /*! diff --git a/syncthingmodel/CMakeLists.txt b/syncthingmodel/CMakeLists.txt index 8ae0a3c..1076233 100644 --- a/syncthingmodel/CMakeLists.txt +++ b/syncthingmodel/CMakeLists.txt @@ -14,6 +14,7 @@ set(HEADER_FILES syncthingdirectorymodel.h syncthingdevicemodel.h syncthingdownloadmodel.h + syncthingfilemodel.h syncthingrecentchangesmodel.h syncthingsortfiltermodel.h syncthingstatuscomputionmodel.h @@ -25,6 +26,7 @@ set(SRC_FILES syncthingdirectorymodel.cpp syncthingdevicemodel.cpp syncthingdownloadmodel.cpp + syncthingfilemodel.cpp syncthingrecentchangesmodel.cpp syncthingsortfiltermodel.cpp syncthingstatuscomputionmodel.cpp diff --git a/syncthingmodel/syncthingfilemodel.cpp b/syncthingmodel/syncthingfilemodel.cpp new file mode 100644 index 0000000..8c02f9f --- /dev/null +++ b/syncthingmodel/syncthingfilemodel.cpp @@ -0,0 +1,280 @@ +#include "./syncthingfilemodel.h" +#include "./syncthingicons.h" + +#include +#include + +#include + +#include + +using namespace std; +using namespace CppUtilities; + +namespace Data { + +SyncthingFileModel::SyncthingFileModel(SyncthingConnection &connection, const QString &dirId, QObject *parent) + : SyncthingModel(connection, parent) + , m_connection(connection) + , m_dirId(dirId) +{ + m_connection.browse(m_dirId, QString(), 1, [this](std::vector &&items) { + const auto last = items.size() - 1; + beginInsertRows(QModelIndex(), 0, last < std::numeric_limits::max() ? static_cast(last) : std::numeric_limits::max()); + m_items = std::move(items); + endInsertRows(); + }); +} + +SyncthingFileModel::~SyncthingFileModel() +{ + QObject::disconnect(m_pendingRequest); +} + +QHash SyncthingFileModel::roleNames() const +{ + const static auto roles = QHash{ + { NameRole, "name" }, + { SizeRole, "size" }, + { ModificationTimeRole, "modificationTime" }, + { Actions, "actions" }, + { ActionNames, "actionNames" }, + { ActionIcons, "actionIcons" }, + }; + return roles; +} + +QModelIndex SyncthingFileModel::index(int row, int column, const QModelIndex &parent) const +{ + if (row < 0 || column < 0 || column > 2) { + return QModelIndex(); + } + + if (!parent.isValid()) { + if (static_cast(row) >= m_items.size()) { + return QModelIndex(); + } + return createIndex(row, column, &m_items[static_cast(row)]); + } + + auto *const parentItem = reinterpret_cast(parent.internalPointer()); + auto &items = parentItem->children; + if (static_cast(row) >= items.size()) { + return QModelIndex(); + } + auto &item = items[static_cast(row)]; + item.parent = parentItem; + return createIndex(row, column, &item); +} + +QString SyncthingFileModel::path(const QModelIndex &index) const +{ + auto res = QString(); + if (!index.isValid()) { + return res; + } + auto parts = QStringList(); + auto size = QString::size_type(); + parts.reserve(reinterpret_cast(index.internalPointer())->level + 1); + for (auto i = index; i.isValid(); i = i.parent()) { + size += parts.emplace_back(reinterpret_cast(i.internalPointer())->name).size(); + } + res.reserve(size + parts.size()); + for (auto i = parts.rbegin(), end = parts.rend(); i != end; ++i) { + res += *i; + res += QChar('/'); + } + return res; +} + +QModelIndex SyncthingFileModel::parent(const QModelIndex &child) const +{ + if (!child.isValid()) { + return QModelIndex(); + } + auto *const childItem = reinterpret_cast(child.internalPointer()); + if (!childItem->parent) { + return QModelIndex(); + } + return createIndex(static_cast(childItem->index), 0, childItem->parent); +} + +QVariant SyncthingFileModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + switch (orientation) { + case Qt::Horizontal: + switch (role) { + case Qt::DisplayRole: + switch (section) { + case 0: + return tr("Name"); + case 1: + return tr("Size"); + case 2: + return tr("Last modified"); + } + break; + default:; + } + break; + default:; + } + return QVariant(); +} + +QVariant SyncthingFileModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return QVariant(); + } + auto *const item = reinterpret_cast(index.internalPointer()); + switch (role) { + case Qt::DisplayRole: + switch (index.column()) { + case 0: + return item->name; + case 1: + return QString::fromStdString(CppUtilities::dataSizeToString(item->size)); + case 2: + return QString::fromStdString(item->modificationTime.toString()); + } + break; + case Qt::DecorationRole: { + const auto &icons = commonForkAwesomeIcons(); + switch (index.column()) { + case 0: + switch (item->type) { + case SyncthingItemType::File: + return icons.file; + case SyncthingItemType::Directory: + return icons.folder; + default: + return icons.cogs; + } + } + break; + } + case NameRole: + return item->name; + case SizeRole: + return static_cast(item->size); + case ModificationTimeRole: + return QString::fromStdString(item->modificationTime.toString()); + case Actions: + if (item->type == SyncthingItemType::Directory) { + return QStringList({ QStringLiteral("refresh") }); + } + break; + case ActionNames: + if (item->type == SyncthingItemType::Directory) { + return QStringList({ tr("Refresh") }); + } + break; + case ActionIcons: + if (item->type == SyncthingItemType::Directory) { + return QStringList({ QStringLiteral("view-refresh") }); + } + break; + } + return QVariant(); +} + +bool SyncthingFileModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + Q_UNUSED(index) + Q_UNUSED(value) + Q_UNUSED(role) + return false; +} + +int SyncthingFileModel::rowCount(const QModelIndex &parent) const +{ + auto res = std::size_t(); + if (!parent.isValid()) { + res = m_items.size(); + } else { + auto *const parentItem = reinterpret_cast(parent.internalPointer()); + res = parentItem->childrenPopulated || parentItem->type != SyncthingItemType::Directory ? parentItem->children.size() : 1; + } + return res < std::numeric_limits::max() ? static_cast(res) : std::numeric_limits::max(); +} + +int SyncthingFileModel::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return 3; +} + +bool SyncthingFileModel::canFetchMore(const QModelIndex &parent) const +{ + if (!parent.isValid()) { + return false; + } + auto *const parentItem = reinterpret_cast(parent.internalPointer()); + return !parentItem->childrenPopulated && parentItem->type == SyncthingItemType::Directory; +} + +/// \cond +static void addLevel(std::vector &items, int level) +{ + for (auto &item : items) { + item.level += level; + addLevel(item.children, level); + } +} +/// \endcond + +void SyncthingFileModel::fetchMore(const QModelIndex &parent) +{ + if (!parent.isValid()) { + return; + } + m_fetchQueue.append(parent); + if (m_fetchQueue.size() == 1) { + processFetchQueue(); + } +} + +void SyncthingFileModel::triggerAction(const QString &action, const QModelIndex &index) +{ + if (action == QLatin1String("refresh")) { + fetchMore(index); + } +} + +void SyncthingFileModel::handleConfigInvalidated() +{ +} + +void SyncthingFileModel::handleNewConfigAvailable() +{ +} + +void SyncthingFileModel::handleForkAwesomeIconsChanged() +{ + invalidateAllIndicies(QVector({ Qt::DecorationRole })); +} + +void SyncthingFileModel::processFetchQueue() +{ + if (m_fetchQueue.isEmpty()) { + return; + } + const auto &parent = m_fetchQueue.front(); + m_pendingRequest = m_connection.browse(m_dirId, path(parent), 1, [this, parent](std::vector &&items) { + auto *const parentItem = reinterpret_cast(parent.internalPointer()); + addLevel(items, parentItem->level); + beginRemoveRows(parent, 0, static_cast(parentItem->children.size() - 1)); + parentItem->children.clear(); + endRemoveRows(); + const auto last = items.size() - 1; + beginInsertRows(parent, 0, last < std::numeric_limits::max() ? static_cast(last) : std::numeric_limits::max()); + parentItem->children = std::move(items); + parentItem->childrenPopulated = true; + endInsertRows(); + m_fetchQueue.removeAll(parent); + processFetchQueue(); + }); +} + +} // namespace Data diff --git a/syncthingmodel/syncthingfilemodel.h b/syncthingmodel/syncthingfilemodel.h new file mode 100644 index 0000000..d1931cf --- /dev/null +++ b/syncthingmodel/syncthingfilemodel.h @@ -0,0 +1,54 @@ +#ifndef DATA_SYNCTHINGFILEMODEL_H +#define DATA_SYNCTHINGFILEMODEL_H + +#include "./syncthingmodel.h" + +#include + +namespace Data { + +struct SyncthingItem; + +class LIB_SYNCTHING_MODEL_EXPORT SyncthingFileModel : public SyncthingModel { + Q_OBJECT +public: + enum SyncthingFileModelRole { NameRole = SyncthingModelUserRole + 1, SizeRole, ModificationTimeRole, Actions, ActionNames, ActionIcons }; + + explicit SyncthingFileModel(SyncthingConnection &connection, const QString &dirId, QObject *parent = nullptr); + ~SyncthingFileModel() override; + +public Q_SLOTS: + QHash roleNames() const override; + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex &child) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role) override; + int rowCount(const QModelIndex &parent) const override; + int columnCount(const QModelIndex &parent) const override; + bool canFetchMore(const QModelIndex &parent) const override; + void fetchMore(const QModelIndex &parent) override; + void triggerAction(const QString &action, const QModelIndex &index); + +public: + QString path(const QModelIndex &path) const; + +private Q_SLOTS: + void handleConfigInvalidated() override; + void handleNewConfigAvailable() override; + void handleForkAwesomeIconsChanged() override; + +private: + void processFetchQueue(); + +private: + SyncthingConnection &m_connection; + QString m_dirId; + QModelIndexList m_fetchQueue; + QMetaObject::Connection m_pendingRequest; + mutable std::vector m_items; +}; + +} // namespace Data + +#endif // DATA_SYNCTHINGFILEMODEL_H diff --git a/syncthingmodel/syncthingicons.cpp b/syncthingmodel/syncthingicons.cpp index b2aab59..aa72e6b 100644 --- a/syncthingmodel/syncthingicons.cpp +++ b/syncthingmodel/syncthingicons.cpp @@ -304,6 +304,7 @@ ForkAwesomeIcons::ForkAwesomeIcons(QtForkAwesome::Renderer &renderer, const QCol , cogs(renderer.pixmap(QtForkAwesome::Icon::Cogs, size, color)) , link(renderer.pixmap(QtForkAwesome::Icon::Link, size, color)) , eye(renderer.pixmap(QtForkAwesome::Icon::Eye, size, color)) + , file(renderer.pixmap(QtForkAwesome::Icon::FileO, size, color)) , fileArchive(renderer.pixmap(QtForkAwesome::Icon::FileArchiveO, size, color)) , folder(renderer.pixmap(QtForkAwesome::Icon::Folder, size, color)) , certificate(renderer.pixmap(QtForkAwesome::Icon::Certificate, size, color)) @@ -366,7 +367,10 @@ QImage aboutDialogImage() void setForkAwesomeThemeOverrides() { auto &renderer = QtForkAwesome::Renderer::global(); + renderer.addThemeOverride(QtForkAwesome::Icon::File, QStringLiteral("text-plain")); renderer.addThemeOverride(QtForkAwesome::Icon::Folder, QStringLiteral("folder-symbolic")); + renderer.addThemeOverride(QtForkAwesome::Icon::FileO, QStringLiteral("text-plain")); + renderer.addThemeOverride(QtForkAwesome::Icon::FolderO, QStringLiteral("folder-symbolic")); renderer.addThemeOverride(QtForkAwesome::Icon::Sitemap, QStringLiteral("network-server-symbolic")); renderer.addThemeOverride(QtForkAwesome::Icon::Download, QStringLiteral("folder-download-symbolic")); renderer.addThemeOverride(QtForkAwesome::Icon::History, QStringLiteral("shallow-history")); diff --git a/syncthingmodel/syncthingicons.h b/syncthingmodel/syncthingicons.h index 1824d04..6ae14ea 100644 --- a/syncthingmodel/syncthingicons.h +++ b/syncthingmodel/syncthingicons.h @@ -138,6 +138,7 @@ struct LIB_SYNCTHING_MODEL_EXPORT ForkAwesomeIcons { QIcon cogs; QIcon link; QIcon eye; + QIcon file; QIcon fileArchive; QIcon folder; QIcon certificate; diff --git a/syncthingmodel/syncthingmodel.cpp b/syncthingmodel/syncthingmodel.cpp index 918b539..df463bd 100644 --- a/syncthingmodel/syncthingmodel.cpp +++ b/syncthingmodel/syncthingmodel.cpp @@ -40,6 +40,23 @@ void SyncthingModel::invalidateNestedIndicies(const QVector &affectedRoles) } } +void SyncthingModel::invalidateAllIndicies(const QVector &affectedRoles, const QModelIndex &parentIndex) +{ + const auto rows = rowCount(parentIndex); + const auto columns = columnCount(parentIndex); + if (rows <= 0 || columns <= 0) { + return; + } + const auto topLeftIndex = index(0, 0, parentIndex); + const auto bottomRightIndex = index(rows - 1, columns - 1, parentIndex); + emit dataChanged(topLeftIndex, bottomRightIndex, affectedRoles); + for (auto row = 0; row != rows; ++row) { + if (const auto idx = index(row, 0, parentIndex); idx.isValid()) { + invalidateAllIndicies(affectedRoles, idx); + } + } +} + void SyncthingModel::setBrightColors(bool brightColors) { if (m_brightColors == brightColors) { diff --git a/syncthingmodel/syncthingmodel.h b/syncthingmodel/syncthingmodel.h index 867f546..b39fec3 100644 --- a/syncthingmodel/syncthingmodel.h +++ b/syncthingmodel/syncthingmodel.h @@ -34,6 +34,7 @@ protected: virtual const QVector &colorRoles() const; void invalidateTopLevelIndicies(const QVector &affectedRoles); void invalidateNestedIndicies(const QVector &affectedRoles); + void invalidateAllIndicies(const QVector &affectedRoles, const QModelIndex &parentIndex = QModelIndex()); private Q_SLOTS: virtual void handleConfigInvalidated(); diff --git a/syncthingwidgets/misc/otherdialogs.cpp b/syncthingwidgets/misc/otherdialogs.cpp index ce20e35..18807f7 100644 --- a/syncthingwidgets/misc/otherdialogs.cpp +++ b/syncthingwidgets/misc/otherdialogs.cpp @@ -3,6 +3,8 @@ #include #include +#include + // use meta-data of syncthingtray application here #include "resources/../../tray/resources/config.h" @@ -12,8 +14,10 @@ #include #include #include +#include #include #include +#include #include using namespace std; @@ -74,4 +78,51 @@ QWidget *ownDeviceIdWidget(Data::SyncthingConnection &connection, int size, QWid setupOwnDeviceIdDialog(connection, size, widget); return widget; } + +QDialog *browseRemoteFilesDialog(Data::SyncthingConnection &connection, const Data::SyncthingDir &dir, QWidget *parent) +{ + auto dlg = new QDialog(parent); + dlg->setWindowTitle(QCoreApplication::translate("QtGui::OtherDialogs", "Remote/global tree of folder \"%1\"").arg(dir.displayName()) + + QStringLiteral(" - " APP_NAME)); + dlg->setWindowIcon(QIcon(QStringLiteral(":/icons/hicolor/scalable/app/syncthingtray.svg"))); + dlg->setAttribute(Qt::WA_DeleteOnClose); + + // setup model/view + auto model = new Data::SyncthingFileModel(connection, dir.id, &connection); + auto view = new QTreeView(dlg); + view->setModel(model); + view->setContextMenuPolicy(Qt::CustomContextMenu); + QObject::connect(view, &QTreeView::customContextMenuRequested, view, [view, model](const QPoint &pos) { + const auto index = view->indexAt(pos); + if (!index.isValid()) { + return; + } + const auto actions = model->data(index, SyncthingFileModel::Actions).toStringList(); + if (actions.isEmpty()) { + return; + } + const auto actionNames = model->data(index, SyncthingFileModel::ActionNames).toStringList(); + const auto actionIcons = model->data(index, SyncthingFileModel::ActionIcons).toStringList(); + auto menu = QMenu(view); + auto actionIndex = qsizetype(); + for (const auto &action : actions) { + QObject::connect(menu.addAction(actionIndex < actionIcons.size() ? QIcon::fromTheme(actionIcons.at(actionIndex)) : QIcon(), + actionIndex < actionNames.size() ? actionNames.at(actionIndex) : action), + &QAction::triggered, model, [model, action, index]() { model->triggerAction(action, index); }); + ++actionIndex; + } + menu.exec(pos); + }); + + // setup layout + auto layout = new QVBoxLayout; + layout->setAlignment(Qt::AlignCenter); + layout->setSpacing(0); + layout->setContentsMargins(QMargins()); + layout->addWidget(view); + dlg->setLayout(layout); + + return dlg; +} + } // namespace QtGui diff --git a/syncthingwidgets/misc/otherdialogs.h b/syncthingwidgets/misc/otherdialogs.h index 1031223..f351f4c 100644 --- a/syncthingwidgets/misc/otherdialogs.h +++ b/syncthingwidgets/misc/otherdialogs.h @@ -10,12 +10,15 @@ QT_FORWARD_DECLARE_CLASS(QWidget) namespace Data { class SyncthingConnection; -} +struct SyncthingDir; +} // namespace Data namespace QtGui { SYNCTHINGWIDGETS_EXPORT QDialog *ownDeviceIdDialog(Data::SyncthingConnection &connection); SYNCTHINGWIDGETS_EXPORT QWidget *ownDeviceIdWidget(Data::SyncthingConnection &connection, int size, QWidget *parent = nullptr); +SYNCTHINGWIDGETS_EXPORT QDialog *browseRemoteFilesDialog( + Data::SyncthingConnection &connection, const Data::SyncthingDir &dir, QWidget *parent = nullptr); } // namespace QtGui #endif // SYNCTHINGWIDGETS_OTHERDIALOGS_H diff --git a/tray/CMakeLists.txt b/tray/CMakeLists.txt index 8445110..0863e95 100644 --- a/tray/CMakeLists.txt +++ b/tray/CMakeLists.txt @@ -51,6 +51,7 @@ set(REQUIRED_ICONS dialog-ok dialog-ok-apply document-open + document-open-remote download edit-copy edit-clear diff --git a/tray/gui/dirview.cpp b/tray/gui/dirview.cpp index 3ff71b3..eae4f95 100644 --- a/tray/gui/dirview.cpp +++ b/tray/gui/dirview.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -102,6 +103,12 @@ void DirView::showContextMenu(const QPoint &position) connect(menu.addAction(QIcon::fromTheme(QStringLiteral("folder"), QIcon(QStringLiteral(":/icons/hicolor/scalable/places/folder-open.svg"))), tr("Open in file browser")), &QAction::triggered, triggerActionForSelectedRow(this, &DirView::openDir)); + if (Settings::values().enableWipFeatures) { + connect(menu.addAction(QIcon::fromTheme(QStringLiteral("document-open-remote"), + QIcon(QStringLiteral(":/icons/hicolor/scalable/places/document-open-remote.svg"))), + tr("Browse remote files")), + &QAction::triggered, triggerActionForSelectedRow(this, &DirView::browseRemoteFiles)); + } } showViewMenu(position, *this, menu); } diff --git a/tray/gui/dirview.h b/tray/gui/dirview.h index f613494..51d7325 100644 --- a/tray/gui/dirview.h +++ b/tray/gui/dirview.h @@ -23,6 +23,7 @@ Q_SIGNALS: void openDir(const Data::SyncthingDir &dir); void scanDir(const Data::SyncthingDir &dir); void pauseResumeDir(const Data::SyncthingDir &dir); + void browseRemoteFiles(const Data::SyncthingDir &dir); protected: void mouseReleaseEvent(QMouseEvent *event) override; diff --git a/tray/gui/traywidget.cpp b/tray/gui/traywidget.cpp index c1cff20..5ff6132 100644 --- a/tray/gui/traywidget.cpp +++ b/tray/gui/traywidget.cpp @@ -203,6 +203,7 @@ TrayWidget::TrayWidget(TrayMenu *parent) connect(m_ui->dirsTreeView, &DirView::scanDir, this, &TrayWidget::scanDir); connect(m_ui->dirsTreeView, &DirView::pauseResumeDir, this, &TrayWidget::pauseResumeDir); connect(m_ui->devsTreeView, &DevView::pauseResumeDev, this, &TrayWidget::pauseResumeDev); + connect(m_ui->dirsTreeView, &DirView::browseRemoteFiles, this, &TrayWidget::browseRemoteFiles); connect(m_ui->downloadsTreeView, &DownloadView::openDir, this, &TrayWidget::openDir); connect(m_ui->downloadsTreeView, &DownloadView::openItemDir, this, &TrayWidget::openItemDir); connect(m_ui->recentChangesTreeView, &QTreeView::customContextMenuRequested, this, &TrayWidget::showRecentChangesContextMenu); @@ -708,6 +709,14 @@ void TrayWidget::pauseResumeDir(const SyncthingDir &dir) } } +void TrayWidget::browseRemoteFiles(const Data::SyncthingDir &dir) +{ + auto *const dlg = browseRemoteFilesDialog(m_connection, dir, this); + dlg->resize(600, 500); + centerWidget(this); + dlg->show(); +} + void TrayWidget::showRecentChangesContextMenu(const QPoint &position) { const auto *const selectionModel = m_ui->recentChangesTreeView->selectionModel(); diff --git a/tray/gui/traywidget.h b/tray/gui/traywidget.h index 8db2fc8..6c6e9e3 100644 --- a/tray/gui/traywidget.h +++ b/tray/gui/traywidget.h @@ -95,6 +95,7 @@ private Q_SLOTS: void scanDir(const Data::SyncthingDir &dir); void pauseResumeDev(const Data::SyncthingDev &dev); void pauseResumeDir(const Data::SyncthingDir &dir); + void browseRemoteFiles(const Data::SyncthingDir &dir); void showRecentChangesContextMenu(const QPoint &position); void changeStatus(); void updateTraffic();