From 83e1dd6d8a044c239e8cd4fc0710518c5124a80b Mon Sep 17 00:00:00 2001 From: Martchus Date: Sat, 4 May 2024 22:37:31 +0200 Subject: [PATCH] Improve file browser --- syncthingconnector/syncthingconnection.h | 3 +- .../syncthingconnection_requests.cpp | 2 + syncthingmodel/syncthingfilemodel.cpp | 110 ++++++++++++++++-- syncthingmodel/syncthingfilemodel.h | 12 +- syncthingwidgets/misc/otherdialogs.cpp | 4 +- 5 files changed, 114 insertions(+), 17 deletions(-) diff --git a/syncthingconnector/syncthingconnection.h b/syncthingconnector/syncthingconnection.h index ec6dfbc..75e32e9 100644 --- a/syncthingconnector/syncthingconnection.h +++ b/syncthingconnector/syncthingconnection.h @@ -49,7 +49,7 @@ struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingLogEntry { QString message; }; -enum class SyncthingItemType { Unknown, File, Directory }; +enum class SyncthingItemType { Unknown, File, Directory, Symlink }; struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingItem { QString name; @@ -58,6 +58,7 @@ struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingItem { 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) + QString path; // not populated but might be set as needed 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 diff --git a/syncthingconnector/syncthingconnection_requests.cpp b/syncthingconnector/syncthingconnection_requests.cpp index b3015e4..b5b3377 100644 --- a/syncthingconnector/syncthingconnection_requests.cpp +++ b/syncthingconnector/syncthingconnection_requests.cpp @@ -1635,6 +1635,8 @@ static void readSyncthingItems(const QJsonArray &array, std::vectortype = SyncthingItemType::File; } else if (type == QLatin1String("FILE_INFO_TYPE_DIRECTORY")) { item->type = SyncthingItemType::Directory; + } else if (type == QLatin1String("FILE_INFO_TYPE_SYMLINK")) { + item->type = SyncthingItemType::Symlink; } readSyncthingItems(children.toArray(), item->children, level + 1, levels); item->childrenPopulated = !levels || level < levels; diff --git a/syncthingmodel/syncthingfilemodel.cpp b/syncthingmodel/syncthingfilemodel.cpp index e9e6a26..fb65349 100644 --- a/syncthingmodel/syncthingfilemodel.cpp +++ b/syncthingmodel/syncthingfilemodel.cpp @@ -4,8 +4,12 @@ #include #include +#include + #include +#include +#include #include using namespace std; @@ -13,12 +17,30 @@ using namespace CppUtilities; namespace Data { +/// \cond +static void populatePath(const QString &root, std::vector> &items) +{ + if (root.isEmpty()) { + for (auto &item : items) { + populatePath(item->path = item->name, item->children); + } + } else { + for (auto &item : items) { + populatePath(item->path = root % QChar('/') % item->name, item->children); + } + } +} +/// \endcond + SyncthingFileModel::SyncthingFileModel(SyncthingConnection &connection, const SyncthingDir &dir, QObject *parent) : SyncthingModel(connection, parent) , m_connection(connection) , m_dirId(dir.id) , m_root(std::make_unique()) { + if (m_connection.isLocal()) { + m_localPath = dir.pathWithoutTrailingSlash().toString(); + } m_root->name = dir.displayName(); m_root->modificationTime = dir.lastFileTime; m_root->size = dir.globalStats.bytes; @@ -33,6 +55,7 @@ SyncthingFileModel::SyncthingFileModel(SyncthingConnection &connection, const Sy } const auto last = items.size() - 1; beginInsertRows(index(0, 0), 0, last < std::numeric_limits::max() ? static_cast(last) : std::numeric_limits::max()); + populatePath(QString(), items); m_root->children = std::move(items); m_root->childrenPopulated = true; endInsertRows(); @@ -50,6 +73,7 @@ QHash SyncthingFileModel::roleNames() const { NameRole, "name" }, { SizeRole, "size" }, { ModificationTimeRole, "modificationTime" }, + { PathRole, "path" }, { Actions, "actions" }, { ActionNames, "actionNames" }, { ActionIcons, "actionIcons" }, @@ -175,9 +199,22 @@ QVariant SyncthingFileModel::data(const QModelIndex &index, int role) const case 0: return item->name; case 1: - return QString::fromStdString(CppUtilities::dataSizeToString(item->size)); + switch (item->type) { + case SyncthingItemType::File: + return QString::fromStdString(CppUtilities::dataSizeToString(item->size)); + case SyncthingItemType::Directory: + return item->childrenPopulated ? tr("%1 elements").arg(item->children.size()) : QString(); + default: + return QString(); + } case 2: - return QString::fromStdString(item->modificationTime.toString()); + switch (item->type) { + case SyncthingItemType::File: + case SyncthingItemType::Directory: + return QString::fromStdString(item->modificationTime.toString()); + default: + return QString(); + } } break; case Qt::DecorationRole: { @@ -189,33 +226,64 @@ QVariant SyncthingFileModel::data(const QModelIndex &index, int role) const return icons.file; case SyncthingItemType::Directory: return icons.folder; + case SyncthingItemType::Symlink: + return icons.link; default: return icons.cogs; } } break; } + case Qt::ToolTipRole: + switch (index.column()) { + case 0: + return item->path; + case 2: + return agoString(item->modificationTime); + } + break; case NameRole: return item->name; case SizeRole: return static_cast(item->size); case ModificationTimeRole: return QString::fromStdString(item->modificationTime.toString()); - case Actions: + case PathRole: + return item->path; + case Actions: { + auto res = QStringList(); + res.reserve(3); if (item->type == SyncthingItemType::Directory) { - return QStringList({ QStringLiteral("refresh") }); + res << QStringLiteral("refresh"); } - break; - case ActionNames: + if (!m_localPath.isEmpty()) { + res << QStringLiteral("open") << QStringLiteral("copy-path"); + } + return res; + } + case ActionNames: { + auto res = QStringList(); + res.reserve(3); if (item->type == SyncthingItemType::Directory) { - return QStringList({ tr("Refresh") }); + res << tr("Refresh"); } - break; - case ActionIcons: + if (!m_localPath.isEmpty()) { + res << (item->type == SyncthingItemType::Directory ? tr("Browse locally") : tr("Open local version")) << tr("Copy local path"); + } + return res; + } + case ActionIcons: { + auto res = QVariantList(); + res.reserve(3); if (item->type == SyncthingItemType::Directory) { - return QStringList({ QStringLiteral("view-refresh") }); + res << QIcon::fromTheme(QStringLiteral("view-refresh"), QIcon(QStringLiteral(":/icons/hicolor/scalable/actions/view-refresh.svg"))); } - break; + if (!m_localPath.isEmpty()) { + res << QIcon::fromTheme(QStringLiteral("folder"), QIcon(QStringLiteral(":/icons/hicolor/scalable/places/folder-open.svg"))); + res << QIcon::fromTheme(QStringLiteral("edit-copy"), QIcon(QStringLiteral(":/icons/hicolor/scalable/places/edit-copy.svg"))); + } + return res; + } } return QVariant(); } @@ -281,6 +349,18 @@ void SyncthingFileModel::triggerAction(const QString &action, const QModelIndex if (action == QLatin1String("refresh")) { fetchMore(index); } + if (m_localPath.isEmpty()) { + return; + } + const auto relPath = index.data(PathRole).toString(); + const auto path = relPath.isEmpty() ? m_localPath : QString(m_localPath % QChar('/') % relPath); + if (action == QLatin1String("open")) { + QtUtilities::openLocalFileOrDir(path); + } else if (action == QLatin1String("copy-path")) { + if (auto *const clipboard = QGuiApplication::clipboard()) { + clipboard->setText(path); + } + } } void SyncthingFileModel::handleConfigInvalidated() @@ -313,7 +393,8 @@ void SyncthingFileModel::processFetchQueue() return; } auto *const refreshedItem = reinterpret_cast(refreshedIndex.internalPointer()); - if (!refreshedItem->children.empty()) { + const auto previousChildCount = refreshedItem->children.size(); + if (previousChildCount) { beginRemoveRows(refreshedIndex, 0, static_cast(refreshedItem->children.size() - 1)); refreshedItem->children.clear(); endRemoveRows(); @@ -324,11 +405,16 @@ void SyncthingFileModel::processFetchQueue() for (auto &item : items) { item->parent = refreshedItem; } + populatePath(refreshedItem->path, items); beginInsertRows(refreshedIndex, 0, last < std::numeric_limits::max() ? static_cast(last) : std::numeric_limits::max()); refreshedItem->children = std::move(items); refreshedItem->childrenPopulated = true; endInsertRows(); } + if (refreshedItem->children.size() != previousChildCount) { + const auto sizeIndex = refreshedIndex.siblingAtColumn(1); + emit dataChanged(sizeIndex, sizeIndex, QList{ Qt::DisplayRole }); + } processFetchQueue(); }); } diff --git a/syncthingmodel/syncthingfilemodel.h b/syncthingmodel/syncthingfilemodel.h index 4a4811a..c4bd2d1 100644 --- a/syncthingmodel/syncthingfilemodel.h +++ b/syncthingmodel/syncthingfilemodel.h @@ -6,14 +6,21 @@ #include #include -#include namespace Data { class LIB_SYNCTHING_MODEL_EXPORT SyncthingFileModel : public SyncthingModel { Q_OBJECT public: - enum SyncthingFileModelRole { NameRole = SyncthingModelUserRole + 1, SizeRole, ModificationTimeRole, Actions, ActionNames, ActionIcons }; + enum SyncthingFileModelRole { + NameRole = SyncthingModelUserRole + 1, + SizeRole, + ModificationTimeRole, + PathRole, + Actions, + ActionNames, + ActionIcons + }; explicit SyncthingFileModel(SyncthingConnection &connection, const SyncthingDir &dir, QObject *parent = nullptr); ~SyncthingFileModel() override; @@ -46,6 +53,7 @@ private: private: SyncthingConnection &m_connection; QString m_dirId; + QString m_localPath; QStringList m_fetchQueue; QMetaObject::Connection m_pendingRequest; std::unique_ptr m_root; diff --git a/syncthingwidgets/misc/otherdialogs.cpp b/syncthingwidgets/misc/otherdialogs.cpp index 1a0a8f1..e0dae09 100644 --- a/syncthingwidgets/misc/otherdialogs.cpp +++ b/syncthingwidgets/misc/otherdialogs.cpp @@ -102,11 +102,11 @@ QDialog *browseRemoteFilesDialog(Data::SyncthingConnection &connection, const Da return; } const auto actionNames = model->data(index, SyncthingFileModel::ActionNames).toStringList(); - const auto actionIcons = model->data(index, SyncthingFileModel::ActionIcons).toStringList(); + const auto actionIcons = model->data(index, SyncthingFileModel::ActionIcons).toList(); 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(), + QObject::connect(menu.addAction(actionIndex < actionIcons.size() ? actionIcons.at(actionIndex).value() : QIcon(), actionIndex < actionNames.size() ? actionNames.at(actionIndex) : action), &QAction::triggered, model, [model, action, index]() { model->triggerAction(action, index); }); ++actionIndex;