diff --git a/components/kube/qml/Kube.qml b/components/kube/qml/Kube.qml index 61933908..8d77ddb5 100644 --- a/components/kube/qml/Kube.qml +++ b/components/kube/qml/Kube.qml @@ -1,322 +1,325 @@ /* * Copyright (C) 2017 Michael Bohlender, * Copyright (C) 2017 Christian Mollekopf, * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import QtQuick 2.7 import QtQuick.Layouts 1.3 import QtQuick.Window 2.0 import QtQuick.Controls 2.0 as Controls2 import org.kube.framework 1.0 as Kube Controls2.ApplicationWindow { id: app property int sidebarWidth: Kube.Units.gridUnit + Kube.Units.largeSpacing height: Screen.desktopAvailableHeight * 0.8 width: Screen.desktopAvailableWidth * 0.8 visible: true //Application default font font.family: Kube.Font.fontFamily //Application context property variant currentFolder onCurrentFolderChanged: { if (!!currentFolder) { Kube.Fabric.postMessage(Kube.Messages.synchronize, {"folder": currentFolder}) } } property variant currentAccount onCurrentAccountChanged: { if (!!currentAccount) { Kube.Fabric.postMessage(Kube.Messages.synchronize, {"accountId": currentAccount}) } } //Interval sync Timer { id: intervalSync //5min interval: 300000 running: !!app.currentFolder repeat: true onTriggered: { Kube.Fabric.postMessage(Kube.Messages.synchronize, {"folder": app.currentFolder}) Kube.Fabric.postMessage(Kube.Messages.synchronize, {"accountId": app.currentAccount, "type": "folder"}) } } Kube.StartupCheck { id: startupCheck } Accounts { } //Listener Kube.Listener { filter: Kube.Messages.accountSelection onMessageReceived: app.currentAccount = message.accountId } Kube.Listener { filter: Kube.Messages.folderSelection onMessageReceived: app.currentFolder = message.folder } Kube.Listener { filter: Kube.Messages.notification onMessageReceived: { if (message.message) { notificationPopup.notify(message.message); } } } //BEGIN Shortcuts Shortcut { sequence: StandardKey.Quit onActivated: Qt.quit() } Shortcut { onActivated: Kube.Fabric.postMessage(Kube.Messages.search, {}) sequence: StandardKey.Find } Shortcut { id: syncShortcut sequence: StandardKey.Refresh onActivated: { if (!!app.currentFolder) { Kube.Fabric.postMessage(Kube.Messages.synchronize, {"folder": app.currentFolder}); Kube.Fabric.postMessage(Kube.Messages.synchronize, {"accountId": app.currentAccount, "type": "folder"}) } else { Kube.Fabric.postMessage(Kube.Messages.synchronize, {"accountId": app.currentAccount}) } } } //END Shortcuts //BEGIN background Rectangle { anchors.fill: parent color: Kube.Colors.backgroundColor } //END background //BEGIN Main content RowLayout { id: mainContent spacing: 0 anchors.fill: parent Rectangle { id: sideBar anchors { top: mainContent.top bottom: mainContent.bottom } width: app.sidebarWidth color: Kube.Colors.textColor Rectangle { anchors.right: parent.right width: 1 height: parent.height color: Kube.Colors.viewBackgroundColor opacity: 0.3 } Controls2.ButtonGroup { id: viewButtonGroup } Column { anchors { top: parent.top topMargin: Kube.Units.smallSpacing horizontalCenter: parent.horizontalCenter } spacing: Kube.Units.largeSpacing - Kube.Units.smallSpacing Repeater { model: Kube.ExtensionModel { id: extensionModel extensionPoint: "views" sortOrder: ["composer", "conversation", "people"] } Kube.IconButton { id: button iconName: model.icon onClicked: kubeViews.showView(model.name) activeFocusOnTab: true checkable: true Controls2.ButtonGroup.group: viewButtonGroup tooltip: model.tooltip checked: kubeViews.currentViewName == model.name } } } Column { anchors { bottom: parent.bottom bottomMargin: Kube.Units.smallSpacing horizontalCenter: parent.horizontalCenter } spacing: Kube.Units.largeSpacing - Kube.Units.smallSpacing Kube.Outbox { height: Kube.Units.gridUnit * 1.5 width: height Kube.ToolTip { text: qsTr("outbox") visible: parent.hovered } } Kube.IconButton { id: logButton iconName: Kube.Icons.info_inverted onClicked: kubeViews.showView("log") activeFocusOnTab: true checkable: true - alert: kubeViews.getView("log").pendingError + Kube.Listener { + filter: Kube.Messages.errorPending + onMessageReceived: logButton.alert = message.errorPending + } checked: kubeViews.currentViewName == "log" Controls2.ButtonGroup.group: viewButtonGroup tooltip: qsTr("logview") } Kube.IconButton { id: accountsButton iconName: Kube.Icons.menu_inverted onClicked: kubeViews.showView("accounts") activeFocusOnTab: true checkable: true checked: kubeViews.currentViewName == "accounts" Controls2.ButtonGroup.group: viewButtonGroup tooltip: qsTr("settings") } } } ViewManager { id: kubeViews anchors { top: mainContent.top bottom: mainContent.bottom } Layout.fillWidth: true extensionModel: extensionModel Component.onCompleted: { dontFocus = true showView("conversation") if (startupCheck.noAccount) { showView("accounts") } dontFocus = false } Kube.Listener { filter: Kube.Messages.reply onMessageReceived: kubeViews.replaceView("composer", {message: message.mail, loadType: Kube.ComposerController.Reply}) } Kube.Listener { filter: Kube.Messages.forward onMessageReceived: kubeViews.replaceView("composer", {message: message.mail, loadType: Kube.ComposerController.Forward}) } Kube.Listener { filter: Kube.Messages.edit onMessageReceived: kubeViews.replaceView("composer", {message: message.mail, loadType: Kube.ComposerController.Draft}) } Kube.Listener { filter: Kube.Messages.compose onMessageReceived: kubeViews.replaceView("composer", {newMessage: true, recipients: message.recipients}) } Kube.Listener { filter: Kube.Messages.requestAccountsConfiguration onMessageReceived: kubeViews.showView("accounts") } Kube.Listener { filter: Kube.Messages.componentDone onMessageReceived: { kubeViews.closeView() } } Kube.Listener { filter: Kube.Messages.requestLogin onMessageReceived: { var view = loginView.createObject(kubeViews, {accountId: message.accountId}) view.forceActiveFocus() } } Component { id: loginView Kube.Popup { id: popup property alias accountId: login.accountId visible: true parent: Controls2.ApplicationWindow.overlay height: app.height width: app.width - app.sidebarWidth x: app.sidebarWidth y: 0 modal: true closePolicy: Controls2.Popup.NoAutoClose Kube.LoginAccount { id: login anchors { fill: parent bottomMargin: Kube.Units.largeSpacing } onDone: { kubeViews.currentItem.forceActiveFocus() popup.destroy() } } } } } } //END Main content //BEGIN Notification Kube.NotificationPopup { id: notificationPopup anchors { left: parent.left leftMargin: app.sidebarWidth - 3 // so it does not align with the border bottom: parent.bottom bottomMargin: Kube.Units.gridUnit * 4 } } //END Notification } diff --git a/components/kube/qml/ViewManager.qml b/components/kube/qml/ViewManager.qml index 4c28403f..15ff2638 100644 --- a/components/kube/qml/ViewManager.qml +++ b/components/kube/qml/ViewManager.qml @@ -1,109 +1,111 @@ /* * Copyright (C) 2018 Christian Mollekopf, * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import QtQuick 2.7 import QtQuick.Controls 2.0 -/* -* TODO -* * Only replace composer if necessary (on reply, edit draft, ...) -* * Shutdown procedure to save draft before destruction -* * Separate logging from view and and make accessible to log (initialize()) call? -*/ + StackView { id: root property string currentViewName: currentItem ? currentItem.objectName : "" property variant extensionModel: null property bool dontFocus: false + /* + * We maintain the view's lifetimes separately from the StackView in the viewDict. + */ property var viewDict: new Object - function getView(name, replaceView) { - if (name in viewDict) { - var item = viewDict[name] - if (item) { - if (replaceView) { - if (item && item.aborted) { - //Call the aborted hook on the view - item.aborted() - } - } else { - return item - } - } - } - - var source = extensionModel.findSource(name, "View.qml"); - var component = Qt.createComponent(source) - if (component.status == Component.Ready) { - var o = component.createObject(root) - viewDict[name] = o - return o - } else if (component.status == Component.Error) { - console.error("Error while loading the component: ", source, "\nError: ", component.errorString()) - } else if (component.status == Component.Loading) { - console.error("Error while loading the component: ", source, "\nThe component is loading.") - } else { - console.error("Unknown error while loading the component: ", source) - } - return null - } onCurrentItemChanged: { if (currentItem && !dontFocus) { currentItem.forceActiveFocus() } } + function pushView(view, properties, name) { + var item = push(view, properties, StackView.Immediate) + item.parent = root + item.anchors.fill = root + item.objectName = name + } + function showOrReplaceView(name, properties, replace) { if (currentItem && currentItem.objectName == name) { return } if (root.depth > 0) { root.pop(StackView.Immediate) } //Avoid trying to push the same item again, if its on top after pop if (currentItem && currentItem.objectName == name) { return } - var view = getView(name, replace) - if (!view) { - return + + var item = name in viewDict ? viewDict[name] : null + if (item) { + if (replace) { + if (item.aborted) { + //Call the aborted hook on the view + item.aborted() + } + //Fall through to create new component to replace this one + } else { + pushView(item, properties, name) + return + } + } + + //Creating a new view + var source = extensionModel.findSource(name, "View.qml"); + //On windows it will be async anyways, so just always create it async + var component = Qt.createComponent(source, Qt.Asynchronous) + + function finishCreation() { + if (component.status == Component.Ready) { + var view = component.createObject(root); + viewDict[name] = view + pushView(view, properties, name) + } else { + console.error("Error while loading the component: ", source, "\nError: ", component.errorString()) + } + } + + if (component.status == Component.Loading) { + component.statusChanged.connect(finishCreation); + } else { + finishCreation(); } - var item = push(view, properties, StackView.Immediate) - item.parent = root - item.anchors.fill = root - item.objectName = name } function showView(name, properties) { showOrReplaceView(name, properties, false) } function replaceView(name, properties) { showOrReplaceView(name, properties, true) } function closeView() { //The initial view remains if (kubeViews.depth > 1) { var item = kubeViews.pop(StackView.Immediate) viewDict[item.objectName] = null item.destroy() } } } diff --git a/framework/qml/Messages.qml b/framework/qml/Messages.qml index 1483a71c..f357b78b 100644 --- a/framework/qml/Messages.qml +++ b/framework/qml/Messages.qml @@ -1,64 +1,65 @@ /* Copyright (C) 2017 Michael Bohlender, This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ pragma Singleton import QtQuick 2.7 Item { //Selections property string folderSelection: "currentFolder" property string mailSelection: "currentMail" property string accountSelection: "currentAccount" //Actions property string moveToTrash: "moveToTrash" property string restoreFromTrash: "restoreFromTrash" property string markAsRead: "markAsRead" property string markAsUnread: "markAsUnread" property string toggleImportant: "toggleImportant" property string moveToFolder: "moveToFolder" property string moveToDrafts: "moveToDrafts" property string unlockKeyring: "unlockKeyring" property string requestLogin: "requestLogin" property string requestAccountsConfiguration: "requestAccountsConfiguration" property string notification: "notification" property string progressNotification: "progressNotification" property string errorNotification: "errorNotification" property string search: "search" property string searchString: "searchString" property string synchronize: "synchronize" property string reply: "reply" property string forward: "forward" property string edit: "edit" property string compose: "compose" property string sendOutbox: "sendOutbox" property string componentDone: "done" + property string errorPending: "errorPending" property string selectNextConversation: "selectNextConversation" property string selectPreviousConversation: "selectPreviousConversation" property string selectNextMessage: "selectNextMessage" property string selectPreviousMessage: "selectPreviousMessage" property string selectNextFolder: "selectNextFolder" property string selectPreviousFolder: "selectPreviousFolder" property string scrollConversationDown: "scrollConversationDown" property string scrollConversationUp: "scrollConversationUp" } diff --git a/views/log/qml/View.qml b/views/log/qml/View.qml index bf06b76c..3aa76025 100644 --- a/views/log/qml/View.qml +++ b/views/log/qml/View.qml @@ -1,415 +1,418 @@ /* * Copyright (C) 2017 Michael Bohlender, * Copyright (C) 2017 Christian Mollekopf, * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import QtQuick 2.4 import QtQuick.Layouts 1.1 import QtQuick.Controls 1.3 as Controls1 import QtQuick.Controls 2.0 as Controls2 import org.kube.framework 1.0 as Kube Controls1.SplitView { id: root property bool pendingError: false; + onPendingErrorChanged: { + Kube.Fabric.postMessage(Kube.Messages.errorPending, {errorPending: pendingError}) + } Controls2.StackView.onActivated: { root.pendingError = false; //Always select the latest notification listView.currentIndex = 0 } Item { id: accountList width: parent.width/3 Layout.fillHeight: true Kube.Listener { filter: Kube.Messages.notification onMessageReceived: { //Ignore noise that we can't usefully render anyways if (!message.message) { return } if (message.type == Kube.Notifications.error) { root.pendingError = true } var error = { timestamp: new Date(), message: message.message, details: message.details, resource: message.resource, // TODO: if we passed entities as a list, it would get // converted to a ListModel, in all likelihood because of // ListDelegate, which we should rewrite in C++ entities: {elements: message.entities} } if (logModel.count > 0) { var lastEntry = logModel.get(0) //Merge if we get an entry of the same subtype if (lastEntry.subtype && lastEntry.subtype == message.subtype) { logModel.set(0, {type: message.type, subtype: message.subtype, errors: [error].concat(lastEntry.errors)}) return } } logModel.insert(0, {type: message.type, subtype: message.subtype, errors: [error]}) } } Kube.Label { anchors.centerIn: parent visible: listView.count == 0 text: qsTr("Nothing here...") } Kube.ListView { id: listView anchors { fill: parent } clip: true model: ListModel { id: logModel objectName: "logModel" } onCurrentItemChanged: { var error = currentItem.currentData.errors.get(0) if (!!error.resource) { details.resourceId = error.resource } details.message = error.message + "\n" + error.details details.timestamp = error.timestamp if (!!currentItem.currentData.subtype) { details.subtype = currentItem.currentData.subtype } else { details.subtype = "" } details.entities = error.entities } delegate: Kube.ListDelegate { border.color: Kube.Colors.buttonColor border.width: 1 Kube.Label { id: description anchors { top: parent.top topMargin: Kube.Units.smallSpacing left: parent.left leftMargin: Kube.Units.largeSpacing } height: Kube.Units.gridUnit width: parent.width - Kube.Units.largeSpacing * 2 text: model.type == Kube.Notifications.error ? qsTr("Error") : qsTr("Info") } Kube.Label { id: message anchors { topMargin: Kube.Units.smallSpacing top: description.bottom left: parent.left leftMargin: Kube.Units.largeSpacing } height: Kube.Units.gridUnit width: parent.width - Kube.Units.largeSpacing * 2 maximumLineCount: 1 elide: Text.ElideRight color: Kube.Colors.disabledTextColor text: model.errors.get(0).message } Kube.Label { id: date anchors { right: parent.right bottom: parent.bottom rightMargin: Kube.Units.smallSpacing } text: Qt.formatDateTime(model.errors.get(0).timestamp, " hh:mm:ss dd MMM yyyy") font.italic: true color: Kube.Colors.disabledTextColor font.pointSize: Kube.Units.smallFontSize } } } } Item { id: details property string subtype: "" property date timestamp property string message: "" property string resourceId: "" property var entities: [] Kube.ModelIndexRetriever { id: retriever model: Kube.AccountsModel { resourceId: details.resourceId } } Loader { id: detailsLoader visible: message != "" clip: true anchors { fill: parent margins: Kube.Units.largeSpacing } property date timestamp: details.timestamp property string message: details.message property string resourceId: details.resourceId property string accountId: retriever.currentData ? retriever.currentData.accountId : "" property string accountName: retriever.currentData ? retriever.currentData.name : "" property var entities: details.entities function getComponent(subtype) { if (subtype == Kube.Notifications.loginError) { return loginErrorComponent } if (subtype == Kube.Notifications.hostNotFoundError) { return hostNotFoundErrorComponent } if (subtype == Kube.Notifications.connectionError) { return hostNotFoundErrorComponent } if (subtype == Kube.Notifications.transmissionError) { return transmissionErrorComponent } return detailsComponent } sourceComponent: getComponent(details.subtype) } } Component { id: detailsComponent Rectangle { color: Kube.Colors.viewBackgroundColor GridLayout { id: gridLayout Layout.minimumWidth: 0 anchors { top: parent.top left: parent.left right: parent.right } columns: 2 Kube.Label { text: qsTr("Account:") visible: accountName } Kube.Label { Layout.fillWidth: true text: accountName visible: accountName elide: Text.ElideRight } Kube.Label { text: qsTr("Account Id:") visible: accountId } Kube.Label { text: accountId visible: accountId Layout.fillWidth: true elide: Text.ElideRight } Kube.Label { text: qsTr("Resource Id:") visible: resourceId } Kube.Label { text: resourceId visible: resourceId Layout.fillWidth: true elide: Text.ElideRight } Kube.Label { text: qsTr("Timestamp:") } Kube.Label { text: Qt.formatDateTime(timestamp, " hh:mm:ss dd MMM yyyy") Layout.fillWidth: true elide: Text.ElideRight } Kube.Label { text: qsTr("Message:") Layout.alignment: Qt.AlignTop } Kube.Label { text: message Layout.fillWidth: true wrapMode: Text.Wrap } Item { Layout.columnSpan: 2 Layout.fillHeight: true Layout.fillWidth: true } } Kube.SelectableItem { layout: gridLayout } } } Component { id: loginErrorComponent Item { Column { anchors { top: parent.top left: parent.left right: parent.right } spacing: Kube.Units.largeSpacing Column { Kube.Heading { id: heading text: qsTr("Failed to login") color: Kube.Colors.warningColor } Kube.Label { id: subHeadline text: accountName + ": " + qsTr("Please check your credentials.") color: Kube.Colors.disabledTextColor wrapMode: Text.Wrap } } Kube.Button { text: qsTr("Change Password") onClicked: { Kube.Fabric.postMessage(Kube.Messages.componentDone, {}) Kube.Fabric.postMessage(Kube.Messages.requestLogin, {accountId: accountId}) } } } } } Component { id: hostNotFoundErrorComponent Item { Column { anchors { top: parent.top left: parent.left right: parent.right } spacing: Kube.Units.largeSpacing Column { Kube.Heading { id: heading text: qsTr("Host not found") color: Kube.Colors.warningColor } Kube.Label { id: subHeadline text: accountName + ": " + qsTr("Please check your network connection and settings.") color: Kube.Colors.disabledTextColor wrapMode: Text.Wrap } } Kube.Button { text: qsTr("Account settings") onClicked: { Kube.Fabric.postMessage(Kube.Messages.componentDone, {}) Kube.Fabric.postMessage(Kube.Messages.requestAccountsConfiguration, {}) } } } } } Component { id: transmissionErrorComponent Item { Column { anchors { top: parent.top left: parent.left right: parent.right } spacing: Kube.Units.largeSpacing Kube.Heading { id: heading text: qsTr("Failed to send the message.") color: Kube.Colors.warningColor } Column { spacing: Kube.Units.largeSpacing Repeater { model: Kube.MailListModel { entityId: entities.elements[0] } delegate: Column { id: subHeadline Kube.Label { text: qsTr("Account") + ": " + accountName color: Kube.Colors.disabledTextColor wrapMode: Text.Wrap } Kube.Label { text: qsTr("Subject") + ": " + model.subject color: Kube.Colors.disabledTextColor wrapMode: Text.Wrap } Kube.Label { text: qsTr("To") + ": " + model.to color: Kube.Colors.disabledTextColor wrapMode: Text.Wrap } Kube.Label { visible: !!model.cc text: qsTr("Cc") + ": " + model.cc; color: Kube.Colors.disabledTextColor wrapMode: Text.Wrap } } } } Kube.Button { text: qsTr("Try again") onClicked: { Kube.Fabric.postMessage(Kube.Messages.sendOutbox, {}) } } } } } }