OSDN Git Service

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