FilteringArrayController.m   [plain text]


/*
 * Copyright (c) 2004-2007 Apple Inc. All rights reserved.
 *
 * IMPORTANT:  This Apple software is supplied to you by Apple Inc. ("Apple") in 
 * consideration of your agreement to the following terms, and your use, installation, 
 * modification or redistribution of this Apple software constitutes acceptance of these
 * terms.  If you do not agree with these terms, please do not use, install, modify or 
 * redistribute this Apple software.
 *
 * In consideration of your agreement to abide by the following terms, and subject to these 
 * terms, Apple grants you a personal, non exclusive license, under AppleÕs copyrights in this 
 * original Apple software (the ÒApple SoftwareÓ), to use, reproduce, modify and redistribute 
 * the Apple Software, with or without modifications, in source and/or binary forms; provided 
 * that if you redistribute the Apple Software in its entirety and without modifications, you 
 * must retain this notice and the following text and disclaimers in all such redistributions 
 * of the Apple Software.  Neither the name, trademarks, service marks or logos of Apple 
 * Computer, Inc. may be used to endorse or promote products derived from the Apple Software 
 * without specific prior written permission from Apple. Except as expressly stated in this 
 * notice, no other rights or licenses, express or implied, are granted by Apple herein, 
 * including but not limited to any patent rights that may be infringed by your derivative 
 * works or by other works in which the Apple Software may be incorporated.
 * 
 * The Apple Software is provided by Apple on an "AS IS" basis.  APPLE MAKES NO WARRANTIES, 
 * EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-
 * INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE 
 * SOFTWARE OR ITS USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. 
 *
 * IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL 
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS 
 * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, 
 * REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND 
 * WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR 
 * OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


//ÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑ
//	Imports
//ÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑ

#import "FilteringArrayController.h"
#import "SCSITargetProberKeys.h"
#import <Foundation/NSKeyValueObserving.h>


//ÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑ
//	Implementation
//ÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑ

@implementation FilteringArrayController


// Called when the SCSITargetProberDocument nib loads.

- ( void ) awakeFromNib
{
	
	// Initialize the search category to be by title (Description).
	searchCategory = kSearchCategoryTitle;
	
	// Set the placeholder string for the search category.
	[ [ searchField cell ] setPlaceholderString: [ self placeholderForTag: searchCategory ] ];
	
	// Setup the search menu.
	[ self setupSearchMenu ];
	
	// Observe any value changes to NSUserDefaultsController.
	[ [ NSUserDefaultsController sharedUserDefaultsController ] addObserver: self
		forKeyPath: kShowTargetIDKeyPath options: NSKeyValueObservingOptionNew context: nil ];

	[ [ NSUserDefaultsController sharedUserDefaultsController ] addObserver: self
		forKeyPath: kShowDescriptionKeyPath options: NSKeyValueObservingOptionNew context: nil ];

	[ [ NSUserDefaultsController sharedUserDefaultsController ] addObserver: self
		forKeyPath: kShowRevisionKeyPath options: NSKeyValueObservingOptionNew context: nil ];

	[ [ NSUserDefaultsController sharedUserDefaultsController ] addObserver: self
		forKeyPath: kShowFeaturesKeyPath options: NSKeyValueObservingOptionNew context: nil ];

	[ [ NSUserDefaultsController sharedUserDefaultsController ] addObserver: self
		forKeyPath: kShowPDTKeyPath options: NSKeyValueObservingOptionNew context: nil ];
	
}


// Called when values change for NSUserDefaultsController.
// See NSKeyValueObserving.h for more information.

- ( void ) observeValueForKeyPath: ( NSString * ) keyPath
						 ofObject: ( id ) object
						   change: ( NSDictionary * ) change
						  context: ( void * ) context
{

#pragma unused ( keyPath )
#pragma unused ( object )
#pragma unused ( change )
#pragma unused ( context )
	
	// Set up the search menu based on any changes observed.
	[ self setupSearchMenu ];
	
}


// Get a localized string to use as a placeholder in the NSSearchFieldCell
// based on the passed in tag (which corresponds to the selected menu item
// in the NSSearchFieldCell's menu).

- ( NSString * ) placeholderForTag: ( int ) tag
{
	
	NSString *	string = nil;
	
	switch ( tag )
	{
		
		case kSearchCategoryID:
			string = kIDString;
			break;
		
		case kSearchCategoryTitle:
			string = kDescriptionString;
			break;
		
		case kSearchCategoryRevision:
			string = kRevisionString;
			break;
		
		case kSearchCategoryFeatures:
			string = kFeaturesString;
			break;
		
		case kSearchCategoryPDT:
			string = kPDTString;
			break;
		
	}
	
	// Get the localized string based on the string we have.
	string = [ [ NSBundle mainBundle ] localizedStringForKey: string
													   value: string
													   table: nil ];
	
	return string;
	
}


// Called to set up the search menu (removes items not
// checked in prefs box and sets checkmark on selected category).

- ( void ) setupSearchMenu
{
	
	NSMenu *	menu	= nil;
	id			values	= nil;
	id			item 	= nil;
	int			index	= 0;
	int			count	= 0;
	BOOL		itemSet	= NO;
	
	// Copy the menu in the nib.
	menu = [ sortMenu copyWithZone: nil ];
	
	values = [ [ NSUserDefaultsController sharedUserDefaultsController ] values ];
	
	// Should we remove the "ID" category from the search menu?
	if ( [ [ values valueForKey: kShowTargetIDString ] boolValue ] == NO )
	{
		
		// Yes, remove it.
		item = [ menu itemWithTag: kSearchCategoryID ];
		[ menu removeItem: item ];
		
	}
	
	// Should we remove the "Description" category from the search menu?
	if ( [ [ values valueForKey: kShowDescriptionString ] boolValue ] == NO )
	{
		
		// Yes, remove it.
		item = [ menu itemWithTag: kSearchCategoryTitle ];
		[ menu removeItem: item ];
		
	}
	
	// Should we remove the "Revision" category from the search menu?
	if ( [ [ values valueForKey: kShowRevisionString ] boolValue ] == NO )
	{
		
		// Yes, remove it.
		item = [ menu itemWithTag: kSearchCategoryRevision ];
		[ menu removeItem: item ];
		
	}
	
	// Should we remove the "Features" category from the search menu?
	if ( [ [ values valueForKey: kShowFeaturesString ] boolValue ] == NO )
	{
		
		// Yes, remove it.
		item = [ menu itemWithTag: kSearchCategoryFeatures ];
		[ menu removeItem: item ];
		
	}
	
	// Should we remove the "PDT" category from the search menu?
	if ( [ [ values valueForKey: kShowPDTString ] boolValue ] == NO )
	{
		
		// Yes, remove it.
		item = [ menu itemWithTag: kSearchCategoryPDT ];
		[ menu removeItem: item ];
		
	}
	
	count = [ menu numberOfItems ];
	for ( index = 0; index < count; index++ )
	{
		
		item = [ menu itemAtIndex: index ];
		
		// Is the item with this tag the one we're currently using as
		// our search category?
		if ( [ item tag ] != searchCategory )
		{
			
			// No, make sure checkmark is off.
			[ item setState: NSOffState ];
			
		}
		
		else
		{
			
			// Yes, make sure checkmark is on.
			[ item setState: NSOnState ];
			
			// Make sure we mark that an item was set.
			itemSet = YES;
			
		}
		
	}
	
	// If there wasn't an item picked (because the preferences removed it), but there are
	// still elements in the list, change the search category to the first available one.
	if ( ( itemSet == NO ) && ( count > 0 ) )
	{
		[ [ searchField cell ] setStringValue: @"" ];
		[ self changeSearchCategory: [ menu itemAtIndex: 0 ] ];
	}
	
	else
	{
		
		// Set the search menu template. If we didn't get into this else, then
		// the search category will get changed and we will come back and hit
		// this case for sure.
		[ [ searchField cell ] setSearchMenuTemplate: menu ];
		
	}
	
}


// Action called by NSSearchField when something has been typed in it.

- ( IBAction ) search: ( id ) sender
{
	
	// Set the search string so we can use it in arrangeObjects:
	searchString = [ sender stringValue ];
	
	// Call rearrange objects. This decides to call arrangeObjects or not.
	[ self rearrangeObjects ];
	
}


// Called by all menu items in the NSMenu attached to the NSSearchFieldCell
// to change the search category.

- ( IBAction ) changeSearchCategory: ( id ) sender
{
	
	// Get the new category by the tag from the menu item which sent the
	// action.
	searchCategory = [ sender tag ];
	
	// Make sure the UI updates the visible items based on the search
	// category change.
	[ self search: searchField ];
	
	// Change the placeholder string so that when the NSSearchField is cleared
	// or inactive, a placeholder exists.
	[ [ searchField cell ] setPlaceholderString: [ self placeholderForTag: searchCategory ] ];
	
	// Change the search menu (changes the checkmarks).
	[ self setupSearchMenu ];
		
}


// Called to arrange objects.

- ( NSArray * ) arrangeObjects: ( NSArray * ) objects
{
	
	// Is there a search string?
	if ( ( searchString == nil ) || ( [ searchString isEqualToString: @"" ] ) )
	{
		// No, short circuit out...
		return [ super arrangeObjects: objects ];
	}
	
	NSMutableArray *	filteredObjects		= nil;
	NSEnumerator *		objectsEnumerator	= nil;
	NSString *			keyPath				= nil;
	id 					item				= nil;
	
	// Create an array of the same size as the original (so it's big
	// enough to hold all the potential filtered objects)
	filteredObjects = [ NSMutableArray arrayWithCapacity: [ objects count ] ];
	
	// Figure out which key path to use based on the search category.
	switch ( searchCategory )
	{
		
		case kSearchCategoryID:
			keyPath = kDeviceIdentifierKeyPath;
			break;
		
		case kSearchCategoryTitle:
			keyPath = kDeviceTitleKeyPath;
			break;
		
		case kSearchCategoryRevision:
			keyPath = kDeviceRevisionKeyPath;
			break;
		
		case kSearchCategoryFeatures:
			keyPath = kDeviceFeaturesKeyPath;
			break;
		
		case kSearchCategoryPDT:
			keyPath = kDevicePDTKeyPath;
			break;
		
	}
	
	// Get an object enumerator
	objectsEnumerator = [ objects objectEnumerator ];
	
	// Loop over the items and based on the search category, figure out if the
	// item should remain visible in the table view or not.
	item = [ objectsEnumerator nextObject ];
	while ( item != nil )
	{
		
		NSString * 	lowerCaseSearchString 	= nil;
		id			value					= nil;
		
		// Change to lower case string for searching.
		lowerCaseSearchString = [ searchString lowercaseString ];
		
		// Get the value at the key path.
		value = [ item valueForKeyPath: keyPath ];
		
		// Add the object if it matches.
		[ self addObject: item toArray: filteredObjects ifValue: value matchesString: lowerCaseSearchString ];
		
		// Get the next object.
		item = [ objectsEnumerator nextObject ];
		
	}
	
	return [ super arrangeObjects: filteredObjects ];
	
}


// Adds the object to the array if the matching string matches.

- ( void ) addObject: ( id ) object
			 toArray: ( NSMutableArray * ) array
			 ifValue: ( id ) inValue
	   matchesString: ( NSString * ) matchingString
{
	
	// Is the value an NSArray of values?
	if ( [ inValue isKindOfClass: [ NSArray class ] ] )
	{			
		
		NSArray *	values 		= nil;
		int			count		= 0;
		int			index		= 0;
		
		// Yes, we have an NSArray of values. We have to loop over the whole array to
		// determine if the item should be visible or not.
		
		values = ( NSArray * ) inValue;
		count = [ values count ];
		
		for ( index = 0; index < count; index++ )
		{
			
			id	value = nil;
			
			// Get the object at index.
			value = [ values objectAtIndex: index ];
			
			// Call this method recursively (the object at that index could be an NSNumber,
			// NSString, NSArray, etc.)
			[ self addObject: object toArray: array ifValue: value matchesString: matchingString ];
			
		}
		
	}
	
	// Is this value an NSNumber?
	else if ( [ inValue isKindOfClass: [ NSNumber class ] ] )
	{
		
		NSString *  string	= nil;
		
		string = [ [ inValue stringValue ] lowercaseString ];
		
		// Is the search string anywhere to be found?
		if ( [ string rangeOfString: matchingString ].location != NSNotFound )
		{
			
			// Yes, add the item to the filtered objects list.
			[ array addObject: object ];
				
		}
	
	}
	
	// Is this value an NSString?
	else if ( [ inValue isKindOfClass: [ NSString class ] ] )
	{	
		
		NSString *  string	= nil;
		
		string = [ inValue lowercaseString ];
		
		// Is the search string anywhere to be found?
		if ( [ string rangeOfString: matchingString ].location != NSNotFound )
		{
			
			// Yes, add the item to the filtered objects list.
			[ array addObject: object ];
				
		}
		
	}
	
	// Add more class types as necessary...
	
}


@end