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).
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.
bool checked = false;
Qt::CheckState checked = Qt::Unchecked;
/// \brief Whether the item is present in the Syncthing database.
bool existsInDb = true;
/// \brief Whether the item is present in the local file system.

View File

@ -9,6 +9,7 @@
#include <c++utilities/conversion/stringconversion.h>
#include <QAction>
#include <QClipboard>
#include <QGuiApplication>
#include <QNetworkReply>
@ -186,6 +187,27 @@ QVariant SyncthingFileModel::headerData(int section, Qt::Orientation orientation
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
{
if (!index.isValid()) {
@ -216,6 +238,15 @@ QVariant SyncthingFileModel::data(const QModelIndex &index, int role) const
}
}
break;
case Qt::CheckStateRole:
if (!m_selectionMode) {
return QVariant();
}
switch (index.column()) {
case 0:
return QVariant(item->checked);
}
break;
case Qt::DecorationRole: {
const auto &icons = commonForkAwesomeIcons();
switch (index.column()) {
@ -262,6 +293,7 @@ QVariant SyncthingFileModel::data(const QModelIndex &index, int role) const
if (item->type == SyncthingItemType::Directory) {
res << QStringLiteral("refresh");
}
res << QStringLiteral("toggle-selection");
if (!m_localPath.isEmpty() && item->isFilesystemItem()) {
res << QStringLiteral("open") << QStringLiteral("copy-path");
}
@ -273,6 +305,7 @@ QVariant SyncthingFileModel::data(const QModelIndex &index, int role) const
if (item->type == SyncthingItemType::Directory) {
res << tr("Refresh");
}
res << (item->checked ? tr("Deselect") : tr("Select"));
if (!m_localPath.isEmpty() && item->isFilesystemItem()) {
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) {
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()) {
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")));
@ -296,12 +330,72 @@ QVariant SyncthingFileModel::data(const QModelIndex &index, int role) const
bool SyncthingFileModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
Q_UNUSED(index)
Q_UNUSED(value)
Q_UNUSED(role)
if (!index.isValid()) {
return false;
}
switch (role) {
case Qt::CheckStateRole:
setCheckState(index, static_cast<Qt::CheckState>(value.toInt()));
return true;
}
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
{
auto res = std::size_t();
@ -345,6 +439,11 @@ void SyncthingFileModel::triggerAction(const QString &action, const QModelIndex
{
if (action == QLatin1String("refresh")) {
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()) {
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()
{
}
@ -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());
refreshedItem->children = std::move(items);
refreshedItem->childrenPopulated = true;
if (refreshedItem->checked == Qt::Checked) {
setChildrenChecked(refreshedItem, Qt::Checked);
}
endInsertRows();
}
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
auto &localItems = *res;
for (auto &child : refreshedItem->children) {
for (auto &child : items) {
auto localItemIter = localItems.find(child->name);
if (localItemIter == localItems.end()) {
continue;
@ -530,6 +656,9 @@ void SyncthingFileModel::handleLocalLookupFinished()
auto &item = items.emplace_back(std::make_unique<SyncthingItem>(std::move(localItem)));
item->parent = refreshedItem;
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);
endInsertRows();
}

View File

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

View File

@ -71,6 +71,8 @@ set(REQUIRED_ICONS
internet-web-browser
system-run
edit-paste
edit-select
edit-undo
list-remove
preferences-desktop-notification
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); });
++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));
});