Files
amnezia-client/client/platforms/macos/macosstatusicon.mm
T

289 lines
8.3 KiB
Plaintext
Raw Normal View History

2023-07-15 14:19:48 -07:00
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "macosstatusicon.h"
#include "leakdetector.h"
#include "logger.h"
#import <Cocoa/Cocoa.h>
#import <UserNotifications/UserNotifications.h>
#import <QResource>
2026-05-29 17:01:08 +03:00
#include <QAction>
@interface MacOSStatusIconMenuTarget : NSObject {
@public
QAction* action;
}
- (void)triggerAction:(id)sender;
@end
@implementation MacOSStatusIconMenuTarget
- (void)triggerAction:(id)sender {
Q_UNUSED(sender);
if (action) {
action->trigger();
}
}
@end
2023-07-15 14:19:48 -07:00
/**
* Creates a NSStatusItem with that can hold an icon. Additionally a NSView is
* set as a subview to the button item of the status item. The view serves as
* an indicator that can be displayed in color eventhough the icon is set as a
* template. In that way we give the system control over its effective
* appearance.
*/
@interface MacOSStatusIconDelegate : NSObject
@property(assign) NSStatusItem* statusItem;
@property(assign) NSView* statusIndicator;
2026-05-29 22:52:34 +03:00
@property(retain) NSMenu* nativeMenu;
@property(retain) NSMutableArray* menuActionTargets;
2023-07-15 14:19:48 -07:00
- (void)setIcon:(NSData*)imageData;
- (void)setIndicator;
- (void)setIndicatorColor:(NSColor*)color;
- (void)setToolTip:(NSString*)tooltip;
2026-05-29 17:01:08 +03:00
- (void)rebuildMenuFromQMenu:(QMenu*)menu;
2023-07-15 14:19:48 -07:00
@end
@implementation MacOSStatusIconDelegate
2026-05-29 22:45:03 +03:00
/**
* Initializes and sets the status item and indicator objects.
*
* @return An instance of MacOSStatusIconDelegate.
*/
2023-07-15 14:19:48 -07:00
- (id)init {
self = [super init];
2026-05-29 17:01:08 +03:00
self.menuActionTargets = [[NSMutableArray alloc] init];
2023-07-15 14:19:48 -07:00
2026-05-29 22:45:03 +03:00
// Create status item
2023-07-15 14:19:48 -07:00
self.statusItem =
[[[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength] retain];
self.statusItem.visible = true;
2026-05-29 22:45:03 +03:00
// Add the indicator as a subview
2023-07-15 14:19:48 -07:00
[self setIndicator];
return self;
}
2026-05-29 17:01:08 +03:00
- (void)dealloc {
self.nativeMenu = nil;
self.menuActionTargets = nil;
[super dealloc];
}
2026-05-29 22:45:03 +03:00
/**
* Sets the image for the status icon.
*
* @param iconPath The data for the icon image.
*/
2023-07-15 14:19:48 -07:00
- (void)setIcon:(NSData*)imageData {
NSImage* image = [[NSImage alloc] initWithData:imageData];
[image setTemplate:true];
[self.statusItem.button setImage:image];
[image release];
}
2026-05-29 22:45:03 +03:00
/**
* Adds status indicator as a subview to the status item button.
*/
2023-07-15 14:19:48 -07:00
- (void)setIndicator {
float viewHeight = NSHeight([self.statusItem.button bounds]);
float dotSize = viewHeight * 0.35;
float dotOrigin = (viewHeight - dotSize) * 0.8;
NSView* dot = [[NSView alloc] initWithFrame:NSMakeRect(dotOrigin, dotOrigin, dotSize, dotSize)];
self.statusIndicator = dot;
self.statusIndicator.wantsLayer = true;
self.statusIndicator.layer.cornerRadius = dotSize * 0.5;
[self.statusItem.button addSubview:self.statusIndicator];
[dot release];
}
2026-05-29 22:45:03 +03:00
/**
* Sets the color if the indicator.
*
* @param color The indicator background color.
*/
2023-07-15 14:19:48 -07:00
- (void)setIndicatorColor:(NSColor*)color {
if (self.statusIndicator) {
self.statusIndicator.layer.backgroundColor = color.CGColor;
}
}
2026-05-29 22:45:03 +03:00
/**
* Sets the status bar menu to the status item.
*
* @param statusBarMenu The menu object that is passed from QT.
*/
- (void)setMenu:(NSMenu*)statusBarMenu {
[self.statusItem setMenu:statusBarMenu];
}
/**
* Sets the tooltip string for the status item.
*
* @param tooltip The tooltip string.
*/
2023-07-15 14:19:48 -07:00
- (void)setToolTip:(NSString*)tooltip {
[self.statusItem.button setToolTip:tooltip];
}
2026-05-29 17:01:08 +03:00
- (void)rebuildMenuFromQMenu:(QMenu*)menu {
[self.menuActionTargets removeAllObjects];
if (self.nativeMenu) {
[self.statusItem setMenu:nil];
self.nativeMenu = nil;
}
if (!menu) {
return;
}
NSMenu* nsMenu = [[NSMenu alloc] initWithTitle:@""];
for (QAction* action : menu->actions()) {
if (action->isSeparator()) {
[nsMenu addItem:[NSMenuItem separatorItem]];
continue;
}
MacOSStatusIconMenuTarget* target = [[MacOSStatusIconMenuTarget alloc] init];
target->action = action;
[self.menuActionTargets addObject:target];
[target release];
NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:action->text().toNSString()
action:@selector(triggerAction:)
keyEquivalent:@""];
[item setTarget:target];
[item setEnabled:action->isEnabled()];
[item setHidden:!action->isVisible()];
[nsMenu addItem:item];
[item release];
}
self.nativeMenu = nsMenu;
[self.statusItem setMenu:nsMenu];
}
2023-07-15 14:19:48 -07:00
@end
namespace {
Logger logger("MacOSStatusIcon");
MacOSStatusIconDelegate* m_statusBarIcon = nullptr;
2026-05-29 17:01:08 +03:00
} // namespace
2023-07-15 14:19:48 -07:00
MacOSStatusIcon::MacOSStatusIcon(QObject* parent) : QObject(parent) {
MZ_COUNT_CTOR(MacOSStatusIcon);
logger.debug() << "Register delegate";
Q_ASSERT(!m_statusBarIcon);
m_statusBarIcon = [[MacOSStatusIconDelegate alloc] init];
}
MacOSStatusIcon::~MacOSStatusIcon() {
MZ_COUNT_DTOR(MacOSStatusIcon);
logger.debug() << "Remove delegate";
Q_ASSERT(m_statusBarIcon);
[static_cast<MacOSStatusIconDelegate*>(m_statusBarIcon) dealloc];
m_statusBarIcon = nullptr;
}
void MacOSStatusIcon::setIcon(const QString& iconPath) {
logger.debug() << "Set icon" << iconPath;
QResource imageResource = QResource(iconPath);
Q_ASSERT(imageResource.isValid());
[m_statusBarIcon setIcon:imageResource.uncompressedData().toNSData()];
}
2026-05-29 22:52:34 +03:00
void MacOSStatusIcon::setIconFromData(const QByteArray& imageData) {
logger.debug() << "Set icon from rendered data";
if (imageData.isEmpty()) {
return;
}
NSData* data = [NSData dataWithBytes:imageData.constData() length:imageData.size()];
[m_statusBarIcon setIcon:data];
}
2023-07-15 14:19:48 -07:00
void MacOSStatusIcon::setIndicatorColor(const QColor& indicatorColor) {
logger.debug() << "Set indicator color";
if (!indicatorColor.isValid()) {
[m_statusBarIcon setIndicatorColor:[NSColor clearColor]];
return;
}
NSColor* color = [NSColor colorWithCalibratedRed:indicatorColor.red() / 255.0f
green:indicatorColor.green() / 255.0f
blue:indicatorColor.blue() / 255.0f
alpha:indicatorColor.alpha() / 255.0f];
[m_statusBarIcon setIndicatorColor:color];
}
2026-05-29 17:01:08 +03:00
void MacOSStatusIcon::setMenu(QMenu* menu) {
m_qtMenu = menu;
rebuildNativeMenu();
if (menu) {
connect(menu, &QMenu::aboutToShow, this, [this]() { rebuildNativeMenu(); });
}
}
void MacOSStatusIcon::rebuildNativeMenu() {
[m_statusBarIcon rebuildMenuFromQMenu:m_qtMenu.data()];
2023-07-15 14:19:48 -07:00
}
void MacOSStatusIcon::setToolTip(const QString& tooltip) {
logger.debug() << "Set tooltip";
[m_statusBarIcon setToolTip:tooltip.toNSString()];
}
void MacOSStatusIcon::showMessage(const QString& title, const QString& message) {
logger.debug() << "Show message";
UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter];
2026-05-29 22:45:03 +03:00
// This is a no-op is authorization has been granted.
2023-07-15 14:19:48 -07:00
[center requestAuthorizationWithOptions:(UNAuthorizationOptionSound | UNAuthorizationOptionAlert |
UNAuthorizationOptionBadge)
2026-05-29 17:01:08 +03:00
completionHandler:^(__unused BOOL granted, NSError* _Nullable error) {
2023-07-15 14:19:48 -07:00
if (error) {
2026-05-29 22:45:03 +03:00
// Note: This error may happen if the application is not signed.
2023-07-15 14:19:48 -07:00
NSLog(@"Error asking for permission to send notifications %@", error);
2026-05-29 22:45:03 +03:00
return;
2023-07-15 14:19:48 -07:00
}
}];
UNMutableNotificationContent* content = [[UNMutableNotificationContent alloc] init];
content.title = [title.toNSString() autorelease];
content.body = [message.toNSString() autorelease];
content.sound = [UNNotificationSound defaultSound];
UNTimeIntervalNotificationTrigger* trigger =
[UNTimeIntervalNotificationTrigger triggerWithTimeInterval:1 repeats:NO];
UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:@"amneziavpn"
content:content
trigger:trigger];
[center addNotificationRequest:request
withCompletionHandler:^(NSError* _Nullable error) {
if (error) {
logger.error() << "Local Notification failed" << error;
}
}];
}