From fa050a422aeb3cdb4c8cd68366f96a195eb6acf9 Mon Sep 17 00:00:00 2001 From: Martchus Date: Sun, 16 Sep 2018 21:37:30 +0200 Subject: [PATCH] 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. --- model/entryfiltermodel.cpp | 36 ++++++++++++++++++++++ model/entryfiltermodel.h | 11 ++++++- model/entrymodel.h | 39 ++++++++++++------------ model/fieldmodel.h | 29 +++++++++--------- qml/EntriesPage.qml | 19 ++++++++++-- qml/main.qml | 61 ++++++++++++++++++++++++++++++-------- quickgui/controller.cpp | 43 +++++++++++++++++++++++++-- quickgui/controller.h | 39 +++++++++++++++++++++--- 8 files changed, 223 insertions(+), 54 deletions(-) diff --git a/model/entryfiltermodel.cpp b/model/entryfiltermodel.cpp index d09fac2..37d11f2 100644 --- a/model/entryfiltermodel.cpp +++ b/model/entryfiltermodel.cpp @@ -1,6 +1,8 @@ #include "./entryfiltermodel.h" #include "./entrymodel.h" +#include + 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(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 diff --git a/model/entryfiltermodel.h b/model/entryfiltermodel.h index ca25ce7..d8f7072 100644 --- a/model/entryfiltermodel.h +++ b/model/entryfiltermodel.h @@ -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 diff --git a/model/entrymodel.h b/model/entrymodel.h index 3182886..91f75a1 100644 --- a/model/entrymodel.h +++ b/model/entrymodel.h @@ -41,7 +41,7 @@ public: explicit EntryModel(QUndoStack *undoStack, QObject *parent = nullptr); #endif - QHash roleNames() const; + QHash 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 &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 itemData(const QModelIndex &index) const; - bool setData(const QModelIndex &index, const QVariant &value, int role); - bool setItemData(const QModelIndex &index, const QMap &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 itemData(const QModelIndex &index) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role) override; + bool setItemData(const QModelIndex &index, const QMap &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(); diff --git a/model/fieldmodel.h b/model/fieldmodel.h index ca6cdc9..ea1c440 100644 --- a/model/fieldmodel.h +++ b/model/fieldmodel.h @@ -53,25 +53,26 @@ public: explicit FieldModel(QUndoStack *undoStack, QObject *parent = nullptr); #endif - QHash roleNames() const; + QHash roleNames() const override; Io::AccountEntry *accountEntry(); const Io::AccountEntry *accountEntry() const; void setAccountEntry(Io::AccountEntry *entry); std::vector *fields(); PasswordVisibility passwordVisibility() const; - QVariant data(const QModelIndex &index, int role) const; - QMap 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 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: diff --git a/qml/EntriesPage.qml b/qml/EntriesPage.qml index a209dd0..3638cdd 100644 --- a/qml/EntriesPage.qml +++ b/qml/EntriesPage.qml @@ -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" } diff --git a/qml/main.qml b/qml/main.qml index 228e62c..b572ed9 100644 --- a/qml/main.qml +++ b/qml/main.qml @@ -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) diff --git a/quickgui/controller.cpp b/quickgui/controller.cpp index f0c1afd..881ec63 100644 --- a/quickgui/controller.cpp +++ b/quickgui/controller.cpp @@ -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 diff --git a/quickgui/controller.h b/quickgui/controller.h index a67e52e..446908d 100644 --- a/quickgui/controller.h +++ b/quickgui/controller.h @@ -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 &cutEntries() const; void setCutEntries(const QList &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(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(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