diff options
Diffstat (limited to 'src/plugins/platforms/cocoa/qcocoasystemtrayicon.mm')
-rw-r--r-- | src/plugins/platforms/cocoa/qcocoasystemtrayicon.mm | 354 |
1 files changed, 112 insertions, 242 deletions
diff --git a/src/plugins/platforms/cocoa/qcocoasystemtrayicon.mm b/src/plugins/platforms/cocoa/qcocoasystemtrayicon.mm index 559188aa5f..cec8301cf6 100644 --- a/src/plugins/platforms/cocoa/qcocoasystemtrayicon.mm +++ b/src/plugins/platforms/cocoa/qcocoasystemtrayicon.mm @@ -1,42 +1,6 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Copyright (C) 2012 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Christoph Schleifenbaum <christoph.schleifenbaum@kdab.com> -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the plugins of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2016 The Qt Company Ltd. +// Copyright (C) 2012 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Christoph Schleifenbaum <christoph.schleifenbaum@kdab.com> +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only /**************************************************************************** ** @@ -72,6 +36,8 @@ ** ****************************************************************************/ +#include <AppKit/AppKit.h> + #include "qcocoasystemtrayicon.h" #ifndef QT_NO_SYSTEMTRAYICON @@ -83,76 +49,59 @@ #include <QtCore/private/qcore_mac_p.h> #include "qcocoamenu.h" +#include "qcocoansmenu.h" -#include "qt_mac_p.h" #include "qcocoahelpers.h" #include "qcocoaintegration.h" #include "qcocoascreen.h" #include <QtGui/private/qcoregraphics_p.h> -#import <AppKit/AppKit.h> - -QT_USE_NAMESPACE +#warning NSUserNotification was deprecated in macOS 11. \ +We should be using UserNotifications.framework instead. \ +See QTBUG-110998 for more information. +#define NSUserNotificationCenter QT_IGNORE_DEPRECATIONS(NSUserNotificationCenter) +#define NSUserNotification QT_IGNORE_DEPRECATIONS(NSUserNotification) -@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_BEGIN_NAMESPACE -QT_NAMESPACE_ALIAS_OBJC_CLASS(QNSStatusItem); +void QCocoaSystemTrayIcon::init() +{ + m_statusItem = [[NSStatusBar.systemStatusBar statusItemWithLength:NSSquareStatusItemLength] retain]; -@interface QT_MANGLE_NAMESPACE(QNSImageView) : NSImageView -@property (nonatomic, assign) BOOL down; -@property (nonatomic, assign) QNSStatusItem *parent; -@end + m_delegate = [[QStatusItemDelegate alloc] initWithSysTray:this]; -QT_NAMESPACE_ALIAS_OBJC_CLASS(QNSImageView); + // In case the status item does not have a menu assigned to it + // we fall back to the item's button to detect activation. + m_statusItem.button.target = m_delegate; + m_statusItem.button.action = @selector(statusItemClicked); + [m_statusItem.button sendActionOn:NSEventMaskLeftMouseDown | NSEventMaskRightMouseDown | NSEventMaskOtherMouseDown]; +} -QT_BEGIN_NAMESPACE -class QSystemTrayIconSys +void QCocoaSystemTrayIcon::cleanup() { -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; -}; + NSUserNotificationCenter *center = NSUserNotificationCenter.defaultUserNotificationCenter; + if (center.delegate == m_delegate) + center.delegate = nil; -void QCocoaSystemTrayIcon::init() -{ - if (!m_sys) - m_sys = new QSystemTrayIconSys(this); + [NSStatusBar.systemStatusBar removeStatusItem:m_statusItem]; + [m_statusItem release]; + m_statusItem = nil; + + [m_delegate release]; + m_delegate = nil; } QRect QCocoaSystemTrayIcon::geometry() const { - if (!m_sys) + if (!m_statusItem) return QRect(); - const QRectF geom = [m_sys->item geometry]; - if (!geom.isNull()) - return geom.toRect(); - else - return QRect(); -} + if (NSWindow *window = m_statusItem.button.window) { + if (QCocoaScreen *screen = QCocoaScreen::get(window.screen)) + return screen->mapFromNative(window.frame).toRect(); + } -void QCocoaSystemTrayIcon::cleanup() -{ - delete m_sys; - m_sys = nullptr; + return QRect(); } static bool heightCompareFunction (QSize a, QSize b) { return (a.height() < b.height()); } @@ -165,17 +114,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 @@ -185,7 +132,7 @@ void QCocoaSystemTrayIcon::updateIcon(const QIcon &icon) qreal devicePixelRatio = qApp->devicePixelRatio(); const int maxPixmapHeight = maxImageHeight * devicePixelRatio; QSize selectedSize; - Q_FOREACH (const QSize& size, sortByHeight(icon.availableSizes())) { + for (const QSize& size : sortByHeight(icon.availableSizes())) { // Select a pixmap based on the height. We want the largest pixmap // with a height smaller or equal to maxPixmapHeight. The pixmap // may rectangular; assume it has a reasonable size. If there is @@ -227,31 +174,49 @@ void QCocoaSystemTrayIcon::updateIcon(const QIcon &icon) r.moveCenter(fullHeightPixmap.rect().center()); p.drawPixmap(r, pixmap); } + fullHeightPixmap.setDevicePixelRatio(devicePixelRatio); - NSImage *nsimage = static_cast<NSImage *>(qt_mac_create_nsimage(fullHeightPixmap)); + auto *nsimage = [NSImage imageFromQImage:fullHeightPixmap.toImage()]; [nsimage setTemplate:icon.isMask()]; - [(NSImageView*)[[m_sys->item item] view] setImage: nsimage]; - [nsimage release]; + m_statusItem.button.image = nsimage; + m_statusItem.button.imageScaling = NSImageScaleProportionallyDown; } void QCocoaSystemTrayIcon::updateMenu(QPlatformMenu *menu) { - if (!m_sys) + auto *nsMenu = menu ? static_cast<QCocoaMenu *>(menu)->nsMenu() : nil; + if (m_statusItem.menu == nsMenu) 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]; + if (m_statusItem.menu) { + [NSNotificationCenter.defaultCenter removeObserver:m_delegate + name:NSMenuDidBeginTrackingNotification + object:m_statusItem.menu + ]; + } + + m_statusItem.menu = nsMenu; + + if (m_statusItem.menu) { + // When a menu is assigned, NSStatusBarButtonCell will intercept the mouse + // down to pop up the menu, and we never see the NSStatusBarButton action. + // To ensure we emit the 'activated' signal in both cases we detect when + // menu starts tracking, which happens before the menu delegate is sent + // the menuWillOpen callback we use to emit aboutToShow for the menu. + [NSNotificationCenter.defaultCenter addObserver:m_delegate + selector:@selector(statusItemMenuBeganTracking:) + name:NSMenuDidBeginTrackingNotification + object:m_statusItem.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 @@ -267,180 +232,85 @@ 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()]; - - if (!icon.isNull()) { - auto *nsimage = qt_mac_create_nsimage(icon); - [nsimage setTemplate:icon.isMask()]; - notification.contentImage = [nsimage autorelease]; - } + 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]; -} -QT_END_NAMESPACE - -@implementation NSStatusItem (Qt) -@end - -@implementation QNSImageView -- (instancetype)initWithParent:(QNSStatusItem *)myParent { - self = [super init]; - self.parent = myParent; - self.down = NO; - return self; } -- (void)menuTrackingDone:(NSNotification *)__unused notification +void QCocoaSystemTrayIcon::emitActivated() { - self.down = NO; + auto *mouseEvent = NSApp.currentEvent; - [self setNeedsDisplay:YES]; -} - -- (void)mousePressed:(NSEvent *)mouseEvent -{ - self.down = YES; - int clickCount = [mouseEvent clickCount]; - [self setNeedsDisplay:YES]; + auto activationReason = QPlatformSystemTrayIcon::Unknown; - if (clickCount == 2) { - [self menuTrackingDone:nil]; - [self.parent doubleClickSelector:self]; + if (mouseEvent.clickCount == 2) { + activationReason = QPlatformSystemTrayIcon::DoubleClick; } else { - [self.parent triggerSelector:self button:cocoaButton2QtButton(mouseEvent)]; + auto mouseButton = cocoaButton2QtButton(mouseEvent); + if (mouseButton == Qt::MiddleButton) + activationReason = QPlatformSystemTrayIcon::MiddleClick; + else if (mouseButton == Qt::RightButton) + activationReason = QPlatformSystemTrayIcon::Context; + else + activationReason = QPlatformSystemTrayIcon::Trigger; } -} - -- (void)mouseDown:(NSEvent *)mouseEvent -{ - [self mousePressed:mouseEvent]; -} - -- (void)mouseUp:(NSEvent *)mouseEvent -{ - Q_UNUSED(mouseEvent); - [self menuTrackingDone:nil]; -} -- (void)rightMouseDown:(NSEvent *)mouseEvent -{ - [self mousePressed:mouseEvent]; + emit activated(activationReason); } -- (void)rightMouseUp:(NSEvent *)mouseEvent -{ - Q_UNUSED(mouseEvent); - [self menuTrackingDone:nil]; -} +QT_END_NAMESPACE -- (void)otherMouseDown:(NSEvent *)mouseEvent -{ - [self mousePressed:mouseEvent]; -} +@implementation QStatusItemDelegate -- (void)otherMouseUp:(NSEvent *)mouseEvent +- (instancetype)initWithSysTray:(QCocoaSystemTrayIcon *)platformSystemTray { - Q_UNUSED(mouseEvent); - [self menuTrackingDone:nil]; -} + if ((self = [super init])) + self.platformSystemTray = platformSystemTray; -- (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]; +- (void)dealloc +{ + self.platformSystemTray = nullptr; [super dealloc]; } -- (NSStatusItem *)item { - return item; -} - -- (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)statusItemClicked +{ + self.platformSystemTray->emitActivated(); } -- (void)doubleClickSelector:(id)sender { - Q_UNUSED(sender); - if (!systray) - return; - emit systray->activated(QPlatformSystemTrayIcon::DoubleClick); +- (void)statusItemMenuBeganTracking:(NSNotification*)notification +{ + self.platformSystemTray->emitActivated(); } -- (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 |