OSDN Git Service

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