Index: chrome/browser/ui/cocoa/download/download_shelf_controller.mm |
=================================================================== |
--- chrome/browser/ui/cocoa/download/download_shelf_controller.mm (revision 69709) |
+++ chrome/browser/ui/cocoa/download/download_shelf_controller.mm (working copy) |
@@ -24,6 +24,24 @@ |
#include "grit/theme_resources.h" |
#import "third_party/GTM/AppKit/GTMNSAnimation+Duration.h" |
+// Download shelf autoclose behavior: |
+// |
+// The download shelf autocloses if all of this is true: |
+// 1) An item on the shelf has just been opened. |
+// 2) All remaining items on the shelf have been opened in the past. |
+// 3) The mouse leaves the shelf and remains off the shelf for 5 seconds. |
+// |
+// If the mouse re-enters the shelf within the 5 second grace period, the |
+// autoclose is canceled. An autoclose can only be scheduled in response to a |
+// shelf item being opened or removed. If an item is opened and then the |
+// resulting autoclose is canceled, subsequent mouse exited events will NOT |
+// trigger an autoclose. |
+// |
+// If the shelf is manually closed while a download is still in progress, that |
+// download is marked as "opened" for these purposes. If the shelf is later |
+// reopened, these previously-in-progress download will not block autoclose, |
+// even if that download was never actually clicked on and opened. |
+ |
namespace { |
// Max number of download views we'll contain. Any time a view is added and |
@@ -39,16 +57,24 @@ |
// Duration for download shelf closing animation, in seconds. |
const NSTimeInterval kDownloadShelfCloseDuration = 0.12; |
+// Amount of time between when the mouse is moved off the shelf and the shelf is |
+// autoclosed, in seconds. |
+const NSTimeInterval kAutoCloseDelaySeconds = 5; |
+ |
} // namespace |
@interface DownloadShelfController(Private) |
- (void)showDownloadShelf:(BOOL)enable; |
- (void)layoutItems:(BOOL)skipFirst; |
- (void)closed; |
+- (BOOL)canAutoClose; |
- (void)updateTheme; |
- (void)themeDidChangeNotification:(NSNotification*)notification; |
- (void)viewFrameDidChange:(NSNotification*)notification; |
+ |
+- (void)installTrackingArea; |
+- (void)cancelAutoCloseAndRemoveTrackingArea; |
@end |
@@ -100,6 +126,7 @@ |
- (void)dealloc { |
[[NSNotificationCenter defaultCenter] removeObserver:self]; |
+ [self cancelAutoCloseAndRemoveTrackingArea]; |
// The controllers will unregister themselves as observers when they are |
// deallocated. No need to do that here. |
@@ -173,10 +200,19 @@ |
[self showDownloadShelf:NO]; |
} |
+- (void)downloadWasOpened:(DownloadItemController*)item_controller { |
+ // This should only be called on the main thead. |
+ DCHECK([NSThread isMainThread]); |
+ |
+ if ([self canAutoClose]) |
+ [self installTrackingArea]; |
+} |
+ |
// We need to explicitly release our download controllers here since they need |
// to remove themselves as observers before the remaining shutdown happens. |
- (void)exiting { |
[[self animatableView] stopAnimation]; |
+ [self cancelAutoCloseAndRemoveTrackingArea]; |
downloadItemControllers_.reset(); |
} |
@@ -216,6 +252,8 @@ |
} |
- (void)hide:(id)sender { |
+ [self cancelAutoCloseAndRemoveTrackingArea]; |
+ |
// If |sender| isn't nil, then we're being closed from the UI by the user and |
// we need to tell our shelf implementation to close. Otherwise, we're being |
// closed programmatically by our shelf implementation. |
@@ -255,6 +293,8 @@ |
- (void)addDownloadItem:(BaseDownloadItemModel*)model { |
DCHECK([NSThread isMainThread]); |
+ [self cancelAutoCloseAndRemoveTrackingArea]; |
+ |
// Insert new item at the left. |
scoped_nsobject<DownloadItemController> controller( |
[[DownloadItemController alloc] initWithModel:model shelf:self]); |
@@ -312,16 +352,75 @@ |
while (i < [downloadItemControllers_ count]) { |
DownloadItemController* itemController = |
[downloadItemControllers_ objectAtIndex:i]; |
+ DownloadItem* download = [itemController download]; |
bool isTransferDone = |
- [itemController download]->state() == DownloadItem::COMPLETE || |
- [itemController download]->state() == DownloadItem::CANCELLED; |
+ download->state() == DownloadItem::COMPLETE || |
+ download->state() == DownloadItem::CANCELLED; |
if (isTransferDone && |
- [itemController download]->safety_state() != DownloadItem::DANGEROUS) { |
+ download->safety_state() != DownloadItem::DANGEROUS) { |
[self remove:itemController]; |
} else { |
+ // Treat the item as opened when we close. This way if we get shown again |
+ // the user need not open this item for the shelf to auto-close. |
+ download->set_opened(true); |
++i; |
} |
} |
} |
+- (void)mouseEntered:(NSEvent*)event { |
+ // If the mouse re-enters the download shelf, cancel the auto-close. Further |
+ // mouse exits should not trigger autoclose, so also remove the tracking area. |
+ [self cancelAutoCloseAndRemoveTrackingArea]; |
+} |
+ |
+- (void)mouseExited:(NSEvent*)event { |
+ // Cancel any previous hide requests, just to be safe. |
+ [NSObject cancelPreviousPerformRequestsWithTarget:self |
+ selector:@selector(hide:) |
+ object:self]; |
+ |
+ // Schedule an autoclose after a delay. If the mouse is moved back into the |
+ // view, or if an item is added to the shelf, the timer will be canceled. |
+ [self performSelector:@selector(hide:) |
+ withObject:self |
+ afterDelay:kAutoCloseDelaySeconds]; |
+} |
+ |
+- (BOOL)canAutoClose { |
+ for (NSUInteger i = 0; i < [downloadItemControllers_ count]; ++i) { |
+ DownloadItemController* itemController = |
+ [downloadItemControllers_ objectAtIndex:i]; |
+ if (![itemController download]->opened()) |
+ return NO; |
+ } |
+ return YES; |
+} |
+ |
+- (void)installTrackingArea { |
+ // Install the tracking area to listen for mouseExited messages and trigger |
+ // the shelf autoclose. |
+ if (trackingArea_.get()) |
+ return; |
+ |
+ trackingArea_.reset([[NSTrackingArea alloc] |
+ initWithRect:[[self view] bounds] |
+ options:NSTrackingMouseEnteredAndExited | |
+ NSTrackingActiveAlways |
+ owner:self |
+ userInfo:nil]); |
+ [[self view] addTrackingArea:trackingArea_]; |
+} |
+ |
+- (void)cancelAutoCloseAndRemoveTrackingArea { |
+ [NSObject cancelPreviousPerformRequestsWithTarget:self |
+ selector:@selector(hide:) |
+ object:self]; |
+ |
+ if (trackingArea_.get()) { |
+ [[self view] removeTrackingArea:trackingArea_]; |
+ trackingArea_.reset(nil); |
+ } |
+} |
+ |
@end |