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:
parent
1ca2eecbf1
commit
aef925743e
|
@ -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.
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue