diff --git a/internal/frontend/bridge-gui/bridge-gui/CMakeLists.txt b/internal/frontend/bridge-gui/bridge-gui/CMakeLists.txt index e491356f..77f06e49 100644 --- a/internal/frontend/bridge-gui/bridge-gui/CMakeLists.txt +++ b/internal/frontend/bridge-gui/bridge-gui/CMakeLists.txt @@ -114,6 +114,7 @@ add_executable(bridge-gui EventStreamWorker.cpp EventStreamWorker.h LogUtils.cpp LogUtils.h main.cpp + TrayIcon.cpp TrayIcon.h Pch.h QMLBackend.cpp QMLBackend.h UserList.cpp UserList.h diff --git a/internal/frontend/bridge-gui/bridge-gui/QMLBackend.cpp b/internal/frontend/bridge-gui/bridge-gui/QMLBackend.cpp index eab26749..2435426c 100644 --- a/internal/frontend/bridge-gui/bridge-gui/QMLBackend.cpp +++ b/internal/frontend/bridge-gui/bridge-gui/QMLBackend.cpp @@ -49,6 +49,9 @@ QMLBackend::QMLBackend() /// \param[in] serviceConfig //**************************************************************************************************************************************************** void QMLBackend::init(GRPCConfig const &serviceConfig) { + trayIcon_.reset(new TrayIcon()); + this->setNormalTrayIcon(); + connect(this, &QMLBackend::fatalError, &app(), &AppController::onFatalError); users_ = new UserList(this); @@ -99,6 +102,14 @@ bool QMLBackend::waitForEventStreamReaderToFinish(qint32 timeoutMs) { } +//**************************************************************************************************************************************************** +/// \return The list of users +//**************************************************************************************************************************************************** +UserList const &QMLBackend::users() const { + return *users_; +} + + //**************************************************************************************************************************************************** /// \return The build year as a string (e.g. 2023) //**************************************************************************************************************************************************** @@ -591,7 +602,6 @@ void QMLBackend::toggleIsTelemetryDisabled(bool isDisabled) { } - //**************************************************************************************************************************************************** /// \param[in] scheme the scheme name //**************************************************************************************************************************************************** @@ -861,6 +871,49 @@ void QMLBackend::sendBadEventUserFeedback(QString const &userID, bool doResync) } +//**************************************************************************************************************************************************** +// +//**************************************************************************************************************************************************** +void QMLBackend::setNormalTrayIcon() { + if (trayIcon_) { + trayIcon_->setState(TrayIcon::State::Normal, tr("Connected"), ":/qml/icons/ic-connected.svg"); + } +} + + +//**************************************************************************************************************************************************** +/// \param[in] stateString A string describing the state. +/// \param[in] statusIcon The path of the status icon. +//**************************************************************************************************************************************************** +void QMLBackend::setErrorTrayIcon(QString const &stateString, QString const &statusIcon) { + if (trayIcon_) { + trayIcon_->setState(TrayIcon::State::Error, stateString, statusIcon); + } +} + + +//**************************************************************************************************************************************************** +/// \param[in] stateString A string describing the state. +/// \param[in] statusIcon The path of the status icon. +//**************************************************************************************************************************************************** +void QMLBackend::setWarnTrayIcon(QString const &stateString, QString const &statusIcon) { + if (trayIcon_) { + trayIcon_->setState(TrayIcon::State::Warn, stateString, statusIcon); + } +} + + +//**************************************************************************************************************************************************** +/// \param[in] stateString A string describing the state. +/// \param[in] statusIcon The path of the status icon. +//**************************************************************************************************************************************************** +void QMLBackend::setUpdateTrayIcon(QString const &stateString, QString const &statusIcon) { + if (trayIcon_) { + trayIcon_->setState(TrayIcon::State::Update, stateString, statusIcon); + } +} + + //**************************************************************************************************************************************************** /// \param[in] imapPort The IMAP port. /// \param[in] smtpPort The SMTP port. @@ -915,7 +968,7 @@ void QMLBackend::onLoginAlreadyLoggedIn(QString const &userID) { //**************************************************************************************************************************************************** /// \param[in] userID The userID. //**************************************************************************************************************************************************** -void QMLBackend::onUserBadEvent(QString const &userID, QString const& ) { +void QMLBackend::onUserBadEvent(QString const &userID, QString const &) { HANDLE_EXCEPTION( if (badEventDisplayQueue_.contains(userID)) { app().log().error("Received 'bad event' for a user that is already in the queue."); @@ -944,8 +997,9 @@ void QMLBackend::onIMAPLoginFailed(QString const &username) { if ((!user) || (user->state() != UserState::SignedOut)) { // We want to pop-up only if a signed-out user has been detected return; } - if (user->isInIMAPLoginFailureCooldown()) + if (user->isInIMAPLoginFailureCooldown()) { return; + } user->startImapLoginFailureCooldown(60 * 60 * 1000); // 1 hour cooldown during which we will not display this notification to this user again. emit selectUser(user->id()); emit imapLoginWhileSignedOut(username); diff --git a/internal/frontend/bridge-gui/bridge-gui/QMLBackend.h b/internal/frontend/bridge-gui/bridge-gui/QMLBackend.h index 2afbc8ee..afde837e 100644 --- a/internal/frontend/bridge-gui/bridge-gui/QMLBackend.h +++ b/internal/frontend/bridge-gui/bridge-gui/QMLBackend.h @@ -22,6 +22,7 @@ #include "MacOS/DockIcon.h" #include "BuildConfig.h" +#include "TrayIcon.h" #include "UserList.h" #include #include @@ -43,6 +44,7 @@ public: // member functions. QMLBackend &operator=(QMLBackend &&) = delete; ///< Disabled move assignment operator. void init(GRPCConfig const &serviceConfig); ///< Initialize the backend. bool waitForEventStreamReaderToFinish(qint32 timeoutMs); ///< Wait for the event stream reader to finish. + UserList const& users() const; ///< Return the list of users // invokable methods can be called from QML. They generally return a value, which slots cannot do. Q_INVOKABLE static QString buildYear(); ///< Return the application build year. @@ -178,6 +180,10 @@ public slots: // slot for signals received from QML -> To be forwarded to Bridge void onVersionChanged(); ///< Slot for the version change signal. void setMailServerSettings(int imapPort, int smtpPort, bool useSSLForIMAP, bool useSSLForSMTP) const; ///< Forwards a connection mode change request from QML to gRPC void sendBadEventUserFeedback(QString const &userID, bool doResync); ///< Slot the providing user feedback for a bad event. + void setNormalTrayIcon(); ///< Set the tray icon to normal. + void setErrorTrayIcon(QString const& stateString, QString const &statusIcon); ///< Set the tray icon to 'error' state. + void setWarnTrayIcon(QString const& stateString, QString const &statusIcon); ///< Set the tray icon to 'warn' state. + void setUpdateTrayIcon(QString const& stateString, QString const &statusIcon); ///< Set the tray icon to 'update' state. public slots: // slot for signals received from gRPC that need transformation instead of simple forwarding void onMailServerSettingsChanged(int imapPort, int smtpPort, bool useSSLForIMAP, bool useSSLForSMTP); ///< Slot for the ConnectionModeChanged gRPC event. @@ -237,8 +243,10 @@ signals: // Signals received from the Go backend, to be forwarded to QML void bugReportSendError(); ///< Signal for the 'bugReportSendError' gRPC stream event. void showMainWindow(); ///< Signal for the 'showMainWindow' gRPC stream event. void hideMainWindow(); ///< Signal for the 'hideMainWindow' gRPC stream event. + void showHelp(); ///< Signal for the 'showHelp' event (from the context menu). + void showSettings(); ///< Signal for the 'showHelp' event (from the context menu). + void selectUser(QString const& userID); ///< Signal emitted in order to selected a user with a given ID in the list. void genericError(QString const &title, QString const &description); ///< Signal for the 'genericError' gRPC stream event. - void selectUser(QString const); ///< Signal that request the given user account to be displayed. void imapLoginWhileSignedOut(QString const& username); ///< Signal for the notification of IMAP login attempt on a signed out account. // This signal is emitted when an exception is intercepted is calls triggered by QML. QML engine would intercept the exception otherwise. @@ -261,7 +269,7 @@ private: // data members bool useSSLForIMAP_ { false }; ///< The cached value for useSSLForIMAP. bool useSSLForSMTP_ { false }; ///< The cached value for useSSLForSMTP. QList badEventDisplayQueue_; ///< THe queue for displaying 'bad event feedback request dialog'. - + std::unique_ptr trayIcon_; friend class AppController; }; diff --git a/internal/frontend/bridge-gui/bridge-gui/Resources.qrc b/internal/frontend/bridge-gui/bridge-gui/Resources.qrc index 640fbbb7..f5a078f5 100644 --- a/internal/frontend/bridge-gui/bridge-gui/Resources.qrc +++ b/internal/frontend/bridge-gui/bridge-gui/Resources.qrc @@ -5,11 +5,7 @@ qml/AccountView.qml qml/Banner.qml qml/Bridge.qml - qml/Bridge_test.qml qml/bridgeqml.qmlproject - qml/BridgeTest/UserControl.qml - qml/BridgeTest/UserList.qml - qml/BridgeTest/UserModel.qml qml/BugReportView.qml qml/Configuration.qml qml/ConfigurationItem.qml @@ -28,6 +24,7 @@ qml/icons/ic-connected.svg qml/icons/ic-copy.svg qml/icons/ic-cross-close.svg + qml/icons/ic-dot.svg qml/icons/ic-drive.svg qml/icons/ic-exclamation-circle-filled.svg qml/icons/ic-external-link.svg @@ -104,17 +101,6 @@ qml/ConnectionModeSettings.qml qml/SplashScreen.qml qml/Status.qml - qml/StatusWindow.qml - qml/tests/Buttons.qml - qml/tests/ButtonsColumn.qml - qml/tests/CheckBoxes.qml - qml/tests/ComboBoxes.qml - qml/tests/RadioButtons.qml - qml/tests/Switches.qml - qml/tests/Test.qml - qml/tests/TestComponents.qml - qml/tests/TextAreas.qml - qml/tests/TextFields.qml qml/WelcomeGuide.qml diff --git a/internal/frontend/bridge-gui/bridge-gui/TrayIcon.cpp b/internal/frontend/bridge-gui/bridge-gui/TrayIcon.cpp new file mode 100644 index 00000000..2a16c740 --- /dev/null +++ b/internal/frontend/bridge-gui/bridge-gui/TrayIcon.cpp @@ -0,0 +1,250 @@ +// Copyright (c) 2023 Proton AG +// +// This file is part of Proton Mail Bridge. +// +// Proton Mail 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. +// +// Proton Mail 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 Proton Mail Bridge. If not, see . + + +#include "TrayIcon.h" +#include "QMLBackend.h" +#include +#include + + +using namespace bridgepp; + + +namespace { + + +QColor const normalColor(30, 168, 133); /// The normal state color. +QColor const errorColor(220, 50, 81); ///< The error state color. +QColor const warnColor(255, 153, 0); ///< The warn state color. +QColor const updateColor(35, 158, 206); ///< The warn state color. +QColor const greyColor(112, 109, 107); ///< The grey color. + + +//**************************************************************************************************************************************************** +/// \brief Create a single resolution icon from an image. Throw an exception in case of failure. +//**************************************************************************************************************************************************** +QIcon loadIconFromImage(QString const &path) { + QPixmap const pixmap(path); + if (pixmap.isNull()) { + throw Exception(QString("Could create icon from image '%1'.").arg(path)); + } + return QIcon(pixmap); +} + + +//**************************************************************************************************************************************************** +/// \brief Retrieve the color associated with a tray icon state. +/// +/// \param[in] state The state. +/// \return The color associated with a given tray icon state. +//**************************************************************************************************************************************************** +QColor stateColor(TrayIcon::State state) { + switch (state) { + case TrayIcon::State::Normal: + return normalColor; + case TrayIcon::State::Error: + return errorColor; + case TrayIcon::State::Warn: + return warnColor; + case TrayIcon::State::Update: + return updateColor; + default: + app().log().error(QString("Unknown tray icon state %1.").arg(static_cast(state))); + return normalColor; + } +} + + +//**************************************************************************************************************************************************** +/// \brief Return the text identifying a state in resource file names. +/// +/// \param[in] state The state. +/// \param[in] The text identifying the state in resource file names. +//**************************************************************************************************************************************************** +QString stateText(TrayIcon::State state) { + switch (state) { + case TrayIcon::State::Normal: + return "norm"; + case TrayIcon::State::Error: + return "error"; + case TrayIcon::State::Warn: + return "warn"; + case TrayIcon::State::Update: + return "update"; + default: + app().log().error(QString("Unknown tray icon state %1.").arg(static_cast(state))); + return "norm"; + } +} + + +} // anonymous namespace + + +//**************************************************************************************************************************************************** +// +//**************************************************************************************************************************************************** +TrayIcon::TrayIcon() + : QSystemTrayIcon() + , menu_(new QMenu) { + + this->generateDotIcons(); + this->setContextMenu(menu_.get()); + + connect(menu_.get(), &QMenu::aboutToShow, this, &TrayIcon::onMenuAboutToShow); + connect(this, &TrayIcon::selectUser, &app().backend(), &QMLBackend::selectUser); + connect(this, &TrayIcon::activated, this, &TrayIcon::onActivated); + + this->show(); + this->setState(State::Normal, QString(), QString()); +} + + +//**************************************************************************************************************************************************** +// +//**************************************************************************************************************************************************** +void TrayIcon::onMenuAboutToShow() { + this->refreshContextMenu(); +} + + +//**************************************************************************************************************************************************** +// +//**************************************************************************************************************************************************** +void TrayIcon::onUserClicked() { + try { + auto action = dynamic_cast(this->sender()); + if (!action) { + throw Exception("Could not retrieve context menu action."); + } + + QString const &userID = action->data().toString(); + if (userID.isNull()) { + throw Exception("Could not retrieve context menu's selected user."); + } + + emit selectUser(userID); + } catch (Exception const &e) { + app().log().error(e.qwhat()); + } +} + + +//**************************************************************************************************************************************************** +/// \param[in] reason The icon activation reason. +//**************************************************************************************************************************************************** +void TrayIcon::onActivated(QSystemTrayIcon::ActivationReason reason) { + if ((QSystemTrayIcon::Trigger == reason) && !onMacOS()) { + app().backend().showMainWindow(); + } +} + + +//**************************************************************************************************************************************************** +// +//**************************************************************************************************************************************************** +void TrayIcon::generateDotIcons() { + QPixmap dotSVG(":/qml/icons/ic-dot.svg"); + struct IconColor { + QIcon &icon; + QColor color; + }; + for (auto pair: QList {{ greenDot_, normalColor }, { greyDot_, greyColor }, { orangeDot_, warnColor }}) { + QPixmap p = dotSVG; + QPainter painter(&p); + painter.setCompositionMode(QPainter::CompositionMode_SourceIn); + painter.fillRect(p.rect(), pair.color); + painter.end(); + pair.icon = QIcon(p); + } +} + + +//**************************************************************************************************************************************************** +/// \param[in] state The state. +/// \param[in] stateString A string describing the state. +/// \param[in] statusIconPath The status icon path. +/// \param[in] statusIconColor The color for the status icon in hex. +//**************************************************************************************************************************************************** +void TrayIcon::setState(TrayIcon::State state, QString const &stateString, QString const &statusIconPath) { + stateString_ = stateString; + state_ = state; + QString const style = onMacOS() ? "mono" : "color"; + QString const text = stateText(state); + + + QIcon icon = loadIconFromImage(QString(":/qml/icons/systray-%1-%2.png").arg(style, text)); + icon.setIsMask(true); + this->setIcon(icon); + + this->generateStatusIcon(statusIconPath, stateColor(state)); +} + + +//**************************************************************************************************************************************************** +/// \param[in] svgPath The path of the SVG file for the icon. +/// \param[in] color The color to apply to the icon. +//**************************************************************************************************************************************************** +void TrayIcon::generateStatusIcon(QString const &svgPath, QColor const &color) { + // We use the SVG path as pixmap mask and fill it with the appropriate color + QString resourcePath = svgPath; + resourcePath.replace(QRegularExpression(R"(^\.\/)"), ":/qml/"); // QML resource path are a bit different from the Qt resources path. + QPixmap pixmap(resourcePath); + QPainter painter(&pixmap); + painter.setCompositionMode(QPainter::CompositionMode_SourceIn); + painter.fillRect(pixmap.rect(), color); + painter.end(); + statusIcon_ = QIcon(pixmap); +} + + +//********************************************************************************************************************** +// +//********************************************************************************************************************** +void TrayIcon::refreshContextMenu() { + if (!menu_) { + app().log().error("Native tray icon context menu is null."); + return; + } + + menu_->clear(); + menu_->addAction(statusIcon_, stateString_, &app().backend(), &QMLBackend::showMainWindow); + menu_->addSeparator(); + UserList const &users = app().backend().users(); + qint32 const userCount = users.count(); + for (qint32 i = 0; i < userCount; i++) { + User const &user = *users.get(i); + UserState const state = user.state(); + auto action = new QAction(user.primaryEmailOrUsername()); + action->setIcon((UserState::Connected == state) ? greenDot_ : (UserState::Locked == state ? orangeDot_ : greyDot_)); + action->setData(user.id()); + connect(action, &QAction::triggered, this, &TrayIcon::onUserClicked); + if (i < 10) { + action->setShortcut(QKeySequence(QString("Ctrl+%1").arg((i + 1) % 10))); + } + menu_->addAction(action); + } + if (userCount) { + menu_->addSeparator(); + } + menu_->addAction(tr("&Open Bridge"), QKeySequence("Ctrl+O"), &app().backend(), &QMLBackend::showMainWindow); + menu_->addAction(tr("&Help"), QKeySequence("Ctrl+F1"), &app().backend(), &QMLBackend::showHelp); + menu_->addAction(tr("&Settings"), QKeySequence("Ctrl+,"), &app().backend(), &QMLBackend::showSettings); + menu_->addSeparator(); + menu_->addAction(tr("&Quit Bridge"), QKeySequence("Ctrl+Q"), &app().backend(), &QMLBackend::quit); +} diff --git a/internal/frontend/bridge-gui/bridge-gui/TrayIcon.h b/internal/frontend/bridge-gui/bridge-gui/TrayIcon.h new file mode 100644 index 00000000..8517ec8f --- /dev/null +++ b/internal/frontend/bridge-gui/bridge-gui/TrayIcon.h @@ -0,0 +1,70 @@ +// Copyright (c) 2023 Proton AG +// +// This file is part of Proton Mail Bridge. +// +// Proton Mail 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. +// +// Proton Mail 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 Proton Mail Bridge. If not, see . + + +#ifndef BRIDGE_GUI_NATIVE_TRAY_ICON_H +#define BRIDGE_GUI_NATIVE_TRAY_ICON_H + + +//********************************************************************************************************************** +/// \brief A native tray icon. +//********************************************************************************************************************** +class TrayIcon: public QSystemTrayIcon { +Q_OBJECT +public: // typedef enum + enum class State { + Normal, + Error, + Warn, + Update, + }; ///< Enumeration for the state. + +public: // data members + TrayIcon(); ///< Default constructor. + ~TrayIcon() override = default; ///< Destructor. + TrayIcon(TrayIcon const&) = delete; ///< Disabled copy-constructor. + TrayIcon(TrayIcon&&) = delete; ///< Disabled assignment copy-constructor. + TrayIcon& operator=(TrayIcon const&) = delete; ///< Disabled assignment operator. + TrayIcon& operator=(TrayIcon&&) = delete; ///< Disabled move assignment operator. + void setState(State state, QString const& stateString, QString const &statusIconPath); ///< Set the state of the icon + +signals: + void selectUser(QString const& userID); ///< Signal for selecting a user with a given userID + +private slots: + void onMenuAboutToShow(); ///< Slot called before the context menu is shown. + void onUserClicked(); ///< Slot triggered when clicking on a user in the context menu. + static void onActivated(QSystemTrayIcon::ActivationReason reason); ///< Slot for the activation of the system tray icon. + +private: // member functions. + void generateDotIcons(); ///< generate the colored dot icons used for user status. + void generateStatusIcon(QString const &svgPath, QColor const& color); ///< Generate the status icon. + void refreshContextMenu(); ///< Refresh the context menu. + +private: // data members + State state_ { State::Normal }; ///< The state of the tray icon. + QString stateString_; ///< The current state string. + std::unique_ptr menu_; ///< The context menu for the tray icon. Not owned by the tray icon. + QIcon statusIcon_; ///< The path of the status icon displayed in the context menu. + QIcon greenDot_; ///< The green dot icon. + QIcon greyDot_; ///< The grey dot icon. + QIcon orangeDot_; ///< The orange dot icon. +}; + + + +#endif //BRIDGE_GUI_NATIVE_TRAY_ICON_H diff --git a/internal/frontend/bridge-gui/bridge-gui/qml/Bridge.qml b/internal/frontend/bridge-gui/bridge-gui/qml/Bridge.qml index 8d116b3a..3e377ad7 100644 --- a/internal/frontend/bridge-gui/bridge-gui/qml/Bridge.qml +++ b/internal/frontend/bridge-gui/bridge-gui/qml/Bridge.qml @@ -26,11 +26,8 @@ import Notifications QtObject { id: root - function isInInterval(num, lower_limit, upper_limit) { - return lower_limit <= num && num <= upper_limit - } - function bound(num, lower_limit, upper_limit) { - return Math.max(lower_limit, Math.min(upper_limit, num)) + function bound(num, lowerLimit, upperLimit) { + return Math.max(lowerLimit, Math.min(upperLimit, num)) } property var title: Backend.appname @@ -38,10 +35,30 @@ QtObject { property Notifications _notifications: Notifications { id: notifications frontendMain: mainWindow - frontendStatus: statusWindow - frontendTray: trayIcon } + property NotificationFilter _trayNotificationFilter: NotificationFilter { + id: trayNotificationFilter + source: root._notifications ? root._notifications.all : undefined + onTopmostChanged: { + if (topmost) { + switch (topmost.type) { + case Notification.NotificationType.Danger: + Backend.setErrorTrayIcon(topmost.brief, topmost.icon) + return + case Notification.NotificationType.Warning: + Backend.setWarnTrayIcon(topmost.brief, topmost.icon) + return + case Notification.NotificationType.Info: + Backend.setUpdateTrayIcon(topmost.brief, topmost.icon) + return + } + } + Backend.setNormalTrayIcon() + } + } + + property MainWindow _mainWindow: MainWindow { id: mainWindow visible: false @@ -66,190 +83,6 @@ QtObject { } } - property StatusWindow _statusWindow: StatusWindow { - id: statusWindow - visible: false - - title: root.title - notifications: root._notifications - - onShowMainWindow: { - mainWindow.showAndRise() - } - - onShowHelp: { - mainWindow.showHelp() - mainWindow.showAndRise() - } - - onShowSettings: { - mainWindow.showSettings() - mainWindow.showAndRise() - } - - onSelectUser: function(userID) { - mainWindow.selectUser(userID) - mainWindow.showAndRise() - } - - onQuit: { - mainWindow.hide() - trayIcon.visible = false - Backend.quit() - } - - property rect screenRect - property rect iconRect - - // use binding from function with width and height as arguments so it will be recalculated every time width and height are changed - property point position: getPosition(width, height) - x: position.x - y: position.y - - function getPosition(_width, _height) { - if (screenRect.width === 0 || screenRect.height === 0) { - return Qt.point(0, 0) - } - - var _x = 0 - var _y = 0 - - // fit above - _y = iconRect.top - height - if (isInInterval(_y, screenRect.top, screenRect.bottom - height)) { - // position preferably in the horizontal center but bound to the screen rect - _x = bound(iconRect.left + (iconRect.width - width)/2, screenRect.left, screenRect.right - width) - return Qt.point(_x, _y) - } - - // fit below - _y = iconRect.bottom - if (isInInterval(_y, screenRect.top, screenRect.bottom - height)) { - // position preferably in the horizontal center but bound to the screen rect - _x = bound(iconRect.left + (iconRect.width - width)/2, screenRect.left, screenRect.right - width) - return Qt.point(_x, _y) - } - - // fit to the left - _x = iconRect.left - width - if (isInInterval(_x, screenRect.left, screenRect.right - width)) { - // position preferably in the vertical center but bound to the screen rect - _y = bound(iconRect.top + (iconRect.height - height)/2, screenRect.top, screenRect.bottom - height) - return Qt.point(_x, _y) - } - - // fit to the right - _x = iconRect.right - if (isInInterval(_x, screenRect.left, screenRect.right - width)) { - // position preferably in the vertical center but bound to the screen rect - _y = bound(iconRect.top + (iconRect.height - height)/2, screenRect.top, screenRect.bottom - height) - return Qt.point(_x, _y) - } - - // Fallback: position status window right above icon and let window manager decide. - console.warn("Can't position status window: screenRect =", screenRect, "iconRect =", iconRect) - _x = bound(iconRect.left + (iconRect.width - width)/2, screenRect.left, screenRect.right - width) - _y = bound(iconRect.top + (iconRect.height - height)/2, screenRect.top, screenRect.bottom - height) - return Qt.point(_x, _y) - } - } - - property SystemTrayIcon _trayIcon: SystemTrayIcon { - id: trayIcon - visible: true - icon.source: getTrayIconPath() - icon.mask: true // make sure that systems like macOS will use proper color - tooltip: `${root.title} v${Backend.version}` - onActivated: function(reason) { - function calcStatusWindowPosition() { - // On some platforms (X11 / Plasma) Qt does not provide icon position and geometry info. - // In this case we rely on cursor position - var iconRect = Qt.rect(geometry.x, geometry.y, geometry.width, geometry.height) - if (geometry.width == 0 && geometry.height == 0) { - var mousePos = Backend.getCursorPos() - iconRect.x = mousePos.x - iconRect.y = mousePos.y - iconRect.width = 0 - iconRect.height = 0 - } - - // Find screen - var screen - for (var i in Qt.application.screens) { - var _screen = Qt.application.screens[i] - if ( - isInInterval(iconRect.x, _screen.virtualX, _screen.virtualX + _screen.width) && - isInInterval(iconRect.y, _screen.virtualY, _screen.virtualY + _screen.height) - ) { - screen = _screen - break - } - } - if (!screen) { - // Fallback to primary screen - screen = Qt.application.screens[0] - } - - // In case we used mouse to detect icon position - we want to make a fake icon rectangle from a point - if (iconRect.width == 0 && iconRect.height == 0) { - iconRect.x = bound(iconRect.x - 16, screen.virtualX, screen.virtualX + screen.width - 32) - iconRect.y = bound(iconRect.y - 16, screen.virtualY, screen.virtualY + screen.height - 32) - iconRect.width = 32 - iconRect.height = 32 - } - - statusWindow.screenRect = Qt.rect(screen.virtualX, screen.virtualY, screen.width, screen.height) - statusWindow.iconRect = iconRect - } - - function toggleWindow(win) { - if (win.visible) { - win.close() - } else { - win.showAndRise() - } - } - - - switch (reason) { - case SystemTrayIcon.Unknown: - break; - case SystemTrayIcon.Context: - case SystemTrayIcon.Trigger: - case SystemTrayIcon.DoubleClick: - case SystemTrayIcon.MiddleClick: - calcStatusWindowPosition() - toggleWindow(statusWindow) - break; - default: - break; - } - } - - property NotificationFilter _systrayfilter: NotificationFilter { - source: root._notifications ? root._notifications.all : undefined - } - - function getTrayIconPath() { - var color = Backend.goos == "darwin" ? "mono" : "color" - - var level = "norm" - if (_systrayfilter.topmost) { - switch (_systrayfilter.topmost.type) { - case Notification.NotificationType.Danger: - level = "error" - break; - case Notification.NotificationType.Warning: - level = "warn" - break; - case Notification.NotificationType.Info: - level = "update" - break; - } - } - return `qrc:/qml/icons/systray-${color}-${level}.png` - } - } Component.onCompleted: { if (!Backend) { @@ -266,7 +99,7 @@ QtObject { var c = Backend.users.count var u = Backend.users.get(0) // DEBUG - if (c != 0) { + if (c !== 0) { console.log("users non zero", c) console.log("first user", u ) } @@ -290,7 +123,7 @@ QtObject { } function setColorScheme() { - if (Backend.colorSchemeName == "light") ProtonStyle.currentStyle = ProtonStyle.lightStyle - if (Backend.colorSchemeName == "dark") ProtonStyle.currentStyle = ProtonStyle.darkStyle + if (Backend.colorSchemeName === "light") ProtonStyle.currentStyle = ProtonStyle.lightStyle + if (Backend.colorSchemeName === "dark") ProtonStyle.currentStyle = ProtonStyle.darkStyle } } diff --git a/internal/frontend/bridge-gui/bridge-gui/qml/BridgeTest/UserControl.qml b/internal/frontend/bridge-gui/bridge-gui/qml/BridgeTest/UserControl.qml deleted file mode 100644 index 349087ae..00000000 --- a/internal/frontend/bridge-gui/bridge-gui/qml/BridgeTest/UserControl.qml +++ /dev/null @@ -1,315 +0,0 @@ -// Copyright (c) 2023 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail 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. -// -// Proton Mail 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 Proton Mail Bridge. If not, see . - -import QtQml -import QtQuick -import QtQuick.Layouts -import QtQuick.Controls - -import Proton - -ColumnLayout { - id: root - - property var user - property var userIndex - - spacing : 5 - - Layout.fillHeight: true - //Layout.fillWidth: true - - property ColorScheme colorScheme - - TextField { - colorScheme: root.colorScheme - Layout.fillWidth: true - - text: user !== undefined ? user.username : "" - - onEditingFinished: { - user.username = text - } - } - - ColumnLayout { - Layout.fillWidth: true - - Switch { - id: userLoginSwitch - colorScheme: root.colorScheme - - text: "LoggedIn" - enabled: user !== undefined && user.username.length > 0 - - checked: user ? root.user.state == EUserState.Connected : false - - onCheckedChanged: { - if (!user) { - return - } - - 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.state = EUserState.Connected - user.resetLoginRequests() - return - } else { - user.state = EUserState.SignedOut - user.resetLoginRequests() - } - } - } - - Switch { - colorScheme: root.colorScheme - - text: "Setup guide seen" - enabled: user !== undefined && user.username.length > 0 - - checked: user ? user.setupGuideSeen : false - - onCheckedChanged: { - if (!user) { - return - } - - user.setupGuideSeen = checked - } - } - } - - - RowLayout { - Layout.fillWidth: true - - Label { - colorScheme: root.colorScheme - id: loginLabel - text: "Login:" - - Layout.preferredWidth: Math.max(loginLabel.implicitWidth, faLabel.implicitWidth, passLabel.implicitWidth) - } - - Button { - colorScheme: root.colorScheme - text: "name/pass error" - enabled: user !== undefined //&& user.isLoginRequested && !user.isLogin2FARequested && !user.isLogin2PasswordProvided - - onClicked: { - Backend.loginUsernamePasswordError("") - user.resetLoginRequests() - } - } - - Button { - colorScheme: root.colorScheme - text: "free user error" - enabled: user !== undefined //&& user.isLoginRequested - onClicked: { - Backend.loginFreeUserError() - user.resetLoginRequests() - } - } - - Button { - colorScheme: root.colorScheme - text: "connection error" - enabled: user !== undefined //&& user.isLoginRequested - onClicked: { - Backend.loginConnectionError("") - user.resetLoginRequests() - } - } - } - - RowLayout { - Layout.fillWidth: true - - Label { - colorScheme: root.colorScheme - id: faLabel - text: "2FA:" - - Layout.preferredWidth: Math.max(loginLabel.implicitWidth, faLabel.implicitWidth, passLabel.implicitWidth) - } - - Button { - colorScheme: root.colorScheme - text: "request" - - enabled: user !== undefined //&& user.isLoginRequested && !user.isLogin2FARequested && !user.isLogin2PasswordRequested - onClicked: { - Backend.login2FARequested(user.username) - user.isLogin2FARequested = true - } - } - - Button { - colorScheme: root.colorScheme - text: "error" - - enabled: user !== undefined //&& user.isLogin2FAProvided && !(user.isLogin2PasswordRequested && !user.isLogin2PasswordProvided) - onClicked: { - Backend.login2FAError("") - user.isLogin2FAProvided = false - } - } - - Button { - colorScheme: root.colorScheme - text: "Abort" - - enabled: user !== undefined //&& user.isLogin2FAProvided && !(user.isLogin2PasswordRequested && !user.isLogin2PasswordProvided) - onClicked: { - Backend.login2FAErrorAbort("") - user.resetLoginRequests() - } - } - } - - RowLayout { - Layout.fillWidth: true - - Label { - colorScheme: root.colorScheme - id: passLabel - text: "2 Password:" - - Layout.preferredWidth: Math.max(loginLabel.implicitWidth, faLabel.implicitWidth, passLabel.implicitWidth) - } - - Button { - colorScheme: root.colorScheme - text: "request" - - enabled: user !== undefined //&& user.isLoginRequested && !user.isLogin2PasswordRequested && !(user.isLogin2FARequested && !user.isLogin2FAProvided) - onClicked: { - Backend.login2PasswordRequested("") - user.isLogin2PasswordRequested = true - } - } - - Button { - colorScheme: root.colorScheme - text: "error" - - enabled: user !== undefined //&& user.isLogin2PasswordProvided && !(user.isLogin2FARequested && !user.isLogin2FAProvided) - onClicked: { - Backend.login2PasswordError("") - - user.isLogin2PasswordProvided = false - } - } - - Button { - colorScheme: root.colorScheme - text: "Abort" - - enabled: user !== undefined //&& user.isLogin2PasswordProvided && !(user.isLogin2FARequested && !user.isLogin2FAProvided) - onClicked: { - Backend.login2PasswordErrorAbort("") - user.resetLoginRequests() - } - } - } - - RowLayout { - Button { - colorScheme: root.colorScheme - text: "Login Finished" - - onClicked: { - Backend.loginFinished(0+loginFinishedIndex.text) - user.resetLoginRequests() - } - } - TextField { - id: loginFinishedIndex - colorScheme: root.colorScheme - label: "Index:" - text: root.userIndex - } - } - - RowLayout { - Button { - colorScheme: root.colorScheme - text: "Already logged in" - - onClicked: { - Backend.loginAlreadyLoggedIn(0+loginAlreadyLoggedInIndex.text) - user.resetLoginRequests() - } - } - TextField { - id: loginAlreadyLoggedInIndex - colorScheme: root.colorScheme - label: "Index:" - text: root.userIndex - } - } - - RowLayout { - TextField { - colorScheme: root.colorScheme - label: "used:" - text: user && user.usedBytes ? user.usedBytes : 0 - onEditingFinished: { - user.usedBytes = parseFloat(text) - } - implicitWidth: 200 - } - TextField { - colorScheme: root.colorScheme - label: "total:" - text: user && user.totalBytes ? user.totalBytes : 0 - onEditingFinished: { - user.totalBytes = parseFloat(text) - } - implicitWidth: 200 - } - } - - RowLayout { - Label {colorScheme: root.colorScheme; text: "Split mode"} - Toggle { colorScheme: root.colorScheme; checked: user ? user.splitMode : false; onClicked: {user.splitMode = !user.splitMode}} - Button { colorScheme: root.colorScheme; text: "Toggle Finished"; onClicked: {user.toggleSplitModeFinished()}} - } - - TextArea { // TODO: this is causing binding loop on implicitWidth - colorScheme: root.colorScheme - text: user && user.addresses ? user.addresses.join("\n") : "user@protonmail.com" - Layout.fillWidth: true - - onEditingFinished: { - user.addresses = text.split("\n") - } - } - - Item { - Layout.fillHeight: true - } -} diff --git a/internal/frontend/bridge-gui/bridge-gui/qml/BridgeTest/UserList.qml b/internal/frontend/bridge-gui/bridge-gui/qml/BridgeTest/UserList.qml deleted file mode 100644 index 92b50335..00000000 --- a/internal/frontend/bridge-gui/bridge-gui/qml/BridgeTest/UserList.qml +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (c) 2023 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail 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. -// -// Proton Mail 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 Proton Mail Bridge. If not, see . - -import QtQuick -import QtQuick.Layouts -import QtQuick.Controls - -import Proton - -ColumnLayout { - id: root - - property ColorScheme colorScheme - - property alias currentIndex: usersListView.currentIndex - ListView { - id: usersListView - Layout.fillHeight: true - Layout.preferredWidth: 200 - - model: Backend.usersTest - highlightFollowsCurrentItem: true - - delegate: Item { - - implicitHeight: children[0].implicitHeight + anchors.topMargin + anchors.bottomMargin - implicitWidth: children[0].implicitWidth + anchors.leftMargin + anchors.rightMargin - - width: usersListView.width - - anchors.margins: 10 - - Label { - colorScheme: root.colorScheme - text: modelData.username - anchors.margins: 10 - anchors.fill: parent - - MouseArea { - anchors.fill: parent - onClicked: { - usersListView.currentIndex = index - } - } - } - } - - highlight: Rectangle { - color: root.colorScheme.interaction_default_active - } - } - - RowLayout { - Layout.fillWidth: true - - Button { - colorScheme: root.colorScheme - - text: "+" - - onClicked: { - var newUserObject = Backend.userComponent.createObject(Backend) - newUserObject.username = Backend.loginUser.username.length > 0 ? Backend.loginUser.username : "test@protonmail.com" - newUserObject.state = EUserState.Connected - newUserObject.setupGuideSeen = true // Backend.loginUser.setupGuideSeen - - Backend.loginUser.username = "" - Backend.loginUser.state = EUserState.SignedOut - Backend.loginUser.setupGuideSeen = false - - Backend.users.append( { object: newUserObject } ) - } - } - Button { - colorScheme: root.colorScheme - text: "-" - - enabled: usersListView.currentIndex != 0 - - onClicked: { - // var userObject = Backend.users.get(usersListView.currentIndex - 1) - Backend.users.remove(usersListView.currentIndex - 1) - // userObject.deleteLater() - } - } - } -} diff --git a/internal/frontend/bridge-gui/bridge-gui/qml/BridgeTest/UserModel.qml b/internal/frontend/bridge-gui/bridge-gui/qml/BridgeTest/UserModel.qml deleted file mode 100644 index a8099aa7..00000000 --- a/internal/frontend/bridge-gui/bridge-gui/qml/BridgeTest/UserModel.qml +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) 2023 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail 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. -// -// Proton Mail 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 Proton Mail Bridge. If not, see . - -import QtQml.Models - -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) - } -} diff --git a/internal/frontend/bridge-gui/bridge-gui/qml/Bridge_test.qml b/internal/frontend/bridge-gui/bridge-gui/qml/Bridge_test.qml deleted file mode 100644 index 8ca4e750..00000000 --- a/internal/frontend/bridge-gui/bridge-gui/qml/Bridge_test.qml +++ /dev/null @@ -1,982 +0,0 @@ -// Copyright (c) 2023 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail 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. -// -// Proton Mail 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 Proton Mail Bridge. If not, see . - -import QtQml -import QtQuick -import QtQuick.Window -import QtQuick.Layouts -import QtQuick.Controls - -import QtQml.Models - -import Qt.labs.platform - -import Proton - -import "./BridgeTest" -import BridgePreview - -import Notifications - -Window { - id: root - - x: 10 - y: 10 - width: 800 - height: 800 - - property ColorScheme colorScheme: ProtonStyle.darkStyle - - flags : Qt.Window | Qt.Dialog - visible : true - title : "Bridge Test GUI" - - // This is needed because on MacOS if first window shown is not transparent - - // all other windows of application will not have transparent background (black - // instead of transparency). In our case that mean that if BridgeTest will be - // shown before StatusWindow - StatusWindow will not have transparent corners. - color: "transparent" - - function getCursorPos() { - return BridgePreview.getCursorPos() - } - - function restart() { - root.quit() - console.log("Restarting....") - root.openBridge() - } - - function openBridge() { - bridge = bridgeComponent.createObject() - var showSetupGuide = false - if (showSetupGuide) { - var newUserObject = root.userComponent.createObject(root) - newUserObject.username = "LerooooyJenkins@protonmail.com" - newUserObject.state = EUserState.Connected - newUserObject.setupGuideSeen = false - root.users.append( { object: newUserObject } ) - } - } - - - function quit() { - if (bridge !== undefined && bridge !== null) { - bridge.destroy() - } - } - - function guiReady() { - console.log("Gui Ready") - } - - function _log(msg, color) { - logTextArea.text += "

" + msg + "

" - logTextArea.text += "\n" - } - - function log(msg) { - console.log(msg) - _log(msg, root.colorScheme.signal_info) - } - - function error(msg) { - console.error(msg) - _log(msg, root.colorScheme.signal_danger) - } - - // No user object should be put in this list until a successful login - property var users: UserModel { - id: _users - - onRowsInserted: { - for (var i = first; i <= last; i++) { - _usersTest.insert(i + 1, { object: get(i) } ) - } - } - - onRowsRemoved: { - _usersTest.remove(first + 1, first - last + 1) - } - - onRowsMoved: { - _usersTest.move(start + 1, row + 1, end - start + 1) - } - - onDataChanged: { - for (var i = topLeft.row; i <= bottomRight.row; i++) { - _usersTest.set(i + 1, { object: get(i) } ) - } - } - } - - // this list is used on test gui: it contains same users list as users above + fake user to represent login request of new user on pos 0 - property var usersTest: UserModel { - id: _usersTest - } - - property var userComponent: Component { - id: _userComponent - - QtObject { - property string username: "" - property bool loggedIn: false - property bool splitMode: false - - property bool setupGuideSeen: true - - property var usedBytes: 5350*1024*1024 - property var totalBytes: 20*1024*1024*1024 - property string avatarText: "jd" - - property string password: "SMj975NnEYYsqu55GGmlpv" - property var addresses: [ - "jaanedoe@protonmail.com", - "jane@pm.me", - "jdoe@pm.me" - ] - - signal loginUsernamePasswordError() - signal loginFreeUserError() - signal loginConnectionError() - signal login2FARequested() - signal login2FAError() - signal login2FAErrorAbort() - signal login2PasswordRequested() - signal login2PasswordError() - signal login2PasswordErrorAbort() - - // Test purpose only: - property bool isFakeUser: this === root.loginUser - - function userSignal(msg) { - if (isFakeUser) { - return - } - - root.log("<- User (" + username + "): " + msg) - } - - function toggleSplitMode(makeActive) { - userSignal("toggle split mode "+makeActive) - } - signal toggleSplitModeFinished() - - function configureAppleMail(address){ - userSignal("configure apple mail "+address) - } - - function logout(){ - userSignal("logout") - loggedIn = false - } - function remove(){ - console.log("remove this", users.count) - for (var i=0; i usersListView.currentIndex) && usersListView.currentIndex != -1) ? root.usersTest.get(usersListView.currentIndex) : undefined - userIndex: usersListView.currentIndex - 1 // -1 because 0 index is fake user - } - } - - RowLayout { - id: notificationsTab - spacing: 5 - - ColumnLayout { - spacing: 5 - - Switch { - text: "Internet connection" - colorScheme: root.colorScheme - checked: true - onCheckedChanged: { - checked ? root.internetOn() : root.internetOff() - } - } - - Button { - text: "Update manual ready" - colorScheme: root.colorScheme - onClicked: { - root.updateManualReady("3.14.1592") - } - } - - Button { - text: "Update manual done" - colorScheme: root.colorScheme - onClicked: { - root.updateManualRestartNeeded() - } - } - - Button { - text: "Update manual error" - colorScheme: root.colorScheme - onClicked: { - root.updateManualError() - } - } - - Button { - text: "Update force" - colorScheme: root.colorScheme - onClicked: { - root.updateForce("3.14.1592") - } - } - - Button { - text: "Update force error" - colorScheme: root.colorScheme - onClicked: { - root.updateForceError() - } - } - - Button { - text: "Update silent done" - colorScheme: root.colorScheme - onClicked: { - root.updateSilentRestartNeeded() - } - } - - Button { - text: "Update silent error" - colorScheme: root.colorScheme - onClicked: { - root.updateSilentError() - } - } - - Button { - text: "Update is latest version" - colorScheme: root.colorScheme - onClicked: { - root.updateIsLatestVersion() - } - } - - Button { - text: "Bug report send OK" - colorScheme: root.colorScheme - onClicked: { - root.reportBugFinished() - root.bugReportSendSuccess() - } - - } - } - - ColumnLayout { - spacing: 5 - - Button { - text: "Bug report send error" - colorScheme: root.colorScheme - onClicked: { - root.reportBugFinished() - root.bugReportSendError() - } - } - - Button { - text: "Cache anavailable" - colorScheme: root.colorScheme - onClicked: { - root.cacheUnavailable() - } - } - - Button { - text: "Cache can't move" - colorScheme: root.colorScheme - onClicked: { - root.cacheCantMove() - } - } - - Button { - text: "Cache location change success" - onClicked: { - root.cacheLocationChangeSuccess() - } - colorScheme: root.colorScheme - } - - Button { - text: "Disk full" - colorScheme: root.colorScheme - onClicked: { - root.diskFull() - } - } - - Button { - text: "No keychain" - colorScheme: root.colorScheme - onClicked: { - root.notifyHasNoKeychain() - } - } - - Button { - text: "Rebuild keychain" - colorScheme: root.colorScheme - onClicked: { - root.notifyRebuildKeychain() - } - } - - Button { - text: "Address changed" - colorScheme: root.colorScheme - onClicked: { - root.addressChanged("p@v.el") - } - } - - Button { - text: "Address changed + Logout" - colorScheme: root.colorScheme - onClicked: { - root.addressChangedLogout("p@v.el") - } - } - } - } - - TextArea { - id: logTextArea - colorScheme: root.colorScheme - Layout.fillHeight: true - Layout.fillWidth: true - - Layout.preferredWidth: 400 - Layout.preferredHeight: 200 - - textFormat: TextEdit.RichText - //readOnly: true - } - - ScrollView { - id: settingsTab - ColumnLayout { - RowLayout { - Label {colorScheme : root.colorScheme ; text : "GOOS : "} - Button {colorScheme : root.colorScheme ; text : "Linux" ; onClicked : root.goos = "linux" ; enabled: root.goos != "linux"} - Button {colorScheme : root.colorScheme ; text : "Windows" ; onClicked : root.goos = "windows" ; enabled: root.goos != "windows"} - Button {colorScheme : root.colorScheme ; text : "macOS" ; onClicked : root.goos = "darwin" ; enabled: root.goos != "darwin"} - } - RowLayout { - Label {colorScheme: root.colorScheme; text: "Automatic updates:"} - Toggle {colorScheme: root.colorScheme; checked: root.isAutomaticUpdateOn; onClicked: root.isAutomaticUpdateOn = !root.isAutomaticUpdateOn} - } - RowLayout { - Label {colorScheme: root.colorScheme; text: "Autostart:"} - Toggle {colorScheme: root.colorScheme; checked: root.isAutostartOn; onClicked: root.isAutostartOn = !root.isAutostartOn} - Button {colorScheme: root.colorScheme; text: "Toggle finished"; onClicked: root.toggleAutostartFinished()} - } - RowLayout { - Label {colorScheme: root.colorScheme; text: "Beta:"} - Toggle {colorScheme: root.colorScheme; checked: root.isBetaEnabled; onClicked: root.isBetaEnabled = !root.isBetaEnabled} - } - RowLayout { - Label {colorScheme: root.colorScheme; text: "DoH:"} - Toggle {colorScheme: root.colorScheme; checked: root.isDoHEnabled; onClicked: root.isDoHEnabled = !root.isDoHEnabled} - } - RowLayout { - Label {colorScheme: root.colorScheme; text: "All Mail disabled:"} - Toggle {colorScheme: root.colorScheme; checked: root.isAllMailVisible; onClicked: root.isAllMailVisible = !root.isAllMailVisible} - } - RowLayout { - Label {colorScheme: root.colorScheme; text: "Ports:"} - TextField { - colorScheme:root.colorScheme - label: "IMAP" - text: root.portIMAP - onEditingFinished: root.portIMAP = this.text*1 - validator: IntValidator {bottom: 1; top: 65536} - } - TextField { - colorScheme:root.colorScheme - label: "SMTP" - text: root.portSMTP - onEditingFinished: root.portSMTP = this.text*1 - validator: IntValidator {bottom: 1; top: 65536} - } - Button {colorScheme: root.colorScheme; text: "Change finished"; onClicked: root.changePortFinished()} - } - RowLayout { - Label {colorScheme: root.colorScheme; text: "SMTP using SSL:"} - Toggle {colorScheme: root.colorScheme; checked: root.useSSLForSMTP; onClicked: root.useSSLForSMTP = !root.useSSLForSMTP} - } - RowLayout { - Label {colorScheme: root.colorScheme; text: "Local cache:"} - Toggle {colorScheme: root.colorScheme; checked: root.isDiskCacheEnabled; onClicked: root.isDiskCacheEnabled = !root.isDiskCacheEnabled} - TextField { - colorScheme:root.colorScheme - label: "Path" - text: root.diskCachePath.toString().replace("file://", "") - implicitWidth: 160 - onEditingFinished: { - root.diskCachePath = Qt.resolvedUrl("file://"+text) - } - } - Button {colorScheme: root.colorScheme; text: "Change finished"; onClicked: root.changeLocalCacheFinished()} - } - RowLayout { - Label {colorScheme: root.colorScheme; text: "Reset:"} - Button {colorScheme: root.colorScheme; text: "Finished"; onClicked: root.resetFinished()} - } - RowLayout { - Label {colorScheme: root.colorScheme; text: "Check update:"} - Button {colorScheme: root.colorScheme; text: "Finished"; onClicked: root.checkUpdatesFinished()} - } - } - } - } - } - - property Bridge bridge - - property string goos: "darwin" - - property bool showOnStartup: true // this actually needs to be false, but since we use Bridge_test for testing purpose - lets default this to true just for convenience - property bool dockIconVisible: false - - // this signals are used only when trying to login with new user (i.e. not in users model) - signal loginUsernamePasswordError(string errorMsg) - signal loginFreeUserError() - signal loginConnectionError(string errorMsg) - signal login2FARequested(string username) - signal login2FAError(string errorMsg) - signal login2FAErrorAbort(string errorMsg) - signal login2PasswordRequested() - signal login2PasswordError(string errorMsg) - signal login2PasswordErrorAbort(string errorMsg) - signal loginFinished(int index) - signal loginAlreadyLoggedIn(int index) - - signal internetOff() - signal internetOn() - - signal updateManualReady(var version) - signal updateManualRestartNeeded() - signal updateManualError() - signal updateForce(var version) - signal updateForceError() - signal updateSilentRestartNeeded() - signal updateSilentError() - signal updateIsLatestVersion() - function checkUpdates(){ - console.log("check updates") - } - signal checkUpdatesFinished() - function installUpdate() { - console.log("manuall install update triggered") - } - - - property bool isDiskCacheEnabled: true - // Qt.resolvedUrl("file:///C:/Users/user/AppData/Roaming/protonmail/bridge-v3/cache/c11/messages") - property url diskCachePath: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0] - signal cacheUnavailable() - signal cacheCantMove() - signal cacheLocationChangeSuccess() - signal diskFull() - function changeLocalCache(enableDiskCache, diskCachePath) { - console.debug("-> disk cache", enableDiskCache, diskCachePath) - } - signal changeLocalCacheFinished() - - - // Settings - property bool isAutomaticUpdateOn : true - function toggleAutomaticUpdate(makeItActive) { - console.debug("-> silent updates", makeItActive, root.isAutomaticUpdateOn) - var callback = function () { - root.isAutomaticUpdateOn = makeItActive; - console.debug("-> CHANGED silent updates", makeItActive, root.isAutomaticUpdateOn) - } - atimer.onTriggered.connect(callback) - atimer.restart() - } - - Timer { - id: atimer - interval: 2000 - running: false - repeat: false - } - - property bool isAutostartOn : true // Example of settings with loading state - function toggleAutostart(makeItActive) { - console.debug("-> autostart", makeItActive, root.isAutostartOn) - } - signal toggleAutostartFinished() - - property bool isBetaEnabled : false - function toggleBeta(makeItActive){ - console.debug("-> beta", makeItActive, root.isBetaEnabled) - root.isBetaEnabled = makeItActive - } - - property bool isDoHEnabled : true - function toggleDoH(makeItActive){ - console.debug("-> DoH", makeItActive, root.isDoHEnabled) - root.isDoHEnabled = makeItActive - } - - property bool isAllMailVisible : true - function changeIsAllMailVisible(isVisible){ - console.debug("-> All Mail Visible", isVisible, root.isAllMailVisible) - root.isAllMailVisible = isVisible - } - - - property bool useSSLForSMTP: false - function toggleUseSSLForSMTP(makeItActive){ - console.debug("-> SMTP SSL", makeItActive, root.useSSLForSMTP) - } - signal toggleUseSSLFinished() - - property string hostname: "127.0.0.1" - property int portIMAP: 1143 - property int portSMTP: 1025 - function changePorts(imapPort, smtpPort){ - console.debug("-> ports", imapPort, smtpPort) - } - function isPortFree(port){ - if (port == portIMAP) return false - if (port == portSMTP) return false - if (port == 12345) return false - return true - } - signal changePortFinished() - signal imapPortStartupError() - signal smtpPortStartupError() - - function triggerReset() { - console.debug("-> trigger reset") - } - signal resetFinished() - - property string version: "2.0.X-BridePreview" - property url logsPath: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0] - property url licensePath: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0] - property url releaseNotesLink: Qt.resolvedUrl("https://proton.me/download/bridge/early_releases.html") - property url dependencyLicensesLink: Qt.resolvedUrl("https://github.com/ProtonMail/proton-bridge/v3/blob/master/COPYING_NOTES.md#dependencies") - property url landingPageLink: Qt.resolvedUrl("https://proton.me/mail/bridge#download") - - property string colorSchemeName: "light" - function changeColorScheme(newScheme){ - root.colorSchemeName = newScheme - } - - - property string currentEmailClient: "" // "Apple Mail 14.0" - function updateCurrentMailClient(){ - currentEmailClient = "Apple Mail 14.0" - } - - function reportBug(description,address,emailClient,includeLogs){ - console.log("report bug") - console.log(" description",description) - console.log(" address",address) - console.log(" emailClient",emailClient) - console.log(" includeLogs",includeLogs) - } - signal reportBugFinished() - signal bugReportSendSuccess() - signal bugReportSendError() - - property var availableKeychain: ["gnome-keyring", "pass", "macos-keychain", "windows-credentials"] - property string currentKeychain: availableKeychain[0] - function changeKeychain(wantedKeychain){ - console.log("Changing keychain from", root.currentKeychain, "to", wantedKeychain) - root.currentKeychain = wantedKeychain - root.changeKeychainFinished() - } - signal changeKeychainFinished() - signal notifyHasNoKeychain() - signal notifyRebuildKeychain() - - signal noActiveKeyForRecipient(string email) - signal showMainWindow() - - signal addressChanged(string address) - signal addressChangedLogout(string address) - signal userDisconnected(string username) - signal apiCertIssue() - - property bool showSplashScreen: false - - - function login(username, password) { - root.log("-> login(" + username + ", " + password + ")") - - loginUser.username = username - loginUser.isLoginRequested = true - } - - function login2FA(username, code) { - root.log("-> login2FA(" + username + ", " + code + ")") - - loginUser.isLogin2FAProvided = true - } - - function login2Password(username, password) { - root.log("-> login2FA(" + username + ", " + password + ")") - - loginUser.isLogin2PasswordProvided = true - } - - function loginAbort(username) { - root.log("-> loginAbort(" + username + ")") - - loginUser.resetLoginRequests() - } - - - onLoginUsernamePasswordError: { - console.debug("<- loginUsernamePasswordError") - } - onLoginFreeUserError: { - console.debug("<- loginFreeUserError") - } - onLoginConnectionError: { - console.debug("<- loginConnectionError") - } - onLogin2FARequested: { - console.debug("<- login2FARequested", username) - } - onLogin2FAError: { - console.debug("<- login2FAError") - } - onLogin2FAErrorAbort: { - console.debug("<- login2FAErrorAbort") - } - onLogin2PasswordRequested: { - console.debug("<- login2PasswordRequested") - } - onLogin2PasswordError: { - console.debug("<- login2PasswordError") - } - onLogin2PasswordErrorAbort: { - console.debug("<- login2PasswordErrorAbort") - } - onLoginFinished: { - console.debug("<- loginFinished", index) - } - onLoginAlreadyLoggedIn: { - console.debug("<- loginAlreadyLoggedIn", index) - } - - onInternetOff: { - console.debug("<- internetOff") - } - onInternetOn: { - console.debug("<- internetOn") - } - - Component { - id: bridgeComponent - - Bridge { - backend: root - - } - } - - onClosing: { - Qt.quit() - } -} diff --git a/internal/frontend/bridge-gui/bridge-gui/qml/MainWindow.qml b/internal/frontend/bridge-gui/bridge-gui/qml/MainWindow.qml index 877eaf9f..9295d7f5 100644 --- a/internal/frontend/bridge-gui/bridge-gui/qml/MainWindow.qml +++ b/internal/frontend/bridge-gui/bridge-gui/qml/MainWindow.qml @@ -24,8 +24,6 @@ import QtQuick.Controls import Proton import Notifications -import "tests" - ApplicationWindow { id: root colorScheme: ProtonStyle.currentStyle @@ -79,10 +77,6 @@ ApplicationWindow { root.showAndRise() } - function onSelectUser(userID) { - root.selectUser(userID) - } - function onLoginFinished(index, wasSignedOut) { var user = Backend.users.get(index) if (user && !wasSignedOut) { @@ -90,6 +84,21 @@ ApplicationWindow { } console.debug("Login finished", index) } + + function onShowHelp() { + root.showHelp() + root.showAndRise() + } + + function onShowSettings() { + root.showSettings() + root.showAndRise() + } + + function onSelectUser(userID) { + contentWrapper.selectUser(userID) + root.showAndRise() + } } StackLayout { diff --git a/internal/frontend/bridge-gui/bridge-gui/qml/Notifications/Notifications.qml b/internal/frontend/bridge-gui/bridge-gui/qml/Notifications/Notifications.qml index f13642b7..1431aa36 100644 --- a/internal/frontend/bridge-gui/bridge-gui/qml/Notifications/Notifications.qml +++ b/internal/frontend/bridge-gui/bridge-gui/qml/Notifications/Notifications.qml @@ -24,8 +24,6 @@ QtObject { id: root property MainWindow frontendMain - property StatusWindow frontendStatus - property SystemTrayIcon frontendTray signal askEnableBeta() signal askEnableSplitMode(var user) @@ -140,7 +138,7 @@ QtObject { property Notification imapPortChangeError: Notification { description: qsTr("The IMAP port could not be changed.") - brief: qsTr("IMAP port change error") + brief: qsTr("IMAP port error") icon: "./icons/ic-alert.svg" type: Notification.NotificationType.Danger group: Notifications.Group.Connection @@ -156,7 +154,7 @@ QtObject { property Notification smtpPortChangeError: Notification { description: qsTr("The SMTP port could not be changed.") - brief: qsTr("SMTP port change error") + brief: qsTr("SMTP port error") icon: "./icons/ic-alert.svg" type: Notification.NotificationType.Danger group: Notifications.Group.Connection @@ -172,7 +170,7 @@ QtObject { property Notification imapConnectionModeChangeError: Notification { description: qsTr("The IMAP connection mode could not be changed.") - brief: qsTr("IMAP Connection mode change error") + brief: qsTr("IMAP Connection mode error") icon: "./icons/ic-alert.svg" type: Notification.NotificationType.Danger group: Notifications.Group.Connection @@ -196,7 +194,7 @@ QtObject { property Notification smtpConnectionModeChangeError: Notification { description: qsTr("The SMTP connection mode could not be changed.") - brief: qsTr("SMTP Connection mode change error") + brief: qsTr("SMTP Connection mode error") icon: "./icons/ic-alert.svg" type: Notification.NotificationType.Danger group: Notifications.Group.Connection @@ -227,7 +225,7 @@ QtObject { var link = Backend.releaseNotesLink return `${descr} ${text}` } - brief: qsTr("Update available.") + brief: qsTr("Update available") icon: "./icons/ic-info-circle-filled.svg" type: Notification.NotificationType.Info group: Notifications.Group.Update | Notifications.Group.Dialogs @@ -514,7 +512,7 @@ QtObject { // login property Notification loginConnectionError: Notification { description: qsTr("Bridge is not able to contact the server, please check your internet connection.") - brief: description + brief: qsTr("Connection error") icon: "./icons/ic-exclamation-circle-filled.svg" type: Notification.NotificationType.Danger group: Notifications.Group.Configuration @@ -538,7 +536,7 @@ QtObject { property Notification onlyPaidUsers: Notification { description: qsTr("Bridge is exclusive to our paid plans. Upgrade your account to use Bridge.") - brief: description + brief: qsTr("Upgrade your account") icon: "./icons/ic-exclamation-circle-filled.svg" type: Notification.NotificationType.Danger group: Notifications.Group.Configuration @@ -562,7 +560,7 @@ QtObject { property Notification alreadyLoggedIn: Notification { description: qsTr("This account is already signed in.") - brief: description + brief: qsTr("Already signed in") icon: "./icons/ic-exclamation-circle-filled.svg" type: Notification.NotificationType.Info group: Notifications.Group.Configuration @@ -587,7 +585,7 @@ QtObject { // Bug reports property Notification bugReportSendSuccess: Notification { description: qsTr("Thank you for the report. We'll get back to you as soon as we can.") - brief: description + brief: qsTr("Report sent") icon: "./icons/ic-info-circle-filled.svg" type: Notification.NotificationType.Success group: Notifications.Group.Configuration @@ -611,7 +609,7 @@ QtObject { property Notification bugReportSendError: Notification { description: qsTr("Report could not be sent. Try again or email us directly.") - brief: description + brief: qsTr("Error sending report") icon: "./icons/ic-exclamation-circle-filled.svg" type: Notification.NotificationType.Danger group: Notifications.Group.Configuration @@ -634,8 +632,8 @@ QtObject { // Cache property Notification cacheUnavailable: Notification { title: qsTr("Cache location is unavailable") - description: qsTr("Check the directory or change it in your settings.") - brief: qsTr("The current cache location is unavailable. Check the directory or change it in your settings.") + description: qsTr("The current cache location is unavailable. Check the directory or change it in your settings.") + brief: title icon: "./icons/ic-exclamation-circle-filled.svg" type: Notification.NotificationType.Warning group: Notifications.Group.Configuration | Notifications.Group.Dialogs @@ -725,7 +723,7 @@ QtObject { // Other property Notification accountChanged: Notification { description: qsTr("The address list for .... account has changed. You need to reconfigure your email client.") - brief: qsTr("The address list for your account has changed. Reconfigure your email client.") + brief: qsTr("Address list changed") icon: "./icons/ic-exclamation-circle-filled.svg" type: Notification.NotificationType.Danger group: Notifications.Group.Configuration @@ -742,7 +740,7 @@ QtObject { property Notification diskFull: Notification { title: qsTr("Your disk is almost full") description: qsTr("Quit Bridge and free disk space or disable the local cache (not recommended).") - brief: qsTr("Your disk is almost full. Free disk space or disable the local cache.") + brief: title icon: "./icons/ic-exclamation-circle-filled.svg" type: Notification.NotificationType.Warning group: Notifications.Group.Configuration | Notifications.Group.Dialogs @@ -948,8 +946,8 @@ QtObject { property Notification noKeychain: Notification { title: qsTr("No keychain available") - description: qsTr("Bridge is not able to detect a supported password manager (pass or secret-service). Please install and setup supported password manager and restart the application.") brief: title + description: qsTr("Bridge is not able to detect a supported password manager (pass or secret-service). Please install and setup supported password manager and restart the application.") icon: "./icons/ic-exclamation-circle-filled.svg" type: Notification.NotificationType.Danger group: Notifications.Group.Dialogs | Notifications.Group.Configuration @@ -982,8 +980,8 @@ QtObject { property Notification rebuildKeychain: Notification { title: qsTr("Your macOS keychain might be corrupted") - description: qsTr("Bridge is not able to access your macOS keychain. Please consult the instructions on our support page.") brief: title + description: qsTr("Bridge is not able to access your macOS keychain. Please consult the instructions on our support page.") icon: "./icons/ic-exclamation-circle-filled.svg" type: Notification.NotificationType.Danger group: Notifications.Group.Dialogs | Notifications.Group.Configuration @@ -1014,8 +1012,8 @@ QtObject { property Notification addressChanged: Notification { title: qsTr("Address list changes") + brief: title description: qsTr("The address list for your account has changed. You might need to reconfigure your email client.") - brief: description icon: "./icons/ic-exclamation-circle-filled.svg" type: Notification.NotificationType.Warning group: Notifications.Group.Configuration @@ -1047,11 +1045,11 @@ QtObject { property Notification apiCertIssue: Notification { title: qsTr("Unable to establish a \nsecure connection to \nProton servers") + brief: qsTr("Cannot establish secure connection") description: qsTr("Bridge cannot verify the authenticity of Proton servers on your current network due to a TLS certificate error. " + "Start Bridge again after ensuring your connection is secure and/or connecting to a VPN. Learn more about TLS pinning " + "here.") - brief: title icon: "./icons/ic-exclamation-circle-filled.svg" type: Notification.NotificationType.Danger group: Notifications.Group.Dialogs | Notifications.Group.Connection @@ -1078,6 +1076,7 @@ QtObject { property Notification noActiveKeyForRecipient: Notification { title: qsTr("Unable to send \nencrypted message") + brief: title description: "#PlaceholderText#" icon: "./icons/ic-exclamation-circle-filled.svg" type: Notification.NotificationType.Danger @@ -1174,8 +1173,9 @@ QtObject { } property Notification genericError: Notification { - title: "#PlaceholderText#" - description: "#PlaceholderText#" + title: "" + brief: title + description: "" icon: "./icons/ic-exclamation-circle-filled.svg" type: Notification.NotificationType.Danger group: Notifications.Group.Dialogs @@ -1201,7 +1201,7 @@ QtObject { property Notification genericQuestion: Notification { title: "" - brief: "" + brief: title description: "" type: Notification.NotificationType.Warning group: Notifications.Group.Dialogs diff --git a/internal/frontend/bridge-gui/bridge-gui/qml/StatusWindow.qml b/internal/frontend/bridge-gui/bridge-gui/qml/StatusWindow.qml deleted file mode 100644 index 2269acaa..00000000 --- a/internal/frontend/bridge-gui/bridge-gui/qml/StatusWindow.qml +++ /dev/null @@ -1,352 +0,0 @@ -// Copyright (c) 2023 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail 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. -// -// Proton Mail 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 Proton Mail Bridge. If not, see . - -import QtQml -import QtQuick -import QtQuick.Window -import QtQuick.Layouts -import QtQuick.Controls - -import Proton -import Notifications - -Window { - id: root - - height: contentLayout.implicitHeight - width: contentLayout.implicitWidth - - flags: (Qt.platform.os === "linux" ? Qt.Tool : 0) | Qt.FramelessWindowHint | Qt.NoDropShadowWindowHint | Qt.WindowStaysOnTopHint | Qt.WA_TranslucentBackground - color: "transparent" - - property ColorScheme colorScheme: ProtonStyle.currentStyle - - property var notifications - - signal showMainWindow() - signal showHelp() - signal showSettings() - signal selectUser(string userID) - signal quit() - - MouseArea { - id: mouseArea - anchors.fill: parent - hoverEnabled: true - } - - function enableHoverOnOpenBridgeButton() { - openBridgeButton.hoverEnabled = true - mouseArea.positionChanged.disconnect(enableHoverOnOpenBridgeButton) - } - - onVisibleChanged: { - if (visible) { // GODT-1479 To avoid a visual glitch where the 'Open bridge button' would appear hovered when the status windows opens, - // we've disabled hover on it when it was last closed. Re-enabling hover here will not work on all platforms. so we temporarily connect - // mouse move event over the window's mouseArea to a function that will re-enable hover on the open bridge button. - openBridgeButton.focus = false - mouseArea.positionChanged.connect(enableHoverOnOpenBridgeButton) - } else { - menu.close() - } - } - - ColumnLayout { - id: contentLayout - - Layout.minimumHeight: 201 - - anchors.fill: parent - spacing: 0 - - ColumnLayout { - Layout.minimumWidth: 448 - 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: ProtonStyle.dialog_radius - - 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.fillWidth: true - - Layout.topMargin: 12 - Layout.bottomMargin: 12 - - colorScheme: root.colorScheme - notifications: root.notifications - - notificationWhitelist: Notifications.Group.Connection | Notifications.Group.Update | Notifications.Group.Configuration - } - - Button { - colorScheme: root.colorScheme - secondary: true - - Layout.topMargin: 12 - Layout.bottomMargin: 12 - - visible: statusItem.activeNotification && statusItem.activeNotification.action.length > 0 - 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: Backend.users - anchors.fill: parent - - anchors.topMargin: 8 - anchors.bottomMargin: 8 - anchors.leftMargin: 24 - anchors.rightMargin: 24 - - interactive: contentHeight > parent.height - snapMode: ListView.SnapToItem - boundsBehavior: Flickable.StopAtBounds - - spacing: 4 - - delegate: Item { - id: viewItem - width: ListView.view.width - - implicitHeight: children[0].implicitHeight - implicitWidth: children[0].implicitWidth - - property var user: Backend.users.get(index) - - RowLayout { - spacing: 0 - anchors.fill: parent - - AccountDelegate { - Layout.fillWidth: true - - Layout.topMargin: 12 - Layout.bottomMargin: 12 - - user: viewItem.user - colorScheme: root.colorScheme - } - - Button { - Layout.topMargin: 12 - Layout.bottomMargin: 12 - - colorScheme: root.colorScheme - visible: viewItem.user ? (viewItem.user.state === EUserState.SignedOut) : false - text: qsTr("Sign in") - onClicked: { - root.selectUser(viewItem.user.id) // selectUser will show login screen if user is in SignedOut state. - root.close() - } - } - } - } - } - } - - 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: ProtonStyle.dialog_radius - - color: root.colorScheme.background_weak - } - - RowLayout { - anchors.fill: parent - anchors.margins: 8 - spacing: 0 - - Button { - id: openBridgeButton - colorScheme: root.colorScheme - secondary: true - text: qsTr("Open Bridge") - - borderless: true - labelType: Label.LabelType.Caption_semibold - - onClicked: { - // GODT-1479: we disable hover for the button to avoid a visual glitch where the button is - // wrongly hovered when re-opening the status window after clicking - hoverEnabled = false; - root.showMainWindow() - root.close() - } - } - - Item { - Layout.fillWidth: true - } - - Button { - colorScheme: root.colorScheme - secondary: true - icon.source: "/qml/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.close() - } - } - MenuItem { - colorScheme: root.colorScheme - text: qsTr("Settings") - onClicked: { - root.showSettings() - root.close() - } - } - MenuItem { - colorScheme: root.colorScheme - text: qsTr("Quit Bridge") - onClicked: { - root.close() - root.quit() - } - } - - onClosed: { - parent.checked = false - } - onOpened: { - parent.checked = true - } - } - } - } - } - } - - onActiveChanged: { - if (!active) root.close() - } - - function showAndRise() { - root.show() - root.raise() - if (!root.active) { - root.requestActivate() - } - } -} diff --git a/internal/frontend/bridge-gui/bridge-gui/qml/icons/ic-dot.svg b/internal/frontend/bridge-gui/bridge-gui/qml/icons/ic-dot.svg new file mode 100644 index 00000000..d025c3ef --- /dev/null +++ b/internal/frontend/bridge-gui/bridge-gui/qml/icons/ic-dot.svg @@ -0,0 +1,7 @@ + + + + + + +