app_list_service_mac.mm revision 6d86b77056ed63eb6871182f42a9fd5f07550f90
1// Copyright 2013 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/app_list/app_list_service_mac.h" 6 7#include <ApplicationServices/ApplicationServices.h> 8#import <Cocoa/Cocoa.h> 9 10#include "base/bind.h" 11#include "base/command_line.h" 12#include "base/file_util.h" 13#include "base/lazy_instance.h" 14#include "base/mac/mac_util.h" 15#include "base/memory/singleton.h" 16#include "base/message_loop/message_loop.h" 17#include "base/prefs/pref_service.h" 18#import "chrome/browser/app_controller_mac.h" 19#include "chrome/browser/browser_process.h" 20#include "chrome/browser/extensions/extension_service.h" 21#include "chrome/browser/profiles/profile_manager.h" 22#include "chrome/browser/ui/app_list/app_list_controller_delegate_impl.h" 23#include "chrome/browser/ui/app_list/app_list_positioner.h" 24#include "chrome/browser/ui/app_list/app_list_service.h" 25#include "chrome/browser/ui/app_list/app_list_service_impl.h" 26#include "chrome/browser/ui/app_list/app_list_util.h" 27#include "chrome/browser/ui/app_list/app_list_view_delegate.h" 28#include "chrome/browser/ui/browser_commands.h" 29#include "chrome/browser/ui/extensions/application_launch.h" 30#include "chrome/browser/web_applications/web_app.h" 31#include "chrome/browser/web_applications/web_app_mac.h" 32#include "chrome/common/chrome_switches.h" 33#include "chrome/common/chrome_version_info.h" 34#include "chrome/common/extensions/manifest_handlers/app_launch_info.h" 35#include "chrome/common/mac/app_mode_common.h" 36#include "chrome/common/pref_names.h" 37#include "content/public/browser/browser_thread.h" 38#include "extensions/browser/extension_system.h" 39#include "grit/chrome_unscaled_resources.h" 40#include "grit/google_chrome_strings.h" 41#include "net/base/url_util.h" 42#import "ui/app_list/cocoa/app_list_view_controller.h" 43#import "ui/app_list/cocoa/app_list_window_controller.h" 44#include "ui/app_list/search_box_model.h" 45#include "ui/base/l10n/l10n_util.h" 46#include "ui/base/resource/resource_bundle.h" 47#include "ui/gfx/display.h" 48#include "ui/gfx/screen.h" 49 50namespace gfx { 51class ImageSkia; 52} 53 54// Controller for animations that show or hide the app list. 55@interface AppListAnimationController : NSObject<NSAnimationDelegate> { 56 @private 57 // When closing, the window to close. Retained until the animation ends. 58 base::scoped_nsobject<NSWindow> window_; 59 // The animation started and owned by |self|. Reset when the animation ends. 60 base::scoped_nsobject<NSViewAnimation> animation_; 61} 62 63// Returns whether |window_| is scheduled to be closed when the animation ends. 64- (BOOL)isClosing; 65 66// Animate |window| to show or close it, after cancelling any current animation. 67// Translates from the current location to |targetOrigin| and fades in or out. 68- (void)animateWindow:(NSWindow*)window 69 targetOrigin:(NSPoint)targetOrigin 70 closing:(BOOL)closing; 71 72// Called on the UI thread once the animation has completed to reset the 73// animation state, close the window (if it is a close animation), and possibly 74// terminate Chrome. 75- (void)cleanupOnUIThread; 76 77@end 78 79namespace { 80 81// Version of the app list shortcut version installed. 82const int kShortcutVersion = 2; 83 84// Duration of show and hide animations. 85const NSTimeInterval kAnimationDuration = 0.2; 86 87// Distance towards the screen edge that the app list moves from when showing. 88const CGFloat kDistanceMovedOnShow = 20; 89 90web_app::ShortcutInfo GetAppListShortcutInfo( 91 const base::FilePath& profile_path) { 92 web_app::ShortcutInfo shortcut_info; 93 chrome::VersionInfo::Channel channel = chrome::VersionInfo::GetChannel(); 94 if (channel == chrome::VersionInfo::CHANNEL_CANARY) { 95 shortcut_info.title = 96 l10n_util::GetStringUTF16(IDS_APP_LIST_SHORTCUT_NAME_CANARY); 97 } else { 98 shortcut_info.title = l10n_util::GetStringUTF16(IDS_APP_LIST_SHORTCUT_NAME); 99 } 100 101 shortcut_info.extension_id = app_mode::kAppListModeId; 102 shortcut_info.description = shortcut_info.title; 103 shortcut_info.profile_path = profile_path; 104 105 return shortcut_info; 106} 107 108void CreateAppListShim(const base::FilePath& profile_path) { 109 DCHECK_CURRENTLY_ON(content::BrowserThread::UI); 110 WebApplicationInfo web_app_info; 111 web_app::ShortcutInfo shortcut_info = 112 GetAppListShortcutInfo(profile_path); 113 114 ResourceBundle& resource_bundle = ResourceBundle::GetSharedInstance(); 115 chrome::VersionInfo::Channel channel = chrome::VersionInfo::GetChannel(); 116 if (channel == chrome::VersionInfo::CHANNEL_CANARY) { 117#if defined(GOOGLE_CHROME_BUILD) 118 shortcut_info.favicon.Add( 119 *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_CANARY_16)); 120 shortcut_info.favicon.Add( 121 *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_CANARY_32)); 122 shortcut_info.favicon.Add( 123 *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_CANARY_128)); 124 shortcut_info.favicon.Add( 125 *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_CANARY_256)); 126#else 127 NOTREACHED(); 128#endif 129 } else { 130 shortcut_info.favicon.Add( 131 *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_16)); 132 shortcut_info.favicon.Add( 133 *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_32)); 134 shortcut_info.favicon.Add( 135 *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_128)); 136 shortcut_info.favicon.Add( 137 *resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_256)); 138 } 139 140 web_app::ShortcutLocations shortcut_locations; 141 PrefService* local_state = g_browser_process->local_state(); 142 int installed_version = 143 local_state->GetInteger(prefs::kAppLauncherShortcutVersion); 144 145 // If this is a first-time install, add a dock icon. Otherwise just update 146 // the target, and wait for OSX to refresh its icon caches. This might not 147 // occur until a reboot, but OSX does not offer a nicer way. Deleting cache 148 // files on disk and killing processes can easily result in icon corruption. 149 if (installed_version == 0) 150 shortcut_locations.in_quick_launch_bar = true; 151 152 web_app::CreateShortcutsForShortcutInfo( 153 web_app::SHORTCUT_CREATION_AUTOMATED, 154 shortcut_locations, 155 shortcut_info); 156 157 local_state->SetInteger(prefs::kAppLauncherShortcutVersion, 158 kShortcutVersion); 159} 160 161NSRunningApplication* ActiveApplicationNotChrome() { 162 NSArray* applications = [[NSWorkspace sharedWorkspace] runningApplications]; 163 for (NSRunningApplication* application in applications) { 164 if (![application isActive]) 165 continue; 166 167 if ([application isEqual:[NSRunningApplication currentApplication]]) 168 return nil; // Chrome is active. 169 170 return application; 171 } 172 173 return nil; 174} 175 176// Determines which screen edge the dock is aligned to. 177AppListPositioner::ScreenEdge DockLocationInDisplay( 178 const gfx::Display& display) { 179 // Assume the dock occupies part of the work area either on the left, right or 180 // bottom of the display. Note in the autohide case, it is always 4 pixels. 181 const gfx::Rect work_area = display.work_area(); 182 const gfx::Rect display_bounds = display.bounds(); 183 if (work_area.bottom() != display_bounds.bottom()) 184 return AppListPositioner::SCREEN_EDGE_BOTTOM; 185 186 if (work_area.x() != display_bounds.x()) 187 return AppListPositioner::SCREEN_EDGE_LEFT; 188 189 if (work_area.right() != display_bounds.right()) 190 return AppListPositioner::SCREEN_EDGE_RIGHT; 191 192 return AppListPositioner::SCREEN_EDGE_UNKNOWN; 193} 194 195// If |display|'s work area is too close to its boundary on |dock_edge|, adjust 196// the work area away from the edge by a constant amount to reduce overlap and 197// ensure the dock icon can still be clicked to dismiss the app list. 198void AdjustWorkAreaForDock(const gfx::Display& display, 199 AppListPositioner* positioner, 200 AppListPositioner::ScreenEdge dock_edge) { 201 const int kAutohideDockThreshold = 10; 202 const int kExtraDistance = 50; // A dock with 40 items is about this size. 203 204 const gfx::Rect work_area = display.work_area(); 205 const gfx::Rect display_bounds = display.bounds(); 206 207 switch (dock_edge) { 208 case AppListPositioner::SCREEN_EDGE_LEFT: 209 if (work_area.x() - display_bounds.x() <= kAutohideDockThreshold) 210 positioner->WorkAreaInset(kExtraDistance, 0, 0, 0); 211 break; 212 case AppListPositioner::SCREEN_EDGE_RIGHT: 213 if (display_bounds.right() - work_area.right() <= kAutohideDockThreshold) 214 positioner->WorkAreaInset(0, 0, kExtraDistance, 0); 215 break; 216 case AppListPositioner::SCREEN_EDGE_BOTTOM: 217 if (display_bounds.bottom() - work_area.bottom() <= 218 kAutohideDockThreshold) { 219 positioner->WorkAreaInset(0, 0, 0, kExtraDistance); 220 } 221 break; 222 case AppListPositioner::SCREEN_EDGE_UNKNOWN: 223 case AppListPositioner::SCREEN_EDGE_TOP: 224 NOTREACHED(); 225 break; 226 } 227} 228 229void GetAppListWindowOrigins( 230 NSWindow* window, NSPoint* target_origin, NSPoint* start_origin) { 231 gfx::Screen* const screen = gfx::Screen::GetScreenFor([window contentView]); 232 // Ensure y coordinates are flipped back into AppKit's coordinate system. 233 bool cursor_is_visible = CGCursorIsVisible(); 234 gfx::Display display; 235 gfx::Point cursor; 236 if (!cursor_is_visible) { 237 // If Chrome is the active application, display on the same display as 238 // Chrome's keyWindow since this will catch activations triggered, e.g, via 239 // WebStore install. If another application is active, OSX doesn't provide a 240 // reliable way to get the display in use. Fall back to the primary display 241 // since it has the menu bar and is likely to be correct, e.g., for 242 // activations from Spotlight. 243 const gfx::NativeView key_view = [[NSApp keyWindow] contentView]; 244 display = key_view && [NSApp isActive] ? 245 screen->GetDisplayNearestWindow(key_view) : 246 screen->GetPrimaryDisplay(); 247 } else { 248 cursor = screen->GetCursorScreenPoint(); 249 display = screen->GetDisplayNearestPoint(cursor); 250 } 251 252 const NSSize ns_window_size = [window frame].size; 253 gfx::Size window_size(ns_window_size.width, ns_window_size.height); 254 int primary_display_height = 255 NSMaxY([[[NSScreen screens] objectAtIndex:0] frame]); 256 AppListServiceMac::FindAnchorPoint(window_size, 257 display, 258 primary_display_height, 259 cursor_is_visible, 260 cursor, 261 target_origin, 262 start_origin); 263} 264 265} // namespace 266 267AppListServiceMac::AppListServiceMac() 268 : profile_(NULL), 269 controller_delegate_(new AppListControllerDelegateImpl(this)) { 270 animation_controller_.reset([[AppListAnimationController alloc] init]); 271} 272 273AppListServiceMac::~AppListServiceMac() {} 274 275// static 276AppListServiceMac* AppListServiceMac::GetInstance() { 277 return Singleton<AppListServiceMac, 278 LeakySingletonTraits<AppListServiceMac> >::get(); 279} 280 281// static 282void AppListServiceMac::FindAnchorPoint(const gfx::Size& window_size, 283 const gfx::Display& display, 284 int primary_display_height, 285 bool cursor_is_visible, 286 const gfx::Point& cursor, 287 NSPoint* target_origin, 288 NSPoint* start_origin) { 289 AppListPositioner positioner(display, window_size, 0); 290 AppListPositioner::ScreenEdge dock_location = DockLocationInDisplay(display); 291 292 gfx::Point anchor; 293 // Snap to the dock edge. If the cursor is greater than the window 294 // width/height away or not visible, anchor to the center of the dock. 295 // Otherwise, anchor to the cursor position. 296 if (dock_location == AppListPositioner::SCREEN_EDGE_UNKNOWN) { 297 anchor = positioner.GetAnchorPointForScreenCorner( 298 AppListPositioner::SCREEN_CORNER_BOTTOM_LEFT); 299 } else { 300 int snap_distance = 301 dock_location == AppListPositioner::SCREEN_EDGE_BOTTOM || 302 dock_location == AppListPositioner::SCREEN_EDGE_TOP ? 303 window_size.height() : 304 window_size.width(); 305 // Subtract the dock area since the display's default work_area will not 306 // subtract it if the dock is set to auto-hide, and the app list should 307 // never overlap the dock. 308 AdjustWorkAreaForDock(display, &positioner, dock_location); 309 if (!cursor_is_visible || positioner.GetCursorDistanceFromShelf( 310 dock_location, cursor) > snap_distance) { 311 anchor = positioner.GetAnchorPointForShelfCenter(dock_location); 312 } else { 313 anchor = positioner.GetAnchorPointForShelfCursor(dock_location, cursor); 314 } 315 } 316 317 *target_origin = NSMakePoint( 318 anchor.x() - window_size.width() / 2, 319 primary_display_height - anchor.y() - window_size.height() / 2); 320 *start_origin = *target_origin; 321 322 // If the launcher is anchored to the dock (regardless of whether the cursor 323 // is visible), animate in inwards from the edge of screen 324 switch (dock_location) { 325 case AppListPositioner::SCREEN_EDGE_UNKNOWN: 326 break; 327 case AppListPositioner::SCREEN_EDGE_LEFT: 328 start_origin->x -= kDistanceMovedOnShow; 329 break; 330 case AppListPositioner::SCREEN_EDGE_RIGHT: 331 start_origin->x += kDistanceMovedOnShow; 332 break; 333 case AppListPositioner::SCREEN_EDGE_TOP: 334 NOTREACHED(); 335 break; 336 case AppListPositioner::SCREEN_EDGE_BOTTOM: 337 start_origin->y -= kDistanceMovedOnShow; 338 break; 339 } 340} 341 342void AppListServiceMac::Init(Profile* initial_profile) { 343 // On Mac, Init() is called multiple times for a process: any time there is no 344 // browser window open and a new window is opened, and during process startup 345 // to handle the silent launch case (e.g. for app shims). In the startup case, 346 // a profile has not yet been determined so |initial_profile| will be NULL. 347 static bool init_called_with_profile = false; 348 if (initial_profile && !init_called_with_profile) { 349 init_called_with_profile = true; 350 PerformStartupChecks(initial_profile); 351 PrefService* local_state = g_browser_process->local_state(); 352 if (!IsAppLauncherEnabled()) { 353 local_state->SetInteger(prefs::kAppLauncherShortcutVersion, 0); 354 } else { 355 int installed_shortcut_version = 356 local_state->GetInteger(prefs::kAppLauncherShortcutVersion); 357 358 if (kShortcutVersion > installed_shortcut_version) 359 CreateShortcut(); 360 } 361 } 362 363 static bool init_called = false; 364 if (init_called) 365 return; 366 367 init_called = true; 368 apps::AppShimHandler::RegisterHandler(app_mode::kAppListModeId, 369 AppListServiceMac::GetInstance()); 370 371 // Handle the case where Chrome was not running and was started with the app 372 // launcher shim. The profile has not yet been loaded. To improve response 373 // times, start animating an empty window which will be populated via 374 // OnShimLaunch(). Note that if --silent-launch is not also passed, the window 375 // will instead populate via StartupBrowserCreator::Launch(). Shim-initiated 376 // launches will always have --silent-launch. 377 if (CommandLine::ForCurrentProcess()->HasSwitch(switches::kShowAppList)) 378 ShowWindowNearDock(); 379} 380 381Profile* AppListServiceMac::GetCurrentAppListProfile() { 382 return profile_; 383} 384 385void AppListServiceMac::CreateForProfile(Profile* requested_profile) { 386 if (profile_ == requested_profile) 387 return; 388 389 profile_ = requested_profile; 390 391 if (!window_controller_) 392 window_controller_.reset([[AppListWindowController alloc] init]); 393 394 scoped_ptr<app_list::AppListViewDelegate> delegate( 395 new AppListViewDelegate(profile_, GetControllerDelegate())); 396 [[window_controller_ appListViewController] setDelegate:delegate.Pass()]; 397} 398 399void AppListServiceMac::ShowForProfile(Profile* requested_profile) { 400 if (requested_profile->IsSupervised()) 401 return; 402 403 InvalidatePendingProfileLoads(); 404 405 if (requested_profile == profile_) { 406 ShowWindowNearDock(); 407 return; 408 } 409 410 SetProfilePath(requested_profile->GetPath()); 411 CreateForProfile(requested_profile); 412 ShowWindowNearDock(); 413} 414 415void AppListServiceMac::DismissAppList() { 416 if (!IsAppListVisible()) 417 return; 418 419 // If the app list is currently the main window, it will activate the next 420 // Chrome window when dismissed. But if a different application was active 421 // when the app list was shown, activate that instead. 422 base::scoped_nsobject<NSRunningApplication> prior_app; 423 if ([[window_controller_ window] isMainWindow]) 424 prior_app.swap(previously_active_application_); 425 else 426 previously_active_application_.reset(); 427 428 // If activation is successful, the app list will lose main status and try to 429 // close itself again. It can't be closed in this runloop iteration without 430 // OSX deciding to raise the next Chrome window, and _then_ activating the 431 // application on top. This also occurs if no activation option is given. 432 if ([prior_app activateWithOptions:NSApplicationActivateIgnoringOtherApps]) 433 return; 434 435 [animation_controller_ animateWindow:[window_controller_ window] 436 targetOrigin:last_start_origin_ 437 closing:YES]; 438} 439 440bool AppListServiceMac::IsAppListVisible() const { 441 return [[window_controller_ window] isVisible] && 442 ![animation_controller_ isClosing]; 443} 444 445void AppListServiceMac::EnableAppList(Profile* initial_profile, 446 AppListEnableSource enable_source) { 447 AppListServiceImpl::EnableAppList(initial_profile, enable_source); 448 AppController* controller = [NSApp delegate]; 449 [controller initAppShimMenuController]; 450} 451 452void AppListServiceMac::CreateShortcut() { 453 CreateAppListShim(GetProfilePath( 454 g_browser_process->profile_manager()->user_data_dir())); 455} 456 457NSWindow* AppListServiceMac::GetAppListWindow() { 458 return [window_controller_ window]; 459} 460 461AppListControllerDelegate* AppListServiceMac::GetControllerDelegate() { 462 return controller_delegate_.get(); 463} 464 465void AppListServiceMac::OnShimLaunch(apps::AppShimHandler::Host* host, 466 apps::AppShimLaunchType launch_type, 467 const std::vector<base::FilePath>& files) { 468 if (profile_ && IsAppListVisible()) { 469 DismissAppList(); 470 } else { 471 // Start by showing a possibly empty window to handle the case where Chrome 472 // is running, but hasn't yet loaded the app launcher profile. 473 ShowWindowNearDock(); 474 Show(); 475 } 476 477 // Always close the shim process immediately. 478 host->OnAppLaunchComplete(apps::APP_SHIM_LAUNCH_DUPLICATE_HOST); 479} 480 481void AppListServiceMac::OnShimClose(apps::AppShimHandler::Host* host) {} 482 483void AppListServiceMac::OnShimFocus(apps::AppShimHandler::Host* host, 484 apps::AppShimFocusType focus_type, 485 const std::vector<base::FilePath>& files) {} 486 487void AppListServiceMac::OnShimSetHidden(apps::AppShimHandler::Host* host, 488 bool hidden) {} 489 490void AppListServiceMac::OnShimQuit(apps::AppShimHandler::Host* host) {} 491 492void AppListServiceMac::ShowWindowNearDock() { 493 if (IsAppListVisible()) 494 return; 495 496 if (!window_controller_) { 497 // Note that this will start showing an unpopulated window, the caller needs 498 // to ensure it will be populated later. 499 window_controller_.reset([[AppListWindowController alloc] init]); 500 } 501 502 NSWindow* window = GetAppListWindow(); 503 DCHECK(window); 504 NSPoint target_origin; 505 GetAppListWindowOrigins(window, &target_origin, &last_start_origin_); 506 [window setFrameOrigin:last_start_origin_]; 507 508 // Before activating, see if an application other than Chrome is currently the 509 // active application, so that it can be reactivated when dismissing. 510 previously_active_application_.reset([ActiveApplicationNotChrome() retain]); 511 512 [animation_controller_ animateWindow:[window_controller_ window] 513 targetOrigin:target_origin 514 closing:NO]; 515 [window makeKeyAndOrderFront:nil]; 516 [NSApp activateIgnoringOtherApps:YES]; 517 RecordAppListLaunch(); 518} 519 520void AppListServiceMac::WindowAnimationDidEnd() { 521 [animation_controller_ cleanupOnUIThread]; 522} 523 524// static 525AppListService* AppListService::Get(chrome::HostDesktopType desktop_type) { 526 return AppListServiceMac::GetInstance(); 527} 528 529// static 530void AppListService::InitAll(Profile* initial_profile) { 531 AppListServiceMac::GetInstance()->Init(initial_profile); 532} 533 534@implementation AppListAnimationController 535 536- (BOOL)isClosing { 537 return !!window_; 538} 539 540- (void)animateWindow:(NSWindow*)window 541 targetOrigin:(NSPoint)targetOrigin 542 closing:(BOOL)closing { 543 // First, stop the existing animation, if there is one. 544 [animation_ stopAnimation]; 545 546 NSRect targetFrame = [window frame]; 547 targetFrame.origin = targetOrigin; 548 549 // NSViewAnimation has a quirk when setting the curve to NSAnimationEaseOut 550 // where it attempts to auto-reverse the animation. FadeOut becomes FadeIn 551 // (good), but FrameKey is also switched (bad). So |targetFrame| needs to be 552 // put on the StartFrameKey when using NSAnimationEaseOut for showing. 553 NSArray* animationArray = @[ 554 @{ 555 NSViewAnimationTargetKey : window, 556 NSViewAnimationEffectKey : NSViewAnimationFadeOutEffect, 557 (closing ? NSViewAnimationEndFrameKey : NSViewAnimationStartFrameKey) : 558 [NSValue valueWithRect:targetFrame] 559 } 560 ]; 561 animation_.reset( 562 [[NSViewAnimation alloc] initWithViewAnimations:animationArray]); 563 [animation_ setDuration:kAnimationDuration]; 564 [animation_ setDelegate:self]; 565 566 if (closing) { 567 [animation_ setAnimationCurve:NSAnimationEaseIn]; 568 window_.reset([window retain]); 569 } else { 570 [window setAlphaValue:0.0f]; 571 [animation_ setAnimationCurve:NSAnimationEaseOut]; 572 window_.reset(); 573 } 574 // Threaded animations are buggy on Snow Leopard. See http://crbug.com/335550. 575 // Note that in the non-threaded case, the animation won't start unless the 576 // UI runloop has spun up, so on <= Lion the animation will only animate if 577 // Chrome is already running. 578 if (base::mac::IsOSMountainLionOrLater()) 579 [animation_ setAnimationBlockingMode:NSAnimationNonblockingThreaded]; 580 else 581 [animation_ setAnimationBlockingMode:NSAnimationNonblocking]; 582 583 [animation_ startAnimation]; 584} 585 586- (void)cleanupOnUIThread { 587 bool closing = [self isClosing]; 588 [window_ close]; 589 window_.reset(); 590 animation_.reset(); 591 592 if (closing) 593 apps::AppShimHandler::MaybeTerminate(); 594} 595 596- (void)animationDidEnd:(NSAnimation*)animation { 597 content::BrowserThread::PostTask( 598 content::BrowserThread::UI, 599 FROM_HERE, 600 base::Bind(&AppListServiceMac::WindowAnimationDidEnd, 601 base::Unretained(AppListServiceMac::GetInstance()))); 602} 603 604@end 605