Allow selecting items in file browser

This is the first step to allowing mass actions like ignoring/unignoring
all selected items.
This commit is contained in:
Martchus 2024-05-18 23:54:46 +02:00
parent 1ca2eecbf1
commit aef925743e
5 changed files with 159 additions and 5 deletions

View File

@ -80,7 +80,7 @@ struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingItem {
/// \brief Whether children are populated (depends on the requested level). /// \brief Whether children are populated (depends on the requested level).
bool childrenPopulated = false; bool childrenPopulated = false;
/// \brief Whether the item is "checked"; not set by default but might be set to flag an item for some mass-action. /// \brief Whether the item is "checked"; not set by default but might be set to flag an item for some mass-action.
bool checked = false; Qt::CheckState checked = Qt::Unchecked;
/// \brief Whether the item is present in the Syncthing database. /// \brief Whether the item is present in the Syncthing database.
bool existsInDb = true; bool existsInDb = true;
/// \brief Whether the item is present in the local file system. /// \brief Whether the item is present in the local file system.

View File

@ -9,6 +9,7 @@
#include <c++utilities/conversion/stringconversion.h> #include <c++utilities/conversion/stringconversion.h>
#include <QAction>
#include <QClipboard> #include <QClipboard>
#include <QGuiApplication> #include <QGuiApplication>
#include <QNetworkReply> #include <QNetworkReply>
@ -186,6 +187,27 @@ QVariant SyncthingFileModel::headerData(int section, Qt::Orientation orientation
return QVariant(); return QVariant();
} }
Qt::ItemFlags SyncthingFileModel::flags(const QModelIndex &index) const
{
auto f = QAbstractItemModel::flags(index);
if (index.isValid()) {
const auto *const item = reinterpret_cast<SyncthingItem *>(index.internalPointer());
switch (item->type) {
case SyncthingItemType::File:
case SyncthingItemType::Symlink:
case SyncthingItemType::Error:
case SyncthingItemType::Loading:
f |= Qt::ItemNeverHasChildren;
break;
default:;
}
}
if (m_selectionMode) {
f |= Qt::ItemIsUserCheckable;
}
return f;
}
QVariant SyncthingFileModel::data(const QModelIndex &index, int role) const QVariant SyncthingFileModel::data(const QModelIndex &index, int role) const
{ {
if (!index.isValid()) { if (!index.isValid()) {
@ -216,6 +238,15 @@ QVariant SyncthingFileModel::data(const QModelIndex &index, int role) const
} }
} }
break; break;
case Qt::CheckStateRole:
if (!m_selectionMode) {
return QVariant();
}
switch (index.column()) {
case 0:
return QVariant(item->checked);
}
break;
case Qt::DecorationRole: { case Qt::DecorationRole: {
const auto &icons = commonForkAwesomeIcons(); const auto &icons = commonForkAwesomeIcons();
switch (index.column()) { switch (index.column()) {
@ -262,6 +293,7 @@ QVariant SyncthingFileModel::data(const QModelIndex &index, int role) const
if (item->type == SyncthingItemType::Directory) { if (item->type == SyncthingItemType::Directory) {
res << QStringLiteral("refresh"); res << QStringLiteral("refresh");
} }
res << QStringLiteral("toggle-selection");
if (!m_localPath.isEmpty() && item->isFilesystemItem()) { if (!m_localPath.isEmpty() && item->isFilesystemItem()) {
res << QStringLiteral("open") << QStringLiteral("copy-path"); res << QStringLiteral("open") << QStringLiteral("copy-path");
} }
@ -273,6 +305,7 @@ QVariant SyncthingFileModel::data(const QModelIndex &index, int role) const
if (item->type == SyncthingItemType::Directory) { if (item->type == SyncthingItemType::Directory) {
res << tr("Refresh"); res << tr("Refresh");
} }
res << (item->checked ? tr("Deselect") : tr("Select"));
if (!m_localPath.isEmpty() && item->isFilesystemItem()) { if (!m_localPath.isEmpty() && item->isFilesystemItem()) {
res << (item->type == SyncthingItemType::Directory ? tr("Browse locally") : tr("Open local version")) << tr("Copy local path"); res << (item->type == SyncthingItemType::Directory ? tr("Browse locally") : tr("Open local version")) << tr("Copy local path");
} }
@ -284,6 +317,7 @@ QVariant SyncthingFileModel::data(const QModelIndex &index, int role) const
if (item->type == SyncthingItemType::Directory) { if (item->type == SyncthingItemType::Directory) {
res << QIcon::fromTheme(QStringLiteral("view-refresh"), QIcon(QStringLiteral(":/icons/hicolor/scalable/actions/view-refresh.svg"))); res << QIcon::fromTheme(QStringLiteral("view-refresh"), QIcon(QStringLiteral(":/icons/hicolor/scalable/actions/view-refresh.svg")));
} }
res << QIcon::fromTheme(QStringLiteral("edit-select"));
if (!m_localPath.isEmpty() && item->isFilesystemItem()) { if (!m_localPath.isEmpty() && item->isFilesystemItem()) {
res << QIcon::fromTheme(QStringLiteral("folder"), QIcon(QStringLiteral(":/icons/hicolor/scalable/places/folder-open.svg"))); 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"))); res << QIcon::fromTheme(QStringLiteral("edit-copy"), QIcon(QStringLiteral(":/icons/hicolor/scalable/places/edit-copy.svg")));
@ -296,12 +330,72 @@ QVariant SyncthingFileModel::data(const QModelIndex &index, int role) const
bool SyncthingFileModel::setData(const QModelIndex &index, const QVariant &value, int role) bool SyncthingFileModel::setData(const QModelIndex &index, const QVariant &value, int role)
{ {
Q_UNUSED(index) if (!index.isValid()) {
Q_UNUSED(value) return false;
Q_UNUSED(role) }
switch (role) {
case Qt::CheckStateRole:
setCheckState(index, static_cast<Qt::CheckState>(value.toInt()));
return true;
}
return false; return false;
} }
/// \brief Sets the whether the children of the specified \a item are checked.
static void setChildrenChecked(SyncthingItem *item, Qt::CheckState checkState)
{
for (auto &childItem : item->children) {
setChildrenChecked(childItem.get(), childItem->checked = checkState);
}
}
/// \brief Sets the check state of the specified \a index updating child and parent indexes accordingly.
void SyncthingFileModel::setCheckState(const QModelIndex &index, Qt::CheckState checkState)
{
static const auto roles = QVector<int>{ Qt::CheckStateRole };
auto *const item = reinterpret_cast<SyncthingItem *>(index.internalPointer());
auto affectedParentIndex = index;
item->checked = checkState;
// set the checked state of child items as well
if (checkState != Qt::PartiallyChecked) {
setChildrenChecked(item, checkState);
}
// update the checked state of parent items accordingly
for (auto *parentItem = item->parent; parentItem; parentItem = parentItem->parent) {
auto hasUncheckedSiblings = false;
auto hasCheckedSiblings = false;
for (auto &siblingItem : parentItem->children) {
switch (siblingItem->checked) {
case Qt::Unchecked:
hasUncheckedSiblings = true;
break;
case Qt::PartiallyChecked:
hasUncheckedSiblings = hasCheckedSiblings = true;
break;
case Qt::Checked:
hasCheckedSiblings = true;
}
if (hasUncheckedSiblings && hasCheckedSiblings) {
break;
}
}
auto parentChecked = hasUncheckedSiblings && hasCheckedSiblings ? Qt::PartiallyChecked : (hasUncheckedSiblings ? Qt::Unchecked : Qt::Checked);
if (parentItem->checked == parentChecked) {
break;
}
parentItem->checked = parentChecked;
affectedParentIndex = createIndex(static_cast<int>(parentItem->index), 0, parentItem);
}
// emit dataChanged() events
if (m_selectionMode) {
emit dataChanged(affectedParentIndex, index, roles);
invalidateAllIndicies(roles, affectedParentIndex);
}
}
int SyncthingFileModel::rowCount(const QModelIndex &parent) const int SyncthingFileModel::rowCount(const QModelIndex &parent) const
{ {
auto res = std::size_t(); auto res = std::size_t();
@ -345,6 +439,11 @@ void SyncthingFileModel::triggerAction(const QString &action, const QModelIndex
{ {
if (action == QLatin1String("refresh")) { if (action == QLatin1String("refresh")) {
fetchMore(index); fetchMore(index);
return;
} else if (action == QLatin1String("toggle-selection")) {
auto *const item = static_cast<SyncthingItem *>(index.internalPointer());
setSelectionModeEnabled(true);
setData(index, item->checked != Qt::Checked ? Qt::Checked : Qt::Unchecked, Qt::CheckStateRole);
} }
if (m_localPath.isEmpty()) { if (m_localPath.isEmpty()) {
return; return;
@ -360,6 +459,30 @@ void SyncthingFileModel::triggerAction(const QString &action, const QModelIndex
} }
} }
QList<QAction *> SyncthingFileModel::selectionActions()
{
auto res = QList<QAction *>();
if (!m_selectionMode) {
return res;
}
auto *const discardAction = new QAction(tr("Discard selection"), this);
discardAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-undo")));
connect(discardAction, &QAction::triggered, this, [this] {
setSelectionModeEnabled(false);
setData(QModelIndex(), Qt::Unchecked, Qt::CheckStateRole);
});
res << discardAction;
return res;
}
void SyncthingFileModel::setSelectionModeEnabled(bool selectionModeEnabled)
{
if (m_selectionMode != selectionModeEnabled) {
m_selectionMode = selectionModeEnabled;
invalidateAllIndicies(QVector<int>{ Qt::CheckStateRole });
}
}
void SyncthingFileModel::handleConfigInvalidated() void SyncthingFileModel::handleConfigInvalidated()
{ {
} }
@ -431,6 +554,9 @@ void SyncthingFileModel::processFetchQueue(const QString &lastItemPath)
refreshedIndex, 0, last < std::numeric_limits<int>::max() ? static_cast<int>(last) : std::numeric_limits<int>::max()); refreshedIndex, 0, last < std::numeric_limits<int>::max() ? static_cast<int>(last) : std::numeric_limits<int>::max());
refreshedItem->children = std::move(items); refreshedItem->children = std::move(items);
refreshedItem->childrenPopulated = true; refreshedItem->childrenPopulated = true;
if (refreshedItem->checked == Qt::Checked) {
setChildrenChecked(refreshedItem, Qt::Checked);
}
endInsertRows(); endInsertRows();
} }
if (refreshedItem->children.size() != previousChildCount) { if (refreshedItem->children.size() != previousChildCount) {
@ -510,7 +636,7 @@ void SyncthingFileModel::handleLocalLookupFinished()
// mark items from the database query as locally existing if they do; mark items from local lookup as existing in the db if they do // mark items from the database query as locally existing if they do; mark items from local lookup as existing in the db if they do
auto &localItems = *res; auto &localItems = *res;
for (auto &child : refreshedItem->children) { for (auto &child : items) {
auto localItemIter = localItems.find(child->name); auto localItemIter = localItems.find(child->name);
if (localItemIter == localItems.end()) { if (localItemIter == localItems.end()) {
continue; continue;
@ -530,6 +656,9 @@ void SyncthingFileModel::handleLocalLookupFinished()
auto &item = items.emplace_back(std::make_unique<SyncthingItem>(std::move(localItem))); auto &item = items.emplace_back(std::make_unique<SyncthingItem>(std::move(localItem)));
item->parent = refreshedItem; item->parent = refreshedItem;
item->index = last; item->index = last;
if (refreshedItem->checked == Qt::Checked) {
setChildrenChecked(item.get(), item->checked = Qt::Checked);
}
populatePath(item->path = refreshedItem->path % QChar('/') % item->name, item->children); populatePath(item->path = refreshedItem->path % QChar('/') % item->name, item->children);
endInsertRows(); endInsertRows();
} }

View File

@ -11,10 +11,14 @@
#include <map> #include <map>
#include <memory> #include <memory>
QT_FORWARD_DECLARE_CLASS(QAction)
namespace Data { namespace Data {
class LIB_SYNCTHING_MODEL_EXPORT SyncthingFileModel : public SyncthingModel { class LIB_SYNCTHING_MODEL_EXPORT SyncthingFileModel : public SyncthingModel {
Q_OBJECT Q_OBJECT
Q_PROPERTY(bool selectionModeEnabled READ isSelectionModeEnabled WRITE setSelectionModeEnabled)
public: public:
enum SyncthingFileModelRole { enum SyncthingFileModelRole {
NameRole = SyncthingModelUserRole + 1, NameRole = SyncthingModelUserRole + 1,
@ -35,6 +39,7 @@ public Q_SLOTS:
QModelIndex index(const QString &path) const; QModelIndex index(const QString &path) const;
QModelIndex parent(const QModelIndex &child) const override; QModelIndex parent(const QModelIndex &child) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
QVariant data(const QModelIndex &index, 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; bool setData(const QModelIndex &index, const QVariant &value, int role) override;
int rowCount(const QModelIndex &parent) const override; int rowCount(const QModelIndex &parent) const override;
@ -42,6 +47,9 @@ public Q_SLOTS:
bool canFetchMore(const QModelIndex &parent) const override; bool canFetchMore(const QModelIndex &parent) const override;
void fetchMore(const QModelIndex &parent) override; void fetchMore(const QModelIndex &parent) override;
void triggerAction(const QString &action, const QModelIndex &index); void triggerAction(const QString &action, const QModelIndex &index);
QList<QAction *> selectionActions();
bool isSelectionModeEnabled() const;
void setSelectionModeEnabled(bool selectionModeEnabled);
public: public:
QString path(const QModelIndex &path) const; QString path(const QModelIndex &path) const;
@ -57,6 +65,7 @@ private Q_SLOTS:
void handleLocalLookupFinished(); void handleLocalLookupFinished();
private: private:
void setCheckState(const QModelIndex &index, Qt::CheckState checkState);
void processFetchQueue(const QString &lastItemPath = QString()); void processFetchQueue(const QString &lastItemPath = QString());
private: private:
@ -76,8 +85,14 @@ private:
QueryResult m_pendingRequest; QueryResult m_pendingRequest;
QFutureWatcher<LocalLookupRes> m_localItemLookup; QFutureWatcher<LocalLookupRes> m_localItemLookup;
std::unique_ptr<SyncthingItem> m_root; std::unique_ptr<SyncthingItem> m_root;
bool m_selectionMode;
}; };
inline bool SyncthingFileModel::isSelectionModeEnabled() const
{
return m_selectionMode;
}
} // namespace Data } // namespace Data
#endif // DATA_SYNCTHINGFILEMODEL_H #endif // DATA_SYNCTHINGFILEMODEL_H

View File

@ -71,6 +71,8 @@ set(REQUIRED_ICONS
internet-web-browser internet-web-browser
system-run system-run
edit-paste edit-paste
edit-select
edit-undo
list-remove list-remove
preferences-desktop-notification preferences-desktop-notification
preferences-system-startup preferences-system-startup

View File

@ -114,6 +114,14 @@ QDialog *browseRemoteFilesDialog(Data::SyncthingConnection &connection, const Da
&QAction::triggered, model, [model, action, index]() { model->triggerAction(action, index); }); &QAction::triggered, model, [model, action, index]() { model->triggerAction(action, index); });
++actionIndex; ++actionIndex;
} }
if (const auto selectionActions = model->selectionActions(); !selectionActions.isEmpty()) {
menu.addSeparator();
auto *const selectionMenu = menu.addMenu(QCoreApplication::translate("QtGui::OtherDialogs", "Selection"));
selectionMenu->addActions(selectionActions);
for (auto *const selectionAction : selectionActions) {
selectionAction->setParent(&menu);
}
}
menu.exec(view->viewport()->mapToGlobal(pos)); menu.exec(view->viewport()->mapToGlobal(pos));
}); });