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).
|
||||
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.
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in New Issue