passwordmanager/qml/main.qml

472 lines
16 KiB
QML

import QtQuick 2.7
import QtQuick.Templates 2.0 as T2
import QtQuick.Controls 2.1 as Controls
import QtQuick.Controls.Material
import QtQuick.Controls.Universal
import QtQuick.Layouts 1.2
import org.kde.kirigami 2.4 as Kirigami
Kirigami.ApplicationWindow {
id: root
property var fieldsPage: undefined
property var lastEntriesPage: undefined
Material.theme: nativeInterface.darkModeEnabled ? Material.Dark : Material.Light
Universal.theme: nativeInterface.darkModeEnabled ? Universal.Dark : Universal.Light
globalDrawer: Kirigami.GlobalDrawer {
id: leftMenu
property bool showNoPasswordWarning: nativeInterface.fileOpen
&& !nativeInterface.passwordSet
visible: true
resetMenuOnTriggered: false
topContent: ColumnLayout {
Layout.fillWidth: true
Item {
Layout.preferredHeight: 4
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: filterTextField.implicitHeight
enabled: nativeInterface.fileOpen
visible: !nativeInterface.filterAsDialog
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
bottomPadding: 0
Layout.fillWidth: true
}
Controls.Label {
id: fileNameLabel
padding: 8
wrapMode: Controls.Label.Wrap
fontSizeMode: Text.HorizontalFit
minimumPixelSize: 10
font.pixelSize: 20
Layout.fillWidth: true
text: nativeInterface.fileOpen ? nativeInterface.fileName : qsTr(
"No file opened")
MouseArea {
id: fileNameMouseArea
anchors.fill: parent
hoverEnabled: true
}
Controls.ToolTip {
z: 1000
text: nativeInterface.filePath
visible: text ? fileNameMouseArea.containsMouse : false
delay: Qt.styleHints.mousePressAndHoldInterval
onAboutToShow: {
x = fileNameMouseArea.mouseX + 10
y = fileNameMouseArea.mouseY + 10
}
}
}
RowLayout {
visible: leftMenu.showNoPasswordWarning
Item {
Layout.preferredWidth: 2
}
Controls.Label {
text: qsTr("No password set\nFile will be saved unencrypted!")
}
}
Item {
visible: leftMenu.showNoPasswordWarning
height: 4
}
}
actions: [
Kirigami.Action {
text: qsTr("Create new file")
icon.name: "document-new"
onTriggered: fileDialog.createNew()
shortcut: StandardKey.New
},
Kirigami.Action {
text: qsTr("Open existing file")
icon.name: "document-open"
onTriggered: fileDialog.openExisting()
shortcut: StandardKey.Open
},
Kirigami.Action {
id: recentlyOpenedAction
text: qsTr("Recently opened ...")
icon.name: "document-open-recent"
children: createRecentlyOpenedActions(
nativeInterface.recentFiles)
visible: nativeInterface.recentFiles.length > 0
shortcut: "Ctrl+R"
},
Kirigami.Action {
text: qsTr("Save modifications")
enabled: nativeInterface.fileOpen
icon.name: "document-save"
onTriggered: nativeInterface.save()
shortcut: StandardKey.Save
},
Kirigami.Action {
text: qsTr("Save as")
enabled: nativeInterface.fileOpen
icon.name: "document-save-as"
onTriggered: fileDialog.saveAs()
shortcut: StandardKey.SaveAs
},
Kirigami.Action {
text: nativeInterface.passwordSet ? qsTr("Change password") : qsTr(
"Add password")
enabled: nativeInterface.fileOpen
icon.name: "document-encrypt"
onTriggered: enterPasswordDialog.askForNewPassword(
qsTr("Change password for %1").arg(
nativeInterface.filePath))
shortcut: "Ctrl+P"
},
Kirigami.Action {
text: qsTr("Details")
enabled: nativeInterface.fileOpen
icon.name: "document-properties"
onTriggered: {
leftMenu.resetMenu()
fileSummaryDialog.show()
}
shortcut: "Ctrl+I"
},
Kirigami.Action {
text: nativeInterface.entryFilter.length === 0 ? qsTr("Search") : qsTr(
"Adjust search")
enabled: nativeInterface.fileOpen
visible: nativeInterface.filterAsDialog
icon.name: "search"
onTriggered: {
leftMenu.resetMenu()
filterDialog.open()
}
shortcut: "Ctrl+F"
},
Kirigami.Action {
text: qsTr("Clear search")
enabled: nativeInterface.fileOpen
visible: nativeInterface.filterAsDialog
&& nativeInterface.entryFilter.length > 0
icon.name: "edit-clear"
onTriggered: {
leftMenu.resetMenu()
nativeInterface.entryFilter = ""
}
shortcut: "Ctrl+Shift+F"
},
Kirigami.Action {
text: qsTr("Undo \"%1\"").arg(nativeInterface.undoText)
visible: nativeInterface.undoText.length !== 0
&& nativeInterface.entryFilter.length === 0
enabled: visible
icon.name: "edit-undo"
shortcut: StandardKey.Undo
onTriggered: nativeInterface.undo()
},
Kirigami.Action {
text: qsTr("Redo \"%1\"").arg(nativeInterface.redoText)
visible: nativeInterface.redoText.length !== 0
&& nativeInterface.entryFilter.length === 0
enabled: visible
icon.name: "edit-redo"
shortcut: StandardKey.Redo
onTriggered: nativeInterface.redo()
},
Kirigami.Action {
text: qsTr("Close file")
enabled: nativeInterface.fileOpen
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()
}
}
]
}
contextDrawer: Kirigami.ContextDrawer {
id: contextDrawer
}
Component.onCompleted: nativeInterface.init()
AboutDialog {
id: aboutDialog
}
PasswordDialog {
id: enterPasswordDialog
onAboutToShow: leftMenu.close()
onRejected: {
if (!nativeInterface.fileOpen) {
leftMenu.open()
}
}
}
BasicDialog {
id: fileSummaryDialog
standardButtons: Controls.Dialog.Ok
title: qsTr("File details")
contentItem: Controls.TextArea {
id: fileSummaryLabel
readOnly: true
text: "No file summary available"
textFormat: Text.RichText
wrapMode: Text.Wrap
width: fileSummaryDialog.availableWidth
}
function show() {
fileSummaryLabel.text = nativeInterface.computeFileSummary()
this.open()
}
}
FileDialog {
id: fileDialog
}
BasicDialog {
id: filterDialog
title: qsTr("Search for categories and accounts")
onAccepted: nativeInterface.entryFilter = filterDialogTextField.text
onReset: {
nativeInterface.entryFilter = ""
filterDialog.close()
}
onVisibleChanged: {
if (visible) {
filterDialogTextField.forceActiveFocus()
}
}
footer: Controls.DialogButtonBox {
Controls.Button {
text: qsTr("Apply search term")
Controls.DialogButtonBox.buttonRole: Controls.DialogButtonBox.AcceptRole
enabled: filterDialogTextField.text.length > 0
}
Controls.Button {
text: qsTr("Clear search")
Controls.DialogButtonBox.buttonRole: Controls.DialogButtonBox.ResetRole
enabled: nativeInterface.entryFilter
}
Controls.Button {
text: qsTr("Quit dialog")
Controls.DialogButtonBox.buttonRole: Controls.DialogButtonBox.RejectRole
}
}
contentItem: ColumnLayout {
Controls.TextField {
id: filterDialogTextField
Layout.preferredWidth: filterDialog.availableWidth
Keys.onPressed: filterDialog.acceptOnReturn(event)
}
}
}
Connections {
target: nativeInterface
function onEntryFilterChanged(newFilter) {
if (filterTextField.text !== newFilter) {
filterTextField.text = newFilter
}
}
function onFileError(errorMessage, retryAction) {
var retryMethod = null
if (retryAction === "load" || retryAction === "save") {
retryMethod = retryAction
}
if (retryMethod) {
showPassiveNotification(errorMessage)
} else {
showPassiveNotification(errorMessage, 2500, qsTr("Retry"),
function () {
nativeInterface[retryMethod]()
})
}
}
function onSettingsError(errorMessage) {
showPassiveNotification(errorMessage)
}
function onPasswordRequired(filePath) {
enterPasswordDialog.askForExistingPassword(
qsTr("Password required to open %1").arg(
nativeInterface.filePath))
leftMenu.resetMenu()
}
function onFileOpenChanged(fileOpen) {
clearStack()
if (!nativeInterface.fileOpen) {
showPassiveNotification(qsTr("%1 closed").arg(
nativeInterface.fileName))
return
}
initStack()
showPassiveNotification(qsTr("%1 opened").arg(
nativeInterface.fileName))
leftMenu.close()
}
function onFileSaved() {
showPassiveNotification(qsTr("%1 saved").arg(
nativeInterface.fileName))
}
function onNewNotification(message) {
showPassiveNotification(message)
}
function onCurrentAccountChanged() {
// remove the fields page if the current account has been removed
if (!nativeInterface.hasCurrentAccount) {
pageStack.pop(lastEntriesPage)
}
}
function onEntryAboutToBeRemoved(removedIndex) {
// 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)
if (!stackPage) {
continue
}
if (stackPage.rootIndex === removedIndex) {
pageStack.pop(lastEntriesPage = pageStack.get(i - 1))
return
}
}
}
function onHasEntryFilterChanged(hasEntryFilter) {
if (nativeInterface.fileOpen) {
pageStack.clear()
initStack()
}
}
}
Component {
id: fileActionComponent
Kirigami.Action {
property string filePath
text: filePath.substring(filePath.lastIndexOf('/') + 1)
onTriggered: {
nativeInterface.clear()
nativeInterface.filePath = filePath
nativeInterface.load()
}
}
}
Component {
id: clearRecentFilesActionComponent
Kirigami.Action {
text: qsTr("Clear recently opened files")
icon.name: "edit-clear"
onTriggered: {
nativeInterface.clearRecentFiles()
leftMenu.resetMenu()
}
}
}
Component {
id: entriesComponent
EntriesPage {
main: root
}
}
Component {
id: fieldsComponent
FieldsPage {
main: root
}
}
Shortcut {
sequence: "Ctrl+M"
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)
}
function pushStackEntry(entryModel, rootIndex) {
pageStack.push(lastEntriesPage = entriesComponent.createObject(root, {
"entryModel": entryModel,
"rootIndex": rootIndex
}))
}
function pushAccountEdit() {
// lazy-initialize fieldsPage
if (!fieldsPage) {
fieldsPage = fieldsComponent.createObject(root)
}
// remove fieldsPage if already shown to prevent warning
if (pageStack.get(pageStack.depth - 1) === fieldsPage) {
pageStack.pop(lastEntriesPage)
}
pageStack.push(fieldsPage)
}
function createFileActions(files) {
return files.map(function (filePath) {
return this.createObject(root, {
"filePath": filePath
})
}, fileActionComponent)
}
function createRecentlyOpenedActions(files) {
var actions = createFileActions(files)
actions.push(clearRecentFilesActionComponent.createObject(root))
return actions
}
}