From 9de372d71555ae6227343b5652dad5db10b1dd81 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Thu, 21 Apr 2022 22:17:38 +0300 Subject: [PATCH] Replaced macOS Qt tray with native implementation. --- Telegram/SourceFiles/platform/mac/tray_mac.h | 8 +- Telegram/SourceFiles/platform/mac/tray_mac.mm | 297 ++++++++++++++---- 2 files changed, 247 insertions(+), 58 deletions(-) diff --git a/Telegram/SourceFiles/platform/mac/tray_mac.h b/Telegram/SourceFiles/platform/mac/tray_mac.h index 584f811247..228a2abc79 100644 --- a/Telegram/SourceFiles/platform/mac/tray_mac.h +++ b/Telegram/SourceFiles/platform/mac/tray_mac.h @@ -12,13 +12,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/unique_qptr.h" class QMenu; -class QSystemTrayIcon; namespace Platform { +class NativeIcon; + class Tray final { public: Tray(); + ~Tray(); [[nodiscard]] rpl::producer<> aboutToShowRequests() const; [[nodiscard]] rpl::producer<> showFromTrayRequests() const; @@ -41,9 +43,11 @@ public: [[nodiscard]] rpl::lifetime &lifetime(); private: - base::unique_qptr _icon; + std::unique_ptr _nativeIcon; base::unique_qptr _menu; + rpl::event_stream<> _showFromTrayRequests; + rpl::lifetime _actionsLifetime; rpl::lifetime _lifetime; diff --git a/Telegram/SourceFiles/platform/mac/tray_mac.mm b/Telegram/SourceFiles/platform/mac/tray_mac.mm index 90996f32d0..b81d5f399e 100644 --- a/Telegram/SourceFiles/platform/mac/tray_mac.mm +++ b/Telegram/SourceFiles/platform/mac/tray_mac.mm @@ -7,15 +7,75 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "platform/mac/tray_mac.h" -#include "base/qt_signal_producer.h" +#include "base/platform/mac/base_utilities_mac.h" #include "core/application.h" +#include "core/sandbox.h" #include "window/window_controller.h" #include "window/window_session_controller.h" -#include "ui/ui_utility.h" #include "styles/style_window.h" #include -#include + +#import +#import + +@interface CommonDelegate : NSObject { +} + +- (void) menuDidClose:(NSMenu *)menu; +- (void) menuWillOpen:(NSMenu *)menu; +- (void) observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context; + +- (rpl::producer<>) closes; +- (rpl::producer<>) aboutToShowRequests; +- (rpl::producer<>) appearanceChanges; + +@end // @interface CommonDelegate + +@implementation CommonDelegate { + rpl::event_stream<> _closes; + rpl::event_stream<> _aboutToShowRequests; + rpl::event_stream<> _appearanceChanges; +} + +- (void) menuDidClose:(NSMenu *)menu { + Core::Sandbox::Instance().customEnterFromEventLoop([&] { + _closes.fire({}); + }); +} + +- (void) menuWillOpen:(NSMenu *)menu { + Core::Sandbox::Instance().customEnterFromEventLoop([&] { + _aboutToShowRequests.fire({}); + }); +} + +// Thanks https://stackoverflow.com/a/64525038 +- (void) observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context { + if ([keyPath isEqualToString:@"button.effectiveAppearance"]) { + _appearanceChanges.fire({}); + } +} + +- (rpl::producer<>) closes { + return _closes.events(); +} + +- (rpl::producer<>) aboutToShowRequests { + return _aboutToShowRequests.events(); +} + +- (rpl::producer<>) appearanceChanges { + return _appearanceChanges.events(); +} + +@end // @implementation MenuDelegate namespace Platform { @@ -92,42 +152,64 @@ void PlaceCounter( img.setDevicePixelRatio(savedRatio); } -[[nodiscard]] QIcon GenerateIconForTray(int counter, bool muted) { - auto result = QIcon(); - auto lightMode = TrayIconBack(false); - auto darkMode = TrayIconBack(true); - auto lightModeActive = darkMode; - auto darkModeActive = darkMode; - lightModeActive.detach(); - darkModeActive.detach(); - const auto size = 22 * cIntRetinaFactor(); - const auto &bg = (muted ? st::trayCounterBgMute : st::trayCounterBg); +void UpdateIcon(const NSStatusItem *status) { + if (!status) { + return; + } + const auto appearance = status.button.effectiveAppearance; + const auto darkMode = [[appearance.name lowercaseString] + containsString:@"dark"]; + + // The recommended maximum title bar icon height is 18 points + // (device independent pixels). The menu height on past and + // current OS X versions is 22 points. Provide some future-proofing + // by deriving the icon height from the menu height. + const int padding = 0; + const int menuHeight = NSStatusBar.systemStatusBar.thickness; + // [[status.button window] backingScaleFactor]; + const int maxImageHeight = (menuHeight - padding) + * style::DevicePixelRatio(); + + // Select pixmap based on the device pixel height. Ideally we would use + // the devicePixelRatio of the target screen, but that value is not + // known until draw time. Use qApp->devicePixelRatio, which returns the + // devicePixelRatio for the "best" screen on the system. + + const auto side = 22 * style::DevicePixelRatio(); + const auto selectedSize = QSize(side, side); + + auto result = TrayIconBack(darkMode); + auto resultActive = result; + resultActive.detach(); + + const auto counter = Core::App().unreadBadge(); + const auto muted = Core::App().unreadBadgeMuted(); + + const auto &bg = (muted ? st::trayCounterBgMute : st::trayCounterBg); const auto &fg = st::trayCounterFg; const auto &fgInvert = st::trayCounterFgMacInvert; const auto &bgInvert = st::trayCounterBgMacInvert; - PlaceCounter(lightMode, size, counter, bg, fg); - PlaceCounter(darkMode, size, counter, bg, muted ? fgInvert : fg); - PlaceCounter(lightModeActive, size, counter, bgInvert, fgInvert); - PlaceCounter(darkModeActive, size, counter, bgInvert, fgInvert); - result.addPixmap(Ui::PixmapFromImage( - std::move(lightMode)), - QIcon::Normal, - QIcon::Off); - result.addPixmap(Ui::PixmapFromImage( - std::move(darkMode)), - QIcon::Normal, - QIcon::On); - result.addPixmap(Ui::PixmapFromImage( - std::move(lightModeActive)), - QIcon::Active, - QIcon::Off); - result.addPixmap(Ui::PixmapFromImage( - std::move(darkModeActive)), - QIcon::Active, - QIcon::On); - return result; + const auto &resultFg = !darkMode ? fg : muted ? fgInvert : fg; + PlaceCounter(result, side, counter, bg, resultFg); + PlaceCounter(resultActive, side, counter, bgInvert, fgInvert); + + // Scale large pixmaps to fit the available menu bar area. + if (result.height() > maxImageHeight) { + result = result.scaledToHeight( + maxImageHeight, + Qt::SmoothTransformation); + } + if (resultActive.height() > maxImageHeight) { + resultActive = resultActive.scaledToHeight( + maxImageHeight, + Qt::SmoothTransformation); + } + + status.button.image = Q2NSImage(result); + status.button.alternateImage = Q2NSImage(resultActive); + status.button.imageScaling = NSImageScaleProportionallyDown; } [[nodiscard]] QWidget *Parent() { @@ -137,36 +219,137 @@ void PlaceCounter( } // namespace +class NativeIcon final { +public: + NativeIcon(); + ~NativeIcon(); + + void updateIcon(); + void showMenu(not_null menu); + void deactivateButton(); + + [[nodiscard]] rpl::producer<> clicks() const; + [[nodiscard]] rpl::producer<> aboutToShowRequests() const; + +private: + CommonDelegate *_delegate; + NSStatusItem *_status; + + rpl::event_stream<> _clicks; + + rpl::lifetime _lifetime; + +}; + +NativeIcon::NativeIcon() +: _delegate([[CommonDelegate alloc] init]) +, _status([ + [NSStatusBar.systemStatusBar + statusItemWithLength:NSSquareStatusItemLength] retain]) { + + [_status + addObserver:_delegate + forKeyPath:@"button.effectiveAppearance" + options:0 + | NSKeyValueObservingOptionNew + | NSKeyValueObservingOptionInitial + context:nil]; + + [_delegate closes] | rpl::start_with_next([=] { + _status.menu = nil; + }, _lifetime); + + [_delegate appearanceChanges] | rpl::start_with_next([=] { + updateIcon(); + }, _lifetime); + + const auto masks = NSEventMaskLeftMouseDown + | NSEventMaskLeftMouseUp + | NSEventMaskRightMouseDown + | NSEventMaskRightMouseUp + | NSEventMaskOtherMouseUp; + [_status.button sendActionOn:masks]; + + id buttonCallback = [^{ + const auto type = NSApp.currentEvent.type; + + if ((type == NSEventTypeLeftMouseDown) + || (type == NSEventTypeRightMouseDown)) { + Core::Sandbox::Instance().customEnterFromEventLoop([=] { + _clicks.fire({}); + }); + } + } copy]; + + _lifetime.add([=] { + [buttonCallback release]; + }); + + _status.button.target = buttonCallback; + _status.button.action = @selector(invoke); + _status.button.toolTip = Q2NSString(AppName.utf16()); +} + +NativeIcon::~NativeIcon() { + [_status + removeObserver:_delegate + forKeyPath:@"button.effectiveAppearance"]; + [NSStatusBar.systemStatusBar removeStatusItem:_status]; + + [_status release]; + [_delegate release]; +} + +void NativeIcon::updateIcon() { + UpdateIcon(_status); +} + +void NativeIcon::showMenu(not_null menu) { + _status.menu = menu->toNSMenu(); + _status.menu.delegate = _delegate; + [_status.button performClick:nil]; +} + +void NativeIcon::deactivateButton() { + [_status.button highlight:false]; +} + +rpl::producer<> NativeIcon::clicks() const { + return _clicks.events(); +} + +rpl::producer<> NativeIcon::aboutToShowRequests() const { + return [_delegate aboutToShowRequests]; +} + Tray::Tray() { } void Tray::createIcon() { - if (!_icon) { - _icon = base::make_unique_q(Parent()); - updateIcon(); - if (Core::App().isActiveForTrayMenu()) { - _icon->setContextMenu(_menu.get()); - } else { - _icon->setContextMenu(nullptr); - } - _icon->setContextMenu(_menu.get()); // Todo. - // attachToTrayIcon(_icon); - } else { - updateIcon(); + if (!_nativeIcon) { + _nativeIcon = std::make_unique(); + // On macOS we are activating the window on click + // instead of showing the menu, when the window is not activated. + _nativeIcon->clicks( + ) | rpl::start_with_next([=] { + if (Core::App().isActiveForTrayMenu()) { + _nativeIcon->showMenu(_menu.get()); + } else { + _nativeIcon->deactivateButton(); + _showFromTrayRequests.fire({}); + } + }, _lifetime); } - - _icon->show(); + updateIcon(); } void Tray::destroyIcon() { - _icon = nullptr; + _nativeIcon = nullptr; } void Tray::updateIcon() { - if (_icon) { - const auto counter = Core::App().unreadBadge(); - const auto muted = Core::App().unreadBadgeMuted(); - _icon->setIcon(GenerateIconForTray(counter, muted)); + if (_nativeIcon) { + _nativeIcon->updateIcon(); } } @@ -204,13 +387,13 @@ bool Tray::hasTrayMessageSupport() const { } rpl::producer<> Tray::aboutToShowRequests() const { - return _menu - ? base::qt_signal_producer(_menu.get(), &QMenu::aboutToShow) + return _nativeIcon + ? _nativeIcon->aboutToShowRequests() : rpl::never<>(); } rpl::producer<> Tray::showFromTrayRequests() const { - return rpl::never<>(); + return _showFromTrayRequests.events(); } rpl::producer<> Tray::hideToTrayRequests() const { @@ -225,4 +408,6 @@ rpl::lifetime &Tray::lifetime() { return _lifetime; } +Tray::~Tray() = default; + } // namespace Platform