tray_icon_cocoa.mm 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499
  1. // Copyright (c) 2014 GitHub, Inc.
  2. // Use of this source code is governed by the MIT license that can be
  3. // found in the LICENSE file.
  4. #include "atom/browser/ui/tray_icon_cocoa.h"
  5. #include "atom/browser/ui/cocoa/NSString+ANSI.h"
  6. #include "atom/browser/ui/cocoa/atom_menu_controller.h"
  7. #include "base/strings/sys_string_conversions.h"
  8. #include "ui/display/screen.h"
  9. #include "ui/events/cocoa/cocoa_event_utils.h"
  10. #include "ui/gfx/image/image.h"
  11. #include "ui/gfx/mac/coordinate_conversion.h"
  12. namespace {
  13. // By default, macOS sets 4px to tray image as left and right padding margin.
  14. const CGFloat kHorizontalMargin = 4;
  15. // macOS tends to make the title 2px lower.
  16. const CGFloat kVerticalTitleMargin = 2;
  17. } // namespace
  18. @interface StatusItemView : NSView {
  19. atom::TrayIconCocoa* trayIcon_; // weak
  20. AtomMenuController* menuController_; // weak
  21. atom::TrayIcon::HighlightMode highlight_mode_;
  22. BOOL ignoreDoubleClickEvents_;
  23. BOOL forceHighlight_;
  24. BOOL inMouseEventSequence_;
  25. BOOL ANSI_;
  26. base::scoped_nsobject<NSImage> image_;
  27. base::scoped_nsobject<NSImage> alternateImage_;
  28. base::scoped_nsobject<NSString> title_;
  29. base::scoped_nsobject<NSMutableAttributedString> attributedTitle_;
  30. base::scoped_nsobject<NSStatusItem> statusItem_;
  31. base::scoped_nsobject<NSTrackingArea> trackingArea_;
  32. }
  33. @end // @interface StatusItemView
  34. @implementation StatusItemView
  35. - (id)initWithImage:(NSImage*)image icon:(atom::TrayIconCocoa*)icon {
  36. image_.reset([image copy]);
  37. trayIcon_ = icon;
  38. highlight_mode_ = atom::TrayIcon::HighlightMode::SELECTION;
  39. ignoreDoubleClickEvents_ = NO;
  40. forceHighlight_ = NO;
  41. inMouseEventSequence_ = NO;
  42. if ((self = [super initWithFrame:CGRectZero])) {
  43. [self registerForDraggedTypes:@[
  44. NSFilenamesPboardType,
  45. NSStringPboardType,
  46. ]];
  47. // Create the status item.
  48. NSStatusItem* item = [[NSStatusBar systemStatusBar]
  49. statusItemWithLength:NSVariableStatusItemLength];
  50. statusItem_.reset([item retain]);
  51. [statusItem_ setView:self];
  52. // Finalize setup by sizing our views
  53. [self updateDimensions];
  54. // Add NSTrackingArea for listening to mouseEnter, mouseExit, and mouseMove
  55. // events
  56. trackingArea_.reset([[NSTrackingArea alloc]
  57. initWithRect:[self bounds]
  58. options:NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved |
  59. NSTrackingActiveAlways
  60. owner:self
  61. userInfo:nil]);
  62. [self addTrackingArea:trackingArea_];
  63. }
  64. return self;
  65. }
  66. - (void)updateDimensions {
  67. NSStatusBar* bar = [NSStatusBar systemStatusBar];
  68. [self setFrame:NSMakeRect(0, 0, [self fullWidth], [bar thickness])];
  69. [self setNeedsDisplay:YES];
  70. }
  71. - (void)removeItem {
  72. // Turn off tracking events to prevent crash
  73. if (trackingArea_) {
  74. [self removeTrackingArea:trackingArea_];
  75. trackingArea_.reset();
  76. }
  77. [[NSStatusBar systemStatusBar] removeStatusItem:statusItem_];
  78. statusItem_.reset();
  79. }
  80. - (void)drawRect:(NSRect)dirtyRect {
  81. // Draw the tray icon and title that align with NSStatusItem, layout:
  82. // ----------------
  83. // | icon | title |
  84. /// ----------------
  85. CGFloat thickness = [[statusItem_ statusBar] thickness];
  86. // Draw the system bar background.
  87. [statusItem_ drawStatusBarBackgroundInRect:self.bounds
  88. withHighlight:[self shouldHighlight]];
  89. // Determine which image to use.
  90. NSImage* image = image_.get();
  91. if (inMouseEventSequence_ && alternateImage_) {
  92. image = alternateImage_.get();
  93. }
  94. // Apply the higlight color if the image is a template image. When this moves
  95. // to using the new [NSStatusItem button] API, this should work automagically.
  96. if ([image isTemplate] == YES) {
  97. NSImage* imageWithColor = [[image copy] autorelease];
  98. [imageWithColor lockFocus];
  99. [[self colorWithHighlight:[self isHighlighted]] set];
  100. CGRect imageBounds = CGRectMake(0, 0, image.size.width, image.size.height);
  101. NSRectFillUsingOperation(imageBounds, NSCompositeSourceAtop);
  102. [imageWithColor unlockFocus];
  103. image = imageWithColor;
  104. }
  105. // Draw the image
  106. [image
  107. drawInRect:CGRectMake(roundf(([self iconWidth] - image.size.width) / 2),
  108. roundf((thickness - image.size.height) / 2),
  109. image.size.width, image.size.height)];
  110. if (title_) {
  111. // Draw title.
  112. NSRect titleDrawRect = NSMakeRect([self iconWidth], -kVerticalTitleMargin,
  113. [self titleWidth], thickness);
  114. [attributedTitle_ drawInRect:titleDrawRect];
  115. }
  116. }
  117. - (BOOL)isDarkMode {
  118. NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
  119. NSString* mode = [defaults stringForKey:@"AppleInterfaceStyle"];
  120. return mode && [mode isEqualToString:@"Dark"];
  121. }
  122. - (BOOL)isHighlighted {
  123. BOOL highlight = [self shouldHighlight];
  124. return highlight | [self isDarkMode];
  125. }
  126. // The width of the full status item.
  127. - (CGFloat)fullWidth {
  128. if (title_)
  129. return [self iconWidth] + [self titleWidth] + kHorizontalMargin;
  130. else
  131. return [self iconWidth];
  132. }
  133. // The width of the icon.
  134. - (CGFloat)iconWidth {
  135. CGFloat thickness = [[NSStatusBar systemStatusBar] thickness];
  136. CGFloat imageHeight = [image_ size].height;
  137. CGFloat imageWidth = [image_ size].width;
  138. CGFloat iconWidth = imageWidth;
  139. if (imageWidth < thickness) {
  140. // Image's width must be larger than menu bar's height.
  141. iconWidth = thickness;
  142. } else {
  143. CGFloat verticalMargin = thickness - imageHeight;
  144. // Image must have same horizontal vertical margin.
  145. if (verticalMargin > 0 && imageWidth != imageHeight)
  146. iconWidth = imageWidth + verticalMargin;
  147. CGFloat horizontalMargin = thickness - imageWidth;
  148. // Image must have at least kHorizontalMargin horizontal margin on each
  149. // side.
  150. if (horizontalMargin < 2 * kHorizontalMargin)
  151. iconWidth = imageWidth + 2 * kHorizontalMargin;
  152. }
  153. return iconWidth;
  154. }
  155. // The width of the title.
  156. - (CGFloat)titleWidth {
  157. if (!title_)
  158. return 0;
  159. return [attributedTitle_ size].width;
  160. }
  161. - (NSColor*)colorWithHighlight:(BOOL)highlight {
  162. return highlight ? [NSColor whiteColor]
  163. : [NSColor colorWithRed:0.265625
  164. green:0.25390625
  165. blue:0.234375
  166. alpha:1.0];
  167. }
  168. - (void)setImage:(NSImage*)image {
  169. image_.reset([image copy]);
  170. [self updateDimensions];
  171. }
  172. - (void)setAlternateImage:(NSImage*)image {
  173. alternateImage_.reset([image copy]);
  174. }
  175. - (void)setHighlight:(atom::TrayIcon::HighlightMode)mode {
  176. highlight_mode_ = mode;
  177. [self setNeedsDisplay:YES];
  178. }
  179. - (void)setIgnoreDoubleClickEvents:(BOOL)ignore {
  180. ignoreDoubleClickEvents_ = ignore;
  181. }
  182. - (BOOL)getIgnoreDoubleClickEvents {
  183. return ignoreDoubleClickEvents_;
  184. }
  185. - (void)setTitle:(NSString*)title {
  186. if (title.length > 0) {
  187. title_.reset([title copy]);
  188. ANSI_ = [title containsANSICodes];
  189. } else {
  190. title_.reset();
  191. ANSI_ = NO;
  192. }
  193. [self updateAttributedTitle];
  194. [self updateDimensions];
  195. }
  196. - (void)updateAttributedTitle {
  197. NSDictionary* attributes =
  198. @{NSFontAttributeName : [NSFont menuBarFontOfSize:0]};
  199. if (ANSI_) {
  200. NSCharacterSet* whites = [NSCharacterSet whitespaceCharacterSet];
  201. NSString* title = [title_ stringByTrimmingCharactersInSet:whites];
  202. attributedTitle_.reset([title attributedStringParsingANSICodes]);
  203. [attributedTitle_ addAttributes:attributes
  204. range:NSMakeRange(0, [attributedTitle_ length])];
  205. return;
  206. }
  207. // check title_ being nil
  208. NSString* title = @"";
  209. if (title_)
  210. title = title_;
  211. attributedTitle_.reset([[NSMutableAttributedString alloc]
  212. initWithString:title
  213. attributes:attributes]);
  214. // NSFontAttributeName:[NSFont menuBarFontOfSize:0],
  215. // NSForegroundColorAttributeName:[self colorWithHighlight: highlight]
  216. [attributedTitle_ addAttributes:attributes
  217. range:NSMakeRange(0, [attributedTitle_ length])];
  218. [attributedTitle_ addAttribute:NSForegroundColorAttributeName
  219. value:[self colorWithHighlight:[self isHighlighted]]
  220. range:NSMakeRange(0, [attributedTitle_ length])];
  221. }
  222. - (void)setMenuController:(AtomMenuController*)menu {
  223. menuController_ = menu;
  224. }
  225. - (void)mouseDown:(NSEvent*)event {
  226. inMouseEventSequence_ = YES;
  227. [self setNeedsDisplay:YES];
  228. }
  229. - (void)mouseUp:(NSEvent*)event {
  230. if (!inMouseEventSequence_) {
  231. // If the menu is showing, when user clicked the tray icon, the `mouseDown`
  232. // event will be dissmissed, we need to close the menu at this time.
  233. [self setNeedsDisplay:YES];
  234. return;
  235. }
  236. inMouseEventSequence_ = NO;
  237. // Show menu when there is a context menu.
  238. // NB(hokein): Make tray's behavior more like official one's.
  239. // When the tray icon gets clicked quickly multiple times, the
  240. // event.clickCount doesn't always return 1. Instead, it returns a value that
  241. // counts the clicked times.
  242. // So we don't check the clickCount here, just pop up the menu for each click
  243. // event.
  244. if (menuController_)
  245. [statusItem_ popUpStatusItemMenu:[menuController_ menu]];
  246. // Don't emit click events when menu is showing.
  247. if (menuController_)
  248. return;
  249. // If we are ignoring double click events, we should ignore the `clickCount`
  250. // value and immediately emit a click event.
  251. BOOL shouldBeHandledAsASingleClick =
  252. (event.clickCount == 1) || ignoreDoubleClickEvents_;
  253. if (shouldBeHandledAsASingleClick)
  254. trayIcon_->NotifyClicked(
  255. gfx::ScreenRectFromNSRect(event.window.frame),
  256. gfx::ScreenPointFromNSPoint([event locationInWindow]),
  257. ui::EventFlagsFromModifiers([event modifierFlags]));
  258. // Double click event.
  259. BOOL shouldBeHandledAsADoubleClick =
  260. (event.clickCount == 2) && !ignoreDoubleClickEvents_;
  261. if (shouldBeHandledAsADoubleClick)
  262. trayIcon_->NotifyDoubleClicked(
  263. gfx::ScreenRectFromNSRect(event.window.frame),
  264. ui::EventFlagsFromModifiers([event modifierFlags]));
  265. [self setNeedsDisplay:YES];
  266. }
  267. - (void)popUpContextMenu:(atom::AtomMenuModel*)menu_model {
  268. // Show a custom menu.
  269. if (menu_model) {
  270. base::scoped_nsobject<AtomMenuController> menuController([
  271. [AtomMenuController alloc] initWithModel:menu_model
  272. useDefaultAccelerator:NO]);
  273. forceHighlight_ = YES; // Should highlight when showing menu.
  274. [self setNeedsDisplay:YES];
  275. [statusItem_ popUpStatusItemMenu:[menuController menu]];
  276. forceHighlight_ = NO;
  277. [self setNeedsDisplay:YES];
  278. return;
  279. }
  280. if (menuController_ && ![menuController_ isMenuOpen]) {
  281. // Redraw the tray icon to show highlight if it is enabled.
  282. [self setNeedsDisplay:YES];
  283. [statusItem_ popUpStatusItemMenu:[menuController_ menu]];
  284. // The popUpStatusItemMenu returns only after the showing menu is closed.
  285. // When it returns, we need to redraw the tray icon to not show highlight.
  286. [self setNeedsDisplay:YES];
  287. }
  288. }
  289. - (void)rightMouseUp:(NSEvent*)event {
  290. trayIcon_->NotifyRightClicked(
  291. gfx::ScreenRectFromNSRect(event.window.frame),
  292. ui::EventFlagsFromModifiers([event modifierFlags]));
  293. }
  294. - (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender {
  295. trayIcon_->NotifyDragEntered();
  296. return NSDragOperationCopy;
  297. }
  298. - (void)mouseExited:(NSEvent*)event {
  299. trayIcon_->NotifyMouseExited(
  300. gfx::ScreenPointFromNSPoint([event locationInWindow]),
  301. ui::EventFlagsFromModifiers([event modifierFlags]));
  302. }
  303. - (void)mouseEntered:(NSEvent*)event {
  304. trayIcon_->NotifyMouseEntered(
  305. gfx::ScreenPointFromNSPoint([event locationInWindow]),
  306. ui::EventFlagsFromModifiers([event modifierFlags]));
  307. }
  308. - (void)mouseMoved:(NSEvent*)event {
  309. trayIcon_->NotifyMouseMoved(
  310. gfx::ScreenPointFromNSPoint([event locationInWindow]),
  311. ui::EventFlagsFromModifiers([event modifierFlags]));
  312. }
  313. - (void)draggingExited:(id<NSDraggingInfo>)sender {
  314. trayIcon_->NotifyDragExited();
  315. }
  316. - (void)draggingEnded:(id<NSDraggingInfo>)sender {
  317. trayIcon_->NotifyDragEnded();
  318. if (NSPointInRect([sender draggingLocation], self.frame)) {
  319. trayIcon_->NotifyDrop();
  320. }
  321. }
  322. - (BOOL)handleDrop:(id<NSDraggingInfo>)sender {
  323. NSPasteboard* pboard = [sender draggingPasteboard];
  324. if ([[pboard types] containsObject:NSFilenamesPboardType]) {
  325. std::vector<std::string> dropFiles;
  326. NSArray* files = [pboard propertyListForType:NSFilenamesPboardType];
  327. for (NSString* file in files)
  328. dropFiles.push_back(base::SysNSStringToUTF8(file));
  329. trayIcon_->NotifyDropFiles(dropFiles);
  330. return YES;
  331. } else if ([[pboard types] containsObject:NSStringPboardType]) {
  332. NSString* dropText = [pboard stringForType:NSStringPboardType];
  333. trayIcon_->NotifyDropText(base::SysNSStringToUTF8(dropText));
  334. return YES;
  335. }
  336. return NO;
  337. }
  338. - (BOOL)prepareForDragOperation:(id<NSDraggingInfo>)sender {
  339. return YES;
  340. }
  341. - (BOOL)performDragOperation:(id<NSDraggingInfo>)sender {
  342. [self handleDrop:sender];
  343. return YES;
  344. }
  345. - (BOOL)shouldHighlight {
  346. switch (highlight_mode_) {
  347. case atom::TrayIcon::HighlightMode::ALWAYS:
  348. return true;
  349. case atom::TrayIcon::HighlightMode::NEVER:
  350. return false;
  351. case atom::TrayIcon::HighlightMode::SELECTION:
  352. BOOL isMenuOpen = menuController_ && [menuController_ isMenuOpen];
  353. return forceHighlight_ || inMouseEventSequence_ || isMenuOpen;
  354. }
  355. }
  356. @end
  357. namespace atom {
  358. TrayIconCocoa::TrayIconCocoa() {}
  359. TrayIconCocoa::~TrayIconCocoa() {
  360. [status_item_view_ removeItem];
  361. if (menu_model_)
  362. menu_model_->RemoveObserver(this);
  363. }
  364. void TrayIconCocoa::SetImage(const gfx::Image& image) {
  365. if (status_item_view_) {
  366. [status_item_view_ setImage:image.AsNSImage()];
  367. } else {
  368. status_item_view_.reset(
  369. [[StatusItemView alloc] initWithImage:image.AsNSImage() icon:this]);
  370. }
  371. }
  372. void TrayIconCocoa::SetPressedImage(const gfx::Image& image) {
  373. [status_item_view_ setAlternateImage:image.AsNSImage()];
  374. }
  375. void TrayIconCocoa::SetToolTip(const std::string& tool_tip) {
  376. [status_item_view_ setToolTip:base::SysUTF8ToNSString(tool_tip)];
  377. }
  378. void TrayIconCocoa::SetTitle(const std::string& title) {
  379. [status_item_view_ setTitle:base::SysUTF8ToNSString(title)];
  380. }
  381. void TrayIconCocoa::SetHighlightMode(TrayIcon::HighlightMode mode) {
  382. [status_item_view_ setHighlight:mode];
  383. }
  384. void TrayIconCocoa::SetIgnoreDoubleClickEvents(bool ignore) {
  385. [status_item_view_ setIgnoreDoubleClickEvents:ignore];
  386. }
  387. bool TrayIconCocoa::GetIgnoreDoubleClickEvents() {
  388. return [status_item_view_ getIgnoreDoubleClickEvents];
  389. }
  390. void TrayIconCocoa::PopUpContextMenu(const gfx::Point& pos,
  391. AtomMenuModel* menu_model) {
  392. [status_item_view_ popUpContextMenu:menu_model];
  393. }
  394. void TrayIconCocoa::SetContextMenu(AtomMenuModel* menu_model) {
  395. // Substribe to MenuClosed event.
  396. if (menu_model_)
  397. menu_model_->RemoveObserver(this);
  398. menu_model->AddObserver(this);
  399. // Create native menu.
  400. menu_.reset([[AtomMenuController alloc] initWithModel:menu_model
  401. useDefaultAccelerator:NO]);
  402. [status_item_view_ setMenuController:menu_.get()];
  403. }
  404. gfx::Rect TrayIconCocoa::GetBounds() {
  405. auto bounds = gfx::ScreenRectFromNSRect([status_item_view_ window].frame);
  406. // Calling [window frame] immediately after the view gets created will have
  407. // negative |y| sometimes.
  408. if (bounds.y() < 0)
  409. bounds.set_y(0);
  410. return bounds;
  411. }
  412. void TrayIconCocoa::OnMenuWillClose() {
  413. [status_item_view_ setNeedsDisplay:YES];
  414. }
  415. // static
  416. TrayIcon* TrayIcon::Create() {
  417. return new TrayIconCocoa;
  418. }
  419. } // namespace atom