// Copyright (c) 2009 Yanagi Asakura // // This software is provided 'as-is', without any express or implied // warranty. In no event will the authors be held liable for any damages // arising from the use of this software. // // Permission is granted to anyone to use this software for any purpose, // including commercial applications, and to alter it and redistribute it // freely, subject to the following restrictions: // // 1. The origin of this software must not be misrepresented; you must not // claim that you wrote the original software. If you use this software // in a product, an acknowledgment in the product documentation would be // appreciated but is not required. // // 2. Altered source versions must be plainly marked as such, and must not be // misrepresented as being the original software. // // 3. This notice may not be removed or altered from any source // distribution. // // ElisController.m // Elis Colors // // Created by 柳 on 09/09/12. // Copyright 2009 __MyCompanyName__. All rights reserved. // #import "ElisController.h" static float convertQTTimeToSecond(QTTime t) { return (float)t.timeValue/t.timeScale; } @implementation ElisController - (void)awakeFromNib { layers = [[NSMutableArray alloc] init]; _animationLayerFactory = [[ElisAnimationLayerFactory alloc] init]; playing = NO; recording = NO; hipTime = 0.0; savePath = nil; ProjectMovieSize = CGRectMake(0, 0, 640, 480); // カスタムフィルタを初期化。 [ElisCustomFilter class]; NSLog(@"Building effects ..."); // エフェクトメニューを構築。 [self buildEffectMenu]; timeLineXShift = 0; #ifdef __SNOW_LEOPARD_GCD__ diq = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0); #endif } - (CALayer*)createNewLayer:(NSString*)path { ElisLayer* layer; CALayer* alayer; ElisMedia* m; layer = [[ElisLayer alloc] init]; NSWorkspace* sharedWorkspace = [NSWorkspace sharedWorkspace]; // 読めるメディアかチェック if([sharedWorkspace type:[sharedWorkspace typeOfFile:path error:nil] conformsToType:@"public.image"]) { m = [[ElisMedia alloc] initWithImageFile:path]; alayer = [_animationLayerFactory createNewAnimationLayer:convertQTTimeToSecond([m duration]) name:[path lastPathComponent] type:@"image"]; } else if([sharedWorkspace type:[sharedWorkspace typeOfFile:path error:nil] conformsToType:@"public.audio"]) { m = [[ElisMedia alloc] initWithSoundFile:path]; alayer = [_animationLayerFactory createNewAnimationLayer:convertQTTimeToSecond([m duration]) name:[path lastPathComponent] type:@"sound"]; } else if([sharedWorkspace type:[sharedWorkspace typeOfFile:path error:nil] conformsToType:@"public.movie"] || [sharedWorkspace type:[sharedWorkspace typeOfFile:path error:nil] conformsToType:@"com.apple.quartz-composer-composition"]) { m = [[ElisMedia alloc] initWithMovieFile:path]; alayer = [_animationLayerFactory createNewAnimationLayer:convertQTTimeToSecond([m duration]) name:[path lastPathComponent] type:@"movie"]; } else { NSLog(@"error: cannot open %@", path); return nil; } layer.media = m; // [alayer setValue:layer forKey:@"ElisLayer"]; [layer setAlayer:alayer]; [layers addObject:layer]; return alayer; } // 絶対時間qttimeと関係があるレイヤーをまとめて返す。 - (void)getFrameForTime:(QTTime)qttime result:(NSMutableArray*)layerSet { int size = [layers count]; // 再生時間オーバー。停止。 if(convertQTTimeToSecond(qttime) >= hipTime){ [_mainView stopDisplayLink]; [self stop:qttime]; return; } globalCurrentTime = qttime; [self moveSliderTo:qttime]; [_tableController reload]; // GCD使ってみたら画面がちらつく。どういうことなの...? #ifdef __SNOW_LEOPARD_GCD__ dispatch_apply(size, diq, ^(size_t i) { ElisLayer* l = [layers objectAtIndex:i]; if([l isInclude:qttime]){ if(playing) [l play]; [layerSet addObject:l]; }else{ if(playing) [l stop]; } }); #else // GCDなりOpenMPなりで並列化すること。 int i; ElisLayer* l; for(i = 0; i < size; i++){ l = [layers objectAtIndex:i]; if([l isInclude:qttime]){ if(playing) [l play]; [layerSet addObject:l]; }else{ if(playing) [l stop]; } } #endif } - (void)play:(QTTime)time { NSMutableArray* interestLayers = [[NSMutableArray alloc] init]; int i, size = [layers count]; hipTime = [self getHipTime]; for(i = 0; i < size; i++) if([[layers objectAtIndex:i] isInclude:time]) [interestLayers addObject:[layers objectAtIndex:i]]; size = [interestLayers count]; for(i = 0; i < size; i++) [(ElisLayer*)[interestLayers objectAtIndex:i] play]; playing = YES; } - (void)stop:(QTTime)time { // NSMutableArray* interestLayers = [[NSMutableArray alloc] init]; int i, size = [layers count]; // hipTime = [self getHipTime]; // // for(i = 0; i < size; i++) //// if([[layers objectAtIndex:i] isInclude:time]) // [interestLayers addObject:[layers objectAtIndex:i]]; // // size = [interestLayers count]; // // for(i = 0; i < size; i++) // [(ElisLayer*)[interestLayers objectAtIndex:i] stop]; for(i = 0; i < size; i++) [(ElisLayer*)[layers objectAtIndex:i] stop]; playing = NO; globalCurrentTime = time; _currentTime = time; } - (IBAction)startPlay:(id)sender { hipTime = [self getHipTime]; QTTime currentTime = QTMakeTime([timeSlider floatValue] * hipTime * DEFAULT_FPS, DEFAULT_FPS); [self seek:currentTime]; [self play:currentTime]; [_mainView startDisplayLink]; } - (IBAction)stopPlay:(id)sender { hipTime = [self getHipTime]; QTTime currentTime = QTMakeTime([timeSlider floatValue] * hipTime * DEFAULT_FPS, DEFAULT_FPS); [self stop:currentTime]; [_mainView stopDisplayLink]; } - (float)getHipTime { int i, size = [layers count]; float hipTimeSecond = 0.0f, candidate; for(i = 0; i < size; i++){ candidate = convertQTTimeToSecond([[layers objectAtIndex:i] mapping].time) + convertQTTimeToSecond([[layers objectAtIndex:i] mapping].duration); if(candidate > hipTimeSecond) hipTimeSecond = candidate; } return hipTimeSecond; } - (void)moveSliderTo:(QTTime)time { float now = convertQTTimeToSecond(time); [timeSlider setFloatValue:now/hipTime]; [timeCodeField setStringValue:QTStringFromTime(QTMakeTime(now * DEFAULT_FPS, DEFAULT_FPS))]; [_timeLineController movePlaybackBar:now*timeLineScale]; } - (IBAction)timeSliderChanged:(id)sender { float seconds = [sender floatValue]; QTTime currentTime = QTMakeTime(seconds * hipTime * DEFAULT_FPS, DEFAULT_FPS); [timeCodeField setStringValue:QTStringFromTime(currentTime)]; _currentTime = currentTime; globalCurrentTime = currentTime; [self refresh]; } - (void)seek:(QTTime)time { int i, size = [layers count]; [_mainView seek:time]; for(i = 0; i < size; i++) [[layers objectAtIndex:i] seek:time]; } - (void)refresh { if(playing) return; hipTime = [self getHipTime]; [_mainView getFrameForQTTime:_currentTime]; } - (void)getSoundTrack:(NSMutableArray*)soundTrack { int i, size = [layers count]; for(i = 0; i < size; i++) [[layers objectAtIndex:i] getSoundTrack:soundTrack]; } - (IBAction)deleteSelectLayer:(id)sender { CALayer* selected = [_timeLineController getSelectLayer]; ElisLayer* layer = [selected valueForKey:@"ElisLayer"]; [layers removeObject:layer]; [selected removeFromSuperlayer]; [_timeLineController removeSelectLayer]; [_tableController createPropertyTable:nil]; [_tableController reload]; [self refresh]; } - (IBAction)recordingStateChanged:(id)sender { recording = !recording; } - (IBAction)removeAllKeyFrame:(id)sender { [_tableController removeAllKeyframe]; [self refresh]; } - (IBAction)removeEffect:(id)sender { [_tableController removeEffect]; [self refresh]; } - (IBAction)writeToFile:(id)sender { NSSavePanel* sp = [NSSavePanel savePanel]; [sp setRequiredFileType:@"mov"]; [sp beginSheetForDirectory:nil file:nil modalForWindow:_mainWindow modalDelegate:self didEndSelector:@selector(writeMovie:returnCode:contextInfo:) contextInfo:NULL]; } - (void)writeMovie:(NSSavePanel*)sheet returnCode:(int)code contextInfo:(void*)info { NSString* path = [sheet filename]; if(code == NSCancelButton) return; [sheet close]; // [_writer write:sheet]; [NSThread detachNewThreadSelector:@selector(write:) toTarget:_writer withObject:sheet]; } // エフェクトのメニュー項目を構築 - (void)buildEffectMenu { NSArray* filterNames; CIFilter* filter; NSDictionary* attrs; filterNames = [CIFilter filterNamesInCategories:[NSArray arrayWithObjects: kCICategoryDistortionEffect, kCICategoryGeometryAdjustment, kCICategoryCompositeOperation, kCICategoryHalftoneEffect, kCICategoryColorAdjustment, kCICategoryColorEffect, kCICategoryTransition, kCICategoryTileEffect, kCICategoryGenerator, kCICategoryGradient, kCICategoryStylize, kCICategorySharpen, kCICategoryBlur, nil]]; NSString* name; NSArray* inputKeys; id elm; NSMenuItem* item = [[NSMenuItem alloc] init], *child; NSMenu* menu = [[NSMenu alloc] init]; int c = 0; [item setTitle:@"Effect"]; [menu setTitle:@"Effect"]; filterNames = [CIFilter filterNamesInCategory:kCICategoryBuiltIn]; for(name in filterNames){ filter = [CIFilter filterWithName:name]; attrs = [filter attributes]; inputKeys = [filter inputKeys]; if([inputKeys count] == 1) goto jimp; for(elm in inputKeys){ if([elm isEqualToString:@"inputImage"]) continue; if([[[attrs valueForKey:elm] valueForKey:kCIAttributeClass] isEqualToString:@"CIVector"]){ if([[[attrs valueForKey:elm] valueForKey:kCIAttributeDefault] count] == 2) goto ok; } if(!([[[attrs valueForKey:elm] valueForKey:kCIAttributeClass] isEqualToString:@"NSNumber"] || [[[attrs valueForKey:elm] valueForKey:kCIAttributeClass] isEqualToString:@"CIColor"])){ goto jimp; } } ok: child = [[[NSMenuItem alloc] init] autorelease]; [child setTitle:name]; [child setAction:@selector(effectMenuPushed:)]; [child setTarget:self]; c++; [menu addItem:child]; jimp: ; // ラベルの後ろには必ずstatementがないといけないらしい。なんで? } NSLog(@"%d effetcs usable.", c); [item setSubmenu:menu]; [menu setAutoenablesItems:NO]; [item setEnabled:YES]; [menu release]; [[NSApp mainMenu] insertItem:item atIndex:5]; [item setTarget:self]; } - (void)effectMenuPushed:(id)sender { CALayer* l; l = [_timeLineController getSelectLayer]; [[l valueForKey:@"ElisLayer"] addEffect:[sender title]]; [self refresh]; [_tableController createPropertyTable:[l valueForKey:@"ElisLayer"]]; [_tableController reload]; } - (void)saveProjectToFile:(NSSavePanel*)sheet returnCode:(int)code contextInfo:(void*)info { NSString* path = [sheet filename]; NSMutableData* data = [NSMutableData data]; NSKeyedArchiver* encoder = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data]; [encoder encodeObject:layers forKey:@"layers"]; [encoder finishEncoding]; savePath = path; [data writeToFile:path atomically:YES]; } - (void)loadProjectFromFile:(NSString*)path { NSMutableData* data = [NSMutableData dataWithContentsOfFile:path]; NSKeyedUnarchiver* decoder = [[NSKeyedUnarchiver alloc] initForReadingWithData:data]; [_timeLineController awakeFromNib]; // これはひどい。 layers = [decoder decodeObjectForKey:@"layers"]; [decoder finishDecoding]; int i, size = [layers count]; ElisLayer* l; CALayer* al; for(i = 0; i < size; i++){ l = [layers objectAtIndex:i]; al = [_animationLayerFactory createNewAnimationLayer:[l duration] name:[l getPath] type:[l getType]]; [l setLayer:al]; [_timeLineController addLayer:al]; } savePath = path; [self refresh]; } - (IBAction)openProjectSaveDialog:(id)sender { NSSavePanel* sp = [NSSavePanel savePanel]; [sp setRequiredFileType:@"elis"]; [sp beginSheetForDirectory:nil file:nil modalForWindow:_mainWindow modalDelegate:self didEndSelector:@selector(saveProjectToFile:returnCode:contextInfo:) contextInfo:NULL]; } - (IBAction)openProjectLoadDialog:(id)sender { NSOpenPanel* op = [NSOpenPanel openPanel]; int st; st = [op runModalForTypes:[NSArray arrayWithObject:@"elis"]]; if(st == NSOKButton) [self loadProjectFromFile:[op filename]]; } - (BOOL)validateMenuItem:(NSMenuItem *)menuItem { if([menuItem action] == @selector(rewriteProject:)) return savePath != nil; // if([menuItem action] == @selector(removeAllKeyFrame:)) // return [_tableController canRemoveAllKeyframe]; if([menuItem action] == @selector(removeEffect:)) return [_tableController canRemoveEffect]; if([menuItem action] == @selector(deleteSelectLayer:)) return [_timeLineController canDeleteLayer]; if([menuItem action] == @selector(undo:)) return [_undoManager canUndo]; if([menuItem action] == @selector(redo:)) return [_undoManager canRedo]; return YES; } - (IBAction)rewriteProject:(id)sender { NSMutableData* data = [NSMutableData data]; NSKeyedArchiver* encoder = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data]; [encoder encodeObject:layers forKey:@"layers"]; [encoder finishEncoding]; [data writeToFile:savePath atomically:YES]; } - (IBAction)changeToSmallWindiw:(id)sender { [_mainWindow close]; [NSBundle loadNibNamed:@"MainMenuForSmallDisplay" owner:self]; } - (IBAction)undo:(id)sender { [_undoManager popOperation]; [self refresh]; } - (IBAction)redo:(id)sender { [_undoManager redoOperation]; [self refresh]; } @end