extension_installed_bubble_controller.mm revision 5821806d5e7f356e8fa4b058a389a808ea183019
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. 4 5#import "chrome/browser/ui/cocoa/extensions/extension_installed_bubble_controller.h" 6 7#include "base/i18n/rtl.h" 8#include "base/mac/bundle_locations.h" 9#include "base/mac/mac_util.h" 10#include "base/sys_string_conversions.h" 11#include "base/utf_string_conversions.h" 12#include "chrome/browser/extensions/api/commands/command_service.h" 13#include "chrome/browser/extensions/api/commands/command_service_factory.h" 14#include "chrome/browser/extensions/bundle_installer.h" 15#include "chrome/browser/extensions/extension_action.h" 16#include "chrome/browser/extensions/extension_action_manager.h" 17#include "chrome/browser/ui/browser.h" 18#include "chrome/browser/ui/browser_navigator.h" 19#include "chrome/browser/ui/browser_window.h" 20#include "chrome/browser/ui/cocoa/browser_window_cocoa.h" 21#include "chrome/browser/ui/cocoa/browser_window_controller.h" 22#include "chrome/browser/ui/cocoa/extensions/browser_actions_controller.h" 23#include "chrome/browser/ui/cocoa/hover_close_button.h" 24#include "chrome/browser/ui/cocoa/info_bubble_view.h" 25#include "chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.h" 26#include "chrome/browser/ui/cocoa/toolbar/toolbar_controller.h" 27#include "chrome/browser/ui/singleton_tabs.h" 28#include "chrome/common/chrome_notification_types.h" 29#include "chrome/common/extensions/extension.h" 30#include "chrome/common/url_constants.h" 31#include "content/public/browser/notification_details.h" 32#include "content/public/browser/notification_registrar.h" 33#include "content/public/browser/notification_source.h" 34#include "grit/chromium_strings.h" 35#include "grit/generated_resources.h" 36#import "skia/ext/skia_utils_mac.h" 37#import "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h" 38#include "ui/base/l10n/l10n_util.h" 39 40using content::BrowserThread; 41using extensions::BundleInstaller; 42using extensions::Extension; 43using extensions::UnloadedExtensionInfo; 44 45// C++ class that receives EXTENSION_LOADED notifications and proxies them back 46// to |controller|. 47class ExtensionLoadedNotificationObserver 48 : public content::NotificationObserver { 49 public: 50 ExtensionLoadedNotificationObserver( 51 ExtensionInstalledBubbleController* controller, Profile* profile) 52 : controller_(controller) { 53 registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_LOADED, 54 content::Source<Profile>(profile)); 55 registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_UNLOADED, 56 content::Source<Profile>(profile)); 57 } 58 59 private: 60 // NotificationObserver implementation. Tells the controller to start showing 61 // its window on the main thread when the extension has finished loading. 62 void Observe(int type, 63 const content::NotificationSource& source, 64 const content::NotificationDetails& details) { 65 if (type == chrome::NOTIFICATION_EXTENSION_LOADED) { 66 const Extension* extension = 67 content::Details<const Extension>(details).ptr(); 68 if (extension == [controller_ extension]) { 69 [controller_ performSelectorOnMainThread:@selector(showWindow:) 70 withObject:controller_ 71 waitUntilDone:NO]; 72 } 73 } else if (type == chrome::NOTIFICATION_EXTENSION_UNLOADED) { 74 const Extension* extension = 75 content::Details<const UnloadedExtensionInfo>(details)->extension; 76 if (extension == [controller_ extension]) { 77 [controller_ performSelectorOnMainThread:@selector(extensionUnloaded:) 78 withObject:controller_ 79 waitUntilDone:NO]; 80 } 81 } else { 82 NOTREACHED() << "Received unexpected notification."; 83 } 84 } 85 86 content::NotificationRegistrar registrar_; 87 ExtensionInstalledBubbleController* controller_; // weak, owns us 88}; 89 90@implementation ExtensionInstalledBubbleController 91 92@synthesize extension = extension_; 93@synthesize bundle = bundle_; 94// Exposed for unit test. 95@synthesize pageActionPreviewShowing = pageActionPreviewShowing_; 96 97- (id)initWithParentWindow:(NSWindow*)parentWindow 98 extension:(const Extension*)extension 99 bundle:(const BundleInstaller*)bundle 100 browser:(Browser*)browser 101 icon:(SkBitmap)icon { 102 NSString* nibName = bundle ? @"ExtensionInstalledBubbleBundle" : 103 @"ExtensionInstalledBubble"; 104 if ((self = [super initWithWindowNibPath:nibName 105 parentWindow:parentWindow 106 anchoredAt:NSZeroPoint])) { 107 extension_ = extension; 108 bundle_ = bundle; 109 DCHECK(browser); 110 browser_ = browser; 111 icon_.reset([gfx::SkBitmapToNSImage(icon) retain]); 112 pageActionPreviewShowing_ = NO; 113 114 extensions::ExtensionActionManager* extension_action_manager = 115 extensions::ExtensionActionManager::Get(browser_->profile()); 116 117 if (bundle_) { 118 type_ = extension_installed_bubble::kBundle; 119 } else if (!extension->omnibox_keyword().empty()) { 120 type_ = extension_installed_bubble::kOmniboxKeyword; 121 } else if (extension_action_manager->GetBrowserAction(*extension)) { 122 type_ = extension_installed_bubble::kBrowserAction; 123 } else if (extension_action_manager->GetPageAction(*extension) && 124 extension->is_verbose_install_message()) { 125 type_ = extension_installed_bubble::kPageAction; 126 } else { 127 type_ = extension_installed_bubble::kGeneric; 128 } 129 130 if (type_ == extension_installed_bubble::kBundle) { 131 [self showWindow:self]; 132 } else { 133 // Start showing window only after extension has fully loaded. 134 extensionObserver_.reset(new ExtensionLoadedNotificationObserver( 135 self, browser->profile())); 136 } 137 } 138 return self; 139} 140 141- (void)windowWillClose:(NSNotification*)notification { 142 // Turn off page action icon preview when the window closes, unless we 143 // already removed it when the window resigned key status. 144 [self removePageActionPreviewIfNecessary]; 145 extension_ = NULL; 146 browser_ = NULL; 147 148 [super windowWillClose:notification]; 149} 150 151// The controller is the delegate of the window, so it receives "did resign 152// key" notifications. When key is resigned, close the window. 153- (void)windowDidResignKey:(NSNotification*)notification { 154 // If the browser window is closing, we need to remove the page action 155 // immediately, otherwise the closing animation may overlap with 156 // browser destruction. 157 [self removePageActionPreviewIfNecessary]; 158 [super windowDidResignKey:notification]; 159} 160 161- (IBAction)closeWindow:(id)sender { 162 DCHECK([[self window] isVisible]); 163 [self close]; 164} 165 166// Extracted to a function here so that it can be overridden for unit testing. 167- (void)removePageActionPreviewIfNecessary { 168 if (!extension_ || !pageActionPreviewShowing_) 169 return; 170 ExtensionAction* page_action = 171 extensions::ExtensionActionManager::Get(browser_->profile())-> 172 GetPageAction(*extension_); 173 if (!page_action) 174 return; 175 pageActionPreviewShowing_ = NO; 176 177 BrowserWindowCocoa* window = 178 static_cast<BrowserWindowCocoa*>(browser_->window()); 179 LocationBarViewMac* locationBarView = 180 [window->cocoa_controller() locationBarBridge]; 181 locationBarView->SetPreviewEnabledPageAction(page_action, 182 false); // disables preview. 183} 184 185// The extension installed bubble points at the browser action icon or the 186// page action icon (shown as a preview), depending on the extension type. 187// We need to calculate the location of these icons and the size of the 188// message itself (which varies with the title of the extension) in order 189// to figure out the origin point for the extension installed bubble. 190// TODO(mirandac): add framework to easily test extension UI components! 191- (NSPoint)calculateArrowPoint { 192 BrowserWindowCocoa* window = 193 static_cast<BrowserWindowCocoa*>(browser_->window()); 194 NSPoint arrowPoint = NSZeroPoint; 195 196 switch(type_) { 197 case extension_installed_bubble::kOmniboxKeyword: { 198 LocationBarViewMac* locationBarView = 199 [window->cocoa_controller() locationBarBridge]; 200 arrowPoint = locationBarView->GetPageInfoBubblePoint(); 201 break; 202 } 203 case extension_installed_bubble::kBrowserAction: { 204 BrowserActionsController* controller = 205 [[window->cocoa_controller() toolbarController] 206 browserActionsController]; 207 arrowPoint = [controller popupPointForBrowserAction:extension_]; 208 break; 209 } 210 case extension_installed_bubble::kPageAction: { 211 LocationBarViewMac* locationBarView = 212 [window->cocoa_controller() locationBarBridge]; 213 214 ExtensionAction* page_action = 215 extensions::ExtensionActionManager::Get(browser_->profile())-> 216 GetPageAction(*extension_); 217 218 // Tell the location bar to show a preview of the page action icon, which 219 // would ordinarily only be displayed on a page of the appropriate type. 220 // We remove this preview when the extension installed bubble closes. 221 locationBarView->SetPreviewEnabledPageAction(page_action, true); 222 pageActionPreviewShowing_ = YES; 223 224 // Find the center of the bottom of the page action icon. 225 arrowPoint = 226 locationBarView->GetPageActionBubblePoint(page_action); 227 break; 228 } 229 case extension_installed_bubble::kBundle: 230 case extension_installed_bubble::kGeneric: { 231 // Point at the bottom of the wrench menu. 232 NSView* wrenchButton = 233 [[window->cocoa_controller() toolbarController] wrenchButton]; 234 const NSRect bounds = [wrenchButton bounds]; 235 NSPoint anchor = NSMakePoint(NSMidX(bounds), NSMaxY(bounds)); 236 arrowPoint = [wrenchButton convertPoint:anchor toView:nil]; 237 break; 238 } 239 default: { 240 NOTREACHED(); 241 } 242 } 243 return arrowPoint; 244} 245 246// Override -[BaseBubbleController showWindow:] to tweak bubble location and 247// set up UI elements. 248- (void)showWindow:(id)sender { 249 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 250 251 // Load nib and calculate height based on messages to be shown. 252 NSWindow* window = [self initializeWindow]; 253 int newWindowHeight = [self calculateWindowHeight]; 254 [self.bubble setFrameSize:NSMakeSize( 255 NSWidth([[window contentView] bounds]), newWindowHeight)]; 256 NSSize windowDelta = NSMakeSize( 257 0, newWindowHeight - NSHeight([[window contentView] bounds])); 258 windowDelta = [[window contentView] convertSize:windowDelta toView:nil]; 259 NSRect newFrame = [window frame]; 260 newFrame.size.height += windowDelta.height; 261 [window setFrame:newFrame display:NO]; 262 263 // Now that we have resized the window, adjust y pos of the messages. 264 [self setMessageFrames:newWindowHeight]; 265 266 // Find window origin, taking into account bubble size and arrow location. 267 self.anchorPoint = 268 [self.parentWindow convertBaseToScreen:[self calculateArrowPoint]]; 269 [super showWindow:sender]; 270} 271 272// Finish nib loading, set arrow location and load icon into window. This 273// function is exposed for unit testing. 274- (NSWindow*)initializeWindow { 275 NSWindow* window = [self window]; // completes nib load 276 277 if (type_ == extension_installed_bubble::kOmniboxKeyword) { 278 [self.bubble setArrowLocation:info_bubble::kTopLeft]; 279 } else { 280 [self.bubble setArrowLocation:info_bubble::kTopRight]; 281 } 282 283 if (type_ == extension_installed_bubble::kBundle) 284 return window; 285 286 // Set appropriate icon, resizing if necessary. 287 if ([icon_ size].width > extension_installed_bubble::kIconSize) { 288 [icon_ setSize:NSMakeSize(extension_installed_bubble::kIconSize, 289 extension_installed_bubble::kIconSize)]; 290 } 291 [iconImage_ setImage:icon_]; 292 [iconImage_ setNeedsDisplay:YES]; 293 return window; 294} 295 296- (bool)hasActivePageAction:(extensions::Command*)command { 297 extensions::CommandService* command_service = 298 extensions::CommandServiceFactory::GetForProfile(browser_->profile()); 299 if (type_ == extension_installed_bubble::kPageAction) { 300 if (extension_->page_action_command() && 301 command_service->GetPageActionCommand( 302 extension_->id(), 303 extensions::CommandService::ACTIVE_ONLY, 304 command, 305 NULL)) { 306 return true; 307 } 308 } 309 310 return false; 311} 312 313- (bool)hasActiveBrowserAction:(extensions::Command*)command { 314 extensions::CommandService* command_service = 315 extensions::CommandServiceFactory::GetForProfile(browser_->profile()); 316 if (type_ == extension_installed_bubble::kBrowserAction) { 317 if (extension_->browser_action_command() && 318 command_service->GetBrowserActionCommand( 319 extension_->id(), 320 extensions::CommandService::ACTIVE_ONLY, 321 command, 322 NULL)) { 323 return true; 324 } 325 } 326 327 return false; 328} 329 330- (NSString*)installMessageForCurrentExtensionAction { 331 if (type_ == extension_installed_bubble::kPageAction) { 332 extensions::Command page_action_command; 333 if ([self hasActivePageAction:&page_action_command]) { 334 return l10n_util::GetNSStringF( 335 IDS_EXTENSION_INSTALLED_PAGE_ACTION_INFO_WITH_SHORTCUT, 336 page_action_command.accelerator().GetShortcutText()); 337 } else { 338 return l10n_util::GetNSString( 339 IDS_EXTENSION_INSTALLED_PAGE_ACTION_INFO); 340 } 341 } else { 342 CHECK_EQ(extension_installed_bubble::kBrowserAction, type_); 343 extensions::Command browser_action_command; 344 if ([self hasActiveBrowserAction:&browser_action_command]) { 345 return l10n_util::GetNSStringF( 346 IDS_EXTENSION_INSTALLED_BROWSER_ACTION_INFO_WITH_SHORTCUT, 347 browser_action_command.accelerator().GetShortcutText()); 348 } else { 349 return l10n_util::GetNSString( 350 IDS_EXTENSION_INSTALLED_BROWSER_ACTION_INFO); 351 } 352 } 353} 354 355// Calculate the height of each install message, resizing messages in their 356// frames to fit window width. Return the new window height, based on the 357// total of all message heights. 358- (int)calculateWindowHeight { 359 // Adjust the window height to reflect the sum height of all messages 360 // and vertical padding. 361 int newWindowHeight = 2 * extension_installed_bubble::kOuterVerticalMargin; 362 363 // First part of extension installed message. 364 if (type_ != extension_installed_bubble::kBundle) { 365 string16 extension_name = UTF8ToUTF16(extension_->name().c_str()); 366 base::i18n::AdjustStringForLocaleDirection(&extension_name); 367 [extensionInstalledMsg_ setStringValue:l10n_util::GetNSStringF( 368 IDS_EXTENSION_INSTALLED_HEADING, extension_name)]; 369 [GTMUILocalizerAndLayoutTweaker 370 sizeToFitFixedWidthTextField:extensionInstalledMsg_]; 371 newWindowHeight += [extensionInstalledMsg_ frame].size.height + 372 extension_installed_bubble::kInnerVerticalMargin; 373 } 374 375 // If type is page action, include a special message about page actions. 376 if (type_ == extension_installed_bubble::kBrowserAction || 377 type_ == extension_installed_bubble::kPageAction) { 378 [extraInfoMsg_ setStringValue:[self 379 installMessageForCurrentExtensionAction]]; 380 [extraInfoMsg_ setHidden:NO]; 381 [[extraInfoMsg_ cell] 382 setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; 383 [GTMUILocalizerAndLayoutTweaker 384 sizeToFitFixedWidthTextField:extraInfoMsg_]; 385 newWindowHeight += [extraInfoMsg_ frame].size.height + 386 extension_installed_bubble::kInnerVerticalMargin; 387 } 388 389 // If type is omnibox keyword, include a special message about the keyword. 390 if (type_ == extension_installed_bubble::kOmniboxKeyword) { 391 [extraInfoMsg_ setStringValue:l10n_util::GetNSStringF( 392 IDS_EXTENSION_INSTALLED_OMNIBOX_KEYWORD_INFO, 393 UTF8ToUTF16(extension_->omnibox_keyword()))]; 394 [extraInfoMsg_ setHidden:NO]; 395 [[extraInfoMsg_ cell] 396 setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; 397 [GTMUILocalizerAndLayoutTweaker 398 sizeToFitFixedWidthTextField:extraInfoMsg_]; 399 newWindowHeight += [extraInfoMsg_ frame].size.height + 400 extension_installed_bubble::kInnerVerticalMargin; 401 } 402 403 // If type is bundle, list the extensions that were installed and those that 404 // failed. 405 if (type_ == extension_installed_bubble::kBundle) { 406 NSInteger installedListHeight = 407 [self addExtensionList:installedHeadingMsg_ 408 itemsMsg:installedItemsMsg_ 409 state:BundleInstaller::Item::STATE_INSTALLED]; 410 411 NSInteger failedListHeight = 412 [self addExtensionList:failedHeadingMsg_ 413 itemsMsg:failedItemsMsg_ 414 state:BundleInstaller::Item::STATE_FAILED]; 415 416 newWindowHeight += installedListHeight + failedListHeight; 417 418 // Put some space between the lists if both are present. 419 if (installedListHeight > 0 && failedListHeight > 0) 420 newWindowHeight += extension_installed_bubble::kInnerVerticalMargin; 421 } else { 422 // Second part of extension installed message. 423 [[extensionInstalledInfoMsg_ cell] 424 setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; 425 [GTMUILocalizerAndLayoutTweaker 426 sizeToFitFixedWidthTextField:extensionInstalledInfoMsg_]; 427 newWindowHeight += [extensionInstalledInfoMsg_ frame].size.height; 428 } 429 430 extensions::Command command; 431 if ([self hasActivePageAction:&command] || 432 [self hasActiveBrowserAction:&command]) { 433 [manageShortcutLink_ setHidden:NO]; 434 [[manageShortcutLink_ cell] 435 setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; 436 newWindowHeight += 2 * extension_installed_bubble::kInnerVerticalMargin; 437 newWindowHeight += [GTMUILocalizerAndLayoutTweaker 438 sizeToFitView:manageShortcutLink_].height; 439 newWindowHeight += extension_installed_bubble::kInnerVerticalMargin; 440 } 441 442 return newWindowHeight; 443} 444 445- (NSInteger)addExtensionList:(NSTextField*)headingMsg 446 itemsMsg:(NSTextField*)itemsMsg 447 state:(BundleInstaller::Item::State)state { 448 string16 heading = bundle_->GetHeadingTextFor(state); 449 bool hidden = heading.empty(); 450 [headingMsg setHidden:hidden]; 451 [itemsMsg setHidden:hidden]; 452 if (hidden) 453 return 0; 454 455 [headingMsg setStringValue:base::SysUTF16ToNSString(heading)]; 456 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:headingMsg]; 457 458 NSMutableString* joinedItems = [NSMutableString string]; 459 BundleInstaller::ItemList items = bundle_->GetItemsWithState(state); 460 for (size_t i = 0; i < items.size(); ++i) { 461 if (i > 0) 462 [joinedItems appendString:@"\n"]; 463 [joinedItems appendString:base::SysUTF16ToNSString( 464 items[i].GetNameForDisplay())]; 465 } 466 467 [itemsMsg setStringValue:joinedItems]; 468 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:itemsMsg]; 469 470 return [headingMsg frame].size.height + 471 extension_installed_bubble::kInnerVerticalMargin + 472 [itemsMsg frame].size.height; 473} 474 475// Adjust y-position of messages to sit properly in new window height. 476- (void)setMessageFrames:(int)newWindowHeight { 477 if (type_ == extension_installed_bubble::kBundle) { 478 // Layout the messages from the bottom up. 479 NSTextField* msgs[] = { failedItemsMsg_, failedHeadingMsg_, 480 installedItemsMsg_, installedHeadingMsg_ }; 481 NSInteger offsetFromBottom = 0; 482 BOOL isFirstVisible = YES; 483 for (size_t i = 0; i < arraysize(msgs); ++i) { 484 if ([msgs[i] isHidden]) 485 continue; 486 487 NSRect frame = [msgs[i] frame]; 488 NSInteger margin = isFirstVisible ? 489 extension_installed_bubble::kOuterVerticalMargin : 490 extension_installed_bubble::kInnerVerticalMargin; 491 492 frame.origin.y = offsetFromBottom + margin; 493 [msgs[i] setFrame:frame]; 494 offsetFromBottom += frame.size.height + margin; 495 496 isFirstVisible = NO; 497 } 498 499 // Move the close button a bit to vertically align it with the heading. 500 NSInteger closeButtonFudge = 1; 501 NSRect frame = [closeButton_ frame]; 502 frame.origin.y = newWindowHeight - (frame.size.height + closeButtonFudge + 503 extension_installed_bubble::kOuterVerticalMargin); 504 [closeButton_ setFrame:frame]; 505 506 return; 507 } 508 509 NSRect extensionMessageFrame1 = [extensionInstalledMsg_ frame]; 510 NSRect extensionMessageFrame2 = [extensionInstalledInfoMsg_ frame]; 511 512 extensionMessageFrame1.origin.y = newWindowHeight - ( 513 extensionMessageFrame1.size.height + 514 extension_installed_bubble::kOuterVerticalMargin); 515 [extensionInstalledMsg_ setFrame:extensionMessageFrame1]; 516 if (extension_->is_verbose_install_message()) { 517 // The extra message is only shown when appropriate. 518 NSRect extraMessageFrame = [extraInfoMsg_ frame]; 519 extraMessageFrame.origin.y = extensionMessageFrame1.origin.y - ( 520 extraMessageFrame.size.height + 521 extension_installed_bubble::kInnerVerticalMargin); 522 [extraInfoMsg_ setFrame:extraMessageFrame]; 523 extensionMessageFrame2.origin.y = extraMessageFrame.origin.y - ( 524 extensionMessageFrame2.size.height + 525 extension_installed_bubble::kInnerVerticalMargin); 526 } else { 527 extensionMessageFrame2.origin.y = extensionMessageFrame1.origin.y - ( 528 extensionMessageFrame2.size.height + 529 extension_installed_bubble::kInnerVerticalMargin); 530 } 531 [extensionInstalledInfoMsg_ setFrame:extensionMessageFrame2]; 532 533 extensions::Command command; 534 if (![manageShortcutLink_ isHidden]) { 535 NSRect manageShortcutFrame = [manageShortcutLink_ frame]; 536 manageShortcutFrame.origin.y = NSMinY(extensionMessageFrame2) - ( 537 NSHeight(manageShortcutFrame) + 538 extension_installed_bubble::kInnerVerticalMargin); 539 // Right-align the link. 540 manageShortcutFrame.origin.x = NSMaxX(extensionMessageFrame2) - 541 NSWidth(manageShortcutFrame); 542 [manageShortcutLink_ setFrame:manageShortcutFrame]; 543 } 544} 545 546// Exposed for unit testing. 547- (NSRect)getExtensionInstalledMsgFrame { 548 return [extensionInstalledMsg_ frame]; 549} 550 551- (NSRect)getExtraInfoMsgFrame { 552 return [extraInfoMsg_ frame]; 553} 554 555- (NSRect)getExtensionInstalledInfoMsgFrame { 556 return [extensionInstalledInfoMsg_ frame]; 557} 558 559- (void)extensionUnloaded:(id)sender { 560 extension_ = NULL; 561} 562 563- (IBAction)onManageShortcutClicked:(id)sender { 564 [self close]; 565 std::string configure_url = chrome::kChromeUIExtensionsURL; 566 configure_url += chrome::kExtensionConfigureCommandsSubPage; 567 chrome::NavigateParams params(chrome::GetSingletonTabNavigateParams( 568 browser_, GURL(configure_url))); 569 chrome::Navigate(¶ms); 570} 571 572@end 573