electron/shell/browser/ui/tray_icon_cocoa.mm
electron-roller[bot] 80a3ba5c68
chore: bump chromium to 116.0.5829.0 (main) (#38726)
* chore: bump chromium in DEPS to 116.0.5823.0

* chore: update patches

* chore: bump chromium in DEPS to 116.0.5825.0

* chore: update patches

* chore: bump chromium in DEPS to 116.0.5827.0

* chore: update patches

* 4568811: Integrate Search Prefetch with Extensions.

https://chromium-review.googlesource.com/c/chromium/src/+/4568811

* 4567511: [DevTools] Add recordCountHistogram API.

https://chromium-review.googlesource.com/c/chromium/src/+/4567511

* 4507692: Delete base/guid.h

https://chromium-review.googlesource.com/c/chromium/src/+/4507692

* 4589551: Convert some of /base to use ARC

https://chromium-review.googlesource.com/c/chromium/src/+/4589551
Also:
4601769: Convert immersive mode controllers to use ARC
https://chromium-review.googlesource.com/c/chromium/src/+/4601769

* [viz] Convert MaybeSizeInBytes() to take in SharedImageFormat

https://chromium-review.googlesource.com/c/chromium/src/+/4594677

* 4564108: [BRP] Enable check_raw_ptr_fields for Mac

https://chromium-review.googlesource.com/c/chromium/src/+/4564108

* chore: bump chromium in DEPS to 116.0.5828.0

* chore: bump chromium in DEPS to 116.0.5829.0

* chore: update patches

---------

Co-authored-by: electron-roller[bot] <84116207+electron-roller[bot]@users.noreply.github.com>
Co-authored-by: PatchUp <73610968+patchup[bot]@users.noreply.github.com>
Co-authored-by: John Kleinschmidt <jkleinsc@electronjs.org>
2023-06-13 14:45:48 -04:00

398 lines
12 KiB
Text

// Copyright (c) 2014 GitHub, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "shell/browser/ui/tray_icon_cocoa.h"
#include <string>
#include <vector>
#include "base/memory/raw_ptr.h"
#include "base/message_loop/message_pump_mac.h"
#include "base/strings/sys_string_conversions.h"
#include "base/task/current_thread.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "shell/browser/ui/cocoa/NSString+ANSI.h"
#include "shell/browser/ui/cocoa/electron_menu_controller.h"
#include "ui/events/cocoa/cocoa_event_utils.h"
#include "ui/gfx/mac/coordinate_conversion.h"
#include "ui/native_theme/native_theme.h"
@interface StatusItemView : NSView {
raw_ptr<electron::TrayIconCocoa> trayIcon_; // weak
ElectronMenuController* menuController_; // weak
BOOL ignoreDoubleClickEvents_;
base::scoped_nsobject<NSStatusItem> statusItem_;
base::scoped_nsobject<NSTrackingArea> trackingArea_;
}
@end // @interface StatusItemView
@implementation StatusItemView
- (void)dealloc {
trayIcon_ = nil;
menuController_ = nil;
[super dealloc];
}
- (id)initWithIcon:(electron::TrayIconCocoa*)icon {
trayIcon_ = icon;
menuController_ = nil;
ignoreDoubleClickEvents_ = NO;
if ((self = [super initWithFrame:CGRectZero])) {
[self registerForDraggedTypes:@[
NSFilenamesPboardType,
NSStringPboardType,
]];
// Create the status item.
NSStatusItem* item = [[NSStatusBar systemStatusBar]
statusItemWithLength:NSVariableStatusItemLength];
statusItem_.reset([item retain]);
[[statusItem_ button] addSubview:self]; // inject custom view
[self updateDimensions];
}
return self;
}
- (void)updateDimensions {
[self setFrame:[statusItem_ button].frame];
}
- (void)updateTrackingAreas {
// Use NSTrackingArea for listening to mouseEnter, mouseExit, and mouseMove
// events.
[self removeTrackingArea:trackingArea_];
trackingArea_.reset([[NSTrackingArea alloc]
initWithRect:[self bounds]
options:NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved |
NSTrackingActiveAlways
owner:self
userInfo:nil]);
[self addTrackingArea:trackingArea_];
}
- (void)removeItem {
// Turn off tracking events to prevent crash.
if (trackingArea_) {
[self removeTrackingArea:trackingArea_];
trackingArea_.reset();
}
[[NSStatusBar systemStatusBar] removeStatusItem:statusItem_];
[self removeFromSuperview];
statusItem_.reset();
}
- (void)setImage:(NSImage*)image {
[[statusItem_ button] setImage:image];
[self updateDimensions];
}
- (void)setAlternateImage:(NSImage*)image {
[[statusItem_ button] setAlternateImage:image];
// We need to change the button type here because the default button type for
// NSStatusItem, NSStatusBarButton, does not display alternate content when
// clicked. NSButtonTypeMomentaryChange displays its alternate content when
// clicked and returns to its normal content when the user releases it, which
// is the behavior users would expect when clicking a button with an alternate
// image set.
[[statusItem_ button] setButtonType:NSButtonTypeMomentaryChange];
[self updateDimensions];
}
- (void)setIgnoreDoubleClickEvents:(BOOL)ignore {
ignoreDoubleClickEvents_ = ignore;
}
- (BOOL)getIgnoreDoubleClickEvents {
return ignoreDoubleClickEvents_;
}
- (void)setTitle:(NSString*)title font_type:(NSString*)font_type {
NSMutableAttributedString* attributed_title =
[[NSMutableAttributedString alloc] initWithString:title];
if ([title containsANSICodes]) {
attributed_title = [title attributedStringParsingANSICodes];
}
// Change font type, if specified
CGFloat existing_size = [[[statusItem_ button] font] pointSize];
if ([font_type isEqualToString:@"monospaced"]) {
if (@available(macOS 10.15, *)) {
NSDictionary* attributes = @{
NSFontAttributeName :
[NSFont monospacedSystemFontOfSize:existing_size
weight:NSFontWeightRegular]
};
[attributed_title
addAttributes:attributes
range:NSMakeRange(0, [attributed_title length])];
}
} else if ([font_type isEqualToString:@"monospacedDigit"]) {
NSDictionary* attributes = @{
NSFontAttributeName :
[NSFont monospacedDigitSystemFontOfSize:existing_size
weight:NSFontWeightRegular]
};
[attributed_title addAttributes:attributes
range:NSMakeRange(0, [attributed_title length])];
}
// Set title
[[statusItem_ button] setAttributedTitle:attributed_title];
// Fix icon margins.
if (title.length > 0) {
[[statusItem_ button] setImagePosition:NSImageLeft];
} else {
[[statusItem_ button] setImagePosition:NSImageOnly];
}
[self updateDimensions];
}
- (NSString*)title {
return [statusItem_ button].title;
}
- (void)setMenuController:(ElectronMenuController*)menu {
menuController_ = menu;
[statusItem_ setMenu:[menuController_ menu]];
}
- (void)handleClickNotifications:(NSEvent*)event {
// If we are ignoring double click events, we should ignore the `clickCount`
// value and immediately emit a click event.
BOOL shouldBeHandledAsASingleClick =
(event.clickCount == 1) || ignoreDoubleClickEvents_;
if (shouldBeHandledAsASingleClick)
trayIcon_->NotifyClicked(
gfx::ScreenRectFromNSRect(event.window.frame),
gfx::ScreenPointFromNSPoint([event locationInWindow]),
ui::EventFlagsFromModifiers([event modifierFlags]));
// Double click event.
BOOL shouldBeHandledAsADoubleClick =
(event.clickCount == 2) && !ignoreDoubleClickEvents_;
if (shouldBeHandledAsADoubleClick)
trayIcon_->NotifyDoubleClicked(
gfx::ScreenRectFromNSRect(event.window.frame),
ui::EventFlagsFromModifiers([event modifierFlags]));
}
- (void)mouseDown:(NSEvent*)event {
trayIcon_->NotifyMouseDown(
gfx::ScreenPointFromNSPoint([event locationInWindow]),
ui::EventFlagsFromModifiers([event modifierFlags]));
// Pass click to superclass to show menu if one exists and has a non-zero
// number of items. Custom mouseUp handler won't be invoked in this case.
if (menuController_ && [[menuController_ menu] numberOfItems] > 0) {
[self handleClickNotifications:event];
[super mouseDown:event];
} else {
[[statusItem_ button] highlight:YES];
}
}
- (void)mouseUp:(NSEvent*)event {
[[statusItem_ button] highlight:NO];
trayIcon_->NotifyMouseUp(
gfx::ScreenPointFromNSPoint([event locationInWindow]),
ui::EventFlagsFromModifiers([event modifierFlags]));
[self handleClickNotifications:event];
}
- (void)popUpContextMenu:(electron::ElectronMenuModel*)menu_model {
// Make sure events can be pumped while the menu is up.
base::CurrentThread::ScopedAllowApplicationTasksInNativeNestedLoop allow;
// Show a custom menu.
if (menu_model) {
base::scoped_nsobject<ElectronMenuController> menuController(
[[ElectronMenuController alloc] initWithModel:menu_model
useDefaultAccelerator:NO]);
// Hacky way to mimic design of ordinary tray menu.
[statusItem_ setMenu:[menuController menu]];
// -performClick: is a blocking call, which will run the task loop inside
// itself. This can potentially include running JS, which can result in
// this object being released. We take a temporary reference here to make
// sure we stay alive long enough to successfully return from this
// function.
// TODO(nornagon/codebytere): Avoid nesting task loops here.
[self retain];
[[statusItem_ button] performClick:self];
[statusItem_ setMenu:[menuController_ menu]];
[self release];
return;
}
if (menuController_ && ![menuController_ isMenuOpen]) {
// Ensure the UI can update while the menu is fading out.
base::ScopedPumpMessagesInPrivateModes pump_private;
[[statusItem_ button] performClick:self];
}
}
- (void)closeContextMenu {
if (menuController_) {
[menuController_ cancel];
}
}
- (void)rightMouseUp:(NSEvent*)event {
trayIcon_->NotifyRightClicked(
gfx::ScreenRectFromNSRect(event.window.frame),
ui::EventFlagsFromModifiers([event modifierFlags]));
}
- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender {
trayIcon_->NotifyDragEntered();
return NSDragOperationCopy;
}
- (void)mouseExited:(NSEvent*)event {
trayIcon_->NotifyMouseExited(
gfx::ScreenPointFromNSPoint([event locationInWindow]),
ui::EventFlagsFromModifiers([event modifierFlags]));
}
- (void)mouseEntered:(NSEvent*)event {
trayIcon_->NotifyMouseEntered(
gfx::ScreenPointFromNSPoint([event locationInWindow]),
ui::EventFlagsFromModifiers([event modifierFlags]));
}
- (void)mouseMoved:(NSEvent*)event {
trayIcon_->NotifyMouseMoved(
gfx::ScreenPointFromNSPoint([event locationInWindow]),
ui::EventFlagsFromModifiers([event modifierFlags]));
}
- (void)draggingExited:(id<NSDraggingInfo>)sender {
trayIcon_->NotifyDragExited();
}
- (void)draggingEnded:(id<NSDraggingInfo>)sender {
trayIcon_->NotifyDragEnded();
if (NSPointInRect([sender draggingLocation], self.frame)) {
trayIcon_->NotifyDrop();
}
}
- (BOOL)handleDrop:(id<NSDraggingInfo>)sender {
NSPasteboard* pboard = [sender draggingPasteboard];
if ([[pboard types] containsObject:NSFilenamesPboardType]) {
std::vector<std::string> dropFiles;
NSArray* files = [pboard propertyListForType:NSFilenamesPboardType];
for (NSString* file in files)
dropFiles.push_back(base::SysNSStringToUTF8(file));
trayIcon_->NotifyDropFiles(dropFiles);
return YES;
} else if ([[pboard types] containsObject:NSStringPboardType]) {
NSString* dropText = [pboard stringForType:NSStringPboardType];
trayIcon_->NotifyDropText(base::SysNSStringToUTF8(dropText));
return YES;
}
return NO;
}
- (BOOL)prepareForDragOperation:(id<NSDraggingInfo>)sender {
return YES;
}
- (BOOL)performDragOperation:(id<NSDraggingInfo>)sender {
[self handleDrop:sender];
return YES;
}
@end
namespace electron {
TrayIconCocoa::TrayIconCocoa() {
status_item_view_.reset([[StatusItemView alloc] initWithIcon:this]);
}
TrayIconCocoa::~TrayIconCocoa() {
[status_item_view_ removeItem];
}
void TrayIconCocoa::SetImage(const gfx::Image& image) {
[status_item_view_ setImage:image.AsNSImage()];
}
void TrayIconCocoa::SetPressedImage(const gfx::Image& image) {
[status_item_view_ setAlternateImage:image.AsNSImage()];
}
void TrayIconCocoa::SetToolTip(const std::string& tool_tip) {
[status_item_view_ setToolTip:base::SysUTF8ToNSString(tool_tip)];
}
void TrayIconCocoa::SetTitle(const std::string& title,
const TitleOptions& options) {
[status_item_view_ setTitle:base::SysUTF8ToNSString(title)
font_type:base::SysUTF8ToNSString(options.font_type)];
}
std::string TrayIconCocoa::GetTitle() {
return base::SysNSStringToUTF8([status_item_view_ title]);
}
void TrayIconCocoa::SetIgnoreDoubleClickEvents(bool ignore) {
[status_item_view_ setIgnoreDoubleClickEvents:ignore];
}
bool TrayIconCocoa::GetIgnoreDoubleClickEvents() {
return [status_item_view_ getIgnoreDoubleClickEvents];
}
void TrayIconCocoa::PopUpOnUI(ElectronMenuModel* menu_model) {
[status_item_view_ popUpContextMenu:menu_model];
}
void TrayIconCocoa::PopUpContextMenu(const gfx::Point& pos,
raw_ptr<ElectronMenuModel> menu_model) {
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(&TrayIconCocoa::PopUpOnUI, weak_factory_.GetWeakPtr(),
base::Unretained(menu_model)));
}
void TrayIconCocoa::CloseContextMenu() {
[status_item_view_ closeContextMenu];
}
void TrayIconCocoa::SetContextMenu(raw_ptr<ElectronMenuModel> menu_model) {
if (menu_model) {
// Create native menu.
menu_.reset([[ElectronMenuController alloc] initWithModel:menu_model
useDefaultAccelerator:NO]);
} else {
menu_.reset();
}
[status_item_view_ setMenuController:menu_.get()];
}
gfx::Rect TrayIconCocoa::GetBounds() {
return gfx::ScreenRectFromNSRect([status_item_view_ window].frame);
}
// static
TrayIcon* TrayIcon::Create(absl::optional<UUID> guid) {
return new TrayIconCocoa;
}
} // namespace electron