OSDN Git Service

LinGui: Internalize all the icons so I don't have to install them in
[handbrake-jp/handbrake-jp-git.git] / macosx / HBQueueController.mm
1 /* HBQueueController
2
3     This file is part of the HandBrake source code.
4     Homepage: <http://handbrake.fr/>.
5     It may be used under the terms of the GNU General Public License. */
6
7 #import "HBQueueController.h"
8 #import "Controller.h"
9 #import "HBImageAndTextCell.h"
10
11 #define HB_ROW_HEIGHT_TITLE_ONLY           17.0
12
13 // Pasteboard type for or drag operations
14 #define HBQueuePboardType            @"HBQueuePboardType"
15
16 //------------------------------------------------------------------------------------
17 // Job ID Utilities
18 //------------------------------------------------------------------------------------
19
20 int MakeJobID(int jobGroupID, int sequenceNum)
21 {
22     return jobGroupID<<16 | sequenceNum;
23 }
24
25 bool IsFirstPass(int jobID)
26 {
27     return LoWord(jobID) == 0;
28 }
29
30 //------------------------------------------------------------------------------------
31 // NSMutableAttributedString (HBAdditions)
32 //------------------------------------------------------------------------------------
33
34 @interface NSMutableAttributedString (HBAdditions)
35 - (void) appendString: (NSString*)aString withAttributes: (NSDictionary *)aDictionary;
36 @end
37
38 @implementation NSMutableAttributedString (HBAdditions)
39 - (void) appendString: (NSString*)aString withAttributes: (NSDictionary *)aDictionary
40 {
41     NSAttributedString * s = [[[NSAttributedString alloc]
42         initWithString: aString
43         attributes: aDictionary] autorelease];
44     [self appendAttributedString: s];
45 }
46 @end
47
48 //------------------------------------------------------------------------------------
49 #pragma mark -
50 //------------------------------------------------------------------------------------
51
52 @implementation HBQueueOutlineView
53
54 - (void)viewDidEndLiveResize
55 {
56     // Since we disabled calculating row heights during a live resize, force them to
57     // recalculate now.
58     [self noteHeightOfRowsWithIndexesChanged:
59             [NSIndexSet indexSetWithIndexesInRange: NSMakeRange(0, [self numberOfRows])]];
60     [super viewDidEndLiveResize];
61 }
62
63 #if HB_QUEUE_DRAGGING
64 - (NSImage *)dragImageForRowsWithIndexes:(NSIndexSet *)dragRows tableColumns:(NSArray *)tableColumns event:(NSEvent*)dragEvent offset:(NSPointPointer)dragImageOffset
65 {
66     // Set the fIsDragging flag so that other's know that a drag operation is being
67     // performed.
68     fIsDragging = YES;
69
70     // By default, NSTableView only drags an image of the first column. Change this to
71     // drag an image of the queue's icon and desc columns.
72     NSArray * cols = [NSArray arrayWithObjects: [self tableColumnWithIdentifier:@"icon"], [self tableColumnWithIdentifier:@"desc"], nil];
73     return [super dragImageForRowsWithIndexes:dragRows tableColumns:cols event:dragEvent offset:dragImageOffset];
74 }
75 #endif
76
77 #if HB_QUEUE_DRAGGING
78 - (void) mouseDown:(NSEvent *)theEvent
79 {
80     // After a drag operation, reset fIsDragging back to NO. This is really the only way
81     // for us to detect when a drag has finished. You can't do it in acceptDrop because
82     // that won't be called if the dragged item is released outside the view.
83     [super mouseDown:theEvent];
84     fIsDragging = NO;
85 }
86 #endif
87
88 #if HB_QUEUE_DRAGGING
89 - (BOOL) isDragging;
90 {
91     return fIsDragging;
92 }
93 #endif
94
95 @end
96
97 #pragma mark -
98
99 //------------------------------------------------------------------------------------
100 // HBJob
101 //------------------------------------------------------------------------------------
102
103 static NSMutableParagraphStyle * _descriptionParagraphStyle = nil;
104 static NSDictionary* _detailAttribute = nil;
105 static NSDictionary* _detailBoldAttribute = nil;
106 static NSDictionary* _titleAttribute = nil;
107 static NSDictionary* _shortHeightAttribute = nil;
108
109 @implementation HBJob
110
111 + (HBJob*) jobWithLibhbJob: (hb_job_t *) job
112 {
113     return [[[HBJob alloc] initWithLibhbJob:job] autorelease];
114 }
115
116 - (id) initWithLibhbJob: (hb_job_t *) job
117 {
118     if (self = [super init])
119     {
120         sequence_id = job->sequence_id;
121
122         chapter_start = job->chapter_start;
123         chapter_end = job->chapter_end;
124         chapter_markers = job->chapter_markers;
125         memcpy(crop, job->crop, sizeof(crop));
126         deinterlace = job->deinterlace;
127         width = job->width;
128         height = job->height;
129         keep_ratio = job->keep_ratio;
130         grayscale = job->grayscale;
131         pixel_ratio = job->pixel_ratio;
132         pixel_aspect_width = job->pixel_aspect_width;
133         pixel_aspect_height = job->pixel_aspect_height;
134         vcodec = job->vcodec;
135         vquality = job->vquality;
136         vbitrate = job->vbitrate;
137         vrate = job->vrate;
138         vrate_base = job->vrate_base;
139         pass = job->pass;
140         h264_level = job->h264_level;
141         crf = job->crf;
142         if (job->x264opts)
143             x264opts = [[NSString stringWithUTF8String:job->x264opts] retain];
144         /* So, with the advent of job->list_audio's I decided why not just use an NSString and concatanate
145          all of the info we need for all of the audio values to display right into an NSString here ? So I
146          did. I have no idea why we are reading libhb stuff just to display it in the queue gui. So here we
147          are with a huge string. But its easy to change and saves alot of messing about. Maybe we move a bunch
148          of other display stuff into strings for display for each job. It's not like they have to actually do
149          anything.*/
150         hb_audio_config_t * audio;
151         NSString * thisJobAudioCodecs = [NSString stringWithFormat:@""];
152         NSString * thisJobAudioInfo = [NSString stringWithFormat:@""];
153         int i;
154         for( i = 0; i < hb_list_count(job->list_audio); i++ )
155         {
156            audio = (hb_audio_config_t *) hb_list_audio_config_item( job->list_audio, i );
157             /* Output Codec */
158             NSString *outputCodec;
159             if (audio->out.codec == HB_ACODEC_AC3)
160                 outputCodec = @"AC3";
161             else if (audio->out.codec == HB_ACODEC_FAAC)
162                 outputCodec = @"AAC";
163             else if (audio->out.codec == HB_ACODEC_LAME)
164                 outputCodec = @"MP3";
165             else if (audio->out.codec == HB_ACODEC_VORBIS)
166                 outputCodec = @"Vorbis";
167             else
168                 outputCodec = @"Unknown Codec";
169             /* Add the codec to the audio codecs list ( We should check against dupes)*/
170             thisJobAudioCodecs = [thisJobAudioCodecs stringByAppendingString:[NSString stringWithFormat:@" %@,",outputCodec]];
171             if (i > 0)
172             {
173                 /* Insert a line break so that we get each track on a separate line */
174                 /* Wicked HACK alert!!, use 18 whitespaces to align offset in display for list > 2 to offset "Audio" in the queue display
175                  Please Fix Me because this is embarrassing (but it works) */
176                 thisJobAudioInfo = [thisJobAudioInfo stringByAppendingString:@"\n                  "];
177             }
178             /* Detailed Job audio track info*/
179             /* Track Number and Mixdown Info */
180             if (audio->out.mixdown == HB_ACODEC_AC3)// Remember for ac3 passthru the mixdown uses the source codec
181                 thisJobAudioInfo = [thisJobAudioInfo stringByAppendingString:[NSString stringWithFormat:@"Track %d: Source: %@ Output: %@, Pass-Thru", i + 1, [NSString stringWithUTF8String:audio->lang.description], outputCodec]];
182             else if (audio->out.mixdown == HB_AMIXDOWN_MONO)
183                 thisJobAudioInfo = [thisJobAudioInfo stringByAppendingString:[NSString stringWithFormat:@"Track %d: Source: %@ Output: %@, Mono", i + 1, [NSString stringWithUTF8String:audio->lang.description], outputCodec]];
184             else if (audio->out.mixdown == HB_AMIXDOWN_STEREO)
185                 thisJobAudioInfo = [thisJobAudioInfo stringByAppendingString:[NSString stringWithFormat:@"Track %d: Source: %@ Output: %@, Stereo", i + 1, [NSString stringWithUTF8String:audio->lang.description], outputCodec]];
186             else if (audio->out.mixdown == HB_AMIXDOWN_DOLBY)
187                 thisJobAudioInfo = [thisJobAudioInfo stringByAppendingString:[NSString stringWithFormat:@"Track %d: Source: %@ Output: %@, Dolby Surround", i + 1, [NSString stringWithUTF8String:audio->lang.description], outputCodec]];
188             else if (audio->out.mixdown == HB_AMIXDOWN_DOLBYPLII)
189                 thisJobAudioInfo = [thisJobAudioInfo stringByAppendingString:[NSString stringWithFormat:@"Track %d: Source: %@ Output: %@, Dolby Pro Logic II", i + 1, [NSString stringWithUTF8String:audio->lang.description], outputCodec]];
190             else if (audio->out.mixdown == HB_AMIXDOWN_6CH)
191                 thisJobAudioInfo = [thisJobAudioInfo stringByAppendingString:[NSString stringWithFormat:@"Track %d: Source: %@ Output: %@, 6 Channel Discreet", i + 1, [NSString stringWithUTF8String:audio->lang.description], outputCodec]];
192             else
193                 thisJobAudioInfo = [thisJobAudioInfo stringByAppendingString:[NSString stringWithFormat:@"Track %d: Source: %@ Output: Unknown Codec Info", i + 1, [NSString stringWithUTF8String:audio->lang.description]]];
194
195             thisJobAudioInfo = [thisJobAudioInfo stringByAppendingString:[NSString stringWithFormat:@", %d kbps, %d Hz", audio->out.bitrate, audio->out.samplerate]];
196
197         }
198         audioinfo_summary = [[NSString stringWithFormat:@"%@",thisJobAudioInfo]retain];
199         audioinfo_codecs = [[NSString stringWithFormat:@"%@",thisJobAudioCodecs]retain];
200
201         subtitle = job->subtitle;
202         mux = job->mux;
203         if (job->file)
204             file = [[NSString stringWithUTF8String:job->file] retain];
205         if (job->title->name)
206             titleName = [[NSString stringWithUTF8String:job->title->name] retain];
207         titleIndex = job->title->index;
208         titleWidth = job->title->width;
209         titleHeight = job->title->height;
210         if (job->subtitle >= 0)
211         {
212             hb_subtitle_t * aSubtitle = (hb_subtitle_t *) hb_list_item(job->title->list_subtitle, job->subtitle);
213             if (aSubtitle)
214                 subtitleLang = [[NSString stringWithUTF8String:aSubtitle->lang] retain];
215         }
216
217         // Calculate and store output dimensions and anamorphic dimensions
218         if (pixel_ratio == 1) // Original PAR Implementation, now called Strict Anamorphic
219         {
220             output_width = titleWidth - crop[2] - crop[3];
221             output_height = titleHeight - crop[0] - crop[1];
222             anamorphic_width = output_width * pixel_aspect_width / pixel_aspect_height;
223             anamorphic_height = output_height;
224         }
225         else if (pixel_ratio == 2) // Loose Anamorphic
226         {
227             // call hb_set_anamorphic_size to do a "dry run" to get the values to be
228             // used by libhb for loose anamorphic.
229             int par_width, par_height;
230             hb_set_anamorphic_size(job, &output_width, &output_height, &par_width, &par_height);
231             anamorphic_width = output_width * par_width / par_height;
232             anamorphic_height = output_height;
233         }
234         else    // No Anamorphic
235         {
236             output_width = width;
237             output_height = height;
238             anamorphic_width = 0;       // not needed for this case
239             anamorphic_height = 0;      // not needed for this case
240         }
241
242     }
243     return self;
244 }
245
246 - (void) dealloc
247 {
248     // jobGroup is a weak reference and does not need to be deleted
249     [x264opts release];
250     [file release];
251     [titleName release];
252     [subtitleLang release];
253     [audioinfo_summary release];
254     [audioinfo_codecs release];
255     [super dealloc];
256 }
257
258 - (HBJobGroup *) jobGroup
259 {
260     return jobGroup;
261 }
262
263 - (void) setJobGroup: (HBJobGroup *)aJobGroup
264 {
265     // This is a weak reference. We don't retain or release it.
266     jobGroup = aJobGroup;
267 }
268
269 //------------------------------------------------------------------------------------
270 // Generate string to display in UI.
271 //------------------------------------------------------------------------------------
272
273 - (NSMutableAttributedString *) attributedDescriptionWithIcon: (BOOL)withIcon
274                               withTitle: (BOOL)withTitle
275                            withPassName: (BOOL)withPassName
276                          withFormatInfo: (BOOL)withFormatInfo
277                         withDestination: (BOOL)withDestination
278                         withPictureInfo: (BOOL)withPictureInfo
279                           withVideoInfo: (BOOL)withVideoInfo
280                            withx264Info: (BOOL)withx264Info
281                           withAudioInfo: (BOOL)withAudioInfo
282                        withSubtitleInfo: (BOOL)withSubtitleInfo
283
284 {
285     NSMutableAttributedString * finalString = [[[NSMutableAttributedString alloc] initWithString: @""] autorelease];
286
287     // Attributes
288     NSMutableParagraphStyle * ps = [HBJob descriptionParagraphStyle];
289     NSDictionary* detailAttr = [HBJob descriptionDetailAttribute];
290     NSDictionary* detailBoldAttr = [HBJob descriptionDetailBoldAttribute];
291     NSDictionary* titleAttr = [HBJob descriptionTitleAttribute];
292     NSDictionary* shortHeightAttr = [HBJob descriptionShortHeightAttribute];
293
294     // Title with summary
295     if (withTitle)
296     {
297         if (withIcon)
298         {
299             NSFileWrapper * wrapper = [[[NSFileWrapper alloc] initWithPath:[[NSBundle mainBundle] pathForImageResource: @"JobSmall"]] autorelease];
300             NSTextAttachment * imageAttachment = [[[NSTextAttachment alloc] initWithFileWrapper:wrapper] autorelease];
301
302             NSDictionary* imageAttributes = [NSDictionary dictionaryWithObjectsAndKeys:
303                             [NSNumber numberWithFloat: -2.0], NSBaselineOffsetAttributeName,
304                             imageAttachment, NSAttachmentAttributeName,
305                             ps, NSParagraphStyleAttributeName,
306                             nil];
307
308             NSAttributedString * imageAsString = [[[NSAttributedString alloc]
309                     initWithString: [NSString stringWithFormat:@"%C%C", NSAttachmentCharacter, NSTabCharacter]
310                     attributes: imageAttributes] autorelease];
311
312             [finalString appendAttributedString:imageAsString];
313         }
314
315         // Note: use title->name instead of title->dvd since name is just the chosen
316         // folder, instead of dvd which is the full path
317         [finalString appendString:titleName withAttributes:titleAttr];
318
319         NSString * summaryInfo;
320
321         NSString * chapterString = (chapter_start == chapter_end) ?
322                 [NSString stringWithFormat:@"Chapter %d", chapter_start] :
323                 [NSString stringWithFormat:@"Chapters %d through %d", chapter_start, chapter_end];
324
325         BOOL hasIndepthScan = (pass == -1);
326         int numVideoPasses = 0;
327
328         // To determine number of video passes, we need to skip past the subtitle scan.
329         if (hasIndepthScan)
330         {
331             // When job is the one currently being processed, then the next in its group
332             // is the the first job in the queue.
333             HBJob * nextjob = nil;
334             NSUInteger index = [jobGroup indexOfJob:self];
335             if (index != NSNotFound)
336                 nextjob = [jobGroup jobAtIndex:index+1];
337             if (nextjob)    // Overly cautious in case there is no next job!
338                 numVideoPasses = MIN( 2, nextjob->pass + 1 );
339         }
340         else
341             numVideoPasses = MIN( 2, pass + 1 );
342
343         if (hasIndepthScan && numVideoPasses == 1)
344             summaryInfo = [NSString stringWithFormat: @"  (Title %d, %@, Deep Scan, Single Video Pass)", titleIndex, chapterString];
345         else if (hasIndepthScan && numVideoPasses > 1)
346             summaryInfo = [NSString stringWithFormat: @"  (Title %d, %@, Deep Scan, %d Video Passes)", titleIndex, chapterString, numVideoPasses];
347         else if (numVideoPasses == 1)
348             summaryInfo = [NSString stringWithFormat: @"  (Title %d, %@, Single Video Pass)", titleIndex, chapterString];
349         else
350             summaryInfo = [NSString stringWithFormat: @"  (Title %d, %@, %d Video Passes)", titleIndex, chapterString, numVideoPasses];
351
352         [finalString appendString:[NSString stringWithFormat:@"%@\n", summaryInfo] withAttributes:detailAttr];
353
354         // Insert a short-in-height line to put some white space after the title
355         [finalString appendString:@"\n" withAttributes:shortHeightAttr];
356     }
357
358     // End of title stuff
359
360
361     // Pass Name
362     if (withPassName)
363     {
364         if (withIcon)
365         {
366             NSString * imageName;
367             switch (pass)
368             {
369                 case -1: imageName = @"JobPassSubtitleSmall"; break;
370                 case  0: imageName = @"JobPassFirstSmall"; break;
371                 case  1: imageName = @"JobPassFirstSmall"; break;
372                 case  2: imageName = @"JobPassSecondSmall"; break;
373                 default: imageName = @"JobPassUnknownSmall"; break;
374             }
375
376             NSFileWrapper * wrapper = [[[NSFileWrapper alloc] initWithPath:[[NSBundle mainBundle] pathForImageResource: imageName]] autorelease];
377             NSTextAttachment * imageAttachment = [[[NSTextAttachment alloc] initWithFileWrapper:wrapper] autorelease];
378
379             NSDictionary* imageAttributes = [NSDictionary dictionaryWithObjectsAndKeys:
380                             [NSNumber numberWithFloat: -2.0], NSBaselineOffsetAttributeName,
381                             imageAttachment, NSAttachmentAttributeName,
382                             ps, NSParagraphStyleAttributeName,
383                             nil];
384
385             NSAttributedString * imageAsString = [[[NSAttributedString alloc]
386                     initWithString: [NSString stringWithFormat:@"%C%C", NSAttachmentCharacter, NSTabCharacter]
387                     attributes: imageAttributes] autorelease];
388
389             [finalString appendAttributedString:imageAsString];
390         }
391
392         NSString * jobPassName;
393         if (pass == -1)
394             jobPassName = NSLocalizedString (@"Deep Scan", nil);
395         else
396         {
397             int passNum = MAX( 1, pass );
398             if (passNum == 0)
399                 jobPassName = NSLocalizedString (@"1st Pass", nil);
400             else if (passNum == 1)
401                 jobPassName = NSLocalizedString (@"1st Pass", nil);
402             else if (passNum == 2)
403                 jobPassName = NSLocalizedString (@"2nd Pass", nil);
404             else
405                 jobPassName = [NSString stringWithFormat: NSLocalizedString(@"Pass %d", nil), passNum];
406         }
407         [finalString appendString:[NSString stringWithFormat:@"%@\n", jobPassName] withAttributes:detailBoldAttr];
408     }
409
410     // Video Codec needed by FormatInfo and withVideoInfo
411     NSString * jobVideoCodec = nil;
412     if (withFormatInfo || withVideoInfo)
413     {
414         // 2097152
415         // Video Codec settings (Encoder in the gui)
416         if (vcodec == HB_VCODEC_FFMPEG)
417             jobVideoCodec = @"FFmpeg"; // HB_VCODEC_FFMPEG
418         else if (vcodec == HB_VCODEC_XVID)
419             jobVideoCodec = @"XviD"; // HB_VCODEC_XVID
420         else if (vcodec == HB_VCODEC_X264)
421         {
422             // Deterimine for sure how we are now setting iPod uuid atom
423             if (h264_level) // We are encoding for iPod
424                 jobVideoCodec = @"x264 (H.264 iPod)"; // HB_VCODEC_X264
425             else
426                 jobVideoCodec = @"x264 (H.264 Main)"; // HB_VCODEC_X264
427         }
428     }
429     if (jobVideoCodec == nil)
430         jobVideoCodec = @"unknown";
431
432     if (withFormatInfo)
433     {
434         NSString * jobFormatInfo;
435         // Muxer settings (File Format in the gui)
436         if (mux == 65536 || mux == 131072 || mux == 1048576)
437             jobFormatInfo = @"MP4"; // HB_MUX_MP4,HB_MUX_PSP,HB_MUX_IPOD
438         else if (mux == 262144)
439             jobFormatInfo = @"AVI"; // HB_MUX_AVI
440         else if (mux == 524288)
441             jobFormatInfo = @"OGM"; // HB_MUX_OGM
442         else if (mux == 2097152)
443             jobFormatInfo = @"MKV"; // HB_MUX_MKV
444         else
445             jobFormatInfo = @"unknown";
446
447         if (chapter_markers == 1)
448             jobFormatInfo = [NSString stringWithFormat:@"%@ Container, %@ Video + %@ Audio, Chapter Markers\n", jobFormatInfo, jobVideoCodec, audioinfo_codecs];
449         else
450             jobFormatInfo = [NSString stringWithFormat:@"%@ Container, %@ Video + %@ Audio\n", jobFormatInfo, jobVideoCodec, audioinfo_codecs];
451
452         [finalString appendString: @"Format: " withAttributes:detailBoldAttr];
453         [finalString appendString: jobFormatInfo withAttributes:detailAttr];
454     }
455
456     if (withDestination)
457     {
458         [finalString appendString: @"Destination: " withAttributes:detailBoldAttr];
459         [finalString appendString:[NSString stringWithFormat:@"%@\n", file] withAttributes:detailAttr];
460     }
461
462
463     if (withPictureInfo)
464     {
465         NSString * jobPictureInfo;
466         if (pixel_ratio == 1) // Original PAR Implementation, now called Strict Anamorphic
467             jobPictureInfo = [NSString stringWithFormat:@"%d x %d (%d x %d Strict Anamorphic)", output_width, output_height, anamorphic_width, anamorphic_height];
468         else if (pixel_ratio == 2) // Loose Anamorphic
469             jobPictureInfo = [NSString stringWithFormat:@"%d x %d (%d x %d Loose Anamorphic)", output_width, output_height, anamorphic_width, anamorphic_height];
470         else
471             jobPictureInfo = [NSString stringWithFormat:@"%d x %d", output_width, output_height];
472         if (keep_ratio == 1)
473             jobPictureInfo = [jobPictureInfo stringByAppendingString:@" Keep Aspect Ratio"];
474
475         if (grayscale == 1)
476             jobPictureInfo = [jobPictureInfo stringByAppendingString:@", Grayscale"];
477
478         if (deinterlace == 1)
479             jobPictureInfo = [jobPictureInfo stringByAppendingString:@", Deinterlace"];
480         if (withIcon)   // implies indent the info
481             [finalString appendString: @"\t" withAttributes:detailBoldAttr];
482         [finalString appendString: @"Picture: " withAttributes:detailBoldAttr];
483         [finalString appendString:[NSString stringWithFormat:@"%@\n", jobPictureInfo] withAttributes:detailAttr];
484     }
485
486     if (withVideoInfo)
487     {
488         NSString * jobVideoQuality;
489         NSString * jobVideoDetail;
490
491         if (vquality <= 0 || vquality >= 1)
492             jobVideoQuality = [NSString stringWithFormat:@"%d kbps", vbitrate];
493         else
494         {
495             NSNumber * vidQuality;
496             vidQuality = [NSNumber numberWithInt:vquality * 100];
497             // this is screwed up kind of. Needs to be formatted properly.
498             if (crf == 1)
499                 jobVideoQuality = [NSString stringWithFormat:@"%@%% CRF", vidQuality];
500             else
501                 jobVideoQuality = [NSString stringWithFormat:@"%@%% CQP", vidQuality];
502         }
503
504         if (vrate_base == 1126125)
505         {
506             // NTSC FILM 23.976
507             jobVideoDetail = [NSString stringWithFormat:@"%@, %@, 23.976 fps", jobVideoCodec, jobVideoQuality];
508         }
509         else if (vrate_base == 900900)
510         {
511             // NTSC 29.97
512             jobVideoDetail = [NSString stringWithFormat:@"%@, %@, 29.97 fps", jobVideoCodec, jobVideoQuality];
513         }
514         else
515         {
516             // Everything else
517             jobVideoDetail = [NSString stringWithFormat:@"%@, %@, %d fps", jobVideoCodec, jobVideoQuality, vrate / vrate_base];
518         }
519         if (withIcon)   // implies indent the info
520             [finalString appendString: @"\t" withAttributes:detailBoldAttr];
521         [finalString appendString: @"Video: " withAttributes:detailBoldAttr];
522         [finalString appendString:[NSString stringWithFormat:@"%@\n", jobVideoDetail] withAttributes:detailAttr];
523     }
524
525     if (withx264Info)
526     {
527         if (vcodec == HB_VCODEC_X264 && x264opts)
528         {
529             if (withIcon)   // implies indent the info
530                 [finalString appendString: @"\t" withAttributes:detailBoldAttr];
531             [finalString appendString: @"x264 Options: " withAttributes:detailBoldAttr];
532             [finalString appendString:[NSString stringWithFormat:@"%@\n", x264opts] withAttributes:detailAttr];
533         }
534     }
535
536     if (withAudioInfo)
537     {
538         if (withIcon)   // implies indent the info
539             [finalString appendString: @"\t" withAttributes:detailBoldAttr];
540         [finalString appendString: @"Audio: " withAttributes:detailBoldAttr];
541         [finalString appendString:[NSString stringWithFormat:@"%@\n", audioinfo_summary] withAttributes:detailAttr];
542     }
543
544     if (withSubtitleInfo)
545     {
546         // subtitle scan == -1 in two cases:
547         // autoselect: when pass == -1
548         // none: when pass != -1
549         if ((subtitle == -1) && (pass == -1))
550         {
551             if (withIcon)   // implies indent the info
552                 [finalString appendString: @"\t" withAttributes:detailBoldAttr];
553             [finalString appendString: @"Subtitles: " withAttributes:detailBoldAttr];
554             [finalString appendString: @"Autoselect " withAttributes:detailAttr];
555         }
556         else if (subtitle >= 0)
557         {
558             if (subtitleLang)
559             {
560                 if (withIcon)   // implies indent the info
561                     [finalString appendString: @"\t" withAttributes:detailBoldAttr];
562                 [finalString appendString: @"Subtitles: " withAttributes:detailBoldAttr];
563                 [finalString appendString: subtitleLang   withAttributes:detailAttr];
564             }
565         }
566     }
567
568
569     if ([[finalString string] hasSuffix: @"\n"])
570         [finalString deleteCharactersInRange: NSMakeRange([[finalString string] length]-1, 1)];
571
572     return finalString;
573 }
574
575 + (NSMutableParagraphStyle *) descriptionParagraphStyle
576 {
577     if (!_descriptionParagraphStyle)
578     {
579         _descriptionParagraphStyle = [[[NSParagraphStyle defaultParagraphStyle] mutableCopy] retain];
580         [_descriptionParagraphStyle setHeadIndent: 40.0];
581         [_descriptionParagraphStyle setParagraphSpacing: 1.0];
582         [_descriptionParagraphStyle setTabStops:[NSArray array]];    // clear all tabs
583         [_descriptionParagraphStyle addTabStop: [[[NSTextTab alloc] initWithType: NSLeftTabStopType location: 20.0] autorelease]];
584     }
585     return _descriptionParagraphStyle;
586 }
587
588 + (NSDictionary *) descriptionDetailAttribute
589 {
590     if (!_detailAttribute)
591         _detailAttribute = [[NSDictionary dictionaryWithObjectsAndKeys:
592                 [NSFont systemFontOfSize:10.0], NSFontAttributeName,
593                 _descriptionParagraphStyle, NSParagraphStyleAttributeName,
594                 nil] retain];
595     return _detailAttribute;
596 }
597
598 + (NSDictionary *) descriptionDetailBoldAttribute
599 {
600     if (!_detailBoldAttribute)
601         _detailBoldAttribute = [[NSDictionary dictionaryWithObjectsAndKeys:
602                 [NSFont boldSystemFontOfSize:10.0], NSFontAttributeName,
603                 _descriptionParagraphStyle, NSParagraphStyleAttributeName,
604                 nil] retain];
605     return _detailBoldAttribute;
606 }
607
608 + (NSDictionary *) descriptionTitleAttribute
609 {
610     if (!_titleAttribute)
611         _titleAttribute = [[NSDictionary dictionaryWithObjectsAndKeys:
612                 [NSFont systemFontOfSize:[NSFont systemFontSize]], NSFontAttributeName,
613                 _descriptionParagraphStyle, NSParagraphStyleAttributeName,
614                 nil] retain];
615     return _titleAttribute;
616 }
617
618 + (NSDictionary *) descriptionShortHeightAttribute
619 {
620     if (!_shortHeightAttribute)
621         _shortHeightAttribute = [[NSDictionary dictionaryWithObjectsAndKeys:
622                 [NSFont systemFontOfSize:2.0], NSFontAttributeName,
623                 nil] retain];
624     return _shortHeightAttribute;
625 }
626
627
628 @end
629
630 #pragma mark -
631
632 //------------------------------------------------------------------------------------
633 // HBJobGroup
634 //------------------------------------------------------------------------------------
635
636 // Notification sent from HBJobGroup setStatus whenever the status changes.
637 NSString *HBJobGroupStatusNotification = @"HBJobGroupStatusNotification";
638
639 @implementation HBJobGroup
640
641 + (HBJobGroup *) jobGroup;
642 {
643     return [[[HBJobGroup alloc] init] autorelease];
644 }
645
646 - (id) init
647 {
648     if (self = [super init])
649     {
650         fJobs = [[NSMutableArray arrayWithCapacity:0] retain];
651         fDescription = [[NSMutableAttributedString alloc] initWithString: @""];
652         [self setNeedsDescription: NO];
653         fStatus = HBStatusNone;
654     }
655     return self;
656 }
657
658 - (void) dealloc
659 {
660     [fPresetName release];
661     [fJobs release];
662     [super dealloc];
663 }
664
665 - (unsigned int) count
666 {
667     return [fJobs count];
668 }
669
670 - (void) addJob: (HBJob *)aJob
671 {
672     [aJob setJobGroup:self];
673     [fJobs addObject: aJob];
674     [self setNeedsDescription: YES];
675     fLastDescriptionHeight = 0;
676     fLastDescriptionWidth = 0;
677 }
678
679 - (HBJob *) jobAtIndex: (unsigned)index
680 {
681     return [fJobs objectAtIndex: index];
682 }
683
684 - (unsigned) indexOfJob: (HBJob *)aJob;
685 {
686     return [fJobs indexOfObject: aJob];
687 }
688
689 - (NSMutableArray *) fJobs
690 {
691     return fJobs;
692 }
693
694 - (void) setNeedsDescription: (BOOL)flag
695 {
696     fNeedsDescription = flag;
697 }
698
699 - (void) updateDescription
700 {
701     fNeedsDescription = NO;
702
703     [fDescription deleteCharactersInRange: NSMakeRange(0, [fDescription length])];
704
705     if ([self count] == 0)
706     {
707         NSAssert(NO, @" jobgroup with no jobs");
708         return;
709     }
710
711     HBJob * job = [self jobAtIndex:0];
712
713     // append the title
714     [fDescription appendAttributedString: [job attributedDescriptionWithIcon: NO
715                             withTitle: YES
716                          withPassName: NO
717                        withFormatInfo: NO
718                       withDestination: NO
719                       withPictureInfo: NO
720                         withVideoInfo: NO
721                          withx264Info: NO
722                         withAudioInfo: NO
723                      withSubtitleInfo: NO]];
724
725     // append the preset name
726     if ([fPresetName length])
727     {
728         [fDescription appendString:@"Preset: " withAttributes:[HBJob descriptionDetailBoldAttribute]];
729         [fDescription appendString:fPresetName withAttributes:[HBJob descriptionDetailAttribute]];
730         [fDescription appendString:@"\n" withAttributes:[HBJob descriptionDetailAttribute]];
731     }
732
733     // append the format and destinaton
734     [fDescription appendAttributedString: [job attributedDescriptionWithIcon: NO
735                             withTitle: NO
736                          withPassName: NO
737                        withFormatInfo: YES
738                       withDestination: YES
739                       withPictureInfo: NO
740                         withVideoInfo: NO
741                          withx264Info: NO
742                         withAudioInfo: NO
743                      withSubtitleInfo: NO]];
744
745
746     NSAttributedString * carriageReturn = [[NSAttributedString alloc] initWithString:@"\n"];
747
748     for( job in fJobs )
749     {
750         int pass = job->pass;
751         [fDescription appendAttributedString:carriageReturn];
752         [fDescription appendAttributedString:
753             [job attributedDescriptionWithIcon: YES
754                                 withTitle: NO
755                              withPassName: YES
756                            withFormatInfo: NO
757                           withDestination: NO
758                           withPictureInfo: pass != -1
759                             withVideoInfo: pass != -1
760                              withx264Info: pass != -1
761                             withAudioInfo: pass == 0 || pass == 2
762                          withSubtitleInfo: YES]];
763     }
764
765 }
766
767 - (NSMutableAttributedString *) attributedDescription
768 {
769     if (fNeedsDescription)
770         [self updateDescription];
771     return fDescription;
772 }
773
774 - (CGFloat) heightOfDescriptionForWidth:(CGFloat)width
775 {
776     // Try to return the cached value if no changes have happened since the last time
777     if ((width == fLastDescriptionWidth) && (fLastDescriptionHeight != 0) && !fNeedsDescription)
778         return fLastDescriptionHeight;
779
780     if (fNeedsDescription)
781         [self updateDescription];
782
783     // Calculate the height
784     NSRect bounds = [fDescription boundingRectWithSize:NSMakeSize(width, 10000) options:NSStringDrawingUsesLineFragmentOrigin];
785     fLastDescriptionHeight = bounds.size.height + 6.0; // add some border to bottom
786     fLastDescriptionWidth = width;
787     return fLastDescriptionHeight;
788
789 /* supposedly another way to do this, in case boundingRectWithSize isn't working
790     NSTextView* tmpView = [[NSTextView alloc] initWithFrame:NSMakeRect(0, 0, width, 1)];
791     [[tmpView textStorage] setAttributedString:aString];
792     [tmpView setHorizontallyResizable:NO];
793     [tmpView setVerticallyResizable:YES];
794 //    [[tmpView textContainer] setHeightTracksTextView: YES];
795 //    [[tmpView textContainer] setContainerSize: NSMakeSize(width, 10000)];
796     [tmpView sizeToFit];
797     float height = [tmpView frame].size.height;
798     [tmpView release];
799     return height;
800 */
801 }
802
803 - (CGFloat) lastDescriptionHeight
804 {
805     return fLastDescriptionHeight;
806 }
807
808 - (void) setStatus: (HBQueueJobGroupStatus)status
809 {
810     // Create a dictionary with the old status
811     NSDictionary * userInfo = [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:self->fStatus] forKey:@"HBOldJobGroupStatus"];
812
813     self->fStatus = status;
814
815     // Send notification with old status
816     [[NSNotificationCenter defaultCenter] postNotificationName:HBJobGroupStatusNotification object:self userInfo:userInfo];
817 }
818
819 - (HBQueueJobGroupStatus) status
820 {
821     return self->fStatus;
822 }
823
824 - (void) setPresetName: (NSString *)name
825 {
826     [name retain];
827     [fPresetName release];
828     fPresetName = name;
829 }
830
831 - (NSString *) presetName
832 {
833     return fPresetName;
834 }
835
836 - (NSString *) name
837 {
838     HBJob * firstJob = [self jobAtIndex:0];
839     return firstJob ? firstJob->titleName : nil;
840 }
841
842 - (NSString *) destinationPath
843 {
844     HBJob * firstJob = [self jobAtIndex:0];
845     return firstJob ? firstJob->file : nil;
846 }
847
848 @end
849
850
851 #pragma mark -
852
853 // Toolbar identifiers
854 static NSString*    HBQueueToolbar                            = @"HBQueueToolbar1";
855 static NSString*    HBQueueStartCancelToolbarIdentifier       = @"HBQueueStartCancelToolbarIdentifier";
856 static NSString*    HBQueuePauseResumeToolbarIdentifier       = @"HBQueuePauseResumeToolbarIdentifier";
857
858 #pragma mark -
859
860 @implementation HBQueueController
861
862 //------------------------------------------------------------------------------------
863 // init
864 //------------------------------------------------------------------------------------
865 - (id)init
866 {
867     if (self = [super initWithWindowNibName:@"Queue"])
868     {
869         // NSWindowController likes to lazily load its window nib. Since this
870         // controller tries to touch the outlets before accessing the window, we
871         // need to force it to load immadiately by invoking its accessor.
872         //
873         // If/when we switch to using bindings, this can probably go away.
874         [self window];
875
876         // Our defaults
877         [[NSUserDefaults standardUserDefaults] registerDefaults:[NSDictionary dictionaryWithObjectsAndKeys:
878             @"NO",      @"QueueWindowIsOpen",
879             @"NO",      @"QueueShowsDetail",
880             @"YES",     @"QueueShowsJobsAsGroups",
881             nil]];
882
883         fJobGroups = [[NSMutableArray arrayWithCapacity:0] retain];
884
885         // Register for HBJobGroup status changes
886         [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(jobGroupStatusNotification:) name:HBJobGroupStatusNotification object:nil];
887     }
888     return self;
889 }
890
891 //------------------------------------------------------------------------------------
892 // dealloc
893 //------------------------------------------------------------------------------------
894 - (void)dealloc
895 {
896     // clear the delegate so that windowWillClose is not attempted
897     if( [[self window] delegate] == self )
898         [[self window] setDelegate:nil];
899
900     [fJobGroups release];
901     [fCurrentJobGroup release];
902     [fSavedExpandedItems release];
903     [fSavedSelectedItems release];
904
905     [[NSNotificationCenter defaultCenter] removeObserver:self];
906
907     [super dealloc];
908 }
909
910 //------------------------------------------------------------------------------------
911 // Receive HB handle
912 //------------------------------------------------------------------------------------
913 - (void)setHandle: (hb_handle_t *)handle
914 {
915     fHandle = handle;
916 }
917
918 //------------------------------------------------------------------------------------
919 // Receive HBController
920 //------------------------------------------------------------------------------------
921 - (void)setHBController: (HBController *)controller
922 {
923     fHBController = controller;
924 }
925
926 #pragma mark -
927 #pragma mark - Getting the currently processing job group
928
929 //------------------------------------------------------------------------------------
930 // Returns the HBJobGroup that is currently being encoded; nil if no encoding is
931 // occurring.
932 //------------------------------------------------------------------------------------
933 - (HBJobGroup *) currentJobGroup;
934 {
935     return fCurrentJobGroup;
936 }
937
938 //------------------------------------------------------------------------------------
939 // Returns the HBJob (pass) that is currently being encoded; nil if no encoding is
940 // occurring.
941 //------------------------------------------------------------------------------------
942 - (HBJob *) currentJob
943 {
944     return fCurrentJob;
945 }
946
947 #pragma mark -
948
949 //------------------------------------------------------------------------------------
950 // Displays and brings the queue window to the front
951 //------------------------------------------------------------------------------------
952 - (IBAction) showQueueWindow: (id)sender
953 {
954     [self showWindow:sender];
955     [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"QueueWindowIsOpen"];
956 }
957
958 //------------------------------------------------------------------------------------
959 // Show or hide the current job pane (fCurrentJobPane).
960 //------------------------------------------------------------------------------------
961 - (void) showCurrentJobPane: (BOOL)showPane
962 {
963     if (showPane == fCurrentJobPaneShown)
964         return;
965
966     // Things to keep in mind:
967     // - When the current job pane is shown, it occupies the upper portion of the
968     //   window with the queue occupying the bottom portion of the window.
969     // - When the current job pane is hidden, it slides up and out of view.
970     //   NSView setHidden is NOT used. The queue pane is resized to occupy the full
971     //   window.
972
973     NSRect windowFrame = [[fCurrentJobPane superview] frame];
974     NSRect queueFrame, jobFrame;
975     if (showPane)
976         NSDivideRect(windowFrame, &jobFrame, &queueFrame, NSHeight([fCurrentJobPane frame]), NSMaxYEdge);
977     else
978     {
979         queueFrame = windowFrame;
980         jobFrame = [fCurrentJobPane frame];
981         jobFrame.origin.y = NSHeight(windowFrame);
982     }
983
984     // Move fCurrentJobPane
985     NSDictionary * dict1 = [NSDictionary dictionaryWithObjectsAndKeys:
986         fCurrentJobPane, NSViewAnimationTargetKey,
987         [NSValue valueWithRect:jobFrame], NSViewAnimationEndFrameKey,
988         nil];
989
990     // Resize fQueuePane
991     NSDictionary * dict2 = [NSDictionary dictionaryWithObjectsAndKeys:
992         fQueuePane, NSViewAnimationTargetKey,
993         [NSValue valueWithRect:queueFrame], NSViewAnimationEndFrameKey,
994         nil];
995
996     NSViewAnimation * anAnimation = [[[NSViewAnimation alloc] initWithViewAnimations:nil] autorelease];
997     [anAnimation setViewAnimations:[NSArray arrayWithObjects:dict1, dict2, nil]];
998     [anAnimation setDuration:0.25];
999     [anAnimation setAnimationBlockingMode:NSAnimationBlocking]; // prevent user from resizing the window during an animation
1000     [anAnimation startAnimation];
1001
1002     fCurrentJobPaneShown = showPane;
1003 }
1004
1005 //------------------------------------------------------------------------------------
1006 // Sets fCurrentJobGroup to a new job group.
1007 //------------------------------------------------------------------------------------
1008 - (void) setCurrentJobGroup: (HBJobGroup *)aJobGroup
1009 {
1010     if (aJobGroup)
1011         [aJobGroup setStatus: HBStatusWorking];
1012
1013     [aJobGroup retain];
1014     [fCurrentJobGroup release];
1015     fCurrentJobGroup = aJobGroup;
1016 }
1017
1018 #pragma mark - Finding job groups
1019
1020 //------------------------------------------------------------------------------------
1021 // Returns the first pending job with a specified destination path or nil if no such
1022 // job exists.
1023 //------------------------------------------------------------------------------------
1024 - (HBJobGroup *) pendingJobGroupWithDestinationPath: (NSString *)path
1025 {
1026     for( HBJobGroup * aJobGroup in fJobGroups )
1027     {
1028         if ([[aJobGroup destinationPath] isEqualToString: path])
1029             return aJobGroup;
1030     }
1031     return nil;
1032 }
1033
1034 //------------------------------------------------------------------------------------
1035 // Locates and returns a HBJob whose sequence_id matches a specified value.
1036 //------------------------------------------------------------------------------------
1037 - (HBJob *) findJobWithID: (int)aJobID
1038 {
1039     for( HBJobGroup * aJobGroup in fJobGroups )
1040     {
1041         for( HBJob * job in [aJobGroup fJobs] )
1042         {
1043             if (job->sequence_id == aJobID)
1044                 return job;
1045         }
1046     }
1047     return nil;
1048 }
1049
1050 //------------------------------------------------------------------------------------
1051 // Locates and returns a libhb job whose sequence_id matches a specified value.
1052 //------------------------------------------------------------------------------------
1053 - (hb_job_t *) findLibhbJobWithID: (int)aJobID
1054 {
1055     hb_job_t * job;
1056     int index = 0;
1057     while( ( job = hb_job( fHandle, index++ ) ) )
1058     {
1059         if (job->sequence_id == aJobID)
1060             return job;
1061     }
1062     return nil;
1063 }
1064
1065 #pragma mark -
1066 #pragma mark Queue Counts
1067
1068 //------------------------------------------------------------------------------------
1069 // Sets a flag indicating that the values for fPendingCount, fCompletedCount,
1070 // fCanceledCount, and fWorkingCount need to be recalculated.
1071 //------------------------------------------------------------------------------------
1072 - (void) setJobGroupCountsNeedUpdating: (BOOL)flag
1073 {
1074     fJobGroupCountsNeedUpdating = flag;
1075 }
1076
1077 //------------------------------------------------------------------------------------
1078 // Recalculates and stores new values in fPendingCount, fCompletedCount,
1079 // fCanceledCount, and fWorkingCount.
1080 //------------------------------------------------------------------------------------
1081 - (void) recalculateJobGroupCounts
1082 {
1083     fPendingCount = 0;
1084     fCompletedCount = 0;
1085     fCanceledCount = 0;
1086     fWorkingCount = 0;
1087
1088     for( HBJobGroup * aJobGroup in fJobGroups )
1089     {
1090         switch ([aJobGroup status])
1091         {
1092             case HBStatusNone:
1093                 // We don't track these.
1094                 break;
1095             case HBStatusPending:
1096                 fPendingCount++;
1097                 break;
1098             case HBStatusCompleted:
1099                 fCompletedCount++;
1100                 break;
1101             case HBStatusCanceled:
1102                 fCanceledCount++;
1103                 break;
1104             case HBStatusWorking:
1105                 fWorkingCount++;
1106                 break;
1107         }
1108     }
1109     fJobGroupCountsNeedUpdating = NO;
1110 }
1111
1112 //------------------------------------------------------------------------------------
1113 // Returns the number of job groups whose status is HBStatusPending.
1114 //------------------------------------------------------------------------------------
1115 - (unsigned int) pendingCount
1116 {
1117     if (fJobGroupCountsNeedUpdating)
1118         [self recalculateJobGroupCounts];
1119     return fPendingCount;
1120 }
1121
1122 //------------------------------------------------------------------------------------
1123 // Returns the number of job groups whose status is HBStatusCompleted.
1124 //------------------------------------------------------------------------------------
1125 - (unsigned int) completedCount
1126 {
1127     if (fJobGroupCountsNeedUpdating)
1128         [self recalculateJobGroupCounts];
1129     return fCompletedCount;
1130 }
1131
1132 //------------------------------------------------------------------------------------
1133 // Returns the number of job groups whose status is HBStatusCanceled.
1134 //------------------------------------------------------------------------------------
1135 - (unsigned int) canceledCount
1136 {
1137     if (fJobGroupCountsNeedUpdating)
1138         [self recalculateJobGroupCounts];
1139     return fCanceledCount;
1140 }
1141
1142 //------------------------------------------------------------------------------------
1143 // Returns the number of job groups whose status is HBStatusWorking.
1144 //------------------------------------------------------------------------------------
1145 - (unsigned int) workingCount
1146 {
1147     if (fJobGroupCountsNeedUpdating)
1148         [self recalculateJobGroupCounts];
1149     return fWorkingCount;
1150 }
1151
1152 #pragma mark -
1153 #pragma mark UI Updating
1154
1155 //------------------------------------------------------------------------------------
1156 // Saves the state of the items that are currently expanded and selected. Calling
1157 // restoreOutlineViewState will restore the state of all items to match what was saved
1158 // by saveOutlineViewState. Nested calls to saveOutlineViewState are not supported.
1159 //------------------------------------------------------------------------------------
1160 - (void) saveOutlineViewState
1161 {
1162     if (!fSavedExpandedItems)
1163         fSavedExpandedItems = [[NSMutableIndexSet alloc] init];
1164     else
1165         [fSavedExpandedItems removeAllIndexes];
1166
1167     // This code stores the sequence_id of the first job of each job group into an
1168     // index set. This is sufficient to identify each group uniquely.
1169
1170     HBJobGroup * aJobGroup;
1171     for( aJobGroup in fJobGroups )
1172     {
1173         if ([fOutlineView isItemExpanded: aJobGroup])
1174             [fSavedExpandedItems addIndex: [aJobGroup jobAtIndex:0]->sequence_id];
1175     }
1176
1177     // Save the selection also.
1178
1179     if (!fSavedSelectedItems)
1180         fSavedSelectedItems = [[NSMutableIndexSet alloc] init];
1181     else
1182         [fSavedSelectedItems removeAllIndexes];
1183
1184     NSIndexSet * selectedRows = [fOutlineView selectedRowIndexes];
1185     NSInteger row = [selectedRows firstIndex];
1186     while (row != NSNotFound)
1187     {
1188         aJobGroup = [fOutlineView itemAtRow: row];
1189         [fSavedSelectedItems addIndex: [aJobGroup jobAtIndex:0]->sequence_id];
1190         row = [selectedRows indexGreaterThanIndex: row];
1191     }
1192
1193 }
1194
1195 //------------------------------------------------------------------------------------
1196 // Restores the expanded state of items in the outline view to match those saved by a
1197 // previous call to saveOutlineViewState.
1198 //------------------------------------------------------------------------------------
1199 - (void) restoreOutlineViewState
1200 {
1201     if (fSavedExpandedItems)
1202     {
1203         for( HBJobGroup * aJobGroup in fJobGroups )
1204         {
1205             HBJob * job = [aJobGroup jobAtIndex:0];
1206             if (job && [fSavedExpandedItems containsIndex: job->sequence_id])
1207                 [fOutlineView expandItem: aJobGroup];
1208         }
1209     }
1210
1211     if (fSavedSelectedItems)
1212     {
1213         NSMutableIndexSet * rowsToSelect = [[[NSMutableIndexSet alloc] init] autorelease];
1214         NSInteger i = 0;
1215         for( HBJobGroup * aJobGroup in fJobGroups )
1216         {
1217             HBJob * job = [aJobGroup jobAtIndex:0];
1218             if (job && [fSavedSelectedItems containsIndex: job->sequence_id])
1219                 [rowsToSelect addIndex: i];
1220             i++;
1221         }
1222         if ([rowsToSelect count] == 0)
1223             [fOutlineView deselectAll: nil];
1224         else
1225             [fOutlineView selectRowIndexes:rowsToSelect byExtendingSelection:NO];
1226     }
1227 }
1228
1229 //------------------------------------------------------------------------------------
1230 // Marks the icon region of a job group in the queue view as needing display.
1231 //------------------------------------------------------------------------------------
1232 - (void) updateJobGroupIconInQueue:(HBJobGroup*)aJobGroup
1233 {
1234     NSInteger row = [fOutlineView rowForItem: aJobGroup];
1235     NSInteger col = [fOutlineView columnWithIdentifier: @"icon"];
1236     if (row != -1 && col != -1)
1237     {
1238         NSRect frame = [fOutlineView frameOfCellAtColumn:col row:row];
1239         [fOutlineView setNeedsDisplayInRect: frame];
1240     }
1241 }
1242
1243 //------------------------------------------------------------------------------------
1244 // Marks the entire region of a job group in the queue view as needing display.
1245 //------------------------------------------------------------------------------------
1246 - (void) updateJobGroupInQueue:(HBJobGroup*)aJobGroup
1247 {
1248     NSInteger row = [fOutlineView rowForItem: aJobGroup];
1249     if (row != -1)
1250     {
1251         NSRect frame = [fOutlineView rectOfRow:row];
1252         [fOutlineView setNeedsDisplayInRect: frame];
1253     }
1254 }
1255
1256 //------------------------------------------------------------------------------------
1257 // If a job is currently processing, its job icon in the queue outline view is
1258 // animated to its next state.
1259 //------------------------------------------------------------------------------------
1260 - (void) animateCurrentJobGroupInQueue:(NSTimer*)theTimer
1261 {
1262     if (fCurrentJobGroup)
1263     {
1264         fAnimationIndex++;
1265         fAnimationIndex %= 6;   // there are 6 animation images; see outlineView:objectValueForTableColumn:byItem: below.
1266         [self updateJobGroupIconInQueue: fCurrentJobGroup];
1267     }
1268 }
1269
1270 //------------------------------------------------------------------------------------
1271 // Starts animating the job icon of the currently processing job in the queue outline
1272 // view.
1273 //------------------------------------------------------------------------------------
1274 - (void) startAnimatingCurrentJobGroupInQueue
1275 {
1276     if (!fAnimationTimer)
1277         fAnimationTimer = [[NSTimer scheduledTimerWithTimeInterval:1.0/12.0     // 1/12 because there are 6 images in the animation cycle
1278                 target:self
1279                 selector:@selector(animateCurrentJobGroupInQueue:)
1280                 userInfo:nil
1281                 repeats:YES] retain];
1282 }
1283
1284 //------------------------------------------------------------------------------------
1285 // Stops animating the job icon of the currently processing job in the queue outline
1286 // view.
1287 //------------------------------------------------------------------------------------
1288 - (void) stopAnimatingCurrentJobGroupInQueue
1289 {
1290     if (fAnimationTimer && [fAnimationTimer isValid])
1291     {
1292         [fAnimationTimer invalidate];
1293         [fAnimationTimer release];
1294         fAnimationTimer = nil;
1295     }
1296 }
1297
1298 //------------------------------------------------------------------------------------
1299 // Generate string to display in UI.
1300 //------------------------------------------------------------------------------------
1301 - (NSString *) progressStatusStringForJob: (HBJob *)job state: (hb_state_t *)s
1302 {
1303     if (s->state == HB_STATE_WORKING)
1304     {
1305         NSString * msg;
1306         if (job->pass == -1)
1307             msg = NSLocalizedString( @"Deep Scan", nil );
1308         else if (job->pass == 1)
1309             msg = NSLocalizedString( @"Analyzing video", nil );
1310         else if ((job->pass == 0) ||  (job->pass == 2))
1311             msg = NSLocalizedString( @"Encoding movie", nil );
1312         else
1313             return @""; // unknown condition!
1314
1315         if( s->param.working.seconds > -1 )
1316         {
1317             return [NSString stringWithFormat:
1318                 NSLocalizedString( @"%@ (%.2f fps, avg %.2f fps)", nil ),
1319                 msg, s->param.working.rate_cur, s->param.working.rate_avg];
1320         }
1321         else
1322             return msg;
1323
1324     }
1325
1326     else if (s->state == HB_STATE_MUXING)
1327         return NSLocalizedString( @"Muxing", nil );
1328
1329     else if (s->state == HB_STATE_PAUSED)
1330         return NSLocalizedString( @"Paused", nil );
1331
1332     else if (s->state == HB_STATE_WORKDONE)
1333         return NSLocalizedString( @"Done", nil );
1334
1335     return @"";
1336 }
1337
1338 //------------------------------------------------------------------------------------
1339 // Generate string to display in UI.
1340 //------------------------------------------------------------------------------------
1341 - (NSString *) progressTimeRemainingStringForJob: (HBJob *)job state: (hb_state_t *)s
1342 {
1343     if (s->state == HB_STATE_WORKING)
1344     {
1345         #define p s->param.working
1346         if (p.seconds < 0)
1347             return @"";
1348
1349         // Minutes always needed
1350         NSString * minutes;
1351         if (p.minutes > 1)
1352           minutes = [NSString stringWithFormat:NSLocalizedString( @"%d minutes ", nil ), p.minutes];
1353         else if (p.minutes == 1)
1354           minutes = NSLocalizedString( @"1 minute ", nil );
1355         else
1356           minutes = @"";
1357
1358         if (p.hours >= 1)
1359         {
1360             NSString * hours;
1361             if (p.hours > 1)
1362               hours = [NSString stringWithFormat:NSLocalizedString( @"%d hours ", nil ), p.hours];
1363             else
1364               hours = NSLocalizedString( @"1 hour ", nil );
1365
1366             return [NSString stringWithFormat:NSLocalizedString( @"%@%@remaining", nil ), hours, minutes];
1367         }
1368
1369         else
1370         {
1371             NSString * seconds;
1372             if (p.seconds > 1)
1373               seconds = [NSString stringWithFormat:NSLocalizedString( @"%d seconds ", nil ), p.seconds];
1374             else
1375               seconds = NSLocalizedString( @"1 second ", nil );
1376
1377             return [NSString stringWithFormat:NSLocalizedString( @"%@%@remaining", nil ), minutes, seconds];
1378         }
1379
1380 /* here is code that does it more like the Finder
1381         if( p.seconds > -1 )
1382         {
1383             float estHours = (p.hours + (p.minutes / 60.0));
1384             float estMinutes = (p.minutes + (p.seconds / 60.0));
1385
1386             if (estHours > 1.5)
1387                 return [NSString stringWithFormat:NSLocalizedString( @"Time remaining: About %d hours", nil ), lrintf(estHours)];
1388             else if (estHours > 0.983)    // 59 minutes
1389                 return NSLocalizedString( @"Time remaining: About 1 hour", nil );
1390             else if (estMinutes > 1.5)
1391                 return [NSString stringWithFormat:NSLocalizedString( @"Time remaining: About %d minutes", nil ), lrintf(estMinutes)];
1392             else if (estMinutes > 0.983)    // 59 seconds
1393                 return NSLocalizedString( @"Time remaining: About 1 minute", nil );
1394             else if (p.seconds <= 5)
1395                 return NSLocalizedString( @"Time remaining: Less than 5 seconds", nil );
1396             else if (p.seconds <= 10)
1397                 return NSLocalizedString( @"Time remaining: Less than 10 seconds", nil );
1398             else
1399                 return NSLocalizedString( @"Time remaining: Less than 1 minute", nil );
1400         }
1401         else
1402             return NSLocalizedString( @"Time remaining: Calculating...", nil );
1403 */
1404         #undef p
1405     }
1406
1407     return @"";
1408 }
1409
1410 //------------------------------------------------------------------------------------
1411 // Refresh progress bar (fProgressTextField) from current state.
1412 //------------------------------------------------------------------------------------
1413 - (void) updateProgressTextForJob: (HBJob *)job state: (hb_state_t *)s
1414 {
1415     NSString * statusMsg = [self progressStatusStringForJob:job state:s];
1416     NSString * timeMsg = [self progressTimeRemainingStringForJob:job state:s];
1417     if ([timeMsg length] > 0)
1418         statusMsg = [NSString stringWithFormat:@"%@ - %@", statusMsg, timeMsg];
1419     [fProgressTextField setStringValue:statusMsg];
1420 }
1421
1422 //------------------------------------------------------------------------------------
1423 // Refresh progress bar (fProgressBar) from current state.
1424 //------------------------------------------------------------------------------------
1425 - (void) updateProgressBarWithState: (hb_state_t *)s
1426 {
1427     if (s->state == HB_STATE_WORKING)
1428     {
1429         #define p s->param.working
1430         [fProgressBar setIndeterminate:NO];
1431         float progress_total = 100.0 * ( p.progress + p.job_cur - 1 ) / p.job_count;
1432         [fProgressBar setDoubleValue:progress_total];
1433         #undef p
1434     }
1435
1436     else if (s->state == HB_STATE_MUXING)
1437     {
1438         #define p s->param.muxing
1439         [fProgressBar setIndeterminate:YES];
1440         [fProgressBar startAnimation:nil];
1441         #undef p
1442     }
1443
1444     else if (s->state == HB_STATE_WORKDONE)
1445     {
1446         [fProgressBar setIndeterminate:NO];
1447         [fProgressBar stopAnimation:nil];
1448         [fProgressBar setDoubleValue:0.0];
1449     }
1450
1451     else
1452         [fProgressBar stopAnimation:nil];    // just in case in was animating
1453 }
1454
1455 //------------------------------------------------------------------------------------
1456 // Refresh queue count text field (fQueueCountField).
1457 //------------------------------------------------------------------------------------
1458 - (void)updateQueueCountField
1459 {
1460     NSString * msg;
1461     int jobCount = [fJobGroups count];
1462     int pendingCount = [self pendingCount];
1463     if (jobCount == 0)
1464         msg = NSLocalizedString(@"No encodes", nil);
1465     else if ((jobCount == 1) && (pendingCount == 0))
1466         msg = NSLocalizedString(@"1 encode", nil);
1467     else if (jobCount == pendingCount)  // ie, all jobs listed are pending
1468     {
1469         if (jobCount == 1)
1470             msg = NSLocalizedString(@"1 pending encode", nil);
1471         else
1472             msg = [NSString stringWithFormat:NSLocalizedString(@"%d pending encodes", nil), pendingCount];
1473     }
1474     else    // some completed, some pending
1475         msg = [NSString stringWithFormat:NSLocalizedString(@"%d encodes (%d pending)", nil), jobCount, pendingCount];
1476
1477     [fQueueCountField setStringValue:msg];
1478 }
1479
1480 //------------------------------------------------------------------------------------
1481 // Refresh the UI in the current job pane. Should be called whenever the current job
1482 // being processed has changed.
1483 //------------------------------------------------------------------------------------
1484 - (void)updateCurrentJobDescription
1485 {
1486     if (fCurrentJob)
1487     {
1488         switch (fCurrentJob->pass)
1489         {
1490             case -1:  // Subtitle scan
1491                 [fJobDescTextField setAttributedStringValue:
1492                     [fCurrentJob attributedDescriptionWithIcon: NO
1493                                 withTitle: YES
1494                              withPassName: YES
1495                            withFormatInfo: NO
1496                           withDestination: NO
1497                           withPictureInfo: NO
1498                             withVideoInfo: NO
1499                              withx264Info: NO
1500                             withAudioInfo: NO
1501                          withSubtitleInfo: YES]];
1502                 break;
1503
1504             case 1:  // video 1st pass
1505                 [fJobDescTextField setAttributedStringValue:
1506                     [fCurrentJob attributedDescriptionWithIcon: NO
1507                                 withTitle: YES
1508                              withPassName: YES
1509                            withFormatInfo: NO
1510                           withDestination: NO
1511                           withPictureInfo: YES
1512                             withVideoInfo: YES
1513                              withx264Info: YES
1514                             withAudioInfo: NO
1515                          withSubtitleInfo: NO]];
1516                 break;
1517
1518             case 0:  // single pass
1519             case 2:  // video 2nd pass + audio
1520                 [fJobDescTextField setAttributedStringValue:
1521                     [fCurrentJob attributedDescriptionWithIcon: NO
1522                                 withTitle: YES
1523                              withPassName: YES
1524                            withFormatInfo: NO
1525                           withDestination: NO
1526                           withPictureInfo: YES
1527                             withVideoInfo: YES
1528                              withx264Info: YES
1529                             withAudioInfo: YES
1530                          withSubtitleInfo: YES]];
1531                 break;
1532
1533             default: // unknown
1534                 [fJobDescTextField setAttributedStringValue:
1535                     [fCurrentJob attributedDescriptionWithIcon: NO
1536                                 withTitle: YES
1537                              withPassName: YES
1538                            withFormatInfo: NO
1539                           withDestination: NO
1540                           withPictureInfo: YES
1541                             withVideoInfo: YES
1542                              withx264Info: YES
1543                             withAudioInfo: YES
1544                          withSubtitleInfo: YES]];
1545         }
1546     }
1547     else
1548     {
1549         [fJobDescTextField setStringValue: @"No encodes pending"];
1550
1551     }
1552
1553 }
1554
1555 //------------------------------------------------------------------------------------
1556 // Refresh the UI in the current job pane. Should be called whenever the current job
1557 // being processed has changed or when progress has changed.
1558 //------------------------------------------------------------------------------------
1559 - (void)updateCurrentJobProgress
1560 {
1561     hb_state_t s;
1562     hb_get_state2( fHandle, &s );
1563     [self updateProgressTextForJob: fCurrentJob state: &s];
1564     [self updateProgressBarWithState:&s];
1565 }
1566
1567 //------------------------------------------------------------------------------------
1568 // Notifies HBQueuecontroller that the contents of fJobGroups is about to be modified.
1569 // HBQueuecontroller remembers the state of the UI (selection and expanded items).
1570 //------------------------------------------------------------------------------------
1571 - (void) beginEditingJobGroupsArray
1572 {
1573     [self saveOutlineViewState];
1574 }
1575
1576 //------------------------------------------------------------------------------------
1577 // Notifies HBQueuecontroller that modifications to fJobGroups as indicated by a prior
1578 // call to beginEditingJobGroupsArray have been completed. HBQueuecontroller reloads
1579 // the queue view and restores the state of the UI (selection and expanded items).
1580 //------------------------------------------------------------------------------------
1581 - (void) endEditingJobGroupsArray
1582 {
1583     [self setJobGroupCountsNeedUpdating:YES];
1584     [fOutlineView noteNumberOfRowsChanged];
1585     [fOutlineView reloadData];
1586     [self restoreOutlineViewState];
1587     [self updateQueueCountField];
1588 }
1589
1590 #pragma mark -
1591 #pragma mark Actions
1592
1593 //------------------------------------------------------------------------------------
1594 // Deletes the selected jobs from HB and the queue UI
1595 //------------------------------------------------------------------------------------
1596 - (IBAction)removeSelectedJobGroups: (id)sender
1597 {
1598     if (!fHandle) return;
1599
1600     NSIndexSet * selectedRows = [fOutlineView selectedRowIndexes];
1601     NSInteger row = [selectedRows firstIndex];
1602     if (row != NSNotFound)
1603     {
1604         [self beginEditingJobGroupsArray];
1605         while (row != NSNotFound)
1606         {
1607             HBJobGroup * jobGroup = [fOutlineView itemAtRow: row];
1608             switch ([jobGroup status])
1609             {
1610                 case HBStatusCompleted:
1611                 case HBStatusCanceled:
1612                     [fJobGroups removeObject: jobGroup];
1613                     break;
1614                 case HBStatusWorking:
1615                     [self cancelCurrentJob: sender];
1616                     break;
1617                 case HBStatusPending:
1618                     // Remove from libhb
1619                     for( HBJob * job in [jobGroup fJobs] )
1620                     {
1621                         hb_job_t * libhbJob = [self findLibhbJobWithID:job->sequence_id];
1622                         if (libhbJob)
1623                             hb_rem( fHandle, libhbJob );
1624                     }
1625                     // Remove from our list
1626                     [fJobGroups removeObject: jobGroup];
1627                     break;
1628                 case HBStatusNone:
1629                     break;
1630             }
1631
1632             row = [selectedRows indexGreaterThanIndex: row];
1633         }
1634         [self endEditingJobGroupsArray];
1635     }
1636 }
1637
1638 //------------------------------------------------------------------------------------
1639 // Reveals the file icons in the Finder of the selected job groups.
1640 //------------------------------------------------------------------------------------
1641 - (IBAction)revealSelectedJobGroups: (id)sender
1642 {
1643     if (!fHandle) return;
1644
1645     NSIndexSet * selectedRows = [fOutlineView selectedRowIndexes];
1646     NSInteger row = [selectedRows firstIndex];
1647     if (row != NSNotFound)
1648     {
1649         while (row != NSNotFound)
1650         {
1651             HBJobGroup * jobGroup = [fOutlineView itemAtRow: row];
1652             if ([[jobGroup destinationPath] length])
1653                 [[NSWorkspace sharedWorkspace] selectFile:[jobGroup destinationPath] inFileViewerRootedAtPath:nil];
1654
1655             row = [selectedRows indexGreaterThanIndex: row];
1656         }
1657     }
1658 }
1659
1660 //------------------------------------------------------------------------------------
1661 // Calls HBController Cancel: which displays an alert asking user if they want to
1662 // cancel encoding of current job. cancelCurrentJob: returns immediately after posting
1663 // the alert. Later, when the user acknowledges the alert, HBController will call
1664 // libhb to cancel the job.
1665 //------------------------------------------------------------------------------------
1666 - (IBAction)cancelCurrentJob: (id)sender
1667 {
1668     [fHBController Cancel:sender];
1669 }
1670
1671 //------------------------------------------------------------------------------------
1672 // Starts or cancels the processing of jobs depending on the current state
1673 //------------------------------------------------------------------------------------
1674 - (IBAction)toggleStartCancel: (id)sender
1675 {
1676     if (!fHandle) return;
1677
1678     hb_state_t s;
1679     hb_get_state2 (fHandle, &s);
1680
1681     if ((s.state == HB_STATE_PAUSED) || (s.state == HB_STATE_WORKING) || (s.state == HB_STATE_MUXING))
1682         [fHBController Cancel: fQueuePane]; // sender == fQueuePane so that warning alert shows up on queue window
1683
1684     else if ([self pendingCount] > 0)
1685         [fHBController doRip];
1686 }
1687
1688 //------------------------------------------------------------------------------------
1689 // Toggles the pause/resume state of libhb
1690 //------------------------------------------------------------------------------------
1691 - (IBAction)togglePauseResume: (id)sender
1692 {
1693     if (!fHandle) return;
1694
1695     hb_state_t s;
1696     hb_get_state2 (fHandle, &s);
1697
1698     if (s.state == HB_STATE_PAUSED)
1699         hb_resume (fHandle);
1700     else if ((s.state == HB_STATE_WORKING) || (s.state == HB_STATE_MUXING))
1701         hb_pause (fHandle);
1702 }
1703
1704 #pragma mark -
1705 #pragma mark Synchronizing with libhb
1706
1707 //------------------------------------------------------------------------------------
1708 // Queues a job group. The job group's status is set to HBStatusPending.
1709 //------------------------------------------------------------------------------------
1710 - (void) addJobGroup: (HBJobGroup *) aJobGroup
1711 {
1712     NSAssert(![fJobGroups containsObject:aJobGroup], @"Duplicate job group");
1713     [aJobGroup setStatus:HBStatusPending];
1714
1715     [self beginEditingJobGroupsArray];
1716     [fJobGroups addObject:aJobGroup];
1717     [self endEditingJobGroupsArray];
1718 }
1719
1720 //------------------------------------------------------------------------------------
1721 // Notifies HBQueueController that libhb's current job has changed
1722 //------------------------------------------------------------------------------------
1723 - (void)currentJobChanged: (HBJob *) currentJob
1724 {
1725     /* if the job has a destination path, lets perform finished job notifications in fHBController
1726      * We call this here so that we pickup the last job in the queue and single encodes before fCurrentJob
1727      * is released. So for the first job and the beginning of single encodes we check for the existence
1728      * of a valid fCurrentJob jobGroup
1729      */
1730     [currentJob retain];
1731     /* We need to compare the job group to determine if this is the end of a job group
1732      * or just the end of a job within a group to keep from sending encode done notification
1733      * after the first pass in a two pass encode
1734      */
1735     HBJobGroup * theJobGroupCheck = [currentJob jobGroup];
1736     if ((theJobGroupCheck == nil) || (theJobGroupCheck != fCurrentJobGroup))
1737     {
1738         /* we need to make sure that we are not at the beginning of a queue and also that the job hasn't
1739          * been cancelled
1740          */
1741         if ([[fCurrentJob jobGroup] destinationPath] && [fCurrentJobGroup status] != HBStatusCanceled)
1742         {
1743             /* send encode messages to fHBController. User prefs are grokked there. */
1744             [fHBController showGrowlDoneNotification: [[fCurrentJob jobGroup] destinationPath]];
1745             [fHBController sendToMetaX: [[fCurrentJob jobGroup] destinationPath]];
1746         }
1747     }
1748     [fCurrentJob release];
1749     fCurrentJob = currentJob;
1750
1751     // Log info about the preset name. We do this for each job, since libhb logs each
1752     // job separately. The preset name is found in the job's job group object.
1753     if (fCurrentJob && [fCurrentJob jobGroup] && ([[[fCurrentJob jobGroup] presetName] length] > 0))
1754         [fHBController writeToActivityLog: "Using preset: %s", [[[fCurrentJob jobGroup] presetName] UTF8String]];
1755
1756     // Check to see if this is also a change in Job Group
1757
1758     HBJobGroup * theJobGroup = [currentJob jobGroup];
1759     if ((theJobGroup == nil) || (theJobGroup != fCurrentJobGroup))     // no more job groups or start of a new group
1760     {
1761         // Previous job has completed
1762         if (fCurrentJobGroup)
1763         {
1764             // Update the status of the job that just finished. If the user canceled,
1765             // the status will have already been set to canceled by libhbWillStop. So
1766             // all other cases are assumed to be a successful encode. BTW, libhb
1767             // doesn't currently report errors back to the GUI.
1768             if ([fCurrentJobGroup status] != HBStatusCanceled)
1769             {
1770                 [fCurrentJobGroup setStatus:HBStatusCompleted];
1771             }
1772
1773         }
1774
1775         // Set the new group
1776         [self setCurrentJobGroup: theJobGroup];
1777
1778         // Update the UI
1779         [self updateCurrentJobDescription];
1780         [self updateCurrentJobProgress];
1781         [self showCurrentJobPane: fCurrentJobGroup != nil];
1782         if (fCurrentJobGroup)
1783             [self startAnimatingCurrentJobGroupInQueue];
1784         else
1785             [self stopAnimatingCurrentJobGroupInQueue];
1786     }
1787
1788     else    // start a new job/pass in the same group
1789     {
1790         // Update the UI
1791         [self updateCurrentJobDescription];
1792         [self updateCurrentJobProgress];
1793     }
1794
1795 }
1796
1797 //------------------------------------------------------------------------------------
1798 // Notifies HBQueueController that hb_stop is about to be called. This signals us that
1799 // the current job is going to be canceled and deleted. This is somewhat of a hack to
1800 // let HBQueueController know when a job group has been cancelled. Otherwise, we'd
1801 // have no way of knowing if a job was canceled or completed sucessfully.
1802 //------------------------------------------------------------------------------------
1803 - (void)libhbWillStop
1804 {
1805     if (fCurrentJobGroup)
1806         [fCurrentJobGroup setStatus: HBStatusCanceled];
1807 }
1808
1809 //------------------------------------------------------------------------------------
1810 // Notifies HBQueueController that libhb's state has changed
1811 //------------------------------------------------------------------------------------
1812 - (void)libhbStateChanged: (hb_state_t)state
1813 {
1814     switch( state.state )
1815     {
1816         case HB_STATE_WORKING:
1817         {
1818             //NSLog(@"job = %x; job_cur = %d; job_count = %d", state.param.working.sequence_id, state.param.working.job_cur, state.param.working.job_count);
1819             // First check to see if libhb has moved on to another job. We get no direct
1820             // message when this happens, so we have to detect it ourself. The new job could
1821             // be either just the next job in the current group, or the start of a new group.
1822             if (fCurrentJobID != state.param.working.sequence_id)
1823             {
1824                 fCurrentJobID = state.param.working.sequence_id;
1825                 HBJob * currentJob = [self findJobWithID:fCurrentJobID];
1826                 [self currentJobChanged: currentJob];
1827             }
1828
1829             if (fCurrentJob)
1830             {
1831                 [self updateCurrentJobProgress];
1832                 [self startAnimatingCurrentJobGroupInQueue];
1833             }
1834             break;
1835         }
1836
1837         case HB_STATE_MUXING:
1838         {
1839             [self updateCurrentJobProgress];
1840             break;
1841         }
1842
1843         case HB_STATE_PAUSED:
1844         {
1845             [self updateCurrentJobProgress];
1846             [self stopAnimatingCurrentJobGroupInQueue];
1847             break;
1848         }
1849
1850         case HB_STATE_WORKDONE:
1851         {
1852             // HB_STATE_WORKDONE means that libhb has finished processing all the jobs
1853             // in *its* queue. This message is NOT sent as each individual job is
1854             // completed.
1855
1856             [self currentJobChanged: nil];
1857             fCurrentJobID = 0;
1858             break;
1859         }
1860
1861     }
1862
1863 }
1864
1865 #if HB_OUTLINE_METRIC_CONTROLS
1866 static CGFloat spacingWidth = 3.0;
1867 - (IBAction)imageSpacingChanged: (id)sender;
1868 {
1869     spacingWidth = [sender floatValue];
1870     [fOutlineView setNeedsDisplay: YES];
1871 }
1872 - (IBAction)indentChanged: (id)sender
1873 {
1874     [fOutlineView setIndentationPerLevel: [sender floatValue]];
1875     [fOutlineView setNeedsDisplay: YES];
1876 }
1877 #endif
1878
1879 #pragma mark -
1880
1881 //------------------------------------------------------------------------------------
1882 // Receives notification whenever an HBJobGroup's status is changed.
1883 //------------------------------------------------------------------------------------
1884 - (void) jobGroupStatusNotification:(NSNotification *)notification
1885 {
1886     [self setJobGroupCountsNeedUpdating: YES];
1887 //    HBQueueJobGroupStatus oldStatus = (HBQueueJobGroupStatus) [[[notification userInfo] objectForKey:@"HBOldJobGroupStatus"] integerValue];
1888     HBJobGroup * jobGroup = [notification object];
1889     if (jobGroup)
1890         [self updateJobGroupInQueue:jobGroup];
1891     [self updateQueueCountField];
1892 }
1893
1894
1895 #pragma mark -
1896 #pragma mark Toolbar
1897
1898 //------------------------------------------------------------------------------------
1899 // setupToolbar
1900 //------------------------------------------------------------------------------------
1901 - (void)setupToolbar
1902 {
1903     // Create a new toolbar instance, and attach it to our window
1904     NSToolbar *toolbar = [[[NSToolbar alloc] initWithIdentifier: HBQueueToolbar] autorelease];
1905
1906     // Set up toolbar properties: Allow customization, give a default display mode, and remember state in user defaults
1907     [toolbar setAllowsUserCustomization: YES];
1908     [toolbar setAutosavesConfiguration: YES];
1909     [toolbar setDisplayMode: NSToolbarDisplayModeIconAndLabel];
1910
1911     // We are the delegate
1912     [toolbar setDelegate: self];
1913
1914     // Attach the toolbar to our window
1915     [[self window] setToolbar:toolbar];
1916 }
1917
1918 //------------------------------------------------------------------------------------
1919 // toolbar:itemForItemIdentifier:willBeInsertedIntoToolbar:
1920 //------------------------------------------------------------------------------------
1921 - (NSToolbarItem *)toolbar:(NSToolbar *)toolbar
1922         itemForItemIdentifier:(NSString *)itemIdentifier
1923         willBeInsertedIntoToolbar:(BOOL)flag
1924 {
1925     // Required delegate method: Given an item identifier, this method returns an item.
1926     // The toolbar will use this method to obtain toolbar items that can be displayed
1927     // in the customization sheet, or in the toolbar itself.
1928
1929     NSToolbarItem *toolbarItem = nil;
1930
1931     if ([itemIdentifier isEqual: HBQueueStartCancelToolbarIdentifier])
1932     {
1933         toolbarItem = [[[NSToolbarItem alloc] initWithItemIdentifier: itemIdentifier] autorelease];
1934
1935         // Set the text label to be displayed in the toolbar and customization palette
1936         [toolbarItem setLabel: @"Start"];
1937         [toolbarItem setPaletteLabel: @"Start/Cancel"];
1938
1939         // Set up a reasonable tooltip, and image
1940         [toolbarItem setToolTip: @"Start Encoding"];
1941         [toolbarItem setImage: [NSImage imageNamed: @"Play"]];
1942
1943         // Tell the item what message to send when it is clicked
1944         [toolbarItem setTarget: self];
1945         [toolbarItem setAction: @selector(toggleStartCancel:)];
1946     }
1947
1948     if ([itemIdentifier isEqual: HBQueuePauseResumeToolbarIdentifier])
1949     {
1950         toolbarItem = [[[NSToolbarItem alloc] initWithItemIdentifier: itemIdentifier] autorelease];
1951
1952         // Set the text label to be displayed in the toolbar and customization palette
1953         [toolbarItem setLabel: @"Pause"];
1954         [toolbarItem setPaletteLabel: @"Pause/Resume"];
1955
1956         // Set up a reasonable tooltip, and image
1957         [toolbarItem setToolTip: @"Pause Encoding"];
1958         [toolbarItem setImage: [NSImage imageNamed: @"Pause"]];
1959
1960         // Tell the item what message to send when it is clicked
1961         [toolbarItem setTarget: self];
1962         [toolbarItem setAction: @selector(togglePauseResume:)];
1963     }
1964
1965     return toolbarItem;
1966 }
1967
1968 //------------------------------------------------------------------------------------
1969 // toolbarDefaultItemIdentifiers:
1970 //------------------------------------------------------------------------------------
1971 - (NSArray *) toolbarDefaultItemIdentifiers: (NSToolbar *) toolbar
1972 {
1973     // Required delegate method: Returns the ordered list of items to be shown in the
1974     // toolbar by default.
1975
1976     return [NSArray arrayWithObjects:
1977         HBQueueStartCancelToolbarIdentifier,
1978         HBQueuePauseResumeToolbarIdentifier,
1979         nil];
1980 }
1981
1982 //------------------------------------------------------------------------------------
1983 // toolbarAllowedItemIdentifiers:
1984 //------------------------------------------------------------------------------------
1985 - (NSArray *) toolbarAllowedItemIdentifiers: (NSToolbar *) toolbar
1986 {
1987     // Required delegate method: Returns the list of all allowed items by identifier.
1988     // By default, the toolbar does not assume any items are allowed, even the
1989     // separator. So, every allowed item must be explicitly listed.
1990
1991     return [NSArray arrayWithObjects:
1992         HBQueueStartCancelToolbarIdentifier,
1993         HBQueuePauseResumeToolbarIdentifier,
1994         NSToolbarCustomizeToolbarItemIdentifier,
1995         NSToolbarFlexibleSpaceItemIdentifier,
1996         NSToolbarSpaceItemIdentifier,
1997         NSToolbarSeparatorItemIdentifier,
1998         nil];
1999 }
2000
2001 //------------------------------------------------------------------------------------
2002 // validateToolbarItem:
2003 //------------------------------------------------------------------------------------
2004 - (BOOL) validateToolbarItem: (NSToolbarItem *) toolbarItem
2005 {
2006     // Optional method: This message is sent to us since we are the target of some
2007     // toolbar item actions.
2008
2009     if (!fHandle) return NO;
2010
2011     BOOL enable = NO;
2012
2013     hb_state_t s;
2014     hb_get_state2 (fHandle, &s);
2015
2016     if ([[toolbarItem itemIdentifier] isEqual: HBQueueStartCancelToolbarIdentifier])
2017     {
2018         if ((s.state == HB_STATE_PAUSED) || (s.state == HB_STATE_WORKING) || (s.state == HB_STATE_MUXING))
2019         {
2020             enable = YES;
2021             [toolbarItem setImage:[NSImage imageNamed: @"Stop"]];
2022             [toolbarItem setLabel: @"Stop"];
2023             [toolbarItem setToolTip: @"Stop Encoding"];
2024         }
2025
2026         else if ([self pendingCount] > 0)
2027         {
2028             enable = YES;
2029             [toolbarItem setImage:[NSImage imageNamed: @"Play"]];
2030             [toolbarItem setLabel: @"Start"];
2031             [toolbarItem setToolTip: @"Start Encoding"];
2032         }
2033
2034         else
2035         {
2036             enable = NO;
2037             [toolbarItem setImage:[NSImage imageNamed: @"Play"]];
2038             [toolbarItem setLabel: @"Start"];
2039             [toolbarItem setToolTip: @"Start Encoding"];
2040         }
2041     }
2042
2043     if ([[toolbarItem itemIdentifier] isEqual: HBQueuePauseResumeToolbarIdentifier])
2044     {
2045         if (s.state == HB_STATE_PAUSED)
2046         {
2047             enable = YES;
2048             [toolbarItem setImage:[NSImage imageNamed: @"Play"]];
2049             [toolbarItem setLabel: @"Resume"];
2050             [toolbarItem setToolTip: @"Resume Encoding"];
2051        }
2052
2053         else if ((s.state == HB_STATE_WORKING) || (s.state == HB_STATE_MUXING))
2054         {
2055             enable = YES;
2056             [toolbarItem setImage:[NSImage imageNamed: @"Pause"]];
2057             [toolbarItem setLabel: @"Pause"];
2058             [toolbarItem setToolTip: @"Pause Encoding"];
2059         }
2060         else
2061         {
2062             enable = NO;
2063             [toolbarItem setImage:[NSImage imageNamed: @"Pause"]];
2064             [toolbarItem setLabel: @"Pause"];
2065             [toolbarItem setToolTip: @"Pause Encoding"];
2066         }
2067     }
2068
2069     return enable;
2070 }
2071
2072 #pragma mark -
2073
2074 //------------------------------------------------------------------------------------
2075 // awakeFromNib
2076 //------------------------------------------------------------------------------------
2077 - (void)awakeFromNib
2078 {
2079     [self setupToolbar];
2080
2081     if( ![[self window] setFrameUsingName:@"Queue"] )
2082         [[self window] center];
2083     [self setWindowFrameAutosaveName:@"Queue"];
2084     [[self window] setExcludedFromWindowsMenu:YES];
2085
2086 #if HB_QUEUE_DRAGGING
2087     [fOutlineView registerForDraggedTypes: [NSArray arrayWithObject:HBQueuePboardType] ];
2088     [fOutlineView setDraggingSourceOperationMask:NSDragOperationEvery forLocal:YES];
2089     [fOutlineView setVerticalMotionCanBeginDrag: YES];
2090 #endif
2091
2092     // Don't allow autoresizing of main column, else the "delete" column will get
2093     // pushed out of view.
2094     [fOutlineView setAutoresizesOutlineColumn: NO];
2095
2096 #if HB_OUTLINE_METRIC_CONTROLS
2097     [fIndentation setHidden: NO];
2098     [fSpacing setHidden: NO];
2099     [fIndentation setIntegerValue:[fOutlineView indentationPerLevel]];  // debug
2100     [fSpacing setIntegerValue:3];       // debug
2101 #endif
2102
2103     // Show/hide UI elements
2104     fCurrentJobPaneShown = YES;     // it's shown in the nib
2105     [self showCurrentJobPane:NO];
2106
2107     [self updateQueueCountField];
2108 }
2109
2110
2111 //------------------------------------------------------------------------------------
2112 // windowWillClose
2113 //------------------------------------------------------------------------------------
2114 - (void)windowWillClose:(NSNotification *)aNotification
2115 {
2116     [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"QueueWindowIsOpen"];
2117 }
2118
2119 #pragma mark -
2120
2121 - (void)moveObjectsInArray:(NSMutableArray *)array fromIndexes:(NSIndexSet *)indexSet toIndex:(NSUInteger)insertIndex
2122 {
2123     NSUInteger index = [indexSet lastIndex];
2124     NSUInteger aboveInsertIndexCount = 0;
2125
2126     while (index != NSNotFound)
2127     {
2128         NSUInteger removeIndex;
2129
2130         if (index >= insertIndex)
2131         {
2132             removeIndex = index + aboveInsertIndexCount;
2133             aboveInsertIndexCount++;
2134         }
2135         else
2136         {
2137             removeIndex = index;
2138             insertIndex--;
2139         }
2140
2141         id object = [[array objectAtIndex:removeIndex] retain];
2142         [array removeObjectAtIndex:removeIndex];
2143         [array insertObject:object atIndex:insertIndex];
2144         [object release];
2145
2146         index = [indexSet indexLessThanIndex:index];
2147     }
2148 }
2149
2150 #pragma mark -
2151 #pragma mark NSOutlineView delegate
2152
2153 - (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item
2154 {
2155     if (item == nil)
2156         return [fJobGroups objectAtIndex:index];
2157
2158     // We are only one level deep, so we can't be asked about children
2159     NSAssert (NO, @"HBQueueController outlineView:child:ofItem: can't handle nested items.");
2160     return nil;
2161 }
2162
2163 - (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item
2164 {
2165     // Our outline view has no levels, but we can still expand every item. Doing so
2166     // just makes the row taller. See heightOfRowByItem below.
2167     return YES;
2168 }
2169
2170 - (BOOL)outlineView:(NSOutlineView *)outlineView shouldExpandItem:(id)item
2171 {
2172     // Our outline view has no levels, but we can still expand every item. Doing so
2173     // just makes the row taller. See heightOfRowByItem below.
2174 #if HB_QUEUE_DRAGGING
2175     // Don't autoexpand while dragging, since we can't drop into the items
2176     return ![(HBQueueOutlineView*)outlineView isDragging];
2177 #else
2178     return YES;
2179 #endif
2180 }
2181
2182 - (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item
2183 {
2184     // Our outline view has no levels, so number of children will be zero for all
2185     // top-level items.
2186     if (item == nil)
2187         return [fJobGroups count];
2188     else
2189         return 0;
2190 }
2191
2192 - (void)outlineViewItemDidCollapse:(NSNotification *)notification
2193 {
2194     id item = [[notification userInfo] objectForKey:@"NSObject"];
2195     NSInteger row = [fOutlineView rowForItem:item];
2196     [fOutlineView noteHeightOfRowsWithIndexesChanged:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(row,1)]];
2197 }
2198
2199 - (void)outlineViewItemDidExpand:(NSNotification *)notification
2200 {
2201     id item = [[notification userInfo] objectForKey:@"NSObject"];
2202     NSInteger row = [fOutlineView rowForItem:item];
2203     [fOutlineView noteHeightOfRowsWithIndexesChanged:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(row,1)]];
2204 }
2205
2206 - (CGFloat)outlineView:(NSOutlineView *)outlineView heightOfRowByItem:(id)item
2207 {
2208     if ([outlineView isItemExpanded: item])
2209     {
2210         // Short-circuit here if in a live resize primarily to fix a bug but also to
2211         // increase resposivness during a resize. There's a bug in NSTableView that
2212         // causes row heights to get messed up if you try to change them during a live
2213         // resize. So if in a live resize, simply return the previously calculated
2214         // height. The row heights will get fixed up after the resize because we have
2215         // implemented viewDidEndLiveResize to force all of them to be recalculated.
2216         if ([outlineView inLiveResize] && [item lastDescriptionHeight] > 0)
2217             return [item lastDescriptionHeight];
2218
2219         CGFloat width = [[outlineView tableColumnWithIdentifier: @"desc"] width];
2220         // Column width is NOT what is ultimately used. I can't quite figure out what
2221         // width to use for calculating text metrics. No matter how I tweak this value,
2222         // there are a few conditions in which the drawn text extends below the bounds
2223         // of the row cell. In previous versions, which ran under Tiger, I was
2224         // reducing width by 47 pixles.
2225         width -= 2;     // (?) for intercell spacing
2226
2227         CGFloat height = [item heightOfDescriptionForWidth: width];
2228         return height;
2229     }
2230     else
2231         return HB_ROW_HEIGHT_TITLE_ONLY;
2232 }
2233
2234 - (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item
2235 {
2236     // nb: The "desc" column is currently an HBImageAndTextCell. However, we are longer
2237     // using the image portion of the cell so we could switch back to a regular NSTextFieldCell.
2238
2239     if ([[tableColumn identifier] isEqualToString:@"desc"])
2240         return [item attributedDescription];
2241     else if ([[tableColumn identifier] isEqualToString:@"icon"])
2242     {
2243         switch ([(HBJobGroup*)item status])
2244         {
2245             case HBStatusCanceled:
2246                 return [NSImage imageNamed:@"EncodeCanceled"];
2247                 break;
2248             case HBStatusCompleted:
2249                 return [NSImage imageNamed:@"EncodeComplete"];
2250                 break;
2251             case HBStatusWorking:
2252                 return [NSImage imageNamed: [NSString stringWithFormat: @"EncodeWorking%d", fAnimationIndex]];
2253                 break;
2254             default:
2255                 return [NSImage imageNamed:@"JobSmall"];
2256                 break;
2257         }
2258     }
2259     else
2260         return @"";
2261 }
2262
2263 - (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item
2264 {
2265     if ([[tableColumn identifier] isEqualToString:@"desc"])
2266     {
2267 #if HB_OUTLINE_METRIC_CONTROLS
2268         NSSize theSize = [cell imageSpacing];
2269         theSize.width = spacingWidth;
2270         [cell setImageSpacing: theSize];
2271 #endif
2272
2273         // nb: The "desc" column is currently an HBImageAndTextCell. However, we are longer
2274         // using the image portion of the cell so we could switch back to a regular NSTextFieldCell.
2275
2276         // Set the image here since the value returned from outlineView:objectValueForTableColumn: didn't specify the image part
2277         [cell setImage:nil];
2278     }
2279
2280     else if ([[tableColumn identifier] isEqualToString:@"action"])
2281     {
2282         [cell setEnabled: YES];
2283         BOOL highlighted = [outlineView isRowSelected:[outlineView rowForItem: item]] && [[outlineView window] isKeyWindow] && ([[outlineView window] firstResponder] == outlineView);
2284         if ([(HBJobGroup*)item status] == HBStatusCompleted)
2285         {
2286             [cell setAction: @selector(revealSelectedJobGroups:)];
2287             if (highlighted)
2288             {
2289                 [cell setImage:[NSImage imageNamed:@"RevealHighlight"]];
2290                 [cell setAlternateImage:[NSImage imageNamed:@"RevealHighlightPressed"]];
2291             }
2292             else
2293                 [cell setImage:[NSImage imageNamed:@"Reveal"]];
2294         }
2295         else
2296         {
2297             [cell setAction: @selector(removeSelectedJobGroups:)];
2298             if (highlighted)
2299             {
2300                 [cell setImage:[NSImage imageNamed:@"DeleteHighlight"]];
2301                 [cell setAlternateImage:[NSImage imageNamed:@"DeleteHighlightPressed"]];
2302             }
2303             else
2304                 [cell setImage:[NSImage imageNamed:@"Delete"]];
2305         }
2306     }
2307 }
2308
2309 - (void)outlineView:(NSOutlineView *)outlineView willDisplayOutlineCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item
2310 {
2311     // By default, the discolsure image gets centered vertically in the cell. We want
2312     // always at the top.
2313     if ([outlineView isItemExpanded: item])
2314         [cell setImagePosition: NSImageAbove];
2315     else
2316         [cell setImagePosition: NSImageOnly];
2317 }
2318
2319 #pragma mark -
2320 #pragma mark NSOutlineView delegate (dragging related)
2321
2322 //------------------------------------------------------------------------------------
2323 // NSTableView delegate
2324 //------------------------------------------------------------------------------------
2325
2326 #if HB_QUEUE_DRAGGING
2327 - (BOOL)outlineView:(NSOutlineView *)outlineView writeItems:(NSArray *)items toPasteboard:(NSPasteboard *)pboard
2328 {
2329     // Dragging is only allowed of the pending items.
2330     for( HBJobGroup * group in items )
2331     {
2332         if ([group status] != HBStatusPending)
2333             return NO;
2334     }
2335
2336     // Don't retain since this is just holding temporaral drag information, and it is
2337     //only used during a drag!  We could put this in the pboard actually.
2338     fDraggedNodes = items;
2339
2340     // Provide data for our custom type, and simple NSStrings.
2341     [pboard declareTypes:[NSArray arrayWithObjects: HBQueuePboardType, nil] owner:self];
2342
2343     // the actual data doesn't matter since DragDropSimplePboardType drags aren't recognized by anyone but us!.
2344     [pboard setData:[NSData data] forType:HBQueuePboardType];
2345
2346     return YES;
2347 }
2348 #endif
2349
2350 #if HB_QUEUE_DRAGGING
2351 - (NSDragOperation)outlineView:(NSOutlineView *)outlineView validateDrop:(id <NSDraggingInfo>)info proposedItem:(id)item proposedChildIndex:(NSInteger)index
2352 {
2353     // Don't allow dropping ONTO an item since they can't really contain any children.
2354     BOOL isOnDropTypeProposal = index == NSOutlineViewDropOnItemIndex;
2355     if (isOnDropTypeProposal)
2356         return NSDragOperationNone;
2357
2358     // Don't allow dropping INTO an item since they can't really contain any children.
2359     if (item != nil)
2360     {
2361         index = [fOutlineView rowForItem: item] + 1;
2362         item = nil;
2363     }
2364
2365     // Prevent dragging into the completed or current job.
2366     int firstPendingIndex = [self completedCount];
2367     if (fCurrentJobGroup)
2368         firstPendingIndex++;
2369     index = MAX (index, firstPendingIndex);
2370
2371     [outlineView setDropItem:item dropChildIndex:index];
2372     return NSDragOperationGeneric;
2373 }
2374 #endif
2375
2376 #if HB_QUEUE_DRAGGING
2377 - (BOOL)outlineView:(NSOutlineView *)outlineView acceptDrop:(id <NSDraggingInfo>)info item:(id)item childIndex:(NSInteger)index
2378 {
2379     NSMutableIndexSet *moveItems = [NSMutableIndexSet indexSet];
2380
2381     for( id obj in fDraggedNodes)
2382     {
2383         [moveItems addIndex:[fJobGroups indexOfObject:obj]];
2384     }
2385
2386     // Rearrange the data and view
2387     [self saveOutlineViewState];
2388     [self moveObjectsInArray:fJobGroups fromIndexes:moveItems toIndex: index];
2389     [fOutlineView reloadData];
2390     [self restoreOutlineViewState];
2391
2392     return YES;
2393 }
2394 #endif
2395
2396
2397 @end