OSDN Git Service

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