Don't show supervised user as "already on this device" while they're being imported.
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / extensions / browser_action_button.mm
blobbe27ff4129a8d8307dd18447ef5059bf43eba5b0
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #import "chrome/browser/ui/cocoa/extensions/browser_action_button.h"
7 #include <algorithm>
8 #include <cmath>
10 #include "base/logging.h"
11 #include "base/memory/weak_ptr.h"
12 #include "base/strings/sys_string_conversions.h"
13 #include "chrome/browser/profiles/profile.h"
14 #include "chrome/browser/ui/browser.h"
15 #include "chrome/browser/ui/browser_window.h"
16 #import "chrome/browser/ui/cocoa/browser_window_controller.h"
17 #import "chrome/browser/ui/cocoa/extensions/browser_actions_controller.h"
18 #import "chrome/browser/ui/cocoa/themed_window.h"
19 #import "chrome/browser/ui/cocoa/toolbar/toolbar_controller.h"
20 #import "chrome/browser/ui/cocoa/wrench_menu/wrench_menu_controller.h"
21 #include "chrome/browser/ui/toolbar/toolbar_action_view_controller.h"
22 #include "chrome/browser/ui/toolbar/toolbar_action_view_delegate.h"
23 #include "chrome/browser/ui/toolbar/toolbar_actions_bar.h"
24 #include "grit/theme_resources.h"
25 #include "skia/ext/skia_utils_mac.h"
26 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSAnimation+Duration.h"
27 #import "ui/base/cocoa/menu_controller.h"
28 #include "ui/gfx/canvas_skia_paint.h"
29 #include "ui/gfx/geometry/rect.h"
30 #include "ui/gfx/image/image.h"
31 #include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h"
33 NSString* const kBrowserActionButtonDraggingNotification =
34     @"BrowserActionButtonDraggingNotification";
35 NSString* const kBrowserActionButtonDragEndNotification =
36     @"BrowserActionButtonDragEndNotification";
38 static const CGFloat kBrowserActionBadgeOriginYOffset = 5;
39 static const CGFloat kAnimationDuration = 0.2;
40 static const CGFloat kMinimumDragDistance = 5;
42 @interface BrowserActionButton ()
43 - (void)endDrag;
44 - (void)updateHighlightedState;
45 - (MenuController*)contextMenuController;
46 - (void)menuDidClose:(NSNotification*)notification;
47 @end
49 // A class to bridge the ToolbarActionViewController and the
50 // BrowserActionButton.
51 class ToolbarActionViewDelegateBridge : public ToolbarActionViewDelegate {
52  public:
53   ToolbarActionViewDelegateBridge(BrowserActionButton* owner,
54                                   BrowserActionsController* controller,
55                                   ToolbarActionViewController* viewController);
56   ~ToolbarActionViewDelegateBridge() override;
58   // Shows the context menu for the owning action.
59   void ShowContextMenu();
61   bool user_shown_popup_visible() const { return user_shown_popup_visible_; }
63  private:
64   // ToolbarActionViewDelegate:
65   content::WebContents* GetCurrentWebContents() const override;
66   void UpdateState() override;
67   bool IsMenuRunning() const override;
68   void OnPopupShown(bool by_user) override;
69   void OnPopupClosed() override;
71   // A helper method to implement showing the context menu.
72   void DoShowContextMenu();
74   // The owning button. Weak.
75   BrowserActionButton* owner_;
77   // The BrowserActionsController that owns the button. Weak.
78   BrowserActionsController* controller_;
80   // The ToolbarActionViewController for which this is the delegate. Weak.
81   ToolbarActionViewController* viewController_;
83   // Whether or not a popup is visible from a user action.
84   bool user_shown_popup_visible_;
86   // Whether or not a context menu is running (or is in the process of opening).
87   bool contextMenuRunning_;
89   base::WeakPtrFactory<ToolbarActionViewDelegateBridge> weakFactory_;
91   DISALLOW_COPY_AND_ASSIGN(ToolbarActionViewDelegateBridge);
94 ToolbarActionViewDelegateBridge::ToolbarActionViewDelegateBridge(
95     BrowserActionButton* owner,
96     BrowserActionsController* controller,
97     ToolbarActionViewController* viewController)
98     : owner_(owner),
99       controller_(controller),
100       viewController_(viewController),
101       user_shown_popup_visible_(false),
102       contextMenuRunning_(false),
103       weakFactory_(this) {
104   viewController_->SetDelegate(this);
107 ToolbarActionViewDelegateBridge::~ToolbarActionViewDelegateBridge() {
108   viewController_->SetDelegate(nullptr);
111 void ToolbarActionViewDelegateBridge::ShowContextMenu() {
112   // We should only be showing the context menu in this way if we're doing so
113   // for an overflowed action.
114   DCHECK(![owner_ superview]);
116   contextMenuRunning_ = true;
117   WrenchMenuController* wrenchMenuController =
118       [[[BrowserWindowController browserWindowControllerForWindow:
119           [controller_ browser]->window()->GetNativeWindow()]
120               toolbarController] wrenchMenuController];
121   // If the wrench menu is open, we have to first close it. Part of this happens
122   // asynchronously, so we have to use a posted task to open the next menu.
123   if ([wrenchMenuController isMenuOpen])
124     [wrenchMenuController cancel];
126   [controller_ toolbarActionsBar]->PopOutAction(
127       viewController_,
128       base::Bind(&ToolbarActionViewDelegateBridge::DoShowContextMenu,
129                  weakFactory_.GetWeakPtr()));
132 content::WebContents* ToolbarActionViewDelegateBridge::GetCurrentWebContents()
133     const {
134   return [controller_ currentWebContents];
137 void ToolbarActionViewDelegateBridge::UpdateState() {
138   [owner_ updateState];
141 bool ToolbarActionViewDelegateBridge::IsMenuRunning() const {
142   MenuController* menuController = [owner_ contextMenuController];
143   return contextMenuRunning_ || (menuController && [menuController isMenuOpen]);
146 void ToolbarActionViewDelegateBridge::OnPopupShown(bool by_user) {
147   if (by_user)
148     user_shown_popup_visible_ = true;
149   [owner_ updateHighlightedState];
152 void ToolbarActionViewDelegateBridge::OnPopupClosed() {
153   user_shown_popup_visible_ = false;
154   [owner_ updateHighlightedState];
157 void ToolbarActionViewDelegateBridge::DoShowContextMenu() {
158   // The point the menu shows matches that of the normal wrench menu - that is,
159   // the right-left most corner of the menu is left-aligned with the wrench
160   // button, and the menu is displayed "a little bit" lower. It would be nice to
161   // be able to avoid the magic '5' here, but since it's built into Cocoa, it's
162   // not too hopeful.
163   NSPoint menuPoint = NSMakePoint(0, NSHeight([owner_ bounds]) + 5);
164   [[owner_ cell] setHighlighted:YES];
165   [[owner_ menu] popUpMenuPositioningItem:nil
166                                atLocation:menuPoint
167                                    inView:owner_];
168   [[owner_ cell] setHighlighted:NO];
169   contextMenuRunning_ = false;
170   // When the menu closed, the ViewController should have popped itself back in.
171   DCHECK(![controller_ toolbarActionsBar]->popped_out_action());
174 @interface BrowserActionCell (Internals)
175 - (void)drawBadgeWithinFrame:(NSRect)frame
176               forWebContents:(content::WebContents*)webContents;
177 @end
179 @implementation BrowserActionButton
181 @synthesize isBeingDragged = isBeingDragged_;
183 + (Class)cellClass {
184   return [BrowserActionCell class];
187 - (id)initWithFrame:(NSRect)frame
188      viewController:(ToolbarActionViewController*)viewController
189          controller:(BrowserActionsController*)controller {
190   if ((self = [super initWithFrame:frame])) {
191     BrowserActionCell* cell = [[[BrowserActionCell alloc] init] autorelease];
192     // [NSButton setCell:] warns to NOT use setCell: other than in the
193     // initializer of a control.  However, we are using a basic
194     // NSButton whose initializer does not take an NSCell as an
195     // object.  To honor the assumed semantics, we do nothing with
196     // NSButton between alloc/init and setCell:.
197     [self setCell:cell];
199     browserActionsController_ = controller;
200     viewController_ = viewController;
201     viewControllerDelegate_.reset(
202         new ToolbarActionViewDelegateBridge(self, controller, viewController));
204     [cell setBrowserActionsController:controller];
205     [cell setViewController:viewController_];
206     [cell
207         accessibilitySetOverrideValue:base::SysUTF16ToNSString(
208             viewController_->GetAccessibleName([controller currentWebContents]))
209         forAttribute:NSAccessibilityDescriptionAttribute];
210     [cell setImageID:IDR_BROWSER_ACTION
211       forButtonState:image_button_cell::kDefaultState];
212     [cell setImageID:IDR_BROWSER_ACTION_H
213       forButtonState:image_button_cell::kHoverState];
214     [cell setImageID:IDR_BROWSER_ACTION_P
215       forButtonState:image_button_cell::kPressedState];
216     [cell setImageID:IDR_BROWSER_ACTION
217       forButtonState:image_button_cell::kDisabledState];
219     [self setTitle:@""];
220     [self setButtonType:NSMomentaryChangeButton];
221     [self setShowsBorderOnlyWhileMouseInside:YES];
223     moveAnimation_.reset([[NSViewAnimation alloc] init]);
224     [moveAnimation_ gtm_setDuration:kAnimationDuration
225                           eventMask:NSLeftMouseUpMask];
226     [moveAnimation_ setAnimationBlockingMode:NSAnimationNonblocking];
228     [self updateState];
229   }
231   return self;
234 - (BOOL)acceptsFirstResponder {
235   return YES;
238 - (void)rightMouseDown:(NSEvent*)theEvent {
239   // Cocoa doesn't allow menus-running-in-menus, so in order to show the
240   // context menu for an overflowed action, we close the wrench menu and show
241   // the context menu over the wrench (similar to what we do for popups).
242   // Let the main bar's button handle showing the context menu, since the wrench
243   // menu will close..
244   if ([browserActionsController_ isOverflow]) {
245     [browserActionsController_ mainButtonForId:viewController_->GetId()]->
246         viewControllerDelegate_->ShowContextMenu();
247   } else {
248     [super rightMouseDown:theEvent];
249   }
252 - (void)mouseDown:(NSEvent*)theEvent {
253   NSPoint location = [self convertPoint:[theEvent locationInWindow]
254                                fromView:nil];
255   // We don't allow dragging in the overflow container because mouse events
256   // don't work well in menus in Cocoa. Specifically, the minute the mouse
257   // leaves the view, the view stops receiving events. This is bad, because the
258   // mouse can leave the view in many ways (user moves the mouse fast, user
259   // tries to drag the icon to a non-applicable place, like outside the menu,
260   // etc). When the mouse leaves, we get no indication (no mouseUp), so we can't
261   // even handle that case - and are left in the middle of a drag. Instead, we
262   // have to simply disable dragging.
263   //
264   // NOTE(devlin): If we use a greedy event loop that consumes all incoming
265   // events (i.e. using [NSWindow nextEventMatchingMask]), we can make this
266   // work. The downside to that is that all other events are lost. Disable this
267   // for now, and revisit it at a later date.
269   if (NSPointInRect(location, [self bounds]) &&
270       ![browserActionsController_ isOverflow]) {
271     dragCouldStart_ = YES;
272     dragStartPoint_ = [self convertPoint:[theEvent locationInWindow]
273                                 fromView:nil];
274     [self updateHighlightedState];
275   }
278 - (void)mouseDragged:(NSEvent*)theEvent {
279   if (!dragCouldStart_)
280     return;
282   NSPoint eventPoint = [theEvent locationInWindow];
283   if (!isBeingDragged_) {
284     // Don't initiate a drag until it moves at least kMinimumDragDistance.
285     NSPoint dragStart = [self convertPoint:dragStartPoint_ toView:nil];
286     CGFloat dx = eventPoint.x - dragStart.x;
287     CGFloat dy = eventPoint.y - dragStart.y;
288     if (dx*dx + dy*dy < kMinimumDragDistance*kMinimumDragDistance)
289       return;
291     // The start of a drag. Position the button above all others.
292     [[self superview] addSubview:self positioned:NSWindowAbove relativeTo:nil];
294     // We reset the |dragStartPoint_| so that the mouse can always be in the
295     // same point along the button's x axis, and we avoid a "jump" when first
296     // starting to drag.
297     dragStartPoint_ = [self convertPoint:eventPoint fromView:nil];
299     isBeingDragged_ = YES;
300   }
302   NSRect buttonFrame = [self frame];
303   // The desired x is the current mouse point, minus the original offset of the
304   // mouse into the button.
305   NSPoint localPoint = [[self superview] convertPoint:eventPoint fromView:nil];
306   CGFloat desiredX = localPoint.x - dragStartPoint_.x;
307   // Clamp the button to be within its superview along the X-axis.
308   NSRect containerBounds = [[self superview] bounds];
309   desiredX = std::min(std::max(NSMinX(containerBounds), desiredX),
310                       NSMaxX(containerBounds) - NSWidth(buttonFrame));
311   buttonFrame.origin.x = desiredX;
313   // If the button is in the overflow menu, it could move along the y-axis, too.
314   if ([browserActionsController_ isOverflow]) {
315     CGFloat desiredY = localPoint.y - dragStartPoint_.y;
316     desiredY = std::min(std::max(NSMinY(containerBounds), desiredY),
317                         NSMaxY(containerBounds) - NSHeight(buttonFrame));
318     buttonFrame.origin.y = desiredY;
319   }
321   [self setFrame:buttonFrame];
322   [self setNeedsDisplay:YES];
323   [[NSNotificationCenter defaultCenter]
324       postNotificationName:kBrowserActionButtonDraggingNotification
325       object:self];
328 - (void)mouseUp:(NSEvent*)theEvent {
329   dragCouldStart_ = NO;
330   // There are non-drag cases where a mouseUp: may happen
331   // (e.g. mouse-down, cmd-tab to another application, move mouse,
332   // mouse-up).
333   NSPoint location = [self convertPoint:[theEvent locationInWindow]
334                                fromView:nil];
335   if (NSPointInRect(location, [self bounds]) && !isBeingDragged_) {
336     // Only perform the click if we didn't drag the button.
337     [self performClick:self];
338   } else {
339     // Make sure an ESC to end a drag doesn't trigger 2 endDrags.
340     if (isBeingDragged_) {
341       [self endDrag];
342     } else {
343       [super mouseUp:theEvent];
344     }
345   }
346   [self updateHighlightedState];
349 - (void)endDrag {
350   isBeingDragged_ = NO;
351   [[NSNotificationCenter defaultCenter]
352       postNotificationName:kBrowserActionButtonDragEndNotification object:self];
355 - (void)updateHighlightedState {
356   // The button's cell is highlighted if either the popup is showing by a user
357   // action, or the user is about to drag the button, unless the button is
358   // overflowed (in which case it is never highlighted).
359   if ([self superview] && ![browserActionsController_ isOverflow]) {
360     BOOL highlighted = viewControllerDelegate_->user_shown_popup_visible() ||
361         dragCouldStart_;
362     [[self cell] setHighlighted:highlighted];
363   } else {
364     [[self cell] setHighlighted:NO];
365   }
368 - (MenuController*)contextMenuController {
369   return contextMenuController_.get();
372 - (void)menuDidClose:(NSNotification*)notification {
373   viewController_->OnContextMenuClosed();
376 - (void)setFrame:(NSRect)frameRect animate:(BOOL)animate {
377   if (!animate) {
378     [self setFrame:frameRect];
379   } else {
380     if ([moveAnimation_ isAnimating])
381       [moveAnimation_ stopAnimation];
383     NSDictionary* animationDictionary = @{
384       NSViewAnimationTargetKey : self,
385       NSViewAnimationStartFrameKey : [NSValue valueWithRect:[self frame]],
386       NSViewAnimationEndFrameKey : [NSValue valueWithRect:frameRect]
387     };
388     [moveAnimation_ setViewAnimations: @[ animationDictionary ]];
389     [moveAnimation_ startAnimation];
390   }
393 - (void)updateState {
394   content::WebContents* webContents =
395       [browserActionsController_ currentWebContents];
396   if (!webContents)
397     return;
399   if (viewController_->WantsToRun(webContents)) {
400     [[self cell] setImageID:IDR_BROWSER_ACTION_R
401         forButtonState:image_button_cell::kDefaultState];
402   } else {
403     [[self cell] setImageID:IDR_BROWSER_ACTION
404         forButtonState:image_button_cell::kDefaultState];
405   }
407   base::string16 tooltip = viewController_->GetTooltip(webContents);
408   [self setToolTip:(tooltip.empty() ? nil : base::SysUTF16ToNSString(tooltip))];
410   gfx::Image image = viewController_->GetIcon(webContents);
412   if (!image.IsEmpty())
413     [self setImage:image.ToNSImage()];
415   [self setEnabled:viewController_->IsEnabled(webContents)];
417   [self setNeedsDisplay:YES];
420 - (void)onRemoved {
421   // The button is being removed from the toolbar, and the backing controller
422   // will also be removed. Destroy the delegate.
423   // We only need to do this because in Cocoa's memory management, removing the
424   // button from the toolbar doesn't synchronously dealloc it.
425   viewControllerDelegate_.reset();
426   // Also reset the context menu, since it has a dependency on the backing
427   // controller (which owns its model).
428   contextMenuController_.reset();
431 - (BOOL)isAnimating {
432   return [moveAnimation_ isAnimating];
435 - (NSRect)frameAfterAnimation {
436   if ([moveAnimation_ isAnimating]) {
437     NSRect endFrame = [[[[moveAnimation_ viewAnimations] objectAtIndex:0]
438         valueForKey:NSViewAnimationEndFrameKey] rectValue];
439     return endFrame;
440   } else {
441     return [self frame];
442   }
445 - (ToolbarActionViewController*)viewController {
446   return viewController_;
449 - (NSImage*)compositedImage {
450   NSRect bounds = [self bounds];
451   NSImage* image = [[[NSImage alloc] initWithSize:bounds.size] autorelease];
452   [image lockFocus];
454   [[NSColor clearColor] set];
455   NSRectFill(bounds);
457   NSImage* actionImage = [self image];
458   const NSSize imageSize = [actionImage size];
459   const NSRect imageRect =
460       NSMakeRect(std::floor((NSWidth(bounds) - imageSize.width) / 2.0),
461                  std::floor((NSHeight(bounds) - imageSize.height) / 2.0),
462                  imageSize.width, imageSize.height);
463   [actionImage drawInRect:imageRect
464                  fromRect:NSZeroRect
465                 operation:NSCompositeSourceOver
466                  fraction:1.0
467            respectFlipped:YES
468                     hints:nil];
470   bounds.origin.y += kBrowserActionBadgeOriginYOffset;
471   [[self cell] drawBadgeWithinFrame:bounds
472                      forWebContents:
473                             [browserActionsController_ currentWebContents]];
475   [image unlockFocus];
476   return image;
479 - (NSMenu*)menu {
480   // Hack: Since Cocoa doesn't support menus-running-in-menus (see also comment
481   // in -rightMouseDown:), it doesn't launch the menu for an overflowed action
482   // on a Control-click. Even more unfortunate, it doesn't even pass us the
483   // mouseDown event for control clicks. However, it does call -menuForEvent:,
484   // which in turn calls -menu:, so we can tap in here and show the menu
485   // programmatically for the Control-click case.
486   if ([browserActionsController_ isOverflow] &&
487       ([NSEvent modifierFlags] & NSControlKeyMask)) {
488     [browserActionsController_ mainButtonForId:viewController_->GetId()]->
489         viewControllerDelegate_->ShowContextMenu();
490     return nil;
491   }
493   NSMenu* menu = nil;
494   if (testContextMenu_) {
495     menu = testContextMenu_;
496   } else {
497     // Make sure we delete any references to an old menu.
498     contextMenuController_.reset();
500     ui::MenuModel* contextMenu = viewController_->GetContextMenu();
501     if (contextMenu) {
502       contextMenuController_.reset(
503           [[MenuController alloc] initWithModel:contextMenu
504                          useWithPopUpButtonCell:NO]);
505       menu = [contextMenuController_ menu];
506     }
507   }
509   if (menu) {
510     [[NSNotificationCenter defaultCenter]
511         addObserver:self
512            selector:@selector(menuDidClose:)
513                name:NSMenuDidEndTrackingNotification
514              object:menu];
515   }
516   return menu;
519 #pragma mark -
520 #pragma mark Testing Methods
522 - (void)setTestContextMenu:(NSMenu*)testContextMenu {
523   testContextMenu_ = testContextMenu;
526 @end
528 @implementation BrowserActionCell
530 @synthesize browserActionsController = browserActionsController_;
531 @synthesize viewController = viewController_;
533 - (void)drawBadgeWithinFrame:(NSRect)frame
534               forWebContents:(content::WebContents*)webContents {
535   gfx::CanvasSkiaPaint canvas(frame, false);
536   canvas.set_composite_alpha(true);
537   gfx::Rect boundingRect(NSRectToCGRect(frame));
538   viewController_->PaintExtra(&canvas, boundingRect, webContents);
541 - (void)drawWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
542   gfx::ScopedNSGraphicsContextSaveGState scopedGState;
543   [super drawWithFrame:cellFrame inView:controlView];
544   DCHECK(viewController_);
545   content::WebContents* webContents =
546       [browserActionsController_ currentWebContents];
547   const NSSize imageSize = self.image.size;
548   const NSRect imageRect =
549       NSMakeRect(std::floor((NSWidth(cellFrame) - imageSize.width) / 2.0),
550                  std::floor((NSHeight(cellFrame) - imageSize.height) / 2.0),
551                  imageSize.width, imageSize.height);
552   [self.image drawInRect:imageRect
553                 fromRect:NSZeroRect
554                operation:NSCompositeSourceOver
555                 fraction:1.0
556           respectFlipped:YES
557                    hints:nil];
559   cellFrame.origin.y += kBrowserActionBadgeOriginYOffset;
560   [self drawBadgeWithinFrame:cellFrame
561               forWebContents:webContents];
564 - (ui::ThemeProvider*)themeProviderForWindow:(NSWindow*)window {
565   ui::ThemeProvider* themeProvider = [window themeProvider];
566   if (!themeProvider)
567     themeProvider =
568         [[browserActionsController_ browser]->window()->GetNativeWindow()
569             themeProvider];
570   return themeProvider;
573 @end