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"
|
#include "base/unique_qptr.h"
|
||||||
|
|
||||||
class QMenu;
|
class QMenu;
|
||||||
class QSystemTrayIcon;
|
|
||||||
|
|
||||||
namespace Platform {
|
namespace Platform {
|
||||||
|
|
||||||
|
class NativeIcon;
|
||||||
|
|
||||||
class Tray final {
|
class Tray final {
|
||||||
public:
|
public:
|
||||||
Tray();
|
Tray();
|
||||||
|
~Tray();
|
||||||
|
|
||||||
[[nodiscard]] rpl::producer<> aboutToShowRequests() const;
|
[[nodiscard]] rpl::producer<> aboutToShowRequests() const;
|
||||||
[[nodiscard]] rpl::producer<> showFromTrayRequests() const;
|
[[nodiscard]] rpl::producer<> showFromTrayRequests() const;
|
||||||
|
@ -41,9 +43,11 @@ public:
|
||||||
[[nodiscard]] rpl::lifetime &lifetime();
|
[[nodiscard]] rpl::lifetime &lifetime();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
base::unique_qptr<QSystemTrayIcon> _icon;
|
std::unique_ptr<NativeIcon> _nativeIcon;
|
||||||
base::unique_qptr<QMenu> _menu;
|
base::unique_qptr<QMenu> _menu;
|
||||||
|
|
||||||
|
rpl::event_stream<> _showFromTrayRequests;
|
||||||
|
|
||||||
rpl::lifetime _actionsLifetime;
|
rpl::lifetime _actionsLifetime;
|
||||||
rpl::lifetime _lifetime;
|
rpl::lifetime _lifetime;
|
||||||
|
|
||||||
|
|
|
@ -7,15 +7,75 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
*/
|
*/
|
||||||
#include "platform/mac/tray_mac.h"
|
#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/application.h"
|
||||||
|
#include "core/sandbox.h"
|
||||||
#include "window/window_controller.h"
|
#include "window/window_controller.h"
|
||||||
#include "window/window_session_controller.h"
|
#include "window/window_session_controller.h"
|
||||||
#include "ui/ui_utility.h"
|
|
||||||
#include "styles/style_window.h"
|
#include "styles/style_window.h"
|
||||||
|
|
||||||
#include <QtWidgets/QMenu>
|
#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 {
|
namespace Platform {
|
||||||
|
|
||||||
|
@ -92,42 +152,64 @@ void PlaceCounter(
|
||||||
img.setDevicePixelRatio(savedRatio);
|
img.setDevicePixelRatio(savedRatio);
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] QIcon GenerateIconForTray(int counter, bool muted) {
|
void UpdateIcon(const NSStatusItem *status) {
|
||||||
auto result = QIcon();
|
if (!status) {
|
||||||
auto lightMode = TrayIconBack(false);
|
return;
|
||||||
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);
|
|
||||||
|
|
||||||
|
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 &fg = st::trayCounterFg;
|
||||||
const auto &fgInvert = st::trayCounterFgMacInvert;
|
const auto &fgInvert = st::trayCounterFgMacInvert;
|
||||||
const auto &bgInvert = st::trayCounterBgMacInvert;
|
const auto &bgInvert = st::trayCounterBgMacInvert;
|
||||||
|
|
||||||
PlaceCounter(lightMode, size, counter, bg, fg);
|
const auto &resultFg = !darkMode ? fg : muted ? fgInvert : fg;
|
||||||
PlaceCounter(darkMode, size, counter, bg, muted ? fgInvert : fg);
|
PlaceCounter(result, side, counter, bg, resultFg);
|
||||||
PlaceCounter(lightModeActive, size, counter, bgInvert, fgInvert);
|
PlaceCounter(resultActive, side, counter, bgInvert, fgInvert);
|
||||||
PlaceCounter(darkModeActive, size, counter, bgInvert, fgInvert);
|
|
||||||
result.addPixmap(Ui::PixmapFromImage(
|
// Scale large pixmaps to fit the available menu bar area.
|
||||||
std::move(lightMode)),
|
if (result.height() > maxImageHeight) {
|
||||||
QIcon::Normal,
|
result = result.scaledToHeight(
|
||||||
QIcon::Off);
|
maxImageHeight,
|
||||||
result.addPixmap(Ui::PixmapFromImage(
|
Qt::SmoothTransformation);
|
||||||
std::move(darkMode)),
|
}
|
||||||
QIcon::Normal,
|
if (resultActive.height() > maxImageHeight) {
|
||||||
QIcon::On);
|
resultActive = resultActive.scaledToHeight(
|
||||||
result.addPixmap(Ui::PixmapFromImage(
|
maxImageHeight,
|
||||||
std::move(lightModeActive)),
|
Qt::SmoothTransformation);
|
||||||
QIcon::Active,
|
}
|
||||||
QIcon::Off);
|
|
||||||
result.addPixmap(Ui::PixmapFromImage(
|
status.button.image = Q2NSImage(result);
|
||||||
std::move(darkModeActive)),
|
status.button.alternateImage = Q2NSImage(resultActive);
|
||||||
QIcon::Active,
|
status.button.imageScaling = NSImageScaleProportionallyDown;
|
||||||
QIcon::On);
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] QWidget *Parent() {
|
[[nodiscard]] QWidget *Parent() {
|
||||||
|
@ -137,36 +219,137 @@ void PlaceCounter(
|
||||||
|
|
||||||
} // namespace
|
} // 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() {
|
Tray::Tray() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void Tray::createIcon() {
|
void Tray::createIcon() {
|
||||||
if (!_icon) {
|
if (!_nativeIcon) {
|
||||||
_icon = base::make_unique_q<QSystemTrayIcon>(Parent());
|
_nativeIcon = std::make_unique<NativeIcon>();
|
||||||
updateIcon();
|
// On macOS we are activating the window on click
|
||||||
if (Core::App().isActiveForTrayMenu()) {
|
// instead of showing the menu, when the window is not activated.
|
||||||
_icon->setContextMenu(_menu.get());
|
_nativeIcon->clicks(
|
||||||
} else {
|
) | rpl::start_with_next([=] {
|
||||||
_icon->setContextMenu(nullptr);
|
if (Core::App().isActiveForTrayMenu()) {
|
||||||
}
|
_nativeIcon->showMenu(_menu.get());
|
||||||
_icon->setContextMenu(_menu.get()); // Todo.
|
} else {
|
||||||
// attachToTrayIcon(_icon);
|
_nativeIcon->deactivateButton();
|
||||||
} else {
|
_showFromTrayRequests.fire({});
|
||||||
updateIcon();
|
}
|
||||||
|
}, _lifetime);
|
||||||
}
|
}
|
||||||
|
updateIcon();
|
||||||
_icon->show();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Tray::destroyIcon() {
|
void Tray::destroyIcon() {
|
||||||
_icon = nullptr;
|
_nativeIcon = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Tray::updateIcon() {
|
void Tray::updateIcon() {
|
||||||
if (_icon) {
|
if (_nativeIcon) {
|
||||||
const auto counter = Core::App().unreadBadge();
|
_nativeIcon->updateIcon();
|
||||||
const auto muted = Core::App().unreadBadgeMuted();
|
|
||||||
_icon->setIcon(GenerateIconForTray(counter, muted));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -204,13 +387,13 @@ bool Tray::hasTrayMessageSupport() const {
|
||||||
}
|
}
|
||||||
|
|
||||||
rpl::producer<> Tray::aboutToShowRequests() const {
|
rpl::producer<> Tray::aboutToShowRequests() const {
|
||||||
return _menu
|
return _nativeIcon
|
||||||
? base::qt_signal_producer(_menu.get(), &QMenu::aboutToShow)
|
? _nativeIcon->aboutToShowRequests()
|
||||||
: rpl::never<>();
|
: rpl::never<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
rpl::producer<> Tray::showFromTrayRequests() const {
|
rpl::producer<> Tray::showFromTrayRequests() const {
|
||||||
return rpl::never<>();
|
return _showFromTrayRequests.events();
|
||||||
}
|
}
|
||||||
|
|
||||||
rpl::producer<> Tray::hideToTrayRequests() const {
|
rpl::producer<> Tray::hideToTrayRequests() const {
|
||||||
|
@ -225,4 +408,6 @@ rpl::lifetime &Tray::lifetime() {
|
||||||
return _lifetime;
|
return _lifetime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Tray::~Tray() = default;
|
||||||
|
|
||||||
} // namespace Platform
|
} // namespace Platform
|
||||||
|
|
Loading…
Reference in New Issue