mirror of
https://github.com/telegramdesktop/tdesktop
synced 2025-03-02 20:38:12 +00:00
Replaced macOS Qt tray with native implementation.
This commit is contained in:
parent
fbae5bdbcf
commit
9de372d715
@ -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;
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user