
/*
**	MiscSwitchView.m 
**	Copyright (C) 1995  David Slotnick 
*/

// RCS identification information
static char *rcsID = "$Id: MiscSwitchView.m,v 1.1 1996/08/06 03:37:00 slotnick Exp $";
static void __AvoidCompilerWarning(void) {if(!rcsID)__AvoidCompilerWarning();}

// NeXTSTEP Headers
#import <InterfaceBuilder/InterfaceBuilder.h>

// System Headers

// Third Party Headers

// Other Headers

// Class Headers
#import "MiscSwitchView.h"

// Private Constants
static NSString* 	MiscSwitchViewDefaultTitle = @"SwitchView";
static int          SwitchViewRelinquishFocusPollingInterval = 0.1;

// Private Macros

// Module variables (static variables)

// Public Constants
int MiscSwitchViewNoViewVisible = -1;



@implementation MiscSwitchView

/*"
   The SwitchView is a Box that manages multiple content views.  A particular content view can be "switched" (made visible) with a variety of control-like action messages.  The SwitchView is particularly useful in the construction of inspector panels, such as those found in the Workspace and Interface Builder.  The SwitchView is palettized for easy usage.

   The SwitchView supports a delegate which is notified before and after all significant operations with the following methods:

   _{-switchViewWillAddView:}
   _{-switchView:willInsertViewAtIndex:}
   _{-switchView:willDeleteViewAtIndex:}
   _{-switchView:willDisplayViewAtIndex:}
   _{-switchViewDidAddView:}
   _{-switchView:didInsertViewAtIndex:}
   _{-switchView:didDeleteViewAtIndex:}
   _{-switchView:didDisplayViewAtIndex:}

   The delegate is only sent messages it responds to.
"*/


//-------------------------------------------------------------------
// 	Accessor methods
//-------------------------------------------------------------------

- (void) setViews:(NSMutableArray*)views
/*"
   Sets the reciver's subview array.
"*/
{
    [_views release];
    _views = [views retain];
}

- (NSMutableArray*) views
/*" 
    Returns the receiver's subview array.
"*/
{
    return _views;
}

- (void) setDelegate:(id)delegate
/*"
   Sets the receiver's delegate.  The argument is %not retained.
"*/
{
    _delegate = delegate;
}

- (id) delegate
/*" 
    Returns the receiver's delegate.
"*/
{
    return _delegate;
}


//-------------------------------------------------------------------
// 	Initialization / deallocation
//-------------------------------------------------------------------

/*
**	Standard initializer. 
*/
- (id) initWithFrame:(NSRect)frameRect
/*"
   Extends the superclass method to set the reciever's title, allocate an array to hold it's subviews, and add a single blank view by default.  Returns self if successful, nil otherwise.
"*/
{
    BOOL error = ([super initWithFrame:frameRect] == nil) ? YES : NO;

    if (!error) {			

        // Set the title.
        [self setTitle:MiscSwitchViewDefaultTitle];

        // Create an array to hold the subviews.
        [self setViews:[NSMutableArray array]];

        // Add the first subview.
        [self addView];

        // Free myself if there was an error.
        if (error) {
            [self autorelease];
        }
    }

    return error ? nil : self;
}

- (void)dealloc
/*"
   Standard deallocator.
"*/
{
	[_views release];

    [super dealloc];
}


//-------------------------------------------------------------------
// 	Manipulating subviews
//-------------------------------------------------------------------

- (void) addView
/*"
   Adds a view to the end of the receiver's subview array.  The view isn't added if the delegate responds to #switchViewWillAddView: with NO.  If the operation is successful, the delegate is notified with #switchViewDidAddView:.
"*/
{
    SEL 	selector = NULL;  // used to improve readability
    id 		del = [self delegate];
    BOOL	error = NO;

    // Ask the delegate.
    if (!error) {
        selector = @selector (switchViewWillAddView:);
        if (del != nil && [del respondsToSelector:selector]) {
            error = ![del switchViewWillAddView:self];
        }	
    }

    // Proceed if the delegate approves.
	if (!error) {
        NSView*		newView = nil;
	
		// Create a new view and add it to our subview array.
		newView = [[[NSView allocWithZone:[self zone]]
            initWithFrame:NSZeroRect]
                autorelease];
		[[self views] addObject:newView];

        // Display the just-added view.
        [self displayViewAtIndex:[self lastIndex]];

        // Added to fix IB bug [dls 4/30/96].
        // Note: this has to happen after the -displayViewAtIndex:
        //   above, since the processing in that method trashes
        //   the object hierarchy again (probably because it resets
        //   the MiscSwitchView's content view).
        [self resetObjectHierarchy];
        // End added to fix IB bug [dls 4/30/96].

        // Notify the delegate that a subview was added.
        selector = @selector (switchViewDidAddView:);
        if (del != nil && [del respondsToSelector:selector]) {
            [del switchViewDidAddView:self];
        }
    }
}

- (void) insertViewAtIndex:(int)index
/*"
   Inserts a new view at the specified index, or raises an NSRangeException if index is invalid.  The view isn't added if the delegate responds to #switchView:willInsertViewAtIndex: with NO.  If the operation is successful, the delegate is notified with #switchView:didInsertViewAtIndex:.
"*/
{
    SEL		selector = NULL; // used to improve readability
    id 		del = [self delegate];
    BOOL	error = NO;
    
	// Check the index for validity.
    if (!error && ![self isIndexValid:index]) {
		[NSException raise:NSRangeException format:@"-[MiscSwitchView insertViewAtIndex:]:: %d out of range", index];
        error = YES;
    }	
	
    // Ask the delegate.
    if (!error) {
        selector = @selector (switchView:willInsertViewAtIndex:);
        if (del != nil && [del respondsToSelector:selector]) {
            error = ![del switchView:self willInsertViewAtIndex:index];
        }
    }

    // Only proceeed if the delegate approves.
    if (!error) {
        NSView*		newView = nil;

        // Create a new view and insert into our subview array
        // at the specified index.
        newView = [[[NSView allocWithZone:[self zone]]
            initWithFrame:NSZeroRect]
                autorelease];
        [[self views] insertObject:newView atIndex:index];

        // Display the just-added view.
        [self displayViewAtIndex:index];

        // Added to fix IB bug [dls 4/30/96].
        // Note: this has to happen after the -displayViewAtIndex:
        //   above, since the processing in that method trashes
        //   the object hierarchy again (probably because it resets
        //   the MiscSwitchView's content view).
        [self resetObjectHierarchy];
        // End added to fix IB bug [dls 4/30/96].

       // Notify the delegate that a subview was inserted.
        // The use of the "sel" variable here is just intended
        // to improve readability.
        selector = @selector (switchView:didInsertViewAtIndex:);
        if (del != nil && [del respondsToSelector:selector]) {
            [del switchView:self didInsertViewAtIndex:index];
        }
    }
}

- (void) displayViewAtIndex:(int)index
/*"
   Displays the view at the specified index, or raises an NSRangeException
   if index is invalid.  The view isn't displayed if the delegate responds to #switchView:willDisplayViewAtIndex: with NO.  If the operation is successful, the delegate is notified with #switchView:didDisplayViewAtIndex:.
"*/
{
    SEL		selector = NULL;  // used to improve readability
    id 		del = [self delegate];
    BOOL	error = NO;

    // Check the index for validity.
    if (!error && ![self isIndexValid:index]) {
        [NSException raise:NSRangeException
                    format:@"-[MiscSwitchView displayViewAtIndex:]:: "
                           @"%d out of range", index];
        error = YES;
    }	
    
    // Ask the delegate.
    if (!error) {
        selector = @selector (switchView:willDisplayViewAtIndex:);
        if (del != nil && [del respondsToSelector:selector]) {
            error = ![del switchView:self willDisplayViewAtIndex:index];
        }
    }
    
    // If the currently focused View is one of my subviews (or
    // one of my subviews' subviews, etc.), the -setContentView:
    //  message below will result in a "removed a View from the
    // View hierarchy that had been lockFocus'ed" error.  So, if
    // the currently focused View is one of my subviews, I resend
    // this message every SwitchViewRelinquishFocusPollingInterval
    // seconds until the View relinquishes focus and *then* proceed
    // with the View switch.  [This might be done more effectively
    // with an NSTimer -- worth investigating.]
    if (!error && [[NSView focusView] isDescendantOf:self]) {

        // Cancel the previous request.
        [NSObject cancelPreviousPerformRequestsWithTarget:self
            selector:@selector (displayViewAtIndex:)
              object:[NSNumber numberWithInt:index]];

        // Reperform the selector.
        [self performSelector:@selector (displayViewAtIndex:)
                   withObject:[NSNumber numberWithInt:index]
                   afterDelay:SwitchViewRelinquishFocusPollingInterval];

        error = YES;
    }

    // Only proceed if the delgate approves.
    if (!error) {
        NSView*	newContentView = [[self views] objectAtIndex:index];
        NSView*	oldContentView = [self contentView];

        // Reset the content view.
        [self setContentView:newContentView];

        // Remove the old content View from the View hierarchy if
        // it's not also the new content View.
        if (oldContentView != newContentView) {
            [oldContentView removeFromSuperview];
        }

        // Notify the delegate that we displayed a new view.
        selector = @selector (switchView:didDisplayViewAtIndex:);
        if (del != nil && [del respondsToSelector:selector]) {
            [del switchView:self didDisplayViewAtIndex:index];
        }

        // Redisplay to reflect changes.
        [self display];
    }
}

- (void) deleteViewAtIndex:(int)index
/*"
   Displays the view at the specified index, or raises an NSRangeException
   if index is invalid.  The view isn't displayed if the delegate responds to #switchView:willDeleteViewAtIndex: with NO.   If the operation is successful, the delegate is notified with #switchView:didDeleteViewAtIndex:.
"*/
{
    SEL		selector = NULL;   // used to improve readability
    id 		del = [self delegate];
    BOOL	error = NO;

    // Check the index for validity.
    if (!error && ![self isIndexValid:index]) {
        [NSException raise:NSRangeException
                    format:@"-[MiscSwitchView deleteViewAtIndex:]:: "
                           @"%d out of range", index];
        error = YES;
    }

    // Don't delete the last view.
    if (!error) {
        if ([self viewCount] == 1) {
            [NSException raise:NSRangeException
                format:@"-[MiscSwitchView deleteViewAtIndex:]:: "
                       @"View count == 1, cannot delete last view"];
            error = YES;
        }
    }

    // Ask the delegate.
    if (!error) {
        selector = @selector (switchView:willDeleteViewAtIndex:);
        if (del != nil && [del respondsToSelector:selector]) {
            error = ![del switchView:self willDeleteViewAtIndex:index];
        }
    }

	if (!error) {
        NSView*		viewToRemove = [[self views] objectAtIndex:index];
        BOOL		isLastView = (index == [self lastIndex]);

        // The Box implementation of -setContentView: messages
        // the current contentView, so if the View we're about
        // to delete is the view we're removing should be detached
        // from it's superview before being removed.
        if (viewToRemove == [self contentView]) {
            [self setContentView:nil];
        }

        // Remove the view.
        [viewToRemove removeFromSuperview];
        [[self views] removeObjectAtIndex:index];

        // Notify the delegate that a view was removed.
        selector = @selector (switchView:didDeleteViewAtIndex:);
        if ([del respondsToSelector:selector]) {
            [del switchView:self didDeleteViewAtIndex:index];
        }

        // Display the view that moved in to close the gap.
        if (isLastView) {
            [self displayViewAtIndex:[self lastIndex]];
        }
        else {
            [self displayViewAtIndex:index];
        }
    }
}

- (int) viewCount
/*"
   Returns the number of views in the receiver's subview array.
"*/
{
    return [[self views] count];
}

- (NSView*) viewAtIndex:(int)index
/*"
    Returns the view at the specified index, or raises an NSRangeException
    if index is invalid.
"*/
{
    NSView*		view = nil;
    BOOL	 	error = NO;

    // Check the index for validity.
    if (!error && ![self isIndexValid:index]) {
        [NSException raise:NSRangeException
           format:@"-[MiscSwitchView viewAtIndex:]:: %d out of range", index];
        error = YES;
    }

    if (!error) {
        view = [[self views] objectAtIndex:index];
    }
		
    return error ? nil : view;
}



//-------------------------------------------------------------------
// 	Obtaining indexes
//-------------------------------------------------------------------

- (int) firstIndex
/*"
   Return the index of the first view in the receiver's subview array.
"*/
{
    return 0;
}

- (int) lastIndex
/*"
   Return the index of the last view in the receiver's subview array.
"*/
{
    return [self viewCount] - 1;
}

- (int) indexBefore:(int)index
/*"
   Returns the index of the view just before the view at the specified index, [self lastIndex] if the argument is equal to [self firstIndex], or [self firstIndex] if the argument is an invalid index.
"*/

{
    BOOL 	error = NO;
    int 	indexBefore = [self firstIndex];
	
    // Check index validity.
	if (![self isIndexValid:index]) {
        error = YES;
    }

    // Compute the index predecessor.
    if (!error) {
		if (index == [self firstIndex]) {
			indexBefore = [self lastIndex];
		}
        else {
			indexBefore = index - 1;
		}
    }
	
    return error ? [self firstIndex] : indexBefore;
}

- (int) indexAfter:(int)index
/*"
    Returns the index of the view just after the view at the specified index, [self firstIndex] if the argument is equal to [self lastIndex], or [self lastIndex] if the argument is an invalid index.
"*/
{
    BOOL 	error = NO;
    int 	indexBefore = [self firstIndex];

    // Check index validity.
    if (![self isIndexValid:index]) {
        error = YES;
    }

    // Compute the index successor.
    if (!error) {
        if (index == [self lastIndex]) {
            indexBefore = [self firstIndex];
        }
        else {
            indexBefore = index + 1;
        }
    }

    return error ? [self lastIndex] : indexBefore;
}

- (int) indexOfVisibleView
/*"
    Returns the index of the currently visible view.
"*/
{
    NSView*		contentView = [self contentView];
    int 		returnIndex = MiscSwitchViewNoViewVisible;

    if (contentView != nil) {
        returnIndex = [[self views] indexOfObject:contentView];
	}
    
	return returnIndex;
}

- (BOOL) isIndexValid:(int)index
/*"
   Returns YES if the specified index is valid, NO otherwise.  Only valid indexes can be passed to the methods in this class that expect an index.  Passing an invalid index to these methods will almost certainly cause the instance to throw an exception.
"*/
{
    return index >= [self firstIndex] && index <= [self lastIndex];
}


//-------------------------------------------------------------------
// 	Contol interoperability
//-------------------------------------------------------------------

- (void) takeIntValueFrom:(id)sender
/*"
   Displays the view at the index obtained by invoking [sender intValue].
"*/
{
    [self displayViewAtIndex:[sender intValue]];
}

- (void) takeStringValueFrom:(id)sender
/*"
   Displays the view at the index obtained by invoking [[sender stringValue] intValue].
"*/
{
    [self displayViewAtIndex:[[sender stringValue] intValue]];
}

- (void) takeFloatValueFrom:(id)sender
/*"
   Displays the view at the index obtained by invoking [sender floatValue] and coercing the result to an integer.
"*/
{
    [self displayViewAtIndex:(int)[sender floatValue]];
}

- (void)takeDoubleValueFrom:(id)sender
/*"
   Displays the view at the index obtained by invoking [sender doubleValue] and coercing the result to an integer.
"*/
{
    [self displayViewAtIndex:(int)[sender doubleValue]];
}

- (void) takeTagValueFrom:(id)sender
/*"
   Displays the view at the index obtained by invoking [sender tag].
"*/
{
    [self displayViewAtIndex:[sender tag]];
}

- (void)takeSelectedTagValueFrom:(id)sender
/*"
   Displays the view at the index obtained by invoking [sender selectedTag].  Nothing is done if sender doesn't have a selected cell (i.e., responds to [sender selectedCell] with nil).
"*/
{
    // The selected tag can be "-1" indicating no selected cell.
    // You can make this happen, for example, by connecting a
    // matrix of button cells to a MiscSwitchView and then clicking
    // on the buttons quickly and randomly; eventually you'll get
    // here without a selected cell.  Sounds like an NSMatrix bug
    // to me...

    if ([sender selectedCell] != nil) {
        [self displayViewAtIndex:[sender selectedTag]];
    }
}


//-------------------------------------------------------------------
// 	Archiving
//-------------------------------------------------------------------

- (id) initWithCoder:(NSCoder*)coder
/*"
   Extends the superclass method to read the subviews from the coder.  Returns self if successful, nil otherwise.
"*/
{
    BOOL error = ([super initWithCoder:coder] == nil) ? YES : NO;

    if (!error) {
        int viewCount = 0;

        // Unarchive the number of views.
        [coder decodeValuesOfObjCTypes:"i", &viewCount];

        // Unarchive the view.
        [self setViews:[NSMutableArray arrayWithCapacity:viewCount]];
        while (viewCount--) {
            NSView*	 view = nil;
            [coder decodeValuesOfObjCTypes:"@", &view];
            [[self views] addObject:view];
        }

        // Display the content view.
        [self displayViewAtIndex:
            [[self views] indexOfObject:[self contentView]]];	

        // Added to fix IB bug [dls 4/30/96].
        // Note: this has to happen after the -displayViewAtIndex:
        //   above, since the processing in that method trashes
        //   the object hierarchy again (probably because it resets
        //   the MiscSwitchView's content view).
        [self resetObjectHierarchy];
        // End added to fix IB bug [dls 4/30/96].

        // Free ourselves if there was an error.
        if (error) {
            [self autorelease];
        }
    }

    return error ? nil : self;
}

- (void) encodeWithCoder:(NSCoder*)coder
/*"
   Extends the superclass method to encode the subviews to the coder.
"*/
{
    BOOL error = NO;

    if (!error) {
        NSEnumerator*	enumerator = nil;
        int				viewCount = 0;
        NSView* 		view = nil;

        // Perform superclass functionality.
        [super encodeWithCoder:coder];

        // Encode the number of views.
        viewCount = [self viewCount];
        [coder encodeValuesOfObjCTypes:"i", &viewCount];

        // Encode the views.
        enumerator = [[self views] objectEnumerator];
        view = [enumerator nextObject];
        while (view != nil) {
            [coder encodeValuesOfObjCTypes:"@", &view];
            view = [enumerator nextObject];
        }
    }
}


// Added to fix IB bug [dls 4/30/96].
- (void) resetObjectHierarchy
/*"	
   Reset the object hierarchy.  This method fixes a strage bug whereby objects couldn't be dragged on a selected MiscSwitchView--the box just wouldn't activate.  Poking around in IB I discovered that the  IBViewEditor for the MiscSwitchView's subview(s) wasn't a subview of the IBBoxEditor for the MiscSwitchView itself.  This code seems to reset that relationship and fix the problem.
"*/
{
    // Added to fix IB bug whereby whereby objects couldn't be added
    // to the MiscSwitchView [dls 4/30/96].
    [[(id <IB>)[NSApplication sharedApplication] activeDocument]
                attachObjects:[self subviews]
                     toParent:self];
    // End added [dls 4/30/96].
}
// End added to fix IB bug [dls 4/30/96].

@end





@implementation MiscSwitchView (PrivateMethods)

- (void) _deleteViewAtIndex:(NSNumber*)aNumber
/*"
   Just a wrapper around -deleteViewAtIndex: that returns void instead
   of int, and is thus invokable via a delayed perform.
"*/
{
    [self deleteViewAtIndex:[aNumber intValue]];
}

@end
