123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499 |
- // 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 "atom/browser/ui/tray_icon_cocoa.h"
- #include "atom/browser/ui/cocoa/NSString+ANSI.h"
- #include "atom/browser/ui/cocoa/atom_menu_controller.h"
- #include "base/strings/sys_string_conversions.h"
- #include "ui/display/screen.h"
- #include "ui/events/cocoa/cocoa_event_utils.h"
- #include "ui/gfx/image/image.h"
- #include "ui/gfx/mac/coordinate_conversion.h"
- namespace {
- // By default, macOS sets 4px to tray image as left and right padding margin.
- const CGFloat kHorizontalMargin = 4;
- // macOS tends to make the title 2px lower.
- const CGFloat kVerticalTitleMargin = 2;
- } // namespace
- @interface StatusItemView : NSView {
- atom::TrayIconCocoa* trayIcon_; // weak
- AtomMenuController* menuController_; // weak
- atom::TrayIcon::HighlightMode highlight_mode_;
- BOOL ignoreDoubleClickEvents_;
- BOOL forceHighlight_;
- BOOL inMouseEventSequence_;
- BOOL ANSI_;
- base::scoped_nsobject<NSImage> image_;
- base::scoped_nsobject<NSImage> alternateImage_;
- base::scoped_nsobject<NSString> title_;
- base::scoped_nsobject<NSMutableAttributedString> attributedTitle_;
- base::scoped_nsobject<NSStatusItem> statusItem_;
- base::scoped_nsobject<NSTrackingArea> trackingArea_;
- }
- @end // @interface StatusItemView
- @implementation StatusItemView
- - (id)initWithImage:(NSImage*)image icon:(atom::TrayIconCocoa*)icon {
- image_.reset([image copy]);
- trayIcon_ = icon;
- highlight_mode_ = atom::TrayIcon::HighlightMode::SELECTION;
- ignoreDoubleClickEvents_ = NO;
- forceHighlight_ = NO;
- inMouseEventSequence_ = 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_ setView:self];
- // Finalize setup by sizing our views
- [self updateDimensions];
- // Add NSTrackingArea for listening to mouseEnter, mouseExit, and mouseMove
- // events
- trackingArea_.reset([[NSTrackingArea alloc]
- initWithRect:[self bounds]
- options:NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved |
- NSTrackingActiveAlways
- owner:self
- userInfo:nil]);
- [self addTrackingArea:trackingArea_];
- }
- return self;
- }
- - (void)updateDimensions {
- NSStatusBar* bar = [NSStatusBar systemStatusBar];
- [self setFrame:NSMakeRect(0, 0, [self fullWidth], [bar thickness])];
- [self setNeedsDisplay:YES];
- }
- - (void)removeItem {
- // Turn off tracking events to prevent crash
- if (trackingArea_) {
- [self removeTrackingArea:trackingArea_];
- trackingArea_.reset();
- }
- [[NSStatusBar systemStatusBar] removeStatusItem:statusItem_];
- statusItem_.reset();
- }
- - (void)drawRect:(NSRect)dirtyRect {
- // Draw the tray icon and title that align with NSStatusItem, layout:
- // ----------------
- // | icon | title |
- /// ----------------
- CGFloat thickness = [[statusItem_ statusBar] thickness];
- // Draw the system bar background.
- [statusItem_ drawStatusBarBackgroundInRect:self.bounds
- withHighlight:[self shouldHighlight]];
- // Determine which image to use.
- NSImage* image = image_.get();
- if (inMouseEventSequence_ && alternateImage_) {
- image = alternateImage_.get();
- }
- // Apply the higlight color if the image is a template image. When this moves
- // to using the new [NSStatusItem button] API, this should work automagically.
- if ([image isTemplate] == YES) {
- NSImage* imageWithColor = [[image copy] autorelease];
- [imageWithColor lockFocus];
- [[self colorWithHighlight:[self isHighlighted]] set];
- CGRect imageBounds = CGRectMake(0, 0, image.size.width, image.size.height);
- NSRectFillUsingOperation(imageBounds, NSCompositeSourceAtop);
- [imageWithColor unlockFocus];
- image = imageWithColor;
- }
- // Draw the image
- [image
- drawInRect:CGRectMake(roundf(([self iconWidth] - image.size.width) / 2),
- roundf((thickness - image.size.height) / 2),
- image.size.width, image.size.height)];
- if (title_) {
- // Draw title.
- NSRect titleDrawRect = NSMakeRect([self iconWidth], -kVerticalTitleMargin,
- [self titleWidth], thickness);
- [attributedTitle_ drawInRect:titleDrawRect];
- }
- }
- - (BOOL)isDarkMode {
- NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
- NSString* mode = [defaults stringForKey:@"AppleInterfaceStyle"];
- return mode && [mode isEqualToString:@"Dark"];
- }
- - (BOOL)isHighlighted {
- BOOL highlight = [self shouldHighlight];
- return highlight | [self isDarkMode];
- }
- // The width of the full status item.
- - (CGFloat)fullWidth {
- if (title_)
- return [self iconWidth] + [self titleWidth] + kHorizontalMargin;
- else
- return [self iconWidth];
- }
- // The width of the icon.
- - (CGFloat)iconWidth {
- CGFloat thickness = [[NSStatusBar systemStatusBar] thickness];
- CGFloat imageHeight = [image_ size].height;
- CGFloat imageWidth = [image_ size].width;
- CGFloat iconWidth = imageWidth;
- if (imageWidth < thickness) {
- // Image's width must be larger than menu bar's height.
- iconWidth = thickness;
- } else {
- CGFloat verticalMargin = thickness - imageHeight;
- // Image must have same horizontal vertical margin.
- if (verticalMargin > 0 && imageWidth != imageHeight)
- iconWidth = imageWidth + verticalMargin;
- CGFloat horizontalMargin = thickness - imageWidth;
- // Image must have at least kHorizontalMargin horizontal margin on each
- // side.
- if (horizontalMargin < 2 * kHorizontalMargin)
- iconWidth = imageWidth + 2 * kHorizontalMargin;
- }
- return iconWidth;
- }
- // The width of the title.
- - (CGFloat)titleWidth {
- if (!title_)
- return 0;
- return [attributedTitle_ size].width;
- }
- - (NSColor*)colorWithHighlight:(BOOL)highlight {
- return highlight ? [NSColor whiteColor]
- : [NSColor colorWithRed:0.265625
- green:0.25390625
- blue:0.234375
- alpha:1.0];
- }
- - (void)setImage:(NSImage*)image {
- image_.reset([image copy]);
- [self updateDimensions];
- }
- - (void)setAlternateImage:(NSImage*)image {
- alternateImage_.reset([image copy]);
- }
- - (void)setHighlight:(atom::TrayIcon::HighlightMode)mode {
- highlight_mode_ = mode;
- [self setNeedsDisplay:YES];
- }
- - (void)setIgnoreDoubleClickEvents:(BOOL)ignore {
- ignoreDoubleClickEvents_ = ignore;
- }
- - (BOOL)getIgnoreDoubleClickEvents {
- return ignoreDoubleClickEvents_;
- }
- - (void)setTitle:(NSString*)title {
- if (title.length > 0) {
- title_.reset([title copy]);
- ANSI_ = [title containsANSICodes];
- } else {
- title_.reset();
- ANSI_ = NO;
- }
- [self updateAttributedTitle];
- [self updateDimensions];
- }
- - (void)updateAttributedTitle {
- NSDictionary* attributes =
- @{NSFontAttributeName : [NSFont menuBarFontOfSize:0]};
- if (ANSI_) {
- NSCharacterSet* whites = [NSCharacterSet whitespaceCharacterSet];
- NSString* title = [title_ stringByTrimmingCharactersInSet:whites];
- attributedTitle_.reset([title attributedStringParsingANSICodes]);
- [attributedTitle_ addAttributes:attributes
- range:NSMakeRange(0, [attributedTitle_ length])];
- return;
- }
- // check title_ being nil
- NSString* title = @"";
- if (title_)
- title = title_;
- attributedTitle_.reset([[NSMutableAttributedString alloc]
- initWithString:title
- attributes:attributes]);
- // NSFontAttributeName:[NSFont menuBarFontOfSize:0],
- // NSForegroundColorAttributeName:[self colorWithHighlight: highlight]
- [attributedTitle_ addAttributes:attributes
- range:NSMakeRange(0, [attributedTitle_ length])];
- [attributedTitle_ addAttribute:NSForegroundColorAttributeName
- value:[self colorWithHighlight:[self isHighlighted]]
- range:NSMakeRange(0, [attributedTitle_ length])];
- }
- - (void)setMenuController:(AtomMenuController*)menu {
- menuController_ = menu;
- }
- - (void)mouseDown:(NSEvent*)event {
- inMouseEventSequence_ = YES;
- [self setNeedsDisplay:YES];
- }
- - (void)mouseUp:(NSEvent*)event {
- if (!inMouseEventSequence_) {
- // If the menu is showing, when user clicked the tray icon, the `mouseDown`
- // event will be dissmissed, we need to close the menu at this time.
- [self setNeedsDisplay:YES];
- return;
- }
- inMouseEventSequence_ = NO;
- // Show menu when there is a context menu.
- // NB(hokein): Make tray's behavior more like official one's.
- // When the tray icon gets clicked quickly multiple times, the
- // event.clickCount doesn't always return 1. Instead, it returns a value that
- // counts the clicked times.
- // So we don't check the clickCount here, just pop up the menu for each click
- // event.
- if (menuController_)
- [statusItem_ popUpStatusItemMenu:[menuController_ menu]];
- // Don't emit click events when menu is showing.
- if (menuController_)
- return;
- // 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]));
- [self setNeedsDisplay:YES];
- }
- - (void)popUpContextMenu:(atom::AtomMenuModel*)menu_model {
- // Show a custom menu.
- if (menu_model) {
- base::scoped_nsobject<AtomMenuController> menuController([
- [AtomMenuController alloc] initWithModel:menu_model
- useDefaultAccelerator:NO]);
- forceHighlight_ = YES; // Should highlight when showing menu.
- [self setNeedsDisplay:YES];
- [statusItem_ popUpStatusItemMenu:[menuController menu]];
- forceHighlight_ = NO;
- [self setNeedsDisplay:YES];
- return;
- }
- if (menuController_ && ![menuController_ isMenuOpen]) {
- // Redraw the tray icon to show highlight if it is enabled.
- [self setNeedsDisplay:YES];
- [statusItem_ popUpStatusItemMenu:[menuController_ menu]];
- // The popUpStatusItemMenu returns only after the showing menu is closed.
- // When it returns, we need to redraw the tray icon to not show highlight.
- [self setNeedsDisplay:YES];
- }
- }
- - (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;
- }
- - (BOOL)shouldHighlight {
- switch (highlight_mode_) {
- case atom::TrayIcon::HighlightMode::ALWAYS:
- return true;
- case atom::TrayIcon::HighlightMode::NEVER:
- return false;
- case atom::TrayIcon::HighlightMode::SELECTION:
- BOOL isMenuOpen = menuController_ && [menuController_ isMenuOpen];
- return forceHighlight_ || inMouseEventSequence_ || isMenuOpen;
- }
- }
- @end
- namespace atom {
- TrayIconCocoa::TrayIconCocoa() {}
- TrayIconCocoa::~TrayIconCocoa() {
- [status_item_view_ removeItem];
- if (menu_model_)
- menu_model_->RemoveObserver(this);
- }
- void TrayIconCocoa::SetImage(const gfx::Image& image) {
- if (status_item_view_) {
- [status_item_view_ setImage:image.AsNSImage()];
- } else {
- status_item_view_.reset(
- [[StatusItemView alloc] initWithImage:image.AsNSImage() icon:this]);
- }
- }
- 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) {
- [status_item_view_ setTitle:base::SysUTF8ToNSString(title)];
- }
- void TrayIconCocoa::SetHighlightMode(TrayIcon::HighlightMode mode) {
- [status_item_view_ setHighlight:mode];
- }
- void TrayIconCocoa::SetIgnoreDoubleClickEvents(bool ignore) {
- [status_item_view_ setIgnoreDoubleClickEvents:ignore];
- }
- bool TrayIconCocoa::GetIgnoreDoubleClickEvents() {
- return [status_item_view_ getIgnoreDoubleClickEvents];
- }
- void TrayIconCocoa::PopUpContextMenu(const gfx::Point& pos,
- AtomMenuModel* menu_model) {
- [status_item_view_ popUpContextMenu:menu_model];
- }
- void TrayIconCocoa::SetContextMenu(AtomMenuModel* menu_model) {
- // Substribe to MenuClosed event.
- if (menu_model_)
- menu_model_->RemoveObserver(this);
- menu_model->AddObserver(this);
- // Create native menu.
- menu_.reset([[AtomMenuController alloc] initWithModel:menu_model
- useDefaultAccelerator:NO]);
- [status_item_view_ setMenuController:menu_.get()];
- }
- gfx::Rect TrayIconCocoa::GetBounds() {
- auto bounds = gfx::ScreenRectFromNSRect([status_item_view_ window].frame);
- // Calling [window frame] immediately after the view gets created will have
- // negative |y| sometimes.
- if (bounds.y() < 0)
- bounds.set_y(0);
- return bounds;
- }
- void TrayIconCocoa::OnMenuWillClose() {
- [status_item_view_ setNeedsDisplay:YES];
- }
- // static
- TrayIcon* TrayIcon::Create() {
- return new TrayIconCocoa;
- }
- } // namespace atom
|