From 0a9748a15d530725c88b6aa3ea775b98151a7532 Mon Sep 17 00:00:00 2001 From: Alexander Bilyak Date: Wed, 4 Aug 2021 14:00:31 +0200 Subject: [PATCH] GODT-22: Facelift - GODT-1199: Add menu to status window - GODT-22: use ColorImage instead of IconLabel - GODT-22: remove banners from MainWindow - GODT-1199: Fix separator width - GODT-1199: Fix StatusWindow button position - GODT-1198: Open main window on startup if no users - GODT-1199: Fix avatar text color - GODT-1198: refactor main window layout - GODT-22: add missing components to qmldir - GODT-22: refactor components having Layout as root item - GODT-22: add more user controls - GODT-1199: Add status window resize and maximum height - GODT-22: WIP: notification arch - GODT-22: Notifications WIP - GODT-22: Fix notification filter, topmost notification - GODT-1199: Add notifications to status window - GODT-22: Add strict typization to colorScheme variable - GODT-1198: WIP Notifications, dialogs and banners - GODT-22: Add backend notifications (Banners & Dialogs) D --- internal/frontend/qml/AccountDelegate.qml | 75 +-- internal/frontend/qml/AccountView.qml | 38 +- internal/frontend/qml/Banner.qml | 226 +++++++++ internal/frontend/qml/Banners.qml | 121 ----- internal/frontend/qml/Bridge.qml | 159 ++++++- .../frontend/qml/BridgeTest/UserControl.qml | 77 +++- internal/frontend/qml/BridgeTest/UserList.qml | 17 +- internal/frontend/qml/Bridge_test.qml | 197 +++++++- internal/frontend/qml/ContentWrapper.qml | 46 +- internal/frontend/qml/DebugWrapper.qml | 6 +- internal/frontend/qml/MainWindow.qml | 129 ++++-- internal/frontend/qml/NotificationDialog.qml | 117 +++++ internal/frontend/qml/NotificationPopups.qml | 91 ++++ .../qml/Notifications/Notification.qml | 49 ++ .../qml/Notifications/NotificationFilter.qml | 114 +++++ .../qml/Notifications/Notifications.qml | 428 ++++++++++++++++++ internal/frontend/qml/Notifications/qmldir | 6 + .../frontend/qml/Proton/ApplicationWindow.qml | 136 ++++++ internal/frontend/qml/Proton/Banner.qml | 117 ----- internal/frontend/qml/Proton/Button.qml | 54 ++- internal/frontend/qml/Proton/CheckBox.qml | 34 +- internal/frontend/qml/Proton/Dialog.qml | 82 ++++ internal/frontend/qml/Proton/Label.qml | 138 ++++++ internal/frontend/qml/Proton/Menu.qml | 68 +++ internal/frontend/qml/Proton/MenuItem.qml | 72 +++ internal/frontend/qml/Proton/Popup.qml | 67 +++ internal/frontend/qml/Proton/ProtonLabel.qml | 43 -- internal/frontend/qml/Proton/RadioButton.qml | 34 +- internal/frontend/qml/Proton/Style.qml | 29 +- internal/frontend/qml/Proton/Switch.qml | 28 +- internal/frontend/qml/Proton/TextArea.qml | 56 +-- internal/frontend/qml/Proton/TextField.qml | 81 ++-- internal/frontend/qml/Proton/qmldir | 29 +- internal/frontend/qml/SetupGuide.qml | 139 +++--- internal/frontend/qml/SignIn.qml | 49 +- internal/frontend/qml/Status.qml | 167 ++++++- internal/frontend/qml/StatusWindow.qml | 300 ++++++++++++ internal/frontend/qml/WelcomeGuide.qml | 294 ++++++++++++ internal/frontend/qml/WelcomeWindow.qml | 297 ------------ internal/frontend/qml/icons/ic-alert.svg | 11 + .../frontend/qml/icons/ic-no-connection.svg | 3 + internal/frontend/qml/icons/ic-success.svg | 10 + .../qml/icons/ic-three-dots-vertical.svg | 9 + internal/frontend/qml/tests/Buttons.qml | 6 +- internal/frontend/qml/tests/ButtonsColumn.qml | 5 +- internal/frontend/qml/tests/CheckBoxes.qml | 5 +- internal/frontend/qml/tests/RadioButtons.qml | 16 +- internal/frontend/qml/tests/Switches.qml | 4 +- .../frontend/qml/tests/TestComponents.qml | 9 +- internal/frontend/qml/tests/TextAreas.qml | 8 +- internal/frontend/qml/tests/TextFields.qml | 37 +- 51 files changed, 3277 insertions(+), 1056 deletions(-) create mode 100644 internal/frontend/qml/Banner.qml delete mode 100644 internal/frontend/qml/Banners.qml create mode 100644 internal/frontend/qml/NotificationDialog.qml create mode 100644 internal/frontend/qml/NotificationPopups.qml create mode 100644 internal/frontend/qml/Notifications/Notification.qml create mode 100644 internal/frontend/qml/Notifications/NotificationFilter.qml create mode 100644 internal/frontend/qml/Notifications/Notifications.qml create mode 100644 internal/frontend/qml/Notifications/qmldir create mode 100644 internal/frontend/qml/Proton/ApplicationWindow.qml delete mode 100644 internal/frontend/qml/Proton/Banner.qml create mode 100644 internal/frontend/qml/Proton/Dialog.qml create mode 100644 internal/frontend/qml/Proton/Label.qml create mode 100644 internal/frontend/qml/Proton/Menu.qml create mode 100644 internal/frontend/qml/Proton/MenuItem.qml create mode 100644 internal/frontend/qml/Proton/Popup.qml delete mode 100644 internal/frontend/qml/Proton/ProtonLabel.qml create mode 100644 internal/frontend/qml/StatusWindow.qml create mode 100644 internal/frontend/qml/WelcomeGuide.qml delete mode 100644 internal/frontend/qml/WelcomeWindow.qml create mode 100644 internal/frontend/qml/icons/ic-alert.svg create mode 100644 internal/frontend/qml/icons/ic-no-connection.svg create mode 100644 internal/frontend/qml/icons/ic-success.svg create mode 100644 internal/frontend/qml/icons/ic-three-dots-vertical.svg diff --git a/internal/frontend/qml/AccountDelegate.qml b/internal/frontend/qml/AccountDelegate.qml index 58ca0443..4782e2ef 100644 --- a/internal/frontend/qml/AccountDelegate.qml +++ b/internal/frontend/qml/AccountDelegate.qml @@ -21,49 +21,58 @@ import QtQuick.Controls 2.12 import Proton 4.0 -RowLayout { +Item { id: root - property var colorScheme: parent.colorScheme - property var text: "janedoe@protonmail.com" - property var avatarText: "jd" - property var captionText: "50.5 MB / 20 GB" + property ColorScheme colorScheme + property var user - spacing: 16 + implicitHeight: children[0].implicitHeight + implicitWidth: children[0].implicitWidth - Rectangle { - id: avatar - Layout.preferredHeight: account.height - Layout.preferredWidth: account.height - radius: 4 + RowLayout { + anchors.fill: parent + spacing: 12 - color: root.colorScheme.background_avatar + Rectangle { + id: avatar - ProtonLabel { - anchors.centerIn: avatar - color: root.colorScheme.text_norm - text: root.avatarText.toUpperCase() - state: "body" - horizontalAlignment: Qt.AlignHCenter - verticalAlignment: Qt.AlignVCenter - } - } + Layout.fillHeight: true + Layout.preferredWidth: height - ColumnLayout { - id: account - Layout.fillHeight: true - Layout.fillWidth: true + radius: 4 - ProtonLabel { - text: root.text - color: root.colorScheme.text_norm - state: "body" + color: root.colorScheme.background_avatar + + Label { + colorScheme: root.colorScheme + anchors.fill: parent + text: root.user.avatarText.toUpperCase() + type: Label.LabelType.Body + color: root.colorScheme.text_invert + horizontalAlignment: Qt.AlignHCenter + verticalAlignment: Qt.AlignVCenter + } } - ProtonLabel { - text: root.captionText - color: root.colorScheme.text_weak - state: "caption" + ColumnLayout { + id: account + Layout.fillHeight: true + Layout.fillWidth: true + + spacing: 0 + + Label { + colorScheme: root.colorScheme + text: user.username + type: Label.LabelType.Body + } + + Label { + colorScheme: root.colorScheme + text: user.captionText + type: Label.LabelType.Caption + } } } } diff --git a/internal/frontend/qml/AccountView.qml b/internal/frontend/qml/AccountView.qml index 4e5ed937..66f87424 100644 --- a/internal/frontend/qml/AccountView.qml +++ b/internal/frontend/qml/AccountView.qml @@ -21,28 +21,34 @@ import QtQuick.Controls 2.12 import Proton 4.0 -ColumnLayout { +Item { id: root - property var colorScheme: parent.colorScheme + property ColorScheme colorScheme - spacing: 0 + implicitHeight: children[0].implicitHeight + implicitWidth: children[0].implicitWidth - Rectangle { - Layout.fillWidth: true - Layout.minimumHeight: 277 - Layout.maximumHeight: 277 + ColumnLayout { + anchors.fill: parent + spacing: 0 - color: root.colorScheme.background_norm + Rectangle { + Layout.fillWidth: true + Layout.minimumHeight: 277 + Layout.maximumHeight: 277 - ColumnLayout { + color: root.colorScheme.background_norm + ColumnLayout { + + } + } + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + + color: root.colorScheme.background_weak } } - - Rectangle { - Layout.fillWidth: true - Layout.fillHeight: true - - color: root.colorScheme.background_weak - } } diff --git a/internal/frontend/qml/Banner.qml b/internal/frontend/qml/Banner.qml new file mode 100644 index 00000000..4a6aaea8 --- /dev/null +++ b/internal/frontend/qml/Banner.qml @@ -0,0 +1,226 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge 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 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge 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 ProtonMail Bridge. If not, see . + +import QtQuick 2.13 +import QtQuick.Layouts 1.12 +import QtQuick.Controls 2.12 +import QtQuick.Controls.impl 2.12 + +import Proton 4.0 +import Notifications 1.0 + +Popup { + id: root + + property ColorScheme colorScheme + property Notification notification + + implicitHeight: contentLayout.implicitHeight + contentLayout.anchors.topMargin + contentLayout.anchors.bottomMargin + implicitWidth: contentLayout.implicitWidth + contentLayout.anchors.leftMargin + contentLayout.anchors.rightMargin + + popupType: ApplicationWindow.PopupType.Banner + + shouldShow: notification ? (notification.active && !notification.dismissed) : false + + Action { + id: defaultDismissAction + + text: qsTr("OK") + onTriggered: { + if (!root.notification) { + return + } + + root.notification.dismissed = true + } + } + + RowLayout { + id: contentLayout + anchors.fill: parent + spacing: 0 + + Item { + Layout.fillHeight: true + Layout.fillWidth: true + + clip: true + implicitHeight: children[1].implicitHeight + children[1].anchors.topMargin + children[1].anchors.bottomMargin + implicitWidth: children[1].implicitWidth + children[1].anchors.leftMargin + children[1].anchors.rightMargin + + Rectangle { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: parent.left + width: parent.width + 10 + radius: 10 + color: { + if (!root.notification) { + return "transparent" + } + + switch (root.notification.type) { + case Notification.NotificationType.Info: + return root.colorScheme.signal_info + case Notification.NotificationType.Success: + return root.colorScheme.signal_success + case Notification.NotificationType.Warning: + return root.colorScheme.signal_warning + case Notification.NotificationType.Danger: + return root.colorScheme.signal_danger + } + } + } + + RowLayout { + anchors.fill: parent + anchors.margins: 12 + + spacing: 10 + + ColorImage { + color: root.colorScheme.text_invert + width: 24 + height: 24 + + sourceSize.width: 24 + sourceSize.height: 24 + + Layout.preferredHeight: 24 + Layout.preferredWidth: 24 + + source: { + if (!root.notification) { + return "" + } + + switch (root.notification.type) { + case Notification.NotificationType.Info: + return "./icons/ic-info-circle-filled.svg" + case Notification.NotificationType.Success: + return "./icons/ic-info-circle-filled.svg" + case Notification.NotificationType.Warning: + return "./icons/ic-exclamation-circle-filled.svg" + case Notification.NotificationType.Danger: + return "./icons/ic-exclamation-circle-filled.svg" + } + } + } + + Label { + colorScheme: root.colorScheme + Layout.fillWidth: true + Layout.fillHeight: true + + color: root.colorScheme.text_invert + text: root.notification ? root.notification.text : "" + + wrapMode: Text.WordWrap + + verticalAlignment: Text.AlignVCenter + } + } + } + + Rectangle { + Layout.fillHeight: true + width: 1 + color: { + if (!root.notification) { + return "transparent" + } + + switch (root.notification.type) { + case Notification.NotificationType.Info: + return root.colorScheme.signal_info_active + case Notification.NotificationType.Success: + return root.colorScheme.signal_success_active + case Notification.NotificationType.Warning: + return root.colorScheme.signal_warning_active + case Notification.NotificationType.Danger: + return root.colorScheme.signal_danger_active + } + } + } + + Button { + colorScheme: root.colorScheme + Layout.fillHeight: true + + id: actionButton + + action: (root.notification && root.notification.action.length > 0) ? root.notification.action[0] : defaultDismissAction + + background: Item { + clip: true + Rectangle { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + width: parent.width + 10 + radius: 10 + color: { + if (!root.notification) { + return "transparent" + } + + var norm + var hover + var active + + switch (root.notification.type) { + case Notification.NotificationType.Info: + norm = root.colorScheme.signal_info + hover = root.colorScheme.signal_info_hover + active = root.colorScheme.signal_info_active + break; + case Notification.NotificationType.Success: + norm = root.colorScheme.signal_success + hover = root.colorScheme.signal_success_hover + active = root.colorScheme.signal_success_active + break; + case Notification.NotificationType.Warning: + norm = root.colorScheme.signal_warning + hover = root.colorScheme.signal_warning_hover + active = root.colorScheme.signal_warning_active + break; + case Notification.NotificationType.Danger: + norm = root.colorScheme.signal_danger + hover = root.colorScheme.signal_danger_hover + active = root.colorScheme.signal_danger_active + break; + } + + if (actionButton.down) { + return active + } + + if (actionButton.enabled && (actionButton.highlighted || actionButton.hovered || actionButton.checked)) { + return hover + } + + if (actionButton.loading) { + return hover + } + + return norm + } + } + } + } + } +} diff --git a/internal/frontend/qml/Banners.qml b/internal/frontend/qml/Banners.qml deleted file mode 100644 index 19365014..00000000 --- a/internal/frontend/qml/Banners.qml +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright (c) 2021 Proton Technologies AG -// -// This file is part of ProtonMail Bridge. -// -// ProtonMail Bridge 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 3 of the License, or -// (at your option) any later version. -// -// ProtonMail Bridge 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 ProtonMail Bridge. If not, see . - -import QtQml 2.12 -import QtQuick 2.13 -import Proton 4.0 -import QtQuick.Controls 2.13 - -Rectangle { - id: root - - property var window - - property bool onTop: false - property bool blocking: root.nDangers != 0 - property int nDangers: 0 - - color: root.getTransparentVersion(window.colorScheme.text_norm,root.blocking ? 0.5 : 0) - - MouseArea { - anchors.fill: root - acceptedButtons: root.blocking ? Qt.AllButtons : Qt.NoButton - enabled: root.blocking - } - - ListModel { - id: notifications - } - - ListView { - id: view - anchors.top : root.top - anchors.bottom : root.bottom - anchors.horizontalCenter : root.horizontalCenter - anchors.topMargin : root.height/20 - anchors.bottomMargin : root.height/20 - - layoutDirection: ListView.Vertical - verticalLayoutDirection: root.onTop ? ListView.TopToBottom : ListView.BottomToTop - - spacing: 5 - - model: notifications - delegate: Banner { - id: bannerDelegate - anchors.horizontalCenter: parent.horizontalCenter - text: model.text - actionText: model.buttonText - state: model.state - - onAccepted: { - switch (model.submitAction) { - case "update": - console.log("I am updating now") - break; - default: - console.log("NOOP") - } - if (model.state == "danger") root.nDangers-=1 - anchors.horizontalCenter = undefined - notifications.remove(index) - } - } - } - - function notify(descriptionText, buttonText, type = "info", submitAction = "noop") { - if (type === "danger") root.nDangers+=1 - notifications.append({ - "text": descriptionText, - "buttonText": buttonText, - "state": type, - "submitAction": submitAction - }) - } - - function notifyOnlyPaidUsers(){ - root.notify( - qsTr("Bridge is exclusive to our paid plans. Upgrade your account to use Bridge."), - qsTr("ok"), "danger" - ) - } - - function notifyConnectionLostWhileLogin(){ - root.notify( - qsTr("Can't connect to the server. Check your internet connection and try again."), - qsTr("ok"), "danger" - ) - } - - function notifyUpdateManually(){ - root.notify( - qsTr("Bridge could not update automatically."), - qsTr("update"), "warning", "update" - ) - } - - function notifyUserAdded(){ - root.notify( - qsTr("Your account has been added to Bridge and you are now signed in."), - qsTr("ok"), "success" - ) - } - - function getTransparentVersion(original, transparency){ - return Qt.rgba(original.r, original.g, original.b, transparency) - } -} diff --git a/internal/frontend/qml/Bridge.qml b/internal/frontend/qml/Bridge.qml index 8f729a56..7fba4a98 100644 --- a/internal/frontend/qml/Bridge.qml +++ b/internal/frontend/qml/Bridge.qml @@ -18,25 +18,34 @@ import QtQml 2.12 import QtQuick 2.13 import QtQuick.Window 2.13 -import Qt.labs.platform 1.0 +import Qt.labs.platform 1.1 + +import Notifications 1.0 QtObject { id: root property var backend - property var users signal login(string username, string password) signal login2FA(string username, string code) signal login2Password(string username, string password) signal loginAbort(string username) - property var mainWindow: MainWindow { + property Notifications _notifications: Notifications { + id: notifications + backend: root.backend + frontendMain: mainWindow + frontendStatus: statusWindow + frontendTray: trayIcon + } + + property MainWindow _mainWindow: MainWindow { id: mainWindow - visible: true + visible: false backend: root.backend - users: root.users + notifications: notifications onLogin: { root.login(username, password) @@ -52,38 +61,142 @@ QtObject { } } - property var _trayMenu: Window { - id: trayMenu - title: "window 2" + property StatusWindow _statusWindow: StatusWindow { + id: statusWindow visible: false - flags: Qt.Dialog + + backend: root.backend + notifications: notifications + + onShowMainWindow: { + mainWindow.visible = true + } + onShowHelp: { + + } + onShowSettings: { + + } + onQuit: { + backend.quit() + } } - property var _trayIcon: SystemTrayIcon { + property SystemTrayIcon _trayIcon: SystemTrayIcon { id: trayIcon visible: true iconSource: "./icons/ic-systray.svg" onActivated: { + function calcStatusWindowPosition(statusWidth, statusHeight) { + function bound(num, lower_limit, upper_limit) { + return Math.max(lower_limit, Math.min(upper_limit, num)) + } + // checks if rect1 fits within rect2 + function isRectFit(rect1, rect2) { + //if (rect2.) + if ((rect2.left > rect1.left) || + (rect2.right < rect1.right) || + (rect2.top > rect1.top) || + (rect2.bottom < rect1.bottom)) { + return false + } + + return true + } + + // First we get icon center position. + // On some platforms (X11 / Wayland) Qt does not provide icon geometry info. + // In this case we rely on cursor position + var iconCenter = Qt.point(geometry.x + (geometry.width / 2), geometry.y + (geometry.height / 2)) + + if (geometry.width == 0 && geometry.height == 0) { + iconCenter = backend.getCursorPos() + } + + // Now bound this position to virtual screen available rect + // TODO: here we should detect which screen mouse is on and use that screen available geometry to bound + iconCenter.x = bound(iconCenter.x, 0, Qt.application.screens[0].desktopAvailableWidth) + iconCenter.y = bound(iconCenter.y, 0, Qt.application.screens[0].desktopAvailableHeight) + + var x = 0 + var y = 0 + + // Check if window may fit above + x = iconCenter.x - statusWidth / 2 + y = iconCenter.y - statusHeight + if (isRectFit( + Qt.rect(x, y, statusWidth, statusHeight), + // TODO: we should detect which screen mouse is on and use that screen available geometry to bound + Qt.rect(0, 0, Qt.application.screens[0].desktopAvailableWidth, Qt.application.screens[0].desktopAvailableHeight) + )) { + return Qt.point(x, y) + } + + // Check if window may fit below + x = iconCenter.x - statusWidth / 2 + y = iconCenter.y + if (isRectFit( + Qt.rect(x, y, statusWidth, statusHeight), + // TODO: we should detect which screen mouse is on and use that screen available geometry to bound + Qt.rect(0, 0, Qt.application.screens[0].desktopAvailableWidth, Qt.application.screens[0].desktopAvailableHeight) + )) { + return Qt.point(x, y) + } + + // Check if window may fit to the left + x = iconCenter.x - statusWidth + y = iconCenter.y - statusHeight / 2 + if (isRectFit( + Qt.rect(x, y, statusWidth, statusHeight), + // TODO: we should detect which screen mouse is on and use that screen available geometry to bound + Qt.rect(0, 0, Qt.application.screens[0].desktopAvailableWidth, Qt.application.screens[0].desktopAvailableHeight) + )) { + return Qt.point(x, y) + } + + // Check if window may fit to the right + x = iconCenter.x + y = iconCenter.y - statusHeight / 2 + if (isRectFit( + Qt.rect(x, y, statusWidth, statusHeight), + // TODO: we should detect which screen mouse is on and use that screen available geometry to bound + Qt.rect(0, 0, Qt.application.screens[0].desktopAvailableWidth, Qt.application.screens[0].desktopAvailableHeight) + )) { + return Qt.point(x, y) + } + + // TODO: add fallback + } + switch (reason) { - case SystemTrayIcon.Unknown: + case SystemTrayIcon.Unknown: break; - case SystemTrayIcon.Context: - trayMenu.x = (Screen.desktopAvailableWidth - trayMenu.width) / 2 - trayMenu.visible = !trayMenu.visible + case SystemTrayIcon.Context: + case SystemTrayIcon.Trigger:!statusWindow.visible + if (!statusWindow.visible) { + var point = calcStatusWindowPosition(statusWindow.width, statusWindow.height) + statusWindow.x = point.x + statusWindow.y = point.y + } + statusWindow.visible = !statusWindow.visible break - case SystemTrayIcon.DoubleClick: + case SystemTrayIcon.DoubleClick: + case SystemTrayIcon.MiddleClick: mainWindow.visible = !mainWindow.visible break; - case SystemTrayIcon.Trigger: - trayMenu.x = (Screen.desktopAvailableWidth - trayMenu.width) / 2 - trayMenu.visible = !trayMenu.visible - break; - case SystemTrayIcon.MiddleClick: - mainWindow.visible = !mainWindow.visible - break; - default: + default: break; } } } + + Component.onCompleted: { + if (root.backend.users.count === 0) { + mainWindow.show() + } + + if (root.backend.users.count === 1 && root.backend.users.get(0).loggedIn === false) { + mainWindow.show() + } + } } diff --git a/internal/frontend/qml/BridgeTest/UserControl.qml b/internal/frontend/qml/BridgeTest/UserControl.qml index 826053f9..881f8695 100644 --- a/internal/frontend/qml/BridgeTest/UserControl.qml +++ b/internal/frontend/qml/BridgeTest/UserControl.qml @@ -20,7 +20,11 @@ import QtQuick 2.13 import QtQuick.Layouts 1.12 import QtQuick.Controls 2.13 +import Proton 4.0 + ColumnLayout { + id: root + property var user property var backend @@ -29,9 +33,10 @@ ColumnLayout { Layout.fillHeight: true //Layout.fillWidth: true - property var colorScheme + property ColorScheme colorScheme TextField { + colorScheme: root.colorScheme Layout.fillWidth: true text: user !== undefined ? user.username : "" @@ -41,39 +46,57 @@ ColumnLayout { } } - RowLayout { + ColumnLayout { Layout.fillWidth: true - Button { - //Layout.fillWidth: true + Switch { + id: userLoginSwitch + colorScheme: root.colorScheme - text: "Login" - enabled: user !== undefined && !user.loggedIn && user.username.length > 0 + text: "LoggedIn" + enabled: user !== undefined && user.username.length > 0 - onClicked: { - if (user === backend.loginUser) { - var newUserObject = backend.userComponent.createObject(backend, {username: user.username, loggedIn: true}) - backend.users.append( { object: newUserObject } ) + checked: user ? user.loggedIn : false - user.username = "" - user.resetLoginRequests() + onCheckedChanged: { + if (!user) { return } - user.loggedIn = true - user.resetLoginRequests() + if (checked) { + if (user === backend.loginUser) { + var newUserObject = backend.userComponent.createObject(backend, {username: user.username, loggedIn: true, setupGuideSeen: user.setupGuideSeen}) + backend.users.append( { object: newUserObject } ) + + user.username = "" + user.resetLoginRequests() + return + } + + user.loggedIn = true + user.resetLoginRequests() + return + } else { + user.loggedIn = false + user.resetLoginRequests() + } } } - Button { - //Layout.fillWidth: true + Switch { + colorScheme: root.colorScheme - text: "Logout" - enabled: user !== undefined && user.loggedIn && user.username.length > 0 + text: "Setup guide seen" + enabled: user !== undefined && user.username.length > 0 - onClicked: { - user.loggedIn = false - user.resetLoginRequests() + checked: user ? user.setupGuideSeen : false + + onCheckedChanged: { + if (!user) { + return + } + + user.setupGuideSeen = checked } } } @@ -83,6 +106,7 @@ ColumnLayout { Layout.fillWidth: true Label { + colorScheme: root.colorScheme id: loginLabel text: "Login:" @@ -90,6 +114,7 @@ ColumnLayout { } Button { + colorScheme: root.colorScheme text: "name/pass error" enabled: user !== undefined && user.isLoginRequested && !user.isLogin2FARequested && !user.isLogin2PasswordProvided @@ -100,6 +125,7 @@ ColumnLayout { } Button { + colorScheme: root.colorScheme text: "free user error" enabled: user !== undefined && user.isLoginRequested onClicked: { @@ -109,6 +135,7 @@ ColumnLayout { } Button { + colorScheme: root.colorScheme text: "connection error" enabled: user !== undefined && user.isLoginRequested onClicked: { @@ -122,6 +149,7 @@ ColumnLayout { Layout.fillWidth: true Label { + colorScheme: root.colorScheme id: faLabel text: "2FA:" @@ -129,6 +157,7 @@ ColumnLayout { } Button { + colorScheme: root.colorScheme text: "request" enabled: user !== undefined && user.isLoginRequested && !user.isLogin2FARequested && !user.isLogin2PasswordRequested @@ -139,6 +168,7 @@ ColumnLayout { } Button { + colorScheme: root.colorScheme text: "error" enabled: user !== undefined && user.isLogin2FAProvided && !(user.isLogin2PasswordRequested && !user.isLogin2PasswordProvided) @@ -149,6 +179,7 @@ ColumnLayout { } Button { + colorScheme: root.colorScheme text: "Abort" enabled: user !== undefined && user.isLogin2FAProvided && !(user.isLogin2PasswordRequested && !user.isLogin2PasswordProvided) @@ -163,6 +194,7 @@ ColumnLayout { Layout.fillWidth: true Label { + colorScheme: root.colorScheme id: passLabel text: "2 Password:" @@ -170,6 +202,7 @@ ColumnLayout { } Button { + colorScheme: root.colorScheme text: "request" enabled: user !== undefined && user.isLoginRequested && !user.isLogin2PasswordRequested && !(user.isLogin2FARequested && !user.isLogin2FAProvided) @@ -180,6 +213,7 @@ ColumnLayout { } Button { + colorScheme: root.colorScheme text: "error" enabled: user !== undefined && user.isLogin2PasswordProvided && !(user.isLogin2FARequested && !user.isLogin2FAProvided) @@ -191,6 +225,7 @@ ColumnLayout { } Button { + colorScheme: root.colorScheme text: "Abort" enabled: user !== undefined && user.isLogin2PasswordProvided && !(user.isLogin2FARequested && !user.isLogin2FAProvided) diff --git a/internal/frontend/qml/BridgeTest/UserList.qml b/internal/frontend/qml/BridgeTest/UserList.qml index 356f363e..c8ddcb86 100644 --- a/internal/frontend/qml/BridgeTest/UserList.qml +++ b/internal/frontend/qml/BridgeTest/UserList.qml @@ -24,7 +24,7 @@ import Proton 4.0 ColumnLayout { id: root - property var colorScheme + property ColorScheme colorScheme property var backend property alias currentIndex: usersListView.currentIndex @@ -46,10 +46,10 @@ ColumnLayout { anchors.margins: 10 Label { + colorScheme: root.colorScheme text: modelData.username anchors.margins: 10 anchors.fill: parent - color: root.colorScheme.text_norm MouseArea { anchors.fill: parent @@ -69,14 +69,25 @@ ColumnLayout { Layout.fillWidth: true Button { + colorScheme: root.colorScheme + text: "+" onClicked: { - var newUserObject = backend.userComponent.createObject(backend, { username: "test@protonmail.com", loggedIn: false } ) + var newUserObject = backend.userComponent.createObject(backend) + newUserObject.username = backend.loginUser.username.length > 0 ? backend.loginUser.username : "test@protonmail.com" + newUserObject.loggedIn = true + newUserObject.setupGuideSeen = true // backend.loginUser.setupGuideSeen + + backend.loginUser.username = "" + backend.loginUser.loggedIn = false + backend.loginUser.setupGuideSeen = false + backend.users.append( { object: newUserObject } ) } } Button { + colorScheme: root.colorScheme text: "-" enabled: usersListView.currentIndex != 0 diff --git a/internal/frontend/qml/Bridge_test.qml b/internal/frontend/qml/Bridge_test.qml index ba998bce..d81e0b1a 100644 --- a/internal/frontend/qml/Bridge_test.qml +++ b/internal/frontend/qml/Bridge_test.qml @@ -26,22 +26,32 @@ import QtQml.Models 2.12 import Proton 4.0 import "./BridgeTest" +import BridgePreview 1.0 + +import Notifications 1.0 Window { id: root width: 640 height: 480 - x: 100 - y: 100 - property var colorScheme: ProtonStyle.darkStyle + property ColorScheme colorScheme: ProtonStyle.darkStyle flags : Qt.Window | Qt.Dialog visible : true title : "Bridge Test GUI" color : colorScheme.background_norm + function getCursorPos() { + return BridgePreview.getCursorPos() + } + function quit() { + if (bridge !== undefined && bridge !== null) { + bridge.destroy() + } + } + function _log(msg, color) { logTextArea.text += "

" + msg + "

" logTextArea.text += "\n" @@ -94,6 +104,11 @@ Window { property string username: "" property bool loggedIn: false + property bool setupGuideSeen: true + + property string captionText: "50.3 MB / 20 GB" + property string avatarText: "jd" + signal loginUsernamePasswordError() signal loginFreeUserError() signal loginConnectionError() @@ -166,6 +181,7 @@ Window { Component.onCompleted: { var newLoginUser = _userComponent.createObject() root.loginUser = newLoginUser + root.loginUser.setupGuideSeen = false _usersTest.append({object: newLoginUser}) newLoginUser.loginUsernamePasswordError.connect(root.loginUsernamePasswordError) @@ -194,7 +210,7 @@ Window { } TabButton { - text: "Playground" + text: "Notifications" } TabButton { @@ -215,16 +231,13 @@ Window { RowLayout { id: globalTab spacing : 5 - property alias colorScheme: root.colorScheme ColumnLayout { spacing : 5 - property alias colorScheme: globalTab.colorScheme - - ProtonLabel { + Label { + colorScheme: root.colorScheme text: "Global settings" - color: globalTab.colorScheme.text_norm } ButtonGroup { @@ -232,6 +245,7 @@ Window { } RadioButton { + colorScheme: root.colorScheme Layout.fillWidth: true text: "Light UI" @@ -246,6 +260,7 @@ Window { } RadioButton { + colorScheme: root.colorScheme Layout.fillWidth: true text: "Dark UI" @@ -262,6 +277,7 @@ Window { } Button { + colorScheme: root.colorScheme //Layout.fillWidth: true text: "Open Bridge" @@ -272,6 +288,7 @@ Window { } Button { + colorScheme: root.colorScheme //Layout.fillWidth: true text: "Close Bridge" @@ -289,14 +306,13 @@ Window { ColumnLayout { spacing : 5 - property alias colorScheme: globalTab.colorScheme - - ProtonLabel { + Label { + colorScheme: root.colorScheme text: "Notifications" - color: globalTab.colorScheme.text_norm } Button { + colorScheme: root.colorScheme text: "Notify: danger" enabled: bridge !== undefined && bridge !== null onClicked: { @@ -305,6 +321,7 @@ Window { } Button { + colorScheme: root.colorScheme text: "Notify: warning" enabled: bridge !== undefined && bridge !== null onClicked: { @@ -313,6 +330,7 @@ Window { } Button { + colorScheme: root.colorScheme text: "Notify: success" enabled: bridge !== undefined && bridge !== null onClicked: { @@ -320,8 +338,6 @@ Window { } } - - Item { Layout.fillHeight: true } @@ -345,14 +361,131 @@ Window { } RowLayout { - id: playgroundTab + id: notificationsTab + spacing: 5 - property var colorScheme: root.colorScheme + ColumnLayout { + spacing: 5 - AccountDelegate{} + Switch { + colorScheme: root.colorScheme + + text: "Internet connection" + checked: true + onCheckedChanged: { + checked ? root.internetOn() : root.internetOff() + } + } + + Button { + colorScheme: root.colorScheme + + text: "Update manual ready" + onClicked: { + root.updateManualReady("3.14.1592") + } + } + Button { + colorScheme: root.colorScheme + + text: "Update manual done" + onClicked: { + root.updateManualRestartNeeded() + } + } + Button { + colorScheme: root.colorScheme + + text: "Update manual error" + onClicked: { + root.updateManualError() + } + } + Button { + colorScheme: root.colorScheme + + text: "Update force" + onClicked: { + root.updateForce("3.14.1592") + } + } + Button { + colorScheme: root.colorScheme + + text: "Update force error" + onClicked: { + root.updateForceError() + } + } + + Button { + colorScheme: root.colorScheme + + text: "Update silent done" + onClicked: { + root.updateSilentRestartNeeded() + } + } + + Button { + colorScheme: root.colorScheme + + text: "Update solent error" + onClicked: { + root.updateSilentError() + } + } + + Button { + colorScheme: root.colorScheme + + text: "Bug report send OK" + onClicked: { + root.bugReportSendSuccess() + } + } + + Button { + colorScheme: root.colorScheme + + text: "Bug report send error" + onClicked: { + root.bugReportSendError() + } + } + + Button { + colorScheme: root.colorScheme + + text: "Cache anavailable" + onClicked: { + root.cacheAnavailable() + } + } + Button { + colorScheme: root.colorScheme + + text: "Cache can't move" + onClicked: { + root.cacheCantMove() + } + } + + Button { + colorScheme: root.colorScheme + + text: "Disk full" + onClicked: { + root.diskFull() + } + } + + + } } TextArea { + colorScheme: root.colorScheme id: logTextArea Layout.fillHeight: true Layout.fillWidth: true @@ -365,7 +498,7 @@ Window { } } - property var bridge + property Bridge bridge // this signals are used only when trying to login with new user (i.e. not in users model) signal loginUsernamePasswordError() @@ -378,6 +511,25 @@ Window { signal login2PasswordError() signal login2PasswordErrorAbort() + signal internetOff() + signal internetOn() + + signal updateManualReady(var version) + signal updateManualRestartNeeded() + signal updateManualError() + signal updateForce(var version) + signal updateForceError() + signal updateSilentRestartNeeded() + signal updateSilentError() + + signal bugReportSendSuccess() + signal bugReportSendError() + + signal cacheAnavailable() + signal cacheCantMove() + + signal diskFull() + onLoginUsernamePasswordError: { console.debug("<- loginUsernamePasswordError") } @@ -406,6 +558,13 @@ Window { console.debug("<- login2PasswordErrorAbort") } + onInternetOff: { + console.debug("<- internetOff") + } + onInternetOn: { + console.debug("<- internetOn") + } + Component { id: bridgeComponent diff --git a/internal/frontend/qml/ContentWrapper.qml b/internal/frontend/qml/ContentWrapper.qml index f78815ac..98db99d0 100644 --- a/internal/frontend/qml/ContentWrapper.qml +++ b/internal/frontend/qml/ContentWrapper.qml @@ -23,9 +23,14 @@ import Proton 4.0 Item { id: root - property var colorScheme: parent.colorScheme + property ColorScheme colorScheme - property var window + property var backend + + signal login(string username, string password) + signal login2FA(string username, string code) + signal login2Password(string username, string password) + signal loginAbort(string username) RowLayout { anchors.fill: parent @@ -33,7 +38,7 @@ Item { Rectangle { id: leftBar - property var colorScheme: ProtonStyle.prominentStyle + property ColorScheme colorScheme: root.colorScheme.prominent Layout.minimumWidth: 264 Layout.maximumWidth: 320 @@ -55,9 +60,8 @@ Item { Layout.preferredHeight: 60 spacing: 0 - property var colorScheme: leftBar.colorScheme - Status { + colorScheme: leftBar.colorScheme Layout.leftMargin: 16 Layout.topMargin: 24 Layout.bottomMargin: 17 @@ -72,6 +76,7 @@ Item { } Button { + colorScheme: leftBar.colorScheme Layout.minimumHeight: 36 Layout.maximumHeight: 36 Layout.preferredHeight: 36 @@ -89,6 +94,7 @@ Item { } Button { + colorScheme: leftBar.colorScheme Layout.minimumHeight: 36 Layout.maximumHeight: 36 Layout.preferredHeight: 36 @@ -127,20 +133,20 @@ Item { header: Rectangle { height: headerLabel.height+16 - color: ProtonStyle.transparent - ProtonLabel{ - id:headerLabel + // color: ProtonStyle.transparent + Label{ + colorScheme: leftBar.colorScheme + id: headerLabel text: qsTr("Accounts") - color: leftBar.colorScheme.text_norm - state: "body" + type: Label.LabelType.Body } } - model: window.backend.users + model: root.backend.users delegate: AccountDelegate{ id: accountDelegate colorScheme: leftBar.colorScheme - text: modelData.username + user: modelData } } @@ -160,9 +166,8 @@ Item { Layout.maximumHeight: 52 Layout.preferredHeight: 52 - property var colorScheme: leftBar.colorScheme - Button { + colorScheme: leftBar.colorScheme width: 36 height: 36 @@ -209,14 +214,13 @@ Item { Layout.fillHeight: true colorScheme: root.colorScheme - user: (root.window.backend.users.count === 1 && root.window.backend.users.get(0).loggedIn === false) ? root.window.backend.users.get(0) : undefined - backend: root.window.backend - window: root.window + user: (root.backend.users.count === 1 && root.backend.users.get(0).loggedIn === false) ? root.backend.users.get(0) : undefined + backend: root.backend - onLogin : { root.window.login ( username , password ) } - onLogin2FA : { root.window.login2FA ( username , code ) } - onLogin2Password : { root.window.login2Password ( username , password ) } - onLoginAbort : { root.window.loginAbort ( username ) } + onLogin : { root.login ( username , password ) } + onLogin2FA : { root.login2FA ( username , code ) } + onLogin2Password : { root.login2Password ( username , password ) } + onLoginAbort : { root.loginAbort ( username ) } } } } diff --git a/internal/frontend/qml/DebugWrapper.qml b/internal/frontend/qml/DebugWrapper.qml index c8b883f4..f99a262f 100644 --- a/internal/frontend/qml/DebugWrapper.qml +++ b/internal/frontend/qml/DebugWrapper.qml @@ -18,16 +18,20 @@ import QtQuick 2.13 import QtQuick.Controls 2.12 +import Proton 4.0 + Rectangle { anchors.fill: parent color: "transparent" border.color: "red" border.width: 1 - z: parent.z - 1 + //z: parent.z - 1 + z: 10000000 Label { text: parent.width + "x" + parent.height anchors.centerIn: parent color: "black" + colorScheme: ProtonStyle.currentStyle } } diff --git a/internal/frontend/qml/MainWindow.qml b/internal/frontend/qml/MainWindow.qml index 51fcb1e5..bcf7cf38 100644 --- a/internal/frontend/qml/MainWindow.qml +++ b/internal/frontend/qml/MainWindow.qml @@ -22,10 +22,11 @@ import QtQuick.Layouts 1.12 import QtQuick.Controls 2.12 import Proton 4.0 +import Notifications 1.0 import "tests" -Window { +ApplicationWindow { id: root title: "ProtonMail Bridge" @@ -35,33 +36,104 @@ Window { minimumHeight: contentLayout.implicitHeight minimumWidth: contentLayout.implicitWidth - property var colorScheme: ProtonStyle.currentStyle + colorScheme: ProtonStyle.currentStyle property var backend - property var users - - - property bool isNoUser: backend.users.count === 0 - property bool isNoLoggedUser: backend.users.count === 1 && backend.users.get(0).loggedIn === false - property bool showSetup: true + property var notifications signal login(string username, string password) signal login2FA(string username, string code) signal login2Password(string username, string password) signal loginAbort(string username) + // show Setup Guide on every new user + Connections { + target: root.backend.users + + onRowsInserted: { + // considerring that users are added one-by-one + var user = root.backend.users.get(first) + + if (!user.loggedIn) { + return + } + + if (user.setupGuideSeen) { + return + } + + root.showSetup(user) + } + + onRowsAboutToBeRemoved: { + for (var i = first; i <= last; i++ ) { + var user = root.backend.users.get(i) + + if (setupGuide.user === user) { + setupGuide.user = null + contentLayout._showSetup = false + return + } + } + } + } + + function showSetup(user) { + setupGuide.user = user + if (setupGuide.user) { + contentLayout._showSetup = true + } else { + contentLayout._showSetup = false + } + } + StackLayout { id: contentLayout anchors.fill: parent - currentIndex: (root.isNoUser || root.isNoLoggedUser) ? 0 : ( root.showSetup ? 1 : 2) + property bool _showSetup: false + currentIndex: { + // show welcome when there are no users or only one non-logged-in user is present + if (backend.users.count === 0) { + return 1 + } - WelcomeWindow { + if (backend.users.count === 1 && backend.users.get(0).loggedIn === false) { + return 1 + } + + if (contentLayout._showSetup) { + return 2 + } + + return 0 + } + + ContentWrapper { + colorScheme: root.colorScheme + backend: root.backend + + Layout.fillHeight: true + Layout.fillWidth: true + + onLogin: { + root.login(username, password) + } + onLogin2FA: { + root.login2FA(username, code) + } + onLogin2Password: { + root.login2Password(username, password) + } + onLoginAbort: { + root.loginAbort(username) + } + } + + WelcomeGuide { colorScheme: root.colorScheme backend: root.backend - window: root - enabled: !banners.blocking Layout.fillHeight: true Layout.fillWidth: true @@ -81,38 +153,21 @@ Window { } SetupGuide { + id: setupGuide colorScheme: root.colorScheme - window: root - enabled: !banners.blocking + backend: root.backend Layout.fillHeight: true Layout.fillWidth: true - } - ContentWrapper { - colorScheme: root.colorScheme - window: root - enabled: !banners.blocking - - Layout.fillHeight: true - Layout.fillWidth: true + onDismissed: { + root.showSetup(null) + } } } - Banners { - id: banners - anchors.fill: parent - window: root - onTop: contentLayout.currentIndex == 0 - } - - function notifyOnlyPaidUsers() { banners.notifyOnlyPaidUsers() } - function notifyConnectionLostWhileLogin() { banners.notifyConnectionLostWhileLogin() } - function notifyUpdateManually() { banners.notifyUpdateManually() } - function notifyUserAdded() { banners.notifyUserAdded() } - - function showSetupGuide(user) { - setupGuide.user = user - root.showSetup = true + NotificationPopups { + colorScheme: root.colorScheme + notifications: root.notifications } } diff --git a/internal/frontend/qml/NotificationDialog.qml b/internal/frontend/qml/NotificationDialog.qml new file mode 100644 index 00000000..914ba8f4 --- /dev/null +++ b/internal/frontend/qml/NotificationDialog.qml @@ -0,0 +1,117 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge 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 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge 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 ProtonMail Bridge. If not, see . + +import QtQml 2.12 +import QtQuick 2.12 +import QtQuick.Layouts 1.12 +import QtQuick.Controls 2.12 + +import Proton 4.0 +import Notifications 1.0 + +Dialog { + id: root + + property var notification + + shouldShow: notification && notification.active && !notification.dismissed + modal: true + + default property alias data: additionalChildrenContainer.children + + ColumnLayout { + spacing: 0 + + Image { + Layout.alignment: Qt.AlignHCenter + + sourceSize.width: 64 + sourceSize.height: 64 + + Layout.preferredHeight: 64 + Layout.preferredWidth: 64 + + Layout.bottomMargin: 16 + + visible: source != "" + + source: { + if (!root.notification) { + return "" + } + + switch (root.notification.type) { + case Notification.NotificationType.Info: + // TODO: Add info icon? + return "" + case Notification.NotificationType.Success: + return "./icons/ic-success.svg" + case Notification.NotificationType.Warning: + case Notification.NotificationType.Danger: + return "./icons/ic-alert.svg" + } + } + } + + Label { + Layout.alignment: Qt.AlignHCenter + Layout.bottomMargin: 8 + colorScheme: root.colorScheme + text: root.notification.text + type: Label.LabelType.Title + } + + Label { + Layout.fillWidth: true + Layout.preferredWidth: 240 + Layout.bottomMargin: 16 + + colorScheme: root.colorScheme + text: root.notification.description + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + type: Label.LabelType.Body + } + + Item { + id: additionalChildrenContainer + + Layout.fillWidth: true + Layout.bottomMargin: 16 + + visible: children.length > 0 + + implicitHeight: additionalChildrenContainer.childrenRect.height + implicitWidth: additionalChildrenContainer.childrenRect.width + } + + ColumnLayout { + spacing: 8 + Repeater { + model: root.notification.action + delegate: Button { + Layout.fillWidth: true + + colorScheme: root.colorScheme + action: modelData + + secondary: index > 0 + } + } + } + } +} diff --git a/internal/frontend/qml/NotificationPopups.qml b/internal/frontend/qml/NotificationPopups.qml new file mode 100644 index 00000000..bb9d6433 --- /dev/null +++ b/internal/frontend/qml/NotificationPopups.qml @@ -0,0 +1,91 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge 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 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge 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 ProtonMail Bridge. If not, see . + +import QtQml 2.12 +import QtQuick 2.12 +import QtQuick.Layouts 1.12 +import QtQuick.Controls 2.12 + +import Proton 4.0 +import Notifications 1.0 + +Item { + id: root + + property ColorScheme colorScheme + property var notifications + + property int notificationWhitelist: NotificationFilter.FilterConsts.All + property int notificationBlacklist: NotificationFilter.FilterConsts.None + + NotificationFilter { + id: bannerNotificationFilter + + source: root.notifications.all + blacklist: Notifications.Group.Dialogs + } + + Banner { + colorScheme: root.colorScheme + notification: bannerNotificationFilter.topmost + } + + NotificationDialog { + colorScheme: root.colorScheme + notification: root.notifications.updateManualReady + + Switch { + colorScheme: root.colorScheme + text: qsTr("Update automatically in the future") + } + } + + NotificationDialog { + colorScheme: root.colorScheme + notification: root.notifications.updateForce + } + + NotificationDialog { + colorScheme: root.colorScheme + notification: root.notifications.updateForceError + } + + NotificationDialog { + colorScheme: root.colorScheme + notification: root.notifications.bugReportSendSuccess + } + + NotificationDialog { + colorScheme: root.colorScheme + notification: root.notifications.bugReportSendError + } + + NotificationDialog { + colorScheme: root.colorScheme + notification: root.notifications.cacheAnavailable + } + + NotificationDialog { + colorScheme: root.colorScheme + notification: root.notifications.cacheCantMove + } + + NotificationDialog { + colorScheme: root.colorScheme + notification: root.notifications.diskFull + } +} diff --git a/internal/frontend/qml/Notifications/Notification.qml b/internal/frontend/qml/Notifications/Notification.qml new file mode 100644 index 00000000..5c267b53 --- /dev/null +++ b/internal/frontend/qml/Notifications/Notification.qml @@ -0,0 +1,49 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge 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 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge 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 ProtonMail Bridge. If not, see . + +import QtQml 2.12 +import QtQuick.Controls 2.12 + +QtObject { + id: root + + default property var children + + enum NotificationType { + Info = 0, + Success = 1, + Warning = 2, + Danger = 3 + } + + property string text + property string description + property string icon + property list action + property int type + property int group + + property bool dismissed: false + property bool active: false + readonly property var occurred: active ? new Date() : undefined + + property var data + + onActiveChanged: { + dismissed = false + } +} diff --git a/internal/frontend/qml/Notifications/NotificationFilter.qml b/internal/frontend/qml/Notifications/NotificationFilter.qml new file mode 100644 index 00000000..65d51e88 --- /dev/null +++ b/internal/frontend/qml/Notifications/NotificationFilter.qml @@ -0,0 +1,114 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge 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 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge 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 ProtonMail Bridge. If not, see . + +import QtQml 2.12 +import QtQml.Models 2.12 + +// contains notifications that satisfy black- and whitelist and are sorted in time-occurred order +ListModel { + id: root + + enum FilterConsts { + None = 0, + All = 255 + } + + property int whitelist: NotificationFilter.FilterConsts.All + property int blacklist: NotificationFilter.FilterConsts.None + + property Notification topmost + property var source + + property bool componentCompleted: false + Component.onCompleted: { + root.componentCompleted = true + root.rebuildList() + } + + // overriding get method to ignore any role and return directly object itself + function get(row) { + if (row < 0 || row >= count) { + return undefined + } + return data(index(row, 0), Qt.DisplayRole) + } + + function rebuildList() { + // avoid evaluation of the list before Component.onCompleted + if (!root.componentCompleted) { + return + } + + for (var i = 0; i < root.count; i++) { + root.get(i).onActiveChanged.disconnect( root.updateList ) + } + + root.clear() + + if (!root.source) { + return + } + + for (i = 0; i < root.source.length; i++) { + var obj = root.source[i] + if (obj.group & root.blacklist) { + continue + } + + if (!(obj.group & root.whitelist)) { + continue + } + + root.append({obj}) + obj.onActiveChanged.connect( root.updateList ) + } + } + + function updateList() { + var topmost = null + + for (var i = 0; i < root.count; i++) { + var obj = root.get(i) + + if (!obj.active) { + continue + } + + if (topmost && (topmost.type > obj.type)) { + continue + } + + if (topmost && (topmost.type === obj.type) && (topmost.occurred > obj.occurred)) { + continue + } + + topmost = obj + } + + root.topmost = topmost + } + + onWhitelistChanged: { + root.rebuildList() + } + onBlacklistChanged: { + root.rebuildList() + } + onSourceChanged: { + root.rebuildList() + } +} diff --git a/internal/frontend/qml/Notifications/Notifications.qml b/internal/frontend/qml/Notifications/Notifications.qml new file mode 100644 index 00000000..f3f110bf --- /dev/null +++ b/internal/frontend/qml/Notifications/Notifications.qml @@ -0,0 +1,428 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge 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 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge 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 ProtonMail Bridge. If not, see . + +import QtQml 2.12 +import Qt.labs.platform 1.1 +import QtQuick.Controls 2.12 +import ".." + +QtObject { + id: root + + property var backend + + property MainWindow frontendMain + property StatusWindow frontendStatus + property SystemTrayIcon frontendTray + + enum Group { + Connection = 1, + Update = 2, + Configuration = 4, + API = 32, + + // Special group for notifications that require dialog popup instead of banner + Dialogs = 64 + } + + property var all: [ + root.noInternet, + root.updateManualReady, + root.updateManualRestartNeeded, + root.updateManualError, + root.updateForce, + root.updateForceError, + root.updateSilentRestartNeeded, + root.updateSilentError, + root.bugReportSendSuccess, + root.bugReportSendError, + root.cacheAnavailable, + root.cacheCantMove, + root.accountChanged, + root.diskFull + ] + + // Connection + property Notification noInternet: Notification { + text: qsTr("No connection") + icon: "./icons/ic-no-connection.svg" + type: Notification.NotificationType.Danger + group: Notifications.Group.Connection + + Connections { + target: root.backend + + onInternetOff: { + root.noInternet.active = true + } + onInternetOn: { + root.noInternet.active = false + } + } + } + + // Updates + property Notification updateManualReady: Notification { + text: qsTr("Update to Bridge") + " " + (data ? data.version : "") + description: qsTr("A new version of ProtonMail Bridge is available. See what's changed.") + icon: "./icons/ic-info-circle-filled.svg" + type: Notification.NotificationType.Info + group: Notifications.Group.Update | Notifications.Group.Dialogs + + Connections { + target: root.backend + onUpdateManualReady: { + root.updateManualReady.data = { version: version } + root.updateManualReady.active = true + } + } + + action: [ + Action { + text: qsTr("Update") + + onTriggered: { + // TODO: call update from backend + root.updateManualReady.active = false + } + }, + Action { + text: qsTr("Remind me later") + + onTriggered: { + // TODO: start timer here + root.updateManualReady.active = false + } + } + ] + } + + property Notification updateManualRestartNeeded: Notification { + text: qsTr("Bridge update is ready") + icon: "./icons/ic-info-circle-filled.svg" + type: Notification.NotificationType.Info + group: Notifications.Group.Update + + Connections { + target: root.backend + onUpdateManualRestartNeeded: { + root.updateManualRestartNeeded.active = true + } + } + + action: Action { + text: qsTr("Restart Bridge") + + onTriggered: { + // TODO + root.updateManualRestartNeeded.active = false + } + } + } + + property Notification updateManualError: Notification { + text: qsTr("Bridge couldn’t update") + icon: "./icons/ic-exclamation-circle-filled.svg" + type: Notification.NotificationType.Warning + group: Notifications.Group.Update + + Connections { + target: root.backend + onUpdateManualError: { + root.updateManualError.active = true + } + } + + action: Action { + text: qsTr("Update manually") + + onTriggered: { + // TODO + root.updateManualError.active = false + } + } + } + + property Notification updateForce: Notification { + text: qsTr("Update to ProtonMail Bridge") + " " + (data ? data.version : "") + description: qsTr("This version of Bridge is no longer supported, please update. Learn why. To update manually, go to: https:/protonmail.com/bridge/download") + icon: "./icons/ic-exclamation-circle-filled.svg" + type: Notification.NotificationType.Danger + group: Notifications.Group.Update | Notifications.Group.Dialogs + + Connections { + target: root.backend + + onUpdateForce: { + root.updateForce.data = { version: version } + root.updateForce.active = true + } + } + + action: [ + Action { + text: qsTr("Update") + + onTriggered: { + // TODO: trigger update here + root.updateForce.active = false + } + }, + Action { + text: qsTr("Quite Bridge") + + onTriggered: { + // TODO: quit Bridge here + root.updateForce.active = false + } + } + ] + } + + property Notification updateForceError: Notification { + text: qsTr("Bridge coudn’t update") + description: qsTr("You must update manually. Go to: https:/protonmail.com/bridge/download") + icon: "./icons/ic-exclamation-circle-filled.svg" + type: Notification.NotificationType.Danger + group: Notifications.Group.Update | Notifications.Group.Dialogs + + Connections { + target: root.backend + + onUpdateForceError: { + root.updateForceError.active = true + } + } + + action: [ + Action { + text: qsTr("Update manually") + + onTriggered: { + // TODO: trigger update here + root.updateForceError.active = false + } + }, + Action { + text: qsTr("Quite Bridge") + + onTriggered: { + // TODO: quit Bridge here + root.updateForce.active = false + } + } + ] + } + + property Notification updateSilentRestartNeeded: Notification { + text: qsTr("Bridge update is ready") + icon: "./icons/ic-info-circle-filled.svg" + type: Notification.NotificationType.Info + group: Notifications.Group.Update + + Connections { + target: root.backend + onUpdateSilentRestartNeeded: { + root.updateSilentRestartNeeded.active = true + } + } + + action: Action { + text: qsTr("Restart Bridge") + + onTriggered: { + // TODO + root.updateSilentRestartNeeded.active = false + } + } + } + + property Notification updateSilentError: Notification { + text: qsTr("Bridge couldn’t update") + icon: "./icons/ic-exclamation-circle-filled.svg" + type: Notification.NotificationType.Warning + group: Notifications.Group.Update + + Connections { + target: root.backend + onUpdateSilentError: { + root.updateSilentError.active = true + } + } + + action: Action { + text: qsTr("Update manually") + + onTriggered: { + // TODO + root.updateSilentError.active = false + } + } + } + + // Bug reports + property Notification bugReportSendSuccess: Notification { + text: qsTr("Bug report sent") + description: qsTr("We’ve received your report, thank you! Our team will get back to you as soon as we can.") + type: Notification.NotificationType.Success + group: Notifications.Group.Configuration | Notifications.Group.Dialogs + + Connections { + target: root.backend + onBugReportSendSuccess: { + root.bugReportSendSuccess.active = true + } + } + + action: [ + Action { + text: qsTr("Ok") + onTriggered: { + root.bugReportSendSuccess.active = false + } + }, + Action { + text: "test" + } + ] + } + + property Notification bugReportSendError: Notification { + text: qsTr("There was a problem") + description: qsTr("There was a problem with sending your report. Please try again later or contact us directly at security@protonmail.com") + type: Notification.NotificationType.Warning + group: Notifications.Group.Configuration | Notifications.Group.Dialogs + + Connections { + target: root.backend + onBugReportSendError: { + root.bugReportSendError.active = true + } + } + + action: Action { + text: qsTr("Ok") + onTriggered: { + root.bugReportSendError.active = false + } + } + } + + // Cache + property Notification cacheAnavailable: Notification { + text: qsTr("Cache location is unavailable") + description: qsTr("Check the directory or change it in your settings.") + type: Notification.NotificationType.Warning + group: Notifications.Group.Configuration | Notifications.Group.Dialogs + + Connections { + target: root.backend + onCacheAnavailable: { + root.cacheAnavailable.active = true + } + } + + action: [ + Action { + text: qsTr("Quit Bridge") + onTriggered: { + root.cacheAnavailable.active = false + } + }, + Action { + text: qsTr("Change location") + onTriggered: { + root.cacheAnavailable.active = false + } + } + ] + } + + property Notification cacheCantMove: Notification { + text: qsTr("Can’t move cache") + description: qsTr("The location you have selected is not available. Make sure you have enough free space or choose another location.") + type: Notification.NotificationType.Warning + group: Notifications.Group.Configuration | Notifications.Group.Dialogs + + Connections { + target: root.backend + onCacheCantMove: { + root.cacheCantMove.active = true + } + } + + action: [ + Action { + text: qsTr("Cancel") + onTriggered: { + root.cacheCantMove.active = false + } + }, + Action { + text: qsTr("Change location") + onTriggered: { + root.cacheCantMove.active = false + } + } + ] + } + + // Other + property Notification accountChanged: Notification { + text: qsTr("The address list for your account has changed") + icon: "./icons/ic-exclamation-circle-filled.svg" + type: Notification.NotificationType.Danger + group: Notifications.Group.Configuration + + action: Action { + text: qsTr("Reconfigure") + + onTriggered: { + // TODO: open configuration window here + } + } + } + + property Notification diskFull: Notification { + text: qsTr("Your disk is almost full") + description: qsTr("Quit Bridge and free disk space or disable the local cache (not recommended).") + type: Notification.NotificationType.Warning + group: Notifications.Group.Configuration | Notifications.Group.Dialogs + + Connections { + target: root.backend + onDiskFull: { + root.diskFull.active = true + } + } + + action: [ + Action { + text: qsTr("Quit Bridge") + onTriggered: { + root.diskFull.active = false + } + }, + Action { + text: qsTr("Settings") + onTriggered: { + root.diskFull.active = false + } + } + ] + } +} diff --git a/internal/frontend/qml/Notifications/qmldir b/internal/frontend/qml/Notifications/qmldir new file mode 100644 index 00000000..0caea552 --- /dev/null +++ b/internal/frontend/qml/Notifications/qmldir @@ -0,0 +1,6 @@ +module Notifications +depends QtQml 2.12 + +Notifications 1.0 Notifications.qml +NotificationFilter 1.0 NotificationFilter.qml +Notification 1.0 Notification.qml diff --git a/internal/frontend/qml/Proton/ApplicationWindow.qml b/internal/frontend/qml/Proton/ApplicationWindow.qml new file mode 100644 index 00000000..0e19a40e --- /dev/null +++ b/internal/frontend/qml/Proton/ApplicationWindow.qml @@ -0,0 +1,136 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge 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 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge 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 ProtonMail Bridge. If not, see . + +import QtQml 2.12 +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Controls.impl 2.12 +import QtQuick.Templates 2.12 as T + +import "." + +T.ApplicationWindow { + id: root + + property ColorScheme colorScheme + + // popup priority based on types + enum PopupType { + Banner = 0, + Dialog = 1 + } + + // contains currently visible popup + property var popupVisible: null + + // list of all popups within ApplicationWindow + property ListModel popups: ListModel { + // overriding get method to ignore any role and return directly object itself + function get(row) { + if (row < 0 || row >= count) { + return undefined + } + return data(index(row, 0), Qt.DisplayRole) + } + + onRowsInserted: { + for (var i = first; i <= last; i++) { + var obj = popups.get(i) + obj.onShouldShowChanged.connect( root.processPopups ) + } + + processPopups() + } + + onRowsAboutToBeRemoved: { + for (var i = first; i <= last; i++ ) { + var obj = popups.get(i) + obj.onShouldShowChanged.disconnect( root.processPopups ) + + // if currently visible popup was removed + if (root.popupVisible === obj) { + root.popupVisible.visible = false + root.popupVisible = null + } + } + + processPopups() + } + } + + function processPopups() { + if ((root.popupVisible) && (!root.popupVisible.shouldShow)) { + root.popupVisible.visible = false + } + + // do nothing if there is already visible popup + if (root.popupVisible) { + return + } + + var topmost = null + for (var i = 0; i < popups.count; i++) { + var obj = popups.get(i) + + if (obj.shouldShow === false) { + continue + } + + if (topmost && (topmost.popupType > obj.popupType)) { + continue + } + + if (topmost && (topmost.popupType === obj.popupType) && (topmost.occurred > obj.occurred)) { + continue + } + + topmost = obj + } + + root.popupVisible = topmost + + if (!topmost) { + return + } + + topmost.visible = true + } + + Connections { + target: root.popupVisible + + onVisibleChanged: { + if (root.popupVisible.visible) { + return + } + + root.popupVisible = null + root.processPopups() + } + } + + color: root.colorScheme.background_norm + + overlay.modal: Rectangle { + color: root.colorScheme.backdrop_norm + } + + overlay.modeless: Rectangle { + color: "transparent" + } +} diff --git a/internal/frontend/qml/Proton/Banner.qml b/internal/frontend/qml/Proton/Banner.qml deleted file mode 100644 index ba1d8cc4..00000000 --- a/internal/frontend/qml/Proton/Banner.qml +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright (c) 2021 Proton Technologies AG -// -// This file is part of ProtonMail Bridge. -// -// ProtonMail Bridge 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 3 of the License, or -// (at your option) any later version. -// -// ProtonMail Bridge 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 ProtonMail Bridge. If not, see . - - -import QtQml 2.12 -import QtQuick 2.13 -import QtQuick.Layouts 1.12 -import QtQuick.Controls.impl 2.12 - -Rectangle { - id: root - - width: layout.width - height: layout.height - - radius: 10 - - signal accepted() - - - property alias text: description.text - property var actionText: "" - - property var colorText: Style.currentStyle.text_invert - property var colorMain: "#000" - property var colorHover: "#000" - property var colorActive: "#000" - property var iconSource: "../icons/ic-exclamation-circle-filled.svg" - - color: root.colorMain - border.color: root.colorActive - border.width: 1 - - property var maxWidth: 600 - property var minWidth: 400 - property var usedWidth: button.width + icon.width - - RowLayout { - id: layout - - IconLabel { - id:icon - Layout.alignment: Qt.AlignCenter - Layout.leftMargin: 17.5 - Layout.topMargin: 15.5 - Layout.bottomMargin: 15.5 - color: root.colorText - icon.source: root.iconSource - icon.color: root.colorText - icon.height: Style.title_line_height - } - - ProtonLabel { - id: description - Layout.alignment: Qt.AlignCenter - Layout.leftMargin: 9.5 - Layout.minimumWidth: root.minWidth - root.usedWidth - Layout.maximumWidth: root.maxWidth - root.usedWidth - - color: root.colorText - state: "body" - - wrapMode: Text.WordWrap - verticalAlignment: Text.AlignVCenter - } - - Button { - id:button - Layout.fillHeight: true - - hoverEnabled: true - - text: root.actionText.toUpperCase() - - onClicked: root.accepted() - - background: RoundedRectangle { - width:parent.width - height:parent.height - strokeColor: root.colorActive - strokeWidth: root.border.width - - radiusTopRight : root.radius - radiusBottomRight : root.radius - radiusTopLeft : 0 - radiusBottomLeft : 0 - - fillColor: button.down ? root.colorActive : ( - button.hovered ? root.colorHover : - root.colorMain - ) - } - } - } - - state: "info" - states: [ - State{ name : "danger" ; PropertyChanges{ target : root ; colorMain : Style.currentStyle.signal_danger ; colorHover : Style.currentStyle.signal_danger_hover ; colorActive : Style.currentStyle.signal_danger_active ; iconSource: "../icons/ic-exclamation-circle-filled.svg"}} , - State{ name : "warning" ; PropertyChanges{ target : root ; colorMain : Style.currentStyle.signal_warning ; colorHover : Style.currentStyle.signal_warning_hover ; colorActive : Style.currentStyle.signal_warning_active ; iconSource: "../icons/ic-exclamation-circle-filled.svg"}} , - State{ name : "success" ; PropertyChanges{ target : root ; colorMain : Style.currentStyle.signal_success ; colorHover : Style.currentStyle.signal_success_hover ; colorActive : Style.currentStyle.signal_success_active ; iconSource: "../icons/ic-info-circle-filled.svg"}} , - State{ name : "info" ; PropertyChanges{ target : root ; colorMain : Style.currentStyle.signal_info ; colorHover : Style.currentStyle.signal_info_hover ; colorActive : Style.currentStyle.signal_info_active ; iconSource: "../icons/ic-info-circle-filled.svg"}} - ] -} diff --git a/internal/frontend/qml/Proton/Button.qml b/internal/frontend/qml/Proton/Button.qml index 19fc2409..f1c77c55 100644 --- a/internal/frontend/qml/Proton/Button.qml +++ b/internal/frontend/qml/Proton/Button.qml @@ -22,7 +22,7 @@ import QtQuick.Templates 2.12 as T import "." T.Button { - property var colorScheme: parent.colorScheme ? parent.colorScheme : Style.currentStyle + property ColorScheme colorScheme property alias secondary: control.flat readonly property bool primary: !secondary @@ -30,6 +30,10 @@ T.Button { property bool loading: false + property bool borderless: false + + property int labelType: Label.LabelType.Body + // TODO: store previous enabled state and restore it? // For now assuming that only enabled buttons could have loading state onLoadingChanged: { @@ -55,9 +59,7 @@ T.Button { horizontalPadding: 16 spacing: 10 - font.family: Style.font_family - font.pixelSize: Style.body_font_size - font.letterSpacing: Style.body_letter_spacing + font: label.font icon.width: 16 icon.height: 16 @@ -65,7 +67,7 @@ T.Button { if (primary && !isIcon) { return "#FFFFFF" } else { - return colorScheme.text_norm + return control.colorScheme.text_norm } } @@ -103,6 +105,7 @@ T.Button { } Label { + colorScheme: root.colorScheme id: label anchors.left: labelIcon.left anchors.top: labelIcon.top @@ -115,15 +118,16 @@ T.Button { verticalAlignment: Qt.AlignVCenter text: control.text - font: control.font color: { if (primary && !isIcon) { return "#FFFFFF" } else { - return colorScheme.text_norm + return control.colorScheme.text_norm } } opacity: control.enabled || control.loading ? 1.0 : 0.5 + + type: labelType } ColorImage { @@ -176,74 +180,74 @@ T.Button { // Primary colors if (control.down) { - return colorScheme.interaction_norm_active + return control.colorScheme.interaction_norm_active } if (control.enabled && (control.highlighted || control.hovered || control.checked)) { - return colorScheme.interaction_norm_hover + return control.colorScheme.interaction_norm_hover } if (control.loading) { - return colorScheme.interaction_norm_hover + return control.colorScheme.interaction_norm_hover } - return colorScheme.interaction_norm + return control.colorScheme.interaction_norm } else { // Secondary colors if (control.down) { - return colorScheme.interaction_default_active + return control.colorScheme.interaction_default_active } if (control.enabled && (control.highlighted || control.hovered || control.checked)) { - return colorScheme.interaction_default_hover + return control.colorScheme.interaction_default_hover } if (control.loading) { - return colorScheme.interaction_default_hover + return control.colorScheme.interaction_default_hover } - return colorScheme.interaction_default + return control.colorScheme.interaction_default } } else { if (primary) { // Primary icon colors if (control.down) { - return colorScheme.interaction_default_active + return control.colorScheme.interaction_default_active } if (control.enabled && (control.highlighted || control.hovered || control.checked)) { - return colorScheme.interaction_default_hover + return control.colorScheme.interaction_default_hover } if (control.loading) { - return colorScheme.interaction_default_hover + return control.colorScheme.interaction_default_hover } - return colorScheme.interaction_default + return control.colorScheme.interaction_default } else { // Secondary icon colors if (control.down) { - return colorScheme.interaction_default_active + return control.colorScheme.interaction_default_active } if (control.enabled && (control.highlighted || control.hovered || control.checked)) { - return colorScheme.interaction_default_hover + return control.colorScheme.interaction_default_hover } if (control.loading) { - return colorScheme.interaction_default_hover + return control.colorScheme.interaction_default_hover } - return colorScheme.interaction_default + return control.colorScheme.interaction_default } } } - border.color: colorScheme.border_norm - border.width: secondary ? 1 : 0 + border.color: control.colorScheme.border_norm + border.width: secondary && !borderless ? 1 : 0 opacity: control.enabled || control.loading ? 1.0 : 0.5 } diff --git a/internal/frontend/qml/Proton/CheckBox.qml b/internal/frontend/qml/Proton/CheckBox.qml index c5144f46..70ded22e 100644 --- a/internal/frontend/qml/Proton/CheckBox.qml +++ b/internal/frontend/qml/Proton/CheckBox.qml @@ -21,7 +21,7 @@ import QtQuick.Controls.impl 2.12 import QtQuick.Templates 2.12 as T T.CheckBox { - property var colorScheme: parent.colorScheme ? parent.colorScheme : Style.currentStyle + property ColorScheme colorScheme property bool error: false @@ -46,39 +46,39 @@ T.CheckBox { color: { if (!checked) { - return colorScheme.background_norm + return control.colorScheme.background_norm } if (!control.enabled) { - return colorScheme.field_disabled + return control.colorScheme.field_disabled } if (control.error) { - return colorScheme.signal_danger + return control.colorScheme.signal_danger } if (control.hovered) { - return colorScheme.interaction_norm_hover + return control.colorScheme.interaction_norm_hover } - return colorScheme.interaction_norm + return control.colorScheme.interaction_norm } border.width: control.checked ? 0 : 1 border.color: { if (!control.enabled) { - return colorScheme.field_disabled + return control.colorScheme.field_disabled } if (control.error) { - return colorScheme.signal_danger + return control.colorScheme.signal_danger } if (control.hovered) { - return colorScheme.interaction_norm_hover + return control.colorScheme.interaction_norm_hover } - return colorScheme.field_norm + return control.colorScheme.field_norm } ColorImage { @@ -112,21 +112,21 @@ T.CheckBox { color: { if (!enabled) { - return colorScheme.text_disabled + return control.colorScheme.text_disabled } if (error) { - return colorScheme.signal_danger + return control.colorScheme.signal_danger } - return colorScheme.text_norm + return control.colorScheme.text_norm } font.family: Style.font_family - font.weight: Style.fontWidth_400 - font.pixelSize: 14 - lineHeight: 20 + font.weight: Style.fontWeight_400 + font.pixelSize: Style.body_font_size + lineHeight: Style.body_line_height lineHeightMode: Text.FixedHeight - font.letterSpacing: 0.2 + font.letterSpacing: Style.body_letter_spacing } } diff --git a/internal/frontend/qml/Proton/Dialog.qml b/internal/frontend/qml/Proton/Dialog.qml new file mode 100644 index 00000000..bcad323a --- /dev/null +++ b/internal/frontend/qml/Proton/Dialog.qml @@ -0,0 +1,82 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge 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 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge 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 ProtonMail Bridge. If not, see . + +import QtQml 2.12 +import QtQuick 2.12 +import QtQuick.Templates 2.12 as T +import QtQuick.Controls 2.12 +import QtQuick.Controls.impl 2.12 + +import "." + +T.Dialog { + id: root + property ColorScheme colorScheme + + Component.onCompleted: { + if (!ApplicationWindow.window) { + return + } + + if (ApplicationWindow.window.popups === undefined) { + return + } + + var obj = this + ApplicationWindow.window.popups.append( { obj } ) + } + + readonly property int popupType: ApplicationWindow.PopupType.Dialog + + property bool shouldShow: false + readonly property var occurred: shouldShow ? new Date() : undefined + function open() { + root.shouldShow = true + } + + function close() { + root.shouldShow = false + } + + anchors.centerIn: Overlay.overlay + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, + contentWidth + leftPadding + rightPadding, + implicitHeaderWidth, + implicitFooterWidth) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + contentHeight + topPadding + bottomPadding + + (implicitHeaderHeight > 0 ? implicitHeaderHeight + spacing : 0) + + (implicitFooterHeight > 0 ? implicitFooterHeight + spacing : 0)) + + padding: 24 + + background: Rectangle { + color: root.colorScheme.background_norm + radius: 10 + } + + // TODO: Add DropShadow here + + T.Overlay.modal: Rectangle { + color: root.colorScheme.backdrop_norm + } + + T.Overlay.modeless: Rectangle { + color: "transparent" + } +} diff --git a/internal/frontend/qml/Proton/Label.qml b/internal/frontend/qml/Proton/Label.qml new file mode 100644 index 00000000..f74a591c --- /dev/null +++ b/internal/frontend/qml/Proton/Label.qml @@ -0,0 +1,138 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge 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 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge 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 ProtonMail Bridge. If not, see . + +import QtQuick 2.13 +import QtQuick.Controls 2.12 +import QtQuick.Controls.impl 2.12 +import QtQuick.Templates 2.12 as T +import "." + +T.Label { + id: root + + property ColorScheme colorScheme + + enum LabelType { + // weight 700, size 28, height 36 + Heading, + // weight 700, size 20, height 24 + Title, + // weight 400, size 18, height 26 + Lead, + // weight 400, size 14, height 20, spacing 0.2 + Body, + // weight 600, size 14, height 20, spacing 0.2 + Body_semibold, + // weight 700, size 14, height 20, spacing 0.2 + Body_bold, + // weight 400, size 12, height 16, spacing 0.4 + Caption, + // weight 600, size 12, height 16, spacing 0.4 + Caption_semibold, + // weight 700, size 12, height 16, spacing 0.4 + Caption_bold + } + property int type: Label.LabelType.Body + + color: root.enabled ? root.colorScheme.text_norm : root.colorScheme.text_disabled + palette.link: root.colorScheme.interaction_norm + + font.family: Style.font_family + lineHeightMode: Text.FixedHeight + + font.weight: { + switch (root.type) { + case Label.LabelType.Heading: + return Style.fontWeight_700 + case Label.LabelType.Title: + return Style.fontWeight_700 + case Label.LabelType.Lead: + return Style.fontWeight_400 + case Label.LabelType.Body: + return Style.fontWeight_400 + case Label.LabelType.Body_semibold: + return Style.fontWeight_600 + case Label.LabelType.Body_bold: + return Style.fontWeight_700 + case Label.LabelType.Caption: + return Style.fontWeight_400 + case Label.LabelType.Caption_semibold: + return Style.fontWeight_600 + case Label.LabelType.Caption_bold: + return Style.fontWeight_700 + } + } + + font.pixelSize: { + switch (root.type) { + case Label.LabelType.Heading: + return Style.heading_font_size + case Label.LabelType.Title: + return Style.title_font_size + case Label.LabelType.Lead: + return Style.lead_font_size + case Label.LabelType.Body: + case Label.LabelType.Body_semibold: + case Label.LabelType.Body_bold: + return Style.body_font_size + case Label.LabelType.Caption: + case Label.LabelType.Caption_semibold: + case Label.LabelType.Caption_bold: + return Style.caption_font_size + } + } + + lineHeight: { + switch (root.type) { + case Label.LabelType.Heading: + return Style.heading_line_height + case Label.LabelType.Title: + return Style.title_line_height + case Label.LabelType.Lead: + return Style.lead_line_height + case Label.LabelType.Body: + case Label.LabelType.Body_semibold: + case Label.LabelType.Body_bold: + return Style.body_line_height + case Label.LabelType.Caption: + case Label.LabelType.Caption_semibold: + case Label.LabelType.Caption_bold: + return Style.caption_line_height + } + } + + font.letterSpacing: { + switch (root.type) { + case Label.LabelType.Heading: + case Label.LabelType.Title: + case Label.LabelType.Lead: + return 0 + case Label.LabelType.Body: + case Label.LabelType.Body_semibold: + case Label.LabelType.Body_bold: + return Style.body_letter_spacing + case Label.LabelType.Caption: + case Label.LabelType.Caption_semibold: + case Label.LabelType.Caption_bold: + return Style.caption_letter_spacing + } + } + + function link(url, text) { + return `${text}` + } +} diff --git a/internal/frontend/qml/Proton/Menu.qml b/internal/frontend/qml/Proton/Menu.qml new file mode 100644 index 00000000..c40203a1 --- /dev/null +++ b/internal/frontend/qml/Proton/Menu.qml @@ -0,0 +1,68 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge 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 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge 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 ProtonMail Bridge. If not, see . + +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Controls.impl 2.12 +import QtQuick.Templates 2.12 as T +import QtQuick.Window 2.12 +import "." + +T.Menu { + id: control + + property ColorScheme colorScheme + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, + contentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + contentHeight + topPadding + bottomPadding) + + margins: 0 + overlap: 1 + + delegate: MenuItem { + colorScheme: control.colorScheme + } + + contentItem: Item { + implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin + implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin + + ListView { + anchors.fill: parent + anchors.margins: 8 + + implicitHeight: contentHeight + model: control.contentModel + interactive: Window.window ? contentHeight > Window.window.height : false + clip: true + currentIndex: control.currentIndex + + ScrollIndicator.vertical: ScrollIndicator {} + } + } + + background: Rectangle { + implicitWidth: 200 + implicitHeight: 40 + color: colorScheme.background_norm + border.width: 1 + border.color: colorScheme.border_weak + radius: 10 + } +} diff --git a/internal/frontend/qml/Proton/MenuItem.qml b/internal/frontend/qml/Proton/MenuItem.qml new file mode 100644 index 00000000..9d6f89f6 --- /dev/null +++ b/internal/frontend/qml/Proton/MenuItem.qml @@ -0,0 +1,72 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge 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 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge 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 ProtonMail Bridge. If not, see . + +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Controls.impl 2.12 +import QtQuick.Templates 2.12 as T +import "." + +T.MenuItem { + id: control + + property ColorScheme colorScheme + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, + implicitContentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + implicitContentHeight + topPadding + bottomPadding, + implicitIndicatorHeight + topPadding + bottomPadding) + + padding: 6 + spacing: 6 + + icon.width: 24 + icon.height: 24 + icon.color: control.enabled ? control.colorScheme.text_norm : control.colorScheme.text_disabled + + font.family: Style.font_family + font.weight: Style.fontWeight_400 + font.pixelSize: Style.body_font_size + font.letterSpacing: Style.body_letter_spacing + + contentItem: IconLabel { + id: iconLabel + readonly property real arrowPadding: control.subMenu && control.arrow ? control.arrow.width + control.spacing : 0 + readonly property real indicatorPadding: control.checkable && control.indicator ? control.indicator.width + control.spacing : 0 + leftPadding: !control.mirrored ? indicatorPadding : arrowPadding + rightPadding: control.mirrored ? indicatorPadding : arrowPadding + + spacing: control.spacing + mirrored: control.mirrored + display: control.display + alignment: Qt.AlignLeft + + icon: control.icon + text: control.text + font: control.font + + color: control.enabled ? control.colorScheme.text_norm : control.colorScheme.text_disabled + } + + background: Rectangle { + implicitWidth: 164 + implicitHeight: 36 + radius: 4 + color: control.down ? control.colorScheme.interaction_default_active : control.highlighted ? control.colorScheme.interaction_default_hover : control.colorScheme.interaction_default + } +} diff --git a/internal/frontend/qml/Proton/Popup.qml b/internal/frontend/qml/Proton/Popup.qml new file mode 100644 index 00000000..28cd77f6 --- /dev/null +++ b/internal/frontend/qml/Proton/Popup.qml @@ -0,0 +1,67 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge 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 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge 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 ProtonMail Bridge. If not, see . + +import QtQml 2.12 +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Controls.impl 2.12 +import QtQuick.Templates 2.12 as T + +T.Popup { + id: root + property ColorScheme colorScheme + + Component.onCompleted: { + if (!ApplicationWindow.window) { + return + } + + if (ApplicationWindow.window.popups === undefined) { + return + } + + var obj = this + ApplicationWindow.window.popups.append( { obj } ) + } + + property int popupType: ApplicationWindow.PopupType.Banner + + property bool shouldShow: false + readonly property var occurred: shouldShow ? new Date() : undefined + function open() { + root.shouldShow = true + } + + function close() { + root.shouldShow = false + } + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, + contentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + contentHeight + topPadding + bottomPadding) + + // TODO: Add DropShadow here + + T.Overlay.modal: Rectangle { + color: root.colorScheme.backdrop_norm + } + + T.Overlay.modeless: Rectangle { + color: "transparent" + } +} diff --git a/internal/frontend/qml/Proton/ProtonLabel.qml b/internal/frontend/qml/Proton/ProtonLabel.qml deleted file mode 100644 index 82959a7b..00000000 --- a/internal/frontend/qml/Proton/ProtonLabel.qml +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) 2021 Proton Technologies AG -// -// This file is part of ProtonMail Bridge. -// -// ProtonMail Bridge 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 3 of the License, or -// (at your option) any later version. -// -// ProtonMail Bridge 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 ProtonMail Bridge. If not, see . - -import QtQuick 2.13 -import QtQuick.Controls 2.12 - -Label { - id: root - - color: Style.currentStyle.text_norm - palette.link: Style.currentStyle.interaction_norm - - font.family: ProtonStyle.font_family - font.weight: ProtonStyle.fontWidth_400 - lineHeightMode: Text.FixedHeight - - function putLink(linkURL,linkText) { - return `${linkText}` - } - - state: "title" - states: [ - State { name : "heading" ; PropertyChanges { target : root ; font.pixelSize : Style.heading_font_size ; lineHeight : Style.heading_line_height } }, - State { name : "title" ; PropertyChanges { target : root ; font.pixelSize : Style.title_font_size ; lineHeight : Style.title_line_height } }, - State { name : "lead" ; PropertyChanges { target : root ; font.pixelSize : Style.lead_font_size ; lineHeight : Style.lead_line_height } }, - State { name : "body" ; PropertyChanges { target : root ; font.pixelSize : Style.body_font_size ; lineHeight : Style.body_line_height ; font.letterSpacing : Style.body_letter_spacing } }, - State { name : "caption" ; PropertyChanges { target : root ; font.pixelSize : Style.caption_font_size ; lineHeight : Style.caption_line_height ; font.letterSpacing : Style.caption_letter_spacing } } - ] -} diff --git a/internal/frontend/qml/Proton/RadioButton.qml b/internal/frontend/qml/Proton/RadioButton.qml index 2d5d187c..43e4d26a 100644 --- a/internal/frontend/qml/Proton/RadioButton.qml +++ b/internal/frontend/qml/Proton/RadioButton.qml @@ -21,7 +21,7 @@ import QtQuick.Controls.impl 2.12 import QtQuick.Templates 2.12 as T T.RadioButton { - property var colorScheme: parent.colorScheme ? parent.colorScheme : Style.currentStyle + property ColorScheme colorScheme property bool error: false @@ -44,22 +44,22 @@ T.RadioButton { x: text ? (control.mirrored ? control.width - width - control.rightPadding : control.leftPadding) : control.leftPadding + (control.availableWidth - width) / 2 y: control.topPadding + (control.availableHeight - height) / 2 - color: colorScheme.background_norm + color: control.colorScheme.background_norm border.width: 1 border.color: { if (!control.enabled) { - return colorScheme.field_disabled + return control.colorScheme.field_disabled } if (control.error) { - return colorScheme.signal_danger + return control.colorScheme.signal_danger } if (control.hovered) { - return colorScheme.interaction_norm_hover + return control.colorScheme.interaction_norm_hover } - return colorScheme.field_norm + return control.colorScheme.field_norm } Rectangle { @@ -70,18 +70,18 @@ T.RadioButton { radius: width / 2 color: { if (!control.enabled) { - return colorScheme.field_disabled + return control.colorScheme.field_disabled } if (control.error) { - return colorScheme.signal_danger + return control.colorScheme.signal_danger } if (control.hovered) { - return colorScheme.interaction_norm_hover + return control.colorScheme.interaction_norm_hover } - return colorScheme.interaction_norm + return control.colorScheme.interaction_norm } visible: control.checked } @@ -95,21 +95,21 @@ T.RadioButton { color: { if (!enabled) { - return colorScheme.text_disabled + return control.colorScheme.text_disabled } if (error) { - return colorScheme.signal_danger + return control.colorScheme.signal_danger } - return colorScheme.text_norm + return control.colorScheme.text_norm } font.family: Style.font_family - font.weight: Style.fontWidth_400 - font.pixelSize: 14 - lineHeight: 20 + font.weight: Style.fontWeight_400 + font.pixelSize: Style.body_font_size + lineHeight: Style.body_line_height lineHeightMode: Text.FixedHeight - font.letterSpacing: 0.2 + font.letterSpacing: Style.body_letter_spacing } } diff --git a/internal/frontend/qml/Proton/Style.qml b/internal/frontend/qml/Proton/Style.qml index ef9ec9bf..dd88eefd 100644 --- a/internal/frontend/qml/Proton/Style.qml +++ b/internal/frontend/qml/Proton/Style.qml @@ -32,9 +32,8 @@ QtObject { // property color primay_norm // ... // } - // and instead of "var" later on "ColorScheme" should be used (also in each component) - property var lightStyle: ColorScheme { + property ColorScheme lightStyle: ColorScheme { id: _lightStyle prominent: prominentStyle @@ -105,7 +104,7 @@ QtObject { backdrop_norm: "#7A262A33" } - property var prominentStyle: ColorScheme { + property ColorScheme prominentStyle: ColorScheme { id: _prominentStyle prominent: this @@ -176,7 +175,7 @@ QtObject { backdrop_norm: "#52000000" } - property var darkStyle: ColorScheme { + property ColorScheme darkStyle: ColorScheme { id: _darkStyle prominent: prominentStyle @@ -249,7 +248,7 @@ QtObject { // TODO: if default style should be loaded from somewhere // (i.e. from preferencies file) - it should be loaded here - property var currentStyle: lightStyle + property ColorScheme currentStyle: lightStyle property string font_family: { switch (Qt.platform.os) { @@ -281,15 +280,13 @@ QtObject { property int caption_line_height: 16 property real caption_letter_spacing: 0.4 - property int fontWidth_100: Font.Thin - property int fontWidth_200: Font.Light - property int fontWidth_300: Font.ExtraLight - property int fontWidth_400: Font.Normal - property int fontWidth_500: Font.Medium - property int fontWidth_600: Font.DemiBold - property int fontWidth_700: Font.Bold - property int fontWidth_800: Font.ExtraBold - property int fontWidth_900: Font.Black - - property var transparent: "#00000000" + property int fontWeight_100: Font.Thin + property int fontWeight_200: Font.Light + property int fontWeight_300: Font.ExtraLight + property int fontWeight_400: Font.Normal + property int fontWeight_500: Font.Medium + property int fontWeight_600: Font.DemiBold + property int fontWeight_700: Font.Bold + property int fontWeight_800: Font.ExtraBold + property int fontWeight_900: Font.Black } diff --git a/internal/frontend/qml/Proton/Switch.qml b/internal/frontend/qml/Proton/Switch.qml index 703fd0ef..af66e54f 100644 --- a/internal/frontend/qml/Proton/Switch.qml +++ b/internal/frontend/qml/Proton/Switch.qml @@ -21,7 +21,7 @@ import QtQuick.Controls 2.12 import QtQuick.Controls.impl 2.12 T.Switch { - property var colorScheme: parent.colorScheme ? parent.colorScheme : Style.currentStyle + property ColorScheme colorScheme property bool loading: false @@ -57,9 +57,9 @@ T.Switch { leftPadding: 0 rightPadding: 0 padding: 0 - color: control.enabled || control.loading ? colorScheme.background_norm : colorScheme.background_strong + color: control.enabled || control.loading ? control.colorScheme.background_norm : control.colorScheme.background_strong border.width: control.enabled && !loading ? 1 : 0 - border.color: control.hovered ? colorScheme.field_hover : colorScheme.field_norm + border.color: control.hovered ? control.colorScheme.field_hover : control.colorScheme.field_norm Rectangle { x: Math.max(0, Math.min(parent.width - width, control.visualPosition * parent.width - (width / 2))) @@ -72,22 +72,22 @@ T.Switch { color: { if (!control.enabled) { - return colorScheme.field_disabled + return control.colorScheme.field_disabled } if (control.checked) { if (control.hovered) { - return colorScheme.interaction_norm_hover + return control.colorScheme.interaction_norm_hover } - return colorScheme.interaction_norm + return control.colorScheme.interaction_norm } if (control.hovered) { - return colorScheme.field_hover + return control.colorScheme.field_hover } - return colorScheme.field_norm + return control.colorScheme.field_norm } ColorImage { @@ -114,7 +114,7 @@ T.Switch { width: 18 height: 18 - color: colorScheme.interaction_norm_hover + color: control.colorScheme.interaction_norm_hover source: "../icons/Loader_16.svg" visible: control.loading @@ -137,13 +137,13 @@ T.Switch { text: control.text - color: control.enabled || control.loading ? colorScheme.text_norm : colorScheme.text_disabled + color: control.enabled || control.loading ? control.colorScheme.text_norm : control.colorScheme.text_disabled font.family: Style.font_family - font.weight: Style.fontWidth_400 - font.pixelSize: 14 - lineHeight: 20 + font.weight: Style.fontWeight_400 + font.pixelSize: Style.body_font_size + lineHeight: Style.body_line_height lineHeightMode: Text.FixedHeight - font.letterSpacing: 0.2 + font.letterSpacing: Style.body_letter_spacing } } diff --git a/internal/frontend/qml/Proton/TextArea.qml b/internal/frontend/qml/Proton/TextArea.qml index e170009f..fb4b0a62 100644 --- a/internal/frontend/qml/Proton/TextArea.qml +++ b/internal/frontend/qml/Proton/TextArea.qml @@ -23,7 +23,7 @@ import QtQuick.Templates 2.12 as T Item { id: root - property var colorScheme: parent.colorScheme ? parent.colorScheme : Style.currentStyle + property ColorScheme colorScheme property alias background: control.background property alias bottomInset: control.bottomInset @@ -104,61 +104,53 @@ Item { radius: 4 visible: true - color: colorScheme.background_norm + color: root.colorScheme.background_norm border.color: { if (!control.enabled) { - return colorScheme.field_disabled + return root.colorScheme.field_disabled } if (control.activeFocus) { - return colorScheme.interaction_norm + return root.colorScheme.interaction_norm } if (root.error) { - return colorScheme.signal_danger + return root.colorScheme.signal_danger } if (control.hovered) { - return colorScheme.field_hover + return root.colorScheme.field_hover } - return colorScheme.field_norm + return root.colorScheme.field_norm } border.width: 1 } Label { + colorScheme: root.colorScheme id: label anchors.top: root.top anchors.left: root.left anchors.bottomMargin: 4 - color: root.enabled ? colorScheme.text_norm : colorScheme.text_disabled + color: root.enabled ? root.colorScheme.text_norm : root.colorScheme.text_disabled - font.family: Style.font_family - font.weight: Style.fontWidth_600 - font.pixelSize: 14 - lineHeight: 20 - lineHeightMode: Text.FixedHeight - font.letterSpacing: 0.2 + type: Label.LabelType.Body_semibold } Label { + colorScheme: root.colorScheme id: hint anchors.right: root.right anchors.bottom: controlView.top anchors.bottomMargin: 5 - color: root.enabled ? colorScheme.text_weak : colorScheme.text_disabled + color: root.enabled ? root.colorScheme.text_weak : root.colorScheme.text_disabled - font.family: Style.font_family - font.weight: Style.fontWidth_400 - font.pixelSize: 12 - lineHeight: 16 - lineHeightMode: Text.FixedHeight - font.letterSpacing: 0.4 + type: Label.LabelType.Caption } ColorImage { @@ -168,10 +160,11 @@ Item { anchors.top: assistiveText.top anchors.bottom: assistiveText.bottom source: "../icons/ic-exclamation-circle-filled.svg" - color: colorScheme.signal_danger + color: root.colorScheme.signal_danger } Label { + colorScheme: root.colorScheme id: assistiveText anchors.left: root.error ? errorIcon.right : parent.left @@ -181,22 +174,17 @@ Item { color: { if (!root.enabled) { - return colorScheme.text_disabled + return root.colorScheme.text_disabled } if (root.error) { - return colorScheme.signal_danger + return root.colorScheme.signal_danger } - return colorScheme.text_weak + return root.colorScheme.text_weak } - font.family: Style.font_family - font.weight: root.error ? Style.fontWidth_600 : Style.fontWidth_400 - font.pixelSize: 12 - lineHeight: 16 - lineHeightMode: Text.FixedHeight - font.letterSpacing: 0.4 + type: root.error ? Label.LabelType.Caption_semibold : Label.LabelType.Caption } ScrollView { @@ -222,8 +210,8 @@ Item { padding: 8 leftPadding: 12 - color: control.enabled ? colorScheme.text_norm : colorScheme.text_disabled - placeholderTextColor: control.enabled ? colorScheme.text_hint : colorScheme.text_disabled + color: control.enabled ? root.colorScheme.text_norm : root.colorScheme.text_disabled + placeholderTextColor: control.enabled ? root.colorScheme.text_hint : root.colorScheme.text_disabled selectionColor: control.palette.highlight selectedTextColor: control.palette.highlightedText @@ -231,7 +219,7 @@ Item { cursorDelegate: Rectangle { id: cursor width: 1 - color: colorScheme.interaction_norm + color: root.colorScheme.interaction_norm visible: control.activeFocus && !control.readOnly && control.selectionStart === control.selectionEnd Connections { diff --git a/internal/frontend/qml/Proton/TextField.qml b/internal/frontend/qml/Proton/TextField.qml index 728e8488..fffc12f3 100644 --- a/internal/frontend/qml/Proton/TextField.qml +++ b/internal/frontend/qml/Proton/TextField.qml @@ -21,10 +21,11 @@ import QtQuick.Controls 2.12 import QtQuick.Controls.impl 2.12 import QtQuick.Templates 2.12 as T import QtQuick.Layouts 1.12 +import "." Item { id: root - property var colorScheme: parent.colorScheme ? parent.colorScheme : Style.currentStyle + property ColorScheme colorScheme property alias background: control.background property alias bottomInset: control.bottomInset @@ -91,7 +92,7 @@ Item { property alias hint: hint.text property alias assistiveText: assistiveText.text - property var echoMode: TextInput.Normal + property int echoMode: TextInput.Normal property bool error: false @@ -117,7 +118,7 @@ Item { function selectAll() { control.selectAll() } function selectWord() { control.selectWord() } function undo() { control.undo() } - function forceActiveFocus() {control.forceActiveFocus()} + function forceActiveFocus() { control.forceActiveFocus() } ColumnLayout { anchors.fill: parent @@ -127,22 +128,22 @@ Item { Layout.fillWidth: true spacing: 0 - ProtonLabel { + Label { + colorScheme: root.colorScheme id: label Layout.fillHeight: true Layout.fillWidth: true - color: root.enabled ? colorScheme.text_norm : colorScheme.text_disabled - font.weight: Style.fontWidth_600 - state: "body" + type: Label.LabelType.Body_semibold } - ProtonLabel { + Label { + colorScheme: root.colorScheme id: hint Layout.fillHeight: true Layout.fillWidth: true - color: root.enabled ? colorScheme.text_weak : colorScheme.text_disabled + color: root.enabled ? root.colorScheme.text_weak : root.colorScheme.text_disabled horizontalAlignment: Text.AlignRight - state: "caption" + type: Label.LabelType.Caption } } @@ -157,25 +158,25 @@ Item { radius: 4 visible: true - color: colorScheme.background_norm + color: root.colorScheme.background_norm border.color: { if (!control.enabled) { - return colorScheme.field_disabled + return root.colorScheme.field_disabled } if (control.activeFocus) { - return colorScheme.interaction_norm + return root.colorScheme.interaction_norm } if (root.error) { - return colorScheme.signal_danger + return root.colorScheme.signal_danger } if (control.hovered) { - return colorScheme.field_hover + return root.colorScheme.field_hover } - return colorScheme.field_norm + return root.colorScheme.field_norm } border.width: 1 @@ -193,25 +194,27 @@ Item { Layout.fillWidth: true implicitWidth: implicitBackgroundWidth + leftInset + rightInset - || Math.max(contentWidth, placeholder.implicitWidth) + leftPadding + rightPadding + || Math.max(contentWidth, placeholder.implicitWidth) + leftPadding + rightPadding implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, - contentHeight + topPadding + bottomPadding, - placeholder.implicitHeight + topPadding + bottomPadding) + contentHeight + topPadding + bottomPadding, + placeholder.implicitHeight + topPadding + bottomPadding) padding: 8 leftPadding: 12 - color: control.enabled ? colorScheme.text_norm : colorScheme.text_disabled + color: control.enabled ? root.colorScheme.text_norm : root.colorScheme.text_disabled selectionColor: control.palette.highlight selectedTextColor: control.palette.highlightedText - placeholderTextColor: control.enabled ? colorScheme.text_hint : colorScheme.text_disabled + placeholderTextColor: control.enabled ? root.colorScheme.text_hint : root.colorScheme.text_disabled verticalAlignment: TextInput.AlignVCenter + echoMode: eyeButton.checked ? TextInput.Normal : root.echoMode + cursorDelegate: Rectangle { id: cursor width: 1 - color: colorScheme.interaction_norm + color: root.colorScheme.interaction_norm visible: control.activeFocus && !control.readOnly && control.selectionStart === control.selectionEnd Connections { @@ -268,22 +271,16 @@ Item { } Button { + colorScheme: root.colorScheme id: eyeButton Layout.fillHeight: true visible: root.echoMode === TextInput.Password - icon.source: control.echoMode == TextInput.Password ? "../icons/ic-eye.svg" : "../icons/ic-eye-slash.svg" icon.color: control.color - background: Rectangle{color: "#00000000"} - onClicked: { - if (control.echoMode == TextInput.Password) { - control.echoMode = TextInput.Normal - } else { - control.echoMode = TextInput.Password - } - } - Component.onCompleted: control.echoMode = root.echoMode + background: Item { } + checkable: true + icon.source: checked ? "../icons/ic-eye-slash.svg" : "../icons/ic-eye.svg" } } } @@ -292,17 +289,16 @@ Item { Layout.fillWidth: true spacing: 0 - // FIXME: maybe somewhere in the future there will be an Icon component capable of setting color to the icon - // but before that moment we need to use IconLabel - IconLabel { + ColorImage { id: errorIcon visible: root.error && (assistiveText.text.length > 0) - icon.source: "../icons/ic-exclamation-circle-filled.svg" - icon.color: colorScheme.signal_danger + source: "../icons/ic-exclamation-circle-filled.svg" + color: root.colorScheme.signal_danger } - ProtonLabel { + Label { + colorScheme: root.colorScheme id: assistiveText Layout.fillHeight: true @@ -311,18 +307,17 @@ Item { color: { if (!root.enabled) { - return colorScheme.text_disabled + return root.colorScheme.text_disabled } if (root.error) { - return colorScheme.signal_danger + return root.colorScheme.signal_danger } - return colorScheme.text_weak + return root.colorScheme.text_weak } - font.weight: root.error ? Style.fontWidth_600 : Style.fontWidth_400 - state: "caption" + type: root.error ? Label.LabelType.Caption_semibold : Label.LabelType.Caption } } } diff --git a/internal/frontend/qml/Proton/qmldir b/internal/frontend/qml/Proton/qmldir index 38d28389..4a0b011e 100644 --- a/internal/frontend/qml/Proton/qmldir +++ b/internal/frontend/qml/Proton/qmldir @@ -1,13 +1,36 @@ +# Copyright (c) 2021 Proton Technologies AG +# +# This file is part of ProtonMail Bridge. +# +# ProtonMail Bridge 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 3 of the License, or +# (at your option) any later version. +# +# ProtonMail Bridge 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 ProtonMail Bridge. If not, see . + module QQtQuick.Controls.Proton depends QtQuick.Controls 2.12 singleton ProtonStyle 4.0 Style.qml -Banner 4.0 Banner.qml +ColorScheme 4.0 ColorScheme.qml + +ApplicationWindow 4.0 ApplicationWindow.qml Button 4.0 Button.qml CheckBox 4.0 CheckBox.qml -ProtonLabel 4.0 ProtonLabel.qml -RoundedRectangle 4.0 RoundedRectangle.qml +Dialog 4.0 Dialog.qml +Label 4.0 Label.qml +Menu 4.0 Menu.qml +MenuItem 4.0 MenuItem.qml +Popup 4.0 Popup.qml RadioButton 4.0 RadioButton.qml +RoundedRectangle 4.0 RoundedRectangle.qml Switch 4.0 Switch.qml TextArea 4.0 TextArea.qml TextField 4.0 TextField.qml diff --git a/internal/frontend/qml/SetupGuide.qml b/internal/frontend/qml/SetupGuide.qml index 36386d4c..ae3b902e 100644 --- a/internal/frontend/qml/SetupGuide.qml +++ b/internal/frontend/qml/SetupGuide.qml @@ -18,92 +18,107 @@ import QtQuick 2.13 import QtQuick.Layouts 1.12 +import QtQuick.Controls 2.12 import QtQuick.Controls.impl 2.12 import Proton 4.0 -RowLayout { +Item { id:root - property var colorScheme - property var window + property ColorScheme colorScheme + property var backend - property var user: { "username": "janedoe@protonmail.com" } + property var user - ColumnLayout { - Layout.fillHeight: true - Layout.leftMargin: 80 - Layout.rightMargin: 80 - Layout.topMargin: 30 - Layout.bottomMargin: 70 + signal dismissed() - ProtonLabel { - text: qsTr("Set up email client") - font.weight: ProtonStyle.fontWidth_700 - state: "heading" - } + implicitHeight: children[0].implicitHeight + implicitWidth: children[0].implicitWidth - ProtonLabel { - text: user.username - color: root.colorScheme.text_weak - state: "lead" - } + RowLayout { + anchors.fill: parent + spacing: 0 - ProtonLabel { - Layout.topMargin: 32 - text: qsTr("Choose an email client") - font.weight: ProtonStyle.fontWidth_600 - state: "body" - } + ColumnLayout { + Layout.fillHeight: true + Layout.leftMargin: 80 + Layout.rightMargin: 80 + Layout.topMargin: 30 + Layout.bottomMargin: 70 + spacing: 0 - ListModel { - id: clients - ListElement{name : "Apple Mail" ; iconSource : "./icons/ic-apple-mail.svg" } - ListElement{name : "Microsoft Outlook" ; iconSource : "./icons/ic-microsoft-outlook.svg" } - ListElement{name : "Mozilla Thunderbird" ; iconSource : "./icons/ic-mozilla-thunderbird.svg" } - ListElement{name : "Other" ; iconSource : "./icons/ic-other-mail-clients.svg" } - } + Label { + colorScheme: root.colorScheme + text: qsTr("Set up email client") + type: Label.LabelType.Heading + } + + Label { + colorScheme: root.colorScheme + text: user ? user.username : "" + color: root.colorScheme.text_weak + type: Label.LabelType.Lead + } + + Label { + colorScheme: root.colorScheme + Layout.topMargin: 32 + text: qsTr("Choose an email client") + type: Label.LabelType.Body_semibold + } + + ListModel { + id: clients + ListElement{name : "Apple Mail" ; iconSource : "./icons/ic-apple-mail.svg" } + ListElement{name : "Microsoft Outlook" ; iconSource : "./icons/ic-microsoft-outlook.svg" } + ListElement{name : "Mozilla Thunderbird" ; iconSource : "./icons/ic-mozilla-thunderbird.svg" } + ListElement{name : "Other" ; iconSource : "./icons/ic-other-mail-clients.svg" } + } - Repeater { - model: clients + Repeater { + model: clients - ColumnLayout { - RowLayout { - Layout.topMargin: 12 - Layout.bottomMargin: 12 - Layout.leftMargin: 16 - Layout.rightMargin: 16 + ColumnLayout { + RowLayout { + Layout.topMargin: 12 + Layout.bottomMargin: 12 + Layout.leftMargin: 16 + Layout.rightMargin: 16 - IconLabel { - icon.source: model.iconSource - icon.height: 36 + ColorImage { + source: model.iconSource + height: 36 + } + + Label { + colorScheme: root.colorScheme + Layout.leftMargin: 12 + text: model.name + type: Label.LabelType.Body + } } - ProtonLabel { - Layout.leftMargin: 12 - text: model.name - state: "body" + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: root.colorScheme.border_weak } } - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 1 - color: root.colorScheme.border_weak - } } - } - Item { Layout.fillHeight: true } + Item { Layout.fillHeight: true } - Button { - text: qsTr("Set up later") - flat: true + Button { + colorScheme: root.colorScheme + text: qsTr("Set up later") + flat: true - onClicked: { - root.window.showSetup = false - root.reset() + onClicked: { + user.setupGuideSeen = true + root.dismissed() + } } } } diff --git a/internal/frontend/qml/SignIn.qml b/internal/frontend/qml/SignIn.qml index f98b3276..8698753b 100644 --- a/internal/frontend/qml/SignIn.qml +++ b/internal/frontend/qml/SignIn.qml @@ -25,7 +25,7 @@ import Proton 4.0 Item { id: root - property var colorScheme: parent.colorScheme + property ColorScheme colorScheme function abort() { root.loginAbort(usernameTextField.text) @@ -173,20 +173,22 @@ Item { spacing: 0 - ProtonLabel { + Label { + colorScheme: root.colorScheme text: qsTr("Sign in") Layout.alignment: Qt.AlignHCenter Layout.topMargin: 16 - font.weight: ProtonStyle.fontWidth_700 + type: Label.LabelType.Title } - ProtonLabel { + Label { + colorScheme: root.colorScheme id: subTitle text: qsTr("Enter your Proton Account details.") Layout.alignment: Qt.AlignHCenter Layout.topMargin: 8 color: root.colorScheme.text_weak - state: "body" + type: Label.LabelType.Body } RowLayout { @@ -201,17 +203,18 @@ Item { source: "./icons/ic-exclamation-circle-filled.svg" } - ProtonLabel { + Label { + colorScheme: root.colorScheme id: errorLabel Layout.leftMargin: 4 color: root.colorScheme.signal_danger - font.weight: root.error ? ProtonStyle.fontWidth_600 : ProtonStyle.fontWidth_400 - state: "caption" + type: root.error ? Label.LabelType.Caption_semibold : Label.LabelType.Caption } } TextField { + colorScheme: root.colorScheme id: usernameTextField label: qsTr("Username or email") @@ -236,6 +239,7 @@ Item { } TextField { + colorScheme: root.colorScheme id: passwordTextField label: qsTr("Password") @@ -259,6 +263,7 @@ Item { } Button { + colorScheme: root.colorScheme id: signInButton text: qsTr("Sign in") @@ -308,12 +313,13 @@ Item { } } - ProtonLabel { + Label { + colorScheme: root.colorScheme textFormat: Text.StyledText - text: putLink("https://protonmail.com/upgrade", qsTr("Create or upgrade your account")) + text: link("https://protonmail.com/upgrade", qsTr("Create or upgrade your account")) Layout.alignment: Qt.AlignHCenter Layout.topMargin: 24 - state: "body" + type: Label.LabelType.Body onLinkActivated: { Qt.openUrlExternally(link) @@ -335,16 +341,16 @@ Item { spacing: 0 - ProtonLabel { + Label { + colorScheme: root.colorScheme text: qsTr("Two-factor authentication") Layout.topMargin: 16 Layout.alignment: Qt.AlignCenter - font.weight: ProtonStyle.fontWidth_700 - - + type: Label.LabelType.Heading } TextField { + colorScheme: root.colorScheme id: twoFactorPasswordTextField label: qsTr("Two-factor authentication code") @@ -360,6 +366,7 @@ Item { } Button { + colorScheme: root.colorScheme id: twoFAButton text: loading ? qsTr("Authenticating") : qsTr("Authenticate") @@ -410,19 +417,16 @@ Item { spacing: 0 - ProtonLabel { + Label { + colorScheme: root.colorScheme text: qsTr("Unlock your mailbox") Layout.topMargin: 16 Layout.alignment: Qt.AlignCenter - font.weight: ProtonStyle.fontWidth_700 + type: Label.LabelType.Heading } - - - - - TextField { + colorScheme: root.colorScheme id: secondPasswordTextField label: qsTr("Mailbox password") @@ -439,6 +443,7 @@ Item { } Button { + colorScheme: root.colorScheme id: secondPasswordButton text: loading ? qsTr("Unlocking") : qsTr("Unlock") diff --git a/internal/frontend/qml/Status.qml b/internal/frontend/qml/Status.qml index 9c0bc41a..4d75fc6b 100644 --- a/internal/frontend/qml/Status.qml +++ b/internal/frontend/qml/Status.qml @@ -22,20 +22,165 @@ import QtQuick.Controls 2.12 import QtQuick.Controls.impl 2.12 import Proton 4.0 +import Notifications 1.0 -RowLayout { - id: layout - spacing: 8 +Item { + id: root - ColorImage { - id: image - source: "./icons/ic-connected.svg" - color: ProtonStyle.currentStyle.signal_success + property var backend + property var notifications + property ColorScheme colorScheme + + property int notificationWhitelist: NotificationFilter.FilterConsts.All + property int notificationBlacklist: NotificationFilter.FilterConsts.None + + readonly property Notification activeNotification: notificationFilter.topmost + + implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin + implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin + + NotificationFilter { + id: notificationFilter + + source: root.notifications.all + whitelist: root.notificationWhitelist + blacklist: root.notificationBlacklist + + onTopmostChanged: { + if (!topmost) { + image.source = "./icons/ic-connected.svg" + image.color = root.colorScheme.signal_success + label.text = qsTr("Connected") + label.color = root.colorScheme.signal_success + return; + } + + image.source = topmost.icon + label.text = topmost.text + + switch (topmost.type) { + case Notification.NotificationType.Danger: + image.color = root.colorScheme.signal_danger + label.color = root.colorScheme.signal_danger + break; + case Notification.NotificationType.Warning: + image.color = root.colorScheme.signal_warning + label.color = root.colorScheme.signal_warning + break; + case Notification.NotificationType.Success: + image.color = root.colorScheme.signal_success + label.color = root.colorScheme.signal_success + break; + case Notification.NotificationType.Info: + image.color = root.colorScheme.signal_info + label.color = root.colorScheme.signal_info + break; + } + } } - Label { - id: label - text: "Connected" - color: ProtonStyle.currentStyle.signal_success + RowLayout { + anchors.fill: parent + spacing: 8 + anchors.margins: 12 + + ColorImage { + id: image + } + + Label { + colorScheme: root.colorScheme + id: label + + Layout.fillHeight: true + Layout.fillWidth: true + + wrapMode: Text.WordWrap + + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + } } + + state: "Connected" + states: [ + State { + name: "Connected" + PropertyChanges { + target: image + source: "./icons/ic-connected.svg" + color: ProtonStyle.currentStyle.signal_success + } + PropertyChanges { + target: label + text: qsTr("Connected") + color: ProtonStyle.currentStyle.signal_success + } + }, + State { + name: "No connection" + PropertyChanges { + target: image + source: "./icons/ic-no-connection.svg" + color: ProtonStyle.currentStyle.signal_danger + } + PropertyChanges { + target: label + text: qsTr("No connection") + color: ProtonStyle.currentStyle.signal_danger + } + }, + State { + name: "Outdated" + PropertyChanges { + target: image + source: "./icons/ic-exclamation-circle-filled.svg" + color: ProtonStyle.currentStyle.signal_danger + } + PropertyChanges { + target: label + text: qsTr("Bridge is outdated") + color: ProtonStyle.currentStyle.signal_danger + } + }, + State { + name: "Account changed" + PropertyChanges { + target: image + source: "./icons/ic-exclamation-circle-filled.svg" + color: ProtonStyle.currentStyle.signal_danger + } + PropertyChanges { + target: label + text: qsTr("The address list for your account has changed") + color: ProtonStyle.currentStyle.signal_danger + } + }, + State { + name: "Auto update failed" + PropertyChanges { + target: image + source: "./icons/ic-info-circle-filled.svg" + color: ProtonStyle.currentStyle.signal_info + } + PropertyChanges { + target: label + text: qsTr("Bridge couldn’t update automatically") + color: ProtonStyle.currentStyle.signal_info + } + }, + State { + name: "Update ready" + PropertyChanges { + target: image + source: "./icons/ic-info-circle-filled.svg" + color: ProtonStyle.currentStyle.signal_info + } + PropertyChanges { + target: label + text: qsTr("Bridge update is ready") + color: ProtonStyle.currentStyle.signal_info + } + } + ] } diff --git a/internal/frontend/qml/StatusWindow.qml b/internal/frontend/qml/StatusWindow.qml new file mode 100644 index 00000000..d74ca461 --- /dev/null +++ b/internal/frontend/qml/StatusWindow.qml @@ -0,0 +1,300 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge 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 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge 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 ProtonMail Bridge. If not, see . + +import QtQml 2.12 +import QtQuick 2.13 +import QtQuick.Window 2.13 +import QtQuick.Layouts 1.12 +import QtQuick.Controls 2.13 + +import Proton 4.0 +import ProtonBackend 1.0 +import Notifications 1.0 + +// Because of https://bugreports.qt.io/browse/QTBUG-69777 and other bugs alike it is impossible +// to use Window with flags: Qt.Popup here since it won't close by it's own on click outside. +PopupWindow { + id: root + title: "ProtonMail Bridge" + + height: contentLayout.implicitHeight + width: contentLayout.implicitWidth + + minimumHeight: 201 + minimumWidth: 448 + + property ColorScheme colorScheme: ProtonStyle.currentStyle + + property var backend + property var notifications + + color: "transparent" + + signal showMainWindow() + signal showHelp() + signal showSettings() + signal quit() + + ColumnLayout { + id: contentLayout + + anchors.fill: parent + spacing: 0 + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + Item { + implicitHeight: 12 + Layout.fillWidth: true + clip: true + Rectangle { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + height: parent.height * 2 + radius: 10 + + color: { + if (!statusItem.activeNotification) { + return root.colorScheme.signal_success + } + + switch (statusItem.activeNotification.type) { + case Notification.NotificationType.Danger: + return root.colorScheme.signal_danger + case Notification.NotificationType.Warning: + return root.colorScheme.signal_warning + case Notification.NotificationType.Success: + return root.colorScheme.signal_success + case Notification.NotificationType.Info: + return root.colorScheme.signal_info + } + } + } + } + + Rectangle { + Layout.fillWidth: true + + implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin + implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin + + color: colorScheme.background_norm + + RowLayout { + anchors.fill: parent + + anchors.topMargin: 8 + anchors.bottomMargin: 8 + anchors.leftMargin: 24 + anchors.rightMargin: 24 + + spacing: 8 + + Status { + id: statusItem + + Layout.fillHeight: true + Layout.fillWidth: true + + colorScheme: root.colorScheme + backend: root.backend + notifications: root.notifications + + notificationWhitelist: Notifications.Group.Connection | Notifications.Group.Update | Notifications.Group.Configuration + } + + Button { + colorScheme: root.colorScheme + secondary: true + + visible: (statusItem.activeNotification && statusItem.activeNotification.action) ? true : false + action: statusItem.activeNotification && statusItem.activeNotification.action.length > 0 ? statusItem.activeNotification.action[0] : null + } + } + } + + Rectangle { + Layout.fillWidth: true + height: 1 + color: root.colorScheme.background_norm + + Rectangle { + anchors.fill: parent + anchors.leftMargin: 24 + anchors.rightMargin: 24 + color: root.colorScheme.border_norm + } + } + } + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + + Layout.maximumHeight: accountListView.count ? + accountListView.contentHeight / accountListView.count * 3 + accountListView.anchors.topMargin + accountListView.anchors.bottomMargin : + Number.POSITIVE_INFINITY + + color: root.colorScheme.background_norm + clip: true + + implicitHeight: children[0].contentHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin + implicitWidth: children[0].contentWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin + + ListView { + id: accountListView + + model: root.backend.users + anchors.fill: parent + + anchors.topMargin: 8 + anchors.bottomMargin: 8 + anchors.leftMargin: 24 + anchors.rightMargin: 24 + + interactive: contentHeight > parent.height + snapMode: ListView.SnapToItem + + delegate: Item { + width: ListView.view.width + + implicitHeight: children[0].implicitHeight + implicitWidth: children[0].implicitWidth + + RowLayout { + spacing: 0 + anchors.fill: parent + + AccountDelegate { + Layout.fillWidth: true + + Layout.margins: 12 + + user: modelData + colorScheme: root.colorScheme + + } + Button { + Layout.margins: 12 + colorScheme: root.colorScheme + visible: true + text: "test" + } + } + } + } + } + + Item { + Layout.fillWidth: true + + implicitHeight: children[1].implicitHeight + children[1].anchors.topMargin + children[1].anchors.bottomMargin + implicitWidth: children[1].implicitWidth + children[1].anchors.leftMargin + children[1].anchors.rightMargin + + // background: + clip: true + Rectangle { + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + height: parent.height * 2 + radius: 10 + + color: root.colorScheme.background_weak + } + + RowLayout { + anchors.fill: parent + anchors.margins: 8 + spacing: 0 + + Button { + colorScheme: root.colorScheme + secondary: true + text: qsTr("Open ProtonBridge") + + borderless: true + labelType: Label.LabelType.Caption_semibold + + onClicked: { + root.showMainWindow() + root.visible = false + } + } + + Item { + Layout.fillWidth: true + } + + Button { + colorScheme: root.colorScheme + secondary: true + icon.source: "./icons/ic-three-dots-vertical.svg" + borderless: true + checkable: true + + onClicked: { + menu.open() + } + + Menu { + id: menu + colorScheme: root.colorScheme + modal: true + + y: 0 - height + + MenuItem { + colorScheme: root.colorScheme + text: qsTr("Help") + onClicked: { + root.showHelp() + root.visible = false + } + } + MenuItem { + colorScheme: root.colorScheme + text: qsTr("Settings") + onClicked: { + root.showSettings() + root.visible = false + } + } + MenuItem { + colorScheme: root.colorScheme + text: qsTr("Quit ProtonBridge") + onClicked: { + root.quit() + root.visible = false + } + } + + onClosed: { + parent.checked = false + } + onOpened: { + parent.checked = true + } + } + } + } + } + } +} diff --git a/internal/frontend/qml/WelcomeGuide.qml b/internal/frontend/qml/WelcomeGuide.qml new file mode 100644 index 00000000..26f56ff9 --- /dev/null +++ b/internal/frontend/qml/WelcomeGuide.qml @@ -0,0 +1,294 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge 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 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge 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 ProtonMail Bridge. If not, see . + +import QtQml 2.12 +import QtQuick 2.13 +import QtQuick.Layouts 1.12 +import QtQuick.Controls 2.12 + +import Proton 4.0 + +Item { + id: root + + property ColorScheme colorScheme + + property var backend + property var window + + signal login(string username, string password) + signal login2FA(string username, string code) + signal login2Password(string username, string password) + signal loginAbort(string username) + + implicitHeight: children[0].implicitHeight + implicitWidth: children[0].implicitWidth + + RowLayout { + anchors.fill: parent + spacing: 0 + + Rectangle { + color: root.colorScheme.background_norm + + Layout.fillHeight: true + Layout.fillWidth: true + + implicitHeight: children[0].implicitHeight + implicitWidth: children[0].implicitWidth + + visible: signInItem.currentIndex == 0 + + GridLayout { + anchors.fill: parent + + columnSpacing: 0 + rowSpacing: 0 + + columns: 3 + + // top margin + Item { + Layout.columnSpan: 3 + Layout.fillWidth: true + + // Using binding component here instead of direct binding to avoid binding loop during construction of element + Binding on Layout.preferredHeight { + value: (parent.height - welcomeContentItem.height) / 4 + } + } + + // left margin + Item { + Layout.minimumWidth: 48 + Layout.maximumWidth: 80 + Layout.fillWidth: true + Layout.preferredHeight: welcomeContentItem.height + } + + ColumnLayout { + id: welcomeContentItem + Layout.fillWidth: true + spacing: 0 + + Image { + source: "icons/img-welcome.svg" + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: 16 + } + + Label { + colorScheme: root.colorScheme + text: qsTr("Welcome to\nProtonMail Bridge") + Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true + Layout.topMargin: 16 + + horizontalAlignment: Text.AlignHCenter + + type: Label.LabelType.Heading + } + + Label { + colorScheme: root.colorScheme + id: longTextLabel + text: qsTr("Now you can securely access and manage ProtonMail messages in your favorite email client. Bridge runs in the background and encrypts and decrypts your messages seamlessly.") + Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true + Layout.topMargin: 16 + Layout.preferredWidth: 320 + + wrapMode: Text.WordWrap + + horizontalAlignment: Text.AlignHCenter + + type: Label.LabelType.Body + } + } + + // Right margin + Item { + Layout.minimumWidth: 48 + Layout.maximumWidth: 80 + Layout.fillWidth: true + Layout.preferredHeight: welcomeContentItem.height + } + + // bottom margin + Item { + Layout.columnSpan: 3 + Layout.fillWidth: true + Layout.fillHeight: true + + implicitHeight: children[0].implicitHeight + children[0].anchors.bottomMargin + children[0].anchors.topMargin + implicitWidth: children[0].implicitWidth + + Image { + id: logoImage + source: "icons/product_logos.svg" + + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.topMargin: 48 + anchors.bottomMargin: 48 + } + } + } + } + + Rectangle { + color: (signInItem.currentIndex == 0) ? root.colorScheme.background_weak : root.colorScheme.background_norm + Layout.fillHeight: true + Layout.fillWidth: true + + implicitHeight: children[0].implicitHeight + implicitWidth: children[0].implicitWidth + + RowLayout { + anchors.fill: parent + spacing: 0 + Item { + Layout.fillHeight: true + Layout.fillWidth: true + Layout.preferredWidth: signInItem.currentIndex == 0 ? 0 : parent.width / 4 + + implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin + implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin + + Button { + colorScheme: root.colorScheme + anchors.left: parent.left + anchors.bottom: parent.bottom + + anchors.leftMargin: 80 + anchors.rightMargin: 80 + anchors.topMargin: 80 + anchors.bottomMargin: 80 + + visible: signInItem.currentIndex != 0 + + secondary: true + text: qsTr("Back") + + onClicked: { + signInItem.abort() + } + } + } + + GridLayout { + Layout.fillHeight: true + Layout.fillWidth: true + + columnSpacing: 0 + rowSpacing: 0 + + columns: 3 + + // top margin + Item { + Layout.columnSpan: 3 + Layout.fillWidth: true + + // Using binding component here instead of direct binding to avoid binding loop during construction of element + Binding on Layout.preferredHeight { + value: (parent.height - signInItem.height) / 4 + } + } + + // left margin + Item { + Layout.minimumWidth: 48 + Layout.maximumWidth: 80 + Layout.fillWidth: true + Layout.preferredHeight: signInItem.height + } + + + SignIn { + id: signInItem + colorScheme: root.colorScheme + + Layout.preferredWidth: 320 + Layout.fillWidth: true + + onLogin: { + root.login(username, password) + } + onLogin2FA: { + root.login2FA(username, code) + } + onLogin2Password: { + root.login2Password(username, password) + } + onLoginAbort: { + root.loginAbort(username) + } + + user: (backend.users.count === 1 && backend.users.get(0).loggedIn === false) ? backend.users.get(0) : undefined + backend: root.backend + window: root.window + } + + // Right margin + Item { + Layout.minimumWidth: 48 + Layout.maximumWidth: 80 + Layout.fillWidth: true + Layout.preferredHeight: signInItem.height + } + + // bottom margin + Item { + Layout.columnSpan: 3 + Layout.fillWidth: true + Layout.fillHeight: true + } + } + + Item { + Layout.fillHeight: true + Layout.preferredWidth: signInItem.currentIndex == 0 ? 0 : parent.width / 4 + } + } + } + + states: [ + State { + name: "Page 1" + PropertyChanges { + target: signInItem + currentIndex: 0 + } + }, + State { + name: "Page 2" + PropertyChanges { + target: signInItem + currentIndex: 1 + } + }, + State { + name: "Page 3" + PropertyChanges { + target: signInItem + currentIndex: 2 + } + } + ] + } +} diff --git a/internal/frontend/qml/WelcomeWindow.qml b/internal/frontend/qml/WelcomeWindow.qml deleted file mode 100644 index e9d7f1d4..00000000 --- a/internal/frontend/qml/WelcomeWindow.qml +++ /dev/null @@ -1,297 +0,0 @@ -// Copyright (c) 2021 Proton Technologies AG -// -// This file is part of ProtonMail Bridge. -// -// ProtonMail Bridge 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 3 of the License, or -// (at your option) any later version. -// -// ProtonMail Bridge 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 ProtonMail Bridge. If not, see . - -import QtQml 2.12 -import QtQuick 2.13 -import QtQuick.Layouts 1.12 -import QtQuick.Controls 2.12 - -import Proton 4.0 - -RowLayout { - id: root - - property var colorScheme: parent.colorScheme - - property var backend - property var window - - signal login(string username, string password) - signal login2FA(string username, string code) - signal login2Password(string username, string password) - signal loginAbort(string username) - - spacing: 0 - - Rectangle { - color: root.colorScheme.background_norm - - Layout.fillHeight: true - Layout.fillWidth: true - - implicitHeight: children[0].implicitHeight - implicitWidth: children[0].implicitWidth - - visible: signInItem.currentIndex == 0 - - GridLayout { - anchors.fill: parent - - columnSpacing: 0 - rowSpacing: 0 - - columns: 3 - - // top margin - Item { - Layout.columnSpan: 3 - Layout.fillWidth: true - - // Using binding component here instead of direct binding to avoid binding loop during construction of element - Binding on Layout.preferredHeight { - value: (parent.height - welcomeContentItem.height) / 4 - } - } - - // left margin - Item { - Layout.minimumWidth: 48 - Layout.maximumWidth: 80 - Layout.fillWidth: true - Layout.preferredHeight: welcomeContentItem.height - } - - ColumnLayout { - id: welcomeContentItem - Layout.fillWidth: true - spacing: 0 - - Image { - source: "icons/img-welcome.svg" - Layout.alignment: Qt.AlignHCenter - Layout.topMargin: 16 - } - - Label { - text: qsTr("Welcome to\nProtonMail Bridge") - Layout.alignment: Qt.AlignHCenter - Layout.fillWidth: true - Layout.topMargin: 16 - - color: root.colorScheme.text_norm - - horizontalAlignment: Text.AlignHCenter - - font.family: ProtonStyle.font_family - font.weight: ProtonStyle.fontWidth_700 - font.pixelSize: 28 - lineHeight: 36 - lineHeightMode: Text.FixedHeight - } - - Label { - id: longTextLabel - text: qsTr("Now you can securely access and manage ProtonMail messages in your favorite email client. Bridge runs in the background and encrypts and decrypts your messages seamlessly.") - Layout.alignment: Qt.AlignHCenter - Layout.fillWidth: true - Layout.topMargin: 16 - Layout.preferredWidth: 320 - - color: root.colorScheme.text_norm - wrapMode: Text.WordWrap - - horizontalAlignment: Text.AlignHCenter - - font.family: ProtonStyle.font_family - font.weight: ProtonStyle.fontWidth_400 - font.pixelSize: 14 - lineHeight: 20 - lineHeightMode: Text.FixedHeight - font.letterSpacing: 0.2 - } - } - - // Right margin - Item { - Layout.minimumWidth: 48 - Layout.maximumWidth: 80 - Layout.fillWidth: true - Layout.preferredHeight: welcomeContentItem.height - } - - // bottom margin - Item { - Layout.columnSpan: 3 - Layout.fillWidth: true - Layout.fillHeight: true - - implicitHeight: children[0].implicitHeight + children[0].anchors.bottomMargin + children[0].anchors.topMargin - implicitWidth: children[0].implicitWidth - - Image { - id: logoImage - source: "icons/product_logos.svg" - - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottom: parent.bottom - anchors.topMargin: 48 - anchors.bottomMargin: 48 - } - } - } - } - - Rectangle { - color: (signInItem.currentIndex == 0) ? root.colorScheme.background_weak : root.colorScheme.background_norm - Layout.fillHeight: true - Layout.fillWidth: true - - implicitHeight: children[0].implicitHeight - implicitWidth: children[0].implicitWidth - - RowLayout { - anchors.fill: parent - spacing: 0 - Item { - Layout.fillHeight: true - Layout.fillWidth: true - Layout.preferredWidth: signInItem.currentIndex == 0 ? 0 : parent.width / 4 - - implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin - implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin - - Button { - anchors.left: parent.left - anchors.bottom: parent.bottom - - anchors.leftMargin: 80 - anchors.rightMargin: 80 - anchors.topMargin: 80 - anchors.bottomMargin: 80 - - visible: signInItem.currentIndex != 0 - - secondary: true - text: qsTr("Back") - - onClicked: { - signInItem.abort() - } - } - } - - GridLayout { - Layout.fillHeight: true - Layout.fillWidth: true - - columnSpacing: 0 - rowSpacing: 0 - - columns: 3 - - // top margin - Item { - Layout.columnSpan: 3 - Layout.fillWidth: true - - // Using binding component here instead of direct binding to avoid binding loop during construction of element - Binding on Layout.preferredHeight { - value: (parent.height - signInItem.height) / 4 - } - } - - // left margin - Item { - Layout.minimumWidth: 48 - Layout.maximumWidth: 80 - Layout.fillWidth: true - Layout.preferredHeight: signInItem.height - } - - - SignIn { - id: signInItem - colorScheme: root.colorScheme - - Layout.preferredWidth: 320 - Layout.fillWidth: true - - onLogin: { - root.login(username, password) - } - onLogin2FA: { - root.login2FA(username, code) - } - onLogin2Password: { - root.login2Password(username, password) - } - onLoginAbort: { - root.loginAbort(username) - } - - user: (backend.users.count === 1 && backend.users.get(0).loggedIn === false) ? backend.users.get(0) : undefined - backend: root.backend - window: root.window - } - - // Right margin - Item { - Layout.minimumWidth: 48 - Layout.maximumWidth: 80 - Layout.fillWidth: true - Layout.preferredHeight: signInItem.height - } - - // bottom margin - Item { - Layout.columnSpan: 3 - Layout.fillWidth: true - Layout.fillHeight: true - } - } - - Item { - Layout.fillHeight: true - Layout.preferredWidth: signInItem.currentIndex == 0 ? 0 : parent.width / 4 - } - } - } - - states: [ - State { - name: "Page 1" - PropertyChanges { - target: signInItem - currentIndex: 0 - } - }, - State { - name: "Page 2" - PropertyChanges { - target: signInItem - currentIndex: 1 - } - }, - State { - name: "Page 3" - PropertyChanges { - target: signInItem - currentIndex: 2 - } - } - ] -} diff --git a/internal/frontend/qml/icons/ic-alert.svg b/internal/frontend/qml/icons/ic-alert.svg new file mode 100644 index 00000000..774aa970 --- /dev/null +++ b/internal/frontend/qml/icons/ic-alert.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/internal/frontend/qml/icons/ic-no-connection.svg b/internal/frontend/qml/icons/ic-no-connection.svg new file mode 100644 index 00000000..01ff8235 --- /dev/null +++ b/internal/frontend/qml/icons/ic-no-connection.svg @@ -0,0 +1,3 @@ + + + diff --git a/internal/frontend/qml/icons/ic-success.svg b/internal/frontend/qml/icons/ic-success.svg new file mode 100644 index 00000000..75ce34f0 --- /dev/null +++ b/internal/frontend/qml/icons/ic-success.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/internal/frontend/qml/icons/ic-three-dots-vertical.svg b/internal/frontend/qml/icons/ic-three-dots-vertical.svg new file mode 100644 index 00000000..6f73bc69 --- /dev/null +++ b/internal/frontend/qml/icons/ic-three-dots-vertical.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/internal/frontend/qml/tests/Buttons.qml b/internal/frontend/qml/tests/Buttons.qml index 2a4bc4fe..d98f0bc4 100644 --- a/internal/frontend/qml/tests/Buttons.qml +++ b/internal/frontend/qml/tests/Buttons.qml @@ -23,10 +23,11 @@ import QtQuick.Controls 2.12 import Proton 4.0 RowLayout { - property var colorScheme: parent.colorScheme + property ColorScheme colorScheme // Primary buttons ButtonsColumn { + colorScheme: root.colorScheme Layout.fillWidth: true Layout.fillHeight: true @@ -35,6 +36,7 @@ RowLayout { // Secondary buttons ButtonsColumn { + colorScheme: root.colorScheme Layout.fillWidth: true Layout.fillHeight: true @@ -44,6 +46,7 @@ RowLayout { // Secondary icons ButtonsColumn { + colorScheme: root.colorScheme Layout.fillWidth: true Layout.fillHeight: true @@ -58,6 +61,7 @@ RowLayout { // Icons ButtonsColumn { + colorScheme: root.colorScheme Layout.fillWidth: true Layout.fillHeight: true diff --git a/internal/frontend/qml/tests/ButtonsColumn.qml b/internal/frontend/qml/tests/ButtonsColumn.qml index 3420269e..27749f0e 100644 --- a/internal/frontend/qml/tests/ButtonsColumn.qml +++ b/internal/frontend/qml/tests/ButtonsColumn.qml @@ -22,7 +22,7 @@ import Proton 4.0 ColumnLayout { id: root - property var colorScheme: parent.colorScheme + property ColorScheme colorScheme property string textNormal: "Button" property string iconNormal: "" @@ -33,6 +33,7 @@ ColumnLayout { property bool secondary: false Button { + colorScheme: root.colorScheme Layout.fillWidth: true Layout.minimumHeight: implicitHeight @@ -45,6 +46,7 @@ ColumnLayout { Button { + colorScheme: root.colorScheme Layout.fillWidth: true Layout.minimumHeight: implicitHeight @@ -58,6 +60,7 @@ ColumnLayout { } Button { + colorScheme: root.colorScheme Layout.fillWidth: true Layout.minimumHeight: implicitHeight diff --git a/internal/frontend/qml/tests/CheckBoxes.qml b/internal/frontend/qml/tests/CheckBoxes.qml index 287c61fe..c028bb20 100644 --- a/internal/frontend/qml/tests/CheckBoxes.qml +++ b/internal/frontend/qml/tests/CheckBoxes.qml @@ -23,11 +23,11 @@ import QtQuick.Controls 2.12 import Proton 4.0 RowLayout { - property var colorScheme: parent.colorScheme + id: root + property ColorScheme colorScheme ColumnLayout { Layout.fillWidth: true - property var colorScheme: parent.colorScheme spacing: parent.spacing @@ -61,7 +61,6 @@ RowLayout { ColumnLayout { Layout.fillWidth: true - property var colorScheme: parent.colorScheme spacing: parent.spacing diff --git a/internal/frontend/qml/tests/RadioButtons.qml b/internal/frontend/qml/tests/RadioButtons.qml index 26fcc3b7..1e526c0e 100644 --- a/internal/frontend/qml/tests/RadioButtons.qml +++ b/internal/frontend/qml/tests/RadioButtons.qml @@ -23,37 +23,42 @@ import QtQuick.Controls 2.12 import Proton 4.0 RowLayout { - property var colorScheme: parent.colorScheme + property ColorScheme colorScheme ColumnLayout { Layout.fillWidth: true - property var colorScheme: parent.colorScheme spacing: parent.spacing RadioButton { + colorScheme: root.colorScheme text: "Radio" } RadioButton { + colorScheme: root.colorScheme text: "Radio" error: true } RadioButton { + colorScheme: root.colorScheme text: "Radio" enabled: false } RadioButton { + colorScheme: root.colorScheme text: "" } RadioButton { + colorScheme: root.colorScheme text: "" error: true } RadioButton { + colorScheme: root.colorScheme text: "" enabled: false } @@ -61,38 +66,43 @@ RowLayout { ColumnLayout { Layout.fillWidth: true - property var colorScheme: parent.colorScheme spacing: parent.spacing RadioButton { + colorScheme: root.colorScheme text: "Radio" checked: true } RadioButton { + colorScheme: root.colorScheme text: "Radio" checked: true error: true } RadioButton { + colorScheme: root.colorScheme text: "Radio" checked: true enabled: false } RadioButton { + colorScheme: root.colorScheme text: "" checked: true } RadioButton { + colorScheme: root.colorScheme text: "" checked: true error: true } RadioButton { + colorScheme: root.colorScheme text: "" checked: true enabled: false diff --git a/internal/frontend/qml/tests/Switches.qml b/internal/frontend/qml/tests/Switches.qml index 87be88a9..b021c950 100644 --- a/internal/frontend/qml/tests/Switches.qml +++ b/internal/frontend/qml/tests/Switches.qml @@ -23,11 +23,10 @@ import QtQuick.Controls 2.12 import Proton 4.0 RowLayout { - property var colorScheme: parent.colorScheme + property ColorScheme colorScheme ColumnLayout { Layout.fillWidth: true - property var colorScheme: parent.colorScheme spacing: parent.spacing @@ -62,7 +61,6 @@ RowLayout { ColumnLayout { Layout.fillWidth: true - property var colorScheme: parent.colorScheme spacing: parent.spacing diff --git a/internal/frontend/qml/tests/TestComponents.qml b/internal/frontend/qml/tests/TestComponents.qml index aed175be..45662cb5 100644 --- a/internal/frontend/qml/tests/TestComponents.qml +++ b/internal/frontend/qml/tests/TestComponents.qml @@ -22,42 +22,47 @@ import QtQuick.Controls 2.12 import Proton 4.0 Rectangle { - property var colorScheme + property ColorScheme colorScheme color: colorScheme.background_norm clip: true ColumnLayout { anchors.fill: parent - property var colorScheme: parent.colorScheme spacing: 5 Buttons { + colorScheme: root.colorScheme Layout.fillWidth: true Layout.margins: 20 } TextFields { + colorScheme: root.colorScheme Layout.fillWidth: true Layout.margins: 20 } TextAreas { + colorScheme: root.colorScheme Layout.fillWidth: true Layout.margins: 20 } CheckBoxes { + colorScheme: root.colorScheme Layout.fillWidth: true Layout.margins: 20 } RadioButtons { + colorScheme: root.colorScheme Layout.fillWidth: true Layout.margins: 20 } Switches { + colorScheme: root.colorScheme Layout.fillWidth: true Layout.margins: 20 } diff --git a/internal/frontend/qml/tests/TextAreas.qml b/internal/frontend/qml/tests/TextAreas.qml index 054c3530..72473863 100644 --- a/internal/frontend/qml/tests/TextAreas.qml +++ b/internal/frontend/qml/tests/TextAreas.qml @@ -23,15 +23,16 @@ import QtQuick.Controls 2.12 import Proton 4.0 RowLayout { - property var colorScheme: parent.colorScheme + id: root + property ColorScheme colorScheme ColumnLayout { Layout.fillWidth: true - property var colorScheme: parent.colorScheme spacing: parent.spacing TextArea { + colorScheme: root.colorScheme Layout.fillWidth: true Layout.preferredHeight: 100 @@ -42,6 +43,7 @@ RowLayout { } TextArea { + colorScheme: root.colorScheme Layout.fillWidth: true Layout.preferredHeight: 100 @@ -54,6 +56,7 @@ RowLayout { TextArea { + colorScheme: root.colorScheme Layout.fillWidth: true Layout.preferredHeight: 100 @@ -68,6 +71,7 @@ RowLayout { TextArea { + colorScheme: root.colorScheme Layout.fillWidth: true Layout.preferredHeight: 100 diff --git a/internal/frontend/qml/tests/TextFields.qml b/internal/frontend/qml/tests/TextFields.qml index 9ea770ee..4ede0ea0 100644 --- a/internal/frontend/qml/tests/TextFields.qml +++ b/internal/frontend/qml/tests/TextFields.qml @@ -23,16 +23,16 @@ import QtQuick.Controls 2.12 import Proton 4.0 RowLayout { - property var colorScheme: parent.colorScheme + property ColorScheme colorScheme // Norm ColumnLayout { Layout.fillWidth: true - property var colorScheme: parent.colorScheme spacing: parent.spacing TextField { + colorScheme: root.colorScheme Layout.fillWidth: true placeholderText: "Placeholder" @@ -42,6 +42,7 @@ RowLayout { } TextField { + colorScheme: root.colorScheme Layout.fillWidth: true text: "Value" @@ -53,6 +54,7 @@ RowLayout { TextField { + colorScheme: root.colorScheme Layout.fillWidth: true error: true @@ -65,6 +67,7 @@ RowLayout { TextField { + colorScheme: root.colorScheme Layout.fillWidth: true text: "Value" @@ -80,11 +83,11 @@ RowLayout { // Masked ColumnLayout { Layout.fillWidth: true - property var colorScheme: parent.colorScheme spacing: parent.spacing TextField { + colorScheme: root.colorScheme Layout.fillWidth: true echoMode: TextInput.Password placeholderText: "Password" @@ -94,6 +97,7 @@ RowLayout { } TextField { + colorScheme: root.colorScheme Layout.fillWidth: true text: "Password" @@ -105,6 +109,7 @@ RowLayout { } TextField { + colorScheme: root.colorScheme Layout.fillWidth: true text: "Password" error: true @@ -117,6 +122,7 @@ RowLayout { } TextField { + colorScheme: root.colorScheme Layout.fillWidth: true text: "Password" enabled: false @@ -132,50 +138,31 @@ RowLayout { // Varia ColumnLayout { Layout.fillWidth: true - property var colorScheme: parent.colorScheme spacing: parent.spacing TextField { + colorScheme: root.colorScheme Layout.fillWidth: true placeholderText: "Placeholder" label: "Label" hint: "Hint" - - Rectangle { - anchors.fill: parent - border.color: "red" - border.width: 1 - z: parent.z - 1 - } } TextField { + colorScheme: root.colorScheme Layout.fillWidth: true placeholderText: "Placeholder" assistiveText: "Assistive text" - - Rectangle { - anchors.fill: parent - border.color: "red" - border.width: 1 - z: parent.z - 1 - } } TextField { + colorScheme: root.colorScheme Layout.fillWidth: true placeholderText: "Placeholder" - - Rectangle { - anchors.fill: parent - border.color: "red" - border.width: 1 - z: parent.z - 1 - } } } }