tray_icon_cocoa.mm 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  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_apple.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];
  48. // We need to set the target and action on the button, otherwise
  49. // VoiceOver doesn't know where to send the select action.
  50. [[statusItem_ button] setTarget:self];
  51. [[statusItem_ button] setAction:@selector(mouseDown:)];
  52. [self updateDimensions];
  53. }
  54. return self;
  55. }
  56. - (void)updateDimensions {
  57. [self setFrame:[statusItem_ button].frame];
  58. }
  59. - (void)updateTrackingAreas {
  60. // Use NSTrackingArea for listening to mouseEnter, mouseExit, and mouseMove
  61. // events.
  62. [self removeTrackingArea:trackingArea_];
  63. trackingArea_ = [[NSTrackingArea alloc]
  64. initWithRect:[self bounds]
  65. options:NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved |
  66. NSTrackingActiveAlways
  67. owner:self
  68. userInfo:nil];
  69. [self addTrackingArea:trackingArea_];
  70. }
  71. - (void)removeItem {
  72. // Turn off tracking events to prevent crash.
  73. if (trackingArea_) {
  74. [self removeTrackingArea:trackingArea_];
  75. trackingArea_ = nil;
  76. }
  77. // Ensure any open menu is closed.
  78. if ([statusItem_ menu])
  79. [[statusItem_ menu] cancelTracking];
  80. [[NSStatusBar systemStatusBar] removeStatusItem:statusItem_];
  81. [self removeFromSuperview];
  82. statusItem_ = nil;
  83. }
  84. - (void)setImage:(NSImage*)image {
  85. [[statusItem_ button] setImage:image];
  86. [self updateDimensions];
  87. }
  88. - (void)setAlternateImage:(NSImage*)image {
  89. [[statusItem_ button] setAlternateImage:image];
  90. // We need to change the button type here because the default button type for
  91. // NSStatusItem, NSStatusBarButton, does not display alternate content when
  92. // clicked. NSButtonTypeMomentaryChange displays its alternate content when
  93. // clicked and returns to its normal content when the user releases it, which
  94. // is the behavior users would expect when clicking a button with an alternate
  95. // image set.
  96. [[statusItem_ button] setButtonType:NSButtonTypeMomentaryChange];
  97. [self updateDimensions];
  98. }
  99. - (void)setIgnoreDoubleClickEvents:(BOOL)ignore {
  100. ignoreDoubleClickEvents_ = ignore;
  101. }
  102. - (BOOL)getIgnoreDoubleClickEvents {
  103. return ignoreDoubleClickEvents_;
  104. }
  105. - (void)setTitle:(NSString*)title font_type:(NSString*)font_type {
  106. NSMutableAttributedString* attributed_title =
  107. [[NSMutableAttributedString alloc] initWithString:title];
  108. if ([title containsANSICodes]) {
  109. attributed_title = [title attributedStringParsingANSICodes];
  110. }
  111. // Change font type, if specified
  112. CGFloat existing_size = [[[statusItem_ button] font] pointSize];
  113. if ([font_type isEqualToString:@"monospaced"]) {
  114. NSDictionary* attributes = @{
  115. NSFontAttributeName :
  116. [NSFont monospacedSystemFontOfSize:existing_size
  117. weight:NSFontWeightRegular]
  118. };
  119. [attributed_title addAttributes:attributes
  120. range:NSMakeRange(0, [attributed_title length])];
  121. } else if ([font_type isEqualToString:@"monospacedDigit"]) {
  122. NSDictionary* attributes = @{
  123. NSFontAttributeName :
  124. [NSFont monospacedDigitSystemFontOfSize:existing_size
  125. weight:NSFontWeightRegular]
  126. };
  127. [attributed_title addAttributes:attributes
  128. range:NSMakeRange(0, [attributed_title length])];
  129. }
  130. // Set title
  131. [[statusItem_ button] setAttributedTitle:attributed_title];
  132. // Fix icon margins.
  133. if (title.length > 0) {
  134. [[statusItem_ button] setImagePosition:NSImageLeft];
  135. } else {
  136. [[statusItem_ button] setImagePosition:NSImageOnly];
  137. }
  138. [self updateDimensions];
  139. }
  140. - (NSString*)title {
  141. return [statusItem_ button].title;
  142. }
  143. - (void)setMenuController:(ElectronMenuController*)menu {
  144. menuController_ = menu;
  145. [statusItem_ setMenu:[menuController_ menu]];
  146. }
  147. - (void)handleClickNotifications:(NSEvent*)event {
  148. // If we are ignoring double click events, we should ignore the `clickCount`
  149. // value and immediately emit a click event.
  150. BOOL shouldBeHandledAsASingleClick =
  151. (event.clickCount == 1) || ignoreDoubleClickEvents_;
  152. if (shouldBeHandledAsASingleClick)
  153. trayIcon_->NotifyClicked(
  154. gfx::ScreenRectFromNSRect(event.window.frame),
  155. gfx::ScreenPointFromNSPoint([event locationInWindow]),
  156. ui::EventFlagsFromModifiers([event modifierFlags]));
  157. // Double click event.
  158. BOOL shouldBeHandledAsADoubleClick =
  159. (event.clickCount == 2) && !ignoreDoubleClickEvents_;
  160. if (shouldBeHandledAsADoubleClick)
  161. trayIcon_->NotifyDoubleClicked(
  162. gfx::ScreenRectFromNSRect(event.window.frame),
  163. ui::EventFlagsFromModifiers([event modifierFlags]));
  164. }
  165. - (void)mouseDown:(NSEvent*)event {
  166. // If |event| does not respond to locationInWindow, we've
  167. // arrived here from VoiceOver, which does not pass an event.
  168. // Create a synthetic event to pass to the click handler.
  169. if (![event respondsToSelector:@selector(locationInWindow)]) {
  170. event = [NSEvent mouseEventWithType:NSEventTypeRightMouseDown
  171. location:NSMakePoint(0, 0)
  172. modifierFlags:0
  173. timestamp:NSApp.currentEvent.timestamp
  174. windowNumber:0
  175. context:nil
  176. eventNumber:0
  177. clickCount:1
  178. pressure:1.0];
  179. // We also need to explicitly call the click handler here, since
  180. // VoiceOver won't trigger mouseUp.
  181. [self handleClickNotifications:event];
  182. }
  183. trayIcon_->NotifyMouseDown(
  184. gfx::ScreenPointFromNSPoint([event locationInWindow]),
  185. ui::EventFlagsFromModifiers([event modifierFlags]));
  186. // Pass click to superclass to show menu if one exists and has a non-zero
  187. // number of items. Custom mouseUp handler won't be invoked in this case.
  188. if (menuController_ && [[menuController_ menu] numberOfItems] > 0) {
  189. [self handleClickNotifications:event];
  190. [super mouseDown:event];
  191. } else {
  192. [[statusItem_ button] highlight:YES];
  193. }
  194. }
  195. - (void)mouseUp:(NSEvent*)event {
  196. [[statusItem_ button] highlight:NO];
  197. trayIcon_->NotifyMouseUp(
  198. gfx::ScreenPointFromNSPoint([event locationInWindow]),
  199. ui::EventFlagsFromModifiers([event modifierFlags]));
  200. [self handleClickNotifications:event];
  201. }
  202. - (void)popUpContextMenu:(electron::ElectronMenuModel*)menu_model {
  203. // Make sure events can be pumped while the menu is up.
  204. base::CurrentThread::ScopedAllowApplicationTasksInNativeNestedLoop allow;
  205. // Show a custom menu.
  206. if (menu_model) {
  207. ElectronMenuController* menuController =
  208. [[ElectronMenuController alloc] initWithModel:menu_model
  209. useDefaultAccelerator:NO];
  210. // Hacky way to mimic design of ordinary tray menu.
  211. [statusItem_ setMenu:[menuController menu]];
  212. base::WeakPtr<electron::TrayIconCocoa> weak_tray_icon =
  213. trayIcon_->GetWeakPtr();
  214. [[statusItem_ button] performClick:self];
  215. // /⚠️ \ Warning! Arbitrary JavaScript and who knows what else has been run
  216. // during -performClick:. This object may have been deleted.
  217. // We check if |trayIcon_| is still alive as it owns us and has the same
  218. // lifetime.
  219. if (!weak_tray_icon)
  220. return;
  221. [statusItem_ setMenu:[menuController_ menu]];
  222. return;
  223. }
  224. if (menuController_ && ![menuController_ isMenuOpen]) {
  225. // Ensure the UI can update while the menu is fading out.
  226. base::ScopedPumpMessagesInPrivateModes pump_private;
  227. [[statusItem_ button] performClick:self];
  228. }
  229. }
  230. - (void)closeContextMenu {
  231. if (menuController_) {
  232. [menuController_ cancel];
  233. }
  234. }
  235. - (void)rightMouseUp:(NSEvent*)event {
  236. trayIcon_->NotifyRightClicked(
  237. gfx::ScreenRectFromNSRect(event.window.frame),
  238. ui::EventFlagsFromModifiers([event modifierFlags]));
  239. }
  240. - (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender {
  241. trayIcon_->NotifyDragEntered();
  242. return NSDragOperationCopy;
  243. }
  244. - (void)mouseExited:(NSEvent*)event {
  245. trayIcon_->NotifyMouseExited(
  246. gfx::ScreenPointFromNSPoint([event locationInWindow]),
  247. ui::EventFlagsFromModifiers([event modifierFlags]));
  248. }
  249. - (void)mouseEntered:(NSEvent*)event {
  250. trayIcon_->NotifyMouseEntered(
  251. gfx::ScreenPointFromNSPoint([event locationInWindow]),
  252. ui::EventFlagsFromModifiers([event modifierFlags]));
  253. }
  254. - (void)mouseMoved:(NSEvent*)event {
  255. trayIcon_->NotifyMouseMoved(
  256. gfx::ScreenPointFromNSPoint([event locationInWindow]),
  257. ui::EventFlagsFromModifiers([event modifierFlags]));
  258. }
  259. - (void)draggingExited:(id<NSDraggingInfo>)sender {
  260. trayIcon_->NotifyDragExited();
  261. }
  262. - (void)draggingEnded:(id<NSDraggingInfo>)sender {
  263. trayIcon_->NotifyDragEnded();
  264. if (NSPointInRect([sender draggingLocation], self.frame)) {
  265. trayIcon_->NotifyDrop();
  266. }
  267. }
  268. - (BOOL)handleDrop:(id<NSDraggingInfo>)sender {
  269. NSPasteboard* pboard = [sender draggingPasteboard];
  270. // TODO(codebytere): update to currently supported NSPasteboardTypeFileURL or
  271. // kUTTypeFileURL.
  272. #pragma clang diagnostic push
  273. #pragma clang diagnostic ignored "-Wdeprecated-declarations"
  274. if ([[pboard types] containsObject:NSFilenamesPboardType]) {
  275. std::vector<std::string> dropFiles;
  276. NSArray* files = [pboard propertyListForType:NSFilenamesPboardType];
  277. for (NSString* file in files)
  278. dropFiles.push_back(base::SysNSStringToUTF8(file));
  279. trayIcon_->NotifyDropFiles(dropFiles);
  280. return YES;
  281. } else if ([[pboard types] containsObject:NSPasteboardTypeString]) {
  282. NSString* dropText = [pboard stringForType:NSPasteboardTypeString];
  283. trayIcon_->NotifyDropText(base::SysNSStringToUTF8(dropText));
  284. return YES;
  285. }
  286. #pragma clang diagnostic pop
  287. return NO;
  288. }
  289. - (BOOL)prepareForDragOperation:(id<NSDraggingInfo>)sender {
  290. return YES;
  291. }
  292. - (BOOL)performDragOperation:(id<NSDraggingInfo>)sender {
  293. [self handleDrop:sender];
  294. return YES;
  295. }
  296. @end
  297. namespace electron {
  298. TrayIconCocoa::TrayIconCocoa() {
  299. status_item_view_ = [[StatusItemView alloc] initWithIcon:this];
  300. }
  301. TrayIconCocoa::~TrayIconCocoa() {
  302. [status_item_view_ removeItem];
  303. }
  304. void TrayIconCocoa::SetImage(const gfx::Image& image) {
  305. [status_item_view_ setImage:image.AsNSImage()];
  306. }
  307. void TrayIconCocoa::SetPressedImage(const gfx::Image& image) {
  308. [status_item_view_ setAlternateImage:image.AsNSImage()];
  309. }
  310. void TrayIconCocoa::SetToolTip(const std::string& tool_tip) {
  311. [status_item_view_ setToolTip:base::SysUTF8ToNSString(tool_tip)];
  312. }
  313. void TrayIconCocoa::SetTitle(const std::string& title,
  314. const TitleOptions& options) {
  315. [status_item_view_ setTitle:base::SysUTF8ToNSString(title)
  316. font_type:base::SysUTF8ToNSString(options.font_type)];
  317. }
  318. std::string TrayIconCocoa::GetTitle() {
  319. return base::SysNSStringToUTF8([status_item_view_ title]);
  320. }
  321. void TrayIconCocoa::SetIgnoreDoubleClickEvents(bool ignore) {
  322. [status_item_view_ setIgnoreDoubleClickEvents:ignore];
  323. }
  324. bool TrayIconCocoa::GetIgnoreDoubleClickEvents() {
  325. return [status_item_view_ getIgnoreDoubleClickEvents];
  326. }
  327. void TrayIconCocoa::PopUpOnUI(base::WeakPtr<ElectronMenuModel> menu_model) {
  328. [status_item_view_ popUpContextMenu:menu_model.get()];
  329. }
  330. void TrayIconCocoa::PopUpContextMenu(
  331. const gfx::Point& pos,
  332. base::WeakPtr<ElectronMenuModel> menu_model) {
  333. content::GetUIThreadTaskRunner({})->PostTask(
  334. FROM_HERE, base::BindOnce(&TrayIconCocoa::PopUpOnUI,
  335. weak_factory_.GetWeakPtr(), menu_model));
  336. }
  337. void TrayIconCocoa::CloseContextMenu() {
  338. [status_item_view_ closeContextMenu];
  339. }
  340. void TrayIconCocoa::SetContextMenu(raw_ptr<ElectronMenuModel> menu_model) {
  341. if (menu_model) {
  342. // Create native menu.
  343. menu_ = [[ElectronMenuController alloc] initWithModel:menu_model
  344. useDefaultAccelerator:NO];
  345. } else {
  346. menu_ = nil;
  347. }
  348. [status_item_view_ setMenuController:menu_];
  349. }
  350. gfx::Rect TrayIconCocoa::GetBounds() {
  351. return gfx::ScreenRectFromNSRect([status_item_view_ window].frame);
  352. }
  353. // static
  354. TrayIcon* TrayIcon::Create(std::optional<UUID> guid) {
  355. return new TrayIconCocoa;
  356. }
  357. } // namespace electron