| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422 |
- /*
- This file is part of Telegram Desktop,
- the official desktop application for the Telegram messaging service.
- For license and copyright information please follow this link:
- https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
- */
- #include "platform/mac/tray_mac.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/painter.h"
- #include "styles/style_window.h"
- #include <QtWidgets/QMenu>
- #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 {
- namespace {
- [[nodiscard]] bool IsAnyActiveForTrayMenu() {
- for (const NSWindow *w in [[NSApplication sharedApplication] windows]) {
- if (w.isKeyWindow) {
- return true;
- }
- }
- return false;
- }
- [[nodiscard]] QImage TrayIconBack(bool darkMode) {
- static const auto WithColor = [](QColor color) {
- return st::macTrayIcon.instance(color, 100);
- };
- static const auto DarkModeResult = WithColor({ 255, 255, 255 });
- static const auto LightModeResult = WithColor({ 0, 0, 0, 180 });
- auto result = darkMode ? DarkModeResult : LightModeResult;
- result.detach();
- return result;
- }
- void PlaceCounter(
- QImage &img,
- int size,
- int count,
- style::color bg,
- style::color color) {
- if (!count) {
- return;
- }
- const auto savedRatio = img.devicePixelRatio();
- img.setDevicePixelRatio(1.);
- {
- Painter p(&img);
- PainterHighQualityEnabler hq(p);
- const auto cnt = (count < 100)
- ? QString("%1").arg(count)
- : QString("..%1").arg(count % 100, 2, 10, QChar('0'));
- const auto cntSize = cnt.size();
- p.setBrush(bg);
- p.setPen(Qt::NoPen);
- int32 fontSize, skip;
- if (size == 22) {
- skip = 1;
- fontSize = 8;
- } else {
- skip = 2;
- fontSize = 16;
- }
- style::font f(fontSize, 0, 0);
- int32 w = f->width(cnt), d, r;
- if (size == 22) {
- d = (cntSize < 2) ? 3 : 2;
- r = (cntSize < 2) ? 6 : 5;
- } else {
- d = (cntSize < 2) ? 6 : 5;
- r = (cntSize < 2) ? 9 : 11;
- }
- p.drawRoundedRect(
- QRect(
- size - w - d * 2 - skip,
- size - f->height - skip,
- w + d * 2,
- f->height),
- r,
- r);
- p.setCompositionMode(QPainter::CompositionMode_Source);
- p.setFont(f);
- p.setPen(color);
- p.drawText(
- size - w - d - skip,
- size - f->height + f->ascent - skip,
- cnt);
- }
- img.setDevicePixelRatio(savedRatio);
- }
- 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;
- 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;
- }
- } // 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 (!_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 (IsAnyActiveForTrayMenu()) {
- _nativeIcon->showMenu(_menu.get());
- } else {
- _nativeIcon->deactivateButton();
- _showFromTrayRequests.fire({});
- }
- }, _lifetime);
- }
- updateIcon();
- }
- void Tray::destroyIcon() {
- _nativeIcon = nullptr;
- }
- void Tray::updateIcon() {
- if (_nativeIcon) {
- _nativeIcon->updateIcon();
- }
- }
- void Tray::createMenu() {
- if (!_menu) {
- _menu = base::make_unique_q<QMenu>(nullptr);
- }
- }
- void Tray::destroyMenu() {
- if (_menu) {
- _menu->clear();
- }
- _actionsLifetime.destroy();
- }
- void Tray::addAction(rpl::producer<QString> text, Fn<void()> &&callback) {
- if (!_menu) {
- return;
- }
- const auto action = _menu->addAction(QString(), std::move(callback));
- std::move(
- text
- ) | rpl::start_with_next([=](const QString &text) {
- action->setText(text);
- }, _actionsLifetime);
- }
- void Tray::showTrayMessage() const {
- }
- bool Tray::hasTrayMessageSupport() const {
- return false;
- }
- rpl::producer<> Tray::aboutToShowRequests() const {
- return _nativeIcon
- ? _nativeIcon->aboutToShowRequests()
- : rpl::never<>();
- }
- rpl::producer<> Tray::showFromTrayRequests() const {
- return _showFromTrayRequests.events();
- }
- rpl::producer<> Tray::hideToTrayRequests() const {
- return rpl::never<>();
- }
- rpl::producer<> Tray::iconClicks() const {
- return rpl::never<>();
- }
- bool Tray::hasIcon() const {
- return _nativeIcon != nullptr;
- }
- rpl::lifetime &Tray::lifetime() {
- return _lifetime;
- }
- Tray::~Tray() = default;
- } // namespace Platform
|