Replaced macOS Qt tray with native implementation.

This commit is contained in:
23rd 2022-04-21 22:17:38 +03:00
parent fbae5bdbcf
commit 9de372d715
2 changed files with 247 additions and 58 deletions

View File

@ -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<QSystemTrayIcon> _icon;
std::unique_ptr<NativeIcon> _nativeIcon;
base::unique_qptr<QMenu> _menu;
rpl::event_stream<> _showFromTrayRequests;
rpl::lifetime _actionsLifetime;
rpl::lifetime _lifetime;

View File

@ -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 <QtWidgets/QMenu>
#include <QtWidgets/QSystemTrayIcon>
#import <AppKit/NSMenu.h>
#import <AppKit/NSStatusItem.h>
@interface CommonDelegate : NSObject<NSMenuDelegate> {
}
- (void) menuDidClose:(NSMenu *)menu;
- (void) menuWillOpen:(NSMenu *)menu;
- (void) observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey, id> *)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<NSKeyValueChangeKey, id> *)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<QMenu*> 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<QMenu*> 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<QSystemTrayIcon>(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<NativeIcon>();
// 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