BWToolkit / BWTabViewController.m

//
//  KSTabViewController.m
//
//  Created by Mike Abdullah on 17/08/2009.
//  Copyright 2009 Karelia Software. All rights reserved.
//

#import "BWTabViewController.h"


@implementation BWTabViewController

#pragma mark Init & Dealloc

- (void)BW_initializeIvars
{
    _viewControllers = [[NSMutableArray alloc] init];
    _tabViewItems = [[NSMutableArray alloc] init];
}

- (id)initWithTabViewType:(NSTabViewType)type;
{
    [super init];
    
    _tabViewType = type;
    [self BW_initializeIvars];
    
    return self;
}

- (id)init
{
    return [self initWithTabViewType:NSTopTabsBezelBorder];
}

- (void)close;  // sends -close message to any .viewControllers that respond to it
{
    for (NSViewController *aController in [self viewControllers])
    {
        if ([aController respondsToSelector:_cmd]) [aController performSelector:_cmd];
    }
}

- (void)dealloc
{
    // Detach controllers first
    [[self viewControllers] makeObjectsPerformSelector:@selector(setParentViewController:)
                                            withObject:nil];
    
    // Tear down views
    [self setTabView:nil];  // releases view and tears down binding/delegation
    
    // Release ivars
    [_tabViewItems release];
    [_viewControllers release];
    
    [super dealloc];
}

#pragma mark View Properties

- (NSTabView *)tabView
{
    [self view];    // ensure it's loaded
    return _tabView;
}

- (void)setTabView:(NSTabView *)tabView
{
    // Dump the old tabview (including delegate) and store the new one
    [[self tabView] setDelegate:nil];
    [[self tabView] unbind:NSSelectedIndexBinding];
    
    [tabView retain];
    [_tabView release], _tabView = tabView;
    
    
    // Wait until after removing items before setting the delegate so we can't get told an item not managed by us is selected. In case someone has explicitly set the delegate in a nib file, temporarily set it to nil.
    [[self tabView] setDelegate:nil];
    
    
    // Remove all existing items as we're now taking control
    for (NSTabViewItem *aTabViewItem in [tabView tabViewItems])
    {
        [tabView removeTabViewItem:aTabViewItem];
    }
    [[self tabView] setDelegate:self];
    
    
    // Add items for our content
    for (NSTabViewItem *aTabViewItem in _tabViewItems)  // …1 item per view controller
    {
        [tabView addTabViewItem:aTabViewItem];
    }
    
    
    // Keep selection in sync. It would be lying to the controller if tabview is not onscreen, since it isn't really appearing. Sending -bind:… to nil has no effect, so is safe to skip
    if ([tabView window]) [[self selectedViewController] viewWillAppear:NO];
    
    [tabView bind:NSSelectedIndexBinding
         toObject:self
      withKeyPath:@"selectedIndex"
          options:nil];
    
    if ([tabView window]) [[self selectedViewController] viewDidAppear:NO];
}

- (BOOL)isTabViewLoaded { return (_tabView != nil); }

- (void)loadView
{
    // If a custom nib hasn't been specified for the view, create one ourselves
    if ([self nibName])
    {
        [super loadView];
    }
    else
    {
        NSTabView *tabView = [[NSTabView alloc] init];
        [tabView setTabViewType:_tabViewType];
        
        // If the tabview is too small, any items added to it will be shrunk to a negative size, potentially ruining their layout. -[NSTabView minimumSize] would appear to be the perfect solution to this, but unfortunately I find it's often wrong, returning undersized results. Instead, we can choose a size that gives a contentRect of 0.
        NSRect contentRect = [tabView contentRect];
        NSSize minSize = [tabView frame].size;
        minSize.width -= contentRect.size.width;
        minSize.height -= contentRect.size.height;
        [tabView setFrameSize:minSize];
        
        [self setView:tabView];
        [self setTabView:tabView];
        [tabView release];
        
        [self didCustomLoadView];
    }
}

#pragma mark Managing the View Controllers

- (NSArray *)viewControllers
{
    return [[_viewControllers copy] autorelease];
}

- (NSUInteger)countOfViewControllers;
{
    return [_viewControllers count];
}

- (void)insertViewController:(NSViewController *)controller atIndex:(NSUInteger)index;
{
    // Store controller
    NSUInteger oldCount = [self countOfViewControllers];
    [_viewControllers insertObject:controller atIndex:index];
    [controller setParentViewController:self];
    
     
    // Store corresponding NSTabViewItem
    NSTabViewItem *tabViewItem = [[NSTabViewItem alloc] initWithIdentifier:[controller identifier]];
    [tabViewItem setLabel:([controller title] ? [controller title] : @"")];
    [_tabViewItems insertObject:tabViewItem atIndex:index];
    [tabViewItem release];
    
    if ([self isTabViewLoaded]) [[self tabView] insertTabViewItem:tabViewItem atIndex:index];
    
    
    //  If this was the first controller to be inserted, also want to force it to be selected otherwise NSTabView doesn't report a change to selectedIndex
    if (oldCount == 0) [self setSelectedViewController:controller];
}

- (void)addViewController:(NSViewController *)controller;
{
    [self insertViewController:controller atIndex:[self countOfViewControllers]];
}

- (void)removeViewController:(NSViewController *)controller;
{
    // Remove controller
    [controller setParentViewController:nil];
    NSUInteger index = [_viewControllers indexOfObject:controller];
    [_viewControllers removeObjectAtIndex:index];
    
    
    // Remove corresponding NSTabViewItem
    NSTabViewItem *tabViewItem = [_tabViewItems objectAtIndex:index];
    if ([self isTabViewLoaded]) [[self tabView] removeTabViewItem:tabViewItem];
    [_tabViewItems removeObjectAtIndex:index];
}

- (NSViewController *)viewControllerWithIdentifier:(NSString *)identifier;
{
    NSParameterAssert(identifier);
    
    for (NSViewController *aController in [self viewControllers])
    {
        if ([[aController identifier] isEqualToString:identifier]) return aController;
    }
    
    return nil;
}

#pragma mark Managing the Selected Tab

// Propogating selection to tabview is performed by binding.
@synthesize selectedViewController = _selectedViewController;
- (void)setSelectedViewController:(NSViewController *)controller;
{
    // Ignore request to redisplay existing selection
    NSViewController *oldSelection = [self selectedViewController];
    if (controller == oldSelection) return;
    
    
    //  Let both ourself and our view controllers know what's going on
    //  Only makes sense to do so if the tabview itself is visible
    BOOL visible = ([self isTabViewLoaded] && [[self tabView] window]);
    
    [self willChangeValueForKey:@"selectedViewController"];
    [self willSelectViewController:controller];
    if (visible) 
    {
        [oldSelection viewWillDisappear:NO];
        [controller viewWillAppear:NO];
    }
    
    _selectedViewController = controller;
    [self didChangeValueForKey:@"selectedViewController"];  // fire here so tabview matches selection before dispatching -viewDidDisappear: etc. messages
    
    [self didSelectViewController];
    if (visible)
    {
        [oldSelection viewDidDisappear:NO];
        [controller viewDidAppear:NO];
    }
}

- (NSUInteger)selectedIndex;
{
    NSUInteger result = [[self viewControllers] indexOfObject:[self selectedViewController]];
    return result;  // will be NSNotFound if receiver has no view controllers
}

- (void)setSelectedIndex:(NSUInteger)index
{
    // Select corresponding view controller
    NSViewController *controller = [[self viewControllers] objectAtIndex:index];
    [self setSelectedViewController:controller];
}

+ (NSSet *)keyPathsForValuesAffectingSelectedIndex
{
    return [NSSet setWithObject:@"selectedViewController"];
}

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
    if ([key isEqualToString:@"selectedIndex"] || [key isEqualToString:@"selectedViewController"])
    {
        return NO;
    }
    else
    {
        return [super automaticallyNotifiesObserversForKey:key];
    }
}

- (void)willSelectViewController:(NSViewController *)controller; { }
- (void)didSelectViewController { }

- (void)tabView:(NSTabView *)tabView willSelectTabViewItem:(NSTabViewItem *)tabViewItem
{
    // Ensure the view corresponding to the tab is loaded, as we do this lazily
    NSUInteger index = [_tabViewItems indexOfObject:tabViewItem];
    NSView *view = [[[self viewControllers] objectAtIndex:index] view];
    if ([tabViewItem view] != view) [tabViewItem setView:view];
}

#pragma mark Presentation

/*  Pass on messages to selected view controller since presentation will affect it too
 */

- (void)viewWillAppear:(BOOL)animated;
{
    [super viewWillAppear:animated];
    [[self selectedViewController] viewWillAppear:animated];
}

- (void)viewDidAppear:(BOOL)animated;
{
    [super viewDidAppear:animated];
    [[self selectedViewController] viewDidAppear:animated];
}

- (void)viewWillDisappear:(BOOL)animated;
{
    [super viewWillDisappear:animated];
    [[self selectedViewController] viewWillDisappear:animated];
}

- (void)viewDidDisappear:(BOOL)animated;
{
    [super viewDidDisappear:animated];
    [[self selectedViewController] viewDidDisappear:animated];
}

#pragma mark Layout

- (NSSize)viewSizeForContentSize:(NSSize)contentSize;
{
    //  Apply the difference between our view and our TabView's contentRect to the contentSize (in reverse)
    NSTabView *tabView = [self tabView];
    NSView *view = [self view];
    
    //  First, need to be working all in one coordinate system
    NSSize myViewContentSize = [view convertSize:[tabView contentRect].size fromView:tabView];
    NSSize viewContentSize = [view convertSize:contentSize fromView:tabView];
    
    // Apply the difference
    NSSize result
    = NSMakeSize(viewContentSize.width + [view bounds].size.width - myViewContentSize.width,
                 viewContentSize.height + [view bounds].size.height - myViewContentSize.height);
    
    return result;
}

#pragma mark NSCoding

- (id)initWithCoder:(NSCoder *)decoder
{
    if (self = [super initWithCoder:decoder])
    {
        [self setIdentifier:[decoder decodeObjectForKey:@"identifier"]];
        
        NSTabView *tabView = [decoder decodeObjectForKey:@"tabView"];
        if (tabView) [self setTabView:tabView];
        
        // Make sure ivars have been initialized before unarchiving view controllers
        [self BW_initializeIvars];
        NSArray *controllers = [decoder decodeObjectForKey:@"viewControllers"];
        for (NSViewController *aController in controllers)
        {
            [self addViewController:aController];
        }
    }
    
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder
{
    [super encodeWithCoder:aCoder];
    
    [aCoder encodeObject:[self identifier] forKey:@"identifier"];
    
    
    // Super should take care of encoding view if it deems necessary. A decoded View Controller should be quite able to create the views it needs, but in case a view is explicitly archived too, want to reference the specific Tab View within it.
    if ([self isTabViewLoaded]) 
    {
        [aCoder encodeConditionalObject:[self tabView] forKey:@"tabView"];
    }
    
    
    // Should selection also be encoded?
    [aCoder encodeObject:[self viewControllers] forKey:@"viewControllers"];
}


@end
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.