Disable view source for Developer Tools.
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / download / download_shelf_controller.mm
blob51c70f7d44dc904954a25c729fa5191ea0bddce2
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/mac/mac_util.h"
9 #include "base/strings/sys_string_conversions.h"
10 #include "chrome/browser/download/download_stats.h"
11 #include "chrome/browser/profiles/profile.h"
12 #include "chrome/browser/themes/theme_service.h"
13 #include "chrome/browser/themes/theme_service_factory.h"
14 #include "chrome/browser/ui/browser.h"
15 #include "chrome/browser/ui/chrome_pages.h"
16 #import "chrome/browser/ui/cocoa/animatable_view.h"
17 #include "chrome/browser/ui/cocoa/browser_window_cocoa.h"
18 #import "chrome/browser/ui/cocoa/browser_window_controller.h"
19 #include "chrome/browser/ui/cocoa/download/download_item_controller.h"
20 #include "chrome/browser/ui/cocoa/download/download_shelf_mac.h"
21 #import "chrome/browser/ui/cocoa/download/download_shelf_view.h"
22 #import "chrome/browser/ui/cocoa/presentation_mode_controller.h"
23 #include "content/public/browser/download_item.h"
24 #include "content/public/browser/download_manager.h"
25 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSAnimation+Duration.h"
26 #import "ui/base/cocoa/hover_button.h"
27 #include "ui/base/l10n/l10n_util.h"
29 using content::DownloadItem;
31 // Download shelf autoclose behavior:
33 // The download shelf autocloses if all of this is true:
34 // 1) An item on the shelf has just been opened or removed.
35 // 2) All remaining items on the shelf have been opened in the past.
36 // 3) The mouse leaves the shelf and remains off the shelf for 5 seconds.
38 // If the mouse re-enters the shelf within the 5 second grace period, the
39 // autoclose is canceled.  An autoclose can only be scheduled in response to a
40 // shelf item being opened or removed.  If an item is opened and then the
41 // resulting autoclose is canceled, subsequent mouse exited events will NOT
42 // trigger an autoclose.
44 // If the shelf is manually closed while a download is still in progress, that
45 // download is marked as "opened" for these purposes.  If the shelf is later
46 // reopened, these previously-in-progress download will not block autoclose,
47 // even if that download was never actually clicked on and opened.
49 namespace {
51 // Max number of download views we'll contain. Any time a view is added and
52 // we already have this many download views, one is removed.
53 const size_t kMaxDownloadItemCount = 16;
55 // Horizontal padding between two download items.
56 const int kDownloadItemPadding = 0;
58 // Duration for the open-new-leftmost-item animation, in seconds.
59 const NSTimeInterval kDownloadItemOpenDuration = 0.8;
61 // Duration for download shelf closing animation, in seconds.
62 const NSTimeInterval kDownloadShelfCloseDuration = 0.12;
64 // Amount of time between when the mouse is moved off the shelf and the shelf is
65 // autoclosed, in seconds.
66 const NSTimeInterval kAutoCloseDelaySeconds = 5;
68 // The size of the x button by default.
69 const NSSize kHoverCloseButtonDefaultSize = { 18, 18 };
71 }  // namespace
73 @interface DownloadShelfController(Private)
74 - (void)removeDownload:(DownloadItemController*)download
75         isShelfClosing:(BOOL)isShelfClosing;
76 - (void)layoutItems:(BOOL)skipFirst;
77 - (void)closed;
78 - (void)maybeAutoCloseAfterDelay;
79 - (void)scheduleAutoClose;
80 - (void)cancelAutoClose;
81 - (void)autoClose;
82 - (void)viewFrameDidChange:(NSNotification*)notification;
83 - (void)installTrackingArea;
84 - (void)removeTrackingArea;
85 - (void)willEnterFullscreen;
86 - (void)willLeaveFullscreen;
87 - (void)updateCloseButton;
88 @end
91 @implementation DownloadShelfController
93 - (id)initWithBrowser:(Browser*)browser
94        resizeDelegate:(id<ViewResizer>)resizeDelegate {
95   if ((self = [super initWithNibName:@"DownloadShelf"
96                               bundle:base::mac::FrameworkBundle()])) {
97     resizeDelegate_ = resizeDelegate;
98     maxShelfHeight_ = NSHeight([[self view] bounds]);
99     currentShelfHeight_ = maxShelfHeight_;
100     if (browser && browser->window())
101       isFullscreen_ = browser->window()->IsFullscreen();
102     else
103       isFullscreen_ = NO;
105     // Reset the download shelf's frame height to zero.  It will be properly
106     // positioned and sized the first time we try to set its height. (Just
107     // setting the rect to NSZeroRect does not work: it confuses Cocoa's view
108     // layout logic. If the shelf's width is too small, cocoa makes the download
109     // item container view wider than the browser window).
110     NSRect frame = [[self view] frame];
111     frame.size.height = 0;
112     [[self view] setFrame:frame];
114     downloadItemControllers_.reset([[NSMutableArray alloc] init]);
116     bridge_.reset(new DownloadShelfMac(browser, self));
117     navigator_ = browser;
118   }
119   return self;
122 - (void)awakeFromNib {
123   DCHECK(hoverCloseButton_);
125   NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
126   [[self animatableView] setResizeDelegate:resizeDelegate_];
127   [[self view] setPostsFrameChangedNotifications:YES];
128   [defaultCenter addObserver:self
129                     selector:@selector(viewFrameDidChange:)
130                         name:NSViewFrameDidChangeNotification
131                       object:[self view]];
133   // These notifications are declared in fullscreen_controller, and are posted
134   // without objects.
135   [defaultCenter addObserver:self
136                     selector:@selector(willEnterFullscreen)
137                         name:kWillEnterFullscreenNotification
138                       object:nil];
139   [defaultCenter addObserver:self
140                     selector:@selector(willLeaveFullscreen)
141                         name:kWillLeaveFullscreenNotification
142                       object:nil];
143   [self installTrackingArea];
146 - (void)dealloc {
147   [[NSNotificationCenter defaultCenter] removeObserver:self];
148   [self cancelAutoClose];
149   [self removeTrackingArea];
151   // The controllers will unregister themselves as observers when they are
152   // deallocated. No need to do that here.
153   [super dealloc];
156 // Called after the frame's rect has changed; usually when the height is
157 // animated.
158 - (void)viewFrameDidChange:(NSNotification*)notification {
159   // Anchor subviews at the top of |view|, so that it looks like the shelf
160   // is sliding out.
161   CGFloat newShelfHeight = NSHeight([[self view] frame]);
162   if (newShelfHeight == currentShelfHeight_)
163     return;
165   for (NSView* view in [[self view] subviews]) {
166     NSRect frame = [view frame];
167     frame.origin.y -= currentShelfHeight_ - newShelfHeight;
168     [view setFrame:frame];
169   }
170   currentShelfHeight_ = newShelfHeight;
173 - (AnimatableView*)animatableView {
174   return static_cast<AnimatableView*>([self view]);
177 - (IBAction)showDownloadsTab:(id)sender {
178   chrome::ShowDownloads(bridge_->browser());
181 - (IBAction)handleClose:(id)sender {
182   bridge_->Close(DownloadShelf::USER_ACTION);
185 - (void)remove:(DownloadItemController*)download {
186   [self removeDownload:download
187         isShelfClosing:NO];
190 - (void)removeDownload:(DownloadItemController*)download
191         isShelfClosing:(BOOL)isShelfClosing {
192   // Look for the download in our controller array and remove it. This will
193   // explicity release it so that it removes itself as an Observer of the
194   // DownloadItem. We don't want to wait for autorelease since the DownloadItem
195   // we are observing will likely be gone by then.
196   [[NSNotificationCenter defaultCenter] removeObserver:download];
198   // TODO(dmaclach): Remove -- http://crbug.com/25845
199   [[download view] removeFromSuperview];
201   [downloadItemControllers_ removeObject:download];
203   if (!isShelfClosing) {
204     [self layoutItems];
206     // If there are no more downloads or if all the remaining downloads have
207     // been opened, we can close the shelf.
208     [self maybeAutoCloseAfterDelay];
209   }
212 - (void)downloadWasOpened:(DownloadItemController*)item_controller {
213   // This should only be called on the main thead.
214   DCHECK([NSThread isMainThread]);
215   [self maybeAutoCloseAfterDelay];
218 // We need to explicitly release our download controllers here since they need
219 // to remove themselves as observers before the remaining shutdown happens.
220 - (void)exiting {
221   [[self animatableView] stopAnimation];
222   [self removeTrackingArea];
223   [self cancelAutoClose];
224   while ([downloadItemControllers_ count] > 0) {
225     [self removeDownload:[downloadItemControllers_ lastObject]
226           isShelfClosing:YES];
227   }
228   downloadItemControllers_.reset();
231 - (void)showDownloadShelf:(BOOL)show
232              isUserAction:(BOOL)isUserAction {
233   [self cancelAutoClose];
234   shouldCloseOnMouseExit_ = NO;
236   if ([self isVisible] == show)
237     return;
239   if (!show) {
240     int numInProgress = 0;
241     for (NSUInteger i = 0; i < [downloadItemControllers_ count]; ++i) {
242       DownloadItem* item = [[downloadItemControllers_ objectAtIndex:i]download];
243       if (item->GetState() == DownloadItem::IN_PROGRESS)
244         ++numInProgress;
245     }
246     RecordDownloadShelfClose(
247         [downloadItemControllers_ count], numInProgress, !isUserAction);
248   }
250   // Animate the shelf out, but not in.
251   // TODO(rohitrao): We do not animate on the way in because Cocoa is already
252   // doing a lot of work to set up the download arrow animation.  I've chosen to
253   // do no animation over janky animation.  Find a way to make animating in
254   // smoother.
255   AnimatableView* view = [self animatableView];
256   if (show)
257     [view setHeight:maxShelfHeight_];
258   else
259     [view animateToNewHeight:0 duration:kDownloadShelfCloseDuration];
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];
278 - (float)height {
279   return maxShelfHeight_;
282 // If |skipFirst| is true, the frame of the leftmost item is not set.
283 - (void)layoutItems:(BOOL)skipFirst {
284   CGFloat currentX = 0;
285   for (DownloadItemController* itemController
286       in downloadItemControllers_.get()) {
287     NSRect frame = [[itemController view] frame];
288     frame.origin.x = currentX;
289     frame.size.width = [itemController preferredSize].width;
290     if (!skipFirst)
291       [[[itemController view] animator] setFrame:frame];
292     currentX += frame.size.width + kDownloadItemPadding;
293     skipFirst = NO;
294   }
297 - (void)layoutItems {
298   [self layoutItems:NO];
301 - (void)addDownloadItem:(DownloadItem*)downloadItem {
302   DCHECK([NSThread isMainThread]);
303   base::scoped_nsobject<DownloadItemController> controller(
304       [[DownloadItemController alloc] initWithDownload:downloadItem
305                                                  shelf:self
306                                              navigator:navigator_]);
307   [self add:controller.get()];
310 - (void)add:(DownloadItemController*)controller {
311   DCHECK([NSThread isMainThread]);
312   [self cancelAutoClose];
313   shouldCloseOnMouseExit_ = NO;
315   // Insert new item at the left.
316   // Adding at index 0 in NSMutableArrays is O(1).
317   [downloadItemControllers_ insertObject:controller atIndex:0];
319   [itemContainerView_ addSubview:[controller view]];
321   // The controller is in charge of removing itself as an observer in its
322   // dealloc.
323   [[NSNotificationCenter defaultCenter]
324     addObserver:controller
325        selector:@selector(updateVisibility:)
326            name:NSViewFrameDidChangeNotification
327          object:[controller view]];
328   [[NSNotificationCenter defaultCenter]
329     addObserver:controller
330        selector:@selector(updateVisibility:)
331            name:NSViewFrameDidChangeNotification
332          object:itemContainerView_];
334   // Start at width 0...
335   NSSize size = [controller preferredSize];
336   NSRect frame = NSMakeRect(0, 0, 0, size.height);
337   [[controller view] setFrame:frame];
339   // ...then animate in
340   frame.size.width = size.width;
341   [NSAnimationContext beginGrouping];
342   [[NSAnimationContext currentContext]
343       gtm_setDuration:kDownloadItemOpenDuration
344             eventMask:NSLeftMouseUpMask];
345   [[[controller view] animator] setFrame:frame];
346   [NSAnimationContext endGrouping];
348   // Keep only a limited number of items in the shelf.
349   if ([downloadItemControllers_ count] > kMaxDownloadItemCount) {
350     DCHECK(kMaxDownloadItemCount > 0);
352     // Since no user will ever see the item being removed (needs a horizontal
353     // screen resolution greater than 3200 at 16 items at 200 pixels each),
354     // there's no point in animating the removal.
355     [self removeDownload:[downloadItemControllers_ lastObject]
356           isShelfClosing:NO];
357   }
359   // Finally, move the remaining items to the right. Skip the first item when
360   // laying out the items, so that the longer animation duration we set up above
361   // is not overwritten.
362   [self layoutItems:YES];
365 - (void)closed {
366   // Don't remove completed downloads if the shelf is just being auto-hidden
367   // rather than explicitly closed by the user.
368   if (bridge_->is_hidden())
369     return;
370   NSUInteger i = 0;
371   while (i < [downloadItemControllers_ count]) {
372     DownloadItemController* itemController =
373         [downloadItemControllers_ objectAtIndex:i];
374     DownloadItem* download = [itemController download];
375     DownloadItem::DownloadState state = download->GetState();
376     bool isTransferDone = state == DownloadItem::COMPLETE ||
377                           state == DownloadItem::CANCELLED ||
378                           state == DownloadItem::INTERRUPTED;
379     if (isTransferDone && !download->IsDangerous()) {
380       [self removeDownload:itemController
381             isShelfClosing:YES];
382     } else {
383       // Treat the item as opened when we close. This way if we get shown again
384       // the user need not open this item for the shelf to auto-close.
385       download->SetOpened(true);
386       ++i;
387     }
388   }
391 - (void)mouseEntered:(NSEvent*)event {
392   isMouseInsideView_ = YES;
393   // If the mouse re-enters the download shelf, cancel the auto-close.  Further
394   // mouse exits should not trigger autoclose.
395   if (shouldCloseOnMouseExit_) {
396     [self cancelAutoClose];
397     shouldCloseOnMouseExit_ = NO;
398   }
401 - (void)mouseExited:(NSEvent*)event {
402   isMouseInsideView_ = NO;
403   if (shouldCloseOnMouseExit_)
404     [self scheduleAutoClose];
407 - (void)scheduleAutoClose {
408   // Cancel any previous hide requests, just to be safe.
409   [self cancelAutoClose];
411   // Schedule an autoclose after a delay.  If the mouse is moved back into the
412   // view, or if an item is added to the shelf, the timer will be canceled.
413   [self performSelector:@selector(autoClose)
414              withObject:nil
415              afterDelay:kAutoCloseDelaySeconds];
418 - (void)cancelAutoClose {
419   [NSObject cancelPreviousPerformRequestsWithTarget:self
420                                            selector:@selector(autoClose)
421                                              object:nil];
424 - (void)maybeAutoCloseAfterDelay {
425   // We can close the shelf automatically if all the downloads on the shelf have
426   // been opened.
427   for (NSUInteger i = 0; i < [downloadItemControllers_ count]; ++i) {
428     DownloadItemController* itemController =
429         [downloadItemControllers_ objectAtIndex:i];
430     if (![itemController download]->GetOpened())
431       return;
432   }
434   if ([self isVisible] && [downloadItemControllers_ count] > 0 &&
435       isMouseInsideView_) {
436     // If there are download items on the shelf and the user is potentially stil
437     // interacting with them, schedule an auto close after the user moves the
438     // mouse off the shelf.
439     shouldCloseOnMouseExit_ = YES;
440   } else {
441     // We notify the DownloadShelf of our intention to close even if the shelf
442     // is currently hidden. If the shelf was temporarily hidden (e.g. because
443     // the browser window entered fullscreen mode), then this prevents the shelf
444     // from being shown again when the browser exits fullscreen mode.
445     [self autoClose];
446   }
449 - (void)autoClose {
450   bridge_->Close(DownloadShelf::AUTOMATIC);
453 - (void)installTrackingArea {
454   // Install the tracking area to listen for mouseEntered and mouseExited
455   // messages.
456   DCHECK(!trackingArea_.get());
458   trackingArea_.reset([[CrTrackingArea alloc]
459                         initWithRect:[[self view] bounds]
460                              options:NSTrackingMouseEnteredAndExited |
461                                      NSTrackingActiveAlways |
462                                      NSTrackingInVisibleRect
463                                owner:self
464                             userInfo:nil]);
465   [[self view] addTrackingArea:trackingArea_.get()];
468 - (void)removeTrackingArea {
469   if (trackingArea_.get()) {
470     [[self view] removeTrackingArea:trackingArea_.get()];
471     trackingArea_.reset(nil);
472   }
475 - (void)willEnterFullscreen {
476   isFullscreen_ = YES;
477   [self updateCloseButton];
480 - (void)willLeaveFullscreen {
481   isFullscreen_ = NO;
482   [self updateCloseButton];
485 - (void)updateCloseButton {
486   if (!barIsVisible_)
487     return;
489   NSRect selfBounds = [[self view] bounds];
490   NSRect hoverFrame = [hoverCloseButton_ frame];
491   NSRect bounds;
493   if (isFullscreen_) {
494     bounds = NSMakeRect(NSMinX(hoverFrame), 0,
495                         selfBounds.size.width - NSMinX(hoverFrame),
496                         selfBounds.size.height);
497   } else {
498     bounds.origin.x = NSMinX(hoverFrame);
499     bounds.origin.y = NSMidY(hoverFrame) -
500                       kHoverCloseButtonDefaultSize.height / 2.0;
501     bounds.size = kHoverCloseButtonDefaultSize;
502   }
504   // Set the tracking off to create a new tracking area for the control.
505   // When changing the bounds/frame on a HoverButton, the tracking isn't updated
506   // correctly, it needs to be turned off and back on.
507   [hoverCloseButton_ setTrackingEnabled:NO];
508   [hoverCloseButton_ setFrame:bounds];
509   [hoverCloseButton_ setTrackingEnabled:YES];
511 @end