Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f34f3509b | |||
| b21d77a911 | |||
| a4edb7279d | |||
| e61b1dfa11 | |||
| 0db1e52468 | |||
| 596a422475 | |||
| 345fbf99de | |||
| 58d6c362f3 | |||
| 9937add4eb | |||
| 7eecc3667f | |||
| 446e7b6a8e | |||
| 10d5bdbc60 | |||
| bb38388140 | |||
| 47834a10a6 | |||
| 61afebcedb | |||
| 9fa03e5387 | |||
| 9857a5d90a | |||
| 2c37305cf0 | |||
| 6fad9f56e4 | |||
| a40bd0d580 |
@@ -267,6 +267,10 @@ if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (LINUX AND NOT ANDROID))
|
||||
set(HEADERS ${HEADERS}
|
||||
${CLIENT_ROOT_DIR}/core/utils/ipcClient.h
|
||||
${CLIENT_ROOT_DIR}/ui/utils/systemTrayNotificationHandler.h
|
||||
${CLIENT_ROOT_DIR}/ui/utils/platformTheme.h
|
||||
${CLIENT_ROOT_DIR}/ui/utils/trayIconBackend.h
|
||||
${CLIENT_ROOT_DIR}/ui/utils/platformTrayTheme.h
|
||||
${CLIENT_ROOT_DIR}/ui/utils/trayIconCommon.h
|
||||
${CLIENT_ROOT_DIR}/core/protocols/openVpnProtocol.h
|
||||
${CLIENT_ROOT_DIR}/core/protocols/wireGuardProtocol.h
|
||||
${CLIENT_ROOT_DIR}/core/protocols/xrayProtocol.h
|
||||
@@ -278,20 +282,81 @@ if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (LINUX AND NOT ANDROID))
|
||||
${CLIENT_ROOT_DIR}/core/utils/ipcClient.cpp
|
||||
${CLIENT_ROOT_DIR}/mozilla/localsocketcontroller.cpp
|
||||
${CLIENT_ROOT_DIR}/ui/utils/systemTrayNotificationHandler.cpp
|
||||
${CLIENT_ROOT_DIR}/ui/utils/platformTheme.cpp
|
||||
${CLIENT_ROOT_DIR}/ui/utils/platformTrayTheme.cpp
|
||||
${CLIENT_ROOT_DIR}/ui/utils/trayIconCommon.cpp
|
||||
${CLIENT_ROOT_DIR}/core/protocols/openVpnProtocol.cpp
|
||||
${CLIENT_ROOT_DIR}/core/protocols/wireGuardProtocol.cpp
|
||||
${CLIENT_ROOT_DIR}/core/protocols/xrayProtocol.cpp
|
||||
${CLIENT_ROOT_DIR}/core/protocols/awgProtocol.cpp
|
||||
)
|
||||
|
||||
if(APPLE AND NOT MACOS_NE)
|
||||
set(HEADERS ${HEADERS}
|
||||
${CLIENT_ROOT_DIR}/platforms/macos/mactrayiconbackend.h
|
||||
${CLIENT_ROOT_DIR}/platforms/macos/mactraytheme.h
|
||||
)
|
||||
set(SOURCES ${SOURCES}
|
||||
${CLIENT_ROOT_DIR}/platforms/macos/mactrayiconbackend.mm
|
||||
${CLIENT_ROOT_DIR}/platforms/macos/mactraytheme.cpp
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(APPLE AND MACOS_NE)
|
||||
# Include only the tray notification handler in NE builds
|
||||
set(HEADERS ${HEADERS}
|
||||
${CLIENT_ROOT_DIR}/ui/utils/systemTrayNotificationHandler.h
|
||||
${CLIENT_ROOT_DIR}/ui/utils/platformTheme.h
|
||||
${CLIENT_ROOT_DIR}/ui/utils/trayIconBackend.h
|
||||
${CLIENT_ROOT_DIR}/ui/utils/platformTrayTheme.h
|
||||
${CLIENT_ROOT_DIR}/ui/utils/trayIconCommon.h
|
||||
${CLIENT_ROOT_DIR}/platforms/windows/wintrayiconbackend.h
|
||||
${CLIENT_ROOT_DIR}/platforms/windows/wintrayicon.h
|
||||
${CLIENT_ROOT_DIR}/platforms/windows/wintraytheme.h
|
||||
${CLIENT_ROOT_DIR}/ui/utils/trayThemeChangeFilter.h
|
||||
)
|
||||
|
||||
set(SOURCES ${SOURCES}
|
||||
${CLIENT_ROOT_DIR}/ui/utils/systemTrayNotificationHandler.cpp
|
||||
${CLIENT_ROOT_DIR}/ui/utils/platformTheme.cpp
|
||||
${CLIENT_ROOT_DIR}/ui/utils/platformTrayTheme.cpp
|
||||
${CLIENT_ROOT_DIR}/ui/utils/trayIconCommon.cpp
|
||||
${CLIENT_ROOT_DIR}/platforms/windows/wintrayiconbackend.cpp
|
||||
${CLIENT_ROOT_DIR}/platforms/windows/wintrayicon.cpp
|
||||
${CLIENT_ROOT_DIR}/platforms/windows/wintraytheme.cpp
|
||||
${CLIENT_ROOT_DIR}/ui/utils/trayThemeChangeFilter.cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
set(HEADERS ${HEADERS}
|
||||
${CLIENT_ROOT_DIR}/platforms/windows/windowsutils.h
|
||||
${CLIENT_ROOT_DIR}/platforms/windows/wintrayiconbackend.h
|
||||
${CLIENT_ROOT_DIR}/platforms/windows/wintrayicon.h
|
||||
${CLIENT_ROOT_DIR}/platforms/windows/wintraytheme.h
|
||||
${CLIENT_ROOT_DIR}/ui/utils/trayThemeChangeFilter.h
|
||||
)
|
||||
set(SOURCES ${SOURCES}
|
||||
${CLIENT_ROOT_DIR}/platforms/windows/windowsutils.cpp
|
||||
${CLIENT_ROOT_DIR}/platforms/windows/wintrayiconbackend.cpp
|
||||
${CLIENT_ROOT_DIR}/platforms/windows/wintrayicon.cpp
|
||||
${CLIENT_ROOT_DIR}/platforms/windows/wintraytheme.cpp
|
||||
${CLIENT_ROOT_DIR}/ui/utils/trayThemeChangeFilter.cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
if(LINUX)
|
||||
set(HEADERS ${HEADERS}
|
||||
${CLIENT_ROOT_DIR}/platforms/linux/linuxutils.h
|
||||
${CLIENT_ROOT_DIR}/platforms/linux/linuxtrayiconbackend.h
|
||||
${CLIENT_ROOT_DIR}/platforms/linux/linuxtraytheme.h
|
||||
${CLIENT_ROOT_DIR}/ui/utils/trayThemeChangeFilter.h
|
||||
)
|
||||
set(SOURCES ${SOURCES}
|
||||
${CLIENT_ROOT_DIR}/platforms/linux/linuxutils.cpp
|
||||
${CLIENT_ROOT_DIR}/platforms/linux/linuxtrayiconbackend.cpp
|
||||
${CLIENT_ROOT_DIR}/platforms/linux/linuxtraytheme.cpp
|
||||
${CLIENT_ROOT_DIR}/ui/utils/trayThemeChangeFilter.cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
@@ -425,7 +425,12 @@ void CoreSignalHandlers::initNotificationHandler()
|
||||
|
||||
auto* trayHandler = qobject_cast<SystemTrayNotificationHandler*>(m_coreController->m_notificationHandler);
|
||||
connect(m_coreController, &CoreController::websiteUrlChanged, trayHandler, &SystemTrayNotificationHandler::updateWebsiteUrl);
|
||||
#endif
|
||||
|
||||
connect(m_coreController->m_connectionUiController, &ConnectionUiController::connectionErrorOccurred, trayHandler,
|
||||
&SystemTrayNotificationHandler::setConnectionError);
|
||||
connect(m_coreController->m_pageController, &PageController::errorMessageClosed, trayHandler,
|
||||
&SystemTrayNotificationHandler::clearConnectionError);
|
||||
#endif
|
||||
}
|
||||
|
||||
void CoreSignalHandlers::initUpdateFoundHandler()
|
||||
|
||||
@@ -65,9 +65,12 @@
|
||||
<file>controls/text-cursor.svg</file>
|
||||
<file>controls/trash.svg</file>
|
||||
<file>controls/x-circle.svg</file>
|
||||
<file>tray/active.png</file>
|
||||
<file>tray/default.png</file>
|
||||
<file>tray/error.png</file>
|
||||
<file>tray/off-black.svg</file>
|
||||
<file>tray/off-light.svg</file>
|
||||
<file>tray/on-black.svg</file>
|
||||
<file>tray/on-white.svg</file>
|
||||
<file>tray/error-black.svg</file>
|
||||
<file>tray/error-white.svg</file>
|
||||
<file>controls/monitor.svg</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
@@ -15,6 +15,10 @@
|
||||
#include "platforms/ios/QtAppDelegate-C-Interface.h"
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_MAC) && !defined(MACOS_NE)
|
||||
#include "platforms/macos/macosutils.h"
|
||||
#endif
|
||||
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
|
||||
bool isAnotherInstanceRunning()
|
||||
{
|
||||
@@ -46,6 +50,10 @@ int main(int argc, char *argv[])
|
||||
AmneziaApplication app(argc, argv);
|
||||
OsSignalHandler::setup();
|
||||
|
||||
#if defined(Q_OS_MAC) && !defined(MACOS_NE)
|
||||
MacOSUtils::patchNSStatusBarSetImageForBigSur();
|
||||
#endif
|
||||
|
||||
ssh_init();
|
||||
QObject::connect(&app, &QCoreApplication::aboutToQuit, []() {
|
||||
ssh_finalize();
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
#include "linuxtrayiconbackend.h"
|
||||
|
||||
#include "ui/utils/trayIconCommon.h"
|
||||
|
||||
#include <QObject>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
constexpr int kLinuxTrayIconSizes[] = { 16, 22, 24, 32, 48, 64, 128 };
|
||||
|
||||
} // namespace
|
||||
|
||||
LinuxTrayIconBackend::LinuxTrayIconBackend(QObject *parent) : m_trayIcon(parent)
|
||||
{
|
||||
}
|
||||
|
||||
void LinuxTrayIconBackend::setMenu(QMenu *menu)
|
||||
{
|
||||
m_trayIcon.setContextMenu(menu);
|
||||
}
|
||||
|
||||
void LinuxTrayIconBackend::setToolTip(const QString &tooltip)
|
||||
{
|
||||
m_trayIcon.setToolTip(tooltip);
|
||||
}
|
||||
|
||||
void LinuxTrayIconBackend::show()
|
||||
{
|
||||
m_trayIcon.show();
|
||||
}
|
||||
|
||||
void LinuxTrayIconBackend::applyVisual(const TrayIconVisual &visual)
|
||||
{
|
||||
if (m_hasLastVisual && visual.connectionState == m_lastState && visual.darkTheme == m_lastDarkTheme) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_lastState = visual.connectionState;
|
||||
m_lastDarkTheme = visual.darkTheme;
|
||||
m_hasLastVisual = true;
|
||||
|
||||
const QIcon icon = buildTrayIcon(visual.connectionState, visual.darkTheme);
|
||||
m_trayIcon.setIcon(icon);
|
||||
}
|
||||
|
||||
void LinuxTrayIconBackend::showMessage(const QString &title, const QString &message, const TrayIconVisual &visual,
|
||||
int timerMsec)
|
||||
{
|
||||
m_trayIcon.showMessage(title, message,
|
||||
buildTrayIcon(Vpn::ConnectionState::Connected, visual.darkTheme),
|
||||
timerMsec);
|
||||
}
|
||||
|
||||
void LinuxTrayIconBackend::rebuildMenu()
|
||||
{
|
||||
}
|
||||
|
||||
void LinuxTrayIconBackend::setActivatedHandler(std::function<void(QSystemTrayIcon::ActivationReason)> handler)
|
||||
{
|
||||
if (!handler) {
|
||||
return;
|
||||
}
|
||||
|
||||
QObject::connect(&m_trayIcon, &QSystemTrayIcon::activated, m_trayIcon.parent(),
|
||||
[handler](QSystemTrayIcon::ActivationReason reason) { handler(reason); });
|
||||
}
|
||||
|
||||
QIcon LinuxTrayIconBackend::buildTrayIcon(Vpn::ConnectionState state, bool darkTheme) const
|
||||
{
|
||||
QIcon icon;
|
||||
for (int size : kLinuxTrayIconSizes) {
|
||||
icon.addPixmap(TrayIconCommon::buildPixmap(size, state, darkTheme));
|
||||
}
|
||||
return icon;
|
||||
}
|
||||
|
||||
std::unique_ptr<TrayIconBackend> createTrayIconBackend(QObject *parent)
|
||||
{
|
||||
return std::make_unique<LinuxTrayIconBackend>(parent);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
#ifndef LINUXTRAYICONBACKEND_H
|
||||
#define LINUXTRAYICONBACKEND_H
|
||||
|
||||
#include "ui/utils/trayIconBackend.h"
|
||||
|
||||
#include <QColor>
|
||||
#include <QIcon>
|
||||
#include <QString>
|
||||
#include <QSystemTrayIcon>
|
||||
|
||||
class LinuxTrayIconBackend final : public TrayIconBackend
|
||||
{
|
||||
public:
|
||||
explicit LinuxTrayIconBackend(QObject *parent);
|
||||
|
||||
void setMenu(QMenu *menu) override;
|
||||
void setToolTip(const QString &tooltip) override;
|
||||
void show() override;
|
||||
void applyVisual(const TrayIconVisual &visual) override;
|
||||
void showMessage(const QString &title, const QString &message, const TrayIconVisual &visual, int timerMsec) override;
|
||||
void rebuildMenu() override;
|
||||
void setActivatedHandler(std::function<void(QSystemTrayIcon::ActivationReason)> handler) override;
|
||||
|
||||
private:
|
||||
QIcon buildTrayIcon(Vpn::ConnectionState state, bool darkTheme) const;
|
||||
|
||||
QSystemTrayIcon m_trayIcon;
|
||||
Vpn::ConnectionState m_lastState = Vpn::ConnectionState::Unknown;
|
||||
bool m_lastDarkTheme = false;
|
||||
bool m_hasLastVisual = false;
|
||||
};
|
||||
|
||||
#endif // LINUXTRAYICONBACKEND_H
|
||||
@@ -0,0 +1,24 @@
|
||||
#include "linuxtraytheme.h"
|
||||
|
||||
#include "platforms/linux/linuxutils.h"
|
||||
#include "ui/utils/trayThemeChangeFilter.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QGuiApplication>
|
||||
#include <QObject>
|
||||
#include <QStyleHints>
|
||||
|
||||
void LinuxTrayTheme::installThemeObserver(const std::function<void()> &onThemeChanged, QObject *parent)
|
||||
{
|
||||
if (!onThemeChanged || !parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (QStyleHints *styleHints = QGuiApplication::styleHints()) {
|
||||
QObject::connect(styleHints, &QStyleHints::colorSchemeChanged, parent, [onThemeChanged]() { onThemeChanged(); });
|
||||
}
|
||||
|
||||
qApp->installEventFilter(new TrayThemeChangeFilter(onThemeChanged, parent));
|
||||
|
||||
LinuxUtils::installThemeChangeObserver(onThemeChanged);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
#ifndef LINUXTRAYTHEME_H
|
||||
#define LINUXTRAYTHEME_H
|
||||
|
||||
#include <functional>
|
||||
|
||||
class QObject;
|
||||
|
||||
namespace LinuxTrayTheme
|
||||
{
|
||||
|
||||
void installThemeObserver(const std::function<void()> &onThemeChanged, QObject *parent);
|
||||
|
||||
} // namespace LinuxTrayTheme
|
||||
|
||||
#endif // LINUXTRAYTHEME_H
|
||||
@@ -0,0 +1,128 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#include "linuxutils.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QDBusConnection>
|
||||
#include <QDBusVariant>
|
||||
#include <QGuiApplication>
|
||||
#include <QPalette>
|
||||
#include <QSettings>
|
||||
#include <QStyleHints>
|
||||
|
||||
namespace {
|
||||
|
||||
bool paletteSuggestsDarkTheme()
|
||||
{
|
||||
const QPalette palette = QGuiApplication::palette();
|
||||
const int windowLightness = palette.color(QPalette::Window).lightness();
|
||||
const int textLightness = palette.color(QPalette::WindowText).lightness();
|
||||
|
||||
if (textLightness - windowLightness > 50) {
|
||||
return true;
|
||||
}
|
||||
if (windowLightness - textLightness > 50) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return windowLightness < 128;
|
||||
}
|
||||
|
||||
class LinuxThemeObserver final : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit LinuxThemeObserver(std::function<void()> callback, QObject *parent = nullptr)
|
||||
: QObject(parent)
|
||||
, m_callback(std::move(callback))
|
||||
{
|
||||
QDBusConnection bus = QDBusConnection::sessionBus();
|
||||
if (!bus.isConnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
bus.connect(QStringLiteral("org.freedesktop.portal.Desktop"),
|
||||
QStringLiteral("/org/freedesktop/portal/desktop"),
|
||||
QStringLiteral("org.freedesktop.portal.Settings"),
|
||||
QStringLiteral("SettingChanged"),
|
||||
this,
|
||||
SLOT(onPortalSettingChanged(QString, QString, QDBusVariant)));
|
||||
}
|
||||
|
||||
void setCallback(std::function<void()> callback)
|
||||
{
|
||||
m_callback = std::move(callback);
|
||||
}
|
||||
|
||||
private slots:
|
||||
void onPortalSettingChanged(const QString &namespaceName, const QString &key, const QDBusVariant &value)
|
||||
{
|
||||
Q_UNUSED(value);
|
||||
|
||||
if (namespaceName == QStringLiteral("org.freedesktop.appearance")
|
||||
&& key == QStringLiteral("color-scheme") && m_callback) {
|
||||
m_callback();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
std::function<void()> m_callback;
|
||||
};
|
||||
|
||||
LinuxThemeObserver *g_themeObserver = nullptr;
|
||||
|
||||
} // namespace
|
||||
|
||||
bool LinuxUtils::isDarkTheme()
|
||||
{
|
||||
if (QStyleHints *styleHints = QGuiApplication::styleHints()) {
|
||||
switch (styleHints->colorScheme()) {
|
||||
case Qt::ColorScheme::Dark:
|
||||
return true;
|
||||
case Qt::ColorScheme::Light:
|
||||
return false;
|
||||
case Qt::ColorScheme::Unknown:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
QSettings settings(QSettings::IniFormat, QSettings::UserScope, QStringLiteral("gtk-3.0"),
|
||||
QStringLiteral("settings"));
|
||||
const QString themeName = settings.value(QStringLiteral("gtk-theme-name")).toString();
|
||||
if (themeName.contains(QStringLiteral("dark"), Qt::CaseInsensitive)) {
|
||||
return true;
|
||||
}
|
||||
if (themeName.contains(QStringLiteral("light"), Qt::CaseInsensitive)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QSettings kdeSettings(QSettings::IniFormat, QSettings::UserScope, QStringLiteral("kdeglobals"));
|
||||
const QString colorScheme = kdeSettings.value(QStringLiteral("General/ColorScheme")).toString();
|
||||
if (colorScheme.contains(QStringLiteral("dark"), Qt::CaseInsensitive)) {
|
||||
return true;
|
||||
}
|
||||
if (colorScheme.contains(QStringLiteral("light"), Qt::CaseInsensitive)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return paletteSuggestsDarkTheme();
|
||||
}
|
||||
|
||||
void LinuxUtils::installThemeChangeObserver(std::function<void()> callback)
|
||||
{
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!g_themeObserver) {
|
||||
g_themeObserver = new LinuxThemeObserver(std::move(callback), qApp);
|
||||
return;
|
||||
}
|
||||
|
||||
g_themeObserver->setCallback(std::move(callback));
|
||||
}
|
||||
|
||||
#include "linuxutils.moc"
|
||||
@@ -0,0 +1,19 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#ifndef LINUXUTILS_H
|
||||
#define LINUXUTILS_H
|
||||
|
||||
#include <functional>
|
||||
|
||||
class LinuxUtils final {
|
||||
public:
|
||||
static bool isDarkTheme();
|
||||
static void installThemeChangeObserver(std::function<void()> callback);
|
||||
|
||||
private:
|
||||
LinuxUtils() = default;
|
||||
};
|
||||
|
||||
#endif // LINUXUTILS_H
|
||||
@@ -5,8 +5,12 @@
|
||||
#ifndef MACOSSTATUSICON_H
|
||||
#define MACOSSTATUSICON_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QColor>
|
||||
#include <QMenu>
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
#include <QString>
|
||||
|
||||
class MacOSStatusIcon final : public QObject {
|
||||
Q_OBJECT
|
||||
@@ -18,10 +22,14 @@ class MacOSStatusIcon final : public QObject {
|
||||
|
||||
public:
|
||||
void setIcon(const QString& iconUrl);
|
||||
void setIndicatorColor(const QColor& indicatorColor);
|
||||
void setMenu(NSMenu* statusBarMenu);
|
||||
void setIconFromData(const QByteArray& imageData, bool asTemplate = true);
|
||||
void setMenu(QMenu* menu);
|
||||
void rebuildNativeMenu();
|
||||
void setToolTip(const QString& tooltip);
|
||||
void showMessage(const QString& title, const QString& message);
|
||||
|
||||
private:
|
||||
QPointer<QMenu> m_qtMenu;
|
||||
};
|
||||
|
||||
#endif // MACOSSTATUSICON_H
|
||||
|
||||
@@ -10,22 +10,38 @@
|
||||
#import <UserNotifications/UserNotifications.h>
|
||||
#import <QResource>
|
||||
|
||||
#include <QAction>
|
||||
|
||||
@interface MacOSStatusIconMenuTarget : NSObject {
|
||||
@public
|
||||
QAction* action;
|
||||
}
|
||||
- (void)triggerAction:(id)sender;
|
||||
@end
|
||||
|
||||
@implementation MacOSStatusIconMenuTarget
|
||||
- (void)triggerAction:(id)sender {
|
||||
Q_UNUSED(sender);
|
||||
if (action) {
|
||||
action->trigger();
|
||||
}
|
||||
}
|
||||
@end
|
||||
|
||||
/**
|
||||
* Creates a NSStatusItem with that can hold an icon. Additionally a NSView is
|
||||
* set as a subview to the button item of the status item. The view serves as
|
||||
* an indicator that can be displayed in color eventhough the icon is set as a
|
||||
* template. In that way we give the system control over it’s effective
|
||||
* appearance.
|
||||
* Creates a NSStatusItem that holds the tray icon. The icon is set as a
|
||||
* template image, so the system controls its effective appearance for the
|
||||
* current menu bar theme. The connection status is baked into the artwork, so
|
||||
* no separate colored indicator is drawn.
|
||||
*/
|
||||
@interface MacOSStatusIconDelegate : NSObject
|
||||
@property(assign) NSStatusItem* statusItem;
|
||||
@property(assign) NSView* statusIndicator;
|
||||
@property(retain) NSMenu* nativeMenu;
|
||||
@property(retain) NSMutableArray* menuActionTargets;
|
||||
|
||||
- (void)setIcon:(NSData*)imageData;
|
||||
- (void)setIndicator;
|
||||
- (void)setIndicatorColor:(NSColor*)color;
|
||||
- (void)setMenu:(NSMenu*)statusBarMenu;
|
||||
- (void)setIcon:(NSData*)imageData asTemplate:(BOOL)asTemplate;
|
||||
- (void)setToolTip:(NSString*)tooltip;
|
||||
- (void)rebuildMenuFromQMenu:(QMenu*)menu;
|
||||
@end
|
||||
|
||||
@implementation MacOSStatusIconDelegate
|
||||
@@ -36,58 +52,38 @@
|
||||
*/
|
||||
- (id)init {
|
||||
self = [super init];
|
||||
self.menuActionTargets = [[NSMutableArray alloc] init];
|
||||
|
||||
// Create status item
|
||||
self.statusItem =
|
||||
[[[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength] retain];
|
||||
self.statusItem.visible = true;
|
||||
// Add the indicator as a subview
|
||||
[self setIndicator];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
self.nativeMenu = nil;
|
||||
self.menuActionTargets = nil;
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the image for the status icon.
|
||||
*
|
||||
* @param iconPath The data for the icon image.
|
||||
* @param imageData The data for the icon image.
|
||||
* @param asTemplate When true the icon is a template image recolored by the
|
||||
* system for the current menu bar appearance. When false the icon is
|
||||
* rendered in its original colors (used for the colored error icon).
|
||||
*/
|
||||
- (void)setIcon:(NSData*)imageData {
|
||||
- (void)setIcon:(NSData*)imageData asTemplate:(BOOL)asTemplate {
|
||||
NSImage* image = [[NSImage alloc] initWithData:imageData];
|
||||
[image setTemplate:true];
|
||||
[image setTemplate:asTemplate];
|
||||
|
||||
[self.statusItem.button setImage:image];
|
||||
[image release];
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds status indicator as a subview to the status item button.
|
||||
*/
|
||||
- (void)setIndicator {
|
||||
float viewHeight = NSHeight([self.statusItem.button bounds]);
|
||||
float dotSize = viewHeight * 0.35;
|
||||
float dotOrigin = (viewHeight - dotSize) * 0.8;
|
||||
|
||||
NSView* dot = [[NSView alloc] initWithFrame:NSMakeRect(dotOrigin, dotOrigin, dotSize, dotSize)];
|
||||
self.statusIndicator = dot;
|
||||
self.statusIndicator.wantsLayer = true;
|
||||
self.statusIndicator.layer.cornerRadius = dotSize * 0.5;
|
||||
|
||||
[self.statusItem.button addSubview:self.statusIndicator];
|
||||
[dot release];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the color if the indicator.
|
||||
*
|
||||
* @param color The indicator background color.
|
||||
*/
|
||||
- (void)setIndicatorColor:(NSColor*)color {
|
||||
if (self.statusIndicator) {
|
||||
self.statusIndicator.layer.backgroundColor = color.CGColor;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the status bar menu to the status item.
|
||||
*
|
||||
@@ -105,6 +101,44 @@
|
||||
- (void)setToolTip:(NSString*)tooltip {
|
||||
[self.statusItem.button setToolTip:tooltip];
|
||||
}
|
||||
|
||||
- (void)rebuildMenuFromQMenu:(QMenu*)menu {
|
||||
[self.menuActionTargets removeAllObjects];
|
||||
|
||||
if (self.nativeMenu) {
|
||||
[self.statusItem setMenu:nil];
|
||||
self.nativeMenu = nil;
|
||||
}
|
||||
|
||||
if (!menu) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSMenu* nsMenu = [[NSMenu alloc] initWithTitle:@""];
|
||||
for (QAction* action : menu->actions()) {
|
||||
if (action->isSeparator()) {
|
||||
[nsMenu addItem:[NSMenuItem separatorItem]];
|
||||
continue;
|
||||
}
|
||||
|
||||
MacOSStatusIconMenuTarget* target = [[MacOSStatusIconMenuTarget alloc] init];
|
||||
target->action = action;
|
||||
[self.menuActionTargets addObject:target];
|
||||
[target release];
|
||||
|
||||
NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:action->text().toNSString()
|
||||
action:@selector(triggerAction:)
|
||||
keyEquivalent:@""];
|
||||
[item setTarget:target];
|
||||
[item setEnabled:action->isEnabled()];
|
||||
[item setHidden:!action->isVisible()];
|
||||
[nsMenu addItem:item];
|
||||
[item release];
|
||||
}
|
||||
|
||||
self.nativeMenu = nsMenu;
|
||||
[self.statusItem setMenu:nsMenu];
|
||||
}
|
||||
@end
|
||||
|
||||
namespace {
|
||||
@@ -138,27 +172,31 @@ void MacOSStatusIcon::setIcon(const QString& iconPath) {
|
||||
QResource imageResource = QResource(iconPath);
|
||||
Q_ASSERT(imageResource.isValid());
|
||||
|
||||
[m_statusBarIcon setIcon:imageResource.uncompressedData().toNSData()];
|
||||
[m_statusBarIcon setIcon:imageResource.uncompressedData().toNSData() asTemplate:true];
|
||||
}
|
||||
|
||||
void MacOSStatusIcon::setIndicatorColor(const QColor& indicatorColor) {
|
||||
logger.debug() << "Set indicator color";
|
||||
void MacOSStatusIcon::setIconFromData(const QByteArray& imageData, bool asTemplate) {
|
||||
logger.debug() << "Set icon from rendered data";
|
||||
|
||||
if (!indicatorColor.isValid()) {
|
||||
[m_statusBarIcon setIndicatorColor:[NSColor clearColor]];
|
||||
if (imageData.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSColor* color = [NSColor colorWithCalibratedRed:indicatorColor.red() / 255.0f
|
||||
green:indicatorColor.green() / 255.0f
|
||||
blue:indicatorColor.blue() / 255.0f
|
||||
alpha:indicatorColor.alpha() / 255.0f];
|
||||
[m_statusBarIcon setIndicatorColor:color];
|
||||
NSData* data = [NSData dataWithBytes:imageData.constData() length:imageData.size()];
|
||||
[m_statusBarIcon setIcon:data asTemplate:asTemplate];
|
||||
}
|
||||
|
||||
void MacOSStatusIcon::setMenu(NSMenu* statusBarMenu) {
|
||||
logger.debug() << "Set menu";
|
||||
[m_statusBarIcon setMenu:statusBarMenu];
|
||||
void MacOSStatusIcon::setMenu(QMenu* menu) {
|
||||
m_qtMenu = menu;
|
||||
rebuildNativeMenu();
|
||||
|
||||
if (menu) {
|
||||
connect(menu, &QMenu::aboutToShow, this, [this]() { rebuildNativeMenu(); });
|
||||
}
|
||||
}
|
||||
|
||||
void MacOSStatusIcon::rebuildNativeMenu() {
|
||||
[m_statusBarIcon rebuildMenuFromQMenu:m_qtMenu.data()];
|
||||
}
|
||||
|
||||
void MacOSStatusIcon::setToolTip(const QString& tooltip) {
|
||||
@@ -174,7 +212,7 @@ void MacOSStatusIcon::showMessage(const QString& title, const QString& message)
|
||||
// This is a no-op is authorization has been granted.
|
||||
[center requestAuthorizationWithOptions:(UNAuthorizationOptionSound | UNAuthorizationOptionAlert |
|
||||
UNAuthorizationOptionBadge)
|
||||
completionHandler:^(BOOL granted, NSError* _Nullable error) {
|
||||
completionHandler:^(__unused BOOL granted, NSError* _Nullable error) {
|
||||
if (error) {
|
||||
// Note: This error may happen if the application is not signed.
|
||||
NSLog(@"Error asking for permission to send notifications %@", error);
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
#include <functional>
|
||||
|
||||
class MacOSUtils final {
|
||||
public:
|
||||
static NSString* appId();
|
||||
@@ -23,6 +25,9 @@ class MacOSUtils final {
|
||||
static void showDockIcon();
|
||||
|
||||
static void patchNSStatusBarSetImageForBigSur();
|
||||
|
||||
static bool isDarkTheme();
|
||||
static void installInterfaceThemeObserver(std::function<void()> callback);
|
||||
};
|
||||
|
||||
#endif // MACOSUTILS_H
|
||||
|
||||
@@ -137,6 +137,30 @@ void MacOSUtils::showDockIcon() {
|
||||
* Original bug (and sample implementation):
|
||||
* https://bugreports.qt.io/browse/QTBUG-88600
|
||||
*/
|
||||
bool MacOSUtils::isDarkTheme() {
|
||||
if (@available(macOS 10.14, *)) {
|
||||
NSAppearanceName appearanceName = [[NSApp effectiveAppearance] name];
|
||||
return appearanceName && [appearanceName isEqualToString:NSAppearanceNameDarkAqua];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void MacOSUtils::installInterfaceThemeObserver(std::function<void()> callback) {
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::function<void()> themeCallback = std::move(callback);
|
||||
|
||||
NSDistributedNotificationCenter *center = [NSDistributedNotificationCenter defaultCenter];
|
||||
[center addObserverForName:@"AppleInterfaceThemeChangedNotification"
|
||||
object:nil
|
||||
queue:[NSOperationQueue mainQueue]
|
||||
usingBlock:^(__unused NSNotification *notification) {
|
||||
themeCallback();
|
||||
}];
|
||||
}
|
||||
|
||||
void MacOSUtils::patchNSStatusBarSetImageForBigSur() {
|
||||
Method original = class_getInstanceMethod([NSStatusBarButton class], @selector(setImage:));
|
||||
Method patched = class_getInstanceMethod([NSStatusBarButton class], @selector(setImagePatched:));
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
#ifndef MACTRAYICONBACKEND_H
|
||||
#define MACTRAYICONBACKEND_H
|
||||
|
||||
#include "ui/utils/trayIconBackend.h"
|
||||
|
||||
#include "macosstatusicon.h"
|
||||
|
||||
class MacTrayIconBackend final : public TrayIconBackend
|
||||
{
|
||||
public:
|
||||
explicit MacTrayIconBackend(QObject *parent);
|
||||
|
||||
void setMenu(QMenu *menu) override;
|
||||
void setToolTip(const QString &tooltip) override;
|
||||
void show() override;
|
||||
void applyVisual(const TrayIconVisual &visual) override;
|
||||
void showMessage(const QString &title, const QString &message, const TrayIconVisual &visual, int timerMsec) override;
|
||||
void rebuildMenu() override;
|
||||
void setActivatedHandler(std::function<void(QSystemTrayIcon::ActivationReason)> handler) override;
|
||||
|
||||
private:
|
||||
MacOSStatusIcon m_statusIcon;
|
||||
};
|
||||
|
||||
#endif // MACTRAYICONBACKEND_H
|
||||
@@ -0,0 +1,55 @@
|
||||
#include "mactrayiconbackend.h"
|
||||
|
||||
#include "ui/utils/trayIconCommon.h"
|
||||
|
||||
MacTrayIconBackend::MacTrayIconBackend(QObject *parent)
|
||||
: m_statusIcon(parent)
|
||||
{
|
||||
}
|
||||
|
||||
void MacTrayIconBackend::setMenu(QMenu *menu)
|
||||
{
|
||||
m_statusIcon.setMenu(menu);
|
||||
}
|
||||
|
||||
void MacTrayIconBackend::setToolTip(const QString &tooltip)
|
||||
{
|
||||
m_statusIcon.setToolTip(tooltip);
|
||||
}
|
||||
|
||||
void MacTrayIconBackend::show()
|
||||
{
|
||||
}
|
||||
|
||||
void MacTrayIconBackend::applyVisual(const TrayIconVisual &visual)
|
||||
{
|
||||
if (TrayIconCommon::isColoredState(visual.connectionState)) {
|
||||
// Error icon carries a red badge: render it in color, not as a template.
|
||||
m_statusIcon.setIconFromData(TrayIconCommon::buildColorPng(visual.connectionState, visual.darkTheme),
|
||||
/*asTemplate*/ false);
|
||||
} else {
|
||||
m_statusIcon.setIconFromData(TrayIconCommon::buildTemplatePng(visual.connectionState), /*asTemplate*/ true);
|
||||
}
|
||||
}
|
||||
|
||||
void MacTrayIconBackend::showMessage(const QString &title, const QString &message, const TrayIconVisual &visual, int timerMsec)
|
||||
{
|
||||
Q_UNUSED(visual);
|
||||
Q_UNUSED(timerMsec);
|
||||
m_statusIcon.showMessage(title, message);
|
||||
}
|
||||
|
||||
void MacTrayIconBackend::rebuildMenu()
|
||||
{
|
||||
m_statusIcon.rebuildNativeMenu();
|
||||
}
|
||||
|
||||
void MacTrayIconBackend::setActivatedHandler(std::function<void(QSystemTrayIcon::ActivationReason)> handler)
|
||||
{
|
||||
Q_UNUSED(handler);
|
||||
}
|
||||
|
||||
std::unique_ptr<TrayIconBackend> createTrayIconBackend(QObject *parent)
|
||||
{
|
||||
return std::make_unique<MacTrayIconBackend>(parent);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#include "mactraytheme.h"
|
||||
|
||||
#include <QObject>
|
||||
|
||||
void MacTrayTheme::installThemeObserver(const std::function<void()> &onThemeChanged, QObject *parent)
|
||||
{
|
||||
Q_UNUSED(onThemeChanged);
|
||||
Q_UNUSED(parent);
|
||||
// macOS template tray icons follow the menu bar appearance automatically.
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
#ifndef MACTRAYTHEME_H
|
||||
#define MACTRAYTHEME_H
|
||||
|
||||
#include <functional>
|
||||
|
||||
class QObject;
|
||||
|
||||
namespace MacTrayTheme
|
||||
{
|
||||
|
||||
void installThemeObserver(const std::function<void()> &onThemeChanged, QObject *parent);
|
||||
|
||||
} // namespace MacTrayTheme
|
||||
|
||||
#endif // MACTRAYTHEME_H
|
||||
@@ -14,7 +14,41 @@
|
||||
|
||||
namespace {
|
||||
Logger logger("WindowsUtils");
|
||||
} // namespace
|
||||
|
||||
constexpr const wchar_t kThemeWatcherClassName[] = L"AmneziaVpnThemeWatcher";
|
||||
|
||||
struct ThemeObserverState
|
||||
{
|
||||
std::function<void()> callback;
|
||||
HWND hwnd = nullptr;
|
||||
};
|
||||
|
||||
ThemeObserverState g_themeObserver;
|
||||
|
||||
bool registryUsesDarkTheme(const QSettings &settings, const QString &lightThemeKey)
|
||||
{
|
||||
if (settings.contains(lightThemeKey)) {
|
||||
return settings.value(lightThemeKey).toInt() != 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
LRESULT CALLBACK themeWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
Q_UNUSED(wParam);
|
||||
|
||||
if (msg == WM_SETTINGCHANGE && lParam != 0) {
|
||||
const wchar_t *section = reinterpret_cast<const wchar_t *>(lParam);
|
||||
if (wcscmp(section, L"ImmersiveColorSet") == 0 || wcscmp(section, L"WindowsThemeElement") == 0) {
|
||||
if (g_themeObserver.callback) {
|
||||
g_themeObserver.callback();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return DefWindowProcW(hwnd, msg, wParam, lParam);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
constexpr const int WINDOWS_11_BUILD =
|
||||
22000; // Build Number of the first release win 11 iso
|
||||
@@ -60,3 +94,51 @@ QString WindowsUtils::windowsVersion() {
|
||||
void WindowsUtils::forceCrash() {
|
||||
RaiseException(0x0000DEAD, EXCEPTION_NONCONTINUABLE, 0, NULL);
|
||||
}
|
||||
|
||||
// static
|
||||
bool WindowsUtils::isDarkTheme() {
|
||||
QSettings settings(
|
||||
QStringLiteral("HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"),
|
||||
QSettings::NativeFormat);
|
||||
settings.sync();
|
||||
|
||||
if (settings.contains(QStringLiteral("SystemUsesLightTheme"))) {
|
||||
return registryUsesDarkTheme(settings, QStringLiteral("SystemUsesLightTheme"));
|
||||
}
|
||||
|
||||
if (settings.contains(QStringLiteral("AppsUseLightTheme"))) {
|
||||
return registryUsesDarkTheme(settings, QStringLiteral("AppsUseLightTheme"));
|
||||
}
|
||||
|
||||
logger.warning() << "SystemUsesLightTheme registry key is unavailable; assuming dark theme";
|
||||
return true;
|
||||
}
|
||||
|
||||
void WindowsUtils::installThemeChangeObserver(std::function<void()> callback)
|
||||
{
|
||||
g_themeObserver.callback = std::move(callback);
|
||||
|
||||
if (g_themeObserver.hwnd) {
|
||||
return;
|
||||
}
|
||||
|
||||
HINSTANCE instance = GetModuleHandleW(nullptr);
|
||||
WNDCLASSW wc = {};
|
||||
wc.lpfnWndProc = themeWndProc;
|
||||
wc.hInstance = instance;
|
||||
wc.lpszClassName = kThemeWatcherClassName;
|
||||
|
||||
WNDCLASSW existing = {};
|
||||
if (!GetClassInfoW(instance, kThemeWatcherClassName, &existing)) {
|
||||
if (!RegisterClassW(&wc)) {
|
||||
WindowsUtils::windowsLog("Failed to register theme watcher window class");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
g_themeObserver.hwnd = CreateWindowExW(0, kThemeWatcherClassName, L"AmneziaVpnThemeWatcher", 0, 0, 0, 0, 0,
|
||||
HWND_MESSAGE, nullptr, instance, nullptr);
|
||||
if (!g_themeObserver.hwnd) {
|
||||
WindowsUtils::windowsLog("Failed to create theme watcher window");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
#include <QString>
|
||||
|
||||
#include <functional>
|
||||
|
||||
class WindowsUtils final {
|
||||
public:
|
||||
static QString getErrorMessage();
|
||||
@@ -18,6 +20,9 @@ class WindowsUtils final {
|
||||
|
||||
// Force an application crash for testing
|
||||
static void forceCrash();
|
||||
|
||||
static bool isDarkTheme();
|
||||
static void installThemeChangeObserver(std::function<void()> callback);
|
||||
};
|
||||
|
||||
#endif // WINDOWSUTILS_H
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
#include "wintrayicon.h"
|
||||
|
||||
#include "ui/utils/trayIconCommon.h"
|
||||
|
||||
#include <QMenu>
|
||||
|
||||
namespace WinTrayIcon
|
||||
{
|
||||
QIcon buildIcon(Vpn::ConnectionState state, bool darkTheme)
|
||||
{
|
||||
return TrayIconCommon::buildIcon(state, darkTheme);
|
||||
}
|
||||
|
||||
void applyTo(QSystemTrayIcon &trayIcon, Vpn::ConnectionState state, bool darkTheme)
|
||||
{
|
||||
trayIcon.setIcon(buildIcon(state, darkTheme));
|
||||
}
|
||||
|
||||
QIcon buildNotifyIcon(bool darkTheme)
|
||||
{
|
||||
return buildIcon(Vpn::ConnectionState::Connected, darkTheme);
|
||||
}
|
||||
|
||||
void configure(QSystemTrayIcon &trayIcon, QMenu *menu, const QString &tooltip)
|
||||
{
|
||||
trayIcon.setContextMenu(menu);
|
||||
trayIcon.setToolTip(tooltip);
|
||||
}
|
||||
|
||||
void show(QSystemTrayIcon &trayIcon)
|
||||
{
|
||||
trayIcon.show();
|
||||
}
|
||||
|
||||
void showMessage(QSystemTrayIcon &trayIcon, const QString &title, const QString &message, bool darkTheme,
|
||||
int timerMsec)
|
||||
{
|
||||
trayIcon.showMessage(title, message, buildNotifyIcon(darkTheme), timerMsec);
|
||||
}
|
||||
} // namespace WinTrayIcon
|
||||
@@ -0,0 +1,25 @@
|
||||
#ifndef WINTRAYICON_H
|
||||
#define WINTRAYICON_H
|
||||
|
||||
#include "core/protocols/vpnProtocol.h"
|
||||
|
||||
#include <QColor>
|
||||
#include <QIcon>
|
||||
#include <QSystemTrayIcon>
|
||||
|
||||
class QMenu;
|
||||
class QString;
|
||||
|
||||
namespace WinTrayIcon
|
||||
{
|
||||
QIcon buildIcon(Vpn::ConnectionState state, bool darkTheme);
|
||||
void applyTo(QSystemTrayIcon &trayIcon, Vpn::ConnectionState state, bool darkTheme);
|
||||
QIcon buildNotifyIcon(bool darkTheme);
|
||||
|
||||
void configure(QSystemTrayIcon &trayIcon, QMenu *menu, const QString &tooltip);
|
||||
void show(QSystemTrayIcon &trayIcon);
|
||||
void showMessage(QSystemTrayIcon &trayIcon, const QString &title, const QString &message, bool darkTheme,
|
||||
int timerMsec);
|
||||
} // namespace WinTrayIcon
|
||||
|
||||
#endif // WINTRAYICON_H
|
||||
@@ -0,0 +1,67 @@
|
||||
#include "wintrayiconbackend.h"
|
||||
|
||||
#include "platforms/windows/wintrayicon.h"
|
||||
|
||||
#include <QObject>
|
||||
|
||||
WinTrayIconBackend::WinTrayIconBackend(QObject *parent) : m_trayIcon(parent)
|
||||
{
|
||||
m_reapplyTimerShort.setSingleShot(true);
|
||||
m_reapplyTimerLong.setSingleShot(true);
|
||||
QObject::connect(&m_reapplyTimerShort, &QTimer::timeout, &m_trayIcon, [this]() { reapplyLastVisual(); });
|
||||
QObject::connect(&m_reapplyTimerLong, &QTimer::timeout, &m_trayIcon, [this]() { reapplyLastVisual(); });
|
||||
}
|
||||
|
||||
void WinTrayIconBackend::reapplyLastVisual()
|
||||
{
|
||||
WinTrayIcon::applyTo(m_trayIcon, m_lastVisual.connectionState, m_lastVisual.darkTheme);
|
||||
}
|
||||
|
||||
void WinTrayIconBackend::setMenu(QMenu *menu)
|
||||
{
|
||||
m_trayIcon.setContextMenu(menu);
|
||||
}
|
||||
|
||||
void WinTrayIconBackend::setToolTip(const QString &tooltip)
|
||||
{
|
||||
m_trayIcon.setToolTip(tooltip);
|
||||
}
|
||||
|
||||
void WinTrayIconBackend::show()
|
||||
{
|
||||
WinTrayIcon::show(m_trayIcon);
|
||||
}
|
||||
|
||||
void WinTrayIconBackend::applyVisual(const TrayIconVisual &visual)
|
||||
{
|
||||
m_lastVisual = visual;
|
||||
WinTrayIcon::applyTo(m_trayIcon, visual.connectionState, visual.darkTheme);
|
||||
|
||||
m_reapplyTimerShort.start(250);
|
||||
m_reapplyTimerLong.start(1200);
|
||||
}
|
||||
|
||||
void WinTrayIconBackend::showMessage(const QString &title, const QString &message, const TrayIconVisual &visual,
|
||||
int timerMsec)
|
||||
{
|
||||
WinTrayIcon::showMessage(m_trayIcon, title, message, visual.darkTheme, timerMsec);
|
||||
}
|
||||
|
||||
void WinTrayIconBackend::rebuildMenu()
|
||||
{
|
||||
}
|
||||
|
||||
void WinTrayIconBackend::setActivatedHandler(std::function<void(QSystemTrayIcon::ActivationReason)> handler)
|
||||
{
|
||||
if (!handler) {
|
||||
return;
|
||||
}
|
||||
|
||||
QObject::connect(&m_trayIcon, &QSystemTrayIcon::activated, m_trayIcon.parent(),
|
||||
[handler](QSystemTrayIcon::ActivationReason reason) { handler(reason); });
|
||||
}
|
||||
|
||||
std::unique_ptr<TrayIconBackend> createTrayIconBackend(QObject *parent)
|
||||
{
|
||||
return std::make_unique<WinTrayIconBackend>(parent);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
#ifndef WINTRAYICONBACKEND_H
|
||||
#define WINTRAYICONBACKEND_H
|
||||
|
||||
#include "ui/utils/trayIconBackend.h"
|
||||
|
||||
#include <QColor>
|
||||
#include <QIcon>
|
||||
#include <QSystemTrayIcon>
|
||||
#include <QTimer>
|
||||
|
||||
class WinTrayIconBackend final : public TrayIconBackend
|
||||
{
|
||||
public:
|
||||
explicit WinTrayIconBackend(QObject *parent);
|
||||
|
||||
void setMenu(QMenu *menu) override;
|
||||
void setToolTip(const QString &tooltip) override;
|
||||
void show() override;
|
||||
void applyVisual(const TrayIconVisual &visual) override;
|
||||
void showMessage(const QString &title, const QString &message, const TrayIconVisual &visual, int timerMsec) override;
|
||||
void rebuildMenu() override;
|
||||
void setActivatedHandler(std::function<void(QSystemTrayIcon::ActivationReason)> handler) override;
|
||||
|
||||
private:
|
||||
void reapplyLastVisual();
|
||||
|
||||
QSystemTrayIcon m_trayIcon;
|
||||
TrayIconVisual m_lastVisual;
|
||||
QTimer m_reapplyTimerShort;
|
||||
QTimer m_reapplyTimerLong;
|
||||
};
|
||||
|
||||
#endif // WINTRAYICONBACKEND_H
|
||||
@@ -0,0 +1,32 @@
|
||||
#include "wintraytheme.h"
|
||||
|
||||
#include "platforms/windows/windowsutils.h"
|
||||
#include "ui/utils/trayThemeChangeFilter.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QGuiApplication>
|
||||
#include <QObject>
|
||||
#include <QStyleHints>
|
||||
#include <QTimer>
|
||||
|
||||
void WinTrayTheme::installThemeObserver(const std::function<void()> &onThemeChanged, QObject *parent)
|
||||
{
|
||||
if (!onThemeChanged || !parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto *debounce = new QTimer(parent);
|
||||
debounce->setSingleShot(true);
|
||||
QObject::connect(debounce, &QTimer::timeout, parent, [onThemeChanged]() { onThemeChanged(); });
|
||||
|
||||
const auto schedule = [debounce]() { debounce->start(150); };
|
||||
|
||||
if (QStyleHints *styleHints = QGuiApplication::styleHints()) {
|
||||
QObject::connect(styleHints, &QStyleHints::colorSchemeChanged, parent,
|
||||
[schedule](Qt::ColorScheme) { schedule(); });
|
||||
}
|
||||
|
||||
qApp->installEventFilter(new TrayThemeChangeFilter([schedule]() { schedule(); }, parent));
|
||||
|
||||
WindowsUtils::installThemeChangeObserver([schedule]() { schedule(); });
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
#ifndef WINTRAYTHEME_H
|
||||
#define WINTRAYTHEME_H
|
||||
|
||||
#include <functional>
|
||||
|
||||
class QObject;
|
||||
|
||||
namespace WinTrayTheme
|
||||
{
|
||||
|
||||
void installThemeObserver(const std::function<void()> &onThemeChanged, QObject *parent);
|
||||
|
||||
} // namespace WinTrayTheme
|
||||
|
||||
#endif // WINTRAYTHEME_H
|
||||
@@ -248,3 +248,8 @@ void PageController::onShowErrorMessage(ErrorCode errorCode)
|
||||
|
||||
emit showErrorMessage(fullMessage);
|
||||
}
|
||||
|
||||
void PageController::onErrorMessageClosed()
|
||||
{
|
||||
emit errorMessageClosed();
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ public slots:
|
||||
int getDrawerDepth() const;
|
||||
int incrementDrawerDepth();
|
||||
int decrementDrawerDepth();
|
||||
|
||||
void onErrorMessageClosed();
|
||||
bool isEdgeToEdgeEnabled();
|
||||
int getStatusBarHeight();
|
||||
int getNavigationBarHeight();
|
||||
@@ -162,7 +162,7 @@ signals:
|
||||
void showErrorMessage(amnezia::ErrorCode);
|
||||
void showErrorMessage(const QString &errorMessage);
|
||||
void showNotificationMessage(const QString &message);
|
||||
|
||||
void errorMessageClosed();
|
||||
void showBusyIndicator(bool visible);
|
||||
void disableControls(bool disabled);
|
||||
void disableTabBar(bool disabled);
|
||||
|
||||
@@ -203,6 +203,13 @@ Window {
|
||||
PopupType {
|
||||
id: popupErrorMessage
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: popupErrorMessage
|
||||
function onClosed() {
|
||||
PageController.onErrorMessageClosed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
#include "platformTheme.h"
|
||||
|
||||
#include <QGuiApplication>
|
||||
#include <QStyleHints>
|
||||
|
||||
#if defined(Q_OS_MAC)
|
||||
# include "platforms/macos/macosutils.h"
|
||||
#elif defined(Q_OS_WIN)
|
||||
# include "platforms/windows/windowsutils.h"
|
||||
#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
|
||||
# include "platforms/linux/linuxutils.h"
|
||||
#endif
|
||||
|
||||
bool platformIsDarkTheme()
|
||||
{
|
||||
#if defined(Q_OS_MAC)
|
||||
return MacOSUtils::isDarkTheme();
|
||||
#elif defined(Q_OS_WIN)
|
||||
return WindowsUtils::isDarkTheme();
|
||||
#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
|
||||
return LinuxUtils::isDarkTheme();
|
||||
#else
|
||||
if (QStyleHints *styleHints = QGuiApplication::styleHints()) {
|
||||
return styleHints->colorScheme() == Qt::ColorScheme::Dark;
|
||||
}
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#ifndef PLATFORMTHEME_H
|
||||
#define PLATFORMTHEME_H
|
||||
|
||||
bool platformIsDarkTheme();
|
||||
|
||||
#endif // PLATFORMTHEME_H
|
||||
@@ -0,0 +1,26 @@
|
||||
#include "platformTrayTheme.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
#if defined(Q_OS_MAC) && !defined(MACOS_NE)
|
||||
# include "platforms/macos/mactraytheme.h"
|
||||
#elif defined(Q_OS_WIN) || (defined(Q_OS_MAC) && defined(MACOS_NE))
|
||||
# include "platforms/windows/wintraytheme.h"
|
||||
#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
|
||||
# include "platforms/linux/linuxtraytheme.h"
|
||||
#endif
|
||||
|
||||
void installTrayThemeObserver(const std::function<void()> &onThemeChanged, QObject *parent)
|
||||
{
|
||||
if (!onThemeChanged || !parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
#if defined(Q_OS_MAC) && !defined(MACOS_NE)
|
||||
MacTrayTheme::installThemeObserver(onThemeChanged, parent);
|
||||
#elif defined(Q_OS_WIN) || (defined(Q_OS_MAC) && defined(MACOS_NE))
|
||||
WinTrayTheme::installThemeObserver(onThemeChanged, parent);
|
||||
#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
|
||||
LinuxTrayTheme::installThemeObserver(onThemeChanged, parent);
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#ifndef PLATFORMTRAYTHEME_H
|
||||
#define PLATFORMTRAYTHEME_H
|
||||
|
||||
#include <functional>
|
||||
|
||||
class QObject;
|
||||
|
||||
void installTrayThemeObserver(const std::function<void()> &onThemeChanged, QObject *parent);
|
||||
|
||||
#endif // PLATFORMTRAYTHEME_H
|
||||
@@ -5,27 +5,26 @@
|
||||
#include <QDebug>
|
||||
#include "systemTrayNotificationHandler.h"
|
||||
|
||||
|
||||
#ifdef Q_OS_MAC
|
||||
# include "platforms/macos/macosutils.h"
|
||||
#endif
|
||||
#include "platformTheme.h"
|
||||
#include "platformTrayTheme.h"
|
||||
#include "trayIconBackend.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QDesktopServices>
|
||||
#include <QIcon>
|
||||
#include <QWindow>
|
||||
|
||||
#include "version.h"
|
||||
|
||||
SystemTrayNotificationHandler::SystemTrayNotificationHandler(QObject* parent) :
|
||||
NotificationHandler(parent),
|
||||
m_systemTrayIcon(parent)
|
||||
|
||||
NotificationHandler(parent)
|
||||
{
|
||||
m_systemTrayIcon.show();
|
||||
connect(&m_systemTrayIcon, &QSystemTrayIcon::activated, this, &SystemTrayNotificationHandler::onTrayActivated);
|
||||
m_trayIcon = createTrayIconBackend(this);
|
||||
m_trayIcon->setMenu(&m_menu);
|
||||
m_trayIcon->setToolTip(APPLICATION_NAME);
|
||||
m_trayIcon->setActivatedHandler([this](QSystemTrayIcon::ActivationReason reason) {
|
||||
onTrayActivated(reason);
|
||||
});
|
||||
|
||||
m_trayActionShow = m_menu.addAction(QIcon(":/images/tray/application.png"), tr("Show") + " " + APPLICATION_NAME, this, [this](){
|
||||
m_trayActionShow = m_menu.addAction(tr("Show") + " " + APPLICATION_NAME, this, [this](){
|
||||
emit raiseRequested();
|
||||
});
|
||||
m_menu.addSeparator();
|
||||
@@ -34,22 +33,22 @@ SystemTrayNotificationHandler::SystemTrayNotificationHandler(QObject* parent) :
|
||||
|
||||
m_menu.addSeparator();
|
||||
|
||||
m_trayActionVisitWebSite = m_menu.addAction(QIcon(":/images/tray/link.png"), tr("Visit Website"), [&](){
|
||||
m_trayActionVisitWebSite = m_menu.addAction(tr("Visit Website"), [&](){
|
||||
QDesktopServices::openUrl(QUrl(websiteUrl));
|
||||
});
|
||||
|
||||
// Quit action: disconnect VPN first on macOS NE, else quit directly
|
||||
m_trayActionQuit = m_menu.addAction(QIcon(":/images/tray/cancel.png"),
|
||||
tr("Quit") + " " + APPLICATION_NAME,
|
||||
m_trayActionQuit = m_menu.addAction(tr("Quit") + " " + APPLICATION_NAME,
|
||||
this,
|
||||
[&](){ qApp->quit(); });
|
||||
|
||||
m_systemTrayIcon.setContextMenu(&m_menu);
|
||||
installTrayThemeObserver([this]() { refreshTheme(); }, this);
|
||||
|
||||
m_isDarkTheme = platformIsDarkTheme();
|
||||
setTrayState(Vpn::ConnectionState::Disconnected);
|
||||
m_trayIcon->show();
|
||||
}
|
||||
|
||||
SystemTrayNotificationHandler::~SystemTrayNotificationHandler() {
|
||||
}
|
||||
SystemTrayNotificationHandler::~SystemTrayNotificationHandler() = default;
|
||||
|
||||
void SystemTrayNotificationHandler::setConnectionState(Vpn::ConnectionState state)
|
||||
{
|
||||
@@ -63,21 +62,65 @@ void SystemTrayNotificationHandler::onTranslationsUpdated()
|
||||
m_trayActionConnect->setText(tr("Connect"));
|
||||
m_trayActionDisconnect->setText(tr("Disconnect"));
|
||||
m_trayActionVisitWebSite->setText(tr("Visit Website"));
|
||||
m_trayActionQuit->setText(tr("Quit")+ " " + APPLICATION_NAME);
|
||||
m_trayActionQuit->setText(tr("Quit") + " " + APPLICATION_NAME);
|
||||
|
||||
if (m_trayIcon) {
|
||||
m_trayIcon->rebuildMenu();
|
||||
}
|
||||
}
|
||||
|
||||
void SystemTrayNotificationHandler::updateWebsiteUrl(const QString &newWebsiteUrl) {
|
||||
void SystemTrayNotificationHandler::updateWebsiteUrl(const QString &newWebsiteUrl)
|
||||
{
|
||||
qDebug() << "Updated website URL:" << newWebsiteUrl;
|
||||
websiteUrl = newWebsiteUrl;
|
||||
}
|
||||
|
||||
void SystemTrayNotificationHandler::setTrayIcon(const QString &iconPath)
|
||||
void SystemTrayNotificationHandler::refreshTheme()
|
||||
{
|
||||
QIcon trayIconMask(QPixmap(iconPath).scaled(128,128));
|
||||
#ifndef Q_OS_MAC
|
||||
trayIconMask.setIsMask(true);
|
||||
const bool isDarkTheme = platformIsDarkTheme();
|
||||
const bool themeChanged = (isDarkTheme != m_isDarkTheme);
|
||||
m_isDarkTheme = isDarkTheme;
|
||||
|
||||
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
|
||||
updateTrayIcon();
|
||||
#else
|
||||
if (themeChanged) {
|
||||
updateTrayIcon();
|
||||
}
|
||||
#endif
|
||||
m_systemTrayIcon.setIcon(trayIconMask);
|
||||
}
|
||||
|
||||
TrayIconVisual SystemTrayNotificationHandler::currentTrayVisual() const
|
||||
{
|
||||
TrayIconVisual visual;
|
||||
visual.connectionState = m_errorLatched ? Vpn::ConnectionState::Error : m_trayState;
|
||||
visual.darkTheme = m_isDarkTheme;
|
||||
return visual;
|
||||
}
|
||||
|
||||
void SystemTrayNotificationHandler::setConnectionError()
|
||||
{
|
||||
m_errorLatched = true;
|
||||
updateTrayIcon();
|
||||
}
|
||||
|
||||
void SystemTrayNotificationHandler::clearConnectionError()
|
||||
{
|
||||
if (!m_errorLatched) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_errorLatched = false;
|
||||
updateTrayIcon();
|
||||
}
|
||||
|
||||
void SystemTrayNotificationHandler::updateTrayIcon()
|
||||
{
|
||||
if (!m_trayIcon) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_trayIcon->applyVisual(currentTrayVisual());
|
||||
}
|
||||
|
||||
void SystemTrayNotificationHandler::onTrayActivated(QSystemTrayIcon::ActivationReason reason)
|
||||
@@ -91,41 +134,48 @@ void SystemTrayNotificationHandler::onTrayActivated(QSystemTrayIcon::ActivationR
|
||||
|
||||
void SystemTrayNotificationHandler::setTrayState(Vpn::ConnectionState state)
|
||||
{
|
||||
QString resourcesPath = ":/images/tray/%1";
|
||||
if (state == Vpn::ConnectionState::Error || state == Vpn::ConnectionState::Unknown) {
|
||||
// Latch the error icon. Both Error and Unknown surface the error message
|
||||
// in the UI. The connection is torn down to Disconnected right after, so
|
||||
// treat the real state as Disconnected and let the latch keep the error
|
||||
// icon visible until the error is acknowledged.
|
||||
m_errorLatched = true;
|
||||
state = Vpn::ConnectionState::Disconnected;
|
||||
} else if (state != Vpn::ConnectionState::Disconnected) {
|
||||
// A new (re)connecting/connected lifecycle clears a previous error.
|
||||
// Plain Disconnected leaves the latch untouched so the auto-Disconnected
|
||||
// that immediately follows an error does not drop the error icon.
|
||||
m_errorLatched = false;
|
||||
}
|
||||
|
||||
m_trayState = state;
|
||||
|
||||
switch (state) {
|
||||
case Vpn::ConnectionState::Disconnected:
|
||||
setTrayIcon(QString(resourcesPath).arg(DisconnectedTrayIconName));
|
||||
m_trayActionConnect->setEnabled(true);
|
||||
m_trayActionDisconnect->setEnabled(false);
|
||||
break;
|
||||
case Vpn::ConnectionState::Preparing:
|
||||
setTrayIcon(QString(resourcesPath).arg(DisconnectedTrayIconName));
|
||||
m_trayActionConnect->setEnabled(false);
|
||||
m_trayActionDisconnect->setEnabled(true);
|
||||
break;
|
||||
case Vpn::ConnectionState::Connecting:
|
||||
setTrayIcon(QString(resourcesPath).arg(DisconnectedTrayIconName));
|
||||
m_trayActionConnect->setEnabled(false);
|
||||
m_trayActionDisconnect->setEnabled(true);
|
||||
break;
|
||||
case Vpn::ConnectionState::Connected:
|
||||
setTrayIcon(QString(resourcesPath).arg(ConnectedTrayIconName));
|
||||
m_trayActionConnect->setEnabled(false);
|
||||
m_trayActionDisconnect->setEnabled(true);
|
||||
break;
|
||||
case Vpn::ConnectionState::Disconnecting:
|
||||
setTrayIcon(QString(resourcesPath).arg(DisconnectedTrayIconName));
|
||||
m_trayActionConnect->setEnabled(false);
|
||||
m_trayActionDisconnect->setEnabled(true);
|
||||
break;
|
||||
case Vpn::ConnectionState::Reconnecting:
|
||||
setTrayIcon(QString(resourcesPath).arg(DisconnectedTrayIconName));
|
||||
m_trayActionConnect->setEnabled(false);
|
||||
m_trayActionDisconnect->setEnabled(true);
|
||||
break;
|
||||
case Vpn::ConnectionState::Error:
|
||||
setTrayIcon(QString(resourcesPath).arg(ErrorTrayIconName));
|
||||
m_trayActionConnect->setEnabled(true);
|
||||
m_trayActionDisconnect->setEnabled(false);
|
||||
break;
|
||||
@@ -133,41 +183,26 @@ void SystemTrayNotificationHandler::setTrayState(Vpn::ConnectionState state)
|
||||
default:
|
||||
m_trayActionConnect->setEnabled(false);
|
||||
m_trayActionDisconnect->setEnabled(true);
|
||||
setTrayIcon(QString(resourcesPath).arg(DisconnectedTrayIconName));
|
||||
break;
|
||||
}
|
||||
|
||||
//#ifdef Q_OS_MAC
|
||||
// // Get theme from current user (note, this app can be launched as root application and in this case this theme can be different from theme of real current user )
|
||||
// bool darkTaskBar = MacOSFunctions::instance().isMenuBarUseDarkTheme();
|
||||
// darkTaskBar = forceUseBrightIcons ? true : darkTaskBar;
|
||||
// resourcesPath = ":/images_mac/tray_icon/%1";
|
||||
// useIconName = useIconName.replace(".png", darkTaskBar ? "@2x.png" : " dark@2x.png");
|
||||
//#endif
|
||||
}
|
||||
updateTrayIcon();
|
||||
|
||||
if (m_trayIcon) {
|
||||
m_trayIcon->rebuildMenu();
|
||||
}
|
||||
}
|
||||
|
||||
void SystemTrayNotificationHandler::notify(NotificationHandler::Message type,
|
||||
const QString& title,
|
||||
const QString& message,
|
||||
int timerMsec) {
|
||||
Q_UNUSED(type);
|
||||
int timerMsec)
|
||||
{
|
||||
Q_UNUSED(type);
|
||||
|
||||
QIcon icon(ConnectedTrayIconName);
|
||||
m_systemTrayIcon.showMessage(title, message, icon, timerMsec);
|
||||
if (!m_trayIcon) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_trayIcon->showMessage(title, message, currentTrayVisual(), timerMsec);
|
||||
}
|
||||
|
||||
void SystemTrayNotificationHandler::showHideWindow() {
|
||||
// QmlEngineHolder* engine = QmlEngineHolder::instance();
|
||||
// if (engine->window()->isVisible()) {
|
||||
// engine->hideWindow();
|
||||
//#ifdef MVPN_MACOS
|
||||
// MacOSUtils::hideDockIcon();
|
||||
//#endif
|
||||
// } else {
|
||||
// engine->showWindow();
|
||||
//#ifdef MVPN_MACOS
|
||||
// MacOSUtils::showDockIcon();
|
||||
//#endif
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
@@ -6,16 +6,18 @@
|
||||
#define SYSTEMTRAYNOTIFICATIONHANDLER_H
|
||||
|
||||
#include "notificationHandler.h"
|
||||
#include "trayIconBackend.h"
|
||||
|
||||
#include <QMenu>
|
||||
#include <QSystemTrayIcon>
|
||||
|
||||
#include <memory>
|
||||
|
||||
class SystemTrayNotificationHandler : public NotificationHandler {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SystemTrayNotificationHandler(QObject* parent);
|
||||
~SystemTrayNotificationHandler();
|
||||
~SystemTrayNotificationHandler() override;
|
||||
|
||||
void setConnectionState(Vpn::ConnectionState state) override;
|
||||
|
||||
@@ -23,35 +25,37 @@ public:
|
||||
|
||||
public slots:
|
||||
void updateWebsiteUrl(const QString &newWebsiteUrl);
|
||||
void setConnectionError();
|
||||
void clearConnectionError();
|
||||
|
||||
protected:
|
||||
virtual void notify(Message type, const QString& title,
|
||||
const QString& message, int timerMsec) override;
|
||||
void notify(Message type, const QString& title,
|
||||
const QString& message, int timerMsec) override;
|
||||
|
||||
private:
|
||||
void showHideWindow();
|
||||
|
||||
void setTrayState(Vpn::ConnectionState state);
|
||||
void onTrayActivated(QSystemTrayIcon::ActivationReason reason);
|
||||
|
||||
void setTrayIcon(const QString &iconPath);
|
||||
void refreshTheme();
|
||||
void updateTrayIcon();
|
||||
TrayIconVisual currentTrayVisual() const;
|
||||
|
||||
private:
|
||||
QMenu m_menu;
|
||||
QSystemTrayIcon m_systemTrayIcon;
|
||||
std::unique_ptr<TrayIconBackend> m_trayIcon;
|
||||
|
||||
QAction* m_trayActionShow = nullptr;
|
||||
QAction* m_trayActionConnect = nullptr;
|
||||
QAction* m_trayActionDisconnect = nullptr;
|
||||
QAction* m_trayActionVisitWebSite = nullptr;
|
||||
QAction* m_trayActionQuit = nullptr;
|
||||
QAction* m_statusLabel = nullptr;
|
||||
QAction* m_statusLabel = nullptr;
|
||||
QAction* m_separator = nullptr;
|
||||
|
||||
const QString ConnectedTrayIconName = "active.png";
|
||||
const QString DisconnectedTrayIconName = "default.png";
|
||||
const QString ErrorTrayIconName = "error.png";
|
||||
QString websiteUrl = "https://amnezia.org";
|
||||
Vpn::ConnectionState m_trayState = Vpn::ConnectionState::Unknown;
|
||||
bool m_isDarkTheme = false;
|
||||
bool m_errorLatched = false;
|
||||
|
||||
QString websiteUrl = "https://amnezia.org";
|
||||
};
|
||||
|
||||
#endif // SYSTEMTRAYNOTIFICATIONHANDLER_H
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
#ifndef TRAYICONBACKEND_H
|
||||
#define TRAYICONBACKEND_H
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
#include <QMenu>
|
||||
#include <QString>
|
||||
#include <QSystemTrayIcon>
|
||||
|
||||
#include "core/protocols/vpnProtocol.h"
|
||||
|
||||
class QObject;
|
||||
|
||||
struct TrayIconVisual {
|
||||
Vpn::ConnectionState connectionState = Vpn::ConnectionState::Unknown;
|
||||
bool darkTheme = false;
|
||||
};
|
||||
|
||||
class TrayIconBackend {
|
||||
public:
|
||||
virtual ~TrayIconBackend() = default;
|
||||
|
||||
virtual void setMenu(QMenu *menu) = 0;
|
||||
virtual void setToolTip(const QString &tooltip) = 0;
|
||||
virtual void show() = 0;
|
||||
virtual void applyVisual(const TrayIconVisual &visual) = 0;
|
||||
virtual void showMessage(const QString &title, const QString &message, const TrayIconVisual &visual, int timerMsec) = 0;
|
||||
virtual void rebuildMenu() = 0;
|
||||
virtual void setActivatedHandler(std::function<void(QSystemTrayIcon::ActivationReason)> handler) = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<TrayIconBackend> createTrayIconBackend(QObject *parent);
|
||||
|
||||
#endif // TRAYICONBACKEND_H
|
||||
@@ -0,0 +1,80 @@
|
||||
#include "trayIconCommon.h"
|
||||
|
||||
#include <QBuffer>
|
||||
#include <QDebug>
|
||||
#include <QPainter>
|
||||
#include <QSvgRenderer>
|
||||
|
||||
namespace TrayIconCommon
|
||||
{
|
||||
QString resourcePathForState(Vpn::ConnectionState state, bool darkTheme)
|
||||
{
|
||||
switch (state) {
|
||||
case Vpn::ConnectionState::Error:
|
||||
return QString::fromLatin1(darkTheme ? kIconErrorWhite : kIconErrorBlack);
|
||||
case Vpn::ConnectionState::Connected:
|
||||
return QString::fromLatin1(darkTheme ? kIconOnWhite : kIconOnBlack);
|
||||
case Vpn::ConnectionState::Disconnected:
|
||||
case Vpn::ConnectionState::Preparing:
|
||||
case Vpn::ConnectionState::Connecting:
|
||||
case Vpn::ConnectionState::Disconnecting:
|
||||
case Vpn::ConnectionState::Reconnecting:
|
||||
case Vpn::ConnectionState::Unknown:
|
||||
default:
|
||||
return QString::fromLatin1(darkTheme ? kIconOffLight : kIconOffBlack);
|
||||
}
|
||||
}
|
||||
|
||||
bool isColoredState(Vpn::ConnectionState state)
|
||||
{
|
||||
return state == Vpn::ConnectionState::Error;
|
||||
}
|
||||
|
||||
QPixmap renderIcon(const QString &resourcePath, int size)
|
||||
{
|
||||
QSvgRenderer renderer(resourcePath);
|
||||
QPixmap pixmap(size, size);
|
||||
pixmap.fill(Qt::transparent);
|
||||
|
||||
if (!renderer.isValid()) {
|
||||
qWarning() << "Failed to load tray icon:" << resourcePath;
|
||||
return pixmap;
|
||||
}
|
||||
|
||||
QPainter painter(&pixmap);
|
||||
renderer.render(&painter, QRectF(0, 0, size, size));
|
||||
return pixmap;
|
||||
}
|
||||
|
||||
QPixmap buildPixmap(int size, Vpn::ConnectionState state, bool darkTheme)
|
||||
{
|
||||
return renderIcon(resourcePathForState(state, darkTheme), size);
|
||||
}
|
||||
|
||||
QIcon buildIcon(Vpn::ConnectionState state, bool darkTheme)
|
||||
{
|
||||
QIcon icon;
|
||||
icon.addPixmap(buildPixmap(kDefaultTrayIconSize, state, darkTheme));
|
||||
return icon;
|
||||
}
|
||||
|
||||
QByteArray pixmapToPng(const QPixmap &pixmap)
|
||||
{
|
||||
QByteArray bytes;
|
||||
QBuffer buffer(&bytes);
|
||||
buffer.open(QIODevice::WriteOnly);
|
||||
pixmap.save(&buffer, "PNG");
|
||||
return bytes;
|
||||
}
|
||||
|
||||
QByteArray buildTemplatePng(Vpn::ConnectionState state)
|
||||
{
|
||||
return pixmapToPng(renderIcon(resourcePathForState(state, /*darkTheme*/ true), kDefaultTrayIconSize));
|
||||
}
|
||||
|
||||
QByteArray buildColorPng(Vpn::ConnectionState state, bool darkTheme)
|
||||
{
|
||||
return pixmapToPng(renderIcon(resourcePathForState(state, darkTheme), kDefaultTrayIconSize));
|
||||
}
|
||||
|
||||
} // namespace TrayIconCommon
|
||||
@@ -0,0 +1,35 @@
|
||||
#ifndef TRAYICONCOMMON_H
|
||||
#define TRAYICONCOMMON_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QIcon>
|
||||
#include <QPixmap>
|
||||
#include <QString>
|
||||
|
||||
#include "core/protocols/vpnProtocol.h"
|
||||
|
||||
namespace TrayIconCommon
|
||||
{
|
||||
constexpr int kDefaultTrayIconSize = 128;
|
||||
|
||||
constexpr char kIconOffBlack[] = ":/images/tray/off-black.svg";
|
||||
constexpr char kIconOffLight[] = ":/images/tray/off-light.svg";
|
||||
constexpr char kIconOnBlack[] = ":/images/tray/on-black.svg";
|
||||
constexpr char kIconOnWhite[] = ":/images/tray/on-white.svg";
|
||||
constexpr char kIconErrorBlack[] = ":/images/tray/error-black.svg";
|
||||
constexpr char kIconErrorWhite[] = ":/images/tray/error-white.svg";
|
||||
|
||||
QString resourcePathForState(Vpn::ConnectionState state, bool darkTheme);
|
||||
|
||||
bool isColoredState(Vpn::ConnectionState state);
|
||||
|
||||
QPixmap renderIcon(const QString &resourcePath, int size);
|
||||
|
||||
QPixmap buildPixmap(int size, Vpn::ConnectionState state, bool darkTheme);
|
||||
QIcon buildIcon(Vpn::ConnectionState state, bool darkTheme);
|
||||
QByteArray buildTemplatePng(Vpn::ConnectionState state);
|
||||
QByteArray buildColorPng(Vpn::ConnectionState state, bool darkTheme);
|
||||
|
||||
} // namespace TrayIconCommon
|
||||
|
||||
#endif // TRAYICONCOMMON_H
|
||||
@@ -0,0 +1,20 @@
|
||||
#include "trayThemeChangeFilter.h"
|
||||
|
||||
#include <QEvent>
|
||||
|
||||
TrayThemeChangeFilter::TrayThemeChangeFilter(std::function<void()> onThemeChanged, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_onThemeChanged(std::move(onThemeChanged))
|
||||
{
|
||||
}
|
||||
|
||||
bool TrayThemeChangeFilter::eventFilter(QObject *watched, QEvent *event)
|
||||
{
|
||||
Q_UNUSED(watched);
|
||||
if (event->type() == QEvent::ApplicationPaletteChange || event->type() == QEvent::ThemeChange) {
|
||||
if (m_onThemeChanged) {
|
||||
m_onThemeChanged();
|
||||
}
|
||||
}
|
||||
return QObject::eventFilter(watched, event);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#ifndef TRAYTHEMECHANGEFILTER_H
|
||||
#define TRAYTHEMECHANGEFILTER_H
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include <QObject>
|
||||
|
||||
class TrayThemeChangeFilter final : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit TrayThemeChangeFilter(std::function<void()> onThemeChanged, QObject *parent = nullptr);
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||
|
||||
private:
|
||||
std::function<void()> m_onThemeChanged;
|
||||
};
|
||||
|
||||
#endif // TRAYTHEMECHANGEFILTER_H
|
||||