EffectStackController.m
/* |
File: EffectStackController.m |
Abstract: This class controls the automatically resizeable effect stack inspector. It must also be able to resize and reconfigure itself when switching documents. |
Version: 2.1 |
Disclaimer: 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 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. |
Copyright (C) 2014 Apple Inc. All Rights Reserved. |
*/ |
#import "EffectStackController.h" |
#import "FunHouseWindowController.h" |
#import "FunHouseDocument.h" |
#import "FunHouseApplication.h" |
#import "FilterView.h" |
#import "EffectStack.h" |
#import "CoreImageView.h" |
#import "ParameterView.h" |
@implementation EffectStackController |
// since the effect stack inspector window is global to all documents, we here provide a way of accessing the shared window |
+ (id)sharedEffectStackController |
{ |
static EffectStackController *_sharedEffectStackController = nil; |
if (!_sharedEffectStackController) |
_sharedEffectStackController = [[EffectStackController allocWithZone:[self zone]] init]; |
return _sharedEffectStackController; |
} |
// load from nib (really only the stuff at the top of the inspector) |
- (id)init |
{ |
self = [self initWithWindowNibName:@"EffectStack"]; |
if (self) |
{ |
[self setWindowFrameAutosaveName:@"EffectStack"]; |
// set up an array to hold the representations of the layers from the effect stack we inspect |
boxes = [[NSMutableArray arrayWithCapacity:10] retain]; |
filterPaletteTopLevelObjects = [[NSMutableArray array] retain]; |
needsUpdate = YES; |
} |
return self; |
} |
// free up the stuff we allocate |
- (void)dealloc |
{ |
[filterClassname release]; |
[categories release]; |
[timer release]; |
[[NSNotificationCenter defaultCenter] removeObserver:self]; |
[boxes release]; |
[filterPaletteTopLevelObjects release]; |
[super dealloc]; |
} |
// this allows us to set up the right pointers when changing documents |
// in particular the core image view and the effect stack |
- (void)setMainWindow:(NSWindow *)mainWindow |
{ |
NSWindowController *controller; |
FunHouseDocument *document; |
// note: if mainWindow is nil, then controller becomes nil here too |
controller = [mainWindow windowController]; |
if (controller && [controller isKindOfClass:[FunHouseWindowController class]]) |
{ |
// we have a core image fun house document window (by controller) |
// get the core image view pointer from it |
_inspectingCoreImageView = [(FunHouseWindowController *)controller coreImageView]; |
// load up the FunHouseDocument pointer |
document = (FunHouseDocument *)[controller document]; |
// and get the effect stack pointer from it |
_inspectingEffectStack = [document effectStack]; |
} |
else |
{ |
// we inspect nothing at the moment |
_inspectingCoreImageView = nil; |
_inspectingEffectStack = nil; |
} |
[self updateLayout]; |
} |
// reset the core image view (used when going to into full screen mode and back out) |
- (void)setCoreImageView:(CoreImageView *)v |
{ |
_inspectingCoreImageView = v; |
} |
// flag that we need to reconfigure ourselves after some effect stack change |
- (void)setNeedsUpdate:(BOOL)b |
{ |
needsUpdate = b; |
} |
- (void)enablePlayButton |
{ |
BOOL enabled; |
NSInteger i, count; |
NSString *type; |
CIFilter *f; |
NSDictionary *attr; |
count = [_inspectingEffectStack layerCount]; |
enabled = NO; |
for (i = 0; i < count; i++) |
{ |
type = [_inspectingEffectStack typeAtIndex:i]; |
if (![type isEqualToString:@"filter"]) |
continue; |
// first find time slider |
f = [_inspectingEffectStack filterAtIndex:i]; |
attr = [f attributes]; |
if ([attr objectForKey:@"inputTime"] != nil) |
{ |
enabled = YES; |
break; |
} |
} |
[playButton setEnabled:enabled]; |
} |
// when a window loads from the nib file, we set up the core image view pointer and effect stack pointers |
// and set up notifications |
- (void)windowDidLoad |
{ |
[super windowDidLoad]; |
[self setMainWindow:[NSApp mainWindow]]; |
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mainWindowChanged:) name:NSWindowDidBecomeMainNotification object:nil]; |
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mainWindowResigned:) name:NSWindowDidResignMainNotification object:nil]; |
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(windowDidUpdate:) name:NSWindowDidUpdateNotification object:nil]; |
[[NSColorPanel sharedColorPanel] setShowsAlpha:YES]; |
} |
// when window changes, update the pointers |
- (void)mainWindowChanged:(NSNotification *)notification |
{ |
[self setMainWindow:[notification object]]; |
} |
// dissociate us when the window is gone. |
- (void)mainWindowResigned:(NSNotification *)notification |
{ |
[self setMainWindow:nil]; |
} |
// when we see an update, check for the flag that tells us to reconfigure our effect stack inspection |
- (void)windowDidUpdate:(NSNotification *)notification |
{ |
NSInteger i, count; |
CIFilter *filter; |
NSWindow *w; |
NSString *type, *string; |
CIImage *image; |
w = (NSWindow *)[notification object]; |
if (w != [self window]) |
return; |
if (needsUpdate) |
{ |
// we need an update |
needsUpdate = NO; |
// remove tthe old boxes from the UI |
count = [boxes count]; |
for (i = 0; i < count; i++) |
{ |
[[boxes objectAtIndex:i] removeFromSuperview]; |
} |
// and clear out the boxes array |
[boxes removeAllObjects]; |
// now, if required, automatically generate the effect stack UI into separate boxes for each layer |
if (_inspectingEffectStack != nil) |
{ |
// create all boxes shown in the effect stack inspector from scratch, and place them into an array for layout purposes |
count = [_inspectingEffectStack layerCount]; |
for (i = 0; i < count; i++) |
{ |
type = [_inspectingEffectStack typeAtIndex:i]; |
if ([type isEqualToString:@"filter"]) |
{ |
filter = [_inspectingEffectStack filterAtIndex:i]; |
[boxes addObject:[[self newUIForFilter:filter index:i] autorelease]]; |
} |
else if ([type isEqualToString:@"image"]) |
{ |
image = [_inspectingEffectStack imageAtIndex:i]; |
[boxes addObject:[[self newUIForImage:image filename:[_inspectingEffectStack filenameAtIndex:i] index:i] autorelease]]; |
} |
else if ([type isEqualToString:@"text"]) |
{ |
string = [_inspectingEffectStack stringAtIndex:i]; |
[boxes addObject:[[self newUIForText:string index:i] autorelease]]; |
} |
} |
} |
// now lay it out |
[self layoutInspector]; |
} |
} |
// this method brings up the "image units palette" (we call it the filter palette) - and it also has buttons for images and text layers |
- (NSDictionary *)collectFilterImageOrText |
{ |
NSInteger i; |
CIImage *im; |
NSURL *url; |
NSOpenPanel *op; |
// when running the filter palette, if a filter is chosen (as opposed to an image or text) then filterClassname returns the |
// class name of the chosen filter |
[filterClassname release]; |
filterClassname = nil; |
// load the nib for the filter palette |
NSArray *topLevelObjects; |
[[NSBundle mainBundle] loadNibNamed:@"FilterPalette" owner:self topLevelObjects:&topLevelObjects]; |
// keep the top level objects in the filterPaletteTopLevelObjects array |
for (i=0; i<[topLevelObjects count]; i++) { |
if (![filterPaletteTopLevelObjects containsObject:[topLevelObjects objectAtIndex:i]]) |
[filterPaletteTopLevelObjects addObject:[topLevelObjects objectAtIndex:i]]; |
} |
// set up the categories data structure, that enumerates all filters for use by the filter palette |
if (categories == nil) |
{ |
categories = [[NSMutableDictionary alloc] init]; |
[self _loadFilterListIntoInspector]; |
} |
else |
[filterTableView reloadData]; |
// set up the usual target-action stuff for the filter palette |
[filterTableView setTarget:self]; |
[filterTableView setDoubleAction:@selector(tableViewDoubleClick:)]; |
[filterOKButton setEnabled:NO]; |
// re-establish the current position in the filters palette |
[categoryTableView selectRowIndexes:[NSIndexSet indexSetWithIndex:currentCategory] byExtendingSelection:NO]; |
[filterTableView selectRowIndexes:[NSIndexSet indexSetWithIndex:currentFilterRow] byExtendingSelection:NO]; |
// run the modal filter palette now |
i = [NSApp runModalForWindow:filterPalette]; |
[filterPalette close]; |
if (i == 100) |
// Apply |
// create the filter layer dictionary |
return [NSDictionary dictionaryWithObjectsAndKeys:@"filter", @"type", [CIFilter filterWithName:filterClassname], @"filter", nil]; |
else if (i == 101) |
// Cancel |
return nil; |
else if (i == 102) |
{ |
// Image |
// use the open panel to open an image |
op = [NSOpenPanel openPanel]; |
[op setAllowsMultipleSelection:NO]; |
[op setCanChooseDirectories:NO]; |
[op setResolvesAliases:YES]; |
[op setCanChooseFiles:YES]; |
// run the open panel with the allowed types |
[op setAllowedFileTypes:[NSArray arrayWithObjects:@"jpg", @"jpeg", @"tif", @"tiff", @"png", @"crw", @"cr2", @"raf", @"mrw", @"nef", @"srf", @"exr", nil]]; |
NSInteger j = [op runModal]; |
if (j == NSOKButton) |
{ |
// get image from open panel |
url = [[op URLs] objectAtIndex:0]; |
im = [[[CIImage alloc] initWithContentsOfURL:url] autorelease]; |
// create the image layer dictionary |
return [NSDictionary dictionaryWithObjectsAndKeys:@"image", @"type", im, @"image", [[url path] lastPathComponent], @"filename", [url path], @"imageFilePath", nil]; |
} |
else if (j == NSCancelButton) |
return nil; |
} |
else if (i == 103) |
// Text |
// create the text layer dictionary |
return [NSDictionary dictionaryWithObjectsAndKeys:@"text", @"type", @"text", @"string", [NSNumber numberWithDouble:10.0], @"scale", nil]; |
return nil; |
} |
// get the currently associated document |
- (FunHouseDocument *)doc |
{ |
return (FunHouseDocument *)[[NSDocumentController sharedDocumentController] currentDocument]; |
} |
// set changes (dirty the document) |
- (void)setChanges |
{ |
[[self doc] updateChangeCount:NSChangeDone]; |
} |
// call to directly update (and re-layout) the configuration of the effect stack inspector |
- (void)updateLayout |
{ |
needsUpdate = YES; |
[[self window] update]; |
} |
// this is the glue code you call to insert a filter layer into the effect stack. this handles save for undo, etc. |
- (void)insertFilter:(CIFilter *)f atIndex:(NSNumber *)index |
{ |
// actually insert the filter layer into the effect stack |
[_inspectingEffectStack insertFilterLayer:f atIndex:[index integerValue]]; |
// set filter attributes to their defaults |
[[_inspectingEffectStack filterAtIndex:[index integerValue]] setDefaults]; |
// set any automatic defaults we need (generally the odd image parameter) |
[self setAutomaticDefaults:[_inspectingEffectStack filterAtIndex:[index integerValue]] atIndex:[index integerValue]]; |
// do "save for undo" |
[[[[self doc] undoManager] prepareWithInvocationTarget:self] removeFilterImageOrTextAtIndex:index]; |
[[[self doc] undoManager] setActionName:[NSString stringWithFormat:@"Filter %@", [CIFilter localizedNameForFilterName:NSStringFromClass([f class])], nil]]; |
// dirty the documdent |
[self setChanges]; |
// redo the effect stack inspector's layout after the change |
[self updateLayout]; |
// finally, let core image render the view |
[_inspectingCoreImageView setNeedsDisplay:YES]; |
} |
// this is the high-level glue code you call to insert an image layer into the effect stack. this handles save for undo, etc. |
- (void)insertImage:(CIImage *)image withFilename:(NSString *)filename andImageFilePath:(NSString *)path atIndex:(NSNumber *)index |
{ |
// actually insert the image layer into the effect stack |
[_inspectingEffectStack insertImageLayer:image withFilename:filename atIndex:[index integerValue]]; |
[_inspectingEffectStack setImageLayer:[index integerValue] imageFilePath:path]; |
// do "save for undo" |
[[[[self doc] undoManager] prepareWithInvocationTarget:self] removeFilterImageOrTextAtIndex:index]; |
[[[self doc] undoManager] setActionName:[NSString stringWithFormat:@"Image %@", [filename lastPathComponent], nil]]; |
// dirty the documdent |
[self setChanges]; |
// redo the effect stack inspector's layout after the change |
[self updateLayout]; |
// finally, let core image render the view |
[_inspectingCoreImageView setNeedsDisplay:YES]; |
} |
// this is the high-level glue code you call to insert a text layer into the effect stack. this handles save for undo, etc. |
- (void)insertString:(NSString *)string withImage:(CIImage *)image atIndex:(NSNumber *)index |
{ |
// actually insert the text layer into the effect stack |
[_inspectingEffectStack insertTextLayer:string withImage:image atIndex:[index integerValue]]; |
// do "save for undo" |
[[[[self doc] undoManager] prepareWithInvocationTarget:self] removeFilterImageOrTextAtIndex:index]; |
[[[self doc] undoManager] setActionName:@"Text"]; |
// dirty the documdent |
[self setChanges]; |
// redo the effect stack inspector's layout after the change |
[self updateLayout]; |
// finally, let core image render the view |
[_inspectingCoreImageView setNeedsDisplay:YES]; |
} |
// this is the high-level glue code you call to remove a layer (of any kind) from the effect stack. this handles save for undo, etc. |
- (void)removeFilterImageOrTextAtIndex:(NSNumber *)index |
{ |
NSString *type = nil; |
CIFilter *filter = nil; |
CIImage *image = nil; |
NSString *filename=nil, *string=nil, *path=nil; |
// first get handles to parameters we want to retain for "save for undo" |
type = [_inspectingEffectStack typeAtIndex:[index integerValue]]; |
if ([type isEqualToString:@"filter"]) |
filter = [[_inspectingEffectStack filterAtIndex:[index integerValue]] retain]; |
else if ([type isEqualToString:@"image"]) |
{ |
image = [[_inspectingEffectStack imageAtIndex:[index integerValue]] retain]; |
filename = [[_inspectingEffectStack filenameAtIndex:[index integerValue]] retain]; |
path = [_inspectingEffectStack imageFilePathAtIndex:[index integerValue]]; |
} |
else if ([type isEqualToString:@"text"]) |
{ |
image = [[_inspectingEffectStack imageAtIndex:[index integerValue]] retain]; |
string = [[_inspectingEffectStack stringAtIndex:[index integerValue]] retain]; |
} |
// actually remove the layer from the effect stack here |
[_inspectingEffectStack removeLayerAtIndex:[index integerValue]]; |
// do "save for undo" |
if ([type isEqualToString:@"filter"]) |
{ |
[[[[self doc] undoManager] prepareWithInvocationTarget:self] insertFilter:filter atIndex:index]; |
[[[self doc] undoManager] setActionName:[NSString stringWithFormat:@"Filter %@", [CIFilter localizedNameForFilterName:NSStringFromClass([filter class])], nil]]; |
} |
else if ([type isEqualToString:@"image"]) |
{ |
[[[[self doc] undoManager] prepareWithInvocationTarget:self] insertImage:image withFilename:filename |
andImageFilePath:path atIndex:index]; |
[[[self doc] undoManager] setActionName:[NSString stringWithFormat:@"Image %@", [filename lastPathComponent], nil]]; |
} |
else if ([type isEqualToString:@"string"]) |
{ |
[[[[self doc] undoManager] prepareWithInvocationTarget:self] insertString:string withImage:image atIndex:index]; |
[[[self doc] undoManager] setActionName:@"Text"]; |
} |
if (filter) |
[filter release]; |
if (image) |
[image release]; |
if (filename) |
[filename release]; |
if (string) |
[string release]; |
// dirty the documdent |
[self setChanges]; |
// redo the effect stack inspector's layout after the change |
[self updateLayout]; |
// finally, let core image render the view |
[_inspectingCoreImageView setNeedsDisplay:YES]; |
} |
// the "global" plus button inserts a layer before the first layer |
- (IBAction)topPlusButtonAction:(id)sender |
{ |
NSDictionary *d; |
d = [self collectFilterImageOrText]; |
if (d == nil) |
return; |
if ([[d valueForKey:@"type"] isEqualToString:@"filter"]) |
[self insertFilter:[d valueForKey:@"filter"] atIndex:[NSNumber numberWithInteger:0]]; |
else if ([[d valueForKey:@"type"] isEqualToString:@"image"]) { |
[self insertImage:[d valueForKey:@"image"] withFilename:[d valueForKey:@"filename"] |
andImageFilePath:[d valueForKey:@"imageFilePath"] atIndex:[NSNumber numberWithInteger:0]]; |
} |
else if ([[d valueForKey:@"type"] isEqualToString:@"text"]) |
[self insertString:[d valueForKey:@"string"] withImage:[d valueForKey:@"image"] atIndex:[NSNumber numberWithInteger:0]]; |
[self enablePlayButton]; |
} |
// this handles a change to each layer's "enable" check box |
- (IBAction)enableCheckBoxAction:(id)sender |
{ |
[_inspectingEffectStack setLayer:[sender tag] enabled:([sender state] == NSOnState)?YES:NO]; |
[self setChanges]; |
[_inspectingCoreImageView setNeedsDisplay:YES]; |
} |
// a layer's plus button inserts another new layer after this one |
- (IBAction)plusButtonAction:(id)sender |
{ |
NSDictionary *d; |
NSInteger index; |
d = [self collectFilterImageOrText]; |
if (d == nil) |
return; |
index = [sender tag] + 1; |
if ([[d valueForKey:@"type"] isEqualToString:@"filter"]) |
[self insertFilter:[d valueForKey:@"filter"] atIndex:[NSNumber numberWithInteger:index]]; |
else if ([[d valueForKey:@"type"] isEqualToString:@"image"]) |
[self insertImage:[d valueForKey:@"image"] withFilename:[d valueForKey:@"filename"] |
andImageFilePath:[d valueForKey:@"imageFilePath"] atIndex:[NSNumber numberWithInteger:index]]; |
else if ([[d valueForKey:@"type"] isEqualToString:@"text"]) |
[self insertString:[d valueForKey:@"string"] withImage:[d valueForKey:@"image"] atIndex:[NSNumber numberWithInteger:index]]; |
[self enablePlayButton]; |
} |
// for a new filter, set up the odd image parameter |
- (void)setAutomaticDefaults:(CIFilter *)f atIndex:(NSInteger)index |
{ |
if ([NSStringFromClass([f class]) isEqualToString:@"CIGlassDistortion"]) |
{ |
// glass distortion gets a default texture file |
[f setValue:[NSApp defaultTexture] forKey:@"inputTexture"]; |
[_inspectingEffectStack setFilterLayer:index imageFilePathValue:[NSApp defaultTexturePath] forKey:@"inputTexture"]; |
} |
else if ([NSStringFromClass([f class]) isEqualToString:@"CIRippleTransition"]) |
{ |
// ripple gets a material map for shading the ripple that has a transparent alpha except specifically for the shines and darkenings |
[f setValue:[NSApp defaultAlphaEMap] forKey:@"inputShadingImage"]; |
[_inspectingEffectStack setFilterLayer:index imageFilePathValue:[NSApp defaultAlphaEMapPath] forKey:@"inputShadingImage"]; |
} |
else if ([NSStringFromClass([f class]) isEqualToString:@"CIPageCurlTransition"]) |
{ |
// we set up a good page curl default material map (like that for the ripple transition) |
[f setValue:[NSApp defaultAlphaEMap] forKey:@"inputShadingImage"]; |
[_inspectingEffectStack setFilterLayer:index imageFilePathValue:[NSApp defaultAlphaEMapPath] forKey:@"inputShadingImage"]; |
// the angle chosen shows off the alpha material map's shine on the leading curl |
[f setValue:[NSNumber numberWithDouble:-M_PI*0.25] forKey:@"inputAngle"]; |
} |
else if ([NSStringFromClass([f class]) isEqualToString:@"CIShadedMaterial"]) |
{ |
// shaded material gets an opaque material map that shows off surfaces well |
[f setValue:[NSApp defaultShadingEMap] forKey:@"inputShadingImage"]; |
[_inspectingEffectStack setFilterLayer:index imageFilePathValue:[NSApp defaultShadingEMapPath] forKey:@"inputShadingImage"]; |
} |
else if ([NSStringFromClass([f class]) isEqualToString:@"CIColorMap"]) |
{ |
// color map gets a gradient image that's a color spectrum |
[f setValue:[NSApp defaultRamp] forKey:@"inputGradientImage"]; |
[_inspectingEffectStack setFilterLayer:index imageFilePathValue:[NSApp defaultRampPath] forKey:@"inputGradientImage"]; |
} |
else if ([NSStringFromClass([f class]) isEqualToString:@"CIDisintegrateWithMaskTransition"]) |
{ |
// disintegrate with mask transition gets a mask that has a growing star |
[f setValue:[NSApp defaultMask] forKey:@"inputMaskImage"]; |
[_inspectingEffectStack setFilterLayer:index imageFilePathValue:[NSApp defaultMaskPath] forKey:@"inputMaskImage"]; |
} |
else if ([NSStringFromClass([f class]) isEqualToString:@"CICircularWrap"]) |
{ |
// circular wrap needs to be aware of the size of the screen to put its data in the right place |
NSRect bounds = [_inspectingCoreImageView bounds]; |
CGFloat cx = bounds.origin.x + 0.5 * bounds.size.width; |
CGFloat cy = bounds.origin.y + 0.5 * bounds.size.height; |
[f setValue:[CIVector vectorWithX:cx Y:cy] forKey:@"inputCenter"]; |
} |
} |
// a layer's mins button removes the layer |
- (IBAction)minusButtonAction:(id)sender |
{ |
[self removeFilterImageOrTextAtIndex:[NSNumber numberWithInteger:[sender tag]]]; |
[self enablePlayButton]; |
} |
// the reset button removes all layers from the effect stack |
- (IBAction)resetButtonAction:(id)sender |
{ |
NSInteger i, count; |
// kill off all layers from the effect stack |
count = [_inspectingEffectStack layerCount]; |
if (count == 0) |
return; |
// note: done using glue primitives so it will be an undoable operation |
if (![[_inspectingEffectStack typeAtIndex:0] isEqualToString:@"image"]) |
{ |
for (i = count - 1; i >= 0; i--) |
[self removeFilterImageOrTextAtIndex:[NSNumber numberWithInteger:i]]; |
} |
else |
{ |
for (i = count - 1; i > 0; i--) // note: spare the image at the start |
[self removeFilterImageOrTextAtIndex:[NSNumber numberWithInteger:i]]; |
} |
// dirty the document |
[self setChanges]; |
// update the configuration of the effect stack inspector |
[self updateLayout]; |
// let core image recompute the view |
[_inspectingCoreImageView setNeedsDisplay:YES]; |
[self enablePlayButton]; |
} |
// stop the transition timer |
- (void)stopTimer |
{ |
if (timer) |
{ |
[timer invalidate]; |
[timer release]; |
timer = nil; |
} |
} |
// start the transition timer |
- (void)startTimer |
{ |
if (!timer) |
{ |
timer = [NSTimer scheduledTimerWithTimeInterval:1.0 / 30.0 target:self selector:@selector(autoTimer:) userInfo:nil repeats:YES]; |
[timer retain]; |
} |
} |
// called by the transition timer every 1/30 second |
// this animates the transitions in sequence - one after another |
- (void)autoTimer:(id)sender |
{ |
NSInteger count, i, transitionIndex; |
CIFilter *f; |
NSString *type; |
NSDictionary *attr; |
double now; |
CGFloat transitionValue, value, lastTimeValue=0.0; |
now = [NSDate timeIntervalSinceReferenceDate]; |
// compute where the global time index within the state of the "n" transitions that are animating |
transitionValue = (now - transitionStartTime) / transitionDuration; |
if (transitionValue < 0.0) |
{ |
[self stopTimer]; |
return; |
} |
// set all times now |
if (transitionValue >= 0.0) |
{ |
// assign an index to each transition |
transitionIndex = 0; |
count = [_inspectingEffectStack layerCount]; |
for (i = 0; i < count; i++) |
{ |
type = [_inspectingEffectStack typeAtIndex:i]; |
if (![type isEqualToString:@"filter"]) |
continue; |
// first find time slider |
f = [_inspectingEffectStack filterAtIndex:i]; |
attr = [f attributes]; |
if ([attr objectForKey:@"inputTime"]) |
{ |
// for this transition decide where it is within its time sequence |
// by subtracting the transition index from the global time index |
value = transitionValue - (CGFloat)transitionIndex; |
// clamp to the time sequence of the transition |
if (value <= 0.0) |
value = 0.0; |
else if (value > 1.0) |
value = 1.0; |
lastTimeValue = value; |
// set the inputTime value |
[f setValue:[NSNumber numberWithDouble:value] forKey:@"inputTime"]; |
// increment the transition index |
transitionIndex++; |
} |
} |
} |
// let core image recompute the view |
[_inspectingCoreImageView setNeedsDisplay:YES]; // force a redisplay |
// terminate the animation if we've animated all transitions to their completion point |
if (now >= transitionEndTime && lastTimeValue == 1.0) |
{ |
// when all transitions are done, update the sliders in the effect stack inspector |
[self updateLayout]; |
// and turn off the timer |
[self stopTimer]; |
return; |
} |
} |
// handle the play button - play all transitions |
- (void)playButtonAction:sender |
{ |
NSInteger i, count, nTransitions; |
NSString *type; |
CIFilter *f; |
NSDictionary *attr, *d; |
count = [_inspectingEffectStack layerCount]; |
// first determine the number of transitions |
nTransitions = 0; |
for (i = 0; i < count; i++) |
{ |
type = [_inspectingEffectStack typeAtIndex:i]; |
// find only filter layers |
if (![type isEqualToString:@"filter"]) |
continue; |
// first find time slider |
f = [_inspectingEffectStack filterAtIndex:i]; |
attr = [f attributes]; |
// basically anything with an "inputTime" is a transition by definition |
d = [attr objectForKey:@"inputTime"]; |
if (d == nil) |
continue; |
// we have a transition |
nTransitions++; |
} |
if (nTransitions == 0) |
return; |
// set up the information governing the global time index over all transitions |
transitionStartTime = [NSDate timeIntervalSinceReferenceDate]; |
transitionDuration = 1.5; |
transitionEndTime = transitionStartTime + transitionDuration * (CGFloat)nTransitions; |
// start the timer now |
[self startTimer]; |
// set all inputTime parameters to 0.0 |
for (i = 0; i < count; i++) |
{ |
type = [_inspectingEffectStack typeAtIndex:i]; |
// find only filters |
if (![type isEqualToString:@"filter"]) |
continue; |
// first find time slider |
f = [_inspectingEffectStack filterAtIndex:i]; |
attr = [f attributes]; |
d = [attr objectForKey:@"inputTime"]; |
if (d == nil) |
continue; |
// set the value to zero |
[f setValue:[NSNumber numberWithDouble:0.0] forKey:@"inputTime"]; |
} |
// let core image recompute the view |
[_inspectingCoreImageView setNeedsDisplay:YES]; // force a redisplay |
} |
// this must be in synch with EffectStack.nib |
#define inspectorTopY 36 |
// lay out the effect stack inspector - this takes the NSBox'es in the boxes array and places them |
// as subviews to the our owned window's content view |
- (void)layoutInspector |
{ |
// decide how inspector is to be sized and layed out |
// boxes are all internally sized properly at this point |
NSInteger i, count, inspectorheight, fvtop; |
FilterView *fv; |
// first estimate the size of the effect stack inspector (with the boxes placed one after another vertically) |
count = [boxes count]; |
inspectorheight = inspectorTopY; |
for (i = 0; i < count; i++) |
{ |
fv = [boxes objectAtIndex:i]; |
CGFloat height = [fv bounds].size.height; |
// add the height of the box plus some spacing |
inspectorheight += height + 6; |
} |
// resize the effect stack inspector now |
NSRect frm = [[self window] frame]; |
CGFloat delta = inspectorheight + [[self window] frame].size.height |
- [[[self window] contentView] frame].size.height - frm.size.height; |
frm.size.height += delta; |
frm.origin.y -= delta; |
[[self window] setFrame:frm display:YES animate:YES]; // animate the window size change |
// and move all the boxes into place |
fvtop = inspectorheight - inspectorTopY; |
for (i = 0; i < count; i++) |
{ |
fv = [boxes objectAtIndex:i]; |
frm = [fv frame]; |
frm.origin.y = fvtop - frm.size.height; |
[fv setFrame:frm]; |
fvtop -= frm.size.height + 6; |
// unhide the box |
[fv setHidden:NO]; |
} |
// finally call for a redisplay of the effect stack inspector |
[[[self window] contentView] setNeedsDisplay:YES]; |
} |
// close down the effect stack inspector: this must be done before quit so our owned window's popsition |
// can be properly saved and subsequently restored on the next launch |
- (void)closeDown |
{ |
// resize inspector now |
NSRect frm = [[self window] frame]; |
CGFloat delta = inspectorTopY + [[self window] frame].size.height |
- [[[self window] contentView] frame].size.height - frm.size.height; |
frm.size.height += delta; |
frm.origin.y -= delta; |
[[self window] setFrame:frm display:YES animate:NO]; // skip animation on quit! |
} |
// automatically generate the UI for an effect stack filter layer |
// returning an NSBox (actually FilterView is a subclass of NSBox) |
- (FilterView *)newUIForFilter:(CIFilter *)f index:(NSInteger)index |
{ |
BOOL hasBackground; |
NSDictionary *attr; |
NSArray *inputKeys; |
NSString *key, *typestring, *classstring; |
NSEnumerator *enumerator; |
NSRect frame; |
FilterView *fv; |
NSView *view; |
// create box first |
view = [[self window] contentView]; |
frame = [view bounds]; |
frame.size.width -= 12; |
frame.origin.x += 6; |
frame.size.height -= inspectorTopY; |
fv = [[FilterView alloc] initWithFrame:frame]; |
[fv setFilter:f]; |
[fv setHidden:YES]; |
[[[self window] contentView] addSubview:fv]; |
[fv setTitlePosition:NSNoTitle]; |
[fv setAutoresizingMask:NSViewWidthSizable|NSViewMinYMargin]; |
[fv setBorderType:NSGrooveBorder]; |
[fv setBoxType:NSBoxPrimary]; |
[fv setMaster:self]; |
[fv setTag:index]; |
// first compute size of box with all the controls |
[fv tryFilterHeader:f]; |
attr = [f attributes]; |
inputKeys = [f inputKeys]; |
// decide if this filter has a background image parameter (true for blend modes and Porter-Duff modes) |
hasBackground = NO; |
enumerator = [inputKeys objectEnumerator]; |
while ((key = [enumerator nextObject]) != nil) |
{ |
id parameter = [attr objectForKey:key]; |
if ([parameter isKindOfClass:[NSDictionary class]]) |
{ |
classstring = [(NSDictionary *)parameter objectForKey: kCIAttributeClass]; |
if ([classstring isEqualToString:@"CIImage"] && [key isEqualToString:@"inputBackgroundImage"]) |
hasBackground = YES; |
} |
} |
// enumerate all input parameters and reserve space for their generated UI |
enumerator = [inputKeys objectEnumerator]; |
while ((key = [enumerator nextObject]) != nil) |
{ |
id parameter = [attr objectForKey:key]; |
if ([parameter isKindOfClass:[NSDictionary class]]) |
{ |
classstring = [(NSDictionary *)parameter objectForKey:kCIAttributeClass]; |
if ([classstring isEqualToString:@"NSNumber"]) |
{ |
typestring = [(NSDictionary *)parameter objectForKey:kCIAttributeType]; |
if ([typestring isEqualToString:kCIAttributeTypeBoolean]) |
// if it's a boolean type, save space for a check box |
[fv tryCheckBoxForFilter:f key:key displayView:_inspectingCoreImageView]; |
else |
// otherwise space space for a slider |
[fv trySliderForFilter:f key:key displayView:_inspectingCoreImageView]; |
} |
else if ([classstring isEqualToString:@"CIColor"]) |
// save space for a color well |
[fv tryColorWellForFilter:f key:key displayView:_inspectingCoreImageView]; |
else if ([classstring isEqualToString:@"CIImage"]) |
{ |
// don't bother to create a UI element for the chained image |
if (hasBackground) |
{ |
// the chained image is the background image for blend modes and Porter-Duff modes |
// it is provided by what's above this layer in the effect stack |
if (![key isEqualToString:@"inputBackgroundImage"]) |
// save space for an image well |
[fv tryImageWellForFilter:f key:key displayView:_inspectingCoreImageView]; |
} |
else |
{ |
// the chained image is the input image for all other filters |
// it is provided by what's above this layer in the effect stack |
if (![key isEqualToString:@"inputImage"]) |
// save space for an image well |
[fv tryImageWellForFilter:f key:key displayView:_inspectingCoreImageView]; |
} |
} |
else if ([classstring isEqualToString:@"NSAffineTransform"]) |
// save space for transform inspection widgets |
[fv tryTransformForFilter:f key:key displayView:_inspectingCoreImageView]; |
else if ([classstring isEqualToString:@"CIVector"]) |
{ |
// check for a vector with no attributes |
typestring = [(NSDictionary *)parameter objectForKey:kCIAttributeType]; |
if (typestring == nil) |
// save space for a 4-element vector inspection widget (4 text fields) |
[fv tryVectorForFilter:f key:key displayView:_inspectingCoreImageView]; |
else if ([typestring isEqualToString:kCIAttributeTypeOffset]) |
[fv tryOffsetForFilter:f key:key displayView:_inspectingCoreImageView]; |
// note: the other CIVector parameters are handled in mouse down processing of the core image view |
} |
} |
} |
// now resize the box to hold the controls we're about to make |
[fv trimBox]; |
// now add all the controls |
[fv addFilterHeader:f tag:index enabled:[_inspectingEffectStack layerEnabled:index]]; |
attr = [f attributes]; |
inputKeys = [f inputKeys]; |
// enumerate all input parameters and generate their UI |
enumerator = [inputKeys objectEnumerator]; |
while ((key = [enumerator nextObject]) != nil) |
{ |
id parameter = [attr objectForKey:key]; |
if ([parameter isKindOfClass:[NSDictionary class]]) |
{ |
classstring = [(NSDictionary *)parameter objectForKey: kCIAttributeClass]; |
if ([classstring isEqualToString:@"NSNumber"]) |
{ |
typestring = [(NSDictionary *)parameter objectForKey: kCIAttributeType]; |
if ([typestring isEqualToString:kCIAttributeTypeBoolean]) |
// if it's a boolean type, generate a check box |
[fv addCheckBoxForFilter:f key:key displayView:_inspectingCoreImageView]; |
else |
// otherwise generate a slider |
[fv addSliderForFilter:f key:key displayView:_inspectingCoreImageView]; |
} |
else if ([classstring isEqualToString:@"CIImage"]) |
{ |
if (hasBackground) |
{ |
// the chained image is the background image for blend modes and Porter-Duff modes |
// it is provided by what's above this layer in the effect stack |
if (![key isEqualToString:@"inputBackgroundImage"]) |
// generate an image well |
[fv addImageWellForFilter:f key:key displayView:_inspectingCoreImageView]; |
} |
else |
{ |
// the chained image is the input image for all other filters |
// it is provided by what's above this layer in the effect stack |
if (![key isEqualToString:@"inputImage"]) |
// generate an image well |
[fv addImageWellForFilter:f key:key displayView:_inspectingCoreImageView]; |
} |
} |
else if ([classstring isEqualToString:@"CIColor"]) |
// generate a color well |
[fv addColorWellForFilter:f key:key displayView:_inspectingCoreImageView]; |
else if ([classstring isEqualToString:@"NSAffineTransform"]) |
// generate transform inspection widgets |
[fv addTransformForFilter:f key:key displayView:_inspectingCoreImageView]; |
else if ([classstring isEqualToString:@"CIVector"]) |
{ |
// check for a vector with no attributes |
typestring = [(NSDictionary *)parameter objectForKey:kCIAttributeType]; |
if (typestring == nil) |
// generate a 4-element vector inspection widget (4 text fields) |
[fv addVectorForFilter:f key:key displayView:_inspectingCoreImageView]; |
else if ([typestring isEqualToString:kCIAttributeTypeOffset]) |
[fv addOffsetForFilter:f key:key displayView:_inspectingCoreImageView]; |
// the rest are handled in mouse down processing |
} |
} |
} |
// retrun the box with the filter's UI |
return fv; |
} |
// automatically generate the UI for an effect stack image layer |
// returning an NSBox (actually FilterView is a subclass of NSBox) |
- (FilterView *)newUIForImage:(CIImage *)im filename:(NSString *)filename index:(NSInteger)index |
{ |
NSRect frame; |
FilterView *fv; |
NSView *view; |
// create the box first |
view = [[self window] contentView]; |
frame = [view bounds]; |
frame.size.width -= 12; |
frame.origin.x += 6; |
frame.size.height -= inspectorTopY; |
fv = [[FilterView alloc] initWithFrame:frame]; |
[fv setFilter:nil]; |
[fv setHidden:YES]; |
[[[self window] contentView] addSubview:fv]; |
[fv setTitlePosition:NSNoTitle]; |
[fv setAutoresizingMask:NSViewWidthSizable|NSViewMinYMargin]; |
[fv setBorderType:NSGrooveBorder]; |
[fv setBoxType:NSBoxPrimary]; |
[fv setMaster:self]; |
[fv setTag:index]; |
// first compute size of box with all the controls |
[fv tryImageHeader:im]; |
[fv tryImageWellForImage:im tag:index displayView:_inspectingCoreImageView]; |
// now resize the box to hold the controls we're about to make |
[fv trimBox]; |
// now add all the controls |
[fv addImageHeader:im filename:filename tag:index enabled:[_inspectingEffectStack layerEnabled:index]]; |
[fv addImageWellForImage:im tag:index displayView:_inspectingCoreImageView]; |
return fv; |
} |
// automatically generate the UI for an effect stack text layer |
// returning an NSBox (actually FilterView is a subclass of NSBox) |
- (FilterView *)newUIForText:(NSString *)string index:(NSInteger)index |
{ |
NSRect frame; |
FilterView *fv; |
NSView *view; |
// create the box first |
view = [[self window] contentView]; |
frame = [view bounds]; |
frame.size.width -= 12; |
frame.origin.x += 6; |
frame.size.height -= inspectorTopY; |
fv = [[FilterView alloc] initWithFrame:frame]; |
[fv setFilter:nil]; |
[fv setHidden:YES]; |
[[[self window] contentView] addSubview:fv]; |
[fv setTitlePosition:NSNoTitle]; |
[fv setAutoresizingMask:NSViewWidthSizable|NSViewMinYMargin]; |
[fv setBorderType:NSGrooveBorder]; |
[fv setBoxType:NSBoxPrimary]; |
[fv setMaster:self]; |
[fv setTag:index]; |
// first compute size of box with all the controls |
[fv tryTextHeader:string]; |
[fv tryTextViewForString]; |
[fv trySliderForText]; |
// now resize the box to hold the controls we're about to make |
[fv trimBox]; |
// now add all the controls |
[fv addTextHeader:string tag:index enabled:[_inspectingEffectStack layerEnabled:index]]; |
[fv addTextViewForString:[_inspectingEffectStack mutableDictionaryAtIndex:index] key:@"string" displayView:_inspectingCoreImageView]; |
[fv addSliderForText:[_inspectingEffectStack mutableDictionaryAtIndex:index] key:@"scale" lo:1.0 hi:100.0 displayView:_inspectingCoreImageView]; |
return fv; |
} |
// handle the filter palette apply button |
- (IBAction)filterOKButtonAction:(id)sender |
{ |
// signal to apply filter |
[NSApp stopModalWithCode:100]; |
} |
// handle the filter palette cancel button |
- (IBAction)filterCancelButtonAction:(id)sender |
{ |
// signal cancel |
[NSApp stopModalWithCode:101]; |
} |
// handle the filter palette image button |
- (IBAction)filterImageButtonAction:(id)sender |
{ |
// signal to get an image |
[NSApp stopModalWithCode:102]; |
} |
- (IBAction)filterTextButtonAction:(id)sender |
{ |
// signal to setup a text layer |
[NSApp stopModalWithCode:103]; |
} |
// return the category name for the category index - used by filter palette category table view |
- (NSString *)categoryNameForIndex:(NSInteger)i |
{ |
NSString *s; |
switch (i) |
{ |
case 0: |
s = [CIFilter localizedNameForCategory:kCICategoryGeometryAdjustment]; |
break; |
case 1: |
s = [CIFilter localizedNameForCategory:kCICategoryDistortionEffect]; |
break; |
case 2: |
s = [CIFilter localizedNameForCategory:kCICategoryBlur]; |
break; |
case 3: |
s = [CIFilter localizedNameForCategory:kCICategorySharpen]; |
break; |
case 4: |
s = [CIFilter localizedNameForCategory:kCICategoryColorAdjustment]; |
break; |
case 5: |
s = [CIFilter localizedNameForCategory:kCICategoryColorEffect]; |
break; |
case 6: |
s = [CIFilter localizedNameForCategory:kCICategoryStylize]; |
break; |
case 7: |
s = [CIFilter localizedNameForCategory:kCICategoryHalftoneEffect]; |
break; |
case 8: |
s = [CIFilter localizedNameForCategory:kCICategoryTileEffect]; |
break; |
case 9: |
s = [CIFilter localizedNameForCategory:kCICategoryGenerator]; |
break; |
case 10: |
s = [CIFilter localizedNameForCategory:kCICategoryGradient]; |
break; |
case 11: |
s = [CIFilter localizedNameForCategory:kCICategoryTransition]; |
break; |
case 12: |
s = [CIFilter localizedNameForCategory:kCICategoryCompositeOperation]; |
break; |
default: |
s = @""; |
break; |
} |
return s; |
} |
// return the category index for the category name - used by filter palette category table view |
- (NSInteger)indexForCategory:(NSString *)nm |
{ |
if ([nm isEqualToString:[CIFilter localizedNameForCategory:kCICategoryGeometryAdjustment]]) |
return 0; |
if ([nm isEqualToString:[CIFilter localizedNameForCategory:kCICategoryDistortionEffect]]) |
return 1; |
if ([nm isEqualToString:[CIFilter localizedNameForCategory:kCICategoryBlur]]) |
return 2; |
if ([nm isEqualToString:[CIFilter localizedNameForCategory:kCICategorySharpen]]) |
return 3; |
if ([nm isEqualToString:[CIFilter localizedNameForCategory:kCICategoryColorAdjustment]]) |
return 4; |
if ([nm isEqualToString:[CIFilter localizedNameForCategory:kCICategoryColorEffect]]) |
return 5; |
if ([nm isEqualToString:[CIFilter localizedNameForCategory:kCICategoryStylize]]) |
return 6; |
if ([nm isEqualToString:[CIFilter localizedNameForCategory:kCICategoryHalftoneEffect]]) |
return 7; |
if ([nm isEqualToString:[CIFilter localizedNameForCategory:kCICategoryTileEffect]]) |
return 8; |
if ([nm isEqualToString:[CIFilter localizedNameForCategory:kCICategoryGenerator]]) |
return 9; |
if ([nm isEqualToString:[CIFilter localizedNameForCategory:kCICategoryGradient]]) |
return 10; |
if ([nm isEqualToString:[CIFilter localizedNameForCategory:kCICategoryTransition]]) |
return 11; |
if ([nm isEqualToString:[CIFilter localizedNameForCategory:kCICategoryCompositeOperation]]) |
return 12; |
return -1; |
} |
// build a dictionary of approved filters in a given category for the filter inspector |
- (NSMutableDictionary *)buildFilterDictionary:(NSArray *)names |
{ |
BOOL inspectable; |
NSDictionary *attr, *parameter; |
NSArray *inputKeys; |
NSEnumerator *enumerator; |
NSMutableDictionary *td, *catfilters; |
NSString *classname, *classstring, *key, *typestring; |
CIFilter *filter; |
NSInteger i; |
catfilters = [NSMutableDictionary dictionary]; |
for (i = 0; i < [names count]; i++) |
{ |
// load the filter class name |
classname = [names objectAtIndex:i]; |
// create an instance of the filter |
filter = [CIFilter filterWithName:classname]; |
if (filter != nil) |
{ |
// search the filter for any input parameters we can't inspect |
inspectable = YES; |
attr = [filter attributes]; |
inputKeys = [filter inputKeys]; |
// enumerate all input parameters and generate their UI |
enumerator = [inputKeys objectEnumerator]; |
while ((key = [enumerator nextObject]) != nil) |
{ |
parameter = [attr objectForKey:key]; |
classstring = [parameter objectForKey:kCIAttributeClass]; |
if ([classstring isEqualToString:@"CIImage"] |
|| [classstring isEqualToString:@"CIColor"] |
|| [classstring isEqualToString:@"NSAffineTransform"] |
|| [classstring isEqualToString:@"NSNumber"]) |
continue; // all inspectable |
else if ([classstring isEqualToString:@"CIVector"]) |
{ |
// check for a vector with no attributes |
typestring = [parameter objectForKey:kCIAttributeType]; |
if (typestring != nil |
&& ![typestring isEqualToString:kCIAttributeTypePosition] |
&& ![typestring isEqualToString:kCIAttributeTypeRectangle] |
&& ![typestring isEqualToString:kCIAttributeTypePosition3] |
&& ![typestring isEqualToString:kCIAttributeTypeOffset]) |
inspectable = NO; |
} |
else |
inspectable = NO; |
} |
if (!inspectable) |
continue; // if we can't inspect it, it's not approved and must be omitted from the list |
// create a dictionary for the filter with filter's class name |
td = [NSMutableDictionary dictionary]; |
[td setObject:classname forKey:kCIAttributeClass]; |
// set it as the value for a key which is the filter's localized name |
[catfilters setObject:td forKey:[CIFilter localizedNameForFilterName:classname]]; |
} |
else |
NSLog(@" could not create '%@' filter", classname); |
} |
return catfilters; |
} |
// build the filter list (enumerates all filters) |
- (void)_loadFilterListIntoInspector |
{ |
NSString *cat; |
NSArray *attrs; |
NSMutableArray *all; |
NSInteger i, m; |
// here's a list of all categories |
attrs = [NSArray arrayWithObjects: |
kCICategoryGeometryAdjustment, |
kCICategoryDistortionEffect, |
kCICategoryBlur, |
kCICategorySharpen, |
kCICategoryColorAdjustment, |
kCICategoryColorEffect, |
kCICategoryStylize, |
kCICategoryHalftoneEffect, |
kCICategoryTileEffect, |
kCICategoryGenerator, |
kCICategoryGradient, |
kCICategoryTransition, |
kCICategoryCompositeOperation, |
nil]; |
// call to load all plug-in image units |
[CIPlugIn loadAllPlugIns]; |
// enumerate all filters in the chosen categories |
m = [attrs count]; |
for (i = 0; i < m; i++) |
{ |
// get this category |
cat = [attrs objectAtIndex:i]; |
// make a list of all filters in this category |
all = [NSMutableArray arrayWithArray:[CIFilter filterNamesInCategory:cat]]; |
// make this category's list of approved filters |
[categories setObject:[self buildFilterDictionary:all] forKey:[CIFilter localizedNameForCategory:cat]]; |
} |
currentCategory = 0; |
currentFilterRow = 0; |
// load up the filter list into the table view |
[filterTableView reloadData]; |
} |
// table view data source methods |
- (NSInteger)numberOfRowsInTableView:(NSTableView *)tv |
{ |
NSInteger count; |
NSString *s; |
NSDictionary *dict; |
NSArray *filterNames; |
switch ([tv tag]) |
{ |
case 0: |
// category table view |
count = 13; |
break; |
case 1: |
default: |
// filter table view |
s = [self categoryNameForIndex:currentCategory]; |
// use category name to get dictionary of filter names |
dict = [categories objectForKey:s]; |
// create an array |
filterNames = [dict allKeys]; |
// return number of filters in this category |
count = [filterNames count]; |
break; |
} |
return count; |
} |
static NSInteger stringCompare(id o1, id o2, void *context) |
{ |
NSString *str1, *str2; |
str1 = o1; |
str2 = o2; |
return [str1 compare:str2]; |
} |
- (id)tableView:(NSTableView *)tv objectValueForTableColumn:(NSTableColumn *)tc row:(NSInteger)row |
{ |
NSString *s; |
NSDictionary *dict; |
NSArray *filterNames; |
NSTextFieldCell *tfc; |
switch ([tv tag]) |
{ |
case 0: |
// category table view |
s = [self categoryNameForIndex:row]; |
tfc = [tc dataCell]; |
// handle names that are too long by ellipsizing the name |
s = [ParameterView ellipsizeField:[tc width] font:[tfc font] string:s]; |
break; |
case 1: |
default: |
// filter table view |
// we need to maintain the filter names in a sorted order. |
s = [self categoryNameForIndex:currentCategory]; |
// use label (category name) to get dictionary of filter names |
dict = [categories objectForKey:s]; |
// create an array of the sorted names (this is inefficient since we don't cache the sorted array) |
filterNames = [[dict allKeys] sortedArrayUsingFunction:stringCompare context:nil]; |
// return filter name |
s = [filterNames objectAtIndex:row]; |
tfc = [tc dataCell]; |
// handle names that are too long by ellipsizing the name |
s = [ParameterView ellipsizeField:[tc width] font:[tfc font] string:s]; |
break; |
} |
return s; |
} |
// this is called when we select a filter from the list |
- (void)addEffect |
{ |
NSInteger row; |
NSTableView *tv; |
NSDictionary *dict, *td; |
NSArray *filterNames; |
// get current category item |
tv = filterTableView; |
// decide current filter name from selected row (or none selected) in the filter name list |
row = [tv selectedRow]; |
if (row == -1) |
{ |
[filterClassname release]; |
filterClassname = nil; |
[filterOKButton setEnabled:NO]; |
return; |
} |
// use label (category name) to get dictionary of filter names |
dict = [categories objectForKey:[self categoryNameForIndex:currentCategory]]; |
// create an array of all filter names for this category |
filterNames = [[dict allKeys] sortedArrayUsingFunction:stringCompare context:nil]; |
// return filter name |
td = [dict objectForKey:[filterNames objectAtIndex:row]]; |
// retain the name in filterClassname for use outside the modal |
[filterClassname release]; |
filterClassname = [[td objectForKey:kCIAttributeClass] retain]; |
// enable the apply button |
[filterOKButton setEnabled:YES]; |
} |
- (void)tableViewSelectionDidChange:(NSNotification *)aNotification |
{ |
NSInteger row; |
NSTableView *tv; |
tv = [aNotification object]; |
row = [tv selectedRow]; |
switch ([tv tag]) |
{ |
case 0: |
// category table view |
// select the category |
currentCategory = row; |
// reload the filter table based on the current category |
[filterTableView reloadData]; |
[filterTableView deselectAll:self]; |
[filterTableView noteNumberOfRowsChanged]; |
break; |
case 1: |
// filter table view |
// select a filter |
// add an effect to current effects list |
currentFilterRow = row; |
[self addEffect]; |
break; |
} |
} |
// if we see a double-click in the filter list, it's like hitting apply |
- (IBAction)tableViewDoubleClick:(id)sender |
{ |
[NSApp stopModalWithCode:100]; |
} |
// glue code for determining if a filter layer has a missing image (and should be drawn red to indicate as such) |
- (BOOL)effectStackFilterHasMissingImage:(CIFilter *)f |
{ |
return [_inspectingEffectStack filterHasMissingImage:f]; |
} |
// glue code to set up an image layer |
- (void)setLayer:(NSInteger)index image:(CIImage *)im andFilename:(NSString *)filename |
{ |
[_inspectingEffectStack setImageLayer:index image:im andFilename:filename]; |
} |
- (void)registerImageLayer:(NSInteger)index imageFilePath:(NSString *)path |
{ |
[_inspectingEffectStack setImageLayer:index imageFilePath:path]; |
} |
- (void)registerFilterLayer:(CIFilter *)filter key:(NSString *)key imageFilePath:(NSString *)path |
{ |
NSInteger i, count; |
NSString *type; |
count = [_inspectingEffectStack layerCount]; |
for (i = 0; i < count; i++) |
{ |
type = [_inspectingEffectStack typeAtIndex:i]; |
if (![type isEqualToString:@"filter"]) |
continue; |
if (filter == [_inspectingEffectStack filterAtIndex:i]) |
{ |
[_inspectingEffectStack setFilterLayer:i imageFilePathValue:path forKey:key]; |
break; |
} |
} |
} |
- (NSString *)imageFilePathForImageLayer:(NSInteger)index |
{ |
return [_inspectingEffectStack imageFilePathAtIndex:index]; |
} |
- (NSString *)imageFilePathForFilterLayer:(CIFilter *)filter key:(NSString *)key |
{ |
NSInteger i, count; |
NSString *type; |
count = [_inspectingEffectStack layerCount]; |
for (i = 0; i < count; i++) |
{ |
type = [_inspectingEffectStack typeAtIndex:i]; |
if (![type isEqualToString:@"filter"]) |
continue; |
if (filter == [_inspectingEffectStack filterAtIndex:i]) |
return [_inspectingEffectStack filterLayer:i imageFilePathValueForKey:key]; |
} |
return nil; |
} |
- (void)reconfigureWindow |
{ |
NSString *path; |
CIImage *image; |
CGRect extent; |
path = [_inspectingEffectStack imageFilePathAtIndex:0]; |
image = [_inspectingEffectStack imageAtIndex:0]; |
extent = [image extent]; |
[[self doc] reconfigureWindowToSize:NSMakeSize(extent.size.width, extent.size.height) andPath:path]; |
} |
@end |
@implementation EffectStackBox |
// this is a subclass of NSBox required so we can draw the interior of the box as red when there's something |
// in the box (namely an image well) that still needs filling |
#define boxInset 3.0 |
#define boxFillet 7.0 |
// control point distance from rectangle corner |
#define cpdelta (boxFillet * 0.35) |
- (void)setFilter:(CIFilter *)f |
{ |
filter = f; |
} |
- (void)setMaster:(EffectStackController *)m |
{ |
master = m; |
} |
- (void)drawRect:(NSRect)r |
{ |
NSBezierPath *path; |
NSPoint bl, br, tr, tl; |
NSRect R; |
[super drawRect:r]; |
if ([master effectStackFilterHasMissingImage:filter]) |
{ |
// overlay the box now - colorized |
[[NSColor colorWithDeviceRed:1.0 green:0.0 blue:0.0 alpha:0.15] set]; |
path = [NSBezierPath bezierPath]; |
R = NSOffsetRect(NSInsetRect([self bounds], boxInset, boxInset), 0, 1); |
bl = R.origin; |
br = NSMakePoint(R.origin.x + R.size.width, R.origin.y); |
tr = NSMakePoint(R.origin.x + R.size.width, R.origin.y + R.size.height); |
tl = NSMakePoint(R.origin.x, R.origin.y + R.size.height); |
[path moveToPoint:NSMakePoint(bl.x + boxFillet, bl.y)]; |
[path lineToPoint:NSMakePoint(br.x - boxFillet, br.y)]; |
[path curveToPoint:NSMakePoint(br.x, br.y + boxFillet) |
controlPoint1:NSMakePoint(br.x - cpdelta, br.y) |
controlPoint2:NSMakePoint(br.x, br.y + cpdelta)]; |
[path lineToPoint:NSMakePoint(tr.x, tr.y - boxFillet)]; |
[path curveToPoint:NSMakePoint(tr.x - boxFillet, tr.y) |
controlPoint1:NSMakePoint(tr.x, tr.y - cpdelta) |
controlPoint2:NSMakePoint(tr.x - cpdelta, tr.y)]; |
[path lineToPoint:NSMakePoint(tl.x + boxFillet, tl.y)]; |
[path curveToPoint:NSMakePoint(tl.x, tl.y - boxFillet) |
controlPoint1:NSMakePoint(tl.x + cpdelta, tl.y) |
controlPoint2:NSMakePoint(tl.x, tl.y - cpdelta)]; |
[path lineToPoint:NSMakePoint(bl.x, bl.y + boxFillet)]; |
[path curveToPoint:NSMakePoint(bl.x + boxFillet, bl.y) |
controlPoint1:NSMakePoint(bl.x, bl.y + cpdelta) |
controlPoint2:NSMakePoint(bl.x + cpdelta, bl.y)]; |
[path closePath]; |
[path fill]; |
} |
} |
@end |
Copyright © 2014 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2014-05-07