tray_icon_cocoa.mm 12 KB

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