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
This commit is contained in:
Martchus 2024-03-17 23:06:06 +01:00
parent 4bf6a91d72
commit 6972256727
15 changed files with 358 additions and 339 deletions

View File

@ -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

View File

@ -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<string> &res)
static void indexToPath(EntryModel *model, const QModelIndex &index, list<string> &res)
{
res.clear();
if (Entry *entry = model->entry(index)) {
@ -209,7 +209,7 @@ void indexToPath(EntryModel *model, const QModelIndex &index, list<string> &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<string> &path)
static Entry *entryFromPath(EntryModel *model, list<string> &path)
{
if (NodeEntry *rootEntry = model->rootEntry()) {
return rootEntry->entryByPath(path);
@ -220,7 +220,7 @@ Entry *entryFromPath(EntryModel *model, list<string> &path)
/*!
* \brief Fetches the entry for the specified \a model and \a path.
*/
Entry *entryFromPathCpy(EntryModel *model, list<string> path)
static Entry *entryFromPathCpy(EntryModel *model, list<string> path)
{
return entryFromPath(model, path);
}

View File

@ -452,31 +452,39 @@ bool EntryModel::moveRows(const QModelIndex &sourceParent, int sourceRow, int co
if (undoStack()) {
return push(make_unique<EntryModelMoveRowsCommand>(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<NodeEntry *>(sourceParent.internalPointer());
auto *const destParentNodeEntry = static_cast<NodeEntry *>(destinationParent.internalPointer());
#if CPP_UTILITIES_DEBUG_BUILD
cout << "destinationChild: " << destinationChild << endl;
#endif
// source rows must be within the valid range
if (static_cast<size_t>(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<std::size_t>(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<size_t>(sourceRow + index)];
Entry *const toMove = srcParentNodeEntry->children()[static_cast<std::size_t>(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<size_t>(sourceRow + index)];
Entry *toMove = srcParentNodeEntry->children()[static_cast<std::size_t>(sourceRow + index)];
if (srcParentNodeEntry == destParentNodeEntry && sourceRow < destinationChild) {
toMove->setParent(destParentNodeEntry, destinationChild + index - 1);
} else {

View File

@ -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<size_t>(sourceRow + count) > m_fields->size() || static_cast<size_t>(destinationChild) >= m_fields->size()
|| static_cast<std::size_t>(sourceRow + count) > m_fields->size() || static_cast<std::size_t>(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<size_t>(count));
vector<Io::Field> tmp(static_cast<size_t>(count));
m_fields->reserve(m_fields->size() + static_cast<std::size_t>(count));
auto tmp = vector<Io::Field>(static_cast<std::size_t>(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

View File

@ -7,8 +7,7 @@ BasicDialog {
id: aboutDialog
standardButtons: Controls.Dialog.Ok
padding: Kirigami.Units.largeSpacing
ColumnLayout {
contentItem: ColumnLayout {
width: aboutDialog.availableWidth
Image {

View File

@ -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) {

View File

@ -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,

116
qml/EntryDelegate.qml Normal file
View File

@ -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"
}
]
}
}

127
qml/FieldsDelegate.qml Normal file
View File

@ -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)
}
]
}
}

View File

@ -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)
}
}
}
}

View File

@ -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"

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -8,6 +8,8 @@
<file alias="AboutDialog.qml">../qml/AboutDialog.qml</file>
<file alias="PasswordDialog.qml">../qml/PasswordDialog.qml</file>
<file alias="EntriesPage.qml">../qml/EntriesPage.qml</file>
<file alias="EntryDelegate.qml">../qml/EntryDelegate.qml</file>
<file alias="FieldsPage.qml">../qml/FieldsPage.qml</file>
<file alias="FieldsDelegate.qml">../qml/FieldsDelegate.qml</file>
</qresource>
</RCC>