| 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
|
|
|