Allow browsing remote/global files

This commit is contained in:
Martchus 2024-04-01 18:57:18 +02:00
parent 1b9450fc62
commit 93b5d66875
17 changed files with 559 additions and 22 deletions

View File

@ -19,7 +19,6 @@
#include <utility>
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<SyncthingFileIte
#endif
struct SyncthingItem {
SyncthingItem(const SyncthingDir *dir, const QString &path);
const SyncthingDir *dir;
SyncthingItem(const Data::SyncthingDir *dir, const QString &path);
const Data::SyncthingDir *dir;
QString path;
QString name;
};
SyncthingItem::SyncthingItem(const SyncthingDir *dir, const QString &path)
SyncthingItem::SyncthingItem(const Data::SyncthingDir *dir, const QString &path)
: dir(dir)
, path(path)
{
@ -78,17 +77,17 @@ QList<QAction *> SyncthingFileItemAction::actions(const KFileItemListProperties
}
struct DirStats {
explicit DirStats(const QList<const SyncthingDir *> &dirs);
explicit DirStats(const QList<const Data::SyncthingDir *> &dirs);
QStringList ids;
bool anyPaused = false;
bool allPaused = true;
};
DirStats::DirStats(const QList<const SyncthingDir *> &dirs)
DirStats::DirStats(const QList<const Data::SyncthingDir *> &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<QAction *> SyncthingFileItemAction::createActions(const KFileItemListPrope
}
// determine relevant Syncthing dirs
QList<const SyncthingDir *> detectedDirs;
QList<const SyncthingDir *> containingDirs;
QList<const Data::SyncthingDir *> detectedDirs;
QList<const Data::SyncthingDir *> containingDirs;
QList<SyncthingItem> 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<QAction *> 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<QAction *> 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<QAction *> 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<QAction *> 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<QAction *> 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<void (SyncthingDirActions::*)(const vector<SyncthingDir> &)>(&SyncthingDirActions::updateStatus));
connect(&connection, &SyncthingConnection::dirStatusChanged, statusActions,
static_cast<bool (SyncthingDirActions::*)(const SyncthingDir &)>(&SyncthingDirActions::updateStatus));
connect(&connection, &Data::SyncthingConnection::newDirs, statusActions,
static_cast<void (SyncthingDirActions::*)(const std::vector<Data::SyncthingDir> &)>(&SyncthingDirActions::updateStatus));
connect(&connection, &Data::SyncthingConnection::dirStatusChanged, statusActions,
static_cast<bool (SyncthingDirActions::*)(const Data::SyncthingDir &)>(&SyncthingDirActions::updateStatus));
actions << *statusActions;
}

View File

@ -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<SyncthingItem> 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<void(std::vector<SyncthingItem> &&)> &&callback);
Q_SIGNALS:
void newConfig(const QJsonObject &rawConfig);
void newDirs(const std::vector<SyncthingDir> &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<void(std::vector<SyncthingItem> &&)> &&callback);
// internal helper methods
struct Reply {
QNetworkReply *reply;

View File

@ -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<void(std::vector<SyncthingItem> &&)> &&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<SyncthingItem> &into, int level, int levels)
{
into.reserve(static_cast<std::size_t>(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<std::size_t>(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<void(std::vector<SyncthingItem> &&)> &&callback)
{
auto const [reply, response] = prepareReply();
if (!reply) {
return;
}
auto items = std::vector<SyncthingItem>();
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
/*!

View File

@ -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

View File

@ -0,0 +1,280 @@
#include "./syncthingfilemodel.h"
#include "./syncthingicons.h"
#include <syncthingconnector/syncthingconnection.h>
#include <syncthingconnector/utils.h>
#include <c++utilities/conversion/stringconversion.h>
#include <QStringBuilder>
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<SyncthingItem> &&items) {
const auto last = items.size() - 1;
beginInsertRows(QModelIndex(), 0, last < std::numeric_limits<int>::max() ? static_cast<int>(last) : std::numeric_limits<int>::max());
m_items = std::move(items);
endInsertRows();
});
}
SyncthingFileModel::~SyncthingFileModel()
{
QObject::disconnect(m_pendingRequest);
}
QHash<int, QByteArray> SyncthingFileModel::roleNames() const
{
const static auto roles = QHash<int, QByteArray>{
{ 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<std::size_t>(row) >= m_items.size()) {
return QModelIndex();
}
return createIndex(row, column, &m_items[static_cast<std::size_t>(row)]);
}
auto *const parentItem = reinterpret_cast<SyncthingItem *>(parent.internalPointer());
auto &items = parentItem->children;
if (static_cast<std::size_t>(row) >= items.size()) {
return QModelIndex();
}
auto &item = items[static_cast<std::size_t>(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<SyncthingItem *>(index.internalPointer())->level + 1);
for (auto i = index; i.isValid(); i = i.parent()) {
size += parts.emplace_back(reinterpret_cast<SyncthingItem *>(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<SyncthingItem *>(child.internalPointer());
if (!childItem->parent) {
return QModelIndex();
}
return createIndex(static_cast<int>(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<SyncthingItem *>(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<qsizetype>(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<SyncthingItem *>(parent.internalPointer());
res = parentItem->childrenPopulated || parentItem->type != SyncthingItemType::Directory ? parentItem->children.size() : 1;
}
return res < std::numeric_limits<int>::max() ? static_cast<int>(res) : std::numeric_limits<int>::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<SyncthingItem *>(parent.internalPointer());
return !parentItem->childrenPopulated && parentItem->type == SyncthingItemType::Directory;
}
/// \cond
static void addLevel(std::vector<SyncthingItem> &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<int>({ 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<SyncthingItem> &&items) {
auto *const parentItem = reinterpret_cast<SyncthingItem *>(parent.internalPointer());
addLevel(items, parentItem->level);
beginRemoveRows(parent, 0, static_cast<int>(parentItem->children.size() - 1));
parentItem->children.clear();
endRemoveRows();
const auto last = items.size() - 1;
beginInsertRows(parent, 0, last < std::numeric_limits<int>::max() ? static_cast<int>(last) : std::numeric_limits<int>::max());
parentItem->children = std::move(items);
parentItem->childrenPopulated = true;
endInsertRows();
m_fetchQueue.removeAll(parent);
processFetchQueue();
});
}
} // namespace Data

View File

@ -0,0 +1,54 @@
#ifndef DATA_SYNCTHINGFILEMODEL_H
#define DATA_SYNCTHINGFILEMODEL_H
#include "./syncthingmodel.h"
#include <vector>
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<int, QByteArray> 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<SyncthingItem> m_items;
};
} // namespace Data
#endif // DATA_SYNCTHINGFILEMODEL_H

View File

@ -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"));

View File

@ -138,6 +138,7 @@ struct LIB_SYNCTHING_MODEL_EXPORT ForkAwesomeIcons {
QIcon cogs;
QIcon link;
QIcon eye;
QIcon file;
QIcon fileArchive;
QIcon folder;
QIcon certificate;

View File

@ -40,6 +40,23 @@ void SyncthingModel::invalidateNestedIndicies(const QVector<int> &affectedRoles)
}
}
void SyncthingModel::invalidateAllIndicies(const QVector<int> &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) {

View File

@ -34,6 +34,7 @@ protected:
virtual const QVector<int> &colorRoles() const;
void invalidateTopLevelIndicies(const QVector<int> &affectedRoles);
void invalidateNestedIndicies(const QVector<int> &affectedRoles);
void invalidateAllIndicies(const QVector<int> &affectedRoles, const QModelIndex &parentIndex = QModelIndex());
private Q_SLOTS:
virtual void handleConfigInvalidated();

View File

@ -3,6 +3,8 @@
#include <syncthingconnector/syncthingconnection.h>
#include <syncthingconnector/syncthingdir.h>
#include <syncthingmodel/syncthingfilemodel.h>
// use meta-data of syncthingtray application here
#include "resources/../../tray/resources/config.h"
@ -12,8 +14,10 @@
#include <QGuiApplication>
#include <QIcon>
#include <QLabel>
#include <QMenu>
#include <QPixmap>
#include <QPushButton>
#include <QTreeView>
#include <QVBoxLayout>
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

View File

@ -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

View File

@ -51,6 +51,7 @@ set(REQUIRED_ICONS
dialog-ok
dialog-ok-apply
document-open
document-open-remote
download
edit-copy
edit-clear

View File

@ -6,6 +6,7 @@
#include <syncthingmodel/syncthingdirectorymodel.h>
#include <syncthingmodel/syncthingsortfiltermodel.h>
#include <syncthingwidgets/misc/direrrorsdialog.h>
#include <syncthingwidgets/settings/settings.h>
#include <QClipboard>
#include <QGuiApplication>
@ -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);
}

View File

@ -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;

View File

@ -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();

View File

@ -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();