Check USB device path access when prompting users to select a device.
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / bookmarks / bookmark_button.mm
blob2dd4798ca2cc2787c282291401c3df2f506d4a42
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/bookmarks/bookmark_button.h"
7 #include <cmath>
9 #include "base/logging.h"
10 #include "base/mac/foundation_util.h"
11 #import "base/mac/scoped_nsobject.h"
12 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window.h"
13 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell.h"
14 #import "chrome/browser/ui/cocoa/browser_window_controller.h"
15 #import "chrome/browser/ui/cocoa/view_id_util.h"
16 #include "components/bookmarks/browser/bookmark_model.h"
17 #include "content/public/browser/user_metrics.h"
18 #import "ui/base/cocoa/nsview_additions.h"
19 #include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h"
21 using base::UserMetricsAction;
22 using bookmarks::BookmarkNode;
24 // The opacity of the bookmark button drag image.
25 static const CGFloat kDragImageOpacity = 0.7;
27 namespace bookmark_button {
29 NSString* const kPulseBookmarkButtonNotification =
30     @"PulseBookmarkButtonNotification";
31 NSString* const kBookmarkKey = @"BookmarkKey";
32 NSString* const kBookmarkPulseFlagKey = @"BookmarkPulseFlagKey";
36 namespace {
37 // We need a class variable to track the current dragged button to enable
38 // proper live animated dragging behavior, and can't do it in the
39 // delegate/controller since you can drag a button from one domain to the
40 // other (from a "folder" menu, to the main bar, or vice versa).
41 BookmarkButton* gDraggedButton = nil; // Weak
44 @interface BookmarkButton(Private)
46 // Make a drag image for the button.
47 - (NSImage*)dragImage;
49 - (void)installCustomTrackingArea;
51 @end  // @interface BookmarkButton(Private)
54 @implementation BookmarkButton
56 @synthesize delegate = delegate_;
57 @synthesize acceptsTrackIn = acceptsTrackIn_;
59 - (id)initWithFrame:(NSRect)frameRect {
60   // BookmarkButton's ViewID may be changed to VIEW_ID_OTHER_BOOKMARKS in
61   // BookmarkBarController, so we can't just override -viewID method to return
62   // it.
63   if ((self = [super initWithFrame:frameRect])) {
64     view_id_util::SetID(self, VIEW_ID_BOOKMARK_BAR_ELEMENT);
65     [self installCustomTrackingArea];
66   }
67   return self;
70 - (void)dealloc {
71   if ([[self cell] respondsToSelector:@selector(safelyStopPulsing)])
72     [[self cell] safelyStopPulsing];
73   view_id_util::UnsetID(self);
75   if (area_) {
76     [self removeTrackingArea:area_];
77     [area_ release];
78   }
80   [super dealloc];
83 - (const BookmarkNode*)bookmarkNode {
84   return [[self cell] bookmarkNode];
87 - (BOOL)isFolder {
88   const BookmarkNode* node = [self bookmarkNode];
89   return (node && node->is_folder());
92 - (BOOL)isEmpty {
93   return [self bookmarkNode] ? NO : YES;
96 - (void)setIsContinuousPulsing:(BOOL)flag {
97   [[self cell] setIsContinuousPulsing:flag];
100 - (BOOL)isContinuousPulsing {
101   return [[self cell] isContinuousPulsing];
104 - (NSPoint)screenLocationForRemoveAnimation {
105   NSPoint point;
107   if (dragPending_) {
108     // Use the position of the mouse in the drag image as the location.
109     point = dragEndScreenLocation_;
110     point.x += dragMouseOffset_.x;
111     if ([self isFlipped]) {
112       point.y += [self bounds].size.height - dragMouseOffset_.y;
113     } else {
114       point.y += dragMouseOffset_.y;
115     }
116   } else {
117     // Use the middle of this button as the location.
118     NSRect bounds = [self bounds];
119     point = NSMakePoint(NSMidX(bounds), NSMidY(bounds));
120     point = [self convertPoint:point toView:nil];
121     point = [[self window] convertBaseToScreen:point];
122   }
124   return point;
128 - (void)updateTrackingAreas {
129   [self installCustomTrackingArea];
130   [super updateTrackingAreas];
133 - (DraggableButtonResult)deltaIndicatesDragStartWithXDelta:(float)xDelta
134                                                     yDelta:(float)yDelta
135                                                xHysteresis:(float)xHysteresis
136                                                yHysteresis:(float)yHysteresis
137                                                  indicates:(BOOL*)result {
138   const float kDownProportion = 1.4142135f; // Square root of 2.
140   // We want to show a folder menu when you drag down on folder buttons,
141   // so don't classify this as a drag for that case.
142   if ([self isFolder] &&
143       (yDelta <= -yHysteresis) &&  // Bottom of hysteresis box was hit.
144       (std::abs(yDelta) / std::abs(xDelta)) >= kDownProportion) {
145     *result = NO;
146     return kDraggableButtonMixinDidWork;
147   }
149   return kDraggableButtonImplUseBase;
153 // By default, NSButton ignores middle-clicks.
154 // But we want them.
155 - (void)otherMouseUp:(NSEvent*)event {
156   [self performClick:self];
159 - (BOOL)acceptsTrackInFrom:(id)sender {
160   return  [self isFolder] || [self acceptsTrackIn];
164 // Overridden from DraggableButton.
165 - (void)beginDrag:(NSEvent*)event {
166   // Don't allow a drag of the empty node.
167   // The empty node is a placeholder for "(empty)", to be revisited.
168   if ([self isEmpty])
169     return;
171   if (![self delegate]) {
172     NOTREACHED();
173     return;
174   }
176   if ([self isFolder]) {
177     // Close the folder's drop-down menu if it's visible.
178     [[self target] closeBookmarkFolder:self];
179   }
181   // At the moment, moving bookmarks causes their buttons (like me!)
182   // to be destroyed and rebuilt.  Make sure we don't go away while on
183   // the stack.
184   [self retain];
186   // Ask our delegate to fill the pasteboard for us.
187   NSPasteboard* pboard = [NSPasteboard pasteboardWithName:NSDragPboard];
188   [[self delegate] fillPasteboard:pboard forDragOfButton:self];
190   // Lock bar visibility, forcing the overlay to stay visible if we are in
191   // fullscreen mode.
192   if ([[self delegate] dragShouldLockBarVisibility]) {
193     DCHECK(!visibilityDelegate_);
194     NSWindow* window = [[self delegate] browserWindow];
195     visibilityDelegate_ =
196         [BrowserWindowController browserWindowControllerForWindow:window];
197     [visibilityDelegate_ lockBarVisibilityForOwner:self
198                                      withAnimation:NO
199                                              delay:NO];
200   }
201   const BookmarkNode* node = [self bookmarkNode];
202   const BookmarkNode* parent = node ? node->parent() : NULL;
203   if (parent && parent->type() == BookmarkNode::FOLDER) {
204     content::RecordAction(UserMetricsAction("BookmarkBarFolder_DragStart"));
205   } else {
206     content::RecordAction(UserMetricsAction("BookmarkBar_DragStart"));
207   }
209   dragMouseOffset_ = [self convertPoint:[event locationInWindow] fromView:nil];
210   dragPending_ = YES;
211   gDraggedButton = self;
213   CGFloat yAt = [self bounds].size.height;
214   NSSize dragOffset = NSMakeSize(0.0, 0.0);
215   NSImage* image = [self dragImage];
216   [self setHidden:YES];
217   [self dragImage:image at:NSMakePoint(0, yAt) offset:dragOffset
218             event:event pasteboard:pboard source:self slideBack:YES];
219   [self setHidden:NO];
221   // And we're done.
222   dragPending_ = NO;
223   gDraggedButton = nil;
225   [self autorelease];
228 // Overridden to release bar visibility.
229 - (DraggableButtonResult)endDrag {
230   gDraggedButton = nil;
232   // visibilityDelegate_ can be nil if we're detached, and that's fine.
233   [visibilityDelegate_ releaseBarVisibilityForOwner:self
234                                       withAnimation:YES
235                                               delay:YES];
236   visibilityDelegate_ = nil;
238   return kDraggableButtonImplUseBase;
241 - (NSDragOperation)draggingSourceOperationMaskForLocal:(BOOL)isLocal {
242   NSDragOperation operation = NSDragOperationCopy;
243   if (isLocal) {
244     operation |= NSDragOperationMove;
245   }
246   if ([delegate_ canDragBookmarkButtonToTrash:self]) {
247     operation |= NSDragOperationDelete;
248   }
249   return operation;
252 - (void)draggedImage:(NSImage *)anImage
253              endedAt:(NSPoint)aPoint
254            operation:(NSDragOperation)operation {
255   gDraggedButton = nil;
256   // Inform delegate of drag source that we're finished dragging,
257   // so it can close auto-opened bookmark folders etc.
258   [delegate_ bookmarkDragDidEnd:self
259                       operation:operation];
260   // Tell delegate if it should delete us.
261   if (operation & NSDragOperationDelete) {
262     dragEndScreenLocation_ = aPoint;
263     [delegate_ didDragBookmarkToTrash:self];
264   }
267 - (DraggableButtonResult)performMouseDownAction:(NSEvent*)theEvent {
268   int eventMask = NSLeftMouseUpMask | NSMouseEnteredMask | NSMouseExitedMask |
269       NSLeftMouseDraggedMask;
271   BOOL keepGoing = YES;
272   [[self target] performSelector:[self action] withObject:self];
273   self.draggableButton.actionHasFired = YES;
275   DraggableButton* insideBtn = nil;
277   while (keepGoing) {
278     theEvent = [[self window] nextEventMatchingMask:eventMask];
279     if (!theEvent)
280       continue;
282     NSPoint mouseLoc = [self convertPoint:[theEvent locationInWindow]
283                                  fromView:nil];
284     BOOL isInside = [self mouse:mouseLoc inRect:[self bounds]];
286     switch ([theEvent type]) {
287       case NSMouseEntered:
288       case NSMouseExited: {
289         NSView* trackedView = (NSView*)[[theEvent trackingArea] owner];
290         if (trackedView && [trackedView isKindOfClass:[self class]]) {
291           BookmarkButton* btn = static_cast<BookmarkButton*>(trackedView);
292           if (![btn acceptsTrackInFrom:self])
293             break;
294           if ([theEvent type] == NSMouseEntered) {
295             [[NSCursor arrowCursor] set];
296             [[btn cell] mouseEntered:theEvent];
297             insideBtn = btn;
298           } else {
299             [[btn cell] mouseExited:theEvent];
300             if (insideBtn == btn)
301               insideBtn = nil;
302           }
303         }
304         break;
305       }
306       case NSLeftMouseDragged: {
307         if (insideBtn)
308           [insideBtn mouseDragged:theEvent];
309         break;
310       }
311       case NSLeftMouseUp: {
312         self.draggableButton.durationMouseWasDown =
313             [theEvent timestamp] - self.draggableButton.whenMouseDown;
314         if (!isInside && insideBtn && insideBtn != self) {
315           // Has tracked onto another BookmarkButton menu item, and released,
316           // so fire its action.
317           [[insideBtn target] performSelector:[insideBtn action]
318                                    withObject:insideBtn];
320         } else {
321           [self secondaryMouseUpAction:isInside];
322           [[self cell] mouseExited:theEvent];
323           [[insideBtn cell] mouseExited:theEvent];
324         }
325         keepGoing = NO;
326         break;
327       }
328       default:
329         /* Ignore any other kind of event. */
330         break;
331     }
332   }
333   return kDraggableButtonMixinDidWork;
338 // mouseEntered: and mouseExited: are called from our
339 // BookmarkButtonCell.  We redirect this information to our delegate.
340 // The controller can then perform menu-like actions (e.g. "hover over
341 // to open menu").
342 - (void)mouseEntered:(NSEvent*)event {
343   [delegate_ mouseEnteredButton:self event:event];
346 // See comments above mouseEntered:.
347 - (void)mouseExited:(NSEvent*)event {
348   [delegate_ mouseExitedButton:self event:event];
351 - (void)mouseMoved:(NSEvent*)theEvent {
352   if ([delegate_ respondsToSelector:@selector(mouseMoved:)])
353     [id(delegate_) mouseMoved:theEvent];
356 - (void)mouseDragged:(NSEvent*)theEvent {
357   if ([delegate_ respondsToSelector:@selector(mouseDragged:)])
358     [id(delegate_) mouseDragged:theEvent];
361 - (void)rightMouseDown:(NSEvent*)event {
362   // Ensure that right-clicking on a button while a context menu is open
363   // highlights the new button.
364   GradientButtonCell* cell =
365       base::mac::ObjCCastStrict<GradientButtonCell>([self cell]);
366   [delegate_ mouseEnteredButton:self event:event];
367   [cell setMouseInside:YES animate:YES];
369   // Keep a ref to |self|, in case -rightMouseDown: deletes this bookmark.
370   base::scoped_nsobject<BookmarkButton> keepAlive([self retain]);
371   [super rightMouseDown:event];
373   if (![cell isMouseReallyInside]) {
374     [cell setMouseInside:NO animate:YES];
375     [delegate_ mouseExitedButton:self event:event];
376   }
379 + (BookmarkButton*)draggedButton {
380   return gDraggedButton;
383 - (BOOL)canBecomeKeyView {
384   if (![super canBecomeKeyView])
385     return NO;
387   // If button is an item in a folder menu, don't become key.
388   return ![[self cell] isFolderButtonCell];
391 // This only gets called after a click that wasn't a drag, and only on folders.
392 - (DraggableButtonResult)secondaryMouseUpAction:(BOOL)wasInside {
393   const NSTimeInterval kShortClickLength = 0.5;
394   // Long clicks that end over the folder button result in the menu hiding.
395   if (wasInside &&
396       self.draggableButton.durationMouseWasDown > kShortClickLength) {
397     [[self target] performSelector:[self action] withObject:self];
398   } else {
399     // Mouse tracked out of button during menu track. Hide menus.
400     if (!wasInside)
401       [delegate_ bookmarkDragDidEnd:self
402                           operation:NSDragOperationNone];
403   }
404   return kDraggableButtonMixinDidWork;
407 - (BOOL)isOpaque {
408   // Make this control opaque so that sub-pixel anti-aliasing works when
409   // CoreAnimation is enabled.
410   return YES;
413 - (void)drawRect:(NSRect)rect {
414   NSView* bookmarkBarToolbarView = [[self superview] superview];
415   [self cr_drawUsingAncestor:bookmarkBarToolbarView inRect:(NSRect)rect];
416   [super drawRect:rect];
419 - (void)viewDidMoveToWindow {
420   [super viewDidMoveToWindow];
421   if ([self window]) {
422     // The new window may have different main window status.
423     // This happens when the view is moved into a TabWindowOverlayWindow for
424     // tab dragging.
425     [self windowDidChangeActive];
426   }
429 // ThemedWindowDrawing implementation.
431 - (void)windowDidChangeTheme {
432   [self setNeedsDisplay:YES];
435 - (void)windowDidChangeActive {
436   [self setNeedsDisplay:YES];
439 @end
441 @implementation BookmarkButton(Private)
444 - (void)installCustomTrackingArea {
445   const NSTrackingAreaOptions options =
446       NSTrackingActiveAlways |
447       NSTrackingMouseEnteredAndExited |
448       NSTrackingEnabledDuringMouseDrag;
450   if (area_) {
451     [self removeTrackingArea:area_];
452     [area_ release];
453   }
455   area_ = [[NSTrackingArea alloc] initWithRect:[self bounds]
456                                        options:options
457                                          owner:self
458                                       userInfo:nil];
459   [self addTrackingArea:area_];
463 - (NSImage*)dragImage {
464   NSRect bounds = [self bounds];
465   base::scoped_nsobject<NSImage> image(
466       [[NSImage alloc] initWithSize:bounds.size]);
467   [image lockFocusFlipped:[self isFlipped]];
469   NSGraphicsContext* context = [NSGraphicsContext currentContext];
470   CGContextRef cgContext = static_cast<CGContextRef>([context graphicsPort]);
471   CGContextBeginTransparencyLayer(cgContext, 0);
472   CGContextSetAlpha(cgContext, kDragImageOpacity);
474   GradientButtonCell* cell =
475       base::mac::ObjCCastStrict<GradientButtonCell>([self cell]);
476   [[cell clipPathForFrame:bounds inView:self] setClip];
477   [cell drawWithFrame:bounds inView:self];
479   CGContextEndTransparencyLayer(cgContext);
480   [image unlockFocus];
482   return image.autorelease();
485 @end  // @implementation BookmarkButton(Private)