From 6972256727d6c9b2c8418f5b05b1003f5517a3d5 Mon Sep 17 00:00:00 2001 From: Martchus Date: Sun, 17 Mar 2024 23:06:06 +0100 Subject: [PATCH] Improve Qt Quick GUI after porting to Qt 6 * Avoid use of deprecated QML features * Fix problems when moving items by making the delegates own items * Improve code in models, especially functions for moving rows * Fix sizing of dialogs * Replace code relying on `bannerClicked` event which has been removed in KF6 * Use the native file dialog if supported (and otherwise not); this should give the desired behavior under each platform out of the box * Update versioning requirements in README --- README.md | 2 +- gui/undocommands.cpp | 6 +- model/entrymodel.cpp | 30 ++++-- model/fieldmodel.cpp | 27 +++--- qml/AboutDialog.qml | 3 +- qml/BasicDialog.qml | 4 +- qml/EntriesPage.qml | 129 +++----------------------- qml/EntryDelegate.qml | 116 +++++++++++++++++++++++ qml/FieldsDelegate.qml | 127 +++++++++++++++++++++++++ qml/FieldsPage.qml | 185 ++++++------------------------------- qml/PasswordDialog.qml | 11 ++- qml/main.qml | 51 +++++----- quickgui/controller.cpp | 2 +- quickgui/initiatequick.cpp | 2 +- resources/qml.qrc | 2 + 15 files changed, 358 insertions(+), 339 deletions(-) create mode 100644 qml/EntryDelegate.qml create mode 100644 qml/FieldsDelegate.qml diff --git a/README.md b/README.md index 17514f9..bad63bc 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ can be passed to CMake to influence the build. ### Optional dependencies * When building any Qt GUI, the library qtutilities is required. * When building with Qt Widgets GUI support, the following Qt modules are required (version 5.6 or higher): core gui widgets -* When building with support for the experimental Qt Quick GUI, the following Qt/KDE modules are required (version 5.12 or higher): core gui qml quick quickcontrols2 kirigami +* When building with support for the experimental Qt Quick GUI, the following Qt/KDE modules are required (version 6.6 or higher): core gui qml quick quickcontrols2 kirigami To specify the major Qt version to use, set `QT_PACKAGE_PREFIX` (e.g. add `-DQT_PACKAGE_PREFIX:STRING=Qt6` to the CMake arguments). There's also `KF_PACKAGE_PREFIX` for KDE dependencies. Note that the Qt Quick GUI diff --git a/gui/undocommands.cpp b/gui/undocommands.cpp index 3bc1845..f5e6e5d 100644 --- a/gui/undocommands.cpp +++ b/gui/undocommands.cpp @@ -197,7 +197,7 @@ bool FieldModelRemoveRowsCommand::internalUndo() /*! * \brief Stores the entry path for the specified \a model and \a index in \a res. */ -void indexToPath(EntryModel *model, const QModelIndex &index, list &res) +static void indexToPath(EntryModel *model, const QModelIndex &index, list &res) { res.clear(); if (Entry *entry = model->entry(index)) { @@ -209,7 +209,7 @@ void indexToPath(EntryModel *model, const QModelIndex &index, list &res) * \brief Fetches the entry for the specified \a model and \a path. * \remarks The \a path will be modified. To prevent this use entryFromPathCpy(). */ -Entry *entryFromPath(EntryModel *model, list &path) +static Entry *entryFromPath(EntryModel *model, list &path) { if (NodeEntry *rootEntry = model->rootEntry()) { return rootEntry->entryByPath(path); @@ -220,7 +220,7 @@ Entry *entryFromPath(EntryModel *model, list &path) /*! * \brief Fetches the entry for the specified \a model and \a path. */ -Entry *entryFromPathCpy(EntryModel *model, list path) +static Entry *entryFromPathCpy(EntryModel *model, list path) { return entryFromPath(model, path); } diff --git a/model/entrymodel.cpp b/model/entrymodel.cpp index a5988a2..f7ebaf0 100644 --- a/model/entrymodel.cpp +++ b/model/entrymodel.cpp @@ -452,31 +452,39 @@ bool EntryModel::moveRows(const QModelIndex &sourceParent, int sourceRow, int co if (undoStack()) { return push(make_unique(this, sourceParent, sourceRow, count, destinationParent, destinationChild)); } +#endif +#if CPP_UTILITIES_DEBUG_BUILD + std::cout << "sourceRow: " << sourceRow << endl; + std::cout << "destinationChild: " << destinationChild << endl; #endif // check validation of specified arguments: source and destination parent entries need to be node entries if (sourceRow < 0 || count <= 0) { return false; } const auto *const srcParentEntry = entry(sourceParent); - const auto *const destParentEntry = entry(sourceParent); + const auto *const destParentEntry = entry(destinationParent); if (!srcParentEntry || !destParentEntry || srcParentEntry->type() != EntryType::Node || destParentEntry->type() != EntryType::Node) { return false; } // determine the source parent entry and dest parent entry as node entries auto *const srcParentNodeEntry = static_cast(sourceParent.internalPointer()); auto *const destParentNodeEntry = static_cast(destinationParent.internalPointer()); -#if CPP_UTILITIES_DEBUG_BUILD - cout << "destinationChild: " << destinationChild << endl; -#endif // source rows must be within the valid range - if (static_cast(sourceRow + count) > srcParentNodeEntry->children().size() - // if source and destination parent are the same the destination child mustn't be in the source range - || !(srcParentNodeEntry != destParentNodeEntry || (destinationChild < sourceRow || (sourceRow + count) < destinationChild))) { + if (static_cast(sourceRow + count) > srcParentNodeEntry->children().size()) { return false; } + // if source and destination parent are the same the destination child mustn't be in the source range + if (srcParentNodeEntry == destParentNodeEntry) { + if (destinationChild == sourceRow) { + return true; + } + if (!(destinationChild < sourceRow || (sourceRow + count) < destinationChild)) { + return false; + } + } // do not move a row to one of its own children! -> check before for (int index = 0; index < count; ++index) { - Entry *const toMove = srcParentNodeEntry->children()[static_cast(sourceRow + index)]; + Entry *const toMove = srcParentNodeEntry->children()[static_cast(sourceRow + index)]; if (toMove->type() != EntryType::Node) { continue; } @@ -485,9 +493,11 @@ bool EntryModel::moveRows(const QModelIndex &sourceParent, int sourceRow, int co } } // actually perform the move operation - beginMoveRows(sourceParent, sourceRow, sourceRow + count - 1, destinationParent, destinationChild); + if (!beginMoveRows(sourceParent, sourceRow, sourceRow + count - 1, destinationParent, destinationChild)) { + return false; + } for (int index = 0; index < count; ++index) { - Entry *toMove = srcParentNodeEntry->children()[static_cast(sourceRow + index)]; + Entry *toMove = srcParentNodeEntry->children()[static_cast(sourceRow + index)]; if (srcParentNodeEntry == destParentNodeEntry && sourceRow < destinationChild) { toMove->setParent(destParentNodeEntry, destinationChild + index - 1); } else { diff --git a/model/fieldmodel.cpp b/model/fieldmodel.cpp index 61bceac..a7584c6 100644 --- a/model/fieldmodel.cpp +++ b/model/fieldmodel.cpp @@ -233,17 +233,15 @@ bool FieldModel::setData(const QModelIndex &index, const QVariant &value, int ro switch (index.column()) { case 0: beginInsertRows(index.parent(), rowCount(), rowCount()); - m_fields->emplace_back(m_accountEntry); - m_fields->back().setName(value.toString().toStdString()); + m_fields->emplace_back(m_accountEntry).setName(value.toString().toStdString()); endInsertRows(); - roles << Qt::DisplayRole << Qt::EditRole << IsLastRow; + roles << Qt::DisplayRole << Qt::EditRole << Key << IsLastRow; break; case 1: beginInsertRows(index.parent(), rowCount(), rowCount()); - m_fields->emplace_back(m_accountEntry); - m_fields->back().setValue(value.toString().toStdString()); + m_fields->emplace_back(m_accountEntry).setValue(value.toString().toStdString()); endInsertRows(); - roles << Qt::DisplayRole << Qt::EditRole << IsLastRow; + roles << Qt::DisplayRole << Qt::EditRole << Value << IsLastRow; break; default:; } @@ -335,25 +333,22 @@ bool FieldModel::moveRows(const QModelIndex &sourceParent, int sourceRow, int co // validate input parameter if (sourceParent.isValid() || destinationParent.isValid() || sourceRow < 0 || count <= 0 || destinationChild < 0 - || static_cast(sourceRow + count) > m_fields->size() || static_cast(destinationChild) >= m_fields->size() + || static_cast(sourceRow + count) > m_fields->size() || static_cast(destinationChild) >= m_fields->size() || (destinationChild >= sourceRow && destinationChild < (sourceRow + count))) { return false; } // begin the move - if (destinationChild > sourceRow) { - // move rows down: the third param is still counted in the initial array! - beginMoveRows(sourceParent, sourceRow, sourceRow + count - 1, destinationParent, destinationChild + count); - } else { - // move rows up - beginMoveRows(sourceParent, sourceRow, sourceRow + count - 1, destinationParent, destinationChild); + // note: When moving rows down (destinationChild > sourceRow) the third param is still counted in the initial array! + if (!beginMoveRows(sourceParent, sourceRow, sourceRow + count - 1, destinationParent, destinationChild > sourceRow ? destinationChild + count : destinationChild)) { + return false; } // reserve space for temporary copies (FIXME: possible to avoid this?) - m_fields->reserve(m_fields->size() + static_cast(count)); - vector tmp(static_cast(count)); + m_fields->reserve(m_fields->size() + static_cast(count)); + auto tmp = vector(static_cast(count)); // move rows to temporary array - move(m_fields->begin() + sourceRow, m_fields->begin() + sourceRow + count, tmp.begin()); + std::move(m_fields->begin() + sourceRow, m_fields->begin() + sourceRow + count, tmp.begin()); // erase slots of rows to be moved m_fields->erase(m_fields->begin() + sourceRow, m_fields->begin() + sourceRow + count); // insert rows again at their new position diff --git a/qml/AboutDialog.qml b/qml/AboutDialog.qml index ce846e0..fe26da1 100644 --- a/qml/AboutDialog.qml +++ b/qml/AboutDialog.qml @@ -7,8 +7,7 @@ BasicDialog { id: aboutDialog standardButtons: Controls.Dialog.Ok padding: Kirigami.Units.largeSpacing - - ColumnLayout { + contentItem: ColumnLayout { width: aboutDialog.availableWidth Image { diff --git a/qml/BasicDialog.qml b/qml/BasicDialog.qml index b3b4e6d..0b667a0 100644 --- a/qml/BasicDialog.qml +++ b/qml/BasicDialog.qml @@ -6,9 +6,7 @@ Controls.Dialog { modal: true focus: true parent: applicationWindow().overlay - //anchors.centerIn: parent // enable if requiring at least Qt 5.12 instead of setting x and y manually - x: (parent.width - width) / 2 - y: (parent.height - height) / 2 + anchors.centerIn: parent width: Math.min(parent.width, Kirigami.Units.gridUnit * 30) function acceptOnReturn(event) { diff --git a/qml/EntriesPage.qml b/qml/EntriesPage.qml index 3a2e947..9b92088 100644 --- a/qml/EntriesPage.qml +++ b/qml/EntriesPage.qml @@ -1,4 +1,4 @@ -import QtQuick 2.4 +import QtQuick 2.15 import QtQuick.Layouts 1.2 import QtQml.Models 2.2 import QtQuick.Controls 2.4 as Controls @@ -64,8 +64,7 @@ Kirigami.ScrollablePage { standardButtons: Controls.Dialog.Ok | Controls.Dialog.Cancel title: qsTr("Delete %1?").arg(entryDesc) onAccepted: entryModel.removeRows(this.entryIndex, 1, rootIndex) - - ColumnLayout { + contentItem: ColumnLayout { Controls.Label { text: " " } @@ -109,13 +108,12 @@ Kirigami.ScrollablePage { entryModel.removeRows(this.entryIndex, 1, rootIndex) } } - - ColumnLayout { + contentItem: ColumnLayout { Controls.TextField { id: entryNameTextField Layout.preferredWidth: renameDialog.availableWidth placeholderText: qsTr("enter new name here") - Keys.onPressed: renameDialog.acceptOnReturn(event) + Keys.onPressed: (event) => renameDialog.acceptOnReturn(event) } } @@ -138,115 +136,6 @@ Kirigami.ScrollablePage { } } - // component representing an entry - Component { - id: listDelegateComponent - - Kirigami.SwipeListItem { - id: listItem - contentItem: RowLayout { - 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, - oldIndex < newIndex ? newIndex + 1 : newIndex) - } - Item { - Layout.fillWidth: true - Layout.fillHeight: true - RowLayout { - anchors.fill: parent - Kirigami.Icon { - width: Kirigami.Units.iconSizes.smallMedium - height: Kirigami.Units.iconSizes.smallMedium - Layout.fillHeight: true - source: delegateModel.isNode( - index) ? "folder-symbolic" : "story-editor" - } - Controls.Label { - Layout.fillWidth: true - Layout.fillHeight: true - text: model.name - verticalAlignment: Text.AlignVCenter - } - } - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.LeftButton | Qt.RightButton - onClicked: { - if (mouse.button === Qt.RightButton) { - entryContextMenu.popup() - return - } - delegateModel.handleEntryClicked(index, model.name) - } - onPressAndHold: entryContextMenu.popup() - } - Controls.Menu { - id: entryContextMenu - Controls.MenuItem { - icon.name: "edit-cut" - text: qsTr("Cut") - enabled: !nativeInterface.hasEntryFilter - onTriggered: { - nativeInterface.cutEntry( - entryModel.index(index, 0, - rootIndex)) - showPassiveNotification(qsTr("Cut %1").arg( - model.name)) - } - } - 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) - } - } - } - } - actions: [ - Kirigami.Action { - icon.name: "edit-cut" - text: qsTr("Cut") - enabled: !nativeInterface.hasEntryFilter - onTriggered: { - nativeInterface.cutEntry(entryModel.index(index, 0, - rootIndex)) - showPassiveNotification(text + " " + model.name) - } - shortcut: StandardKey.Cut - }, - Kirigami.Action { - icon.name: "edit-delete" - text: qsTr("Delete") - enabled: !nativeInterface.hasEntryFilter - onTriggered: confirmDeletionDialog.confirmDeletion( - model.name, index) - shortcut: StandardKey.Delete - }, - Kirigami.Action { - icon.name: "edit-rename" - text: qsTr("Rename") - enabled: !nativeInterface.hasEntryFilter - onTriggered: renameDialog.renameEntry(model.name, index) - shortcut: "F2" - } - ] - } - } - // list view to display one hierarchy level of entry model ListView { id: entriesListView @@ -257,9 +146,17 @@ Kirigami.ScrollablePage { easing.type: Easing.InOutQuad } } + reuseItems: true model: DelegateModel { id: delegateModel - delegate: listDelegateComponent + delegate: EntryDelegate { + width: entriesListView.width + view: entriesListView + onMoveRequested: + (oldIndex, newIndex) => { + entryModel.moveRows(rootIndex, oldIndex, 1, rootIndex, oldIndex < newIndex ? newIndex + 1 : newIndex) + } + } function isNode(rowNumber) { return entryModel.isNode(entryModel.index(rowNumber, 0, diff --git a/qml/EntryDelegate.qml b/qml/EntryDelegate.qml new file mode 100644 index 0000000..742348d --- /dev/null +++ b/qml/EntryDelegate.qml @@ -0,0 +1,116 @@ +import QtQuick 2.4 +import QtQuick.Layouts 1.2 +import QtQml.Models 2.2 +import QtQuick.Controls 2.4 as Controls +import org.kde.kirigami 2.5 as Kirigami + +Item { + id: delegate + + property ListView view + implicitHeight : listItem.implicitHeight + + signal moveRequested(int oldIndex, int newIndex) + + Kirigami.SwipeListItem { + id: listItem + contentItem: RowLayout { + Kirigami.ListItemDragHandle { + listItem: listItem + listView: view + enabled: !nativeInterface.hasEntryFilter + onMoveRequested: + (oldIndex, newIndex) => delegate.moveRequested(oldIndex, newIndex) + } + Item { + Layout.fillWidth: true + Layout.fillHeight: true + RowLayout { + anchors.fill: parent + Kirigami.Icon { + width: Kirigami.Units.iconSizes.smallMedium + height: Kirigami.Units.iconSizes.smallMedium + Layout.fillHeight: true + source: delegateModel.isNode( + index) ? "folder-symbolic" : "story-editor" + } + Controls.Label { + Layout.fillWidth: true + Layout.fillHeight: true + text: model.name + verticalAlignment: Text.AlignVCenter + } + } + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + onClicked: (mouse) => { + if (mouse.button === Qt.RightButton) { + entryContextMenu.popup() + return + } + delegateModel.handleEntryClicked(index, model.name) + } + onPressAndHold: entryContextMenu.popup() + } + Controls.Menu { + id: entryContextMenu + Controls.MenuItem { + icon.name: "edit-cut" + text: qsTr("Cut") + enabled: !nativeInterface.hasEntryFilter + onTriggered: { + nativeInterface.cutEntry( + entryModel.index(index, 0, + rootIndex)) + showPassiveNotification(qsTr("Cut %1").arg( + model.name)) + } + } + 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) + } + } + } + } + actions: [ + Kirigami.Action { + icon.name: "edit-cut" + text: qsTr("Cut") + enabled: !nativeInterface.hasEntryFilter + onTriggered: { + nativeInterface.cutEntry(entryModel.index(index, 0, + rootIndex)) + showPassiveNotification(text + " " + model.name) + } + shortcut: StandardKey.Cut + }, + Kirigami.Action { + icon.name: "edit-delete" + text: qsTr("Delete") + enabled: !nativeInterface.hasEntryFilter + onTriggered: confirmDeletionDialog.confirmDeletion( + model.name, index) + shortcut: StandardKey.Delete + }, + Kirigami.Action { + icon.name: "edit-rename" + text: qsTr("Rename") + enabled: !nativeInterface.hasEntryFilter + onTriggered: renameDialog.renameEntry(model.name, index) + shortcut: "F2" + } + ] + } +} diff --git a/qml/FieldsDelegate.qml b/qml/FieldsDelegate.qml new file mode 100644 index 0000000..276b67a --- /dev/null +++ b/qml/FieldsDelegate.qml @@ -0,0 +1,127 @@ +import QtQuick 2.4 +import QtQuick.Layouts 1.2 +import QtQml.Models 2.2 +import QtQuick.Controls 2.4 as Controls +import org.kde.kirigami 2.5 as Kirigami + +Item { + id: delegate + + property ListView view + implicitHeight : listItem.implicitHeight + + signal moveRequested(int oldIndex, int newIndex) + + Kirigami.SwipeListItem { + id: listItem + visible: !model.isLastRow + contentItem: RowLayout { + id: fieldRow + Kirigami.ListItemDragHandle { + listItem: listItem + listView: view + onMoveRequested: (oldIndex, newIndex) => delegate.moveRequested(oldIndex, newIndex) + } + Item { + Layout.fillWidth: true + Layout.fillHeight: true + RowLayout { + anchors.fill: parent + Controls.Label { + Layout.fillWidth: true + Layout.fillHeight: true + text: { + let pieces = [] + if (model.key) { + pieces.push(model.key) + } + if (model.value) { + pieces.push(model.value) + } + return pieces.join(": ") + } + color: palette.text + verticalAlignment: Text.AlignVCenter + } + } + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + onClicked: (mouse) => { + if (mouse.button === Qt.RightButton) { + return fieldContextMenu.popup() + } + fieldDialog.init(model, index) + fieldDialog.open() + } + onPressAndHold: fieldContextMenu.popup() + } + Controls.Menu { + id: fieldContextMenu + Controls.MenuItem { + icon.name: !model.isPassword ? "password-show-off" : "password-show-on" + text: model.isPassword ? qsTr("Mark as normal field") : qsTr( + "Mark as password field") + onClicked: view.model.setData( + view.model.index(index, + 0), + model.isPassword ? 0 : 1, 0x0100 + 1) + } + Controls.MenuItem { + icon.name: "edit-copy" + text: model.isPassword ? qsTr("Copy password") : qsTr( + "Copy value") + onClicked: showPassiveNotification( + nativeInterface.copyToClipboard( + model.actualValue) ? qsTr("Copied") : qsTr( + "Unable to access clipboard")) + } + Controls.MenuItem { + icon.name: "edit-delete" + text: qsTr("Delete field") + onClicked: view.model.removeRows(index, 1) + } + Controls.MenuItem { + icon.name: "list-add" + text: qsTr("Insert empty field after this") + onClicked: view.model.insertRows( + index + 1, 1) + } + } + } + } + actions: [ + Kirigami.Action { + icon.name: !model.isPassword ? "password-show-off" : "password-show-on" + text: model.isPassword ? qsTr( + "Mark as normal field") : qsTr( + "Mark as password field") + onTriggered: view.model.setData( + view.model.index(index, 0), + model.isPassword ? 0 : 1, 0x0100 + 1) + }, + Kirigami.Action { + icon.name: "edit-copy" + text: model.isPassword ? qsTr("Copy password") : qsTr( + "Copy value") + onTriggered: showPassiveNotification( + nativeInterface.copyToClipboard( + model.actualValue) ? qsTr("Copied") : qsTr( + "Unable to access clipboard")) + shortcut: StandardKey.Cut + }, + Kirigami.Action { + icon.name: "edit-delete" + text: qsTr("Delete field") + onTriggered: view.model.removeRows(index, 1) + shortcut: StandardKey.Delete + }, + Kirigami.Action { + icon.name: "list-add" + text: qsTr("Insert empty field after this") + enabled: !nativeInterface.hasEntryFilter + onTriggered: view.model.insertRows(index + 1, 1) + } + ] + } +} diff --git a/qml/FieldsPage.qml b/qml/FieldsPage.qml index 1b8842e..7204e88 100644 --- a/qml/FieldsPage.qml +++ b/qml/FieldsPage.qml @@ -1,4 +1,4 @@ -import QtQuick 2.4 +import QtQuick 2.15 import QtQuick.Layouts 1.2 import QtQml.Models 2.2 import QtQuick.Controls 2.4 as Controls @@ -10,6 +10,21 @@ Kirigami.ScrollablePage { Layout.fillWidth: true title: nativeInterface.currentAccountName + actions:[ + Kirigami.Action { + icon.name: "list-add" + text: qsTr("Add field") + visible: !nativeInterface.hasEntryFilter + enabled: !nativeInterface.hasEntryFilter + onTriggered: { + const delegateModel = fieldsListView.model + const row = delegateModel.rowCount() - 1 + fieldDialog.init(delegateModel, row) + fieldDialog.open() + } + shortcut: "Ctrl+Shift+A" + } + ] background: Rectangle { color: Kirigami.Theme.backgroundColor } @@ -32,8 +47,7 @@ Kirigami.ScrollablePage { fieldsListView.model.setData(column0, isPassword ? 1 : 0, 0x0100 + 1) } - - ColumnLayout { + contentItem: ColumnLayout { GridLayout { Layout.preferredWidth: fieldDialog.availableWidth columns: 2 @@ -43,7 +57,7 @@ Kirigami.ScrollablePage { id: fieldNameEdit Layout.fillWidth: true text: fieldDialog.fieldName - Keys.onPressed: fieldDialog.acceptOnReturn(event) + Keys.onPressed: (event) => fieldDialog.acceptOnReturn(event) } Controls.RoundButton { flat: true @@ -69,7 +83,7 @@ Kirigami.ScrollablePage { // fix ugly bullet points under Android font.pointSize: hideCharacters ? fieldNameEdit.font.pointSize * 0.5 : fieldNameEdit.font.pointSize - Keys.onPressed: fieldDialog.acceptOnReturn(event) + Keys.onPressed: (event) => fieldDialog.acceptOnReturn(event) } Controls.RoundButton { flat: true @@ -107,166 +121,27 @@ Kirigami.ScrollablePage { } } - // component representing a field - Component { - id: fieldsListDelegateComponent - - Kirigami.SwipeListItem { - id: fieldsListItem - contentItem: RowLayout { - id: fieldRow - readonly property bool isLast: model.isLastRow - - Kirigami.ListItemDragHandle { - listItem: fieldsListItem - listView: fieldsListView - onMoveRequested: fieldsListView.model.moveRows( - fieldsListView.model.index(-1, 0), - oldIndex, 1, - fieldsListView.model.index(-1, 0), - newIndex) - visible: !fieldRow.isLast - } - Kirigami.Icon { - width: Kirigami.Units.iconSizes.smallMedium - height: width - source: "list-add" - opacity: 0.6 - visible: fieldRow.isLast - MouseArea { - anchors.fill: parent - onClicked: { - fieldDialog.init(model, index) - fieldDialog.open() - } - } - } - Item { - Layout.fillWidth: true - Layout.fillHeight: true - RowLayout { - anchors.fill: parent - Controls.Label { - Layout.fillWidth: true - Layout.fillHeight: true - text: { - if (fieldRow.isLast) { - return qsTr("click to append new field") - } - - var pieces = [] - if (model.key) { - pieces.push(model.key) - } - if (model.value) { - pieces.push(model.value) - } - return pieces.join(": ") - } - color: fieldRow.isLast ? "gray" : palette.text - verticalAlignment: Text.AlignVCenter - } - } - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.LeftButton | Qt.RightButton - onClicked: { - if (mouse.button === Qt.RightButton) { - if (!fieldRow.isLast) { - fieldContextMenu.popup() - } - return - } - fieldDialog.init(model, index) - fieldDialog.open() - } - onPressAndHold: fieldContextMenu.popup() - } - Controls.Menu { - id: fieldContextMenu - Controls.MenuItem { - icon.name: !model.isPassword ? "password-show-off" : "password-show-on" - text: model.isPassword ? qsTr("Mark as normal field") : qsTr( - "Mark as password field") - onClicked: fieldsListView.model.setData( - fieldsListView.model.index(index, - 0), - model.isPassword ? 0 : 1, 0x0100 + 1) - } - Controls.MenuItem { - icon.name: "edit-copy" - text: model.isPassword ? qsTr("Copy password") : qsTr( - "Copy value") - onClicked: showPassiveNotification( - nativeInterface.copyToClipboard( - model.actualValue) ? qsTr("Copied") : qsTr( - "Unable to access clipboard")) - } - Controls.MenuItem { - icon.name: "edit-delete" - text: qsTr("Delete field") - onClicked: fieldsListView.model.removeRows(index, 1) - } - Controls.MenuItem { - icon.name: "list-add" - text: qsTr("Insert empty field after this") - onClicked: fieldsListView.model.insertRows( - index + 1, 1) - } - } - } - } - actions: [ - Kirigami.Action { - icon.name: !model.isPassword ? "password-show-off" : "password-show-on" - text: model.isPassword ? qsTr( - "Mark as normal field") : qsTr( - "Mark as password field") - onTriggered: fieldsListView.model.setData( - fieldsListView.model.index(index, 0), - model.isPassword ? 0 : 1, 0x0100 + 1) - visible: !fieldRow.isLast - }, - Kirigami.Action { - icon.name: "edit-copy" - text: model.isPassword ? qsTr("Copy password") : qsTr( - "Copy value") - onTriggered: showPassiveNotification( - nativeInterface.copyToClipboard( - model.actualValue) ? qsTr("Copied") : qsTr( - "Unable to access clipboard")) - shortcut: StandardKey.Cut - visible: !fieldRow.isLast - }, - Kirigami.Action { - icon.name: "edit-delete" - text: qsTr("Delete field") - onTriggered: fieldsListView.model.removeRows(index, 1) - shortcut: StandardKey.Delete - visible: !fieldRow.isLast - }, - Kirigami.Action { - icon.name: "list-add" - text: qsTr("Insert empty field after this") - enabled: !nativeInterface.hasEntryFilter - onTriggered: fieldsListView.model.insertRows(index + 1, 1) - visible: !fieldRow.isLast - } - ] - } - } - // list view to edit the currently selected account ListView { id: fieldsListView implicitWidth: Kirigami.Units.gridUnit * 30 model: nativeInterface.fieldModel + reuseItems: true moveDisplaced: Transition { YAnimator { duration: Kirigami.Units.longDuration easing.type: Easing.InOutQuad } } - delegate: fieldsListDelegateComponent + delegate: FieldsDelegate { + width: fieldsListView.width + view: fieldsListView + onMoveRequested: + (oldIndex, newIndex) => { + const model = fieldsListView.model + const invalidIndex = model.index(-1, 0) + model.moveRows(invalidIndex, oldIndex, 1, invalidIndex, newIndex) + } + } } } diff --git a/qml/PasswordDialog.qml b/qml/PasswordDialog.qml index 51ab60a..8a0120b 100644 --- a/qml/PasswordDialog.qml +++ b/qml/PasswordDialog.qml @@ -30,8 +30,7 @@ BasicDialog { qsTr("You aborted. The password has not been altered.")) } } - - ColumnLayout { + contentItem: ColumnLayout { Controls.Label { id: instructionLabel Layout.preferredWidth: passwordDialog.availableWidth @@ -42,19 +41,21 @@ BasicDialog { id: passwordTextField Layout.preferredWidth: passwordDialog.availableWidth echoMode: showCharactersCheckBox.checked ? TextInput.Normal : TextInput.Password - placeholderText: qsTr("enter password here, leave empty for no encryption") + placeholderText: newPassword + ? qsTr("enter password here, leave empty for no encryption") + : qsTr("enter password here") color: "#101010" placeholderTextColor: "#505050" background: Rectangle { border.color: "#5d5e6d" } - Keys.onPressed: passwordDialog.acceptOnReturn(event) + Keys.onPressed: (event) => passwordDialog.acceptOnReturn(event) } Controls.TextField { id: repeatPasswordTextField Layout.preferredWidth: passwordDialog.availableWidth visible: passwordDialog.newPassword - && !showCharactersCheckBox.checked + enabled: visible && !showCharactersCheckBox.checked echoMode: TextInput.Password placeholderText: qsTr("repeat password") color: "#101010" diff --git a/qml/main.qml b/qml/main.qml index bedf80c..446c94d 100644 --- a/qml/main.qml +++ b/qml/main.qml @@ -16,11 +16,6 @@ Kirigami.ApplicationWindow { title: app.applicationName titleIcon: "qrc://icons/hicolor/scalable/apps/passwordmanager.svg" - // FIXME: not sure why this doesn't work anymore - //onBannerClicked: () => { - // leftMenu.resetMenu() - // aboutDialog.open() - //} visible: true resetMenuOnTriggered: false @@ -208,15 +203,20 @@ Kirigami.ApplicationWindow { icon.name: "document-close" shortcut: StandardKey.Close onTriggered: nativeInterface.close() + }, + Kirigami.Action { + separator: true + }, + Kirigami.Action { + text: qsTr("About") + icon.name: "help-about" + shortcut: "Ctrl+?" + onTriggered: { + leftMenu.resetMenu() + aboutDialog.open() + } } ] - - Controls.Switch { - text: qsTr("Use native file dialog") - checked: nativeInterface.useNativeFileDialog - visible: nativeInterface.supportsNativeFileDialog - onCheckedChanged: nativeInterface.useNativeFileDialog = checked - } } contextDrawer: Kirigami.ContextDrawer { id: contextDrawer @@ -241,9 +241,9 @@ Kirigami.ApplicationWindow { id: fileSummaryDialog standardButtons: Controls.Dialog.Ok title: qsTr("File details") - - Controls.Label { + contentItem: Controls.TextArea { id: fileSummaryLabel + readOnly: true text: "No file summary available" textFormat: Text.RichText wrapMode: Text.Wrap @@ -289,8 +289,7 @@ Kirigami.ApplicationWindow { Controls.DialogButtonBox.buttonRole: Controls.DialogButtonBox.RejectRole } } - - ColumnLayout { + contentItem: ColumnLayout { Controls.TextField { id: filterDialogTextField Layout.preferredWidth: filterDialog.availableWidth @@ -301,12 +300,12 @@ Kirigami.ApplicationWindow { Connections { target: nativeInterface - onEntryFilterChanged: { + function onEntryFilterChanged(newFilter) { if (filterTextField.text !== newFilter) { filterTextField.text = newFilter } } - onFileError: { + function onFileError(errorMessage, retryAction) { var retryMethod = null if (retryAction === "load" || retryAction === "save") { retryMethod = retryAction @@ -320,16 +319,16 @@ Kirigami.ApplicationWindow { }) } } - onSettingsError: { + function onSettingsError(errorMessage) { showPassiveNotification(errorMessage) } - onPasswordRequired: { + function onPasswordRequired(filePath) { enterPasswordDialog.askForExistingPassword( qsTr("Password required to open %1").arg( nativeInterface.filePath)) leftMenu.resetMenu() } - onFileOpenChanged: { + function onFileOpenChanged(fileOpen) { clearStack() if (!nativeInterface.fileOpen) { showPassiveNotification(qsTr("%1 closed").arg( @@ -341,20 +340,20 @@ Kirigami.ApplicationWindow { nativeInterface.fileName)) leftMenu.close() } - onFileSaved: { + function onFileSaved() { showPassiveNotification(qsTr("%1 saved").arg( nativeInterface.fileName)) } - onNewNotification: { + function onNewNotification(message) { showPassiveNotification(message) } - onCurrentAccountChanged: { + function onCurrentAccountChanged() { // remove the fields page if the current account has been removed if (!nativeInterface.hasCurrentAccount) { pageStack.pop(lastEntriesPage) } } - onEntryAboutToBeRemoved: { + function onEntryAboutToBeRemoved(removedIndex) { // get the filter entry index if (nativeInterface.hasEntryFilter) { removedIndex = nativeInterface.filterEntryIndex(removedIndex) @@ -372,7 +371,7 @@ Kirigami.ApplicationWindow { } } } - onHasEntryFilterChanged: { + function onHasEntryFilterChanged(hasEntryFilter) { if (nativeInterface.fileOpen) { pageStack.clear() initStack() diff --git a/quickgui/controller.cpp b/quickgui/controller.cpp index 0f7e6fa..3f99a71 100644 --- a/quickgui/controller.cpp +++ b/quickgui/controller.cpp @@ -43,7 +43,7 @@ Controller::Controller(QSettings &settings, const QString &filePath, QObject *pa #endif , m_fileOpen(false) , m_fileModified(false) - , m_useNativeFileDialog(false) + , m_useNativeFileDialog(supportsNativeFileDialog()) , m_filterAsDialog( #ifdef Q_OS_ANDROID true diff --git a/quickgui/initiatequick.cpp b/quickgui/initiatequick.cpp index d996088..0b89cd5 100644 --- a/quickgui/initiatequick.cpp +++ b/quickgui/initiatequick.cpp @@ -85,8 +85,8 @@ int runQuickGui(int argc, char *argv[], const QtConfigArguments &qtConfigArgs, c LOAD_QT_TRANSLATIONS; // init Quick GUI - auto engine = QQmlApplicationEngine(); auto controller = Controller(*settings, file); + auto engine = QQmlApplicationEngine(); #ifdef Q_OS_ANDROID registerControllerForAndroid(&controller); #endif diff --git a/resources/qml.qrc b/resources/qml.qrc index 92f55d3..711c843 100644 --- a/resources/qml.qrc +++ b/resources/qml.qrc @@ -8,6 +8,8 @@ ../qml/AboutDialog.qml ../qml/PasswordDialog.qml ../qml/EntriesPage.qml + ../qml/EntryDelegate.qml ../qml/FieldsPage.qml + ../qml/FieldsDelegate.qml