From 6d726bb10386b3d7f5481d41b735ec06cb2163ad Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Tue, 9 Jan 2018 09:35:59 +0100 Subject: Install composer/converations/people as separate views and load them dynamically. --- views/CMakeLists.txt | 11 + views/accounts/metadata.json | 4 + views/accounts/qml/View.qml | 143 ++++++ views/accounts/tests/tst_accountsview.qml | 37 ++ views/composer/metadata.json | 4 + views/composer/qml/AddresseeListEditor.qml | 138 ++++++ views/composer/qml/View.qml | 533 ++++++++++++++++++++++ views/composer/tests/tst_composerview.qml | 97 ++++ views/conversation/metadata.json | 4 + views/conversation/qml/View.qml | 120 +++++ views/conversation/tests/tst_conversationview.qml | 37 ++ views/log/metadata.json | 4 + views/log/qml/View.qml | 329 +++++++++++++ views/log/tests/tst_logview.qml | 65 +++ views/people/metadata.json | 4 + views/people/qml/View.qml | 38 ++ views/people/tests/tst_peopleview.qml | 37 ++ 17 files changed, 1605 insertions(+) create mode 100644 views/CMakeLists.txt create mode 100644 views/accounts/metadata.json create mode 100644 views/accounts/qml/View.qml create mode 100644 views/accounts/tests/tst_accountsview.qml create mode 100644 views/composer/metadata.json create mode 100644 views/composer/qml/AddresseeListEditor.qml create mode 100644 views/composer/qml/View.qml create mode 100644 views/composer/tests/tst_composerview.qml create mode 100644 views/conversation/metadata.json create mode 100644 views/conversation/qml/View.qml create mode 100644 views/conversation/tests/tst_conversationview.qml create mode 100644 views/log/metadata.json create mode 100644 views/log/qml/View.qml create mode 100644 views/log/tests/tst_logview.qml create mode 100644 views/people/metadata.json create mode 100644 views/people/qml/View.qml create mode 100644 views/people/tests/tst_peopleview.qml (limited to 'views') diff --git a/views/CMakeLists.txt b/views/CMakeLists.txt new file mode 100644 index 00000000..4004e310 --- /dev/null +++ b/views/CMakeLists.txt @@ -0,0 +1,11 @@ +macro(install_view name) + install(DIRECTORY ${name}/qml/ DESTINATION ${QML_INSTALL_DIR}/org/kube/views/${name}) + install(FILES ${name}/metadata.json DESTINATION ${QML_INSTALL_DIR}/org/kube/views/${name}) + add_test(NAME viewtest-${name} COMMAND kubetestrunner WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/${name}/tests) +endmacro() + +install_view(composer) +install_view(conversation) +install_view(people) +install_view(log) +install_view(accounts) diff --git a/views/accounts/metadata.json b/views/accounts/metadata.json new file mode 100644 index 00000000..55091f97 --- /dev/null +++ b/views/accounts/metadata.json @@ -0,0 +1,4 @@ +{ + "tooltip": "Account configuration.", + "hidden": true +} diff --git a/views/accounts/qml/View.qml b/views/accounts/qml/View.qml new file mode 100644 index 00000000..9b774907 --- /dev/null +++ b/views/accounts/qml/View.qml @@ -0,0 +1,143 @@ +/* + * 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. + */ + +import QtQuick 2.4 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 1.3 as Controls +import QtQuick.Controls 2.0 +import org.kube.framework 1.0 as Kube +import org.kube.components.accounts 1.0 as KubeAccounts + +FocusScope { + id: root + //Defines whether more than one account is supported. + property bool singleAccountMode: false + //Defines available account types. + property var availableAccountPlugins: ["kolabnow", "imap", "maildir", "gmail"] + + Controls.SplitView { + height: parent.height + width: parent.width + + Item { + id: accountList + width: Kube.Units.gridUnit * 12 + Layout.fillHeight: true + visible: !root.singleAccountMode + + Kube.PositiveButton { + id: newAccountButton + anchors { + top: parent.top + left: parent.left + right: parent.right + margins: Kube.Units.largeSpacing + } + text: qsTr("New Account") + + onClicked: accountWizard.open() + } + + Kube.ListView { + id: listView + + anchors { + top: newAccountButton.bottom + left: parent.left + right: parent.right + bottom: parent.bottom + topMargin: Kube.Units.largeSpacing + } + + clip: true + + model: Kube.AccountsModel {} + + onCurrentItemChanged: { + if (currentItem) { + edit.accountId = currentItem.currentData.accountId + } + } + + delegate: Kube.ListDelegate { + id: delegateRoot + + Kube.Label { + anchors { + verticalCenter: parent.verticalCenter + left: parent.left + leftMargin: Kube.Units.largeSpacing + } + width: parent.width - Kube.Units.largeSpacing * 2 + + text: model.name + color: delegateRoot.textColor + elide: Text.ElideRight + } + } + } + } + + Item { + height: parent.height + width: Kube.Units.gridUnit * 20 + Layout.fillWidth: true + + Kube.EditAccount { + id: edit + anchors { + fill: parent + bottomMargin: Kube.Units.largeSpacing + } + + canRemove: !root.singleAccountMode + + Component.onCompleted: { + //We don't have any accounts setup if accountId is empty, so we trigger the accountWizard + //FIXME: this assumes we load accounts synchronously, which we do right now. + if (accountId == "") { + //Require the setup to be completed since it's the first account + accountWizard.requireSetup = true + //Launch account wizard + accountWizard.open() + } + } + } + } + } + + onActiveFocusChanged: { + if (activeFocus && accountWizard.visible) { + accountWizard.forceActiveFocus() + } + } + + //BEGIN AccountWizard + KubeAccounts.AccountWizard { + id: accountWizard + + parent: ApplicationWindow.overlay + height: app.height + width: app.width - app.sidebarWidth + x: app.sidebarWidth + y: 0 + + availableAccountPlugins: root.availableAccountPlugins + } + //END AccountWizard +} diff --git a/views/accounts/tests/tst_accountsview.qml b/views/accounts/tests/tst_accountsview.qml new file mode 100644 index 00000000..096ca9eb --- /dev/null +++ b/views/accounts/tests/tst_accountsview.qml @@ -0,0 +1,37 @@ +/* + * Copyright 2017 Christian Mollekopf + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, 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 Library General Public License for more details + * + * You should have received a copy of the GNU Library 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 QtTest 1.0 +import "../qml" + +TestCase { + id: testCase + width: 400 + height: 400 + name: "AccountsView" + + View { + id: accountsView + } + + function test_start() { + verify(accountsView) + } +} diff --git a/views/composer/metadata.json b/views/composer/metadata.json new file mode 100644 index 00000000..c4654fba --- /dev/null +++ b/views/composer/metadata.json @@ -0,0 +1,4 @@ +{ + "icon": "document-edit-inverted", + "tooltip": "Compose new messages." +} diff --git a/views/composer/qml/AddresseeListEditor.qml b/views/composer/qml/AddresseeListEditor.qml new file mode 100644 index 00000000..8f9862e7 --- /dev/null +++ b/views/composer/qml/AddresseeListEditor.qml @@ -0,0 +1,138 @@ +/* + * 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.Controls 1.3 +import QtQuick.Layouts 1.1 + +import org.kube.framework 1.0 as Kube + +FocusScope { + id: root + property variant controller + property variant completer + property bool encrypt: false + + implicitHeight: listView.height + lineEdit.height + height: implicitHeight + + Column { + anchors.fill: parent + + spacing: Kube.Units.smallSpacing + + ListView { + id: listView + anchors { + left: parent.left + right: parent.right + } + height: contentHeight + spacing: Kube.Units.smallSpacing + model: controller.model + delegate: Rectangle { + height: Kube.Units.gridUnit + Kube.Units.smallSpacing * 2 //smallSpacing for padding + width: parent.width + color: Kube.Colors.buttonColor + Row { + anchors { + top: parent.top + bottom: parent.bottom + left: parent.left + right: removeButton.left + margins: Kube.Units.smallSpacing + } + spacing: Kube.Units.smallSpacing + Kube.Label { + id: label + anchors { + top: parent.top + } + text: model.name + elide: Text.ElideRight + } + Kube.Icon { + anchors { + top: parent.top + } + height: Kube.Units.gridUnit + width: height + visible: root.encrypt + iconName: model.keyFound ? Kube.Icons.secure: Kube.Icons.insecure + } + } + Kube.IconButton { + id: removeButton + anchors { + right: parent.right + verticalCenter: parent.verticalCenter + margins: Kube.Units.smallSpacing + } + height: Kube.Units.gridUnit + width: height + onClicked: root.controller.remove(model.id) + padding: 0 + iconName: Kube.Icons.remove + } + } + } + + FocusScope { + height: Kube.Units.gridUnit * Kube.Units.smallSpacing * 2 + width: parent.width + focus: true + + Kube.TextButton { + id: button + text: "+ " + qsTr("Add recipient") + textColor: Kube.Colors.highlightColor + focus: true + onClicked: { + lineEdit.visible = true + lineEdit.forceActiveFocus() + } + } + + Kube.AutocompleteLineEdit { + id: lineEdit + anchors { + left: parent.left + right: parent.right + } + visible: false + + placeholderText: "+ " + qsTr("Add recipient") + model: root.completer.model + onSearchTermChanged: root.completer.searchString = searchTerm + onAccepted: { + root.controller.add({name: text}); + clear() + visible = false + button.forceActiveFocus() + } + onAborted: { + clear() + visible = false + button.forceActiveFocus() + } + } + } + } +} diff --git a/views/composer/qml/View.qml b/views/composer/qml/View.qml new file mode 100644 index 00000000..1eb88bb4 --- /dev/null +++ b/views/composer/qml/View.qml @@ -0,0 +1,533 @@ +/* + * 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.Controls 1.3 +import QtQuick.Controls 2.0 as Controls2 +import QtQuick.Layouts 1.1 +import QtQuick.Dialogs 1.0 as Dialogs + +import org.kube.framework 1.0 as Kube + +Kube.View { + id: root + + property bool newMessage: false + property bool loadAsDraft: false + property variant message: {} + property variant recipients: [] + + resources: [ + Kube.ComposerController { + id: composerController + htmlBody: html.checked + sign: signCheckbox.checked + encrypt: encryptCheckbox.checked + onDone: Kube.Fabric.postMessage(Kube.Messages.componentDone, {}) + + property bool foundAllKeys: to.foundAllKeys && cc.foundAllKeys && bcc.foundAllKeys + + sendAction.enabled: composerController.accountId && composerController.subject && (!composerController.encrypt || composerController.foundAllKeys) && (!composerController.sign && !composerController.encrypt || composerController.foundPersonalKeys) + saveAsDraftAction.enabled: composerController.accountId + } + ] + + Component.onCompleted: loadMessage(root.message, root.loadAsDraft) + + Controls2.StackView.onActivated: { + Kube.Fabric.postMessage(Kube.Messages.synchronize, {"type": "mail", "specialPurpose": "drafts"}) + //For autocompletion + Kube.Fabric.postMessage(Kube.Messages.synchronize, {"type": "contacts"}) + } + + function loadMessage(message, loadAsDraft) { + if (message) { + composerController.loadMessage(message, loadAsDraft) + //Forward focus for replies directly + if (!loadAsDraft) { + subject.forceActiveFocus() + } + } else if (newMessage) { + composerController.clear() + if (root.recipients) { + for (var i = 0; i < root.recipients.length; ++i) { + composerController.to.add({name: root.recipients[i]}) + } + } + subject.forceActiveFocus() + } + } + + function closeFirstSplitIfNecessary() { + //Move the view forward + if (root.currentIndex == 0) { + root.incrementCurrentIndex() + } + } + + //Drafts + Rectangle { + + anchors { + top: parent.top + bottom: parent.bottom + } + + width: Kube.Units.gridUnit * 10 + Layout.minimumWidth: Kube.Units.gridUnit * 5 + + color: Kube.Colors.textColor + + ColumnLayout { + + anchors { + fill: parent + margins: Kube.Units.largeSpacing + } + + spacing: Kube.Units.largeSpacing + + Kube.PositiveButton { + objectName: "newMailButton" + anchors { + left: parent.left + right: parent.right + } + focus: true + text: qsTr("New Email") + onClicked: { + listView.currentIndex = -1 + composerController.clear() + subject.forceActiveFocus() + } + } + + Kube.Label{ + text: qsTr("Drafts") + color: Kube.Colors.highlightedTextColor + } + + Kube.ListView { + id: listView + activeFocusOnTab: true + + anchors { + left: parent.left + right: parent.right + } + + Layout.fillHeight: true + clip: true + currentIndex: -1 + highlightFollowsCurrentItem: false + + //BEGIN keyboard nav + onActiveFocusChanged: { + if (activeFocus && currentIndex < 0) { + currentIndex = 0 + } + } + Keys.onDownPressed: { + listView.incrementCurrentIndex() + } + Keys.onUpPressed: { + listView.decrementCurrentIndex() + } + //END keyboard nav + + onCurrentItemChanged: { + if (currentItem) { + root.loadMessage(currentItem.currentData.domainObject, true) + } + } + + model: Kube.MailListModel { + id: mailListModel + showDrafts: true + } + + delegate: Kube.ListDelegate { + id: delegateRoot + + color: Kube.Colors.textColor + border.width: 0 + + Item { + id: content + + anchors { + fill: parent + margins: Kube.Units.smallSpacing + } + + Kube.Label { + width: content.width + text: model.subject == "" ? "no subject" : model.subject + color: Kube.Colors.highlightedTextColor + maximumLineCount: 2 + wrapMode: Text.WrapAnywhere + elide: Text.ElideRight + } + + Kube.Label { + anchors { + right: parent.right + bottom: parent.bottom + } + text: Qt.formatDateTime(model.date, "dd MMM yyyy") + font.italic: true + color: Kube.Colors.disabledTextColor + font.pointSize: Kube.Units.smallFontSize + visible: !delegateRoot.hovered + } + } + Row { + id: buttons + + anchors { + right: parent.right + bottom: parent.bottom + margins: Kube.Units.smallSpacing + } + + visible: delegateRoot.hovered + spacing: Kube.Units.smallSpacing + opacity: 0.7 + + Kube.IconButton { + id: deleteButton + activeFocusOnTab: true + iconName: Kube.Icons.moveToTrash + visible: enabled + enabled: !!model.mail + onClicked: Kube.Fabric.postMessage(Kube.Messages.moveToTrash, {"mail": model.mail}) + } + } + } + } + } + } + + //Content + Rectangle { + Layout.fillWidth: true + Layout.minimumWidth: Kube.Units.gridUnit * 5 + anchors { + top: parent.top + bottom: parent.bottom + } + color: Kube.Colors.backgroundColor + + ColumnLayout { + anchors { + fill: parent + margins: Kube.Units.largeSpacing + leftMargin: Kube.Units.largeSpacing + Kube.Units.gridUnit * 2 + rightMargin: Kube.Units.largeSpacing + Kube.Units.gridUnit * 2 + } + + spacing: Kube.Units.smallSpacing + + Kube.TextField { + id: subject + Layout.fillWidth: true + activeFocusOnTab: true + + placeholderText: qsTr("Enter Subject...") + text: composerController.subject + onTextChanged: composerController.subject = text; + onActiveFocusChanged: { + if (activeFocus) { + closeFirstSplitIfNecessary() + } + } + } + + Flow { + id: attachments + + Layout.fillWidth: true + layoutDirection: Qt.RightToLeft + spacing: Kube.Units.smallSpacing + clip: true + + Repeater { + model: composerController.attachments.model + delegate: Kube.AttachmentDelegate { + name: model.filename + icon: model.iconname + clip: true + actionIcon: Kube.Icons.remove + onExecute: composerController.attachments.remove(model.id) + } + } + } + + RowLayout { + + spacing: Kube.Units.largeSpacing + + Kube.Switch { + id: html + text: checked ? qsTr("plain") : qsTr("html") + focusPolicy: Qt.TabFocus + focus: false + checked: composerController.htmlBody + } + + Row { + visible: html.checked + spacing: 1 + + Kube.IconButton { + iconName: Kube.Icons.bold + checkable: true + checked: textEditor.bold + onClicked: textEditor.bold = !textEditor.bold + focusPolicy: Qt.TabFocus + focus: false + } + Kube.IconButton { + iconName: Kube.Icons.italic + checkable: true + checked: textEditor.italic + onClicked: textEditor.italic = !textEditor.italic + focusPolicy: Qt.TabFocus + focus: false + } + Kube.IconButton { + iconName: Kube.Icons.underline + checkable: true + checked: textEditor.underline + onClicked: textEditor.underline = !textEditor.underline + focusPolicy: Qt.TabFocus + focus: false + } + } + + Item { + height: 1 + Layout.fillWidth: true + } + + Kube.Button { + text: qsTr("Attach file") + + onClicked: { + fileDialogComponent.createObject(parent) + } + + Component { + id: fileDialogComponent + Dialogs.FileDialog { + id: fileDialog + visible: true + title: "Choose a file to attach" + selectFolder: false + onAccepted: { + composerController.attachments.add({url: fileDialog.fileUrl}) + } + } + } + } + } + + Kube.TextEditor { + id: textEditor + + Layout.fillWidth: true + Layout.fillHeight: true + htmlEnabled: html.checked + + onActiveFocusChanged: closeFirstSplitIfNecessary() + Keys.onEscapePressed: recipients.forceActiveFocus() + initialText: composerController.body + onTextChanged: composerController.body = text; + } + } + } + + //Recepients + FocusScope { + id: recipients + anchors { + top: parent.top + bottom: parent.bottom + } + width: Kube.Units.gridUnit * 15 + activeFocusOnTab: true + + //background + Rectangle { + anchors.fill: parent + color: Kube.Colors.backgroundColor + + Rectangle { + height: parent.height + width: 1 + color: Kube.Colors.buttonColor + } + } + + //Content + ColumnLayout { + anchors { + fill: parent + margins: Kube.Units.largeSpacing + } + + spacing: Kube.Units.largeSpacing + ColumnLayout { + Layout.maximumWidth: parent.width + Layout.fillWidth: true + Layout.fillHeight: true + + Kube.Label { + text: qsTr("Sending Email to:") + } + + AddresseeListEditor { + Layout.preferredHeight: implicitHeight + Layout.fillWidth: true + focus: true + activeFocusOnTab: true + encrypt: composerController.encrypt + controller: composerController.to + completer: composerController.recipientCompleter + } + + Kube.Label { + text: qsTr("Sending Copy to (CC):") + } + AddresseeListEditor { + id: cc + Layout.preferredHeight: cc.implicitHeight + Layout.fillWidth: true + activeFocusOnTab: true + encrypt: composerController.encrypt + controller: composerController.cc + completer: composerController.recipientCompleter + } + + Kube.Label { + text: qsTr("Sending Secret Copy to (Bcc):") + } + AddresseeListEditor { + id: bcc + Layout.preferredHeight: bcc.implicitHeight + Layout.fillWidth: true + activeFocusOnTab: true + encrypt: composerController.encrypt + controller: composerController.bcc + completer: composerController.recipientCompleter + } + Item { + width: parent.width + Layout.fillHeight: true + } + } + + RowLayout { + enabled: composerController.foundPersonalKeys + Kube.CheckBox { + id: encryptCheckbox + checked: composerController.encrypt + } + Kube.Label { + text: qsTr("encrypt") + } + } + + RowLayout { + enabled: composerController.foundPersonalKeys + Kube.CheckBox { + id: signCheckbox + checked: composerController.sign + } + Kube.Label { + text: qsTr("sign") + } + } + Kube.Label { + visible: !composerController.foundPersonalKeys + Layout.maximumWidth: parent.width + text: qsTr("Encryption is not available because your personal key has not been found.") + wrapMode: Text.Wrap + } + + RowLayout { + Layout.maximumWidth: parent.width + width: parent.width + height: Kube.Units.gridUnit + + Kube.Button { + width: saveDraftButton.width + text: qsTr("Discard") + onClicked: Kube.Fabric.postMessage(Kube.Messages.componentDone, {}) + } + + Kube.Button { + id: saveDraftButton + + text: qsTr("Save as Draft") + enabled: composerController.saveAsDraftAction.enabled + onClicked: { + composerController.saveAsDraftAction.execute() + } + } + } + + ColumnLayout { + Layout.maximumWidth: parent.width + Layout.fillWidth: true + Kube.Label { + id: fromLabel + text: qsTr("You are sending this from:") + } + + Kube.ComboBox { + id: identityCombo + + width: parent.width - Kube.Units.largeSpacing * 2 + + model: composerController.identitySelector.model + textRole: "address" + Layout.fillWidth: true + onCurrentIndexChanged: { + composerController.identitySelector.currentIndex = currentIndex + } + } + } + + Kube.PositiveButton { + objectName: "sendButton" + id: sendButton + + width: parent.width + + text: qsTr("Send") + enabled: composerController.sendAction.enabled + onClicked: { + composerController.sendAction.execute() + } + } + } + }//FocusScope +} diff --git a/views/composer/tests/tst_composerview.qml b/views/composer/tests/tst_composerview.qml new file mode 100644 index 00000000..eac391c1 --- /dev/null +++ b/views/composer/tests/tst_composerview.qml @@ -0,0 +1,97 @@ +/* + * Copyright 2017 Christian Mollekopf + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, 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 Library General Public License for more details + * + * You should have received a copy of the GNU Library 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 QtTest 1.0 +import "../qml" +import org.kube.framework 1.0 as Kube +import org.kube.test 1.0 + +TestCase { + id: testCase + width: 400 + height: 400 + name: "ComposerView" + when: windowShown + + Component { + id:composerComponent + View { + focus: true + } + } + + function test_1start() { + var composer = createTemporaryObject(composerComponent, testCase, {}) + verify(composer) + } + + function test_2verifyInitialFocus() { + var composer = createTemporaryObject(composerComponent, testCase, {}) + var newMailButton = findChild(composer, "newMailButton"); + verify(newMailButton) + verify(newMailButton.activeFocus) + } + + function test_3sendMessage() { + var initialState = { + accounts: [{ + id: "account1", + }], + identities: [{ + account: "account1", + name: "Test Identity", + address: "identity@example.org" + }], + resources: [{ + id: "resource1", + account: "account1", + type: "dummy" + }, + { + id: "resource2", + account: "account1", + type: "mailtransport" + }], + mails:[{ + resource: "resource1", + subject: "subject", + body: "body", + to: ["to@example.org"], + cc: ["cc@example.org"], + bcc: ["bcc@example.org"], + draft: true + }] + } + TestStore.setup(initialState) + var composer = createTemporaryObject(composerComponent, testCase, {}) + + var createdMail = TestStore.load("mail", {resource: "resource1"}) + + var loadAsDraft = true + composer.loadMessage(createdMail, loadAsDraft) + var sendMailButton = findChild(composer, "sendButton") + verify(sendMailButton) + tryVerify(function(){ return sendMailButton.enabled }) + sendMailButton.clicked() + + tryVerify(function(){ return TestStore.load("mail", {resource: "resource2"}) }) + tryVerify(function(){ return !TestStore.load("mail", {resource: "resource1"}) }) + } +} diff --git a/views/conversation/metadata.json b/views/conversation/metadata.json new file mode 100644 index 00000000..870ff2aa --- /dev/null +++ b/views/conversation/metadata.json @@ -0,0 +1,4 @@ +{ + "icon": "mail-message-inverted", + "tooltip": "Follow conversations." +} diff --git a/views/conversation/qml/View.qml b/views/conversation/qml/View.qml new file mode 100644 index 00000000..8b2b0caf --- /dev/null +++ b/views/conversation/qml/View.qml @@ -0,0 +1,120 @@ +/* + * 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.Controls 1.3 +import QtQuick.Controls 2.0 as Controls2 +import QtQuick.Layouts 1.1 + +import org.kube.framework 1.0 as Kube + +FocusScope { + SplitView { + anchors.fill: parent + Rectangle { + width: Kube.Units.gridUnit * 10 + Layout.fillHeight: parent.height + color: Kube.Colors.textColor + + Kube.PositiveButton { + id: newMailButton + + anchors { + top: parent.top + left: parent.left + right: parent.right + margins: Kube.Units.largeSpacing + } + focus: true + text: qsTr("New Email") + onClicked: Kube.Fabric.postMessage(Kube.Messages.compose, {}) + } + + Kube.InlineAccountSwitcher { + id: accountFolderview + activeFocusOnTab: true + anchors { + top: newMailButton.bottom + topMargin: Kube.Units.largeSpacing + bottom: statusBarContainer.top + left: newMailButton.left + right: parent.right + } + } + + Item { + id: statusBarContainer + anchors { + topMargin: Kube.Units.smallSpacing + bottom: parent.bottom + left: parent.left + right: parent.right + } + height: childrenRect.height + + Rectangle { + id: border + visible: statusBar.visible + anchors { + right: parent.right + left: parent.left + margins: Kube.Units.smallSpacing + } + height: 1 + color: Kube.Colors.viewBackgroundColor + opacity: 0.3 + } + Kube.StatusBar { + id: statusBar + accountId: accountFolderview.currentAccount + height: Kube.Units.gridUnit * 2 + anchors { + top: border.bottom + left: statusBarContainer.left + right: statusBarContainer.right + } + } + } + } + + Rectangle { + width: Kube.Units.gridUnit * 18 + Layout.fillHeight: parent.height + + color: "transparent" + border.width: 1 + border.color: Kube.Colors.buttonColor + + Kube.MailListView { + id: mailListView + anchors.fill: parent + activeFocusOnTab: true + Layout.minimumWidth: Kube.Units.gridUnit * 10 + } + } + + Kube.ConversationView { + id: mailView + Layout.fillWidth: true + Layout.fillHeight: parent.height + activeFocusOnTab: true + } + } +} diff --git a/views/conversation/tests/tst_conversationview.qml b/views/conversation/tests/tst_conversationview.qml new file mode 100644 index 00000000..467c049a --- /dev/null +++ b/views/conversation/tests/tst_conversationview.qml @@ -0,0 +1,37 @@ +/* + * Copyright 2017 Christian Mollekopf + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, 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 Library General Public License for more details + * + * You should have received a copy of the GNU Library 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 QtTest 1.0 +import "../qml" + +TestCase { + id: testCase + width: 400 + height: 400 + name: "MailView" + + View { + id: mailView + } + + function test_start() { + verify(mailView) + } +} diff --git a/views/log/metadata.json b/views/log/metadata.json new file mode 100644 index 00000000..1bcba1fb --- /dev/null +++ b/views/log/metadata.json @@ -0,0 +1,4 @@ +{ + "tooltip": "Log messages.", + "hidden": true +} diff --git a/views/log/qml/View.qml b/views/log/qml/View.qml new file mode 100644 index 00000000..4ae1a67c --- /dev/null +++ b/views/log/qml/View.qml @@ -0,0 +1,329 @@ +/* + * 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 Controls +import QtQuick.Controls 2.0 as Controls2 +import org.kube.framework 1.0 as Kube + +Controls.SplitView { + id: root + + property bool pendingError: false; + + 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: { + if (message.type == Kube.Notifications.error) { + root.pendingError = true + } + var error = {timestamp: new Date(), message: message.message, details: message.details, resource: message.resource} + 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 = "" + } + } + + 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: "" + + 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 : "" + + function getComponent(subtype) { + if (subtype == Kube.Notifications.loginError) { + return loginErrorComponent + } + if (subtype == Kube.Notifications.hostNotFoundError) { + return hostNotFoundErrorComponent + } + if (subtype == Kube.Notifications.connectionError) { + return hostNotFoundErrorComponent + } + 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, {}) + } + } + } + } + } +} diff --git a/views/log/tests/tst_logview.qml b/views/log/tests/tst_logview.qml new file mode 100644 index 00000000..a78d71cb --- /dev/null +++ b/views/log/tests/tst_logview.qml @@ -0,0 +1,65 @@ +/* + * Copyright 2017 Christian Mollekopf + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, 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 Library General Public License for more details + * + * You should have received a copy of the GNU Library 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 +import QtQuick.Window 2.1 +import QtTest 1.0 +import org.kube.framework 1.0 as Kube +import "../qml" + +TestCase { + id: logviewTestcase + width: 400 + height: 400 + name: "LogView" + + View { + id: logView + } + + function test_logview() { + var listModel = findChild(logView, "logModel"); + verify(listModel) + compare(listModel.count, 0) + //ignore progress + Kube.Fabric.postMessage(Kube.Messages.progressNotification, {}) + compare(listModel.count, 0) + + Kube.Fabric.postMessage(Kube.Messages.notification, {type: Kube.Notifications.info, message: "foobar", resource: "resource"}) + compare(listModel.count, 1) + compare(logView.pendingError, false) + + Kube.Fabric.postMessage(Kube.Messages.notification, {"type": Kube.Notifications.error, message: "foobar", resource: "resource"}) + compare(listModel.count, 2) + compare(logView.pendingError, true) + compare(listModel.get(0).type, Kube.Notifications.error) + compare(listModel.get(0).errors.count, 1) + compare(listModel.get(0).errors.get(0).message, "foobar") + compare(listModel.get(0).errors.get(0).resource, "resource") + + Kube.Fabric.postMessage(Kube.Messages.notification, {"type": Kube.Notifications.error, "subtype": "merge", message: "merge1", resource: "resource1"}) + compare(listModel.count, 3) + Kube.Fabric.postMessage(Kube.Messages.notification, {"type": Kube.Notifications.error, "subtype": "merge", message: "merge2", resource: "resource2"}) + compare(listModel.count, 3) + compare(listModel.get(0).errors.count, 2) + compare(listModel.get(0).errors.get(0).message, "merge2") + compare(listModel.get(0).errors.get(0).resource, "resource2") + } +} diff --git a/views/people/metadata.json b/views/people/metadata.json new file mode 100644 index 00000000..44296a7e --- /dev/null +++ b/views/people/metadata.json @@ -0,0 +1,4 @@ +{ + "icon": "im-user-inverted", + "tooltip": "Your contacts." +} diff --git a/views/people/qml/View.qml b/views/people/qml/View.qml new file mode 100644 index 00000000..3f1b9261 --- /dev/null +++ b/views/people/qml/View.qml @@ -0,0 +1,38 @@ +/* + * 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.Controls 2.1 +import org.kube.framework 1.0 as Kube + +Item { + + StackView.onActivated: { + Kube.Fabric.postMessage(Kube.Messages.synchronize, {"type": "contacts"}) + } + + Kube.People { + id: people + anchors { + fill: parent + margins: Kube.Units.smallSpacing + } + } +} diff --git a/views/people/tests/tst_peopleview.qml b/views/people/tests/tst_peopleview.qml new file mode 100644 index 00000000..263247ad --- /dev/null +++ b/views/people/tests/tst_peopleview.qml @@ -0,0 +1,37 @@ +/* + * Copyright 2017 Christian Mollekopf + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, 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 Library General Public License for more details + * + * You should have received a copy of the GNU Library 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 QtTest 1.0 +import "../qml" + +TestCase { + id: testCase + width: 400 + height: 400 + name: "PeopleView" + + View { + id: peopleView + } + + function test_start() { + verify(peopleView) + } +} -- cgit v1.2.3