Index: ios/chrome/browser/ui/dialogs/dialog_presenter.mm |
diff --git a/ios/chrome/browser/ui/dialogs/dialog_presenter.mm b/ios/chrome/browser/ui/dialogs/dialog_presenter.mm |
new file mode 100644 |
index 0000000000000000000000000000000000000000..552826677beb7442a653a12a7ccbe3d29e17e0a4 |
--- /dev/null |
+++ b/ios/chrome/browser/ui/dialogs/dialog_presenter.mm |
@@ -0,0 +1,545 @@ |
+// Copyright 2015 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 "ios/chrome/browser/ui/dialogs/dialog_presenter.h" |
+ |
+#include <deque> |
+#include <map> |
+ |
+#import "base/ios/block_types.h" |
+#import "base/ios/weak_nsobject.h" |
+#include "base/logging.h" |
+#import "base/mac/scoped_nsobject.h" |
+#include "base/strings/sys_string_conversions.h" |
+#include "components/strings/grit/components_strings.h" |
+#import "ios/chrome/browser/ui/alert_coordinator/action_sheet_coordinator.h" |
+#import "ios/chrome/browser/ui/alert_coordinator/alert_coordinator.h" |
+#import "ios/chrome/browser/ui/alert_coordinator/input_alert_coordinator.h" |
+#import "ios/chrome/browser/ui/dialogs/javascript_dialog_blocking_util.h" |
+#import "ios/chrome/browser/ui/dialogs/nsurl_protection_space_util.h" |
+#include "ios/chrome/browser/ui/ui_util.h" |
+#include "ios/chrome/grit/ios_strings.h" |
+#include "ios/web/public/web_state/web_state.h" |
+#include "ui/base/l10n/l10n_util.h" |
+#include "url/gurl.h" |
+ |
+// Externed accessibility identifier. |
+NSString* const kJavaScriptDialogTextFieldAccessibiltyIdentifier = |
+ @"JavaScriptDialogTextFieldAccessibiltyIdentifier"; |
+ |
+namespace { |
+// The hostname to use for JavaScript alerts when there is no valid hostname in |
+// the URL passed to |+localizedTitleForJavaScriptAlertFromPage:type:|. |
+const char kAboutNullHostname[] = "about:null"; |
+} // namespace |
+ |
+@interface DialogPresenter () { |
+ // Backing objects for properties of the same name. |
+ base::WeakNSProtocol<id<DialogPresenterDelegate>> _delegate; |
+ base::WeakNSObject<UIViewController> _viewController; |
+ // Queue of WebStates which correspond to the keys in |
+ // |_dialogCoordinatorsForWebStates|. |
+ std::deque<web::WebState*> _queuedWebStates; |
+ // A map associating queued webStates with their coordinators. |
+ std::map<web::WebState*, base::scoped_nsobject<AlertCoordinator>> |
+ _dialogCoordinatorsForWebStates; |
+ web::WebState* _presentedDialogWebState; |
+ base::scoped_nsobject<AlertCoordinator> _presentedDialogCoordinator; |
+ base::scoped_nsobject<ActionSheetCoordinator> |
+ _blockingConfirmationCoordinator; |
+} |
+ |
+// The delegate passed on initialization. |
+@property(nonatomic, readonly) id<DialogPresenterDelegate> delegate; |
+ |
+// The presenting view controller passed on initialization. |
+@property(nonatomic, readonly) UIViewController* viewController; |
+ |
+// Whether a modal dialog is currently being shown. |
+@property(nonatomic, readonly, getter=isShowingDialog) BOOL showingDialog; |
+ |
+// The webState for |presentedDialog|. |
+@property(nonatomic) web::WebState* presentedDialogWebState; |
+ |
+// The dialog that's currently being shown, if any. |
+@property(nonatomic, retain) AlertCoordinator* presentedDialogCoordinator; |
+ |
+// The JavaScript dialog blocking confirmation action sheet being shown, if any. |
+@property(nonatomic, retain) AlertCoordinator* blockingConfirmationCoordinator; |
+ |
+// Adds |context| and |coordinator| to the queue. If a dialog is not already |
+// being shown, |coordinator| will be presented. Otherwise, |coordinator| will |
+// be displayed once the previously shown dialog is dismissed. |
+- (void)addDialogCoordinator:(AlertCoordinator*)coordinator |
+ forWebState:(web::WebState*)webState; |
+ |
+// Shows the dialog associated with the next context in |contextQueue|. |
+- (void)showNextDialog; |
+ |
+// Called when a button in |coordinator| is tapped. |
+- (void)buttonWasTappedForCoordinator:(AlertCoordinator*)coordinator; |
+ |
+// Adds buttons to |alertCoordinator|. A confirmation button with |label| as |
+// the text will be added for |confirmAction|, and a cancel button will be added |
+// for |cancelAction|. |
+- (void)setUpAlertCoordinator:(AlertCoordinator*)alertCoordinator |
+ confirmAction:(ProceduralBlock)confirmAction |
+ cancelAction:(ProceduralBlock)cancelAction |
+ OKLabel:(NSString*)label; |
+ |
+// Sets up the JavaScript dialog blocking option for |alertCoordinator|. |
+// Overrides |alertCoordinator|'s |startAction| to call |
+// JavaScriptDialogWasShown(). Depending on the value of |
+// ShouldShowDialogBlockingOption() for |webState|, optionally adds a button to |
+// |alertCoordinator| allowing for the blocking of future dialogs. In addition |
+// to blocking dialogs for the WebState, the added button will call |
+// |alertCoordinator|'s |cancelAction|. |
+- (void)setUpBlockingOptionForCoordinator:(AlertCoordinator*)alertCoordinator |
+ webState:(web::WebState*)webState; |
+ |
+// The block to use for the JavaScript dialog blocking option for |coordinator|. |
+- (ProceduralBlock)blockingActionForCoordinator:(AlertCoordinator*)coordinator; |
+ |
+@end |
+ |
+@implementation DialogPresenter |
+ |
+@synthesize active = _active; |
+ |
+- (instancetype)initWithDelegate:(id<DialogPresenterDelegate>)delegate |
+ presentingViewController:(UIViewController*)viewController { |
+ if ((self = [super init])) { |
+ DCHECK(delegate); |
+ DCHECK(viewController); |
+ _delegate.reset(delegate); |
+ _viewController.reset(viewController); |
+ } |
+ return self; |
+} |
+ |
+#pragma mark - Accessors |
+ |
+- (void)setActive:(BOOL)active { |
+ if (_active != active) { |
+ _active = active; |
+ [self tryToPresent]; |
+ } |
+} |
+ |
+- (id<DialogPresenterDelegate>)delegate { |
+ return _delegate; |
+} |
+ |
+- (UIViewController*)viewController { |
+ return _viewController; |
+} |
+ |
+- (BOOL)isShowingDialog { |
+ DCHECK_EQ(self.presentedDialogWebState != nullptr, |
+ self.presentedDialogCoordinator != nil); |
+ return self.presentedDialogCoordinator != nil; |
+} |
+ |
+- (web::WebState*)presentedDialogWebState { |
+ return _presentedDialogWebState; |
+} |
+ |
+- (void)setPresentedDialogWebState:(web::WebState*)presentedDialogWebState { |
+ _presentedDialogWebState = presentedDialogWebState; |
+} |
+ |
+- (AlertCoordinator*)presentedDialogCoordinator { |
+ return _presentedDialogCoordinator; |
+} |
+ |
+- (void)setPresentedDialogCoordinator: |
+ (AlertCoordinator*)presentedDialogCoordinator { |
+ _presentedDialogCoordinator.reset([presentedDialogCoordinator retain]); |
+} |
+ |
+- (ActionSheetCoordinator*)blockingConfirmationCoordinator { |
+ return _blockingConfirmationCoordinator; |
+} |
+ |
+- (void)setBlockingConfirmationCoordinator: |
+ (ActionSheetCoordinator*)blockingConfirmationActionSheetCoordinator { |
+ _blockingConfirmationCoordinator.reset( |
+ [blockingConfirmationActionSheetCoordinator retain]); |
+} |
+ |
+#pragma mark - Public |
+ |
+- (void)runJavaScriptAlertPanelWithMessage:(NSString*)message |
+ requestURL:(const GURL&)requestURL |
+ webState:(web::WebState*)webState |
+ completionHandler:(void (^)(void))completionHandler { |
+ NSString* title = |
+ [DialogPresenter localizedTitleForJavaScriptAlertFromPage:requestURL]; |
+ AlertCoordinator* alertCoordinator = [[[AlertCoordinator alloc] |
+ initWithBaseViewController:self.viewController |
+ title:title |
+ message:message] autorelease]; |
+ |
+ // Handler. |
+ base::WeakNSObject<DialogPresenter> weakSelf(self); |
+ base::WeakNSObject<AlertCoordinator> weakCoordinator(alertCoordinator); |
+ ProceduralBlock OKHandler = ^{ |
+ if (completionHandler) |
+ completionHandler(); |
+ [weakSelf buttonWasTappedForCoordinator:weakCoordinator]; |
+ }; |
+ |
+ // Add button. |
+ [alertCoordinator addItemWithTitle:l10n_util::GetNSString(IDS_OK) |
+ action:OKHandler |
+ style:UIAlertActionStyleDefault]; |
+ |
+ // Add cancel handler. |
+ alertCoordinator.cancelAction = completionHandler; |
+ alertCoordinator.noInteractionAction = completionHandler; |
+ |
+ // Blocking option setup. |
+ [self setUpBlockingOptionForCoordinator:alertCoordinator webState:webState]; |
+ |
+ [self addDialogCoordinator:alertCoordinator forWebState:webState]; |
+} |
+ |
+- (void)runJavaScriptConfirmPanelWithMessage:(NSString*)message |
+ requestURL:(const GURL&)requestURL |
+ webState:(web::WebState*)webState |
+ completionHandler: |
+ (void (^)(BOOL isConfirmed))completionHandler { |
+ NSString* title = |
+ [DialogPresenter localizedTitleForJavaScriptAlertFromPage:requestURL]; |
+ AlertCoordinator* alertCoordinator = [[[AlertCoordinator alloc] |
+ initWithBaseViewController:self.viewController |
+ title:title |
+ message:message] autorelease]; |
+ |
+ // Actions. |
+ ProceduralBlock confirmAction = ^{ |
+ if (completionHandler) |
+ completionHandler(YES); |
+ }; |
+ |
+ ProceduralBlock cancelAction = ^{ |
+ if (completionHandler) |
+ completionHandler(NO); |
+ }; |
+ |
+ // Coordinator Setup. |
+ NSString* OKLabel = l10n_util::GetNSString(IDS_OK); |
+ [self setUpAlertCoordinator:alertCoordinator |
+ confirmAction:confirmAction |
+ cancelAction:cancelAction |
+ OKLabel:OKLabel]; |
+ |
+ // Blocking option setup. |
+ [self setUpBlockingOptionForCoordinator:alertCoordinator webState:webState]; |
+ |
+ [self addDialogCoordinator:alertCoordinator forWebState:webState]; |
+} |
+ |
+- (void)runJavaScriptTextInputPanelWithPrompt:(NSString*)message |
+ defaultText:(NSString*)defaultText |
+ requestURL:(const GURL&)requestURL |
+ webState:(web::WebState*)webState |
+ completionHandler: |
+ (void (^)(NSString* input))completionHandler { |
+ NSString* title = |
+ [DialogPresenter localizedTitleForJavaScriptAlertFromPage:requestURL]; |
+ InputAlertCoordinator* alertCoordinator = [[[InputAlertCoordinator alloc] |
+ initWithBaseViewController:self.viewController |
+ title:title |
+ message:message] autorelease]; |
+ |
+ // Actions. |
+ base::WeakNSObject<InputAlertCoordinator> weakCoordinator(alertCoordinator); |
+ ProceduralBlock confirmAction = ^{ |
+ if (completionHandler) { |
+ NSString* textInput = [weakCoordinator textFields].firstObject.text; |
+ completionHandler(textInput ? textInput : @""); |
+ } |
+ }; |
+ |
+ ProceduralBlock cancelAction = ^{ |
+ if (completionHandler) |
+ completionHandler(nil); |
+ }; |
+ |
+ // Coordinator Setup. |
+ NSString* OKLabel = l10n_util::GetNSString(IDS_OK); |
+ [self setUpAlertCoordinator:alertCoordinator |
+ confirmAction:confirmAction |
+ cancelAction:cancelAction |
+ OKLabel:OKLabel]; |
+ |
+ // Blocking option setup. |
+ [self setUpBlockingOptionForCoordinator:alertCoordinator webState:webState]; |
+ |
+ // Add text field. |
+ [alertCoordinator |
+ addTextFieldWithConfigurationHandler:^(UITextField* textField) { |
+ textField.text = defaultText; |
+ textField.accessibilityIdentifier = |
+ kJavaScriptDialogTextFieldAccessibiltyIdentifier; |
+ }]; |
+ |
+ [self addDialogCoordinator:alertCoordinator forWebState:webState]; |
+} |
+ |
+- (void)runAuthDialogForProtectionSpace:(NSURLProtectionSpace*)protectionSpace |
+ proposedCredential:(NSURLCredential*)credential |
+ webState:(web::WebState*)webState |
+ completionHandler:(void (^)(NSString* user, |
+ NSString* password))handler { |
+ NSString* title = l10n_util::GetNSStringWithFixup(IDS_LOGIN_DIALOG_TITLE); |
+ NSString* message = |
+ ios_internal::nsurlprotectionspace_util::MessageForHTTPAuth( |
+ protectionSpace); |
+ |
+ InputAlertCoordinator* alertCoordinator = [[[InputAlertCoordinator alloc] |
+ initWithBaseViewController:self.viewController |
+ title:title |
+ message:message] autorelease]; |
+ |
+ // Actions. |
+ base::WeakNSObject<InputAlertCoordinator> weakCoordinator(alertCoordinator); |
+ ProceduralBlock confirmAction = ^{ |
+ if (handler) { |
+ NSString* username = [[weakCoordinator textFields] objectAtIndex:0].text; |
+ NSString* password = [[weakCoordinator textFields] objectAtIndex:1].text; |
+ handler(username, password); |
+ } |
+ }; |
+ |
+ ProceduralBlock cancelAction = ^{ |
+ if (handler) |
+ handler(nil, nil); |
+ }; |
+ |
+ // Coordinator Setup. |
+ NSString* OKLabel = |
+ l10n_util::GetNSStringWithFixup(IDS_LOGIN_DIALOG_OK_BUTTON_LABEL); |
+ [self setUpAlertCoordinator:alertCoordinator |
+ confirmAction:confirmAction |
+ cancelAction:cancelAction |
+ OKLabel:OKLabel]; |
+ |
+ // Add text fields. |
+ NSString* username = credential.user ? credential.user : @""; |
+ [alertCoordinator |
+ addTextFieldWithConfigurationHandler:^(UITextField* textField) { |
+ textField.text = username; |
+ textField.placeholder = l10n_util::GetNSString( |
+ IDS_IOS_HTTP_LOGIN_DIALOG_USERNAME_PLACEHOLDER); |
+ }]; |
+ [alertCoordinator |
+ addTextFieldWithConfigurationHandler:^(UITextField* textField) { |
+ textField.placeholder = l10n_util::GetNSString( |
+ IDS_IOS_HTTP_LOGIN_DIALOG_PASSWORD_PLACEHOLDER); |
+ textField.secureTextEntry = YES; |
+ }]; |
+ |
+ [self addDialogCoordinator:alertCoordinator forWebState:webState]; |
+} |
+ |
+- (void)cancelDialogForWebState:(web::WebState*)webState { |
+ DCHECK_NE(webState, self.presentedDialogWebState); |
+ AlertCoordinator* dialogToCancel = _dialogCoordinatorsForWebStates[webState]; |
+ if (dialogToCancel) { |
+ auto it = |
+ std::find(_queuedWebStates.begin(), _queuedWebStates.end(), webState); |
+ DCHECK(it != _queuedWebStates.end()); |
+ _queuedWebStates.erase(it); |
+ [dialogToCancel executeCancelHandler]; |
+ [dialogToCancel stop]; |
+ _dialogCoordinatorsForWebStates.erase(webState); |
+ } |
+} |
+ |
+- (void)cancelAllDialogs { |
+ [self.presentedDialogCoordinator executeCancelHandler]; |
+ [self.presentedDialogCoordinator stop]; |
+ self.presentedDialogCoordinator = nil; |
+ self.presentedDialogWebState = nil; |
+ while (!_queuedWebStates.empty()) { |
+ [self cancelDialogForWebState:_queuedWebStates.front()]; |
+ } |
+} |
+ |
+- (void)tryToPresent { |
+ // Don't try to present if a JavaScript dialog blocking confirmation sheet is |
+ // displayed. |
+ if (self.blockingConfirmationCoordinator) |
+ return; |
+ // The active TabModel can't be changed while a JavaScript dialog is shown. |
+ DCHECK(!self.showingDialog); |
+ if (_active && !_queuedWebStates.empty() && !self.delegate.presenting) |
+ [self showNextDialog]; |
+} |
+ |
++ (NSString*)localizedTitleForJavaScriptAlertFromPage:(const GURL&)pageURL { |
+ NSString* hostname = base::SysUTF8ToNSString(pageURL.host()); |
+ if (!hostname.length) |
+ hostname = base::SysUTF8ToNSString(kAboutNullHostname); |
+ return l10n_util::GetNSStringF(IDS_JAVASCRIPT_MESSAGEBOX_TITLE, |
+ base::SysNSStringToUTF16(hostname)); |
+} |
+ |
+#pragma mark - Private methods. |
+ |
+- (void)addDialogCoordinator:(AlertCoordinator*)coordinator |
+ forWebState:(web::WebState*)webState { |
+ DCHECK(coordinator); |
+ DCHECK(webState); |
+ DCHECK_NE(webState, self.presentedDialogWebState); |
+ DCHECK(!_dialogCoordinatorsForWebStates[webState]); |
+ _queuedWebStates.push_back(webState); |
+ _dialogCoordinatorsForWebStates[webState] = |
+ base::scoped_nsobject<AlertCoordinator>([coordinator retain]); |
+ |
+ if (self.active && !self.showingDialog && !self.delegate.presenting) |
+ [self showNextDialog]; |
+} |
+ |
+- (void)showNextDialog { |
+ DCHECK(self.active); |
+ DCHECK(!self.showingDialog); |
+ DCHECK(!_queuedWebStates.empty()); |
+ // Update properties and remove context and the dialog from queue. |
+ self.presentedDialogWebState = _queuedWebStates.front(); |
+ _queuedWebStates.pop_front(); |
+ self.presentedDialogCoordinator = |
+ _dialogCoordinatorsForWebStates[self.presentedDialogWebState]; |
+ _dialogCoordinatorsForWebStates.erase(self.presentedDialogWebState); |
+ // Notify the delegate and display the dialog. |
+ [self.delegate dialogPresenter:self |
+ willShowDialogForWebState:self.presentedDialogWebState]; |
+ [self.presentedDialogCoordinator start]; |
+} |
+ |
+- (void)buttonWasTappedForCoordinator:(AlertCoordinator*)coordinator { |
+ if (coordinator != self.presentedDialogCoordinator) |
+ return; |
+ self.presentedDialogWebState = nil; |
+ self.presentedDialogCoordinator = nil; |
+ self.blockingConfirmationCoordinator = nil; |
+ if (!_queuedWebStates.empty() && !self.delegate.presenting) |
+ [self showNextDialog]; |
+} |
+ |
+- (void)setUpAlertCoordinator:(AlertCoordinator*)alertCoordinator |
+ confirmAction:(ProceduralBlock)confirmAction |
+ cancelAction:(ProceduralBlock)cancelAction |
+ OKLabel:(NSString*)label { |
+ // Handlers. |
+ base::WeakNSObject<DialogPresenter> weakSelf(self); |
+ base::WeakNSObject<AlertCoordinator> weakCoordinator(alertCoordinator); |
+ |
+ ProceduralBlock confirmHandler = ^{ |
+ if (confirmAction) |
+ confirmAction(); |
+ [weakSelf buttonWasTappedForCoordinator:weakCoordinator]; |
+ }; |
+ |
+ ProceduralBlock cancelHandler = ^{ |
+ if (cancelAction) |
+ cancelAction(); |
+ [weakSelf buttonWasTappedForCoordinator:weakCoordinator]; |
+ }; |
+ |
+ // Add buttons. |
+ [alertCoordinator addItemWithTitle:label |
+ action:confirmHandler |
+ style:UIAlertActionStyleDefault]; |
+ [alertCoordinator addItemWithTitle:l10n_util::GetNSString(IDS_CANCEL) |
+ action:cancelHandler |
+ style:UIAlertActionStyleCancel]; |
+ |
+ // Add cancel handler. |
+ alertCoordinator.cancelAction = cancelAction; |
+ alertCoordinator.noInteractionAction = cancelAction; |
+} |
+ |
+- (void)setUpBlockingOptionForCoordinator:(AlertCoordinator*)alertCoordinator |
+ webState:(web::WebState*)webState { |
+ DCHECK(alertCoordinator); |
+ DCHECK(webState); |
+ |
+ // Set up the start action. |
+ base::WeakNSObject<DialogPresenter> weakSelf(self); |
+ base::WeakNSObject<AlertCoordinator> weakCoordinator(alertCoordinator); |
+ ProceduralBlock originalStartAction = alertCoordinator.startAction; |
+ alertCoordinator.startAction = ^{ |
+ if (originalStartAction) |
+ originalStartAction(); |
+ JavaScriptDialogWasShown(webState); |
+ }; |
+ |
+ // Early return if a blocking option should not be added. |
+ if (!ShouldShowDialogBlockingOption(webState)) |
+ return; |
+ |
+ ProceduralBlock blockingAction = |
+ [self blockingActionForCoordinator:alertCoordinator]; |
+ NSString* blockingOptionTitle = |
+ l10n_util::GetNSString(IDS_IOS_JAVA_SCRIPT_DIALOG_BLOCKING_BUTTON_TEXT); |
+ [alertCoordinator addItemWithTitle:blockingOptionTitle |
+ action:blockingAction |
+ style:UIAlertActionStyleDefault]; |
+} |
+ |
+- (ProceduralBlock)blockingActionForCoordinator:(AlertCoordinator*)coordinator { |
+ base::WeakNSObject<DialogPresenter> weakSelf(self); |
+ base::WeakNSObject<AlertCoordinator> weakCoordinator(coordinator); |
+ base::WeakNSObject<UIViewController> weakBaseViewController( |
+ coordinator.baseViewController); |
+ ProceduralBlock cancelAction = coordinator.cancelAction; |
+ return [[^{ |
+ // Create the confirmation coordinator. Use an action sheet on iPhone and |
+ // an alert on iPhone. |
+ NSString* confirmMessage = |
+ l10n_util::GetNSString(IDS_JAVASCRIPT_MESSAGEBOX_SUPPRESS_OPTION); |
+ AlertCoordinator* confirmationCoordinator = |
+ IsIPadIdiom() |
+ ? [[[AlertCoordinator alloc] |
+ initWithBaseViewController:weakBaseViewController |
+ title:nil |
+ message:confirmMessage] autorelease] |
+ : [[[ActionSheetCoordinator alloc] |
+ initWithBaseViewController:weakBaseViewController |
+ title:nil |
+ message:confirmMessage |
+ rect:CGRectZero |
+ view:nil] autorelease]; |
+ // Set up button actions. |
+ ProceduralBlock confirmHandler = ^{ |
+ if (cancelAction) |
+ cancelAction(); |
+ base::scoped_nsobject<DialogPresenter> strongSelf([weakSelf retain]); |
+ if (!strongSelf) |
+ return; |
+ DialogBlockingOptionSelected([strongSelf presentedDialogWebState]); |
+ [strongSelf buttonWasTappedForCoordinator:weakCoordinator]; |
+ }; |
+ ProceduralBlock cancelHandler = ^{ |
+ if (cancelAction) |
+ cancelAction(); |
+ [weakSelf buttonWasTappedForCoordinator:weakCoordinator]; |
+ }; |
+ NSString* blockingOptionTitle = |
+ l10n_util::GetNSString(IDS_IOS_JAVA_SCRIPT_DIALOG_BLOCKING_BUTTON_TEXT); |
+ [confirmationCoordinator addItemWithTitle:blockingOptionTitle |
+ action:confirmHandler |
+ style:UIAlertActionStyleDestructive]; |
+ [confirmationCoordinator addItemWithTitle:l10n_util::GetNSString(IDS_CANCEL) |
+ action:cancelHandler |
+ style:UIAlertActionStyleCancel]; |
+ [weakSelf setBlockingConfirmationCoordinator:confirmationCoordinator]; |
+ [[weakSelf blockingConfirmationCoordinator] start]; |
+ } copy] autorelease]; |
+} |
+ |
+@end |