From 45192ae16df64c8c9dfb2701467311627d208372 Mon Sep 17 00:00:00 2001 From: Farcimin Date: Fri, 23 Jan 2026 10:55:46 +0300 Subject: [PATCH 1/2] macOS: recover UI after sleep via app state change --- src/WindowManager.cpp | 59 ++++++++++++++++++++++++++++++++++++++++++- src/WindowManager.h | 5 ++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/src/WindowManager.cpp b/src/WindowManager.cpp index 5d6696e9..cf34dad6 100644 --- a/src/WindowManager.cpp +++ b/src/WindowManager.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include "Application.h" #include "constants.h" @@ -41,6 +42,9 @@ WindowManager::WindowManager(QObject *parent) connect(qApp, SIGNAL(anotherInstanceStarted()), this, SLOT(raise())); connect(qApp, &QGuiApplication::lastWindowClosed, this, &WindowManager::quitAfterLastWindow); +#if defined(Q_OS_MACOS) + connect(qApp, &QGuiApplication::applicationStateChanged, this, &WindowManager::onApplicationStateChanged); +#endif m_tray = new QSystemTrayIcon(icons()->icon("appicons/64x64.png")); m_tray->setToolTip("Feather Wallet"); @@ -621,6 +625,59 @@ void WindowManager::onWalletPassphraseNeeded(bool on_device) { m_walletManager->onPassphraseEntered(passphrase, false, false); } +void WindowManager::onApplicationStateChanged(Qt::ApplicationState state) { +#if defined(Q_OS_MACOS) + if (state == Qt::ApplicationInactive || state == Qt::ApplicationHidden) { + m_inactiveTimer.start(); + return; + } + + if (state == Qt::ApplicationActive && m_inactiveTimer.isValid()) { + // Only run recovery after longer inactivity (sleep/wake) to avoid normal app switches. + constexpr qint64 wakeThresholdMs = 60 * 1000; + const qint64 inactiveMs = m_inactiveTimer.elapsed(); + m_inactiveTimer.invalidate(); + if (inactiveMs >= wakeThresholdMs) { + QTimer::singleShot(0, this, &WindowManager::recoverFromSleep); + } + } +#else + Q_UNUSED(state); +#endif +} + +void WindowManager::recoverFromSleep() { +#if defined(Q_OS_MACOS) + // Recreate tray icon to work around macOS Qt status item issues after sleep. + const bool trayVisible = conf()->get(Config::showTrayIcon).toBool(); + if (m_tray) { + m_tray->setVisible(false); + delete m_tray; + m_tray = nullptr; + } + + m_tray = new QSystemTrayIcon(icons()->icon("appicons/64x64.png")); + m_tray->setToolTip("Feather Wallet"); + this->buildTrayMenu(); + m_tray->setVisible(trayVisible); + + for (const auto &window : m_windows) { + if (!window) { + continue; + } + if (!window->isHidden()) { + window->bringToFront(); + } else { + window->update(); + } + } + if (m_wizard && !m_wizard->isHidden()) { + m_wizard->raise(); + m_wizard->activateWindow(); + } +#endif +} + // ######################## TRAY ######################## void WindowManager::buildTrayMenu() { @@ -818,4 +875,4 @@ WindowManager* WindowManager::instance() } return m_instance; -} \ No newline at end of file +} diff --git a/src/WindowManager.h b/src/WindowManager.h index d0f3e460..ebdc2f68 100644 --- a/src/WindowManager.h +++ b/src/WindowManager.h @@ -6,6 +6,7 @@ #include #include +#include #include "utils/EventFilter.h" #include "utils/nodes.h" @@ -71,6 +72,8 @@ private slots: void onDeviceError(const QString &errorMessage, quint64 errorCode); void onWalletPassphraseNeeded(bool on_device); void onChangeTheme(const QString &themeName); + void onApplicationStateChanged(Qt::ApplicationState state); + void recoverFromSleep(); private: void tryCreateWallet(Seed seed, const QString &path, const QString &password, const QString &seedLanguage, const QString &seedOffset, const QString &subaddressLookahead, bool newWallet); @@ -115,6 +118,8 @@ private slots: bool m_initialNetworkConfigured = false; QThread *m_cleanupThread; + + QElapsedTimer m_inactiveTimer; }; inline WindowManager* windowManager() From 34030a264f617dc2631ee52cd43a9a8e06c53320 Mon Sep 17 00:00:00 2001 From: Farcimin Date: Fri, 23 Jan 2026 11:06:09 +0300 Subject: [PATCH 2/2] macOS: listen for sleep/wake via NSWorkspace --- src/CMakeLists.txt | 5 +++ src/WindowManager.cpp | 30 ++++--------- src/WindowManager.h | 7 ++- src/utils/os/macos_sleep.h | 27 +++++++++++ src/utils/os/macos_sleep.mm | 90 +++++++++++++++++++++++++++++++++++++ 5 files changed, 133 insertions(+), 26 deletions(-) create mode 100644 src/utils/os/macos_sleep.h create mode 100644 src/utils/os/macos_sleep.mm diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1ad529ca..218a6ddd 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -77,6 +77,11 @@ file(GLOB SOURCE_FILES "plugins/*.h" ) +if(APPLE) + list(APPEND SOURCE_FILES + "utils/os/macos_sleep.mm") +endif() + get_cmake_property(_vars VARIABLES) set(PLUGIN_PREFIX "WITH_PLUGIN_") diff --git a/src/WindowManager.cpp b/src/WindowManager.cpp index cf34dad6..095036aa 100644 --- a/src/WindowManager.cpp +++ b/src/WindowManager.cpp @@ -7,7 +7,11 @@ #include #include #include + +#if defined(Q_OS_MACOS) +#include "utils/os/macos_sleep.h" #include +#endif #include "Application.h" #include "constants.h" @@ -43,7 +47,10 @@ WindowManager::WindowManager(QObject *parent) connect(qApp, SIGNAL(anotherInstanceStarted()), this, SLOT(raise())); connect(qApp, &QGuiApplication::lastWindowClosed, this, &WindowManager::quitAfterLastWindow); #if defined(Q_OS_MACOS) - connect(qApp, &QGuiApplication::applicationStateChanged, this, &WindowManager::onApplicationStateChanged); + m_macSleepObserver = new MacSleepObserver(this); + connect(m_macSleepObserver, &MacSleepObserver::didWake, this, [this] { + QTimer::singleShot(0, this, &WindowManager::recoverFromSleep); + }); #endif m_tray = new QSystemTrayIcon(icons()->icon("appicons/64x64.png")); @@ -625,27 +632,6 @@ void WindowManager::onWalletPassphraseNeeded(bool on_device) { m_walletManager->onPassphraseEntered(passphrase, false, false); } -void WindowManager::onApplicationStateChanged(Qt::ApplicationState state) { -#if defined(Q_OS_MACOS) - if (state == Qt::ApplicationInactive || state == Qt::ApplicationHidden) { - m_inactiveTimer.start(); - return; - } - - if (state == Qt::ApplicationActive && m_inactiveTimer.isValid()) { - // Only run recovery after longer inactivity (sleep/wake) to avoid normal app switches. - constexpr qint64 wakeThresholdMs = 60 * 1000; - const qint64 inactiveMs = m_inactiveTimer.elapsed(); - m_inactiveTimer.invalidate(); - if (inactiveMs >= wakeThresholdMs) { - QTimer::singleShot(0, this, &WindowManager::recoverFromSleep); - } - } -#else - Q_UNUSED(state); -#endif -} - void WindowManager::recoverFromSleep() { #if defined(Q_OS_MACOS) // Recreate tray icon to work around macOS Qt status item issues after sleep. diff --git a/src/WindowManager.h b/src/WindowManager.h index ebdc2f68..a55b35de 100644 --- a/src/WindowManager.h +++ b/src/WindowManager.h @@ -6,7 +6,6 @@ #include #include -#include #include "utils/EventFilter.h" #include "utils/nodes.h" @@ -72,7 +71,6 @@ private slots: void onDeviceError(const QString &errorMessage, quint64 errorCode); void onWalletPassphraseNeeded(bool on_device); void onChangeTheme(const QString &themeName); - void onApplicationStateChanged(Qt::ApplicationState state); void recoverFromSleep(); private: @@ -118,8 +116,9 @@ private slots: bool m_initialNetworkConfigured = false; QThread *m_cleanupThread; - - QElapsedTimer m_inactiveTimer; +#if defined(Q_OS_MACOS) + class MacSleepObserver *m_macSleepObserver = nullptr; +#endif }; inline WindowManager* windowManager() diff --git a/src/utils/os/macos_sleep.h b/src/utils/os/macos_sleep.h new file mode 100644 index 00000000..f786db0d --- /dev/null +++ b/src/utils/os/macos_sleep.h @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: The Monero Project + +#ifndef FEATHER_MACOS_SLEEP_H +#define FEATHER_MACOS_SLEEP_H + +#include + +class MacSleepObserver : public QObject { + Q_OBJECT + +public: + explicit MacSleepObserver(QObject *parent = nullptr); + ~MacSleepObserver() override; + + void notifyWillSleep(); + void notifyDidWake(); + +signals: + void willSleep(); + void didWake(); + +private: + void *m_observer = nullptr; +}; + +#endif // FEATHER_MACOS_SLEEP_H diff --git a/src/utils/os/macos_sleep.mm b/src/utils/os/macos_sleep.mm new file mode 100644 index 00000000..5fc79f7e --- /dev/null +++ b/src/utils/os/macos_sleep.mm @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: The Monero Project + +#include "macos_sleep.h" + +#if defined(Q_OS_MACOS) + +#import + +@interface FeatherSleepObserver : NSObject +@property (nonatomic, assign) MacSleepObserver *owner; +@end + +@implementation FeatherSleepObserver +- (instancetype)initWithOwner:(MacSleepObserver *)owner { + self = [super init]; + if (self) { + _owner = owner; + NSNotificationCenter *center = [[NSWorkspace sharedWorkspace] notificationCenter]; + [center addObserver:self selector:@selector(onWillSleep:) name:NSWorkspaceWillSleepNotification object:nil]; + [center addObserver:self selector:@selector(onDidWake:) name:NSWorkspaceDidWakeNotification object:nil]; + } + return self; +} + +- (void)dealloc { + NSNotificationCenter *center = [[NSWorkspace sharedWorkspace] notificationCenter]; + [center removeObserver:self]; +#if !__has_feature(objc_arc) + [super dealloc]; +#endif +} + +- (void)onWillSleep:(NSNotification *)__unused notification { + if (_owner) { + _owner->notifyWillSleep(); + } +} + +- (void)onDidWake:(NSNotification *)__unused notification { + if (_owner) { + _owner->notifyDidWake(); + } +} +@end + +MacSleepObserver::MacSleepObserver(QObject *parent) + : QObject(parent) +{ + FeatherSleepObserver *observer = [[FeatherSleepObserver alloc] initWithOwner:this]; +#if __has_feature(objc_arc) + m_observer = (__bridge_retained void *)observer; +#else + m_observer = observer; +#endif +} + +MacSleepObserver::~MacSleepObserver() { + if (m_observer) { +#if !__has_feature(objc_arc) + FeatherSleepObserver *observer = static_cast(m_observer); + [observer release]; +#else + CFBridgingRelease(m_observer); +#endif + m_observer = nullptr; + } +} + +void MacSleepObserver::notifyWillSleep() { + emit willSleep(); +} + +void MacSleepObserver::notifyDidWake() { + emit didWake(); +} + +#else + +MacSleepObserver::MacSleepObserver(QObject *parent) + : QObject(parent) +{} + +MacSleepObserver::~MacSleepObserver() = default; + +void MacSleepObserver::notifyWillSleep() {} + +void MacSleepObserver::notifyDidWake() {} + +#endif