Quick GUI: Allow filtering entries

So far all operations on entries are disabled when
in filtered state and switching between normal and
filtered view invalidates the stack.
This commit is contained in:
Martchus 2018-09-16 21:37:30 +02:00
parent c3775775d1
commit fa050a422a
8 changed files with 223 additions and 54 deletions

View File

@ -1,6 +1,8 @@
#include "./entryfiltermodel.h"
#include "./entrymodel.h"
#include <cassert>
namespace QtGui {
/*!
@ -15,9 +17,43 @@ namespace QtGui {
*/
EntryFilterModel::EntryFilterModel(QObject *parent)
: QSortFilterProxyModel(parent)
, m_sourceModel(nullptr)
{
}
bool EntryFilterModel::isNode(const QModelIndex &parent) const
{
return m_sourceModel->isNode(mapToSource(parent));
}
void EntryFilterModel::setSourceModel(QAbstractItemModel *sourceModel)
{
if (!sourceModel) {
QSortFilterProxyModel::setSourceModel(sourceModel);
m_sourceModel = nullptr;
return;
}
auto *const entryModel = qobject_cast<EntryModel *>(sourceModel);
assert(entryModel);
QSortFilterProxyModel::setSourceModel(sourceModel);
m_sourceModel = entryModel;
}
void EntryFilterModel::setInsertTypeToNode()
{
if (m_sourceModel) {
m_sourceModel->setInsertTypeToNode();
}
}
void EntryFilterModel::setInsertTypeToAccount()
{
if (m_sourceModel) {
m_sourceModel->setInsertTypeToAccount();
}
}
bool EntryFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
// just use default implementation

View File

@ -5,17 +5,26 @@
namespace QtGui {
class EntryModel;
class EntryFilterModel : public QSortFilterProxyModel {
Q_OBJECT
public:
explicit EntryFilterModel(QObject *parent = nullptr);
void setSourceModel(QAbstractItemModel *sourceModel) override;
Q_INVOKABLE bool isNode(const QModelIndex &parent) const;
Q_INVOKABLE void setInsertTypeToNode();
Q_INVOKABLE void setInsertTypeToAccount();
protected:
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const;
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
private:
bool hasAcceptedChildren(const QModelIndex &index) const;
EntryModel *m_sourceModel;
};
} // namespace QtGui
#endif // ENTRYFILTERMODEL_H

View File

@ -41,7 +41,7 @@ public:
explicit EntryModel(QUndoStack *undoStack, QObject *parent = nullptr);
#endif
QHash<int, QByteArray> roleNames() const;
QHash<int, QByteArray> roleNames() const override;
Io::NodeEntry *rootEntry();
void setRootEntry(Io::NodeEntry *entry);
Q_INVOKABLE Io::Entry *entry(const QModelIndex &index);
@ -49,26 +49,27 @@ public:
Q_INVOKABLE bool insertEntries(int row, const QModelIndex &parent, const QList<Io::Entry *> &entries);
Io::EntryType insertType() const;
void setInsertType(Io::EntryType type);
QModelIndex index(int row, int column, const QModelIndex &parent) const;
QModelIndex index(int row, int column, const QModelIndex &parent) const override;
QModelIndex index(Io::Entry *entry) const;
QModelIndex parent(const QModelIndex &child) const;
bool hasChildren(const QModelIndex &parent) const;
QModelIndex parent(const QModelIndex &child) const override;
bool hasChildren(const QModelIndex &parent) const override;
Q_INVOKABLE bool isNode(const QModelIndex &parent) const;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
QMap<int, QVariant> itemData(const QModelIndex &index) const;
bool setData(const QModelIndex &index, const QVariant &value, int role);
bool setItemData(const QModelIndex &index, const QMap<int, QVariant> &roles);
Qt::ItemFlags flags(const QModelIndex &index) const;
QVariant headerData(int section, Qt::Orientation orientation, int role) const;
Q_INVOKABLE int rowCount(const QModelIndex &parent = QModelIndex()) const;
Q_INVOKABLE int columnCount(const QModelIndex &parent = QModelIndex()) const;
Q_INVOKABLE bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex());
Q_INVOKABLE bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex());
Q_INVOKABLE bool moveRows(const QModelIndex &sourceParent, int sourceRow, int count, const QModelIndex &destinationParent, int destinationChild);
QStringList mimeTypes() const;
QMimeData *mimeData(const QModelIndexList &indexes) const;
bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent);
Qt::DropActions supportedDropActions() const;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QMap<int, QVariant> itemData(const QModelIndex &index) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role) override;
bool setItemData(const QModelIndex &index, const QMap<int, QVariant> &roles) override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
Q_INVOKABLE int rowCount(const QModelIndex &parent = QModelIndex()) const override;
Q_INVOKABLE int columnCount(const QModelIndex &parent = QModelIndex()) const override;
Q_INVOKABLE bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex()) override;
Q_INVOKABLE bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override;
Q_INVOKABLE bool moveRows(
const QModelIndex &sourceParent, int sourceRow, int count, const QModelIndex &destinationParent, int destinationChild) override;
QStringList mimeTypes() const override;
QMimeData *mimeData(const QModelIndexList &indexes) const override;
bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override;
Qt::DropActions supportedDropActions() const override;
Q_INVOKABLE void setInsertTypeToNode();
Q_INVOKABLE void setInsertTypeToAccount();

View File

@ -53,25 +53,26 @@ public:
explicit FieldModel(QUndoStack *undoStack, QObject *parent = nullptr);
#endif
QHash<int, QByteArray> roleNames() const;
QHash<int, QByteArray> roleNames() const override;
Io::AccountEntry *accountEntry();
const Io::AccountEntry *accountEntry() const;
void setAccountEntry(Io::AccountEntry *entry);
std::vector<Io::Field> *fields();
PasswordVisibility passwordVisibility() const;
QVariant data(const QModelIndex &index, int role) const;
QMap<int, QVariant> itemData(const QModelIndex &index) const;
bool setData(const QModelIndex &index, const QVariant &value, int role);
Qt::ItemFlags flags(const QModelIndex &index) const;
QVariant headerData(int section, Qt::Orientation orientation, int role) const;
Q_INVOKABLE int rowCount(const QModelIndex &parent = QModelIndex()) const;
Q_INVOKABLE int columnCount(const QModelIndex &parent = QModelIndex()) const;
Q_INVOKABLE bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex());
Q_INVOKABLE bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex());
Q_INVOKABLE bool moveRows(const QModelIndex &sourceParent, int sourceRow, int count, const QModelIndex &destinationParent, int destinationChild);
bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent);
QStringList mimeTypes() const;
QMimeData *mimeData(const QModelIndexList &indices) const;
QVariant data(const QModelIndex &index, int role) const override;
QMap<int, QVariant> itemData(const QModelIndex &index) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role) override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
Q_INVOKABLE int rowCount(const QModelIndex &parent = QModelIndex()) const override;
Q_INVOKABLE int columnCount(const QModelIndex &parent = QModelIndex()) const override;
Q_INVOKABLE bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex()) override;
Q_INVOKABLE bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override;
Q_INVOKABLE bool moveRows(
const QModelIndex &sourceParent, int sourceRow, int count, const QModelIndex &destinationParent, int destinationChild) override;
bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override;
QStringList mimeTypes() const override;
QMimeData *mimeData(const QModelIndexList &indices) const override;
Q_INVOKABLE const Io::Field *field(std::size_t row) const;
public Q_SLOTS:

View File

@ -11,18 +11,24 @@ Kirigami.ScrollablePage {
property alias rootIndex: delegateModel.rootIndex
Layout.fillWidth: true
title: entryModel.data(rootIndex)
title: {
var currentEntryName = entryModel.data(rootIndex)
return currentEntryName ? currentEntryName : ""
}
actions {
main: Kirigami.Action {
iconName: "list-add"
text: qsTr("Add account")
visible: !nativeInterface.hasEntryFilter
enabled: !nativeInterface.hasEntryFilter
onTriggered: insertEntry("Account")
shortcut: "Ctrl+A"
}
left: Kirigami.Action {
iconName: "edit-paste"
text: qsTr("Paste account")
enabled: nativeInterface.canPaste
visible: !nativeInterface.hasEntryFilter
enabled: nativeInterface.canPaste && !nativeInterface.hasEntryFilter
onTriggered: {
var pastedEntries = nativeInterface.pasteEntries(rootIndex)
if (pastedEntries.length < 1) {
@ -38,6 +44,8 @@ Kirigami.ScrollablePage {
right: Kirigami.Action {
iconName: "folder-add"
text: qsTr("Add category")
visible: !nativeInterface.hasEntryFilter
enabled: !nativeInterface.hasEntryFilter
onTriggered: insertEntry("Node")
shortcut: "Ctrl+Shift+A"
}
@ -127,6 +135,7 @@ Kirigami.ScrollablePage {
Kirigami.ListItemDragHandle {
listItem: listItem
listView: entriesListView
enabled: !nativeInterface.hasEntryFilter
// FIXME: not sure why newIndex + 1 is required to be able to move a row at the end
onMoveRequested: entryModel.moveRows(
rootIndex, oldIndex, 1, rootIndex,
@ -167,6 +176,7 @@ Kirigami.ScrollablePage {
Controls.MenuItem {
icon.name: "edit-cut"
text: qsTr("Cut")
enabled: !nativeInterface.hasEntryFilter
onTriggered: {
nativeInterface.cutEntry(
entryModel.index(index, 0,
@ -177,12 +187,14 @@ Kirigami.ScrollablePage {
Controls.MenuItem {
icon.name: "edit-delete"
text: qsTr("Delete")
enabled: !nativeInterface.hasEntryFilter
onTriggered: confirmDeletionDialog.confirmDeletion(
model.name, index)
}
Controls.MenuItem {
icon.name: "edit-rename"
text: qsTr("Rename")
enabled: !nativeInterface.hasEntryFilter
onTriggered: renameDialog.renameEntry(model.name,
index)
}
@ -193,6 +205,7 @@ Kirigami.ScrollablePage {
Kirigami.Action {
iconName: "edit-cut"
text: qsTr("Cut")
enabled: !nativeInterface.hasEntryFilter
onTriggered: {
nativeInterface.cutEntry(entryModel.index(index, 0,
rootIndex))
@ -203,6 +216,7 @@ Kirigami.ScrollablePage {
Kirigami.Action {
iconName: "edit-delete"
text: qsTr("Delete")
enabled: !nativeInterface.hasEntryFilter
onTriggered: confirmDeletionDialog.confirmDeletion(
model.name, index)
shortcut: StandardKey.Delete
@ -210,6 +224,7 @@ Kirigami.ScrollablePage {
Kirigami.Action {
iconName: "edit-rename"
text: qsTr("Rename")
enabled: !nativeInterface.hasEntryFilter
onTriggered: renameDialog.renameEntry(model.name, index)
shortcut: "F2"
}

View File

@ -1,4 +1,5 @@
import QtQuick 2.7
import QtQuick.Templates 2.0 as T2
import QtQuick.Controls 2.1 as Controls
import QtQuick.Layouts 1.2
import QtQuick.Dialogs 1.3
@ -15,15 +16,6 @@ Kirigami.ApplicationWindow {
minimumHeight: 0
preferredHeight: Kirigami.Units.gridUnit * 2.3
maximumHeight: Kirigami.Units.gridUnit * 3
/*
Controls.TextField {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
placeholderText: qsTr("Filter")
width: Kirigami.Units.gridUnit * 8
}
*/
}
globalDrawer: Kirigami.GlobalDrawer {
id: leftMenu
@ -37,6 +29,35 @@ Kirigami.ApplicationWindow {
topContent: ColumnLayout {
Layout.fillWidth: true
Item {
Layout.preferredHeight: 4
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: filterTextField.implicitHeight
enabled: nativeInterface.fileOpen
Controls.TextField {
id: filterTextField
anchors.fill: parent
placeholderText: qsTr("Filter")
onTextChanged: nativeInterface.entryFilter = text
}
Kirigami.Icon {
source: "edit-clear"
anchors.right: parent.right
anchors.rightMargin: 6
anchors.verticalCenter: parent.verticalCenter
width: Kirigami.Units.iconSizes.small
height: Kirigami.Units.iconSizes.small
visible: filterTextField.text.length !== 0
MouseArea {
anchors.fill: parent
onClicked: filterTextField.text = ""
}
}
}
Controls.MenuSeparator {
padding: 0
topPadding: 8
@ -104,6 +125,7 @@ Kirigami.ApplicationWindow {
shortcut: StandardKey.Open
},
Kirigami.Action {
id: recentlyOpenedAction
text: qsTr("Recently opened ...")
iconName: "document-open-recent"
children: createRecentlyOpenedActions(
@ -210,9 +232,7 @@ Kirigami.ApplicationWindow {
nativeInterface.fileName))
return
}
var entryModel = nativeInterface.entryModel
var rootIndex = entryModel.index(0, 0)
pushStackEntry(entryModel, rootIndex)
initStack()
showPassiveNotification(qsTr("%1 opened").arg(
nativeInterface.fileName))
leftMenu.close()
@ -231,6 +251,11 @@ Kirigami.ApplicationWindow {
}
}
onEntryAboutToBeRemoved: {
// get the filter entry index
if (nativeInterface.hasEntryFilter) {
removedIndex = nativeInterface.filterEntryIndex(removedIndex)
}
// remove all possibly open stack pages of the removed entry and its children
for (var i = pageStack.depth - 1; i >= 0; --i) {
var stackPage = pageStack.get(i)
@ -243,6 +268,12 @@ Kirigami.ApplicationWindow {
}
}
}
onHasEntryFilterChanged: {
if (nativeInterface.fileOpen) {
pageStack.clear()
initStack()
}
}
}
Component {
@ -285,6 +316,12 @@ Kirigami.ApplicationWindow {
onActivated: leftMenu.visible = !leftMenu.visible
}
function initStack() {
var entryModel = nativeInterface.hasEntryFilter ? nativeInterface.entryFilterModel : nativeInterface.entryModel
var rootIndex = entryModel.index(0, 0)
pushStackEntry(entryModel, rootIndex)
}
function clearStack() {
pageStack.pop(lastEntriesPage = root.pageStack.initialPage,
Controls.StackView.Immediate)

View File

@ -309,15 +309,41 @@ void Controller::handleRecentFilesChanged()
m_settings.setValue(QStringLiteral("recententries"), m_recentFiles);
}
QStringList Controller::pasteEntries(const QModelIndex &destinationParent, int row)
QStringList Controller::pasteEntries(const QModelIndex &destinationParentMaybeFromFilterModel, int row)
{
if (m_cutEntries.isEmpty() || !m_entryModel.isNode(destinationParent)) {
// skip if no entries have been cut
if (m_cutEntries.isEmpty()) {
return QStringList();
}
// determine destinationParent and row in the source model
QModelIndex destinationParent;
if (destinationParentMaybeFromFilterModel.model() == &m_entryFilterModel) {
if (row < 0) {
row = m_entryFilterModel.rowCount(destinationParentMaybeFromFilterModel);
}
const auto destinationIndexInFilter = m_entryFilterModel.index(row, 0, destinationParentMaybeFromFilterModel);
if (destinationIndexInFilter.isValid()) {
const auto destinationIndex = m_entryFilterModel.mapToSource(destinationIndexInFilter);
destinationParent = destinationIndex.parent();
row = destinationIndex.row();
} else {
destinationParent = m_entryFilterModel.mapToSource(destinationParentMaybeFromFilterModel);
row = -1;
}
} else {
destinationParent = destinationParentMaybeFromFilterModel;
}
if (row < 0) {
row = m_entryModel.rowCount(destinationParent);
}
// skip if destination is no node
if (!m_entryModel.isNode(destinationParent)) {
return QStringList();
}
// move the entries
QStringList successfullyMovedEntries;
successfullyMovedEntries.reserve(m_cutEntries.size());
for (const QPersistentModelIndex &cutIndex : m_cutEntries) {
@ -401,4 +427,17 @@ void Controller::setUseNativeFileDialog(bool useNativeFileDialog)
m_settings.setValue(QStringLiteral("usenativefiledialog"), m_useNativeFileDialog);
}
void Controller::setEntryFilter(const QString &filter)
{
const auto previousFilter(m_entryFilterModel.filterRegExp().pattern());
if (filter == previousFilter) {
return;
}
m_entryFilterModel.setFilterRegExp(filter);
emit entryFilterChanged(filter);
if (previousFilter.isEmpty() != filter.isEmpty()) {
emit hasEntryFilterChanged(!filter.isEmpty());
}
}
} // namespace QtGui

View File

@ -34,6 +34,8 @@ class Controller : public QObject {
Q_PROPERTY(QStringList recentFiles READ recentFiles RESET clearRecentFiles NOTIFY recentFilesChanged)
Q_PROPERTY(bool useNativeFileDialog READ useNativeFileDialog WRITE setUseNativeFileDialog NOTIFY useNativeFileDialogChanged)
Q_PROPERTY(bool supportsNativeFileDialog READ supportsNativeFileDialog NOTIFY supportsNativeFileDialogChanged)
Q_PROPERTY(QString entryFilter READ entryFilter WRITE setEntryFilter NOTIFY entryFilterChanged)
Q_PROPERTY(bool hasEntryFilter READ hasEntryFilter NOTIFY hasEntryFilterChanged)
public:
explicit Controller(QSettings &settings, const QString &filePath = QString(), QObject *parent = nullptr);
@ -53,12 +55,12 @@ public:
Io::AccountEntry *currentAccount();
void setCurrentAccount(Io::AccountEntry *entry);
QModelIndex currentAccountIndex() const;
void setCurrentAccountIndex(const QModelIndex &accountIndex);
void setCurrentAccountIndex(const QModelIndex &accountIndexMaybeFromFilterModel);
bool hasCurrentAccount() const;
const QList<QPersistentModelIndex> &cutEntries() const;
void setCutEntries(const QList<QPersistentModelIndex> &cutEntries);
QString currentAccountName() const;
Q_INVOKABLE void cutEntry(const QModelIndex &entryIndex);
Q_INVOKABLE void cutEntry(const QModelIndex &entryIndexMaybeFromFilterModel);
Q_INVOKABLE QStringList pasteEntries(const QModelIndex &destinationParent, int row = -1);
Q_INVOKABLE bool copyToClipboard(const QString &text) const;
bool canPaste() const;
@ -67,6 +69,10 @@ public:
bool useNativeFileDialog() const;
void setUseNativeFileDialog(bool useNativeFileDialog);
bool supportsNativeFileDialog() const;
Q_INVOKABLE QModelIndex filterEntryIndex(const QModelIndex &entryIndex) const;
QString entryFilter() const;
void setEntryFilter(const QString &filter);
bool hasEntryFilter() const;
public slots:
void init();
@ -99,6 +105,8 @@ signals:
void useNativeFileDialogChanged(bool useNativeFileDialog);
void supportsNativeFileDialogChanged();
void entryAboutToBeRemoved(const QModelIndex &removedIndex);
void entryFilterChanged(const QString &newFilter);
void hasEntryFilterChanged(bool hasEntryFilter);
private slots:
void handleEntriesRemoved(const QModelIndex &parentIndex, int first, int last);
@ -109,6 +117,7 @@ private:
void updateWindowTitle();
void setFileOpen(bool fileOpen);
void emitIoError(const QString &when);
QModelIndex ensureSourceEntryIndex(const QModelIndex &entryIndexMaybeFromFilterModel) const;
QSettings &m_settings;
QString m_filePath;
@ -127,6 +136,12 @@ private:
bool m_useNativeFileDialog;
};
inline QModelIndex Controller::ensureSourceEntryIndex(const QModelIndex &entryIndexMaybeFromFilterModel) const
{
return entryIndexMaybeFromFilterModel.model() == &m_entryFilterModel ? m_entryFilterModel.mapToSource(entryIndexMaybeFromFilterModel)
: entryIndexMaybeFromFilterModel;
}
inline const QString &Controller::filePath() const
{
return m_filePath;
@ -193,8 +208,9 @@ inline QModelIndex Controller::currentAccountIndex() const
return m_fieldModel.accountEntry() ? m_entryModel.index(const_cast<Io::AccountEntry *>(m_fieldModel.accountEntry())) : QModelIndex();
}
inline void Controller::setCurrentAccountIndex(const QModelIndex &accountIndex)
inline void Controller::setCurrentAccountIndex(const QModelIndex &accountIndexMaybeFromFilterModel)
{
const auto accountIndex = ensureSourceEntryIndex(accountIndexMaybeFromFilterModel);
m_fieldModel.setAccountEntry(m_entryModel.isNode(accountIndex) ? nullptr : static_cast<Io::AccountEntry *>(m_entryModel.entry(accountIndex)));
emit currentAccountChanged();
}
@ -221,7 +237,7 @@ inline QString Controller::currentAccountName() const
inline void Controller::cutEntry(const QModelIndex &entryIndex)
{
cutEntriesChanged(m_cutEntries << QPersistentModelIndex(entryIndex));
cutEntriesChanged(m_cutEntries << QPersistentModelIndex(ensureSourceEntryIndex(entryIndex)));
}
inline bool Controller::canPaste() const
@ -248,6 +264,21 @@ inline bool Controller::supportsNativeFileDialog() const
#endif
}
inline QModelIndex Controller::filterEntryIndex(const QModelIndex &entryIndex) const
{
return m_entryFilterModel.mapFromSource(entryIndex);
}
inline QString Controller::entryFilter() const
{
return m_entryFilterModel.filterRegExp().pattern();
}
inline bool Controller::hasEntryFilter() const
{
return !m_entryFilterModel.filterRegExp().isEmpty();
}
} // namespace QtGui
#endif // QT_QUICK_GUI_CONTROLLER_H