123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429 |
- // 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_apple.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_;
- NSStatusItem* __strong statusItem_;
- NSTrackingArea* __strong trackingArea_;
- }
- @end // @interface StatusItemView
- @implementation StatusItemView
- - (void)dealloc {
- trayIcon_ = nil;
- menuController_ = nil;
- }
- - (id)initWithIcon:(electron::TrayIconCocoa*)icon {
- trayIcon_ = icon;
- menuController_ = nil;
- ignoreDoubleClickEvents_ = NO;
- if ((self = [super initWithFrame:CGRectZero])) {
- [self registerForDraggedTypes:@[
- #pragma clang diagnostic push
- #pragma clang diagnostic ignored "-Wdeprecated-declarations"
- NSFilenamesPboardType,
- #pragma clang diagnostic pop
- NSPasteboardTypeString,
- ]];
- // Create the status item.
- NSStatusItem* item = [[NSStatusBar systemStatusBar]
- statusItemWithLength:NSVariableStatusItemLength];
- statusItem_ = item;
- [[statusItem_ button] addSubview:self];
- // We need to set the target and action on the button, otherwise
- // VoiceOver doesn't know where to send the select action.
- [[statusItem_ button] setTarget:self];
- [[statusItem_ button] setAction:@selector(mouseDown:)];
- [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_ = [[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_ = nil;
- }
- // Ensure any open menu is closed.
- if ([statusItem_ menu])
- [[statusItem_ menu] cancelTracking];
- [[NSStatusBar systemStatusBar] removeStatusItem:statusItem_];
- [self removeFromSuperview];
- statusItem_ = nil;
- }
- - (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"]) {
- 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 {
- // If |event| does not respond to locationInWindow, we've
- // arrived here from VoiceOver, which does not pass an event.
- // Create a synthetic event to pass to the click handler.
- if (![event respondsToSelector:@selector(locationInWindow)]) {
- event = [NSEvent mouseEventWithType:NSEventTypeRightMouseDown
- location:NSMakePoint(0, 0)
- modifierFlags:0
- timestamp:NSApp.currentEvent.timestamp
- windowNumber:0
- context:nil
- eventNumber:0
- clickCount:1
- pressure:1.0];
- // We also need to explicitly call the click handler here, since
- // VoiceOver won't trigger mouseUp.
- [self handleClickNotifications: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) {
- ElectronMenuController* menuController =
- [[ElectronMenuController alloc] initWithModel:menu_model
- useDefaultAccelerator:NO];
- // Hacky way to mimic design of ordinary tray menu.
- [statusItem_ setMenu:[menuController menu]];
- base::WeakPtr<electron::TrayIconCocoa> weak_tray_icon =
- trayIcon_->GetWeakPtr();
- [[statusItem_ button] performClick:self];
- // /⚠️ \ Warning! Arbitrary JavaScript and who knows what else has been run
- // during -performClick:. This object may have been deleted.
- // We check if |trayIcon_| is still alive as it owns us and has the same
- // lifetime.
- if (!weak_tray_icon)
- return;
- [statusItem_ setMenu:[menuController_ menu]];
- 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];
- // TODO(codebytere): update to currently supported NSPasteboardTypeFileURL or
- // kUTTypeFileURL.
- #pragma clang diagnostic push
- #pragma clang diagnostic ignored "-Wdeprecated-declarations"
- 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:NSPasteboardTypeString]) {
- NSString* dropText = [pboard stringForType:NSPasteboardTypeString];
- trayIcon_->NotifyDropText(base::SysNSStringToUTF8(dropText));
- return YES;
- }
- #pragma clang diagnostic pop
- 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_ = [[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(base::WeakPtr<ElectronMenuModel> menu_model) {
- [status_item_view_ popUpContextMenu:menu_model.get()];
- }
- void TrayIconCocoa::PopUpContextMenu(
- const gfx::Point& pos,
- base::WeakPtr<ElectronMenuModel> menu_model) {
- content::GetUIThreadTaskRunner({})->PostTask(
- FROM_HERE, base::BindOnce(&TrayIconCocoa::PopUpOnUI,
- weak_factory_.GetWeakPtr(), menu_model));
- }
- void TrayIconCocoa::CloseContextMenu() {
- [status_item_view_ closeContextMenu];
- }
- void TrayIconCocoa::SetContextMenu(raw_ptr<ElectronMenuModel> menu_model) {
- if (menu_model) {
- // Create native menu.
- menu_ = [[ElectronMenuController alloc] initWithModel:menu_model
- useDefaultAccelerator:NO];
- } else {
- menu_ = nil;
- }
- [status_item_view_ setMenuController:menu_];
- }
- gfx::Rect TrayIconCocoa::GetBounds() {
- return gfx::ScreenRectFromNSRect([status_item_view_ window].frame);
- }
- // static
- TrayIcon* TrayIcon::Create(std::optional<UUID> guid) {
- return new TrayIconCocoa;
- }
- } // namespace electron
|