macOS: Modernize QCocoaSystemTrayIcon

The code had not been touched in a very long time and was overgrown
with weeds from pre-QPA times.

We no longer maintain an indirection through QSystemTrayIconSys,
which was a remnant from Qt 4 times.

The Objective-C helper class used for callbacks has been slimmed
down to just a simple delegate, with the actual work done in the
QCocoaSystemTrayIcon implementation, further reducing indirection.

We no longer use a custom NSView for the status bar item, something
that has been deprecated for a long time. Instead we set properties
on the NSStatusItem's button. This gives us automatic support for
drawing the icon with the right highlight, including in dark mode.

Finally, the code has been updated to modern Objective-C syntax.

Change-Id: I59706081f6b179035b8216a7a6ebc08a47cec127
Fixes: QTBUG-77189
Reviewed-by: Volker Hilsheimer <volker.hilsheimer@qt.io>
This commit is contained in:
Tor Arne Vestbø 2020-03-09 20:41:34 +01:00 committed by Tor Arne Vestbø
parent 54f8be6cc0
commit 395e2d9bc4
2 changed files with 110 additions and 213 deletions

View File

@ -49,14 +49,23 @@
#include "QtCore/qstring.h"
#include "QtGui/qpa/qplatformsystemtrayicon.h"
QT_BEGIN_NAMESPACE
#include "qcocoamenu.h"
class QSystemTrayIconSys;
QT_FORWARD_DECLARE_CLASS(QCocoaSystemTrayIcon);
@interface QT_MANGLE_NAMESPACE(QStatusItemDelegate) : NSObject <NSUserNotificationCenterDelegate>
- (instancetype)initWithSysTray:(QCocoaSystemTrayIcon *)platformSystemTray;
@property (nonatomic, assign) QCocoaSystemTrayIcon *platformSystemTray;
@end
QT_NAMESPACE_ALIAS_OBJC_CLASS(QStatusItemDelegate);
QT_BEGIN_NAMESPACE
class Q_GUI_EXPORT QCocoaSystemTrayIcon : public QPlatformSystemTrayIcon
{
public:
QCocoaSystemTrayIcon() : m_sys(nullptr) {}
QCocoaSystemTrayIcon() {}
void init() override;
void cleanup() override;
@ -70,8 +79,12 @@ public:
bool isSystemTrayAvailable() const override;
bool supportsMessages() const override;
void statusItemClicked();
private:
QSystemTrayIconSys *m_sys;
NSStatusItem *m_statusItem = nullptr;
QStatusItemDelegate *m_delegate = nullptr;
QCocoaMenu *m_menu = nullptr;
};
QT_END_NAMESPACE

View File

@ -92,67 +92,46 @@
#import <AppKit/AppKit.h>
QT_USE_NAMESPACE
@interface QT_MANGLE_NAMESPACE(QNSStatusItem) : NSObject <NSUserNotificationCenterDelegate>
@property (nonatomic, assign) QCocoaMenu *menu;
@property (nonatomic, assign) QIcon icon;
@property (nonatomic, readonly) NSStatusItem *item;
@property (nonatomic, readonly) QRectF geometry;
- (instancetype)initWithSysTray:(QCocoaSystemTrayIcon *)systray;
- (void)triggerSelector:(id)sender button:(Qt::MouseButton)mouseButton;
- (void)doubleClickSelector:(id)sender;
@end
QT_NAMESPACE_ALIAS_OBJC_CLASS(QNSStatusItem);
@interface QT_MANGLE_NAMESPACE(QNSImageView) : NSImageView
@property (nonatomic, assign) BOOL down;
@property (nonatomic, assign) QNSStatusItem *parent;
@end
QT_NAMESPACE_ALIAS_OBJC_CLASS(QNSImageView);
QT_BEGIN_NAMESPACE
class QSystemTrayIconSys
{
public:
QSystemTrayIconSys(QCocoaSystemTrayIcon *sys) {
item = [[QNSStatusItem alloc] initWithSysTray:sys];
NSUserNotificationCenter.defaultUserNotificationCenter.delegate = item;
}
~QSystemTrayIconSys() {
[[[item item] view] setHidden: YES];
NSUserNotificationCenter *center = NSUserNotificationCenter.defaultUserNotificationCenter;
if (center.delegate == item)
center.delegate = nil;
[item release];
}
QNSStatusItem *item;
};
void QCocoaSystemTrayIcon::init()
{
if (!m_sys)
m_sys = new QSystemTrayIconSys(this);
}
m_statusItem = [[NSStatusBar.systemStatusBar statusItemWithLength:NSSquareStatusItemLength] retain];
QRect QCocoaSystemTrayIcon::geometry() const
{
if (!m_sys)
return QRect();
m_delegate = [[QStatusItemDelegate alloc] initWithSysTray:this];
const QRectF geom = [m_sys->item geometry];
if (!geom.isNull())
return geom.toRect();
else
return QRect();
m_statusItem.button.target = m_delegate;
m_statusItem.button.action = @selector(statusItemClicked);
[m_statusItem.button sendActionOn:NSEventMaskLeftMouseUp | NSEventMaskRightMouseUp | NSEventMaskOtherMouseUp];
}
void QCocoaSystemTrayIcon::cleanup()
{
delete m_sys;
m_sys = nullptr;
NSUserNotificationCenter *center = NSUserNotificationCenter.defaultUserNotificationCenter;
if (center.delegate == m_delegate)
center.delegate = nil;
[NSStatusBar.systemStatusBar removeStatusItem:m_statusItem];
[m_statusItem release];
m_statusItem = nil;
[m_delegate release];
m_delegate = nil;
m_menu = nullptr;
}
QRect QCocoaSystemTrayIcon::geometry() const
{
if (!m_statusItem)
return QRect();
if (NSWindow *window = m_statusItem.button.window) {
if (QCocoaScreen *screen = QCocoaScreen::get(window.screen))
return screen->mapFromNative(window.frame).toRect();
}
return QRect();
}
static bool heightCompareFunction (QSize a, QSize b) { return (a.height() < b.height()); }
@ -165,17 +144,15 @@ static QList<QSize> sortByHeight(const QList<QSize> &sizes)
void QCocoaSystemTrayIcon::updateIcon(const QIcon &icon)
{
if (!m_sys)
if (!m_statusItem)
return;
m_sys->item.icon = icon;
// The reccomended maximum title bar icon height is 18 points
// 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 = 4;
const int menuHeight = [[NSStatusBar systemStatusBar] thickness];
const int menuHeight = NSStatusBar.systemStatusBar.thickness;
const int maxImageHeight = menuHeight - padding;
// Select pixmap based on the device pixel height. Ideally we would use
@ -230,27 +207,26 @@ void QCocoaSystemTrayIcon::updateIcon(const QIcon &icon)
auto *nsimage = [NSImage imageFromQImage:fullHeightPixmap.toImage()];
[nsimage setTemplate:icon.isMask()];
[(NSImageView*)[[m_sys->item item] view] setImage: nsimage];
m_statusItem.button.image = nsimage;
m_statusItem.button.imageScaling = NSImageScaleProportionallyDown;
}
void QCocoaSystemTrayIcon::updateMenu(QPlatformMenu *menu)
{
if (!m_sys)
return;
m_sys->item.menu = static_cast<QCocoaMenu *>(menu);
if (menu && [m_sys->item.menu->nsMenu() numberOfItems] > 0) {
[[m_sys->item item] setHighlightMode:YES];
} else {
[[m_sys->item item] setHighlightMode:NO];
}
// We don't set the menu property of the NSStatusItem here,
// as that would prevent us from receiving the action for the
// click, and we wouldn't be able to emit the activated signal.
// Instead we show the menu manually when the status item is
// clicked.
m_menu = static_cast<QCocoaMenu *>(menu);
}
void QCocoaSystemTrayIcon::updateToolTip(const QString &toolTip)
{
if (!m_sys)
if (!m_statusItem)
return;
[[[m_sys->item item] view] setToolTip:toolTip.toNSString()];
m_statusItem.button.toolTip = toolTip.toNSString();
}
bool QCocoaSystemTrayIcon::isSystemTrayAvailable() const
@ -266,175 +242,83 @@ bool QCocoaSystemTrayIcon::supportsMessages() const
void QCocoaSystemTrayIcon::showMessage(const QString &title, const QString &message,
const QIcon& icon, MessageIcon, int msecs)
{
if (!m_sys)
if (!m_statusItem)
return;
NSUserNotification *notification = [[NSUserNotification alloc] init];
notification.title = [NSString stringWithUTF8String:title.toUtf8().data()];
notification.informativeText = [NSString stringWithUTF8String:message.toUtf8().data()];
auto *notification = [[NSUserNotification alloc] init];
notification.title = title.toNSString();
notification.informativeText = message.toNSString();
notification.contentImage = [NSImage imageFromQIcon:icon];
NSUserNotificationCenter *center = NSUserNotificationCenter.defaultUserNotificationCenter;
center.delegate = m_sys->item;
[center deliverNotification:notification];
center.delegate = m_delegate;
[center deliverNotification:[notification autorelease]];
if (msecs) {
NSTimeInterval timeout = msecs / 1000.0;
[center performSelector:@selector(removeDeliveredNotification:) withObject:notification afterDelay:timeout];
}
[notification release];
}
void QCocoaSystemTrayIcon::statusItemClicked()
{
auto *mouseEvent = NSApp.currentEvent;
auto activationReason = QPlatformSystemTrayIcon::Unknown;
if (mouseEvent.clickCount == 2) {
activationReason = QPlatformSystemTrayIcon::DoubleClick;
} else {
auto mouseButton = cocoaButton2QtButton(mouseEvent);
if (mouseButton == Qt::MidButton)
activationReason = QPlatformSystemTrayIcon::MiddleClick;
else if (mouseButton == Qt::RightButton)
activationReason = QPlatformSystemTrayIcon::Context;
else
activationReason = QPlatformSystemTrayIcon::Trigger;
}
emit activated(activationReason);
if (NSMenu *menu = m_menu ? m_menu->nsMenu() : nil)
[m_statusItem popUpStatusItemMenu:menu];
}
QT_END_NAMESPACE
@implementation NSStatusItem (Qt)
@end
@implementation QStatusItemDelegate
- (instancetype)initWithSysTray:(QCocoaSystemTrayIcon *)platformSystemTray
{
if ((self = [super init]))
self.platformSystemTray = platformSystemTray;
@implementation QNSImageView
- (instancetype)initWithParent:(QNSStatusItem *)myParent {
self = [super init];
self.parent = myParent;
self.down = NO;
return self;
}
- (void)menuTrackingDone:(NSNotification *)__unused notification
- (void)dealloc
{
self.down = NO;
[self setNeedsDisplay:YES];
}
- (void)mousePressed:(NSEvent *)mouseEvent
{
self.down = YES;
int clickCount = [mouseEvent clickCount];
[self setNeedsDisplay:YES];
if (clickCount == 2) {
[self menuTrackingDone:nil];
[self.parent doubleClickSelector:self];
} else {
[self.parent triggerSelector:self button:cocoaButton2QtButton(mouseEvent)];
}
}
- (void)mouseDown:(NSEvent *)mouseEvent
{
[self mousePressed:mouseEvent];
}
- (void)mouseUp:(NSEvent *)mouseEvent
{
Q_UNUSED(mouseEvent);
[self menuTrackingDone:nil];
}
- (void)rightMouseDown:(NSEvent *)mouseEvent
{
[self mousePressed:mouseEvent];
}
- (void)rightMouseUp:(NSEvent *)mouseEvent
{
Q_UNUSED(mouseEvent);
[self menuTrackingDone:nil];
}
- (void)otherMouseDown:(NSEvent *)mouseEvent
{
[self mousePressed:mouseEvent];
}
- (void)otherMouseUp:(NSEvent *)mouseEvent
{
Q_UNUSED(mouseEvent);
[self menuTrackingDone:nil];
}
- (void)drawRect:(NSRect)rect {
[[self.parent item] drawStatusBarBackgroundInRect:rect withHighlight:self.down];
[super drawRect:rect];
}
@end
@implementation QNSStatusItem {
QCocoaSystemTrayIcon *systray;
NSStatusItem *item;
QNSImageView *imageCell;
}
@synthesize menu = menu;
@synthesize icon = icon;
- (instancetype)initWithSysTray:(QCocoaSystemTrayIcon *)sys
{
self = [super init];
if (self) {
item = [[[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength] retain];
menu = nullptr;
systray = sys;
imageCell = [[QNSImageView alloc] initWithParent:self];
[item setView: imageCell];
}
return self;
}
- (void)dealloc {
[[NSStatusBar systemStatusBar] removeStatusItem:item];
[[NSNotificationCenter defaultCenter] removeObserver:imageCell];
imageCell.parent = nil;
[imageCell release];
[item release];
self.platformSystemTray = nullptr;
[super dealloc];
}
- (NSStatusItem *)item {
return item;
- (void)statusItemClicked
{
self.platformSystemTray->statusItemClicked();
}
- (QRectF)geometry {
if (NSWindow *window = item.view.window) {
if (QCocoaScreen *screen = QCocoaScreen::get(window.screen))
return screen->mapFromNative(window.frame);
}
return QRectF();
}
- (void)triggerSelector:(id)sender button:(Qt::MouseButton)mouseButton {
Q_UNUSED(sender);
if (!systray)
return;
if (mouseButton == Qt::MidButton)
emit systray->activated(QPlatformSystemTrayIcon::MiddleClick);
else
emit systray->activated(QPlatformSystemTrayIcon::Trigger);
if (menu) {
NSMenu *m = menu->nsMenu();
[[NSNotificationCenter defaultCenter] addObserver:imageCell
selector:@selector(menuTrackingDone:)
name:NSMenuDidEndTrackingNotification
object:m];
[item popUpStatusItemMenu: m];
}
}
- (void)doubleClickSelector:(id)sender {
Q_UNUSED(sender);
if (!systray)
return;
emit systray->activated(QPlatformSystemTrayIcon::DoubleClick);
}
- (BOOL)userNotificationCenter:(NSUserNotificationCenter *)center shouldPresentNotification:(NSUserNotification *)notification {
- (BOOL)userNotificationCenter:(NSUserNotificationCenter *)center shouldPresentNotification:(NSUserNotification *)notification
{
Q_UNUSED(center);
Q_UNUSED(notification);
return YES;
}
- (void)userNotificationCenter:(NSUserNotificationCenter *)center didActivateNotification:(NSUserNotification *)notification {
- (void)userNotificationCenter:(NSUserNotificationCenter *)center didActivateNotification:(NSUserNotification *)notification
{
[center removeDeliveredNotification:notification];
emit systray->messageClicked();
emit self.platformSystemTray->messageClicked();
}
@end