Upstream version 7.36.149.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / ui / cocoa / bookmarks / bookmark_button.mm
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.
4
5 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_button.h"
6
7 #include <cmath>
8
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/nsview_additions.h"
16 #import "chrome/browser/ui/cocoa/view_id_util.h"
17 #include "components/bookmarks/core/browser/bookmark_model.h"
18 #include "content/public/browser/user_metrics.h"
19 #include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h"
20
21 using base::UserMetricsAction;
22
23 // The opacity of the bookmark button drag image.
24 static const CGFloat kDragImageOpacity = 0.7;
25
26
27 namespace bookmark_button {
28
29 NSString* const kPulseBookmarkButtonNotification =
30     @"PulseBookmarkButtonNotification";
31 NSString* const kBookmarkKey = @"BookmarkKey";
32 NSString* const kBookmarkPulseFlagKey = @"BookmarkPulseFlagKey";
33
34 };
35
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
42 };
43
44 @interface BookmarkButton(Private)
45
46 // Make a drag image for the button.
47 - (NSImage*)dragImage;
48
49 - (void)installCustomTrackingArea;
50
51 @end  // @interface BookmarkButton(Private)
52
53
54 @implementation BookmarkButton
55
56 @synthesize delegate = delegate_;
57 @synthesize acceptsTrackIn = acceptsTrackIn_;
58
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;
68 }
69
70 - (void)dealloc {
71   if ([[self cell] respondsToSelector:@selector(safelyStopPulsing)])
72     [[self cell] safelyStopPulsing];
73   view_id_util::UnsetID(self);
74
75   if (area_) {
76     [self removeTrackingArea:area_];
77     [area_ release];
78   }
79
80   [super dealloc];
81 }
82
83 - (const BookmarkNode*)bookmarkNode {
84   return [[self cell] bookmarkNode];
85 }
86
87 - (BOOL)isFolder {
88   const BookmarkNode* node = [self bookmarkNode];
89   return (node && node->is_folder());
90 }
91
92 - (BOOL)isEmpty {
93   return [self bookmarkNode] ? NO : YES;
94 }
95
96 - (void)setIsContinuousPulsing:(BOOL)flag {
97   [[self cell] setIsContinuousPulsing:flag];
98 }
99
100 - (BOOL)isContinuousPulsing {
101   return [[self cell] isContinuousPulsing];
102 }
103
104 - (NSPoint)screenLocationForRemoveAnimation {
105   NSPoint point;
106
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   }
123
124   return point;
125 }
126
127
128 - (void)updateTrackingAreas {
129   [self installCustomTrackingArea];
130   [super updateTrackingAreas];
131 }
132
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.
139
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   }
148
149   return kDraggableButtonImplUseBase;
150 }
151
152
153 // By default, NSButton ignores middle-clicks.
154 // But we want them.
155 - (void)otherMouseUp:(NSEvent*)event {
156   [self performClick:self];
157 }
158
159 - (BOOL)acceptsTrackInFrom:(id)sender {
160   return  [self isFolder] || [self acceptsTrackIn];
161 }
162
163
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;
170
171   if (![self delegate]) {
172     NOTREACHED();
173     return;
174   }
175
176   if ([self isFolder]) {
177     // Close the folder's drop-down menu if it's visible.
178     [[self target] closeBookmarkFolder:self];
179   }
180
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];
185
186   // Ask our delegate to fill the pasteboard for us.
187   NSPasteboard* pboard = [NSPasteboard pasteboardWithName:NSDragPboard];
188   [[self delegate] fillPasteboard:pboard forDragOfButton:self];
189
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   }
208
209   dragMouseOffset_ = [self convertPoint:[event locationInWindow] fromView:nil];
210   dragPending_ = YES;
211   gDraggedButton = self;
212
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];
220
221   // And we're done.
222   dragPending_ = NO;
223   gDraggedButton = nil;
224
225   [self autorelease];
226 }
227
228 // Overridden to release bar visibility.
229 - (DraggableButtonResult)endDrag {
230   gDraggedButton = nil;
231
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;
237
238   return kDraggableButtonImplUseBase;
239 }
240
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;
250 }
251
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   }
265 }
266
267 - (DraggableButtonResult)performMouseDownAction:(NSEvent*)theEvent {
268   int eventMask = NSLeftMouseUpMask | NSMouseEnteredMask | NSMouseExitedMask |
269       NSLeftMouseDraggedMask;
270
271   BOOL keepGoing = YES;
272   [[self target] performSelector:[self action] withObject:self];
273   self.draggableButton.actionHasFired = YES;
274
275   DraggableButton* insideBtn = nil;
276
277   while (keepGoing) {
278     theEvent = [[self window] nextEventMatchingMask:eventMask];
279     if (!theEvent)
280       continue;
281
282     NSPoint mouseLoc = [self convertPoint:[theEvent locationInWindow]
283                                  fromView:nil];
284     BOOL isInside = [self mouse:mouseLoc inRect:[self bounds]];
285
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];
319
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;
334 }
335
336
337
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];
344 }
345
346 // See comments above mouseEntered:.
347 - (void)mouseExited:(NSEvent*)event {
348   [delegate_ mouseExitedButton:self event:event];
349 }
350
351 - (void)mouseMoved:(NSEvent*)theEvent {
352   if ([delegate_ respondsToSelector:@selector(mouseMoved:)])
353     [id(delegate_) mouseMoved:theEvent];
354 }
355
356 - (void)mouseDragged:(NSEvent*)theEvent {
357   if ([delegate_ respondsToSelector:@selector(mouseDragged:)])
358     [id(delegate_) mouseDragged:theEvent];
359 }
360
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];
368
369   // Keep a ref to |self|, in case -rightMouseDown: deletes this bookmark.
370   base::scoped_nsobject<BookmarkButton> keepAlive([self retain]);
371   [super rightMouseDown:event];
372
373   if (![cell isMouseReallyInside]) {
374     [cell setMouseInside:NO animate:YES];
375     [delegate_ mouseExitedButton:self event:event];
376   }
377 }
378
379 + (BookmarkButton*)draggedButton {
380   return gDraggedButton;
381 }
382
383 - (BOOL)canBecomeKeyView {
384   if (![super canBecomeKeyView])
385     return NO;
386
387   // If button is an item in a folder menu, don't become key.
388   return ![[self cell] isFolderButtonCell];
389 }
390
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;
405 }
406
407 - (BOOL)isOpaque {
408   // Make this control opaque so that sub-pixel anti-aliasing works when
409   // CoreAnimation is enabled.
410   return YES;
411 }
412
413 - (void)drawRect:(NSRect)rect {
414   NSView* bookmarkBarToolbarView = [[self superview] superview];
415   [self cr_drawUsingAncestor:bookmarkBarToolbarView inRect:(NSRect)rect];
416   [super drawRect:rect];
417 }
418
419 @end
420
421 @implementation BookmarkButton(Private)
422
423
424 - (void)installCustomTrackingArea {
425   const NSTrackingAreaOptions options =
426       NSTrackingActiveAlways |
427       NSTrackingMouseEnteredAndExited |
428       NSTrackingEnabledDuringMouseDrag;
429
430   if (area_) {
431     [self removeTrackingArea:area_];
432     [area_ release];
433   }
434
435   area_ = [[NSTrackingArea alloc] initWithRect:[self bounds]
436                                        options:options
437                                          owner:self
438                                       userInfo:nil];
439   [self addTrackingArea:area_];
440 }
441
442
443 - (NSImage*)dragImage {
444   NSRect bounds = [self bounds];
445   base::scoped_nsobject<NSImage> image(
446       [[NSImage alloc] initWithSize:bounds.size]);
447   [image lockFocusFlipped:[self isFlipped]];
448
449   NSGraphicsContext* context = [NSGraphicsContext currentContext];
450   CGContextRef cgContext = static_cast<CGContextRef>([context graphicsPort]);
451   CGContextBeginTransparencyLayer(cgContext, 0);
452   CGContextSetAlpha(cgContext, kDragImageOpacity);
453
454   GradientButtonCell* cell =
455       base::mac::ObjCCastStrict<GradientButtonCell>([self cell]);
456   [[cell clipPathForFrame:bounds inView:self] setClip];
457   [cell drawWithFrame:bounds inView:self];
458
459   CGContextEndTransparencyLayer(cgContext);
460   [image unlockFocus];
461
462   return image.autorelease();
463 }
464
465 @end  // @implementation BookmarkButton(Private)