OSDN Git Service

MacGui: Use nil instead of NULL. Patch by blindjimmy. http://handbrake.djgraphite...
[handbrake-jp/handbrake-jp-git.git] / macosx / HBQueueController.mm
1 /* HBQueueController
2
3     This file is part of the HandBrake source code.
4     Homepage: <http://handbrake.fr/>.
5     It may be used under the terms of the GNU General Public License. */
6
7 #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 = nil;
104 static NSDictionary* _detailAttribute = nil;
105 static NSDictionary* _detailBoldAttribute = nil;
106 static NSDictionary* _titleAttribute = nil;
107 static NSDictionary* _shortHeightAttribute = nil;
108
109 @implementation HBJob
110
111 + (HBJob*) jobWithLibhbJob: (hb_job_t *) job
112 {
113     return [[[HBJob alloc] initWithLibhbJob:job] autorelease];
114 }
115
116 - (id) initWithLibhbJob: (hb_job_t *) job
117 {
118     if (self = [super init])
119     {
120         sequence_id = job->sequence_id;
121
122         chapter_start = job->chapter_start;
123         chapter_end = job->chapter_end;
124         chapter_markers = job->chapter_markers;
125         memcpy(crop, job->crop, sizeof(crop));
126         deinterlace = job->deinterlace;
127         width = job->width;
128         height = job->height;
129         keep_ratio = job->keep_ratio;
130         grayscale = job->grayscale;
131         pixel_ratio = job->pixel_ratio;
132         pixel_aspect_width = job->pixel_aspect_width;
133         pixel_aspect_height = job->pixel_aspect_height;
134         vcodec = job->vcodec;
135         vquality = job->vquality;
136         vbitrate = job->vbitrate;
137         vrate = job->vrate;
138         vrate_base = job->vrate_base;
139         pass = job->pass;
140         h264_level = job->h264_level;
141         crf = job->crf;
142         if (job->x264opts)
143             x264opts = [[NSString stringWithUTF8String:job->x264opts] retain];
144         /* So, with the advent of job->list_audio's I decided why not just use an NSString and concatanate
145          all of the info we need for all of the audio values to display right into an NSString here ? So I
146          did. I have no idea why we are reading libhb stuff just to display it in the queue gui. So here we
147          are with a huge string. But its easy to change and saves alot of messing about. Maybe we move a bunch
148          of other display stuff into strings for display for each job. It's not like they have to actually do
149          anything.*/
150         hb_audio_config_t * audio;
151         NSString * thisJobAudioCodecs = [NSString stringWithFormat:@""];
152         NSString * thisJobAudioInfo = [NSString stringWithFormat:@""];
153         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             NSUInteger 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 - (CGFloat) heightOfDescriptionForWidth:(CGFloat)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 - (CGFloat) 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 initWithWindowNibName:@"Queue"])
885     {
886         // NSWindowController likes to lazily load its window nib. Since this
887         // controller tries to touch the outlets before accessing the window, we
888         // need to force it to load immadiately by invoking its accessor.
889         //
890         // If/when we switch to using bindings, this can probably go away.
891         [self window];
892
893         // Our defaults
894         [[NSUserDefaults standardUserDefaults] registerDefaults:[NSDictionary dictionaryWithObjectsAndKeys:
895             @"NO",      @"QueueWindowIsOpen",
896             @"NO",      @"QueueShowsDetail",
897             @"YES",     @"QueueShowsJobsAsGroups",
898             nil]];
899
900         fJobGroups = [[NSMutableArray arrayWithCapacity:0] retain];
901
902         // Register for HBJobGroup status changes
903         [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(jobGroupStatusNotification:) name:HBJobGroupStatusNotification object:nil];
904     }
905     return self;
906 }
907
908 //------------------------------------------------------------------------------------
909 // dealloc
910 //------------------------------------------------------------------------------------
911 - (void)dealloc
912 {
913     // clear the delegate so that windowWillClose is not attempted
914     if( [[self window] delegate] == self )
915         [[self window] setDelegate:nil];
916
917     [fJobGroups release];
918     [fCurrentJobGroup release];
919     [fSavedExpandedItems release];
920     [fSavedSelectedItems release];
921
922     [[NSNotificationCenter defaultCenter] removeObserver:self];
923
924     [super dealloc];
925 }
926
927 //------------------------------------------------------------------------------------
928 // Receive HB handle
929 //------------------------------------------------------------------------------------
930 - (void)setHandle: (hb_handle_t *)handle
931 {
932     fHandle = handle;
933 }
934
935 //------------------------------------------------------------------------------------
936 // Receive HBController
937 //------------------------------------------------------------------------------------
938 - (void)setHBController: (HBController *)controller
939 {
940     fHBController = controller;
941 }
942
943 #pragma mark -
944 #pragma mark - Getting the currently processing job group
945
946 //------------------------------------------------------------------------------------
947 // Returns the HBJobGroup that is currently being encoded; nil if no encoding is
948 // occurring.
949 //------------------------------------------------------------------------------------
950 - (HBJobGroup *) currentJobGroup;
951 {
952     return fCurrentJobGroup;
953 }
954
955 //------------------------------------------------------------------------------------
956 // Returns the HBJob (pass) that is currently being encoded; nil if no encoding is
957 // occurring.
958 //------------------------------------------------------------------------------------
959 - (HBJob *) currentJob
960 {
961     return fCurrentJob;
962 }
963
964 #pragma mark -
965
966 //------------------------------------------------------------------------------------
967 // Displays and brings the queue window to the front
968 //------------------------------------------------------------------------------------
969 - (IBAction) showQueueWindow: (id)sender
970 {
971     [self showWindow:sender];
972     [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"QueueWindowIsOpen"];
973 }
974
975 //------------------------------------------------------------------------------------
976 // Show or hide the current job pane (fCurrentJobPane).
977 //------------------------------------------------------------------------------------
978 - (void) showCurrentJobPane: (BOOL)showPane
979 {
980     if (showPane == fCurrentJobPaneShown)
981         return;
982     
983     // Things to keep in mind:
984     // - When the current job pane is shown, it occupies the upper portion of the
985     //   window with the queue occupying the bottom portion of the window.
986     // - When the current job pane is hidden, it slides up and out of view.
987     //   NSView setHidden is NOT used. The queue pane is resized to occupy the full
988     //   window.
989     
990     NSRect windowFrame = [[fCurrentJobPane superview] frame];
991     NSRect queueFrame, jobFrame;
992     if (showPane)
993         NSDivideRect(windowFrame, &jobFrame, &queueFrame, NSHeight([fCurrentJobPane frame]), NSMaxYEdge);
994     else
995     {
996         queueFrame = windowFrame;
997         jobFrame = [fCurrentJobPane frame];
998         jobFrame.origin.y = NSHeight(windowFrame);
999     }
1000     
1001     // Move fCurrentJobPane
1002     NSDictionary * dict1 = [NSDictionary dictionaryWithObjectsAndKeys:
1003         fCurrentJobPane, NSViewAnimationTargetKey,
1004         [NSValue valueWithRect:jobFrame], NSViewAnimationEndFrameKey,
1005         nil];
1006
1007     // Resize fQueuePane
1008     NSDictionary * dict2 = [NSDictionary dictionaryWithObjectsAndKeys:
1009         fQueuePane, NSViewAnimationTargetKey,
1010         [NSValue valueWithRect:queueFrame], NSViewAnimationEndFrameKey,
1011         nil];
1012
1013     NSViewAnimation * anAnimation = [[[NSViewAnimation alloc] initWithViewAnimations:nil] autorelease];
1014     [anAnimation setViewAnimations:[NSArray arrayWithObjects:dict1, dict2, nil]];
1015     [anAnimation setDuration:0.25];
1016     [anAnimation setAnimationBlockingMode:NSAnimationBlocking]; // prevent user from resizing the window during an animation
1017     [anAnimation startAnimation];
1018     
1019     fCurrentJobPaneShown = showPane;
1020 }
1021
1022 //------------------------------------------------------------------------------------
1023 // Sets fCurrentJobGroup to a new job group.
1024 //------------------------------------------------------------------------------------
1025 - (void) setCurrentJobGroup: (HBJobGroup *)aJobGroup
1026 {
1027     if (aJobGroup)
1028         [aJobGroup setStatus: HBStatusWorking];
1029
1030     [aJobGroup retain];
1031     [fCurrentJobGroup release];
1032     fCurrentJobGroup = aJobGroup;
1033 }
1034
1035 #pragma mark - Finding job groups
1036
1037 //------------------------------------------------------------------------------------
1038 // Returns the first pending job with a specified destination path or nil if no such
1039 // job exists.
1040 //------------------------------------------------------------------------------------
1041 - (HBJobGroup *) pendingJobGroupWithDestinationPath: (NSString *)path
1042 {
1043     HBJobGroup * aJobGroup;
1044     NSEnumerator * groupEnum = [fJobGroups objectEnumerator];
1045     while ( (aJobGroup = [groupEnum nextObject]) )
1046     {
1047         if ([[aJobGroup destinationPath] isEqualToString: path])
1048             return aJobGroup;
1049     }
1050     return nil;
1051 }
1052
1053 //------------------------------------------------------------------------------------
1054 // Locates and returns a HBJob whose sequence_id matches a specified value.
1055 //------------------------------------------------------------------------------------
1056 - (HBJob *) findJobWithID: (int)aJobID
1057 {
1058     HBJobGroup * aJobGroup;
1059     NSEnumerator * groupEnum = [fJobGroups objectEnumerator];
1060     while ( (aJobGroup = [groupEnum nextObject]) )
1061     {
1062         HBJob * job;
1063         NSEnumerator * jobEnum = [aJobGroup jobEnumerator];
1064         while ( (job = [jobEnum nextObject]) )
1065         {
1066             if (job->sequence_id == aJobID)
1067                 return job;
1068         }
1069     }
1070     return nil;
1071 }
1072
1073 //------------------------------------------------------------------------------------
1074 // Locates and returns a libhb job whose sequence_id matches a specified value.
1075 //------------------------------------------------------------------------------------
1076 - (hb_job_t *) findLibhbJobWithID: (int)aJobID
1077 {
1078     hb_job_t * job;
1079     int index = 0;
1080     while( ( job = hb_job( fHandle, index++ ) ) )
1081     {
1082         if (job->sequence_id == aJobID)
1083             return job;
1084     }
1085     return nil;
1086 }
1087
1088 #pragma mark -
1089 #pragma mark Queue Counts
1090
1091 //------------------------------------------------------------------------------------
1092 // Sets a flag indicating that the values for fPendingCount, fCompletedCount,
1093 // fCanceledCount, and fWorkingCount need to be recalculated.
1094 //------------------------------------------------------------------------------------
1095 - (void) setJobGroupCountsNeedUpdating: (BOOL)flag
1096 {
1097     fJobGroupCountsNeedUpdating = flag;
1098 }
1099
1100 //------------------------------------------------------------------------------------
1101 // Recalculates and stores new values in fPendingCount, fCompletedCount,
1102 // fCanceledCount, and fWorkingCount.
1103 //------------------------------------------------------------------------------------
1104 - (void) recalculateJobGroupCounts
1105 {
1106     fPendingCount = 0;
1107     fCompletedCount = 0;
1108     fCanceledCount = 0;
1109     fWorkingCount = 0;
1110
1111     NSEnumerator * groupEnum = [fJobGroups objectEnumerator];
1112     HBJobGroup * aJobGroup;
1113     while ( (aJobGroup = [groupEnum nextObject]) )
1114     {
1115         switch ([aJobGroup status])
1116         {
1117             case HBStatusNone:
1118                 // We don't track these.
1119                 break;
1120             case HBStatusPending:
1121                 fPendingCount++;
1122                 break;
1123             case HBStatusCompleted:
1124                 fCompletedCount++;
1125                 break;
1126             case HBStatusCanceled:
1127                 fCanceledCount++;
1128                 break;
1129             case HBStatusWorking:
1130                 fWorkingCount++;
1131                 break;
1132         }
1133     }
1134     fJobGroupCountsNeedUpdating = NO;
1135 }
1136
1137 //------------------------------------------------------------------------------------
1138 // Returns the number of job groups whose status is HBStatusPending.
1139 //------------------------------------------------------------------------------------
1140 - (unsigned int) pendingCount
1141 {
1142     if (fJobGroupCountsNeedUpdating)
1143         [self recalculateJobGroupCounts];
1144     return fPendingCount;
1145 }
1146
1147 //------------------------------------------------------------------------------------
1148 // Returns the number of job groups whose status is HBStatusCompleted.
1149 //------------------------------------------------------------------------------------
1150 - (unsigned int) completedCount
1151 {
1152     if (fJobGroupCountsNeedUpdating)
1153         [self recalculateJobGroupCounts];
1154     return fCompletedCount;
1155 }
1156
1157 //------------------------------------------------------------------------------------
1158 // Returns the number of job groups whose status is HBStatusCanceled.
1159 //------------------------------------------------------------------------------------
1160 - (unsigned int) canceledCount
1161 {
1162     if (fJobGroupCountsNeedUpdating)
1163         [self recalculateJobGroupCounts];
1164     return fCanceledCount;
1165 }
1166
1167 //------------------------------------------------------------------------------------
1168 // Returns the number of job groups whose status is HBStatusWorking.
1169 //------------------------------------------------------------------------------------
1170 - (unsigned int) workingCount
1171 {
1172     if (fJobGroupCountsNeedUpdating)
1173         [self recalculateJobGroupCounts];
1174     return fWorkingCount;
1175 }
1176
1177 #pragma mark -
1178 #pragma mark UI Updating
1179
1180 //------------------------------------------------------------------------------------
1181 // Saves the state of the items that are currently expanded and selected. Calling
1182 // restoreOutlineViewState will restore the state of all items to match what was saved
1183 // by saveOutlineViewState. Nested calls to saveOutlineViewState are not supported.
1184 //------------------------------------------------------------------------------------
1185 - (void) saveOutlineViewState
1186 {
1187     if (!fSavedExpandedItems)
1188         fSavedExpandedItems = [[NSMutableIndexSet alloc] init];
1189     else
1190         [fSavedExpandedItems removeAllIndexes];
1191     
1192     // This code stores the sequence_id of the first job of each job group into an
1193     // index set. This is sufficient to identify each group uniquely.
1194     
1195     HBJobGroup * aJobGroup;
1196     NSEnumerator * e = [fJobGroups objectEnumerator];
1197     while ( (aJobGroup = [e nextObject]) )
1198     {
1199         if ([fOutlineView isItemExpanded: aJobGroup])
1200             [fSavedExpandedItems addIndex: [aJobGroup jobAtIndex:0]->sequence_id];
1201     }
1202     
1203     // Save the selection also.
1204
1205     if (!fSavedSelectedItems)
1206         fSavedSelectedItems = [[NSMutableIndexSet alloc] init];
1207     else
1208         [fSavedSelectedItems removeAllIndexes];
1209
1210     NSIndexSet * selectedRows = [fOutlineView selectedRowIndexes];
1211     NSInteger row = [selectedRows firstIndex];
1212     while (row != NSNotFound)
1213     {
1214         aJobGroup = [fOutlineView itemAtRow: row];
1215         [fSavedSelectedItems addIndex: [aJobGroup jobAtIndex:0]->sequence_id];
1216         row = [selectedRows indexGreaterThanIndex: row];
1217     }
1218
1219 }
1220
1221 //------------------------------------------------------------------------------------
1222 // Restores the expanded state of items in the outline view to match those saved by a
1223 // previous call to saveOutlineViewState.
1224 //------------------------------------------------------------------------------------
1225 - (void) restoreOutlineViewState
1226 {
1227     if (fSavedExpandedItems)
1228     {
1229         HBJobGroup * aJobGroup;
1230         NSEnumerator * e = [fJobGroups objectEnumerator];
1231         while ( (aJobGroup = [e nextObject]) )
1232         {
1233             HBJob * job = [aJobGroup jobAtIndex:0];
1234             if (job && [fSavedExpandedItems containsIndex: job->sequence_id])
1235                 [fOutlineView expandItem: aJobGroup];
1236         }
1237     }
1238     
1239     if (fSavedSelectedItems)
1240     {
1241         NSMutableIndexSet * rowsToSelect = [[[NSMutableIndexSet alloc] init] autorelease];
1242         HBJobGroup * aJobGroup;
1243         NSEnumerator * e = [fJobGroups objectEnumerator];
1244         NSInteger i = 0;
1245         while ( (aJobGroup = [e nextObject]) )
1246         {
1247             HBJob * job = [aJobGroup jobAtIndex:0];
1248             if (job && [fSavedSelectedItems containsIndex: job->sequence_id])
1249                 [rowsToSelect addIndex: i];
1250             i++;
1251         }
1252         if ([rowsToSelect count] == 0)
1253             [fOutlineView deselectAll: nil];
1254         else
1255             [fOutlineView selectRowIndexes:rowsToSelect byExtendingSelection:NO];
1256     }
1257 }
1258
1259 //------------------------------------------------------------------------------------
1260 // Marks the icon region of a job group in the queue view as needing display.
1261 //------------------------------------------------------------------------------------
1262 - (void) updateJobGroupIconInQueue:(HBJobGroup*)aJobGroup
1263 {
1264     NSInteger row = [fOutlineView rowForItem: aJobGroup];
1265     NSInteger col = [fOutlineView columnWithIdentifier: @"icon"];
1266     if (row != -1 && col != -1)
1267     {
1268         NSRect frame = [fOutlineView frameOfCellAtColumn:col row:row];
1269         [fOutlineView setNeedsDisplayInRect: frame];
1270     }
1271 }
1272
1273 //------------------------------------------------------------------------------------
1274 // Marks the entire region of a job group in the queue view as needing display.
1275 //------------------------------------------------------------------------------------
1276 - (void) updateJobGroupInQueue:(HBJobGroup*)aJobGroup
1277 {
1278     NSInteger row = [fOutlineView rowForItem: aJobGroup];
1279     if (row != -1)
1280     {
1281         NSRect frame = [fOutlineView rectOfRow:row];
1282         [fOutlineView setNeedsDisplayInRect: frame];
1283     }
1284 }
1285
1286 //------------------------------------------------------------------------------------
1287 // If a job is currently processing, its job icon in the queue outline view is
1288 // animated to its next state.
1289 //------------------------------------------------------------------------------------
1290 - (void) animateCurrentJobGroupInQueue:(NSTimer*)theTimer
1291 {
1292     if (fCurrentJobGroup)
1293     {
1294         fAnimationIndex++;
1295         fAnimationIndex %= 6;   // there are 6 animation images; see outlineView:objectValueForTableColumn:byItem: below.
1296         [self updateJobGroupIconInQueue: fCurrentJobGroup];
1297     }
1298 }
1299
1300 //------------------------------------------------------------------------------------
1301 // Starts animating the job icon of the currently processing job in the queue outline
1302 // view.
1303 //------------------------------------------------------------------------------------
1304 - (void) startAnimatingCurrentJobGroupInQueue
1305 {
1306     if (!fAnimationTimer)
1307         fAnimationTimer = [[NSTimer scheduledTimerWithTimeInterval:1.0/12.0     // 1/12 because there are 6 images in the animation cycle
1308                 target:self
1309                 selector:@selector(animateCurrentJobGroupInQueue:)
1310                 userInfo:nil
1311                 repeats:YES] retain];
1312 }
1313
1314 //------------------------------------------------------------------------------------
1315 // Stops animating the job icon of the currently processing job in the queue outline
1316 // view.
1317 //------------------------------------------------------------------------------------
1318 - (void) stopAnimatingCurrentJobGroupInQueue
1319 {
1320     if (fAnimationTimer && [fAnimationTimer isValid])
1321     {
1322         [fAnimationTimer invalidate];
1323         [fAnimationTimer release];
1324         fAnimationTimer = nil;
1325     }
1326 }
1327
1328 //------------------------------------------------------------------------------------
1329 // Generate string to display in UI.
1330 //------------------------------------------------------------------------------------
1331 - (NSString *) progressStatusStringForJob: (HBJob *)job state: (hb_state_t *)s
1332 {
1333     if (s->state == HB_STATE_WORKING)
1334     {
1335         NSString * msg;
1336         if (job->pass == -1)
1337             msg = NSLocalizedString( @"Deep Scan", nil );
1338         else if (job->pass == 1)
1339             msg = NSLocalizedString( @"Analyzing video", nil );
1340         else if ((job->pass == 0) ||  (job->pass == 2))
1341             msg = NSLocalizedString( @"Encoding movie", nil );
1342         else
1343             return @""; // unknown condition!
1344             
1345         if( s->param.working.seconds > -1 )
1346         {
1347             return [NSString stringWithFormat:
1348                 NSLocalizedString( @"%@ (%.2f fps, avg %.2f fps)", nil ),
1349                 msg, s->param.working.rate_cur, s->param.working.rate_avg];
1350         }
1351         else
1352             return msg;
1353
1354     }
1355
1356     else if (s->state == HB_STATE_MUXING)
1357         return NSLocalizedString( @"Muxing", nil );
1358
1359     else if (s->state == HB_STATE_PAUSED)
1360         return NSLocalizedString( @"Paused", nil );
1361
1362     else if (s->state == HB_STATE_WORKDONE)
1363         return NSLocalizedString( @"Done", nil );
1364     
1365     return @"";
1366 }
1367
1368 //------------------------------------------------------------------------------------
1369 // Generate string to display in UI.
1370 //------------------------------------------------------------------------------------
1371 - (NSString *) progressTimeRemainingStringForJob: (HBJob *)job state: (hb_state_t *)s
1372 {
1373     if (s->state == HB_STATE_WORKING)
1374     {
1375         #define p s->param.working
1376         if (p.seconds < 0)
1377             return @"";
1378         
1379         // Minutes always needed
1380         NSString * minutes;
1381         if (p.minutes > 1)
1382           minutes = [NSString stringWithFormat:NSLocalizedString( @"%d minutes ", nil ), p.minutes];
1383         else if (p.minutes == 1)
1384           minutes = NSLocalizedString( @"1 minute ", nil );
1385         else
1386           minutes = @"";
1387         
1388         if (p.hours >= 1)
1389         {
1390             NSString * hours;
1391             if (p.hours > 1)
1392               hours = [NSString stringWithFormat:NSLocalizedString( @"%d hours ", nil ), p.hours];
1393             else
1394               hours = NSLocalizedString( @"1 hour ", nil );
1395
1396             return [NSString stringWithFormat:NSLocalizedString( @"%@%@remaining", nil ), hours, minutes];
1397         }
1398         
1399         else
1400         {
1401             NSString * seconds;
1402             if (p.seconds > 1)
1403               seconds = [NSString stringWithFormat:NSLocalizedString( @"%d seconds ", nil ), p.seconds];
1404             else
1405               seconds = NSLocalizedString( @"1 second ", nil );
1406
1407             return [NSString stringWithFormat:NSLocalizedString( @"%@%@remaining", nil ), minutes, seconds];
1408         }
1409
1410 /* here is code that does it more like the Finder
1411         if( p.seconds > -1 )
1412         {
1413             float estHours = (p.hours + (p.minutes / 60.0));
1414             float estMinutes = (p.minutes + (p.seconds / 60.0));
1415
1416             if (estHours > 1.5)
1417                 return [NSString stringWithFormat:NSLocalizedString( @"Time remaining: About %d hours", nil ), lrintf(estHours)];
1418             else if (estHours > 0.983)    // 59 minutes
1419                 return NSLocalizedString( @"Time remaining: About 1 hour", nil );
1420             else if (estMinutes > 1.5)
1421                 return [NSString stringWithFormat:NSLocalizedString( @"Time remaining: About %d minutes", nil ), lrintf(estMinutes)];
1422             else if (estMinutes > 0.983)    // 59 seconds
1423                 return NSLocalizedString( @"Time remaining: About 1 minute", nil );
1424             else if (p.seconds <= 5)
1425                 return NSLocalizedString( @"Time remaining: Less than 5 seconds", nil );
1426             else if (p.seconds <= 10)
1427                 return NSLocalizedString( @"Time remaining: Less than 10 seconds", nil );
1428             else
1429                 return NSLocalizedString( @"Time remaining: Less than 1 minute", nil );
1430         }
1431         else
1432             return NSLocalizedString( @"Time remaining: Calculating...", nil );
1433 */
1434         #undef p
1435     }
1436     
1437     return @"";
1438 }
1439
1440 //------------------------------------------------------------------------------------
1441 // Refresh progress bar (fProgressTextField) from current state.
1442 //------------------------------------------------------------------------------------
1443 - (void) updateProgressTextForJob: (HBJob *)job state: (hb_state_t *)s
1444 {
1445     NSString * statusMsg = [self progressStatusStringForJob:job state:s];
1446     NSString * timeMsg = [self progressTimeRemainingStringForJob:job state:s];
1447     if ([timeMsg length] > 0)
1448         statusMsg = [NSString stringWithFormat:@"%@ - %@", statusMsg, timeMsg];
1449     [fProgressTextField setStringValue:statusMsg];
1450 }
1451
1452 //------------------------------------------------------------------------------------
1453 // Refresh progress bar (fProgressBar) from current state.
1454 //------------------------------------------------------------------------------------
1455 - (void) updateProgressBarWithState: (hb_state_t *)s
1456 {
1457     if (s->state == HB_STATE_WORKING)
1458     {
1459         #define p s->param.working
1460         [fProgressBar setIndeterminate:NO];
1461         float progress_total = 100.0 * ( p.progress + p.job_cur - 1 ) / p.job_count;
1462         [fProgressBar setDoubleValue:progress_total];
1463         #undef p
1464     }
1465     
1466     else if (s->state == HB_STATE_MUXING)
1467     {
1468         #define p s->param.muxing
1469         [fProgressBar setIndeterminate:YES];
1470         [fProgressBar startAnimation:nil];
1471         #undef p
1472     }
1473
1474     else if (s->state == HB_STATE_WORKDONE)
1475     {
1476         [fProgressBar setIndeterminate:NO];
1477         [fProgressBar stopAnimation:nil];
1478         [fProgressBar setDoubleValue:0.0];
1479         
1480         
1481     }
1482     
1483     else
1484         [fProgressBar stopAnimation:nil];    // just in case in was animating
1485 }
1486
1487 //------------------------------------------------------------------------------------
1488 // Refresh queue count text field (fQueueCountField).
1489 //------------------------------------------------------------------------------------
1490 - (void)updateQueueCountField
1491 {
1492     NSString * msg;
1493     int jobCount = [fJobGroups count];
1494     int pendingCount = [self pendingCount];
1495     if (jobCount == 0)
1496         msg = NSLocalizedString(@"No encodes", nil);
1497     else if ((jobCount == 1) && (pendingCount == 0))
1498         msg = NSLocalizedString(@"1 encode", nil);
1499     else if (jobCount == pendingCount)  // ie, all jobs listed are pending
1500     {
1501         if (jobCount == 1)
1502             msg = NSLocalizedString(@"1 pending encode", nil);
1503         else
1504             msg = [NSString stringWithFormat:NSLocalizedString(@"%d pending encodes", nil), pendingCount];
1505     }
1506     else    // some completed, some pending
1507         msg = [NSString stringWithFormat:NSLocalizedString(@"%d encodes (%d pending)", nil), jobCount, pendingCount];
1508
1509     [fQueueCountField setStringValue:msg];
1510 }
1511
1512 //------------------------------------------------------------------------------------
1513 // Refresh the UI in the current job pane. Should be called whenever the current job
1514 // being processed has changed.
1515 //------------------------------------------------------------------------------------
1516 - (void)updateCurrentJobDescription
1517 {
1518     if (fCurrentJob)
1519     {
1520         switch (fCurrentJob->pass)
1521         {
1522             case -1:  // Subtitle scan
1523                 [fJobDescTextField setAttributedStringValue:
1524                     [fCurrentJob attributedDescriptionWithIcon: NO
1525                                 withTitle: YES
1526                              withPassName: YES
1527                            withFormatInfo: NO
1528                           withDestination: NO
1529                           withPictureInfo: NO
1530                             withVideoInfo: NO
1531                              withx264Info: NO
1532                             withAudioInfo: NO
1533                          withSubtitleInfo: YES]];
1534                 break;
1535                 
1536             case 1:  // video 1st pass
1537                 [fJobDescTextField setAttributedStringValue:
1538                     [fCurrentJob attributedDescriptionWithIcon: NO
1539                                 withTitle: YES
1540                              withPassName: YES
1541                            withFormatInfo: NO
1542                           withDestination: NO
1543                           withPictureInfo: YES
1544                             withVideoInfo: YES
1545                              withx264Info: YES
1546                             withAudioInfo: NO
1547                          withSubtitleInfo: NO]];
1548                 break;
1549             
1550             case 0:  // single pass
1551             case 2:  // video 2nd pass + audio
1552                 [fJobDescTextField setAttributedStringValue:
1553                     [fCurrentJob attributedDescriptionWithIcon: NO
1554                                 withTitle: YES
1555                              withPassName: YES
1556                            withFormatInfo: NO
1557                           withDestination: NO
1558                           withPictureInfo: YES
1559                             withVideoInfo: YES
1560                              withx264Info: YES
1561                             withAudioInfo: YES
1562                          withSubtitleInfo: YES]];
1563                 break;
1564             
1565             default: // unknown
1566                 [fJobDescTextField setAttributedStringValue:
1567                     [fCurrentJob attributedDescriptionWithIcon: NO
1568                                 withTitle: YES
1569                              withPassName: YES
1570                            withFormatInfo: NO
1571                           withDestination: NO
1572                           withPictureInfo: YES
1573                             withVideoInfo: YES
1574                              withx264Info: YES
1575                             withAudioInfo: YES
1576                          withSubtitleInfo: YES]];
1577         }
1578     }
1579     else
1580     {
1581         [fJobDescTextField setStringValue: @"No encodes pending"];
1582     
1583     }
1584     
1585 }
1586
1587 //------------------------------------------------------------------------------------
1588 // Refresh the UI in the current job pane. Should be called whenever the current job
1589 // being processed has changed or when progress has changed.
1590 //------------------------------------------------------------------------------------
1591 - (void)updateCurrentJobProgress
1592 {
1593     hb_state_t s;
1594     hb_get_state2( fHandle, &s );
1595     [self updateProgressTextForJob: fCurrentJob state: &s];
1596     [self updateProgressBarWithState:&s];
1597 }
1598
1599 //------------------------------------------------------------------------------------
1600 // Notifies HBQueuecontroller that the contents of fJobGroups is about to be modified.
1601 // HBQueuecontroller remembers the state of the UI (selection and expanded items).
1602 //------------------------------------------------------------------------------------
1603 - (void) beginEditingJobGroupsArray
1604 {
1605     [self saveOutlineViewState];
1606 }
1607
1608 //------------------------------------------------------------------------------------
1609 // Notifies HBQueuecontroller that modifications to fJobGroups as indicated by a prior
1610 // call to beginEditingJobGroupsArray have been completed. HBQueuecontroller reloads
1611 // the queue view and restores the state of the UI (selection and expanded items).
1612 //------------------------------------------------------------------------------------
1613 - (void) endEditingJobGroupsArray
1614 {
1615     [self setJobGroupCountsNeedUpdating:YES];
1616     [fOutlineView noteNumberOfRowsChanged];
1617     [fOutlineView reloadData];
1618     [self restoreOutlineViewState];    
1619     [self updateQueueCountField];
1620 }
1621
1622 #pragma mark -
1623 #pragma mark Actions
1624
1625 //------------------------------------------------------------------------------------
1626 // Deletes the selected jobs from HB and the queue UI
1627 //------------------------------------------------------------------------------------
1628 - (IBAction)removeSelectedJobGroups: (id)sender
1629 {
1630     if (!fHandle) return;
1631     
1632     NSIndexSet * selectedRows = [fOutlineView selectedRowIndexes];
1633     NSInteger row = [selectedRows firstIndex];
1634     if (row != NSNotFound)
1635     {
1636         [self beginEditingJobGroupsArray];
1637         while (row != NSNotFound)
1638         {
1639             HBJobGroup * jobGroup = [fOutlineView itemAtRow: row];
1640             switch ([jobGroup status])
1641             {
1642                 case HBStatusCompleted:
1643                 case HBStatusCanceled:
1644                     [fJobGroups removeObject: jobGroup];
1645                     break;
1646                 case HBStatusWorking:
1647                     [self cancelCurrentJob: sender];
1648                     break;
1649                 case HBStatusPending:
1650                     // Remove from libhb
1651                     HBJob * job;
1652                     NSEnumerator * e = [jobGroup jobEnumerator];
1653                     while (job = [e nextObject])
1654                     {
1655                         hb_job_t * libhbJob = [self findLibhbJobWithID:job->sequence_id];
1656                         if (libhbJob)
1657                             hb_rem( fHandle, libhbJob );
1658                     }
1659                     // Remove from our list
1660                     [fJobGroups removeObject: jobGroup];
1661                     break;
1662                 case HBStatusNone:
1663                     break;
1664             }
1665         
1666             row = [selectedRows indexGreaterThanIndex: row];
1667         }
1668         [self endEditingJobGroupsArray];
1669     } 
1670 }
1671
1672 //------------------------------------------------------------------------------------
1673 // Reveals the file icons in the Finder of the selected job groups.
1674 //------------------------------------------------------------------------------------
1675 - (IBAction)revealSelectedJobGroups: (id)sender
1676 {
1677     if (!fHandle) return;
1678     
1679     NSIndexSet * selectedRows = [fOutlineView selectedRowIndexes];
1680     NSInteger row = [selectedRows firstIndex];
1681     if (row != NSNotFound)
1682     {
1683         while (row != NSNotFound)
1684         {
1685             HBJobGroup * jobGroup = [fOutlineView itemAtRow: row];
1686             if ([[jobGroup destinationPath] length])
1687                 [[NSWorkspace sharedWorkspace] selectFile:[jobGroup destinationPath] inFileViewerRootedAtPath:nil];
1688         
1689             row = [selectedRows indexGreaterThanIndex: row];
1690         }
1691     } 
1692 }
1693
1694 //------------------------------------------------------------------------------------
1695 // Calls HBController Cancel: which displays an alert asking user if they want to
1696 // cancel encoding of current job. cancelCurrentJob: returns immediately after posting
1697 // the alert. Later, when the user acknowledges the alert, HBController will call
1698 // libhb to cancel the job.
1699 //------------------------------------------------------------------------------------
1700 - (IBAction)cancelCurrentJob: (id)sender
1701 {
1702     [fHBController Cancel:sender];
1703 }
1704
1705 //------------------------------------------------------------------------------------
1706 // Starts or cancels the processing of jobs depending on the current state
1707 //------------------------------------------------------------------------------------
1708 - (IBAction)toggleStartCancel: (id)sender
1709 {
1710     if (!fHandle) return;
1711     
1712     hb_state_t s;
1713     hb_get_state2 (fHandle, &s);
1714
1715     if ((s.state == HB_STATE_PAUSED) || (s.state == HB_STATE_WORKING) || (s.state == HB_STATE_MUXING))
1716         [fHBController Cancel: fQueuePane]; // sender == fQueuePane so that warning alert shows up on queue window
1717
1718     else if ([self pendingCount] > 0)
1719         [fHBController doRip];
1720 }
1721
1722 //------------------------------------------------------------------------------------
1723 // Toggles the pause/resume state of libhb
1724 //------------------------------------------------------------------------------------
1725 - (IBAction)togglePauseResume: (id)sender
1726 {
1727     if (!fHandle) return;
1728     
1729     hb_state_t s;
1730     hb_get_state2 (fHandle, &s);
1731
1732     if (s.state == HB_STATE_PAUSED)
1733         hb_resume (fHandle);
1734     else if ((s.state == HB_STATE_WORKING) || (s.state == HB_STATE_MUXING))
1735         hb_pause (fHandle);
1736 }
1737
1738 #pragma mark -
1739 #pragma mark Synchronizing with libhb 
1740
1741 //------------------------------------------------------------------------------------
1742 // Queues a job group. The job group's status is set to HBStatusPending.
1743 //------------------------------------------------------------------------------------
1744 - (void) addJobGroup: (HBJobGroup *) aJobGroup
1745 {
1746     NSAssert(![fJobGroups containsObject:aJobGroup], @"Duplicate job group");
1747     [aJobGroup setStatus:HBStatusPending];
1748     
1749     [self beginEditingJobGroupsArray];
1750     [fJobGroups addObject:aJobGroup];
1751     [self endEditingJobGroupsArray];
1752 }
1753
1754 //------------------------------------------------------------------------------------
1755 // Notifies HBQueueController that libhb's current job has changed
1756 //------------------------------------------------------------------------------------
1757 - (void)currentJobChanged: (HBJob *) currentJob
1758 {
1759     /* if the job has a destination path, lets perform finished job notifications in fHBController 
1760      * We call this here so that we pickup the last job in the queue and single encodes before fCurrentJob
1761      * is released. So for the first job and the beginning of single encodes we check for the existence
1762      * of a valid fCurrentJob jobGroup
1763      */
1764     [currentJob retain];
1765     /* We need to compare the job group to determine if this is the end of a job group
1766      * or just the end of a job within a group to keep from sending encode done notification
1767      * after the first pass in a two pass encode
1768      */
1769     HBJobGroup * theJobGroupCheck = [currentJob jobGroup];
1770     if ((theJobGroupCheck == nil) || (theJobGroupCheck != fCurrentJobGroup))
1771     {
1772         /* we need to make sure that we are not at the beginning of a queue and also that the job hasn't
1773          * been cancelled
1774          */
1775         if ([[fCurrentJob jobGroup] destinationPath] && [fCurrentJobGroup status] != HBStatusCanceled)
1776         {
1777             /* send encode messages to fHBController. User prefs are grokked there. */
1778             [fHBController showGrowlDoneNotification: [[fCurrentJob jobGroup] destinationPath]];
1779             [fHBController sendToMetaX: [[fCurrentJob jobGroup] destinationPath]];
1780         }
1781     }
1782     [fCurrentJob release];
1783     fCurrentJob = currentJob;
1784
1785     // Log info about the preset name. We do this for each job, since libhb logs each
1786     // job separately. The preset name is found in the job's job group object.
1787     if (fCurrentJob && [fCurrentJob jobGroup] && ([[[fCurrentJob jobGroup] presetName] length] > 0))
1788         [fHBController writeToActivityLog: "Using preset: %s", [[[fCurrentJob jobGroup] presetName] UTF8String]];
1789
1790     // Check to see if this is also a change in Job Group
1791     
1792     HBJobGroup * theJobGroup = [currentJob jobGroup];
1793     if ((theJobGroup == nil) || (theJobGroup != fCurrentJobGroup))     // no more job groups or start of a new group
1794     {
1795         // Previous job has completed
1796         if (fCurrentJobGroup)
1797         {
1798             // Update the status of the job that just finished. If the user canceled,
1799             // the status will have already been set to canceled by libhbWillStop. So
1800             // all other cases are assumed to be a successful encode. BTW, libhb
1801             // doesn't currently report errors back to the GUI.
1802             if ([fCurrentJobGroup status] != HBStatusCanceled)
1803             {
1804                 [fCurrentJobGroup setStatus:HBStatusCompleted];
1805             }
1806             
1807             
1808         }
1809         
1810         // Set the new group
1811         [self setCurrentJobGroup: theJobGroup];
1812     
1813         // Update the UI
1814         [self updateCurrentJobDescription];
1815         [self updateCurrentJobProgress];
1816         [self showCurrentJobPane: fCurrentJobGroup != nil];
1817         if (fCurrentJobGroup)
1818             [self startAnimatingCurrentJobGroupInQueue];
1819         else
1820             [self stopAnimatingCurrentJobGroupInQueue];
1821     }
1822     
1823     else    // start a new job/pass in the same group
1824     {
1825         // Update the UI
1826         [self updateCurrentJobDescription];
1827         [self updateCurrentJobProgress];
1828     }
1829
1830 }
1831
1832 //------------------------------------------------------------------------------------
1833 // Notifies HBQueueController that hb_stop is about to be called. This signals us that
1834 // the current job is going to be canceled and deleted. This is somewhat of a hack to
1835 // let HBQueueController know when a job group has been cancelled. Otherwise, we'd
1836 // have no way of knowing if a job was canceled or completed sucessfully.
1837 //------------------------------------------------------------------------------------
1838 - (void)libhbWillStop
1839 {
1840     if (fCurrentJobGroup)
1841         [fCurrentJobGroup setStatus: HBStatusCanceled];
1842 }
1843
1844 //------------------------------------------------------------------------------------
1845 // Notifies HBQueueController that libhb's state has changed
1846 //------------------------------------------------------------------------------------
1847 - (void)libhbStateChanged: (hb_state_t &)state
1848 {
1849     switch( state.state )
1850     {
1851         case HB_STATE_WORKING:
1852         {
1853             //NSLog(@"job = %x; job_cur = %d; job_count = %d", state.param.working.sequence_id, state.param.working.job_cur, state.param.working.job_count);
1854             // First check to see if libhb has moved on to another job. We get no direct
1855             // message when this happens, so we have to detect it ourself. The new job could
1856             // be either just the next job in the current group, or the start of a new group.
1857             if (fCurrentJobID != state.param.working.sequence_id)
1858             {
1859                 fCurrentJobID = state.param.working.sequence_id;
1860                 HBJob * currentJob = [self findJobWithID:fCurrentJobID];
1861                 [self currentJobChanged: currentJob];
1862             }
1863
1864             if (fCurrentJob)
1865             {
1866                 [self updateCurrentJobProgress];
1867                 [self startAnimatingCurrentJobGroupInQueue];
1868             }
1869             break;
1870         }
1871
1872         case HB_STATE_MUXING:
1873         {
1874             [self updateCurrentJobProgress];
1875             break;
1876         }
1877
1878         case HB_STATE_PAUSED:
1879         {
1880             [self updateCurrentJobProgress];
1881             [self stopAnimatingCurrentJobGroupInQueue];
1882             break;
1883         }
1884
1885         case HB_STATE_WORKDONE:
1886         {
1887             // HB_STATE_WORKDONE means that libhb has finished processing all the jobs
1888             // in *its* queue. This message is NOT sent as each individual job is
1889             // completed.
1890
1891             [self currentJobChanged: nil];
1892             fCurrentJobID = 0;
1893             break;
1894         }
1895
1896     }
1897
1898 }
1899
1900 #if HB_OUTLINE_METRIC_CONTROLS
1901 static CGFloat spacingWidth = 3.0;
1902 - (IBAction)imageSpacingChanged: (id)sender;
1903 {
1904     spacingWidth = [sender floatValue];
1905     [fOutlineView setNeedsDisplay: YES];
1906 }
1907 - (IBAction)indentChanged: (id)sender
1908 {
1909     [fOutlineView setIndentationPerLevel: [sender floatValue]];
1910     [fOutlineView setNeedsDisplay: YES];
1911 }
1912 #endif
1913
1914 #pragma mark -
1915
1916 //------------------------------------------------------------------------------------
1917 // Receives notification whenever an HBJobGroup's status is changed.
1918 //------------------------------------------------------------------------------------
1919 - (void) jobGroupStatusNotification:(NSNotification *)notification
1920 {
1921     [self setJobGroupCountsNeedUpdating: YES];
1922 //    HBQueueJobGroupStatus oldStatus = (HBQueueJobGroupStatus) [[[notification userInfo] objectForKey:@"HBOldJobGroupStatus"] intValue];
1923     HBJobGroup * jobGroup = [notification object];
1924     if (jobGroup)
1925         [self updateJobGroupInQueue:jobGroup];
1926     [self updateQueueCountField];
1927 }
1928
1929
1930 #pragma mark -
1931 #pragma mark Toolbar
1932
1933 //------------------------------------------------------------------------------------
1934 // setupToolbar
1935 //------------------------------------------------------------------------------------
1936 - (void)setupToolbar
1937 {
1938     // Create a new toolbar instance, and attach it to our window
1939     NSToolbar *toolbar = [[[NSToolbar alloc] initWithIdentifier: HBQueueToolbar] autorelease];
1940
1941     // Set up toolbar properties: Allow customization, give a default display mode, and remember state in user defaults
1942     [toolbar setAllowsUserCustomization: YES];
1943     [toolbar setAutosavesConfiguration: YES];
1944     [toolbar setDisplayMode: NSToolbarDisplayModeIconAndLabel];
1945
1946     // We are the delegate
1947     [toolbar setDelegate: self];
1948
1949     // Attach the toolbar to our window
1950     [[self window] setToolbar:toolbar];
1951 }
1952
1953 //------------------------------------------------------------------------------------
1954 // toolbar:itemForItemIdentifier:willBeInsertedIntoToolbar:
1955 //------------------------------------------------------------------------------------
1956 - (NSToolbarItem *)toolbar:(NSToolbar *)toolbar
1957         itemForItemIdentifier:(NSString *)itemIdentifier
1958         willBeInsertedIntoToolbar:(BOOL)flag
1959 {
1960     // Required delegate method: Given an item identifier, this method returns an item.
1961     // The toolbar will use this method to obtain toolbar items that can be displayed
1962     // in the customization sheet, or in the toolbar itself.
1963     
1964     NSToolbarItem *toolbarItem = nil;
1965     
1966     if ([itemIdentifier isEqual: HBQueueStartCancelToolbarIdentifier])
1967     {
1968         toolbarItem = [[[NSToolbarItem alloc] initWithItemIdentifier: itemIdentifier] autorelease];
1969         
1970         // Set the text label to be displayed in the toolbar and customization palette 
1971         [toolbarItem setLabel: @"Start"];
1972         [toolbarItem setPaletteLabel: @"Start/Cancel"];
1973         
1974         // Set up a reasonable tooltip, and image
1975         [toolbarItem setToolTip: @"Start Encoding"];
1976         [toolbarItem setImage: [NSImage imageNamed: @"Play"]];
1977         
1978         // Tell the item what message to send when it is clicked 
1979         [toolbarItem setTarget: self];
1980         [toolbarItem setAction: @selector(toggleStartCancel:)];
1981     }
1982     
1983     if ([itemIdentifier isEqual: HBQueuePauseResumeToolbarIdentifier])
1984     {
1985         toolbarItem = [[[NSToolbarItem alloc] initWithItemIdentifier: itemIdentifier] autorelease];
1986         
1987         // Set the text label to be displayed in the toolbar and customization palette 
1988         [toolbarItem setLabel: @"Pause"];
1989         [toolbarItem setPaletteLabel: @"Pause/Resume"];
1990         
1991         // Set up a reasonable tooltip, and image
1992         [toolbarItem setToolTip: @"Pause Encoding"];
1993         [toolbarItem setImage: [NSImage imageNamed: @"Pause"]];
1994         
1995         // Tell the item what message to send when it is clicked 
1996         [toolbarItem setTarget: self];
1997         [toolbarItem setAction: @selector(togglePauseResume:)];
1998     }
1999     
2000     return toolbarItem;
2001 }
2002
2003 //------------------------------------------------------------------------------------
2004 // toolbarDefaultItemIdentifiers:
2005 //------------------------------------------------------------------------------------
2006 - (NSArray *) toolbarDefaultItemIdentifiers: (NSToolbar *) toolbar
2007 {
2008     // Required delegate method: Returns the ordered list of items to be shown in the
2009     // toolbar by default.
2010     
2011     return [NSArray arrayWithObjects:
2012         HBQueueStartCancelToolbarIdentifier,
2013         HBQueuePauseResumeToolbarIdentifier,
2014         nil];
2015 }
2016
2017 //------------------------------------------------------------------------------------
2018 // toolbarAllowedItemIdentifiers:
2019 //------------------------------------------------------------------------------------
2020 - (NSArray *) toolbarAllowedItemIdentifiers: (NSToolbar *) toolbar
2021 {
2022     // Required delegate method: Returns the list of all allowed items by identifier.
2023     // By default, the toolbar does not assume any items are allowed, even the
2024     // separator. So, every allowed item must be explicitly listed.
2025
2026     return [NSArray arrayWithObjects:
2027         HBQueueStartCancelToolbarIdentifier,
2028         HBQueuePauseResumeToolbarIdentifier,
2029         NSToolbarCustomizeToolbarItemIdentifier,
2030         NSToolbarFlexibleSpaceItemIdentifier,
2031         NSToolbarSpaceItemIdentifier,
2032         NSToolbarSeparatorItemIdentifier,
2033         nil];
2034 }
2035
2036 //------------------------------------------------------------------------------------
2037 // validateToolbarItem:
2038 //------------------------------------------------------------------------------------
2039 - (BOOL) validateToolbarItem: (NSToolbarItem *) toolbarItem
2040 {
2041     // Optional method: This message is sent to us since we are the target of some
2042     // toolbar item actions.
2043
2044     if (!fHandle) return NO;
2045
2046     BOOL enable = NO;
2047
2048     hb_state_t s;
2049     hb_get_state2 (fHandle, &s);
2050
2051     if ([[toolbarItem itemIdentifier] isEqual: HBQueueStartCancelToolbarIdentifier])
2052     {
2053         if ((s.state == HB_STATE_PAUSED) || (s.state == HB_STATE_WORKING) || (s.state == HB_STATE_MUXING))
2054         {
2055             enable = YES;
2056             [toolbarItem setImage:[NSImage imageNamed: @"Stop"]];
2057             [toolbarItem setLabel: @"Stop"];
2058             [toolbarItem setToolTip: @"Stop Encoding"];
2059         }
2060
2061         else if ([self pendingCount] > 0)
2062         {
2063             enable = YES;
2064             [toolbarItem setImage:[NSImage imageNamed: @"Play"]];
2065             [toolbarItem setLabel: @"Start"];
2066             [toolbarItem setToolTip: @"Start Encoding"];
2067         }
2068
2069         else
2070         {
2071             enable = NO;
2072             [toolbarItem setImage:[NSImage imageNamed: @"Play"]];
2073             [toolbarItem setLabel: @"Start"];
2074             [toolbarItem setToolTip: @"Start Encoding"];
2075         }
2076     }
2077     
2078     if ([[toolbarItem itemIdentifier] isEqual: HBQueuePauseResumeToolbarIdentifier])
2079     {
2080         if (s.state == HB_STATE_PAUSED)
2081         {
2082             enable = YES;
2083             [toolbarItem setImage:[NSImage imageNamed: @"Play"]];
2084             [toolbarItem setLabel: @"Resume"];
2085             [toolbarItem setToolTip: @"Resume Encoding"];
2086        }
2087         
2088         else if ((s.state == HB_STATE_WORKING) || (s.state == HB_STATE_MUXING))
2089         {
2090             enable = YES;
2091             [toolbarItem setImage:[NSImage imageNamed: @"Pause"]];
2092             [toolbarItem setLabel: @"Pause"];
2093             [toolbarItem setToolTip: @"Pause Encoding"];
2094         }
2095         else
2096         {
2097             enable = NO;
2098             [toolbarItem setImage:[NSImage imageNamed: @"Pause"]];
2099             [toolbarItem setLabel: @"Pause"];
2100             [toolbarItem setToolTip: @"Pause Encoding"];
2101         }
2102     }
2103     
2104     return enable;
2105 }
2106
2107 #pragma mark -
2108
2109 //------------------------------------------------------------------------------------
2110 // awakeFromNib
2111 //------------------------------------------------------------------------------------
2112 - (void)awakeFromNib
2113 {
2114     [self setupToolbar];
2115
2116     if( ![[self window] setFrameUsingName:@"Queue"] )
2117         [[self window] center];
2118     [self setWindowFrameAutosaveName:@"Queue"];
2119     [[self window] setExcludedFromWindowsMenu:YES];
2120
2121 #if HB_QUEUE_DRAGGING
2122     [fOutlineView registerForDraggedTypes: [NSArray arrayWithObject:HBQueuePboardType] ];
2123     [fOutlineView setDraggingSourceOperationMask:NSDragOperationEvery forLocal:YES];
2124     [fOutlineView setVerticalMotionCanBeginDrag: YES];
2125 #endif
2126
2127     // Don't allow autoresizing of main column, else the "delete" column will get
2128     // pushed out of view.
2129     [fOutlineView setAutoresizesOutlineColumn: NO];
2130
2131 #if HB_OUTLINE_METRIC_CONTROLS
2132     [fIndentation setHidden: NO];
2133     [fSpacing setHidden: NO];
2134     [fIndentation setIntValue:[fOutlineView indentationPerLevel]];  // debug
2135     [fSpacing setIntValue:3];       // debug
2136 #endif
2137
2138     // Show/hide UI elements
2139     fCurrentJobPaneShown = YES;     // it's shown in the nib
2140     [self showCurrentJobPane:NO];
2141
2142     [self updateQueueCountField];
2143 }
2144
2145
2146 //------------------------------------------------------------------------------------
2147 // windowWillClose
2148 //------------------------------------------------------------------------------------
2149 - (void)windowWillClose:(NSNotification *)aNotification
2150 {
2151     [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"QueueWindowIsOpen"];
2152 }
2153
2154 #pragma mark -
2155
2156 - (void)moveObjectsInArray:(NSMutableArray *)array fromIndexes:(NSIndexSet *)indexSet toIndex:(NSUInteger)insertIndex
2157 {
2158     NSUInteger index = [indexSet lastIndex];
2159     NSUInteger aboveInsertIndexCount = 0;
2160     
2161     while (index != NSNotFound)
2162     {
2163         NSUInteger removeIndex;
2164         
2165         if (index >= insertIndex)
2166         {
2167             removeIndex = index + aboveInsertIndexCount;
2168             aboveInsertIndexCount++;
2169         }
2170         else
2171         {
2172             removeIndex = index;
2173             insertIndex--;
2174         }
2175         
2176         id object = [[array objectAtIndex:removeIndex] retain];
2177         [array removeObjectAtIndex:removeIndex];
2178         [array insertObject:object atIndex:insertIndex];
2179         [object release];
2180         
2181         index = [indexSet indexLessThanIndex:index];
2182     }
2183 }
2184
2185 #pragma mark -
2186 #pragma mark NSOutlineView delegate
2187
2188 - (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item
2189 {
2190     if (item == nil)
2191         return [fJobGroups objectAtIndex:index];
2192     
2193     // We are only one level deep, so we can't be asked about children
2194     NSAssert (NO, @"HBQueueController outlineView:child:ofItem: can't handle nested items.");
2195     return nil;
2196 }
2197
2198 - (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item
2199 {
2200     // Our outline view has no levels, but we can still expand every item. Doing so
2201     // just makes the row taller. See heightOfRowByItem below.
2202     return YES;
2203 }
2204
2205 - (BOOL)outlineView:(NSOutlineView *)outlineView shouldExpandItem:(id)item
2206 {
2207     // Our outline view has no levels, but we can still expand every item. Doing so
2208     // just makes the row taller. See heightOfRowByItem below.
2209 #if HB_QUEUE_DRAGGING
2210         // Don't autoexpand while dragging, since we can't drop into the items
2211         return ![(HBQueueOutlineView*)outlineView isDragging];
2212 #else
2213         return YES;
2214 #endif
2215 }
2216
2217 - (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item
2218 {
2219     // Our outline view has no levels, so number of children will be zero for all
2220     // top-level items.
2221     if (item == nil)
2222         return [fJobGroups count];
2223     else
2224         return 0;
2225 }
2226
2227 - (void)outlineViewItemDidCollapse:(NSNotification *)notification
2228 {
2229     id item = [[notification userInfo] objectForKey:@"NSObject"];
2230     NSInteger row = [fOutlineView rowForItem:item];
2231     [fOutlineView noteHeightOfRowsWithIndexesChanged:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(row,1)]];
2232 }
2233
2234 - (void)outlineViewItemDidExpand:(NSNotification *)notification
2235 {
2236     id item = [[notification userInfo] objectForKey:@"NSObject"];
2237     NSInteger row = [fOutlineView rowForItem:item];
2238     [fOutlineView noteHeightOfRowsWithIndexesChanged:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(row,1)]];
2239 }
2240
2241 - (CGFloat)outlineView:(NSOutlineView *)outlineView heightOfRowByItem:(id)item
2242 {
2243     if ([outlineView isItemExpanded: item])
2244     {
2245         // Short-circuit here if in a live resize primarily to fix a bug but also to
2246         // increase resposivness during a resize. There's a bug in NSTableView that
2247         // causes row heights to get messed up if you try to change them during a live
2248         // resize. So if in a live resize, simply return the previously calculated
2249         // height. The row heights will get fixed up after the resize because we have
2250         // implemented viewDidEndLiveResize to force all of them to be recalculated.
2251         if ([outlineView inLiveResize] && [item lastDescriptionHeight] > 0)
2252             return [item lastDescriptionHeight];
2253         
2254         CGFloat width = [[outlineView tableColumnWithIdentifier: @"desc"] width];
2255         // Column width is NOT what is ultimately used. I can't quite figure out what
2256         // width to use for calculating text metrics. No matter how I tweak this value,
2257         // there are a few conditions in which the drawn text extends below the bounds
2258         // of the row cell. In previous versions, which ran under Tiger, I was
2259         // reducing width by 47 pixles.
2260         width -= 2;     // (?) for intercell spacing
2261         
2262         CGFloat height = [item heightOfDescriptionForWidth: width];
2263         return height;
2264     }
2265     else
2266         return HB_ROW_HEIGHT_TITLE_ONLY;
2267 }
2268
2269 - (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item
2270 {
2271         // nb: The "desc" column is currently an HBImageAndTextCell. However, we are longer
2272         // using the image portion of the cell so we could switch back to a regular NSTextFieldCell.
2273         
2274     if ([[tableColumn identifier] isEqualToString:@"desc"])
2275         return [item attributedDescription];
2276     else if ([[tableColumn identifier] isEqualToString:@"icon"])
2277     {
2278         switch ([(HBJobGroup*)item status])
2279         {
2280             case HBStatusCanceled:
2281                 return [NSImage imageNamed:@"EncodeCanceled"];
2282                 break;
2283             case HBStatusCompleted:
2284                 return [NSImage imageNamed:@"EncodeComplete"];
2285                 break;
2286             case HBStatusWorking:
2287                 return [NSImage imageNamed: [NSString stringWithFormat: @"EncodeWorking%d", fAnimationIndex]];
2288                 break;
2289             default:
2290                 return [NSImage imageNamed:@"JobSmall"];
2291                 break;
2292         }
2293     }
2294     else
2295         return @"";
2296 }
2297
2298 - (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item
2299 {
2300     if ([[tableColumn identifier] isEqualToString:@"desc"])
2301     {
2302 #if HB_OUTLINE_METRIC_CONTROLS
2303         NSSize theSize = [cell imageSpacing];
2304         theSize.width = spacingWidth;
2305         [cell setImageSpacing: theSize];
2306 #endif
2307         
2308                 // nb: The "desc" column is currently an HBImageAndTextCell. However, we are longer
2309                 // using the image portion of the cell so we could switch back to a regular NSTextFieldCell.
2310
2311         // Set the image here since the value returned from outlineView:objectValueForTableColumn: didn't specify the image part
2312         [cell setImage:nil];
2313     }
2314     
2315     else if ([[tableColumn identifier] isEqualToString:@"action"])
2316     {
2317         [cell setEnabled: YES];
2318         BOOL highlighted = [outlineView isRowSelected:[outlineView rowForItem: item]] && [[outlineView window] isKeyWindow] && ([[outlineView window] firstResponder] == outlineView);
2319         if ([(HBJobGroup*)item status] == HBStatusCompleted)
2320         {
2321             [cell setAction: @selector(revealSelectedJobGroups:)];
2322             if (highlighted)
2323             {
2324                 [cell setImage:[NSImage imageNamed:@"RevealHighlight"]];
2325                 [cell setAlternateImage:[NSImage imageNamed:@"RevealHighlightPressed"]];
2326             }
2327             else
2328                 [cell setImage:[NSImage imageNamed:@"Reveal"]];
2329         }
2330         else
2331         {
2332             [cell setAction: @selector(removeSelectedJobGroups:)];
2333             if (highlighted)
2334             {
2335                 [cell setImage:[NSImage imageNamed:@"DeleteHighlight"]];
2336                 [cell setAlternateImage:[NSImage imageNamed:@"DeleteHighlightPressed"]];
2337             }
2338             else
2339                 [cell setImage:[NSImage imageNamed:@"Delete"]];
2340         }
2341     }
2342 }
2343
2344 - (void)outlineView:(NSOutlineView *)outlineView willDisplayOutlineCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item
2345 {
2346     // By default, the discolsure image gets centered vertically in the cell. We want
2347     // always at the top.
2348     if ([outlineView isItemExpanded: item])
2349         [cell setImagePosition: NSImageAbove];
2350     else
2351         [cell setImagePosition: NSImageOnly];
2352 }
2353
2354 #pragma mark -
2355 #pragma mark NSOutlineView delegate (dragging related)
2356
2357 //------------------------------------------------------------------------------------
2358 // NSTableView delegate
2359 //------------------------------------------------------------------------------------
2360
2361 #if HB_QUEUE_DRAGGING
2362 - (BOOL)outlineView:(NSOutlineView *)outlineView writeItems:(NSArray *)items toPasteboard:(NSPasteboard *)pboard
2363 {
2364         // Dragging is only allowed of the pending items.
2365         NSEnumerator * e = [items objectEnumerator];
2366         HBJobGroup * group;
2367         while ( (group = [e nextObject]) )
2368         {
2369                 if ([group status] != HBStatusPending)
2370                         return NO;
2371         }
2372         
2373     // Don't retain since this is just holding temporaral drag information, and it is
2374     //only used during a drag!  We could put this in the pboard actually.
2375     fDraggedNodes = items;
2376         
2377     // Provide data for our custom type, and simple NSStrings.
2378     [pboard declareTypes:[NSArray arrayWithObjects: HBQueuePboardType, nil] owner:self];
2379
2380     // the actual data doesn't matter since DragDropSimplePboardType drags aren't recognized by anyone but us!.
2381     [pboard setData:[NSData data] forType:HBQueuePboardType]; 
2382
2383     return YES;
2384 }
2385 #endif
2386
2387 #if HB_QUEUE_DRAGGING
2388 - (NSDragOperation)outlineView:(NSOutlineView *)outlineView validateDrop:(id <NSDraggingInfo>)info proposedItem:(id)item proposedChildIndex:(NSInteger)index
2389 {
2390         // Don't allow dropping ONTO an item since they can't really contain any children.
2391     BOOL isOnDropTypeProposal = index == NSOutlineViewDropOnItemIndex;
2392     if (isOnDropTypeProposal)
2393         return NSDragOperationNone;
2394
2395         // Don't allow dropping INTO an item since they can't really contain any children.
2396         if (item != nil)
2397         {
2398                 index = [fOutlineView rowForItem: item] + 1;
2399                 item = nil;
2400         }
2401
2402         // Prevent dragging into the completed or current job.
2403         int firstPendingIndex = [fCompleted count];
2404         if (fCurrentJobGroup)
2405                 firstPendingIndex++;
2406         index = MAX (index, firstPendingIndex);
2407         
2408         [outlineView setDropItem:item dropChildIndex:index];
2409     return NSDragOperationGeneric;
2410 }
2411 #endif
2412
2413 #if HB_QUEUE_DRAGGING
2414 - (BOOL)outlineView:(NSOutlineView *)outlineView acceptDrop:(id <NSDraggingInfo>)info item:(id)item childIndex:(NSInteger)index
2415 {
2416     NSMutableIndexSet *moveItems = [NSMutableIndexSet indexSet];
2417     
2418     id obj;
2419     NSEnumerator *enumerator = [fDraggedNodes objectEnumerator];
2420     while (obj = [enumerator nextObject])
2421     {
2422         [moveItems addIndex:[fJobGroups indexOfObject:obj]];
2423     }
2424
2425     // Rearrange the data and view
2426     [self saveOutlineViewState];
2427     [self moveObjectsInArray:fJobGroups fromIndexes:moveItems toIndex: index];
2428     [fOutlineView reloadData];
2429     [self restoreOutlineViewState];
2430         
2431     return YES;
2432 }
2433 #endif
2434
2435
2436 @end