| // Copyright 2013 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "chrome/browser/ui/app_list/app_list_service_mac.h" |
| |
| #include <ApplicationServices/ApplicationServices.h> |
| #import <Cocoa/Cocoa.h> |
| |
| #include "base/bind.h" |
| #include "base/command_line.h" |
| #include "base/file_util.h" |
| #include "base/lazy_instance.h" |
| #include "base/mac/mac_util.h" |
| #include "base/memory/singleton.h" |
| #include "base/message_loop/message_loop.h" |
| #include "base/prefs/pref_service.h" |
| #import "chrome/browser/app_controller_mac.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/extensions/extension_service.h" |
| #include "chrome/browser/profiles/profile_manager.h" |
| #include "chrome/browser/ui/app_list/app_list_controller_delegate_impl.h" |
| #include "chrome/browser/ui/app_list/app_list_positioner.h" |
| #include "chrome/browser/ui/app_list/app_list_service.h" |
| #include "chrome/browser/ui/app_list/app_list_service_impl.h" |
| #include "chrome/browser/ui/app_list/app_list_util.h" |
| #include "chrome/browser/ui/app_list/app_list_view_delegate.h" |
| #include "chrome/browser/ui/browser_commands.h" |
| #include "chrome/browser/ui/extensions/application_launch.h" |
| #include "chrome/browser/web_applications/web_app.h" |
| #include "chrome/browser/web_applications/web_app_mac.h" |
| #include "chrome/common/chrome_switches.h" |
| #include "chrome/common/chrome_version_info.h" |
| #include "chrome/common/extensions/manifest_handlers/app_launch_info.h" |
| #include "chrome/common/mac/app_mode_common.h" |
| #include "chrome/common/pref_names.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "extensions/browser/extension_system.h" |
| #include "grit/chrome_unscaled_resources.h" |
| #include "grit/google_chrome_strings.h" |
| #include "net/base/url_util.h" |
| #import "ui/app_list/cocoa/app_list_view_controller.h" |
| #import "ui/app_list/cocoa/app_list_window_controller.h" |
| #include "ui/app_list/search_box_model.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/resource/resource_bundle.h" |
| #include "ui/gfx/display.h" |
| #include "ui/gfx/screen.h" |
| |
| namespace gfx { |
| class ImageSkia; |
| } |
| |
| // Controller for animations that show or hide the app list. |
| @interface AppListAnimationController : NSObject<NSAnimationDelegate> { |
| @private |
| // When closing, the window to close. Retained until the animation ends. |
| base::scoped_nsobject<NSWindow> window_; |
| // The animation started and owned by |self|. Reset when the animation ends. |
| base::scoped_nsobject<NSViewAnimation> animation_; |
| } |
| |
| // Returns whether |window_| is scheduled to be closed when the animation ends. |
| - (BOOL)isClosing; |
| |
| // Animate |window| to show or close it, after cancelling any current animation. |
| // Translates from the current location to |targetOrigin| and fades in or out. |
| - (void)animateWindow:(NSWindow*)window |
| targetOrigin:(NSPoint)targetOrigin |
| closing:(BOOL)closing; |
| |
| // Called on the UI thread once the animation has completed to reset the |
| // animation state, close the window (if it is a close animation), and possibly |
| // terminate Chrome. |
| - (void)cleanupOnUIThread; |
| |
| @end |
| |
| namespace { |
| |
| // Version of the app list shortcut version installed. |
| const int kShortcutVersion = 2; |
| |
| // Duration of show and hide animations. |
| const NSTimeInterval kAnimationDuration = 0.2; |
| |
| // Distance towards the screen edge that the app list moves from when showing. |
| const CGFloat kDistanceMovedOnShow = 20; |
| |
| web_app::ShortcutInfo GetAppListShortcutInfo( |
| const base::FilePath& profile_path) { |
| web_app::ShortcutInfo shortcut_info; |
| chrome::VersionInfo::Channel channel = chrome::VersionInfo::GetChannel(); |
| if (channel == chrome::VersionInfo::CHANNEL_CANARY) { |
| shortcut_info.title = |
| l10n_util::GetStringUTF16(IDS_APP_LIST_SHORTCUT_NAME_CANARY); |
| } else { |
| shortcut_info.title = l10n_util::GetStringUTF16(IDS_APP_LIST_SHORTCUT_NAME); |
| } |
| |
| shortcut_info.extension_id = app_mode::kAppListModeId; |
| shortcut_info.description = shortcut_info.title; |
| shortcut_info.profile_path = profile_path; |
| |
| return shortcut_info; |
| } |
| |
| void CreateAppListShim(const base::FilePath& profile_path) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| WebApplicationInfo web_app_info; |
| web_app::ShortcutInfo shortcut_info = |
| GetAppListShortcutInfo(profile_path); |
| |
| ResourceBundle& resource_bundle = ResourceBundle::GetSharedInstance(); |
| chrome::VersionInfo::Channel channel = chrome::VersionInfo::GetChannel(); |
| if (channel == chrome::VersionInfo::CHANNEL_CANARY) { |
| #if defined(GOOGLE_CHROME_BUILD) |
| shortcut_info.favicon.Add( |
| *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_CANARY_16)); |
| shortcut_info.favicon.Add( |
| *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_CANARY_32)); |
| shortcut_info.favicon.Add( |
| *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_CANARY_128)); |
| shortcut_info.favicon.Add( |
| *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_CANARY_256)); |
| #else |
| NOTREACHED(); |
| #endif |
| } else { |
| shortcut_info.favicon.Add( |
| *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_16)); |
| shortcut_info.favicon.Add( |
| *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_32)); |
| shortcut_info.favicon.Add( |
| *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_128)); |
| shortcut_info.favicon.Add( |
| *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_256)); |
| } |
| |
| web_app::ShortcutLocations shortcut_locations; |
| PrefService* local_state = g_browser_process->local_state(); |
| int installed_version = |
| local_state->GetInteger(prefs::kAppLauncherShortcutVersion); |
| |
| // If this is a first-time install, add a dock icon. Otherwise just update |
| // the target, and wait for OSX to refresh its icon caches. This might not |
| // occur until a reboot, but OSX does not offer a nicer way. Deleting cache |
| // files on disk and killing processes can easily result in icon corruption. |
| if (installed_version == 0) |
| shortcut_locations.in_quick_launch_bar = true; |
| |
| web_app::CreateShortcutsForShortcutInfo( |
| web_app::SHORTCUT_CREATION_AUTOMATED, |
| shortcut_locations, |
| shortcut_info); |
| |
| local_state->SetInteger(prefs::kAppLauncherShortcutVersion, |
| kShortcutVersion); |
| } |
| |
| NSRunningApplication* ActiveApplicationNotChrome() { |
| NSArray* applications = [[NSWorkspace sharedWorkspace] runningApplications]; |
| for (NSRunningApplication* application in applications) { |
| if (![application isActive]) |
| continue; |
| |
| if ([application isEqual:[NSRunningApplication currentApplication]]) |
| return nil; // Chrome is active. |
| |
| return application; |
| } |
| |
| return nil; |
| } |
| |
| // Determines which screen edge the dock is aligned to. |
| AppListPositioner::ScreenEdge DockLocationInDisplay( |
| const gfx::Display& display) { |
| // Assume the dock occupies part of the work area either on the left, right or |
| // bottom of the display. Note in the autohide case, it is always 4 pixels. |
| const gfx::Rect work_area = display.work_area(); |
| const gfx::Rect display_bounds = display.bounds(); |
| if (work_area.bottom() != display_bounds.bottom()) |
| return AppListPositioner::SCREEN_EDGE_BOTTOM; |
| |
| if (work_area.x() != display_bounds.x()) |
| return AppListPositioner::SCREEN_EDGE_LEFT; |
| |
| if (work_area.right() != display_bounds.right()) |
| return AppListPositioner::SCREEN_EDGE_RIGHT; |
| |
| return AppListPositioner::SCREEN_EDGE_UNKNOWN; |
| } |
| |
| // If |display|'s work area is too close to its boundary on |dock_edge|, adjust |
| // the work area away from the edge by a constant amount to reduce overlap and |
| // ensure the dock icon can still be clicked to dismiss the app list. |
| void AdjustWorkAreaForDock(const gfx::Display& display, |
| AppListPositioner* positioner, |
| AppListPositioner::ScreenEdge dock_edge) { |
| const int kAutohideDockThreshold = 10; |
| const int kExtraDistance = 50; // A dock with 40 items is about this size. |
| |
| const gfx::Rect work_area = display.work_area(); |
| const gfx::Rect display_bounds = display.bounds(); |
| |
| switch (dock_edge) { |
| case AppListPositioner::SCREEN_EDGE_LEFT: |
| if (work_area.x() - display_bounds.x() <= kAutohideDockThreshold) |
| positioner->WorkAreaInset(kExtraDistance, 0, 0, 0); |
| break; |
| case AppListPositioner::SCREEN_EDGE_RIGHT: |
| if (display_bounds.right() - work_area.right() <= kAutohideDockThreshold) |
| positioner->WorkAreaInset(0, 0, kExtraDistance, 0); |
| break; |
| case AppListPositioner::SCREEN_EDGE_BOTTOM: |
| if (display_bounds.bottom() - work_area.bottom() <= |
| kAutohideDockThreshold) { |
| positioner->WorkAreaInset(0, 0, 0, kExtraDistance); |
| } |
| break; |
| case AppListPositioner::SCREEN_EDGE_UNKNOWN: |
| case AppListPositioner::SCREEN_EDGE_TOP: |
| NOTREACHED(); |
| break; |
| } |
| } |
| |
| void GetAppListWindowOrigins( |
| NSWindow* window, NSPoint* target_origin, NSPoint* start_origin) { |
| gfx::Screen* const screen = gfx::Screen::GetScreenFor([window contentView]); |
| // Ensure y coordinates are flipped back into AppKit's coordinate system. |
| bool cursor_is_visible = CGCursorIsVisible(); |
| gfx::Display display; |
| gfx::Point cursor; |
| if (!cursor_is_visible) { |
| // If Chrome is the active application, display on the same display as |
| // Chrome's keyWindow since this will catch activations triggered, e.g, via |
| // WebStore install. If another application is active, OSX doesn't provide a |
| // reliable way to get the display in use. Fall back to the primary display |
| // since it has the menu bar and is likely to be correct, e.g., for |
| // activations from Spotlight. |
| const gfx::NativeView key_view = [[NSApp keyWindow] contentView]; |
| display = key_view && [NSApp isActive] ? |
| screen->GetDisplayNearestWindow(key_view) : |
| screen->GetPrimaryDisplay(); |
| } else { |
| cursor = screen->GetCursorScreenPoint(); |
| display = screen->GetDisplayNearestPoint(cursor); |
| } |
| |
| const NSSize ns_window_size = [window frame].size; |
| gfx::Size window_size(ns_window_size.width, ns_window_size.height); |
| int primary_display_height = |
| NSMaxY([[[NSScreen screens] objectAtIndex:0] frame]); |
| AppListServiceMac::FindAnchorPoint(window_size, |
| display, |
| primary_display_height, |
| cursor_is_visible, |
| cursor, |
| target_origin, |
| start_origin); |
| } |
| |
| } // namespace |
| |
| AppListServiceMac::AppListServiceMac() |
| : profile_(NULL), |
| controller_delegate_(new AppListControllerDelegateImpl(this)) { |
| animation_controller_.reset([[AppListAnimationController alloc] init]); |
| } |
| |
| AppListServiceMac::~AppListServiceMac() {} |
| |
| // static |
| AppListServiceMac* AppListServiceMac::GetInstance() { |
| return Singleton<AppListServiceMac, |
| LeakySingletonTraits<AppListServiceMac> >::get(); |
| } |
| |
| // static |
| void AppListServiceMac::FindAnchorPoint(const gfx::Size& window_size, |
| const gfx::Display& display, |
| int primary_display_height, |
| bool cursor_is_visible, |
| const gfx::Point& cursor, |
| NSPoint* target_origin, |
| NSPoint* start_origin) { |
| AppListPositioner positioner(display, window_size, 0); |
| AppListPositioner::ScreenEdge dock_location = DockLocationInDisplay(display); |
| |
| gfx::Point anchor; |
| // Snap to the dock edge. If the cursor is greater than the window |
| // width/height away or not visible, anchor to the center of the dock. |
| // Otherwise, anchor to the cursor position. |
| if (dock_location == AppListPositioner::SCREEN_EDGE_UNKNOWN) { |
| anchor = positioner.GetAnchorPointForScreenCorner( |
| AppListPositioner::SCREEN_CORNER_BOTTOM_LEFT); |
| } else { |
| int snap_distance = |
| dock_location == AppListPositioner::SCREEN_EDGE_BOTTOM || |
| dock_location == AppListPositioner::SCREEN_EDGE_TOP ? |
| window_size.height() : |
| window_size.width(); |
| // Subtract the dock area since the display's default work_area will not |
| // subtract it if the dock is set to auto-hide, and the app list should |
| // never overlap the dock. |
| AdjustWorkAreaForDock(display, &positioner, dock_location); |
| if (!cursor_is_visible || positioner.GetCursorDistanceFromShelf( |
| dock_location, cursor) > snap_distance) { |
| anchor = positioner.GetAnchorPointForShelfCenter(dock_location); |
| } else { |
| anchor = positioner.GetAnchorPointForShelfCursor(dock_location, cursor); |
| } |
| } |
| |
| *target_origin = NSMakePoint( |
| anchor.x() - window_size.width() / 2, |
| primary_display_height - anchor.y() - window_size.height() / 2); |
| *start_origin = *target_origin; |
| |
| // If the launcher is anchored to the dock (regardless of whether the cursor |
| // is visible), animate in inwards from the edge of screen |
| switch (dock_location) { |
| case AppListPositioner::SCREEN_EDGE_UNKNOWN: |
| break; |
| case AppListPositioner::SCREEN_EDGE_LEFT: |
| start_origin->x -= kDistanceMovedOnShow; |
| break; |
| case AppListPositioner::SCREEN_EDGE_RIGHT: |
| start_origin->x += kDistanceMovedOnShow; |
| break; |
| case AppListPositioner::SCREEN_EDGE_TOP: |
| NOTREACHED(); |
| break; |
| case AppListPositioner::SCREEN_EDGE_BOTTOM: |
| start_origin->y -= kDistanceMovedOnShow; |
| break; |
| } |
| } |
| |
| void AppListServiceMac::Init(Profile* initial_profile) { |
| // On Mac, Init() is called multiple times for a process: any time there is no |
| // browser window open and a new window is opened, and during process startup |
| // to handle the silent launch case (e.g. for app shims). In the startup case, |
| // a profile has not yet been determined so |initial_profile| will be NULL. |
| static bool init_called_with_profile = false; |
| if (initial_profile && !init_called_with_profile) { |
| init_called_with_profile = true; |
| PerformStartupChecks(initial_profile); |
| PrefService* local_state = g_browser_process->local_state(); |
| if (!IsAppLauncherEnabled()) { |
| local_state->SetInteger(prefs::kAppLauncherShortcutVersion, 0); |
| } else { |
| int installed_shortcut_version = |
| local_state->GetInteger(prefs::kAppLauncherShortcutVersion); |
| |
| if (kShortcutVersion > installed_shortcut_version) |
| CreateShortcut(); |
| } |
| } |
| |
| static bool init_called = false; |
| if (init_called) |
| return; |
| |
| init_called = true; |
| apps::AppShimHandler::RegisterHandler(app_mode::kAppListModeId, |
| AppListServiceMac::GetInstance()); |
| |
| // Handle the case where Chrome was not running and was started with the app |
| // launcher shim. The profile has not yet been loaded. To improve response |
| // times, start animating an empty window which will be populated via |
| // OnShimLaunch(). Note that if --silent-launch is not also passed, the window |
| // will instead populate via StartupBrowserCreator::Launch(). Shim-initiated |
| // launches will always have --silent-launch. |
| if (CommandLine::ForCurrentProcess()->HasSwitch(switches::kShowAppList)) |
| ShowWindowNearDock(); |
| } |
| |
| Profile* AppListServiceMac::GetCurrentAppListProfile() { |
| return profile_; |
| } |
| |
| void AppListServiceMac::CreateForProfile(Profile* requested_profile) { |
| if (profile_ == requested_profile) |
| return; |
| |
| profile_ = requested_profile; |
| |
| if (!window_controller_) |
| window_controller_.reset([[AppListWindowController alloc] init]); |
| |
| scoped_ptr<app_list::AppListViewDelegate> delegate( |
| new AppListViewDelegate(profile_, GetControllerDelegate())); |
| [[window_controller_ appListViewController] setDelegate:delegate.Pass()]; |
| } |
| |
| void AppListServiceMac::ShowForProfile(Profile* requested_profile) { |
| if (requested_profile->IsSupervised()) |
| return; |
| |
| InvalidatePendingProfileLoads(); |
| |
| if (requested_profile == profile_) { |
| ShowWindowNearDock(); |
| return; |
| } |
| |
| SetProfilePath(requested_profile->GetPath()); |
| CreateForProfile(requested_profile); |
| ShowWindowNearDock(); |
| } |
| |
| void AppListServiceMac::DismissAppList() { |
| if (!IsAppListVisible()) |
| return; |
| |
| // If the app list is currently the main window, it will activate the next |
| // Chrome window when dismissed. But if a different application was active |
| // when the app list was shown, activate that instead. |
| base::scoped_nsobject<NSRunningApplication> prior_app; |
| if ([[window_controller_ window] isMainWindow]) |
| prior_app.swap(previously_active_application_); |
| else |
| previously_active_application_.reset(); |
| |
| // If activation is successful, the app list will lose main status and try to |
| // close itself again. It can't be closed in this runloop iteration without |
| // OSX deciding to raise the next Chrome window, and _then_ activating the |
| // application on top. This also occurs if no activation option is given. |
| if ([prior_app activateWithOptions:NSApplicationActivateIgnoringOtherApps]) |
| return; |
| |
| [animation_controller_ animateWindow:[window_controller_ window] |
| targetOrigin:last_start_origin_ |
| closing:YES]; |
| } |
| |
| bool AppListServiceMac::IsAppListVisible() const { |
| return [[window_controller_ window] isVisible] && |
| ![animation_controller_ isClosing]; |
| } |
| |
| void AppListServiceMac::EnableAppList(Profile* initial_profile, |
| AppListEnableSource enable_source) { |
| AppListServiceImpl::EnableAppList(initial_profile, enable_source); |
| AppController* controller = [NSApp delegate]; |
| [controller initAppShimMenuController]; |
| } |
| |
| void AppListServiceMac::CreateShortcut() { |
| CreateAppListShim(GetProfilePath( |
| g_browser_process->profile_manager()->user_data_dir())); |
| } |
| |
| NSWindow* AppListServiceMac::GetAppListWindow() { |
| return [window_controller_ window]; |
| } |
| |
| AppListControllerDelegate* AppListServiceMac::GetControllerDelegate() { |
| return controller_delegate_.get(); |
| } |
| |
| void AppListServiceMac::OnShimLaunch(apps::AppShimHandler::Host* host, |
| apps::AppShimLaunchType launch_type, |
| const std::vector<base::FilePath>& files) { |
| if (profile_ && IsAppListVisible()) { |
| DismissAppList(); |
| } else { |
| // Start by showing a possibly empty window to handle the case where Chrome |
| // is running, but hasn't yet loaded the app launcher profile. |
| ShowWindowNearDock(); |
| Show(); |
| } |
| |
| // Always close the shim process immediately. |
| host->OnAppLaunchComplete(apps::APP_SHIM_LAUNCH_DUPLICATE_HOST); |
| } |
| |
| void AppListServiceMac::OnShimClose(apps::AppShimHandler::Host* host) {} |
| |
| void AppListServiceMac::OnShimFocus(apps::AppShimHandler::Host* host, |
| apps::AppShimFocusType focus_type, |
| const std::vector<base::FilePath>& files) {} |
| |
| void AppListServiceMac::OnShimSetHidden(apps::AppShimHandler::Host* host, |
| bool hidden) {} |
| |
| void AppListServiceMac::OnShimQuit(apps::AppShimHandler::Host* host) {} |
| |
| void AppListServiceMac::ShowWindowNearDock() { |
| if (IsAppListVisible()) |
| return; |
| |
| if (!window_controller_) { |
| // Note that this will start showing an unpopulated window, the caller needs |
| // to ensure it will be populated later. |
| window_controller_.reset([[AppListWindowController alloc] init]); |
| } |
| |
| NSWindow* window = GetAppListWindow(); |
| DCHECK(window); |
| NSPoint target_origin; |
| GetAppListWindowOrigins(window, &target_origin, &last_start_origin_); |
| [window setFrameOrigin:last_start_origin_]; |
| |
| // Before activating, see if an application other than Chrome is currently the |
| // active application, so that it can be reactivated when dismissing. |
| previously_active_application_.reset([ActiveApplicationNotChrome() retain]); |
| |
| [animation_controller_ animateWindow:[window_controller_ window] |
| targetOrigin:target_origin |
| closing:NO]; |
| [window makeKeyAndOrderFront:nil]; |
| [NSApp activateIgnoringOtherApps:YES]; |
| RecordAppListLaunch(); |
| } |
| |
| void AppListServiceMac::WindowAnimationDidEnd() { |
| [animation_controller_ cleanupOnUIThread]; |
| } |
| |
| // static |
| AppListService* AppListService::Get(chrome::HostDesktopType desktop_type) { |
| return AppListServiceMac::GetInstance(); |
| } |
| |
| // static |
| void AppListService::InitAll(Profile* initial_profile) { |
| AppListServiceMac::GetInstance()->Init(initial_profile); |
| } |
| |
| @implementation AppListAnimationController |
| |
| - (BOOL)isClosing { |
| return !!window_; |
| } |
| |
| - (void)animateWindow:(NSWindow*)window |
| targetOrigin:(NSPoint)targetOrigin |
| closing:(BOOL)closing { |
| // First, stop the existing animation, if there is one. |
| [animation_ stopAnimation]; |
| |
| NSRect targetFrame = [window frame]; |
| targetFrame.origin = targetOrigin; |
| |
| // NSViewAnimation has a quirk when setting the curve to NSAnimationEaseOut |
| // where it attempts to auto-reverse the animation. FadeOut becomes FadeIn |
| // (good), but FrameKey is also switched (bad). So |targetFrame| needs to be |
| // put on the StartFrameKey when using NSAnimationEaseOut for showing. |
| NSArray* animationArray = @[ |
| @{ |
| NSViewAnimationTargetKey : window, |
| NSViewAnimationEffectKey : NSViewAnimationFadeOutEffect, |
| (closing ? NSViewAnimationEndFrameKey : NSViewAnimationStartFrameKey) : |
| [NSValue valueWithRect:targetFrame] |
| } |
| ]; |
| animation_.reset( |
| [[NSViewAnimation alloc] initWithViewAnimations:animationArray]); |
| [animation_ setDuration:kAnimationDuration]; |
| [animation_ setDelegate:self]; |
| |
| if (closing) { |
| [animation_ setAnimationCurve:NSAnimationEaseIn]; |
| window_.reset([window retain]); |
| } else { |
| [window setAlphaValue:0.0f]; |
| [animation_ setAnimationCurve:NSAnimationEaseOut]; |
| window_.reset(); |
| } |
| // Threaded animations are buggy on Snow Leopard. See http://crbug.com/335550. |
| // Note that in the non-threaded case, the animation won't start unless the |
| // UI runloop has spun up, so on <= Lion the animation will only animate if |
| // Chrome is already running. |
| if (base::mac::IsOSMountainLionOrLater()) |
| [animation_ setAnimationBlockingMode:NSAnimationNonblockingThreaded]; |
| else |
| [animation_ setAnimationBlockingMode:NSAnimationNonblocking]; |
| |
| [animation_ startAnimation]; |
| } |
| |
| - (void)cleanupOnUIThread { |
| bool closing = [self isClosing]; |
| [window_ close]; |
| window_.reset(); |
| animation_.reset(); |
| |
| if (closing) |
| apps::AppShimHandler::MaybeTerminate(); |
| } |
| |
| - (void)animationDidEnd:(NSAnimation*)animation { |
| content::BrowserThread::PostTask( |
| content::BrowserThread::UI, |
| FROM_HERE, |
| base::Bind(&AppListServiceMac::WindowAnimationDidEnd, |
| base::Unretained(AppListServiceMac::GetInstance()))); |
| } |
| |
| @end |