diff --git a/atom/browser/api/atom_api_tray.cc b/atom/browser/api/atom_api_tray.cc index 649967a2b1eb..a209537c940f 100644 --- a/atom/browser/api/atom_api_tray.cc +++ b/atom/browser/api/atom_api_tray.cc @@ -60,6 +60,14 @@ void Tray::OnBalloonClosed() { Emit("balloon-closed"); } +void Tray::OnRightClicked(const gfx::Rect& bounds) { + Emit("right-clicked", bounds); +} + +void Tray::OnDropFiles(const std::vector& files) { + Emit("drop-files", files); +} + bool Tray::IsDestroyed() const { return !tray_icon_; } @@ -102,6 +110,12 @@ void Tray::DisplayBalloon(mate::Arguments* args, tray_icon_->DisplayBalloon(icon, title, content); } +void Tray::PopContextMenu(mate::Arguments* args) { + gfx::Point pos; + args->GetNext(&pos); + tray_icon_->PopContextMenu(pos); +} + void Tray::SetContextMenu(mate::Arguments* args, Menu* menu) { tray_icon_->SetContextMenu(menu->model()); } @@ -117,6 +131,7 @@ void Tray::BuildPrototype(v8::Isolate* isolate, .SetMethod("setTitle", &Tray::SetTitle) .SetMethod("setHighlightMode", &Tray::SetHighlightMode) .SetMethod("displayBalloon", &Tray::DisplayBalloon) + .SetMethod("popContextMenu", &Tray::PopContextMenu) .SetMethod("_setContextMenu", &Tray::SetContextMenu); } diff --git a/atom/browser/api/atom_api_tray.h b/atom/browser/api/atom_api_tray.h index 1a4a498d16b9..10ea3836819e 100644 --- a/atom/browser/api/atom_api_tray.h +++ b/atom/browser/api/atom_api_tray.h @@ -6,6 +6,7 @@ #define ATOM_BROWSER_API_ATOM_API_TRAY_H_ #include +#include #include "atom/browser/api/event_emitter.h" #include "atom/browser/ui/tray_icon_observer.h" @@ -41,11 +42,13 @@ class Tray : public mate::EventEmitter, virtual ~Tray(); // TrayIconObserver: - void OnClicked(const gfx::Rect&) override; + void OnClicked(const gfx::Rect& bounds) override; void OnDoubleClicked() override; void OnBalloonShow() override; void OnBalloonClicked() override; void OnBalloonClosed() override; + void OnRightClicked(const gfx::Rect& bounds) override; + void OnDropFiles(const std::vector& files) override; // mate::Wrappable: bool IsDestroyed() const override; @@ -57,6 +60,7 @@ class Tray : public mate::EventEmitter, void SetTitle(mate::Arguments* args, const std::string& title); void SetHighlightMode(mate::Arguments* args, bool highlight); void DisplayBalloon(mate::Arguments* args, const mate::Dictionary& options); + void PopContextMenu(mate::Arguments* args); void SetContextMenu(mate::Arguments* args, Menu* menu); private: diff --git a/atom/browser/ui/tray_icon.cc b/atom/browser/ui/tray_icon.cc index a3878f718a62..456cbe47a5a3 100644 --- a/atom/browser/ui/tray_icon.cc +++ b/atom/browser/ui/tray_icon.cc @@ -26,6 +26,9 @@ void TrayIcon::DisplayBalloon(const gfx::Image& icon, const base::string16& contents) { } +void TrayIcon::PopContextMenu(const gfx::Point& pos) { +} + void TrayIcon::NotifyClicked(const gfx::Rect& bounds) { FOR_EACH_OBSERVER(TrayIconObserver, observers_, OnClicked(bounds)); } @@ -46,4 +49,12 @@ void TrayIcon::NotifyBalloonClosed() { FOR_EACH_OBSERVER(TrayIconObserver, observers_, OnBalloonClosed()); } +void TrayIcon::NotifyRightClicked(const gfx::Rect& bounds) { + FOR_EACH_OBSERVER(TrayIconObserver, observers_, OnRightClicked(bounds)); +} + +void TrayIcon::NotfiyDropFiles(const std::vector& files) { + FOR_EACH_OBSERVER(TrayIconObserver, observers_, OnDropFiles(files)); +} + } // namespace atom diff --git a/atom/browser/ui/tray_icon.h b/atom/browser/ui/tray_icon.h index 7dc67da1bac4..d6885699d01d 100644 --- a/atom/browser/ui/tray_icon.h +++ b/atom/browser/ui/tray_icon.h @@ -6,6 +6,7 @@ #define ATOM_BROWSER_UI_TRAY_ICON_H_ #include +#include #include "atom/browser/ui/tray_icon_observer.h" #include "base/observer_list.h" @@ -46,6 +47,8 @@ class TrayIcon { const base::string16& title, const base::string16& contents); + virtual void PopContextMenu(const gfx::Point& pos); + // Set the context menu for this icon. virtual void SetContextMenu(ui::SimpleMenuModel* menu_model) = 0; @@ -56,6 +59,8 @@ class TrayIcon { void NotifyBalloonShow(); void NotifyBalloonClicked(); void NotifyBalloonClosed(); + void NotifyRightClicked(const gfx::Rect& bounds = gfx::Rect()); + void NotfiyDropFiles(const std::vector& files); protected: TrayIcon(); diff --git a/atom/browser/ui/tray_icon_cocoa.h b/atom/browser/ui/tray_icon_cocoa.h index 5723cb6b2196..9aa801ec5ead 100644 --- a/atom/browser/ui/tray_icon_cocoa.h +++ b/atom/browser/ui/tray_icon_cocoa.h @@ -13,7 +13,7 @@ #include "base/mac/scoped_nsobject.h" @class AtomMenuController; -@class StatusItemController; +@class StatusItemView; namespace atom { @@ -27,12 +27,12 @@ class TrayIconCocoa : public TrayIcon { void SetToolTip(const std::string& tool_tip) override; void SetTitle(const std::string& title) override; void SetHighlightMode(bool highlight) override; + void PopContextMenu(const gfx::Point& pos) override; void SetContextMenu(ui::SimpleMenuModel* menu_model) override; private: - base::scoped_nsobject item_; - - base::scoped_nsobject controller_; + // Atom custom view for NSStatusItem. + base::scoped_nsobject status_item_view_; // Status menu shown when right-clicking the system icon. base::scoped_nsobject menu_; diff --git a/atom/browser/ui/tray_icon_cocoa.mm b/atom/browser/ui/tray_icon_cocoa.mm index f989b9b580e2..fddbd8c16eb0 100644 --- a/atom/browser/ui/tray_icon_cocoa.mm +++ b/atom/browser/ui/tray_icon_cocoa.mm @@ -9,81 +9,242 @@ #include "ui/gfx/image/image.h" #include "ui/gfx/screen.h" -@interface StatusItemController : NSObject { +namespace { + +const CGFloat kStatusItemLength = 26; +const CGFloat kMargin = 3; + +} // namespace + +@interface StatusItemView : NSView { atom::TrayIconCocoa* trayIcon_; // weak + AtomMenuController* menuController_; // weak + BOOL isHighlightEnable_; + BOOL inMouseEventSequence_; + base::scoped_nsobject image_; + base::scoped_nsobject alternateImage_; + base::scoped_nsobject title_; + base::scoped_nsobject statusItem_; } -- (id)initWithIcon:(atom::TrayIconCocoa*)icon; -- (void)handleClick:(id)sender; -- (void)handleDoubleClick:(id)sender; -@end // @interface StatusItemController +@end // @interface StatusItemView -@implementation StatusItemController +@implementation StatusItemView - (id)initWithIcon:(atom::TrayIconCocoa*)icon { trayIcon_ = icon; + isHighlightEnable_ = YES; + statusItem_.reset([[[NSStatusBar systemStatusBar] statusItemWithLength: + NSVariableStatusItemLength] retain]); + NSRect frame = NSMakeRect(0, + 0, + kStatusItemLength, + [[statusItem_ statusBar] thickness]); + if ((self = [super initWithFrame:frame])) { + [self registerForDraggedTypes: + [NSArray arrayWithObjects:NSFilenamesPboardType, nil]]; + [statusItem_ setView:self]; + } return self; } -- (void)handleClick:(id)sender { - // Get the frame of the NSStatusItem. - NSRect frame = [NSApp currentEvent].window.frame; +- (void)removeItem { + [[NSStatusBar systemStatusBar] removeStatusItem:statusItem_]; + statusItem_.reset(); +} + +- (void)drawRect:(NSRect)dirtyRect { + // Draw the tray icon and title that align with NSSStatusItem, layout: + // ---------------- + // | icon | title | + /// ---------------- + BOOL highlight = [self shouldHighlight]; + CGFloat titleWidth = [self titleWidth]; + // Calculate the total icon bounds. + NSRect statusItemBounds = NSMakeRect(0, + 0, + kStatusItemLength + titleWidth, + [[statusItem_ statusBar] thickness]); + [statusItem_ drawStatusBarBackgroundInRect:statusItemBounds + withHighlight:highlight]; + [statusItem_ setLength:titleWidth + kStatusItemLength]; + if (title_) { + NSRect titleDrawRect = NSMakeRect(kStatusItemLength, + 0, + titleWidth + kStatusItemLength, + [[statusItem_ statusBar] thickness]); + [title_ drawInRect:titleDrawRect + withAttributes:[self titleAttributes]]; + } + + NSRect iconRect = NSMakeRect(0, + 0, + kStatusItemLength, + [[statusItem_ statusBar] thickness]); + if (highlight && alternateImage_) { + [alternateImage_ drawInRect:NSInsetRect(iconRect, kMargin, kMargin) + fromRect:NSZeroRect + operation:NSCompositeSourceOver + fraction:1]; + } else { + [image_ drawInRect:NSInsetRect(iconRect, kMargin, kMargin) + fromRect:NSZeroRect + operation:NSCompositeSourceOver + fraction:1]; + } +} + +- (CGFloat)titleWidth { + if (!title_) return 0; + NSAttributedString* attributes = + [[NSAttributedString alloc] initWithString:title_ + attributes:[self titleAttributes]]; + return [attributes size].width; +} + +- (NSDictionary*)titleAttributes { + NSFont* font = [NSFont menuBarFontOfSize:0]; + NSColor* foregroundColor = [NSColor blackColor]; + + return [NSDictionary dictionaryWithObjectsAndKeys: + font, NSFontAttributeName, + foregroundColor, NSForegroundColorAttributeName, + nil]; +} + +- (void)setImage:(NSImage*)image { + image_.reset([image copy]); + [self setNeedsDisplay:YES]; +} + +- (void)setAlternateImage:(NSImage*)image { + alternateImage_.reset([image copy]); +} + +- (void)setHighlight:(BOOL)highlight { + isHighlightEnable_ = highlight; +} + +- (void)setTitle:(NSString*)title { + title_.reset([title copy]); + [self setNeedsDisplay:YES]; +} + +- (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; + if (event.clickCount == 1) { + if (menuController_) { + [statusItem_ popUpStatusItemMenu:[menuController_ menu]]; + } + + trayIcon_->NotifyClicked([self getBoundsFromEvent:event]); + } + + if (event.clickCount == 2 && !menuController_) { + trayIcon_->NotifyDoubleClicked(); + } + [self setNeedsDisplay:YES]; +} + +- (void)popContextMenu { + if (menuController_ && ![menuController_ isMenuOpen]) { + // redraw the dray 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([self getBoundsFromEvent:event]); +} + +- (NSDragOperation)draggingEntered:(id )sender { + return NSDragOperationCopy; +} + +- (BOOL)performDragOperation:(id )sender { + NSPasteboard* pboard = [sender draggingPasteboard]; + + if ([[pboard types] containsObject:NSFilenamesPboardType]) { + std::vector dropFiles; + NSArray* files = [pboard propertyListForType:NSFilenamesPboardType]; + for (NSString* file in files) + dropFiles.push_back(base::SysNSStringToUTF8(file)); + trayIcon_->NotfiyDropFiles(dropFiles); + return YES; + } + return NO; +} + +- (BOOL)shouldHighlight { + BOOL is_menu_open = [menuController_ isMenuOpen]; + return isHighlightEnable_ && (inMouseEventSequence_ || is_menu_open); +} + +- (gfx::Rect)getBoundsFromEvent:(NSEvent*)event { + NSRect frame = event.window.frame; gfx::Rect bounds(frame.origin.x, 0, NSWidth(frame), NSHeight(frame)); - // Flip coordinates to gfx (0,0 in top-left corner) using current screen. NSScreen* screen = [[NSScreen screens] objectAtIndex:0]; bounds.set_y(NSHeight([screen frame]) - NSMaxY(frame)); - - trayIcon_->NotifyClicked(bounds); + return bounds; } - -- (void)handleDoubleClick:(id)sender { - trayIcon_->NotifyDoubleClicked(); -} - @end namespace atom { TrayIconCocoa::TrayIconCocoa() { - controller_.reset([[StatusItemController alloc] initWithIcon:this]); - - item_.reset([[[NSStatusBar systemStatusBar] - statusItemWithLength:NSVariableStatusItemLength] retain]); - [item_ setEnabled:YES]; - [item_ setTarget:controller_]; - [item_ setAction:@selector(handleClick:)]; - [item_ setDoubleAction:@selector(handleDoubleClick:)]; - [item_ setHighlightMode:YES]; + status_item_view_.reset([[StatusItemView alloc] initWithIcon:this]); } TrayIconCocoa::~TrayIconCocoa() { - // Remove the status item from the status bar. - [[NSStatusBar systemStatusBar] removeStatusItem:item_]; + [status_item_view_ removeItem]; } void TrayIconCocoa::SetImage(const gfx::Image& image) { - [item_ setImage:image.AsNSImage()]; + [status_item_view_ setImage:image.AsNSImage()]; } void TrayIconCocoa::SetPressedImage(const gfx::Image& image) { - [item_ setAlternateImage:image.AsNSImage()]; + [status_item_view_ setAlternateImage:image.AsNSImage()]; } void TrayIconCocoa::SetToolTip(const std::string& tool_tip) { - [item_ setToolTip:base::SysUTF8ToNSString(tool_tip)]; + [status_item_view_ setToolTip:base::SysUTF8ToNSString(tool_tip)]; } void TrayIconCocoa::SetTitle(const std::string& title) { - [item_ setTitle:base::SysUTF8ToNSString(title)]; + [status_item_view_ setTitle:base::SysUTF8ToNSString(title)]; } void TrayIconCocoa::SetHighlightMode(bool highlight) { - [item_ setHighlightMode:highlight]; + [status_item_view_ setHighlight:highlight]; +} + +void TrayIconCocoa::PopContextMenu(const gfx::Point& pos) { + [status_item_view_ popContextMenu]; } void TrayIconCocoa::SetContextMenu(ui::SimpleMenuModel* menu_model) { menu_.reset([[AtomMenuController alloc] initWithModel:menu_model]); - [item_ setMenu:[menu_ menu]]; + [status_item_view_ setMenuController:menu_.get()]; } // static diff --git a/atom/browser/ui/tray_icon_observer.h b/atom/browser/ui/tray_icon_observer.h index 3a34888b5318..6c9839a38a1f 100644 --- a/atom/browser/ui/tray_icon_observer.h +++ b/atom/browser/ui/tray_icon_observer.h @@ -5,6 +5,9 @@ #ifndef ATOM_BROWSER_UI_TRAY_ICON_OBSERVER_H_ #define ATOM_BROWSER_UI_TRAY_ICON_OBSERVER_H_ +#include +#include + namespace gfx { class Rect; } @@ -13,11 +16,13 @@ namespace atom { class TrayIconObserver { public: - virtual void OnClicked(const gfx::Rect&) {} + virtual void OnClicked(const gfx::Rect& bounds) {} virtual void OnDoubleClicked() {} virtual void OnBalloonShow() {} virtual void OnBalloonClicked() {} virtual void OnBalloonClosed() {} + virtual void OnRightClicked(const gfx::Rect& bounds) {} + virtual void OnDropFiles(const std::vector& files) {} protected: virtual ~TrayIconObserver() {} diff --git a/atom/browser/ui/win/notify_icon.cc b/atom/browser/ui/win/notify_icon.cc index 955a047fe1f5..bc9ff46f939a 100644 --- a/atom/browser/ui/win/notify_icon.cc +++ b/atom/browser/ui/win/notify_icon.cc @@ -47,38 +47,22 @@ NotifyIcon::~NotifyIcon() { void NotifyIcon::HandleClickEvent(const gfx::Point& cursor_pos, bool left_mouse_click) { + NOTIFYICONIDENTIFIER icon_id; + memset(&icon_id, 0, sizeof(NOTIFYICONIDENTIFIER)); + icon_id.uID = icon_id_; + icon_id.hWnd = window_; + icon_id.cbSize = sizeof(NOTIFYICONIDENTIFIER); + RECT rect = { 0 }; + Shell_NotifyIconGetRect(&icon_id, &rect); + // Pass to the observer if appropriate. if (left_mouse_click) { - NOTIFYICONIDENTIFIER icon_id; - memset(&icon_id, 0, sizeof(NOTIFYICONIDENTIFIER)); - icon_id.uID = icon_id_; - icon_id.hWnd = window_; - icon_id.cbSize = sizeof(NOTIFYICONIDENTIFIER); - - RECT rect = { 0 }; - Shell_NotifyIconGetRect(&icon_id, &rect); - NotifyClicked(gfx::Rect(rect)); return; } - if (!menu_model_) - return; - - // Set our window as the foreground window, so the context menu closes when - // we click away from it. - if (!SetForegroundWindow(window_)) - return; - - views::MenuRunner menu_runner( - menu_model_, - views::MenuRunner::CONTEXT_MENU | views::MenuRunner::HAS_MNEMONICS); - ignore_result(menu_runner.RunMenuAt( - NULL, - NULL, - gfx::Rect(cursor_pos, gfx::Size()), - views::MENU_ANCHOR_TOPLEFT, - ui::MENU_SOURCE_MOUSE)); + NotifyRightClicked(gfx::Rect(rect)); + PopContextMenu(cursor_pos); } void NotifyIcon::ResetIcon() { @@ -151,6 +135,23 @@ void NotifyIcon::DisplayBalloon(const gfx::Image& icon, LOG(WARNING) << "Unable to create status tray balloon."; } +void NotifyIcon::PopContextMenu(const gfx::Point& pos) { + // Set our window as the foreground window, so the context menu closes when + // we click away from it. + if (!SetForegroundWindow(window_)) + return; + + views::MenuRunner menu_runner( + menu_model_, + views::MenuRunner::CONTEXT_MENU | views::MenuRunner::HAS_MNEMONICS); + ignore_result(menu_runner.RunMenuAt( + NULL, + NULL, + gfx::Rect(pos, gfx::Size()), + views::MENU_ANCHOR_TOPLEFT, + ui::MENU_SOURCE_MOUSE)); +} + void NotifyIcon::SetContextMenu(ui::SimpleMenuModel* menu_model) { menu_model_ = menu_model; } diff --git a/atom/browser/ui/win/notify_icon.h b/atom/browser/ui/win/notify_icon.h index 12eea1fcf725..8e00f1267920 100644 --- a/atom/browser/ui/win/notify_icon.h +++ b/atom/browser/ui/win/notify_icon.h @@ -49,6 +49,7 @@ class NotifyIcon : public TrayIcon { void DisplayBalloon(const gfx::Image& icon, const base::string16& title, const base::string16& contents) override; + void PopContextMenu(const gfx::Point& pos) override; void SetContextMenu(ui::SimpleMenuModel* menu_model) override; private: diff --git a/docs/api/tray.md b/docs/api/tray.md index 25c9cb451b1d..30225dd6f3ed 100644 --- a/docs/api/tray.md +++ b/docs/api/tray.md @@ -25,7 +25,6 @@ app.on('ready', function(){ __Platform limitations:__ -* On OS X `clicked` event will be ignored if the tray icon has context menu. * On Linux app indicator will be used if it is supported, otherwise `GtkStatusIcon` will be used instead. * App indicator will only be showed when it has context menu. @@ -57,6 +56,20 @@ Emitted when the tray icon is clicked. __Note:__ The `bounds` payload is only implemented on OS X and Windows 7 or newer. +### Event: 'right-clicked' + +* `event` +* `bounds` Object - the bounds of tray icon + * `x` Integer + * `y` Integer + * `width` Integer + * `height` Integer + +Emitted when the tray icon is right clicked. + +__Note:__ This is only implemented on OS X and Windows. On Windows, this event +will be emitted if the tray icon has context menu. + ### Event: 'double-clicked' Emitted when the tray icon is double clicked. @@ -82,6 +95,15 @@ closes it. __Note:__ This is only implemented on Windows. +### Event: 'drop-files' + +* `event` +* `files` Array - the file path of dropped files. + +Emitted when dragged files are dropped in the tray icon. + +__Note:__ This is only implemented on OS X. + ### Tray.destroy() Destroys the tray icon immediately. @@ -131,6 +153,15 @@ Displays a tray balloon. __Note:__ This is only implemented on Windows. +### Tray.popContextMenu([position]) + +* `position` Object - The pop position + * `x` Integer + * `y` Integer + +__Note:__ This is only implemented on OS X and Windows. +The `position` is only available on Windows, and it is (0, 0) by default. + ### Tray.setContextMenu(menu) * `menu` Menu