tray_icon_cocoa.mm 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  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 "shell/browser/ui/tray_icon_cocoa.h"
  5. #include <string>
  6. #include <vector>
  7. #include "base/memory/raw_ptr.h"
  8. #include "base/message_loop/message_pump_mac.h"
  9. #include "base/strings/sys_string_conversions.h"
  10. #include "base/task/current_thread.h"
  11. #include "content/public/browser/browser_task_traits.h"
  12. #include "content/public/browser/browser_thread.h"
  13. #include "shell/browser/ui/cocoa/NSString+ANSI.h"
  14. #include "shell/browser/ui/cocoa/electron_menu_controller.h"
  15. #include "ui/events/cocoa/cocoa_event_utils.h"
  16. #include "ui/gfx/mac/coordinate_conversion.h"
  17. #include "ui/native_theme/native_theme.h"
  18. @interface StatusItemView : NSView {
  19. raw_ptr<electron::TrayIconCocoa> trayIcon_; // weak
  20. ElectronMenuController* menuController_; // weak
  21. BOOL ignoreDoubleClickEvents_;
  22. NSStatusItem* __strong statusItem_;
  23. NSTrackingArea* __strong trackingArea_;
  24. }
  25. @end // @interface StatusItemView
  26. @implementation StatusItemView
  27. - (void)dealloc {
  28. trayIcon_ = nil;
  29. menuController_ = nil;
  30. }
  31. - (id)initWithIcon:(electron::TrayIconCocoa*)icon {
  32. trayIcon_ = icon;
  33. menuController_ = nil;
  34. ignoreDoubleClickEvents_ = NO;
  35. if ((self = [super initWithFrame:CGRectZero])) {
  36. [self registerForDraggedTypes:@[
  37. #pragma clang diagnostic push
  38. #pragma clang diagnostic ignored "-Wdeprecated-declarations"
  39. NSFilenamesPboardType,
  40. #pragma clang diagnostic pop
  41. NSPasteboardTypeString,
  42. ]];
  43. // Create the status item.
  44. NSStatusItem* item = [[NSStatusBar systemStatusBar]
  45. statusItemWithLength:NSVariableStatusItemLength];
  46. statusItem_ = item;
  47. [[statusItem_ button] addSubview:self]; // inject custom view
  48. [self updateDimensions];
  49. }
  50. return self;
  51. }
  52. - (void)updateDimensions {
  53. [self setFrame:[statusItem_ button].frame];
  54. }
  55. - (void)updateTrackingAreas {
  56. // Use NSTrackingArea for listening to mouseEnter, mouseExit, and mouseMove
  57. // events.
  58. [self removeTrackingArea:trackingArea_];
  59. trackingArea_ = [[NSTrackingArea alloc]
  60. initWithRect:[self bounds]
  61. options:NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved |
  62. NSTrackingActiveAlways
  63. owner:self
  64. userInfo:nil];
  65. [self addTrackingArea:trackingArea_];
  66. }
  67. - (void)removeItem {
  68. // Turn off tracking events to prevent crash.
  69. if (trackingArea_) {
  70. [self removeTrackingArea:trackingArea_];
  71. trackingArea_ = nil;
  72. }
  73. [[NSStatusBar systemStatusBar] removeStatusItem:statusItem_];
  74. [self removeFromSuperview];
  75. statusItem_ = nil;
  76. }
  77. - (void)setImage:(NSImage*)image {
  78. [[statusItem_ button] setImage:image];
  79. [self updateDimensions];
  80. }
  81. - (void)setAlternateImage:(NSImage*)image {
  82. [[statusItem_ button] setAlternateImage:image];
  83. // We need to change the button type here because the default button type for
  84. // NSStatusItem, NSStatusBarButton, does not display alternate content when
  85. // clicked. NSButtonTypeMomentaryChange displays its alternate content when
  86. // clicked and returns to its normal content when the user releases it, which
  87. // is the behavior users would expect when clicking a button with an alternate
  88. // image set.
  89. [[statusItem_ button] setButtonType:NSButtonTypeMomentaryChange];
  90. [self updateDimensions];
  91. }
  92. - (void)setIgnoreDoubleClickEvents:(BOOL)ignore {
  93. ignoreDoubleClickEvents_ = ignore;
  94. }
  95. - (BOOL)getIgnoreDoubleClickEvents {
  96. return ignoreDoubleClickEvents_;
  97. }
  98. - (void)setTitle:(NSString*)title font_type:(NSString*)font_type {
  99. NSMutableAttributedString* attributed_title =
  100. [[NSMutableAttributedString alloc] initWithString:title];
  101. if ([title containsANSICodes]) {
  102. attributed_title = [title attributedStringParsingANSICodes];
  103. }
  104. // Change font type, if specified
  105. CGFloat existing_size = [[[statusItem_ button] font] pointSize];
  106. if ([font_type isEqualToString:@"monospaced"]) {
  107. NSDictionary* attributes = @{
  108. NSFontAttributeName :
  109. [NSFont monospacedSystemFontOfSize:existing_size
  110. weight:NSFontWeightRegular]
  111. };
  112. [attributed_title addAttributes:attributes
  113. range:NSMakeRange(0, [attributed_title length])];
  114. } else if ([font_type isEqualToString:@"monospacedDigit"]) {
  115. NSDictionary* attributes = @{
  116. NSFontAttributeName :
  117. [NSFont monospacedDigitSystemFontOfSize:existing_size
  118. weight:NSFontWeightRegular]
  119. };
  120. [attributed_title addAttributes:attributes
  121. range:NSMakeRange(0, [attributed_title length])];
  122. }
  123. // Set title
  124. [[statusItem_ button] setAttributedTitle:attributed_title];
  125. // Fix icon margins.
  126. if (title.length > 0) {
  127. [[statusItem_ button] setImagePosition:NSImageLeft];
  128. } else {
  129. [[statusItem_ button] setImagePosition:NSImageOnly];
  130. }
  131. [self updateDimensions];
  132. }
  133. - (NSString*)title {
  134. return [statusItem_ button].title;
  135. }
  136. - (void)setMenuController:(ElectronMenuController*)menu {
  137. menuController_ = menu;
  138. [statusItem_ setMenu:[menuController_ menu]];
  139. }
  140. - (void)handleClickNotifications:(NSEvent*)event {
  141. // If we are ignoring double click events, we should ignore the `clickCount`
  142. // value and immediately emit a click event.
  143. BOOL shouldBeHandledAsASingleClick =
  144. (event.clickCount == 1) || ignoreDoubleClickEvents_;
  145. if (shouldBeHandledAsASingleClick)
  146. trayIcon_->NotifyClicked(
  147. gfx::ScreenRectFromNSRect(event.window.frame),
  148. gfx::ScreenPointFromNSPoint([event locationInWindow]),
  149. ui::EventFlagsFromModifiers([event modifierFlags]));
  150. // Double click event.
  151. BOOL shouldBeHandledAsADoubleClick =
  152. (event.clickCount == 2) && !ignoreDoubleClickEvents_;
  153. if (shouldBeHandledAsADoubleClick)
  154. trayIcon_->NotifyDoubleClicked(
  155. gfx::ScreenRectFromNSRect(event.window.frame),
  156. ui::EventFlagsFromModifiers([event modifierFlags]));
  157. }
  158. - (void)mouseDown:(NSEvent*)event {
  159. trayIcon_->NotifyMouseDown(
  160. gfx::ScreenPointFromNSPoint([event locationInWindow]),
  161. ui::EventFlagsFromModifiers([event modifierFlags]));
  162. // Pass click to superclass to show menu if one exists and has a non-zero
  163. // number of items. Custom mouseUp handler won't be invoked in this case.
  164. if (menuController_ && [[menuController_ menu] numberOfItems] > 0) {
  165. [self handleClickNotifications:event];
  166. [super mouseDown:event];
  167. } else {
  168. [[statusItem_ button] highlight:YES];
  169. }
  170. }
  171. - (void)mouseUp:(NSEvent*)event {
  172. [[statusItem_ button] highlight:NO];
  173. trayIcon_->NotifyMouseUp(
  174. gfx::ScreenPointFromNSPoint([event locationInWindow]),
  175. ui::EventFlagsFromModifiers([event modifierFlags]));
  176. [self handleClickNotifications:event];
  177. }
  178. - (void)popUpContextMenu:(electron::ElectronMenuModel*)menu_model {
  179. // Make sure events can be pumped while the menu is up.
  180. base::CurrentThread::ScopedAllowApplicationTasksInNativeNestedLoop allow;
  181. // Show a custom menu.
  182. if (menu_model) {
  183. ElectronMenuController* menuController =
  184. [[ElectronMenuController alloc] initWithModel:menu_model
  185. useDefaultAccelerator:NO];
  186. // Hacky way to mimic design of ordinary tray menu.
  187. [statusItem_ setMenu:[menuController menu]];
  188. [[statusItem_ button] performClick:self];
  189. [statusItem_ setMenu:[menuController_ menu]];
  190. return;
  191. }
  192. if (menuController_ && ![menuController_ isMenuOpen]) {
  193. // Ensure the UI can update while the menu is fading out.
  194. base::ScopedPumpMessagesInPrivateModes pump_private;
  195. [[statusItem_ button] performClick:self];
  196. }
  197. }
  198. - (void)closeContextMenu {
  199. if (menuController_) {
  200. [menuController_ cancel];
  201. }
  202. }
  203. - (void)rightMouseUp:(NSEvent*)event {
  204. trayIcon_->NotifyRightClicked(
  205. gfx::ScreenRectFromNSRect(event.window.frame),
  206. ui::EventFlagsFromModifiers([event modifierFlags]));
  207. }
  208. - (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender {
  209. trayIcon_->NotifyDragEntered();
  210. return NSDragOperationCopy;
  211. }
  212. - (void)mouseExited:(NSEvent*)event {
  213. trayIcon_->NotifyMouseExited(
  214. gfx::ScreenPointFromNSPoint([event locationInWindow]),
  215. ui::EventFlagsFromModifiers([event modifierFlags]));
  216. }
  217. - (void)mouseEntered:(NSEvent*)event {
  218. trayIcon_->NotifyMouseEntered(
  219. gfx::ScreenPointFromNSPoint([event locationInWindow]),
  220. ui::EventFlagsFromModifiers([event modifierFlags]));
  221. }
  222. - (void)mouseMoved:(NSEvent*)event {
  223. trayIcon_->NotifyMouseMoved(
  224. gfx::ScreenPointFromNSPoint([event locationInWindow]),
  225. ui::EventFlagsFromModifiers([event modifierFlags]));
  226. }
  227. - (void)draggingExited:(id<NSDraggingInfo>)sender {
  228. trayIcon_->NotifyDragExited();
  229. }
  230. - (void)draggingEnded:(id<NSDraggingInfo>)sender {
  231. trayIcon_->NotifyDragEnded();
  232. if (NSPointInRect([sender draggingLocation], self.frame)) {
  233. trayIcon_->NotifyDrop();
  234. }
  235. }
  236. - (BOOL)handleDrop:(id<NSDraggingInfo>)sender {
  237. NSPasteboard* pboard = [sender draggingPasteboard];
  238. // TODO(codebytere): update to currently supported NSPasteboardTypeFileURL or
  239. // kUTTypeFileURL.
  240. #pragma clang diagnostic push
  241. #pragma clang diagnostic ignored "-Wdeprecated-declarations"
  242. if ([[pboard types] containsObject:NSFilenamesPboardType]) {
  243. std::vector<std::string> dropFiles;
  244. NSArray* files = [pboard propertyListForType:NSFilenamesPboardType];
  245. for (NSString* file in files)
  246. dropFiles.push_back(base::SysNSStringToUTF8(file));
  247. trayIcon_->NotifyDropFiles(dropFiles);
  248. return YES;
  249. } else if ([[pboard types] containsObject:NSPasteboardTypeString]) {
  250. NSString* dropText = [pboard stringForType:NSPasteboardTypeString];
  251. trayIcon_->NotifyDropText(base::SysNSStringToUTF8(dropText));
  252. return YES;
  253. }
  254. #pragma clang diagnostic pop
  255. return NO;
  256. }
  257. - (BOOL)prepareForDragOperation:(id<NSDraggingInfo>)sender {
  258. return YES;
  259. }
  260. - (BOOL)performDragOperation:(id<NSDraggingInfo>)sender {
  261. [self handleDrop:sender];
  262. return YES;
  263. }
  264. @end
  265. namespace electron {
  266. TrayIconCocoa::TrayIconCocoa() {
  267. status_item_view_ = [[StatusItemView alloc] initWithIcon:this];
  268. }
  269. TrayIconCocoa::~TrayIconCocoa() {
  270. [status_item_view_ removeItem];
  271. }
  272. void TrayIconCocoa::SetImage(const gfx::Image& image) {
  273. [status_item_view_ setImage:image.AsNSImage()];
  274. }
  275. void TrayIconCocoa::SetPressedImage(const gfx::Image& image) {
  276. [status_item_view_ setAlternateImage:image.AsNSImage()];
  277. }
  278. void TrayIconCocoa::SetToolTip(const std::string& tool_tip) {
  279. [status_item_view_ setToolTip:base::SysUTF8ToNSString(tool_tip)];
  280. }
  281. void TrayIconCocoa::SetTitle(const std::string& title,
  282. const TitleOptions& options) {
  283. [status_item_view_ setTitle:base::SysUTF8ToNSString(title)
  284. font_type:base::SysUTF8ToNSString(options.font_type)];
  285. }
  286. std::string TrayIconCocoa::GetTitle() {
  287. return base::SysNSStringToUTF8([status_item_view_ title]);
  288. }
  289. void TrayIconCocoa::SetIgnoreDoubleClickEvents(bool ignore) {
  290. [status_item_view_ setIgnoreDoubleClickEvents:ignore];
  291. }
  292. bool TrayIconCocoa::GetIgnoreDoubleClickEvents() {
  293. return [status_item_view_ getIgnoreDoubleClickEvents];
  294. }
  295. void TrayIconCocoa::PopUpOnUI(ElectronMenuModel* menu_model) {
  296. [status_item_view_ popUpContextMenu:menu_model];
  297. }
  298. void TrayIconCocoa::PopUpContextMenu(const gfx::Point& pos,
  299. raw_ptr<ElectronMenuModel> menu_model) {
  300. content::GetUIThreadTaskRunner({})->PostTask(
  301. FROM_HERE,
  302. base::BindOnce(&TrayIconCocoa::PopUpOnUI, weak_factory_.GetWeakPtr(),
  303. base::Unretained(menu_model)));
  304. }
  305. void TrayIconCocoa::CloseContextMenu() {
  306. [status_item_view_ closeContextMenu];
  307. }
  308. void TrayIconCocoa::SetContextMenu(raw_ptr<ElectronMenuModel> menu_model) {
  309. if (menu_model) {
  310. // Create native menu.
  311. menu_ = [[ElectronMenuController alloc] initWithModel:menu_model
  312. useDefaultAccelerator:NO];
  313. } else {
  314. menu_ = nil;
  315. }
  316. [status_item_view_ setMenuController:menu_];
  317. }
  318. gfx::Rect TrayIconCocoa::GetBounds() {
  319. return gfx::ScreenRectFromNSRect([status_item_view_ window].frame);
  320. }
  321. // static
  322. TrayIcon* TrayIcon::Create(absl::optional<UUID> guid) {
  323. return new TrayIconCocoa;
  324. }
  325. } // namespace electron