Popular sites on the NTP: check that experiment group StartsWith (rather than IS...
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / download / download_shelf_controller.mm
blobfafbb78d8d59b9a92fa75c743133c378e3d56390
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/download/download_shelf_controller.h"
7 #include "base/mac/bundle_locations.h"
8 #include "base/strings/sys_string_conversions.h"
9 #include "chrome/browser/download/download_stats.h"
10 #include "chrome/browser/profiles/profile.h"
11 #include "chrome/browser/themes/theme_service.h"
12 #include "chrome/browser/themes/theme_service_factory.h"
13 #include "chrome/browser/ui/browser.h"
14 #include "chrome/browser/ui/chrome_pages.h"
15 #import "chrome/browser/ui/cocoa/animatable_view.h"
16 #include "chrome/browser/ui/cocoa/browser_window_cocoa.h"
17 #import "chrome/browser/ui/cocoa/browser_window_controller.h"
18 #include "chrome/browser/ui/cocoa/download/download_item_controller.h"
19 #include "chrome/browser/ui/cocoa/download/download_shelf_mac.h"
20 #import "chrome/browser/ui/cocoa/download/download_shelf_view_cocoa.h"
21 #import "chrome/browser/ui/cocoa/presentation_mode_controller.h"
22 #include "content/public/browser/download_item.h"
23 #include "content/public/browser/download_manager.h"
24 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSAnimation+Duration.h"
25 #import "ui/base/cocoa/hover_button.h"
27 using content::DownloadItem;
29 // Download shelf autoclose behavior:
31 // The download shelf autocloses if all of this is true:
32 // 1) An item on the shelf has just been opened or removed.
33 // 2) All remaining items on the shelf have been opened in the past.
34 // 3) The mouse leaves the shelf and remains off the shelf for 5 seconds.
36 // If the mouse re-enters the shelf within the 5 second grace period, the
37 // autoclose is canceled.  An autoclose can only be scheduled in response to a
38 // shelf item being opened or removed.  If an item is opened and then the
39 // resulting autoclose is canceled, subsequent mouse exited events will NOT
40 // trigger an autoclose.
42 // If the shelf is manually closed while a download is still in progress, that
43 // download is marked as "opened" for these purposes.  If the shelf is later
44 // reopened, these previously-in-progress download will not block autoclose,
45 // even if that download was never actually clicked on and opened.
47 namespace {
49 // Max number of download views we'll contain. Any time a view is added and
50 // we already have this many download views, one is removed.
51 const size_t kMaxDownloadItemCount = 16;
53 // Horizontal padding between two download items.
54 const int kDownloadItemPadding = 0;
56 // Duration for the open-new-leftmost-item animation, in seconds.
57 const NSTimeInterval kDownloadItemOpenDuration = 0.8;
59 // Duration for download shelf closing animation, in seconds.
60 const NSTimeInterval kDownloadShelfCloseDuration = 0.12;
62 // Amount of time between when the mouse is moved off the shelf and the shelf is
63 // autoclosed, in seconds.
64 const NSTimeInterval kAutoCloseDelaySeconds = 5;
66 // The size of the x button by default.
67 const NSSize kHoverCloseButtonDefaultSize = { 18, 18 };
69 }  // namespace
71 @interface DownloadShelfController(Private)
72 - (void)removeDownload:(DownloadItemController*)download
73         isShelfClosing:(BOOL)isShelfClosing;
74 - (void)layoutItems:(BOOL)skipFirst;
75 - (void)closed;
76 - (void)maybeAutoCloseAfterDelay;
77 - (void)scheduleAutoClose;
78 - (void)cancelAutoClose;
79 - (void)autoClose;
80 - (void)viewFrameDidChange:(NSNotification*)notification;
81 - (void)installTrackingArea;
82 - (void)removeTrackingArea;
83 - (void)willEnterFullscreen;
84 - (void)willLeaveFullscreen;
85 - (void)updateCloseButton;
86 @end
89 @implementation DownloadShelfController
91 - (id)initWithBrowser:(Browser*)browser
92        resizeDelegate:(id<ViewResizer>)resizeDelegate {
93   if ((self = [super initWithNibName:@"DownloadShelf"
94                               bundle:base::mac::FrameworkBundle()])) {
95     resizeDelegate_ = resizeDelegate;
96     maxShelfHeight_ = NSHeight([[self view] bounds]);
97     currentShelfHeight_ = maxShelfHeight_;
98     if (browser && browser->window())
99       isFullscreen_ = browser->window()->IsFullscreen();
100     else
101       isFullscreen_ = NO;
103     // Reset the download shelf's frame height to zero.  It will be properly
104     // positioned and sized the first time we try to set its height. (Just
105     // setting the rect to NSZeroRect does not work: it confuses Cocoa's view
106     // layout logic. If the shelf's width is too small, cocoa makes the download
107     // item container view wider than the browser window).
108     NSRect frame = [[self view] frame];
109     frame.size.height = 0;
110     [[self view] setFrame:frame];
112     downloadItemControllers_.reset([[NSMutableArray alloc] init]);
114     bridge_.reset(new DownloadShelfMac(browser, self));
115     navigator_ = browser;
116   }
117   return self;
120 - (void)awakeFromNib {
121   DCHECK(hoverCloseButton_);
123   NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
124   [[self animatableView] setResizeDelegate:resizeDelegate_];
125   [[self view] setPostsFrameChangedNotifications:YES];
126   [defaultCenter addObserver:self
127                     selector:@selector(viewFrameDidChange:)
128                         name:NSViewFrameDidChangeNotification
129                       object:[self view]];
131   // These notifications are declared in fullscreen_controller, and are posted
132   // without objects.
133   [defaultCenter addObserver:self
134                     selector:@selector(willEnterFullscreen)
135                         name:kWillEnterFullscreenNotification
136                       object:nil];
137   [defaultCenter addObserver:self
138                     selector:@selector(willLeaveFullscreen)
139                         name:kWillLeaveFullscreenNotification
140                       object:nil];
141   [self installTrackingArea];
144 - (void)dealloc {
145   [self browserWillBeDestroyed];
146   [super dealloc];
149 - (void)browserWillBeDestroyed {
150   [[NSNotificationCenter defaultCenter] removeObserver:self];
152   // We need to explicitly release our download controllers here since they need
153   // to remove themselves as observers before the remaining shutdown happens.
154   [[self animatableView] stopAnimation];
155   [self removeTrackingArea];
156   [self cancelAutoClose];
157   while ([downloadItemControllers_ count] > 0) {
158     [self removeDownload:[downloadItemControllers_ lastObject]
159           isShelfClosing:YES];
160   }
161   downloadItemControllers_.reset();
163   bridge_.reset();
164   navigator_ = nullptr;
167 // Called after the frame's rect has changed; usually when the height is
168 // animated.
169 - (void)viewFrameDidChange:(NSNotification*)notification {
170   // Anchor subviews at the top of |view|, so that it looks like the shelf
171   // is sliding out.
172   CGFloat newShelfHeight = NSHeight([[self view] frame]);
173   if (newShelfHeight == currentShelfHeight_)
174     return;
176   for (NSView* view in [[self view] subviews]) {
177     NSRect frame = [view frame];
178     frame.origin.y -= currentShelfHeight_ - newShelfHeight;
179     [view setFrame:frame];
180   }
181   currentShelfHeight_ = newShelfHeight;
184 - (AnimatableView*)animatableView {
185   return static_cast<AnimatableView*>([self view]);
188 - (IBAction)showDownloadsTab:(id)sender {
189   chrome::ShowDownloads(bridge_->browser());
192 - (IBAction)handleClose:(id)sender {
193   bridge_->Close(DownloadShelf::USER_ACTION);
196 - (void)remove:(DownloadItemController*)download {
197   [self removeDownload:download
198         isShelfClosing:NO];
201 - (void)removeDownload:(DownloadItemController*)download
202         isShelfClosing:(BOOL)isShelfClosing {
203   // Look for the download in our controller array and remove it. This will
204   // explicity release it so that it removes itself as an Observer of the
205   // DownloadItem. We don't want to wait for autorelease since the DownloadItem
206   // we are observing will likely be gone by then.
207   [[NSNotificationCenter defaultCenter] removeObserver:download];
209   // TODO(dmaclach): Remove -- http://crbug.com/25845
210   [[download view] removeFromSuperview];
212   [downloadItemControllers_ removeObject:download];
214   if (!isShelfClosing) {
215     [self layoutItems];
217     // If there are no more downloads or if all the remaining downloads have
218     // been opened, we can close the shelf.
219     [self maybeAutoCloseAfterDelay];
220   }
223 - (void)downloadWasOpened:(DownloadItemController*)item_controller {
224   // This should only be called on the main thead.
225   DCHECK([NSThread isMainThread]);
226   [self maybeAutoCloseAfterDelay];
229 - (void)showDownloadShelf:(BOOL)show
230              isUserAction:(BOOL)isUserAction {
231   [self cancelAutoClose];
232   shouldCloseOnMouseExit_ = NO;
234   if ([self isVisible] == show)
235     return;
237   if (!show) {
238     int numInProgress = 0;
239     for (NSUInteger i = 0; i < [downloadItemControllers_ count]; ++i) {
240       DownloadItem* item = [[downloadItemControllers_ objectAtIndex:i]download];
241       if (item->GetState() == DownloadItem::IN_PROGRESS)
242         ++numInProgress;
243     }
244     RecordDownloadShelfClose(
245         [downloadItemControllers_ count], numInProgress, !isUserAction);
246   }
248   // Animate the shelf out, but not in.
249   // TODO(rohitrao): We do not animate on the way in because Cocoa is already
250   // doing a lot of work to set up the download arrow animation.  I've chosen to
251   // do no animation over janky animation.  Find a way to make animating in
252   // smoother.
253   AnimatableView* view = [self animatableView];
254   if (show) {
255     [view setHeight:maxShelfHeight_];
256     [view setHidden:NO];
257   } else {
258     [view animateToNewHeight:0 duration:kDownloadShelfCloseDuration];
259   }
261   barIsVisible_ = show;
262   [self updateCloseButton];
265 - (DownloadShelf*)bridge {
266   return bridge_.get();
269 - (BOOL)isVisible {
270   return barIsVisible_;
273 - (void)animationDidEnd:(NSAnimation*)animation {
274   if (![self isVisible]) {
275     [self closed];
276     [[self view] setHidden:YES];  // So that it doesn't appear in AX hierarchy.
277   }
280 - (float)height {
281   return maxShelfHeight_;
284 // If |skipFirst| is true, the frame of the leftmost item is not set.
285 - (void)layoutItems:(BOOL)skipFirst {
286   CGFloat currentX = 0;
287   for (DownloadItemController* itemController
288       in downloadItemControllers_.get()) {
289     NSRect frame = [[itemController view] frame];
290     frame.origin.x = currentX;
291     frame.size.width = [itemController preferredSize].width;
292     if (!skipFirst)
293       [[[itemController view] animator] setFrame:frame];
294     currentX += frame.size.width + kDownloadItemPadding;
295     skipFirst = NO;
296   }
299 - (void)layoutItems {
300   [self layoutItems:NO];
303 - (void)addDownloadItem:(DownloadItem*)downloadItem {
304   DCHECK([NSThread isMainThread]);
305   base::scoped_nsobject<DownloadItemController> controller(
306       [[DownloadItemController alloc] initWithDownload:downloadItem
307                                                  shelf:self
308                                              navigator:navigator_]);
309   [self add:controller.get()];
312 - (void)add:(DownloadItemController*)controller {
313   DCHECK([NSThread isMainThread]);
314   [self cancelAutoClose];
315   shouldCloseOnMouseExit_ = NO;
317   // Insert new item at the left.
318   // Adding at index 0 in NSMutableArrays is O(1).
319   [downloadItemControllers_ insertObject:controller atIndex:0];
321   [itemContainerView_ addSubview:[controller view]];
323   // The controller is in charge of removing itself as an observer in its
324   // dealloc.
325   [[NSNotificationCenter defaultCenter]
326     addObserver:controller
327        selector:@selector(updateVisibility:)
328            name:NSViewFrameDidChangeNotification
329          object:[controller view]];
330   [[NSNotificationCenter defaultCenter]
331     addObserver:controller
332        selector:@selector(updateVisibility:)
333            name:NSViewFrameDidChangeNotification
334          object:itemContainerView_];
336   // Start at width 0...
337   NSSize size = [controller preferredSize];
338   NSRect frame = NSMakeRect(0, 0, 0, size.height);
339   [[controller view] setFrame:frame];
341   // ...then animate in
342   frame.size.width = size.width;
343   [NSAnimationContext beginGrouping];
344   [[NSAnimationContext currentContext]
345       gtm_setDuration:kDownloadItemOpenDuration
346             eventMask:NSLeftMouseUpMask];
347   [[[controller view] animator] setFrame:frame];
348   [NSAnimationContext endGrouping];
350   // Keep only a limited number of items in the shelf.
351   if ([downloadItemControllers_ count] > kMaxDownloadItemCount) {
352     DCHECK(kMaxDownloadItemCount > 0);
354     // Since no user will ever see the item being removed (needs a horizontal
355     // screen resolution greater than 3200 at 16 items at 200 pixels each),
356     // there's no point in animating the removal.
357     [self removeDownload:[downloadItemControllers_ lastObject]
358           isShelfClosing:NO];
359   }
361   // Finally, move the remaining items to the right. Skip the first item when
362   // laying out the items, so that the longer animation duration we set up above
363   // is not overwritten.
364   [self layoutItems:YES];
367 - (void)closed {
368   // Don't remove completed downloads if the shelf is just being auto-hidden
369   // rather than explicitly closed by the user.
370   if (bridge_->is_hidden())
371     return;
372   NSUInteger i = 0;
373   while (i < [downloadItemControllers_ count]) {
374     DownloadItemController* itemController =
375         [downloadItemControllers_ objectAtIndex:i];
376     DownloadItem* download = [itemController download];
377     DownloadItem::DownloadState state = download->GetState();
378     bool isTransferDone = state == DownloadItem::COMPLETE ||
379                           state == DownloadItem::CANCELLED ||
380                           state == DownloadItem::INTERRUPTED;
381     if (isTransferDone && !download->IsDangerous()) {
382       [self removeDownload:itemController
383             isShelfClosing:YES];
384     } else {
385       // Treat the item as opened when we close. This way if we get shown again
386       // the user need not open this item for the shelf to auto-close.
387       download->SetOpened(true);
388       ++i;
389     }
390   }
393 - (void)mouseEntered:(NSEvent*)event {
394   isMouseInsideView_ = YES;
395   // If the mouse re-enters the download shelf, cancel the auto-close.  Further
396   // mouse exits should not trigger autoclose.
397   if (shouldCloseOnMouseExit_) {
398     [self cancelAutoClose];
399     shouldCloseOnMouseExit_ = NO;
400   }
403 - (void)mouseExited:(NSEvent*)event {
404   isMouseInsideView_ = NO;
405   if (shouldCloseOnMouseExit_)
406     [self scheduleAutoClose];
409 - (void)scheduleAutoClose {
410   // Cancel any previous hide requests, just to be safe.
411   [self cancelAutoClose];
413   // Schedule an autoclose after a delay.  If the mouse is moved back into the
414   // view, or if an item is added to the shelf, the timer will be canceled.
415   [self performSelector:@selector(autoClose)
416              withObject:nil
417              afterDelay:kAutoCloseDelaySeconds];
420 - (void)cancelAutoClose {
421   [NSObject cancelPreviousPerformRequestsWithTarget:self
422                                            selector:@selector(autoClose)
423                                              object:nil];
426 - (void)maybeAutoCloseAfterDelay {
427   // We can close the shelf automatically if all the downloads on the shelf have
428   // been opened.
429   for (NSUInteger i = 0; i < [downloadItemControllers_ count]; ++i) {
430     DownloadItemController* itemController =
431         [downloadItemControllers_ objectAtIndex:i];
432     if (![itemController download]->GetOpened())
433       return;
434   }
436   if ([self isVisible] && [downloadItemControllers_ count] > 0 &&
437       isMouseInsideView_) {
438     // If there are download items on the shelf and the user is potentially stil
439     // interacting with them, schedule an auto close after the user moves the
440     // mouse off the shelf.
441     shouldCloseOnMouseExit_ = YES;
442   } else {
443     // We notify the DownloadShelf of our intention to close even if the shelf
444     // is currently hidden. If the shelf was temporarily hidden (e.g. because
445     // the browser window entered fullscreen mode), then this prevents the shelf
446     // from being shown again when the browser exits fullscreen mode.
447     [self autoClose];
448   }
451 - (void)autoClose {
452   bridge_->Close(DownloadShelf::AUTOMATIC);
455 - (void)installTrackingArea {
456   // Install the tracking area to listen for mouseEntered and mouseExited
457   // messages.
458   DCHECK(!trackingArea_.get());
460   trackingArea_.reset([[CrTrackingArea alloc]
461                         initWithRect:[[self view] bounds]
462                              options:NSTrackingMouseEnteredAndExited |
463                                      NSTrackingActiveAlways |
464                                      NSTrackingInVisibleRect
465                                owner:self
466                             userInfo:nil]);
467   [[self view] addTrackingArea:trackingArea_.get()];
470 - (void)removeTrackingArea {
471   if (trackingArea_.get()) {
472     [[self view] removeTrackingArea:trackingArea_.get()];
473     trackingArea_.reset(nil);
474   }
477 - (void)willEnterFullscreen {
478   isFullscreen_ = YES;
479   [self updateCloseButton];
482 - (void)willLeaveFullscreen {
483   isFullscreen_ = NO;
484   [self updateCloseButton];
487 - (void)updateCloseButton {
488   if (!barIsVisible_)
489     return;
491   NSRect selfBounds = [[self view] bounds];
492   NSRect hoverFrame = [hoverCloseButton_ frame];
493   NSRect bounds;
495   if (isFullscreen_) {
496     bounds = NSMakeRect(NSMinX(hoverFrame), 0,
497                         selfBounds.size.width - NSMinX(hoverFrame),
498                         selfBounds.size.height);
499   } else {
500     bounds.origin.x = NSMinX(hoverFrame);
501     bounds.origin.y = NSMidY(hoverFrame) -
502                       kHoverCloseButtonDefaultSize.height / 2.0;
503     bounds.size = kHoverCloseButtonDefaultSize;
504   }
506   // Set the tracking off to create a new tracking area for the control.
507   // When changing the bounds/frame on a HoverButton, the tracking isn't updated
508   // correctly, it needs to be turned off and back on.
509   [hoverCloseButton_ setTrackingEnabled:NO];
510   [hoverCloseButton_ setFrame:bounds];
511   [hoverCloseButton_ setTrackingEnabled:YES];
513 @end