Index: ios/chrome/browser/ui/stack_view/card_set.mm |
diff --git a/ios/chrome/browser/ui/stack_view/card_set.mm b/ios/chrome/browser/ui/stack_view/card_set.mm |
new file mode 100644 |
index 0000000000000000000000000000000000000000..67617e44aa16e96ffe327399377cb7f2c742ac3e |
--- /dev/null |
+++ b/ios/chrome/browser/ui/stack_view/card_set.mm |
@@ -0,0 +1,598 @@ |
+// Copyright 2012 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/stack_view/card_set.h" |
+ |
+#import <QuartzCore/QuartzCore.h> |
+ |
+#include "base/logging.h" |
+#import "base/mac/scoped_nsobject.h" |
+#import "ios/chrome/browser/tabs/tab.h" |
+#import "ios/chrome/browser/tabs/tab_model.h" |
+#include "ios/chrome/browser/ui/rtl_geometry.h" |
+#import "ios/chrome/browser/ui/stack_view/card_stack_layout_manager.h" |
+#import "ios/chrome/browser/ui/stack_view/page_animation_util.h" |
+#import "ios/chrome/browser/ui/stack_view/stack_card.h" |
+#include "ios/chrome/browser/ui/ui_util.h" |
+#import "ios/web/web_state/ui/crw_web_controller.h" |
+ |
+namespace { |
+const CGFloat kMaxCardStaggerPercentage = 0.35; |
+} |
+ |
+@interface CardSet ()<StackCardViewProvider, TabModelObserver> { |
+ base::scoped_nsobject<TabModel> tabModel_; |
+ base::scoped_nsobject<UIView> view_; |
+ base::scoped_nsobject<CardStackLayoutManager> stackModel_; |
+ base::scoped_nsobject<UIImageView> stackShadow_; |
+} |
+ |
+// Set to |YES| when the card set should draw a shadow around the entire stack. |
+@property(nonatomic, assign) BOOL shouldShowShadow; |
+ |
+// Creates and returns an autoreleased StackCard from the given |tab| (which |
+// must not be nil). |
+- (StackCard*)buildCardFromTab:(Tab*)tab; |
+ |
+// Rebuilds the set of cards from the current state of the tab model. |
+- (void)rebuildCards; |
+ |
+// Makes |card| visible (or in the view hierarchy but hidden if it's covered |
+// by other cards) in the current display view at the right z-order relative |
+// to any other cards from the set that are already displayed. |
+- (void)displayCard:(StackCard*)card; |
+ |
+// Updates the tab display side of the cards in the set based on the current |
+// layout orientation. |
+- (void)updateCardTabs; |
+ |
+@end |
+ |
+#pragma mark - |
+ |
+@implementation CardSet |
+ |
+@synthesize observer = observer_; |
+@synthesize ignoresTabModelChanges = ignoresTabModelChanges_; |
+@synthesize defersCardHiding = defersCardHiding_; |
+@synthesize keepOnlyVisibleCardViewsAlive = keepOnlyVisibleCardViewsAlive_; |
+@synthesize shouldShowShadow = shouldShowShadow_; |
+@synthesize closingCard = closingCard_; |
+ |
+- (CardStackLayoutManager*)stackModel { |
+ return stackModel_.get(); |
+} |
+ |
+- (id)initWithModel:(TabModel*)model { |
+ if ((self = [super init])) { |
+ tabModel_.reset([model retain]); |
+ [tabModel_ addObserver:self]; |
+ stackModel_.reset([[CardStackLayoutManager alloc] init]); |
+ [self rebuildCards]; |
+ self.shouldShowShadow = YES; |
+ } |
+ return self; |
+} |
+ |
+- (void)dealloc { |
+ [tabModel_ removeObserver:self]; |
+ [super dealloc]; |
+} |
+ |
+#pragma mark Properties |
+ |
+- (TabModel*)tabModel { |
+ return tabModel_; |
+} |
+ |
+- (void)setTabModel:(TabModel*)tabModel { |
+ DCHECK([tabModel count] == 0); |
+ DCHECK([tabModel_ count] == 0); |
+ [tabModel_ removeObserver:self]; |
+ tabModel_.reset([tabModel retain]); |
+ if (!ignoresTabModelChanges_) |
+ [tabModel_ addObserver:self]; |
+} |
+ |
+- (NSArray*)cards { |
+ return [stackModel_ cards]; |
+} |
+ |
+- (StackCard*)currentCard { |
+ DCHECK(!ignoresTabModelChanges_); |
+ Tab* currentTab = [tabModel_ currentTab]; |
+ if (!currentTab) |
+ return nil; |
+ NSUInteger currentTabIndex = [tabModel_ indexOfTab:currentTab]; |
+ // There is a period of time during closing the current tab where currentTab |
+ // is still the closed tab, but that tab is no longer *in* the model. |
+ // TODO(stuartmorgan): Fix this in TabModel; this is dumb. |
+ if (currentTabIndex == NSNotFound) |
+ return nil; |
+ DCHECK(currentTabIndex < [self.cards count]); |
+ return [self.cards objectAtIndex:currentTabIndex]; |
+} |
+ |
+- (void)setCurrentCard:(StackCard*)card { |
+ DCHECK(!ignoresTabModelChanges_); |
+ NSInteger cardIndex = [self.cards indexOfObject:card]; |
+ DCHECK(cardIndex != NSNotFound); |
+ [tabModel_ setCurrentTab:[tabModel_ tabAtIndex:cardIndex]]; |
+} |
+ |
+- (UIView*)displayView { |
+ return view_.get(); |
+} |
+ |
+- (void)setDisplayView:(UIView*)view { |
+ if (view == view_.get()) |
+ return; |
+ for (StackCard* card in self.cards) { |
+ if (card.viewIsLive) { |
+ [card.view removeFromSuperview]; |
+ [card releaseView]; |
+ } |
+ } |
+ [stackShadow_ removeFromSuperview]; |
+ view_.reset([view retain]); |
+ // Add the stack shadow view to the new display view. |
+ if (!stackShadow_) { |
+ UIImage* shadowImage = [UIImage imageNamed:kCardShadowImageName]; |
+ shadowImage = [shadowImage |
+ resizableImageWithCapInsets:UIEdgeInsetsMake( |
+ shadowImage.size.height / 2.0, |
+ shadowImage.size.width / 2.0, |
+ shadowImage.size.height / 2.0, |
+ shadowImage.size.width / 2.0)]; |
+ stackShadow_.reset([[UIImageView alloc] initWithImage:shadowImage]); |
+ [stackShadow_ setHidden:!self.cards.count]; |
+ } |
+ [view_ addSubview:stackShadow_]; |
+ // Don't set the stack's end limit when the view is set to nil in order to |
+ // avoid losing existing card positions; these positions will be needed |
+ // if/when the view is restored (e.g., if the view was purged due to a memory |
+ // warning while in a modal view and then restored when exiting the modal |
+ // view). |
+ if (view_.get()) |
+ [self displayViewSizeWasChanged]; |
+} |
+ |
+- (CardCloseButtonSide)closeButtonSide { |
+ return [stackModel_ layoutIsVertical] ? CardCloseButtonSide::TRAILING |
+ : CardCloseButtonSide::LEADING; |
+} |
+ |
+- (void)setIgnoresTabModelChanges:(BOOL)ignoresTabModelChanges { |
+ if (ignoresTabModelChanges_ == ignoresTabModelChanges) |
+ return; |
+ ignoresTabModelChanges_ = ignoresTabModelChanges; |
+ if (ignoresTabModelChanges) { |
+ [tabModel_ removeObserver:self]; |
+ } else { |
+ [self rebuildCards]; |
+ [tabModel_ addObserver:self]; |
+ } |
+} |
+ |
+- (void)setDefersCardHiding:(BOOL)defersCardHiding { |
+ if (defersCardHiding_ == defersCardHiding) |
+ return; |
+ defersCardHiding_ = defersCardHiding; |
+ if (!defersCardHiding_) |
+ [self updateCardVisibilities]; |
+} |
+ |
+- (CGFloat)maximumOverextensionAmount { |
+ return [stackModel_ maximumOverextensionAmount]; |
+} |
+ |
+- (void)setMaximumOverextensionAmount:(CGFloat)amount { |
+ [stackModel_ setMaximumOverextensionAmount:amount]; |
+} |
+ |
+- (void)setKeepOnlyVisibleCardViewsAlive:(BOOL)keepAlive { |
+ if (keepOnlyVisibleCardViewsAlive_ == keepAlive) |
+ return; |
+ keepOnlyVisibleCardViewsAlive_ = keepAlive; |
+ if (!keepOnlyVisibleCardViewsAlive_) |
+ [self updateCardVisibilities]; |
+} |
+ |
+- (void)setShouldShowShadow:(BOOL)shouldShowShadow { |
+ if (shouldShowShadow_ != shouldShowShadow) { |
+ shouldShowShadow_ = shouldShowShadow; |
+ [stackShadow_ setHidden:!shouldShowShadow_]; |
+ } |
+} |
+ |
+- (void)setClosingCard:(StackCard*)closingCard { |
+ if (closingCard_ != closingCard) { |
+ closingCard_ = closingCard; |
+ if (closingCard) { |
+ self.shouldShowShadow = self.cards.count > 1; |
+ [self updateShadowLayout]; |
+ closingCard.view.shouldShowShadow = YES; |
+ closingCard.view.shouldMaskShadow = NO; |
+ StackCard* nextVisibleCard = nil; |
+ NSUInteger nextVisibleCardIdx = [self.cards indexOfObject:closingCard]; |
+ while (++nextVisibleCardIdx < self.cards.count) { |
+ nextVisibleCard = self.cards[nextVisibleCardIdx]; |
+ if ([nextVisibleCard viewIsLive] && !nextVisibleCard.view.hidden) |
+ break; |
+ } |
+ nextVisibleCard.view.shouldShowShadow = YES; |
+ nextVisibleCard.view.shouldMaskShadow = NO; |
+ } else { |
+ self.shouldShowShadow = YES; |
+ [self updateCardVisibilities]; |
+ } |
+ } |
+} |
+ |
+#pragma mark Public Methods |
+ |
+- (void)configureLayoutParametersWithMargin:(CGFloat)margin { |
+ DCHECK(view_); |
+ |
+ [stackModel_ setStartLimit:margin]; |
+ |
+ BOOL isVertical = [stackModel_ layoutIsVertical]; |
+ CGSize cardSize = [stackModel_ cardSize]; |
+ CGFloat cardLength = isVertical ? cardSize.height : cardSize.width; |
+ [stackModel_ setMaxStagger:(kMaxCardStaggerPercentage * cardLength)]; |
+} |
+ |
+- (void)displayViewSizeWasChanged { |
+ for (StackCard* card in self.cards) { |
+ LayoutRect layout = card.layout; |
+ layout.boundingWidth = CGRectGetWidth(self.displayView.bounds); |
+ card.layout = layout; |
+ } |
+ CGFloat endLimit = [stackModel_ layoutIsVertical] ? [view_ bounds].size.height |
+ : [view_ bounds].size.width; |
+ [stackModel_ setEndLimit:endLimit]; |
+} |
+ |
+- (void)setCardSize:(CGSize)cardSize { |
+ [stackModel_ setCardSize:cardSize]; |
+} |
+ |
+- (void)setLayoutAxisPosition:(CGFloat)position |
+ isVertical:(BOOL)layoutIsVertical { |
+ [stackModel_ setLayoutIsVertical:layoutIsVertical]; |
+ [stackModel_ setLayoutAxisPosition:position]; |
+ [self updateCardTabs]; |
+ [self updateShadowLayout]; |
+} |
+ |
+- (void)layOutStartStack { |
+ [stackModel_ layOutStartStack]; |
+ [self updateCardVisibilities]; |
+} |
+ |
+- (void)fanOutCards { |
+ if ([self.cards count] == 0) |
+ return; |
+ [self fanOutCardsWithStartIndex:0]; |
+} |
+ |
+- (void)fanOutCardsWithStartIndex:(NSUInteger)startIndex { |
+ if (![self.cards count]) |
+ return; |
+ DCHECK(startIndex < [self.cards count]); |
+ [stackModel_ fanOutCardsWithStartIndex:startIndex]; |
+ [self updateCardVisibilities]; |
+} |
+ |
+- (std::vector<LayoutRect>)cardLayouts { |
+ std::vector<LayoutRect> cardLayouts; |
+ for (StackCard* card in self.cards) |
+ cardLayouts.push_back(card.layout); |
+ return cardLayouts; |
+} |
+ |
+- (void)scrollCardAtIndex:(NSUInteger)index |
+ byDelta:(CGFloat)delta |
+ allowEarlyOverscroll:(BOOL)allowEarlyOverscroll |
+ decayOnOverscroll:(BOOL)decayOnOverscroll |
+ scrollLeadingCards:(BOOL)scrollLeadingCards { |
+ DCHECK(index < [self.cards count]); |
+ [stackModel_ scrollCardAtIndex:index |
+ byDelta:delta |
+ allowEarlyOverscroll:allowEarlyOverscroll |
+ decayOnOverscroll:decayOnOverscroll |
+ scrollLeadingCards:scrollLeadingCards]; |
+ [self updateCardVisibilities]; |
+} |
+ |
+- (BOOL)stackIsOverextended { |
+ if ([self.cards count] == 0) |
+ return NO; |
+ return ([self overextensionOnCardAtIndex:0]); |
+} |
+ |
+- (BOOL)overextensionOnCardAtIndex:(NSUInteger)index { |
+ DCHECK(index < [self.cards count]); |
+ if ([self overextensionTowardStartOnCardAtIndex:index]) |
+ return YES; |
+ if ((index == 0) && [stackModel_ overextensionTowardEndOnFirstCard]) |
+ return YES; |
+ return NO; |
+} |
+ |
+- (BOOL)overextensionTowardStartOnCardAtIndex:(NSUInteger)index { |
+ DCHECK(index < [self.cards count]); |
+ return [stackModel_ overextensionTowardStartOnCardAtIndex:index]; |
+} |
+ |
+- (void)eliminateOverextension { |
+ [stackModel_ eliminateOverextension]; |
+ [self updateCardVisibilities]; |
+} |
+ |
+- (void)scrollCardAtIndex:(NSUInteger)index awayFromNeighbor:(BOOL)preceding { |
+ DCHECK(index < [self.cards count]); |
+ [stackModel_ scrollCardAtIndex:index awayFromNeighbor:preceding]; |
+ [self updateCardVisibilities]; |
+} |
+ |
+- (void)updateCardVisibilities { |
+ BOOL shouldHideNextVisibleCardShadow = YES; |
+ for (StackCard* card in self.cards) { |
+ if ([stackModel_ cardIsCovered:card]) { |
+ if (card.viewIsLive && !defersCardHiding_) { |
+ if (keepOnlyVisibleCardViewsAlive_) { |
+ [card.view removeFromSuperview]; |
+ [card releaseView]; |
+ } else { |
+ card.view.hidden = YES; |
+ } |
+ } |
+ } else { |
+ [self displayCard:card]; |
+ // Hide the first visible card's shadow. |
+ card.view.shouldShowShadow = !shouldHideNextVisibleCardShadow; |
+ if (shouldHideNextVisibleCardShadow) |
+ shouldHideNextVisibleCardShadow = NO; |
+ card.view.shouldMaskShadow = card.view.shouldShowShadow; |
+ } |
+ } |
+ if (self.shouldShowShadow) |
+ [self updateShadowLayout]; |
+ // Updates VoiceOver with currently accessible elements. |
+ UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, |
+ nil); |
+} |
+ |
+- (BOOL)preloadNextCard { |
+ if (keepOnlyVisibleCardViewsAlive_) |
+ return NO; |
+ // Find the next card to preload. |
+ StackCard* nextCard = nil; |
+ for (nextCard in self.cards) { |
+ // TODO(stuartmorgan): Change the selection here to favor the cards that |
+ // are closest to becoming visible. |
+ if (!nextCard.viewIsLive) |
+ break; |
+ } |
+ // If there was one, preload it. |
+ if (nextCard) { |
+ // Visible card views should have already been synchronously loaded. |
+ DCHECK([stackModel_ cardIsCovered:nextCard]); |
+ [self displayCard:nextCard]; |
+ } |
+ return nextCard != nil; |
+} |
+ |
+- (void)clearGestureRecognizerTargetAndDelegateFromCards:(id)object { |
+ for (StackCard* card in self.cards) { |
+ // Ensure that views aren't created just to remove their recognizers. |
+ if (!card.viewIsLive) |
+ continue; |
+ for (UIGestureRecognizer* recognizer in card.view.gestureRecognizers) { |
+ if (recognizer.delegate == object) |
+ recognizer.delegate = nil; |
+ // Passing NULL as the value of the |action| parameter causes all actions |
+ // associated with this target to be removed. Note that if |object| is |
+ // not a target of |recognizer| this method is a no-op. |
+ [recognizer removeTarget:object action:NULL]; |
+ } |
+ } |
+} |
+ |
+- (void)removeCardAtIndex:(NSUInteger)index { |
+ DCHECK(index < [self.cards count]); |
+ StackCard* card = [self.cards objectAtIndex:index]; |
+ [[card retain] autorelease]; |
+ [self.observer cardSet:self willRemoveCard:card atIndex:index]; |
+ [stackModel_ removeCard:card]; |
+ |
+ [self.observer cardSet:self didRemoveCard:card atIndex:index]; |
+} |
+ |
+#pragma mark Card Construction/Display |
+ |
+- (StackCard*)buildCardFromTab:(Tab*)tab { |
+ DCHECK(tab); |
+ StackCard* card = [[[StackCard alloc] initWithViewProvider:self] autorelease]; |
+ card.size = [stackModel_ cardSize]; |
+ card.tabID = reinterpret_cast<NSUInteger>(tab); |
+ |
+ return card; |
+} |
+ |
+- (void)rebuildCards { |
+ [stackModel_ removeAllCards]; |
+ |
+ for (Tab* tab in tabModel_.get()) { |
+ StackCard* card = [self buildCardFromTab:tab]; |
+ [stackModel_ addCard:card]; |
+ } |
+ |
+ [self.observer cardSetRecreatedCards:self]; |
+} |
+ |
+- (void)displayCard:(StackCard*)card { |
+ DCHECK(view_); |
+ card.view.hidden = [stackModel_ cardIsCovered:card]; |
+ |
+ if (card.view.superview) |
+ return; |
+ // Find the card view (if any) that's above the card to add and already in the |
+ // view. |
+ StackCard* cardAboveNewCard = nil; |
+ NSUInteger indexOfCard = [self.cards indexOfObject:card]; |
+ DCHECK(indexOfCard != NSNotFound); |
+ for (NSUInteger i = indexOfCard + 1; i < [self.cards count]; ++i) { |
+ StackCard* nextCard = [self.cards objectAtIndex:i]; |
+ if (nextCard.viewIsLive && nextCard.view.superview) { |
+ cardAboveNewCard = nextCard; |
+ break; |
+ } |
+ } |
+ if (cardAboveNewCard) |
+ [view_ insertSubview:card.view belowSubview:cardAboveNewCard.view]; |
+ else |
+ [view_ addSubview:card.view]; |
+ |
+ LayoutRect layout = card.layout; |
+ layout.boundingWidth = CGRectGetWidth([view_ bounds]); |
+ card.layout = layout; |
+ |
+ [self.observer cardSet:self displayedCard:card]; |
+} |
+ |
+#pragma mark Deck Management |
+ |
+- (void)updateCardTabs { |
+ CardCloseButtonSide closeButtonSide = self.closeButtonSide; |
+ for (StackCard* card in self.cards) { |
+ if (card.viewIsLive) |
+ card.view.closeButtonSide = closeButtonSide; |
+ } |
+} |
+ |
+#pragma mark - |
+#pragma mark StackCardViewProvider Methods |
+ |
+- (CardView*)cardViewWithFrame:(CGRect)frame forStackCard:(StackCard*)card { |
+ DCHECK(!ignoresTabModelChanges_); |
+ NSUInteger cardIndex = [self.cards indexOfObject:card]; |
+ DCHECK(cardIndex != NSNotFound); |
+ Tab* tab = [tabModel_ tabAtIndex:cardIndex]; |
+ DCHECK(tab); |
+ NSString* title = tab.title; |
+ if (![title length]) |
+ title = tab.urlDisplayString; |
+ CardView* view = |
+ [[[CardView alloc] initWithFrame:frame |
+ isIncognito:[tabModel_ isOffTheRecord]] autorelease]; |
+ [view setTitle:title]; |
+ [view setFavicon:[tab favicon]]; |
+ [tab retrieveSnapshot:^(UIImage* image) { |
+ [view setImage:image]; |
+ }]; |
+ if (!view.image) |
+ [view setImage:[CRWWebController defaultSnapshotImage]]; |
+ view.closeButtonSide = self.closeButtonSide; |
+ |
+ return view; |
+} |
+ |
+#pragma mark TabModelObserver Methods |
+ |
+- (void)tabModel:(TabModel*)model |
+ didInsertTab:(Tab*)tab |
+ atIndex:(NSUInteger)index |
+ inForeground:(BOOL)fg { |
+ DCHECK(model == tabModel_); |
+ StackCard* newCard = [self buildCardFromTab:tab]; |
+ [stackModel_ insertCard:newCard atIndex:index]; |
+ [self.observer cardSet:self didAddCard:newCard]; |
+} |
+ |
+// A tab was removed at the given index. |
+- (void)tabModel:(TabModel*)model |
+ didRemoveTab:(Tab*)tab |
+ atIndex:(NSUInteger)index { |
+ [self removeCardAtIndex:index]; |
+} |
+ |
+- (CGFloat)maximumStackLength { |
+ return [stackModel_ maximumStackLength]; |
+} |
+ |
+- (BOOL)cardIsCollapsed:(StackCard*)card { |
+ return [stackModel_ cardIsCollapsed:card]; |
+} |
+ |
+- (BOOL)stackIsFullyCollapsed { |
+ return [stackModel_ stackIsFullyCollapsed]; |
+} |
+ |
+- (BOOL)stackIsFullyFannedOut { |
+ return [stackModel_ stackIsFullyFannedOut]; |
+} |
+ |
+- (BOOL)stackIsFullyOverextended { |
+ return [stackModel_ stackIsFullyOverextended]; |
+} |
+ |
+- (CGFloat)overextensionAmount { |
+ return [stackModel_ overextensionAmount]; |
+} |
+ |
+- (BOOL)isCardInStartStaggerRegion:(StackCard*)card { |
+ NSInteger cardIndex = [self.cards indexOfObject:card]; |
+ DCHECK(cardIndex != NSNotFound); |
+ // The last card in the start stack is not actually collapsed, thus the -1. |
+ NSInteger indexOfLastCardInStartStaggerRegion = |
+ [stackModel_ lastStartStackCardIndex] - 1; |
+ return (cardIndex <= indexOfLastCardInStartStaggerRegion); |
+} |
+ |
+- (BOOL)isCardInEndStaggerRegion:(StackCard*)card { |
+ NSInteger cardIndex = [self.cards indexOfObject:card]; |
+ DCHECK(cardIndex != NSNotFound); |
+ NSInteger indexOfFirstCardInEndStaggerRegion = |
+ [stackModel_ firstEndStackCardIndex]; |
+ return (indexOfFirstCardInEndStaggerRegion != -1 && |
+ cardIndex >= indexOfFirstCardInEndStaggerRegion); |
+} |
+ |
+- (void)updateShadowLayout { |
+ CGRect stackFrame = CGRectNull; |
+ for (StackCard* card in self.cards) { |
+ if (![card isEqual:self.closingCard]) { |
+ CGRect cardFrame = |
+ AlignRectOriginAndSizeToPixels(LayoutRectGetRect(card.layout)); |
+ stackFrame = CGRectIsNull(stackFrame) |
+ ? cardFrame |
+ : CGRectUnion(stackFrame, cardFrame); |
+ } |
+ } |
+ [stackShadow_ |
+ setHidden:CGRectIsNull(stackFrame) || CGRectIsEmpty(stackFrame)]; |
+ if (![stackShadow_ isHidden]) { |
+ [stackShadow_ |
+ setFrame:UIEdgeInsetsInsetRect(stackFrame, kCardShadowLayoutOutsets)]; |
+ } |
+} |
+ |
+@end |
+ |
+@implementation CardSet (Testing) |
+ |
+- (StackCard*)cardForTab:(Tab*)tab { |
+ NSUInteger tabIndex = [tabModel_ indexOfTab:tab]; |
+ if (tabIndex == NSNotFound) |
+ return nil; |
+ return [self.cards objectAtIndex:tabIndex]; |
+} |
+ |
+- (void)setStackModelForTesting:(CardStackLayoutManager*)stackModel { |
+ stackModel_.reset([stackModel retain]); |
+} |
+ |
+@end |