OSDN Git Service

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