// 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]; // Quartz Composerのデフォルト再生時間を変更。 NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults]; [defaults setInteger:60*10 forKey:@"QuartzComposerDefaultMovieDuration"]; timeLineXShift = 0; usingStampMode = NO; #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; } - (CALayer*)createNewTextLayer:(NSString*)t { // [_textLayerField selectAll:nil]; // NSAttributedString* as = [[NSAttributedString alloc] // initWithRTF:[_textLayerField RTFFromRange:[_textLayerField selectedRange]] documentAttributes:nil]; NSAttributedString* as = [_textLayerField attributedString]; ElisLayer* layer = [[ElisLayer alloc] init]; ElisMedia* m = [[ElisMedia alloc] initWithText:as]; CALayer* cal = [_animationLayerFactory createNewAnimationLayer:convertQTTimeToSecond([m duration]) name:@"text" type:@"text"]; layer.media = m; [layer setAlayer:cal]; [layers addObject:layer]; return cal; } // 絶対時間qttimeと関係があるレイヤーをまとめて返す。 - (void)getFrameForTime:(QTTime)qttime result:(NSMutableArray*)layerSet { int size = [layers count]; // 再生時間オーバー。停止。 if(convertQTTimeToSecond(qttime) >= hipTime){ [_mainView stopDisplayLink]; [self stop:qttime]; [_playstopButton setState:NSOffState]; 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); [self performSelectorOnMainThread:@selector(moveSliderOnThread:) withObject:[NSNumber numberWithFloat:now] waitUntilDone:NO]; // [timeSlider setFloatValue:now/hipTime]; // [timeCodeField setStringValue:QTStringFromTime(QTMakeTime(now * DEFAULT_FPS, DEFAULT_FPS))]; // [_timeLineController movePlaybackBar:now*timeLineScale]; } - (void)moveSliderOnThread:(NSNumber*)nowv { float now = [nowv floatValue]; [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"]; layer.media = nil; [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]; ElisWriterLegacy* writer; writer = [[ElisWriterLegacy alloc] init]; [writer setMainWindow:_mainWindow]; [writer setMainController:self]; [writer setMainView:_mainView]; // [_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"]]; [_undoManager pushOperation:[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 encodeFloat:ProjectMovieSize.size.width forKey:@"movieWidth"]; [encoder encodeFloat:ProjectMovieSize.size.height forKey:@"movieHeight"]; [encoder encodeObject:ELIS_VERSION forKey:@"version"]; [encoder finishEncoding]; savePath = path; [data writeToFile:path atomically:YES]; } - (void)loadProjectFromFile:(NSString*)path { NSMutableData* data = [NSMutableData dataWithContentsOfFile:path]; NSKeyedUnarchiver* decoder = [[NSKeyedUnarchiver alloc] initForReadingWithData:data]; float w, h; [_timeLineController awakeFromNib]; // これはひどい。 NSString* version = [decoder decodeObjectForKey:@"version"]; if([version compare:ELIS_VERSION] > 0) return; layers = [decoder decodeObjectForKey:@"layers"]; w = [decoder decodeFloatForKey:@"movieWidth"]; h = [decoder decodeFloatForKey:@"movieHeight"]; [decoder finishDecoding]; ProjectMovieSize.size.width = w; ProjectMovieSize.size.height = h; 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 printName] 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]; if([menuItem action] == @selector(changeMovieSpeed:)) return [self canChangeMovieSpeed]; if([menuItem action] == @selector(cutLayerAtCurrentTime:)) return [_timeLineController canDeleteLayer]; if([menuItem action] == @selector(effectMenuPushed:)) return [self canAddEffect]; return YES; } - (BOOL)canAddEffect { return ![[[[[_timeLineController getSelectLayer] valueForKey:@"ElisLayer"] media] type] isEqualToString:@"sound"]; } - (IBAction)rewriteProject:(id)sender { NSMutableData* data = [NSMutableData data]; NSKeyedArchiver* encoder = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data]; [encoder encodeObject:layers forKey:@"layers"]; [encoder encodeFloat:ProjectMovieSize.size.width forKey:@"movieWidth"]; [encoder encodeFloat:ProjectMovieSize.size.height forKey:@"movieHeight"]; [encoder finishEncoding]; [data writeToFile:savePath atomically:YES]; } - (IBAction)changeToSmallWindiw:(id)sender { [_mainWindow close]; [NSBundle loadNibNamed:@"MainMenuForSmallDisplay" owner:self]; } - (IBAction)undo:(id)sender { ElisLayer* l = [_undoManager popOperation]; [self refresh]; [_timeLineController updateKeyframeLayer]; [_tableController createPropertyTable:l]; [_tableController reload]; } - (IBAction)redo:(id)sender { [_undoManager redoOperation]; [self refresh]; [_timeLineController updateKeyframeLayer]; [_tableController reload]; } - (IBAction)changeMovieSize:(id)sender { ElisMovieSizeWindowController* c; c = [[ElisMovieSizeWindowController alloc] init]; [c setMainWindow:_mainWindow]; [c setMainView:_mainView]; [c run]; } - (IBAction)preference:(id)sender { ElisPreferenceController*c; c = [[ElisPreferenceController alloc] init]; [c setMainWindow:_mainWindow]; [c run]; } - (void)textDidChange:(NSNotification*)n { CALayer* l = [_timeLineController getSelectLayer]; ElisLayer* layer; if(l == nil) return; layer = [l valueForKey:@"ElisLayer"]; if([[[layer media] type] isEqualToString:@"text"]){ NSAttributedString* s = [_textLayerField attributedString]; [[layer media] setText:s]; [self refresh]; } } - (IBAction)changeMovieSpeed:(id)sender { ElisMovieSpeedController* c; c = [[ElisMovieSpeedController alloc] init]; [_undoManager pushOperation:[[_timeLineController getSelectLayer] valueForKey:@"ElisLayer"]]; [c setMainWindow:_mainWindow]; [c setLayer:[[_timeLineController getSelectLayer] valueForKey:@"ElisLayer"]]; [c run]; [self refresh]; } - (BOOL)canChangeMovieSpeed { CALayer* l; l = [_timeLineController getSelectLayer]; if(l == nil) return NO; if([[[[l valueForKey:@"ElisLayer"] media] type] isEqualToString:@"movie"]) return YES; return NO; } - (IBAction)gotoNextKeyTime:(id)sender { float currentTime = [self getHipTime] * [timeSlider floatValue]; NSMutableArray* array = [[NSMutableArray alloc] init]; ElisLayer* l; QTTime nextTime; int i, size = [layers count]; for(i = 0; i < size; i++){ [array addObject:[NSNumber numberWithFloat:0.0]]; [array addObject:[NSNumber numberWithFloat:convertQTTimeToSecond([[layers objectAtIndex:i] mapping].time)]]; [array addObject:[NSNumber numberWithFloat:convertQTTimeToSecond([[layers objectAtIndex:i] mapping].time) + convertQTTimeToSecond([[layers objectAtIndex:i] mapping].duration)]]; } [array sortUsingSelector:@selector(compare:)]; size = [array count]; for(i = 0; i < size; i++) if([[array objectAtIndex:i] floatValue] > currentTime){ nextTime = QTMakeTime(([[array objectAtIndex:i] floatValue] +1.0/60.0)* DEFAULT_FPS, DEFAULT_FPS); globalCurrentTime = nextTime; _currentTime = nextTime; [self moveSliderTo:nextTime]; [self refresh]; return; } } - (IBAction)gotoPrevKeyTime:(id)sender { float currentTime = [self getHipTime] * [timeSlider floatValue]; NSMutableArray* array = [[NSMutableArray alloc] init]; ElisLayer* l; QTTime nextTime; int i, size = [layers count]; for(i = 0; i < size; i++){ [array addObject:[NSNumber numberWithFloat:0.0]]; [array addObject:[NSNumber numberWithFloat:convertQTTimeToSecond([[layers objectAtIndex:i] mapping].time)]]; [array addObject:[NSNumber numberWithFloat:convertQTTimeToSecond([[layers objectAtIndex:i] mapping].time) + convertQTTimeToSecond([[layers objectAtIndex:i] mapping].duration)]]; } [array sortUsingSelector:@selector(compare:)]; size = [array count]; for(i = size-1; i >= 0; i--) if([[array objectAtIndex:i] floatValue] < currentTime){ // if(i % 2 != 0) // nextTime = QTMakeTime(([[array objectAtIndex:i] floatValue] -1.0/60)* DEFAULT_FPS, DEFAULT_FPS); // else // nextTime = QTMakeTime(([[array objectAtIndex:i] floatValue] +1.0/60)* DEFAULT_FPS, DEFAULT_FPS); nextTime = QTMakeTime(([[array objectAtIndex:i] floatValue] +0.0/60)* DEFAULT_FPS, DEFAULT_FPS); globalCurrentTime = nextTime; _currentTime = nextTime; [self moveSliderTo:nextTime]; [self refresh]; return; } } - (IBAction)cutLayerAtCurrentTime:(id)sender { ElisLayer* layer = [[_timeLineController getSelectLayer] valueForKey:@"ElisLayer"]; ElisLayer* new; new = [layer cutAtTime:globalCurrentTime]; [layers addObject:new]; [_timeLineController addLayer:[new alayer]]; } - (IBAction)playStop:(id)sender { [[NSGarbageCollector defaultCollector] collectExhaustively]; if(playing) [self stopPlay:nil]; else [self startPlay:nil]; } - (NSMutableArray*)writeAudioFiles { NSMutableArray* paths = [[NSMutableArray alloc] init]; int i, size = [layers count]; for(i = 0; i < size; i++){ if([[[[layers objectAtIndex:i] media] type] isEqualToString:@"sound"]){ ElisAudioWriter* w = [[ElisAudioWriter alloc] initWithLayer:[layers objectAtIndex:i]]; NSString* outPath = [NSString stringWithFormat:@"/tmp/%d.wav", i]; [w writeToFile:outPath]; [paths addObject:outPath]; } } return paths; } @end