OSDN Git Service

d9d62e4632e040018a8bc227707229cf1281487f
[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     HBJob * job = [self jobAtIndex:0];
622     
623     [fDescription appendAttributedString: [job attributedDescriptionWithHBHandle: handle
624                              withIcon: NO
625                             withTitle: YES
626                          withPassName: NO
627                        withFormatInfo: YES
628                       withDestination: YES
629                       withPictureInfo: NO
630                         withVideoInfo: NO
631                          withx264Info: NO
632                         withAudioInfo: NO
633                      withSubtitleInfo: NO]];
634
635     static NSAttributedString * carriageReturn = [[NSAttributedString alloc] initWithString:@"\n"];
636     
637     NSEnumerator * e = [self jobEnumerator];
638     while ( (job = [e nextObject]) )
639     {
640         int pass = [job job]->pass;
641         [fDescription appendAttributedString:carriageReturn];
642         [fDescription appendAttributedString:
643             [job attributedDescriptionWithHBHandle: handle
644                                  withIcon: YES
645                                 withTitle: NO
646                              withPassName: YES
647                            withFormatInfo: NO
648                           withDestination: NO
649                           withPictureInfo: pass != -1
650                             withVideoInfo: pass != -1
651                              withx264Info: pass != -1
652                             withAudioInfo: pass == 0 || pass == 2
653                          withSubtitleInfo: YES]];
654     }
655     
656     fNeedsDescription = NO;
657 }
658
659 - (NSMutableAttributedString *) attributedDescriptionWithHBHandle: (hb_handle_t *)handle
660 {
661     if (fNeedsDescription)
662         [self updateDescriptionWithHBHandle: handle];
663     return fDescription;
664 }
665
666 - (float) heightOfDescriptionForWidth:(float)width withHBHandle: (hb_handle_t *)handle
667 {
668     // Try to return the cached value if no changes have happened since the last time
669     if ((width == fLastDescriptionWidth) && (fLastDescriptionHeight != 0) && !fNeedsDescription)
670         return fLastDescriptionHeight;
671     
672     if (fNeedsDescription)
673         [self updateDescriptionWithHBHandle: handle];
674
675     // Calculate the height    
676     NSRect bounds = [fDescription boundingRectWithSize:NSMakeSize(width, 10000) options:NSStringDrawingUsesLineFragmentOrigin];
677     fLastDescriptionHeight = bounds.size.height + 6.0; // add some border to bottom
678     fLastDescriptionWidth = width;
679     return fLastDescriptionHeight;
680
681 /* supposedly another way to do this, in case boundingRectWithSize isn't working
682     NSTextView* tmpView = [[NSTextView alloc] initWithFrame:NSMakeRect(0, 0, width, 1)];
683     [[tmpView textStorage] setAttributedString:aString];
684     [tmpView setHorizontallyResizable:NO];
685     [tmpView setVerticallyResizable:YES];
686 //    [[tmpView textContainer] setHeightTracksTextView: YES];
687 //    [[tmpView textContainer] setContainerSize: NSMakeSize(width, 10000)];
688     [tmpView sizeToFit];
689     float height = [tmpView frame].size.height;
690     [tmpView release];
691     return height;
692 */
693 }
694
695 - (float) lastDescriptionHeight
696 {
697     return fLastDescriptionHeight;
698 }
699
700 @end
701
702
703 #pragma mark -
704
705 // Toolbar identifiers
706 static NSString*    HBQueueToolbar                            = @"HBQueueToolbar1";
707 static NSString*    HBQueueStartCancelToolbarIdentifier       = @"HBQueueStartCancelToolbarIdentifier";
708 static NSString*    HBQueuePauseResumeToolbarIdentifier       = @"HBQueuePauseResumeToolbarIdentifier";
709
710
711 @implementation HBQueueController
712
713 //------------------------------------------------------------------------------------
714 // init
715 //------------------------------------------------------------------------------------
716 - (id)init
717 {
718     if (self = [super init])
719     {
720         // Our defaults
721         [[NSUserDefaults standardUserDefaults] registerDefaults:[NSDictionary dictionaryWithObjectsAndKeys:
722             @"NO",      @"QueueWindowIsOpen",
723             @"NO",      @"QueueShowsDetail",
724             @"YES",     @"QueueShowsJobsAsGroups",
725             nil]];
726
727         fJobGroups = [[NSMutableArray arrayWithCapacity:0] retain];
728     }
729     return self; 
730 }
731
732 //------------------------------------------------------------------------------------
733 // dealloc
734 //------------------------------------------------------------------------------------
735 - (void)dealloc
736 {
737     [fAnimation release];
738     
739     // clear the delegate so that windowWillClose is not attempted
740     if ([fQueueWindow delegate] == self)
741         [fQueueWindow setDelegate:nil];
742     
743     [fJobGroups release];
744     [fSavedExpandedItems release];
745
746     [super dealloc];
747 }
748
749 //------------------------------------------------------------------------------------
750 // Receive HB handle
751 //------------------------------------------------------------------------------------
752 - (void)setHandle: (hb_handle_t *)handle
753 {
754     fHandle = handle;
755 }
756
757 //------------------------------------------------------------------------------------
758 // Receive HBController
759 //------------------------------------------------------------------------------------
760 - (void)setHBController: (HBController *)controller
761 {
762     fHBController = controller;
763 }
764
765 //------------------------------------------------------------------------------------
766 // Displays and brings the queue window to the front
767 //------------------------------------------------------------------------------------
768 - (IBAction) showQueueWindow: (id)sender
769 {
770     if (!fQueueWindow)
771     {
772         BOOL loadSucceeded = [NSBundle loadNibNamed:@"Queue" owner:self] && fQueueWindow;
773         NSAssert(loadSucceeded, @"Could not open Queue nib file");
774     }
775
776     [self updateQueueUI];
777     [self updateCurrentJobUI];
778
779     [fQueueWindow makeKeyAndOrderFront: self];
780
781     [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"QueueWindowIsOpen"];
782 }
783 //------------------------------------------------------------------------------------
784 // Show or hide the current job pane (fCurrentJobPane).
785 //------------------------------------------------------------------------------------
786 - (void) showCurrentJobPane: (BOOL)showPane
787 {
788     if (showPane == fCurrentJobPaneShown)
789         return;
790     
791     // Things to keep in mind:
792     // - When the current job pane is shown, it occupies the upper portion of the
793     //   window with the queue occupying the bottom portion of the window.
794     // - When the current job pane is hidden, it slides up and out of view.
795     //   NSView setHidden is NOT used. The queue pane is resized to occupy the full
796     //   window.
797     
798     NSRect windowFrame = [[fCurrentJobPane superview] frame];
799     NSRect queueFrame, jobFrame;
800     if (showPane)
801         NSDivideRect(windowFrame, &jobFrame, &queueFrame, NSHeight([fCurrentJobPane frame]), NSMaxYEdge);
802     else
803     {
804         queueFrame = windowFrame;
805         jobFrame = [fCurrentJobPane frame];
806         jobFrame.origin.y = NSHeight(windowFrame);
807     }
808     
809     // Move fCurrentJobPane
810     NSDictionary * dict1 = [NSDictionary dictionaryWithObjectsAndKeys:
811         fCurrentJobPane, NSViewAnimationTargetKey,
812         [NSValue valueWithRect:jobFrame], NSViewAnimationEndFrameKey,
813         nil];
814
815     // Resize fQueuePane
816     NSDictionary * dict2 = [NSDictionary dictionaryWithObjectsAndKeys:
817         fQueuePane, NSViewAnimationTargetKey,
818         [NSValue valueWithRect:queueFrame], NSViewAnimationEndFrameKey,
819         nil];
820
821     if (!fAnimation)
822         fAnimation = [[NSViewAnimation alloc] initWithViewAnimations:nil];
823
824     [fAnimation setViewAnimations:[NSArray arrayWithObjects:dict1, dict2, nil]];
825     [fAnimation setDuration:0.25];
826     [fAnimation setAnimationBlockingMode:NSAnimationBlocking]; // prevent user from resizing the window during an animation
827     [fAnimation startAnimation];
828     fCurrentJobPaneShown = showPane;
829 }
830
831 //------------------------------------------------------------------------------------
832 // Rebuilds the contents of fJobGroups which is a hierarchy of HBJobGroup and HBJobs.
833 //------------------------------------------------------------------------------------
834 - (void)rebuildJobGroups
835 {
836     [fJobGroups autorelease];
837     fJobGroups = [[NSMutableArray arrayWithCapacity:0] retain];
838
839     HBJobGroup * aJobGroup = [HBJobGroup jobGroup];
840
841     hb_job_t * nextJob = hb_group( fHandle, 0 );
842     while( nextJob )
843     {
844         if (nextJob->sequence_id == 0)
845         {
846             // Encountered a new group. Add the current one to fJobGroups and then start a new one.
847             if ([aJobGroup count] > 0)
848             {
849                 [fJobGroups addObject: aJobGroup];
850                 aJobGroup = [HBJobGroup jobGroup];
851             }
852         }
853         [aJobGroup addJob: [HBJob jobWithJob:nextJob]];
854         nextJob = hb_next_job (fHandle, nextJob);
855     }
856     if ([aJobGroup count] > 0)
857     {
858         [fJobGroups addObject:aJobGroup];
859     }
860 }
861
862 //------------------------------------------------------------------------------------
863 // Saves the state of the items that are currently expanded. Calling restoreOutlineViewState
864 // will restore the state of all items to match what was saved by saveOutlineViewState.
865 //------------------------------------------------------------------------------------
866 - (void) saveOutlineViewState
867 {
868     if (!fSavedExpandedItems)
869         fSavedExpandedItems = [[NSMutableIndexSet alloc] init];
870     else
871         [fSavedExpandedItems removeAllIndexes];
872     
873     // NB: This code is stuffing the address of each job into an index set. While it
874     // works 99.9% of the time, it's not the ideal solution. We need unique ids in
875     // each job, possibly using the existing sequence_id field. Could use the high
876     // word as a unique encode id and the low word the sequence number.
877     
878     HBJobGroup * aJobGroup;
879     NSEnumerator * e = [fJobGroups objectEnumerator];
880     while ( (aJobGroup = [e nextObject]) )
881     {
882         if ([fOutlineView isItemExpanded: aJobGroup])
883             [fSavedExpandedItems addIndex: (unsigned int)[[aJobGroup jobAtIndex:0] job]];
884     }
885     
886     // Save the selection also. This is really UGLY code. Since I have to rebuild the
887     // entire outline hierachy every time hblib changes its job list, there's no easy
888     // way for me to remember the selection state other than saving off the first
889     // hb_job_t item in each selected group. This is done by saving the object's
890     // address. This could go away if I'd save a unique id in each job object.
891
892     int selection = [fOutlineView selectedRow];
893     if (selection == -1)
894         fSavedSelectedItem = 0;
895     else
896     {
897         HBJobGroup * jobGroup = [fOutlineView itemAtRow: selection];
898         fSavedSelectedItem = (unsigned int)[[jobGroup jobAtIndex:0] job];
899     }
900     
901 }
902
903 //------------------------------------------------------------------------------------
904 // Restores the expanded state of items in the outline view to match those saved by a
905 // previous call to saveOutlineViewState.
906 //------------------------------------------------------------------------------------
907 - (void) restoreOutlineViewState
908 {
909     if (fSavedExpandedItems)
910     {
911         HBJobGroup * aJobGroup;
912         NSEnumerator * e = [fJobGroups objectEnumerator];
913         while ( (aJobGroup = [e nextObject]) )
914         {
915             hb_job_t * j = [[aJobGroup jobAtIndex:0] job];
916             if ([fSavedExpandedItems containsIndex: (unsigned int)j])
917                 [fOutlineView expandItem: aJobGroup];
918         }
919     }
920     
921     if (fSavedSelectedItem)
922     {
923         // Ugh. Have to cycle through each row looking for the previously selected job.
924         // See the explanation in saveExpandedItems about the logic here.
925         
926         // Find out what hb_job_t was selected
927         hb_job_t * j = (hb_job_t *)fSavedSelectedItem;
928         
929         int rowToSelect = -1;
930         for (int i = 0; i < [fOutlineView numberOfRows]; i++)
931         {
932             HBJobGroup * jobGroup = [fOutlineView itemAtRow: i];
933             // Test to see if the group's first job is a match
934             if ([[jobGroup jobAtIndex:0] job] == j)
935             {
936                 rowToSelect = i;
937                 break;
938             }
939         }
940         if (rowToSelect == -1)
941             [fOutlineView deselectAll: nil];
942         else
943             [fOutlineView selectRow:rowToSelect byExtendingSelection:NO];
944     }
945 }
946
947 //------------------------------------------------------------------------------------
948 // Generate string to display in UI.
949 //------------------------------------------------------------------------------------
950 - (NSString *) progressStatusStringForJob: (hb_job_t *)job state: (hb_state_t *)s
951 {
952     if (s->state == HB_STATE_WORKING)
953     {
954         NSString * msg;
955         if (job->pass == -1)
956             msg = NSLocalizedString( @"Deep Scan", nil );
957         else if (job->pass == 1)
958             msg = NSLocalizedString( @"Analyzing video", nil );
959         else if ((job->pass == 0) ||  (job->pass == 2))
960             msg = NSLocalizedString( @"Encoding movie", nil );
961         else
962             return @""; // unknown condition!
963             
964         if( s->param.working.seconds > -1 )
965         {
966             return [NSString stringWithFormat:
967                 NSLocalizedString( @"%@ (%.2f fps, avg %.2f fps)", nil ),
968                 msg, s->param.working.rate_cur, s->param.working.rate_avg];
969         }
970         else
971             return msg;
972
973     }
974
975     else if (s->state == HB_STATE_MUXING)
976         return NSLocalizedString( @"Muxing", nil );
977
978     else if (s->state == HB_STATE_PAUSED)
979         return NSLocalizedString( @"Paused", nil );
980
981     else if (s->state == HB_STATE_WORKDONE)
982         return NSLocalizedString( @"Done", nil );
983     
984     return @"";
985 }
986
987 //------------------------------------------------------------------------------------
988 // Generate string to display in UI.
989 //------------------------------------------------------------------------------------
990 - (NSString *) progressTimeRemainingStringForJob: (hb_job_t *)job state: (hb_state_t *)s
991 {
992     if (s->state == HB_STATE_WORKING)
993     {
994         #define p s->param.working
995         if (p.seconds < 0)
996             return @"";
997         
998         // Minutes always needed
999         NSString * minutes;
1000         if (p.minutes > 1)
1001           minutes = [NSString stringWithFormat:NSLocalizedString( @"%d minutes ", nil ), p.minutes];
1002         else if (p.minutes == 1)
1003           minutes = NSLocalizedString( @"1 minute ", nil );
1004         else
1005           minutes = @"";
1006         
1007         if (p.hours >= 1)
1008         {
1009             NSString * hours;
1010             if (p.hours > 1)
1011               hours = [NSString stringWithFormat:NSLocalizedString( @"%d hours ", nil ), p.hours];
1012             else
1013               hours = NSLocalizedString( @"1 hour ", nil );
1014
1015             return [NSString stringWithFormat:NSLocalizedString( @"%@%@remaining", nil ), hours, minutes];
1016         }
1017         
1018         else
1019         {
1020             NSString * seconds;
1021             if (p.seconds > 1)
1022               seconds = [NSString stringWithFormat:NSLocalizedString( @"%d seconds ", nil ), p.seconds];
1023             else
1024               seconds = NSLocalizedString( @"1 second ", nil );
1025
1026             return [NSString stringWithFormat:NSLocalizedString( @"%@%@remaining", nil ), minutes, seconds];
1027         }
1028
1029 /* here is code that does it more like the Finder
1030         if( p.seconds > -1 )
1031         {
1032             float estHours = (p.hours + (p.minutes / 60.0));
1033             float estMinutes = (p.minutes + (p.seconds / 60.0));
1034
1035             if (estHours > 1.5)
1036                 return [NSString stringWithFormat:NSLocalizedString( @"Time remaining: About %d hours", nil ), lrintf(estHours)];
1037             else if (estHours > 0.983)    // 59 minutes
1038                 return NSLocalizedString( @"Time remaining: About 1 hour", nil );
1039             else if (estMinutes > 1.5)
1040                 return [NSString stringWithFormat:NSLocalizedString( @"Time remaining: About %d minutes", nil ), lrintf(estMinutes)];
1041             else if (estMinutes > 0.983)    // 59 seconds
1042                 return NSLocalizedString( @"Time remaining: About 1 minute", nil );
1043             else if (p.seconds <= 5)
1044                 return NSLocalizedString( @"Time remaining: Less than 5 seconds", nil );
1045             else if (p.seconds <= 10)
1046                 return NSLocalizedString( @"Time remaining: Less than 10 seconds", nil );
1047             else
1048                 return NSLocalizedString( @"Time remaining: Less than 1 minute", nil );
1049         }
1050         else
1051             return NSLocalizedString( @"Time remaining: Calculating...", nil );
1052 */
1053         #undef p
1054     }
1055     
1056     return @"";
1057 }
1058
1059 //------------------------------------------------------------------------------------
1060 // Refresh progress bar (fProgressBar) from current state.
1061 //------------------------------------------------------------------------------------
1062 - (void) updateProgressBarWithState: (hb_state_t *)s
1063 {
1064     if (s->state == HB_STATE_WORKING)
1065     {
1066         #define p s->param.working
1067         [fProgressBar setIndeterminate:NO];
1068         float progress_total = 100.0 * ( p.progress + p.job_cur - 1 ) / p.job_count;
1069         [fProgressBar setDoubleValue:progress_total];
1070         #undef p
1071     }
1072     
1073     else if (s->state == HB_STATE_MUXING)
1074     {
1075         #define p s->param.muxing
1076         [fProgressBar setIndeterminate:YES];
1077         [fProgressBar startAnimation:nil];
1078         #undef p
1079     }
1080
1081     else if (s->state == HB_STATE_WORKDONE)
1082     {
1083         [fProgressBar setIndeterminate:NO];
1084         [fProgressBar setDoubleValue:0.0];
1085     }
1086 }
1087
1088 //------------------------------------------------------------------------------------
1089 // Refresh queue count text field (fQueueCountField).
1090 //------------------------------------------------------------------------------------
1091 - (void)updateQueueCountField
1092 {
1093     NSString * msg;
1094     int jobCount;
1095     
1096     jobCount = fHandle ? hb_group_count(fHandle) : 0;
1097     if (jobCount == 1)
1098         msg = NSLocalizedString(@"1 pending encode", nil);
1099     else
1100         msg = [NSString stringWithFormat:NSLocalizedString(@"%d pending encodes", nil), jobCount];
1101
1102     [fQueueCountField setStringValue:msg];
1103 }
1104
1105 //------------------------------------------------------------------------------------
1106 // Refresh the UI in the current job pane. Should be called whenever the current job
1107 // being processed has changed or when progress has changed.
1108 //------------------------------------------------------------------------------------
1109 - (void)updateCurrentJobUI
1110 {
1111     hb_state_t s;
1112     hb_job_t * job = nil;
1113     
1114     if (fHandle)
1115     {
1116         hb_get_state2( fHandle, &s );
1117         job = hb_current_job(fHandle);
1118     }
1119
1120     if (job)
1121     {
1122         if (fLastKnownCurrentJob != job)
1123         {
1124             HBJob * currentJob = [HBJob jobWithJob: job];
1125             
1126             switch (job->pass)
1127             {
1128                 case -1:  // Subtitle scan
1129                     [fJobDescTextField setAttributedStringValue:
1130                         [currentJob attributedDescriptionWithHBHandle:fHandle
1131                                      withIcon: NO
1132                                     withTitle: YES
1133                                  withPassName: YES
1134                                withFormatInfo: NO
1135                               withDestination: NO
1136                               withPictureInfo: NO
1137                                 withVideoInfo: NO
1138                                  withx264Info: NO
1139                                 withAudioInfo: NO
1140                              withSubtitleInfo: YES]];
1141                     break;
1142                     
1143                 case 1:  // video 1st pass
1144                     [fJobDescTextField setAttributedStringValue:
1145                         [currentJob attributedDescriptionWithHBHandle:fHandle
1146                                      withIcon: NO
1147                                     withTitle: YES
1148                                  withPassName: YES
1149                                withFormatInfo: NO
1150                               withDestination: NO
1151                               withPictureInfo: YES
1152                                 withVideoInfo: YES
1153                                  withx264Info: YES
1154                                 withAudioInfo: NO
1155                              withSubtitleInfo: NO]];
1156                     break;
1157                 
1158                 case 0:  // single pass
1159                 case 2:  // video 2nd pass + audio
1160                     [fJobDescTextField setAttributedStringValue:
1161                         [currentJob attributedDescriptionWithHBHandle:fHandle
1162                                      withIcon: NO
1163                                     withTitle: YES
1164                                  withPassName: YES
1165                                withFormatInfo: NO
1166                               withDestination: NO
1167                               withPictureInfo: YES
1168                                 withVideoInfo: YES
1169                                  withx264Info: YES
1170                                 withAudioInfo: YES
1171                              withSubtitleInfo: YES]];
1172                     break;
1173                 
1174                 default: // unknown
1175                     [fJobDescTextField setAttributedStringValue:
1176                         [currentJob attributedDescriptionWithHBHandle:fHandle
1177                                      withIcon: NO
1178                                     withTitle: YES
1179                                  withPassName: YES
1180                                withFormatInfo: NO
1181                               withDestination: NO
1182                               withPictureInfo: YES
1183                                 withVideoInfo: YES
1184                                  withx264Info: YES
1185                                 withAudioInfo: YES
1186                              withSubtitleInfo: YES]];
1187             }
1188
1189             [self showCurrentJobPane:YES];
1190             [fJobIconView setImage: [NSImage imageNamed:@"JobLarge"]];
1191         }
1192
1193         NSString * statusMsg = [self progressStatusStringForJob:job state:&s];
1194         NSString * timeMsg = [self progressTimeRemainingStringForJob:job state:&s];
1195         if ([timeMsg length] > 0)
1196             statusMsg = [NSString stringWithFormat:@"%@ - %@", statusMsg, timeMsg];
1197         [fProgressTextField setStringValue:statusMsg];
1198         [self updateProgressBarWithState:&s];
1199     }
1200     else
1201     {
1202         [fJobDescTextField setStringValue:NSLocalizedString(@"No job processing", nil)];
1203
1204         [self showCurrentJobPane:NO];
1205         [fProgressBar stopAnimation:nil];    // just in case in was animating
1206     }
1207         
1208     fLastKnownCurrentJob = job;
1209 }
1210
1211 //------------------------------------------------------------------------------------
1212 // Refresh the UI in the queue pane. Should be called whenever the content of HB's job
1213 // list has changed so that HBQueueController can sync up.
1214 //------------------------------------------------------------------------------------
1215 - (void)updateQueueUI
1216 {
1217     [self saveOutlineViewState];
1218     [self rebuildJobGroups];
1219     [fOutlineView noteNumberOfRowsChanged];
1220     [fOutlineView reloadData];
1221     [self restoreOutlineViewState];    
1222     [self updateQueueCountField];
1223 }
1224
1225 //------------------------------------------------------------------------------------
1226 // Deletes the selected job from HB and the queue UI
1227 //------------------------------------------------------------------------------------
1228 - (IBAction)removeSelectedJob: (id)sender
1229 {
1230     if (!fHandle) return;
1231     
1232     int row = [sender selectedRow];
1233     if (row != -1)
1234     {
1235 #if HB_UNI_QUEUE
1236         if (row == 0)
1237         {
1238             [self cancelCurrentJob:sender];
1239         }
1240         else
1241         {
1242             row--;
1243             hb_rem_group( fHandle, hb_group( fHandle, row ) );
1244         }
1245 #else
1246         HBJobGroup * jobGroup = [fOutlineView itemAtRow: row];
1247         hb_job_t * job = [[jobGroup jobAtIndex: 0] job];
1248         hb_rem_group( fHandle, job );
1249 #endif
1250         [self updateQueueUI];
1251     }
1252 }
1253
1254 //------------------------------------------------------------------------------------
1255 // Prompts user if the want to cancel encoding of current job. If so, doCancelCurrentJob
1256 // gets called.
1257 //------------------------------------------------------------------------------------
1258 - (IBAction)cancelCurrentJob: (id)sender
1259 {
1260     [fHBController Cancel:sender];
1261 }
1262
1263 //------------------------------------------------------------------------------------
1264 // Starts or cancels the processing of jobs depending on the current state
1265 //------------------------------------------------------------------------------------
1266 - (IBAction)toggleStartCancel: (id)sender
1267 {
1268     if (!fHandle) return;
1269     
1270     hb_state_t s;
1271     hb_get_state2 (fHandle, &s);
1272
1273     if ((s.state == HB_STATE_PAUSED) || (s.state == HB_STATE_WORKING) || (s.state == HB_STATE_MUXING))
1274         [fHBController Cancel: fQueuePane]; // sender == fQueuePane so that warning alert shows up on queue window
1275
1276     else if (hb_group_count(fHandle) > 0)
1277         [fHBController doRip];
1278 }
1279
1280 //------------------------------------------------------------------------------------
1281 // Toggles the pause/resume state of hblib
1282 //------------------------------------------------------------------------------------
1283 - (IBAction)togglePauseResume: (id)sender
1284 {
1285     if (!fHandle) return;
1286     
1287     hb_state_t s;
1288     hb_get_state2 (fHandle, &s);
1289
1290     if (s.state == HB_STATE_PAUSED)
1291         hb_resume (fHandle);
1292     else if ((s.state == HB_STATE_WORKING) || (s.state == HB_STATE_MUXING))
1293         hb_pause (fHandle);
1294 }
1295
1296 #if HB_OUTLINE_METRIC_CONTROLS
1297 static float spacingWidth = 3.0;
1298 - (IBAction)imageSpacingChanged: (id)sender;
1299 {
1300     spacingWidth = [sender floatValue];
1301     [fOutlineView setNeedsDisplay: YES];
1302 }
1303 - (IBAction)indentChanged: (id)sender
1304 {
1305     [fOutlineView setIndentationPerLevel: [sender floatValue]];
1306     [fOutlineView setNeedsDisplay: YES];
1307 }
1308 #endif
1309
1310
1311 #pragma mark -
1312 #pragma mark Toolbar
1313
1314 //------------------------------------------------------------------------------------
1315 // setupToolbar
1316 //------------------------------------------------------------------------------------
1317 - (void)setupToolbar
1318 {
1319     // Create a new toolbar instance, and attach it to our window 
1320     NSToolbar *toolbar = [[[NSToolbar alloc] initWithIdentifier: HBQueueToolbar] autorelease];
1321     
1322     // Set up toolbar properties: Allow customization, give a default display mode, and remember state in user defaults 
1323     [toolbar setAllowsUserCustomization: YES];
1324     [toolbar setAutosavesConfiguration: YES];
1325     [toolbar setDisplayMode: NSToolbarDisplayModeIconAndLabel];
1326     
1327     // We are the delegate
1328     [toolbar setDelegate: self];
1329     
1330     // Attach the toolbar to our window 
1331     [fQueueWindow setToolbar: toolbar];
1332 }
1333
1334 //------------------------------------------------------------------------------------
1335 // toolbar:itemForItemIdentifier:willBeInsertedIntoToolbar:
1336 //------------------------------------------------------------------------------------
1337 - (NSToolbarItem *)toolbar:(NSToolbar *)toolbar
1338         itemForItemIdentifier:(NSString *)itemIdentifier
1339         willBeInsertedIntoToolbar:(BOOL)flag
1340 {
1341     // Required delegate method: Given an item identifier, this method returns an item.
1342     // The toolbar will use this method to obtain toolbar items that can be displayed
1343     // in the customization sheet, or in the toolbar itself.
1344     
1345     NSToolbarItem *toolbarItem = nil;
1346     
1347     if ([itemIdentifier isEqual: HBQueueStartCancelToolbarIdentifier])
1348     {
1349         toolbarItem = [[[NSToolbarItem alloc] initWithItemIdentifier: itemIdentifier] autorelease];
1350                 
1351         // Set the text label to be displayed in the toolbar and customization palette 
1352                 [toolbarItem setLabel: @"Start"];
1353                 [toolbarItem setPaletteLabel: @"Start/Cancel"];
1354                 
1355                 // Set up a reasonable tooltip, and image
1356                 [toolbarItem setToolTip: @"Start Encoding"];
1357                 [toolbarItem setImage: [NSImage imageNamed: @"Play"]];
1358                 
1359                 // Tell the item what message to send when it is clicked 
1360                 [toolbarItem setTarget: self];
1361                 [toolbarItem setAction: @selector(toggleStartCancel:)];
1362         }
1363     
1364     if ([itemIdentifier isEqual: HBQueuePauseResumeToolbarIdentifier])
1365     {
1366         toolbarItem = [[[NSToolbarItem alloc] initWithItemIdentifier: itemIdentifier] autorelease];
1367                 
1368         // Set the text label to be displayed in the toolbar and customization palette 
1369                 [toolbarItem setLabel: @"Pause"];
1370                 [toolbarItem setPaletteLabel: @"Pause/Resume"];
1371                 
1372                 // Set up a reasonable tooltip, and image
1373                 [toolbarItem setToolTip: @"Pause Encoding"];
1374                 [toolbarItem setImage: [NSImage imageNamed: @"Pause"]];
1375                 
1376                 // Tell the item what message to send when it is clicked 
1377                 [toolbarItem setTarget: self];
1378                 [toolbarItem setAction: @selector(togglePauseResume:)];
1379         }
1380     
1381     return toolbarItem;
1382 }
1383
1384 //------------------------------------------------------------------------------------
1385 // toolbarDefaultItemIdentifiers:
1386 //------------------------------------------------------------------------------------
1387 - (NSArray *) toolbarDefaultItemIdentifiers: (NSToolbar *) toolbar
1388 {
1389     // Required delegate method: Returns the ordered list of items to be shown in the
1390     // toolbar by default.
1391     
1392     return [NSArray arrayWithObjects:
1393         HBQueueStartCancelToolbarIdentifier,
1394         HBQueuePauseResumeToolbarIdentifier,
1395         nil];
1396 }
1397
1398 //------------------------------------------------------------------------------------
1399 // toolbarAllowedItemIdentifiers:
1400 //------------------------------------------------------------------------------------
1401 - (NSArray *) toolbarAllowedItemIdentifiers: (NSToolbar *) toolbar
1402 {
1403     // Required delegate method: Returns the list of all allowed items by identifier.
1404     // By default, the toolbar does not assume any items are allowed, even the
1405     // separator. So, every allowed item must be explicitly listed.
1406
1407     return [NSArray arrayWithObjects:
1408         HBQueueStartCancelToolbarIdentifier,
1409         HBQueuePauseResumeToolbarIdentifier,
1410                 NSToolbarCustomizeToolbarItemIdentifier,
1411                 NSToolbarFlexibleSpaceItemIdentifier,
1412         NSToolbarSpaceItemIdentifier,
1413                 NSToolbarSeparatorItemIdentifier,
1414         nil];
1415 }
1416
1417 //------------------------------------------------------------------------------------
1418 // validateToolbarItem:
1419 //------------------------------------------------------------------------------------
1420 - (BOOL) validateToolbarItem: (NSToolbarItem *) toolbarItem
1421 {
1422     // Optional method: This message is sent to us since we are the target of some
1423     // toolbar item actions.
1424
1425     if (!fHandle) return NO;
1426
1427     BOOL enable = NO;
1428
1429     hb_state_t s;
1430     hb_get_state2 (fHandle, &s);
1431
1432     if ([[toolbarItem itemIdentifier] isEqual: HBQueueStartCancelToolbarIdentifier])
1433     {
1434         if ((s.state == HB_STATE_PAUSED) || (s.state == HB_STATE_WORKING) || (s.state == HB_STATE_MUXING))
1435         {
1436             enable = YES;
1437             [toolbarItem setImage:[NSImage imageNamed: @"Stop"]];
1438                         [toolbarItem setLabel: @"Stop"];
1439                         [toolbarItem setToolTip: @"Stop Encoding"];
1440         }
1441
1442         else if (hb_count(fHandle) > 0)
1443         {
1444             enable = YES;
1445             [toolbarItem setImage:[NSImage imageNamed: @"Play"]];
1446                         [toolbarItem setLabel: @"Start"];
1447                         [toolbarItem setToolTip: @"Start Encoding"];
1448         }
1449
1450         else
1451         {
1452             enable = NO;
1453             [toolbarItem setImage:[NSImage imageNamed: @"Play"]];
1454                         [toolbarItem setLabel: @"Start"];
1455                         [toolbarItem setToolTip: @"Start Encoding"];
1456         }
1457         }
1458     
1459     if ([[toolbarItem itemIdentifier] isEqual: HBQueuePauseResumeToolbarIdentifier])
1460     {
1461         if (s.state == HB_STATE_PAUSED)
1462         {
1463             enable = YES;
1464             [toolbarItem setImage:[NSImage imageNamed: @"Play"]];
1465                         [toolbarItem setLabel: @"Resume"];
1466                         [toolbarItem setToolTip: @"Resume Encoding"];
1467        }
1468         
1469         else if ((s.state == HB_STATE_WORKING) || (s.state == HB_STATE_MUXING))
1470         {
1471             enable = YES;
1472             [toolbarItem setImage:[NSImage imageNamed: @"Pause"]];
1473                         [toolbarItem setLabel: @"Pause"];
1474                         [toolbarItem setToolTip: @"Pause Encoding"];
1475         }
1476         else
1477         {
1478             enable = NO;
1479             [toolbarItem setImage:[NSImage imageNamed: @"Pause"]];
1480                         [toolbarItem setLabel: @"Pause"];
1481                         [toolbarItem setToolTip: @"Pause Encoding"];
1482         }
1483         }
1484     
1485         return enable;
1486 }
1487
1488 #pragma mark -
1489
1490 //------------------------------------------------------------------------------------
1491 // awakeFromNib
1492 //------------------------------------------------------------------------------------
1493 - (void)awakeFromNib
1494 {
1495     [self setupToolbar];
1496     
1497     if (![fQueueWindow setFrameUsingName:@"Queue"])
1498         [fQueueWindow center];
1499     [fQueueWindow setFrameAutosaveName: @"Queue"];
1500     [fQueueWindow setExcludedFromWindowsMenu:YES];
1501
1502 #if HB_QUEUE_DRAGGING
1503     [fOutlineView registerForDraggedTypes: [NSArray arrayWithObject:HBQueuePboardType] ];
1504     [fOutlineView setDraggingSourceOperationMask:NSDragOperationEvery forLocal:YES];
1505     [fOutlineView setVerticalMotionCanBeginDrag: YES];
1506 #endif
1507
1508     // Don't allow autoresizing of main column, else the "delete" column will get
1509     // pushed out of view.
1510     [fOutlineView setAutoresizesOutlineColumn: NO];
1511     [fOutlineView setIndentationPerLevel:21];
1512
1513 #if HB_OUTLINE_METRIC_CONTROLS
1514     [fIndentation setHidden: NO];
1515     [fSpacing setHidden: NO];
1516     [fIndentation setIntValue:[fOutlineView indentationPerLevel]];  // debug
1517     [fSpacing setIntValue:3];       // debug
1518 #endif
1519
1520     // Show/hide UI elements
1521     fCurrentJobPaneShown = YES;     // it's shown in the nib
1522     [self showCurrentJobPane:NO];
1523 }
1524
1525
1526 //------------------------------------------------------------------------------------
1527 // windowWillClose
1528 //------------------------------------------------------------------------------------
1529 - (void)windowWillClose:(NSNotification *)aNotification
1530 {
1531     [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"QueueWindowIsOpen"];
1532 }
1533
1534 #pragma mark -
1535 #pragma mark NSOutlineView delegate
1536
1537 - (id)outlineView:(NSOutlineView *)outlineView child:(int)index ofItem:(id)item
1538 {
1539     if (item == nil)
1540         return [fJobGroups objectAtIndex:index];
1541     
1542     // We are only one level deep, so we can't be asked about children
1543     NSAssert (NO, @"HBQueueController outlineView:child:ofItem: can't handle nested items.");
1544     return nil;
1545 }
1546
1547 - (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item
1548 {
1549     // Our outline view has no levels, but we can still expand every item. Doing so
1550     // just makes the row taller. See heightOfRowByItem below.
1551     return YES;
1552 }
1553
1554 - (int)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item
1555 {
1556     // Our outline view has no levels, so number of children will be zero for all
1557     // top-level items.
1558     if (item == nil)
1559         return [fJobGroups count];
1560     else
1561         return 0;
1562 }
1563
1564 - (void)outlineViewItemDidCollapse:(NSNotification *)notification
1565 {
1566     id item = [[notification userInfo] objectForKey:@"NSObject"];
1567     int row = [fOutlineView rowForItem:item];
1568     [fOutlineView noteHeightOfRowsWithIndexesChanged:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(row,1)]];
1569 }
1570
1571 - (void)outlineViewItemDidExpand:(NSNotification *)notification
1572 {
1573     id item = [[notification userInfo] objectForKey:@"NSObject"];
1574     int row = [fOutlineView rowForItem:item];
1575     [fOutlineView noteHeightOfRowsWithIndexesChanged:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(row,1)]];
1576 }
1577
1578 - (float)outlineView:(NSOutlineView *)outlineView heightOfRowByItem:(id)item
1579 {
1580     if ([outlineView isItemExpanded: item])
1581     {
1582         // Short-circuit here if in a live resize primarily to fix a bug but also to
1583         // increase resposivness during a resize. There's a bug in NSTableView that
1584         // causes row heights to get messed up if you try to change them during a live
1585         // resize. So if in a live resize, simply return the previously calculated
1586         // height. The row heights will get fixed up after the resize because we have
1587         // implemented viewDidEndLiveResize to force all of them to be recalculated.
1588         if ([outlineView inLiveResize] && [item lastDescriptionHeight] > 0)
1589             return [item lastDescriptionHeight];
1590         
1591         float width = [[outlineView tableColumnWithIdentifier: @"desc"] width];
1592         // Column width is NOT what is ultimately used
1593         width -= 47;    // 26 pixels for disclosure triangle, 20 for icon, 1 for intercell spacing
1594         
1595         float height = [item heightOfDescriptionForWidth: width withHBHandle: fHandle];
1596         return height;
1597     }
1598     else
1599         return HB_ROW_HEIGHT_TITLE_ONLY;
1600 }
1601
1602 - (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item
1603 {
1604     if ([[tableColumn identifier] isEqualToString:@"desc"])
1605         return [item attributedDescriptionWithHBHandle: fHandle];
1606     else
1607         return @"";
1608 }
1609
1610 - (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item
1611 {
1612     if ([[tableColumn identifier] isEqualToString:@"desc"])
1613     {
1614 #if HB_OUTLINE_METRIC_CONTROLS
1615         NSSize theSize = [cell imageSpacing];
1616         theSize.width = spacingWidth;
1617         [cell setImageSpacing: theSize];
1618 #endif
1619         
1620         // Set the image here since the value returned from outlineView:objectValueForTableColumn: didn't specify the image part
1621         [cell setImage:[NSImage imageNamed:@"JobSmall"]];
1622     }
1623     
1624     else if ([[tableColumn identifier] isEqualToString:@"delete"])
1625     {
1626         // The Delete action can only be applied for group items, not indivdual jobs.
1627         [cell setEnabled: YES];
1628         BOOL highlighted = [outlineView isRowSelected:[outlineView rowForItem: item]] && [[outlineView window] isKeyWindow] && ([[outlineView window] firstResponder] == outlineView);
1629         if (highlighted)
1630         {
1631             [cell setImage:[NSImage imageNamed:@"DeleteHighlight"]];
1632             [cell setAlternateImage:[NSImage imageNamed:@"DeleteHighlightPressed"]];
1633         }
1634         else
1635             [cell setImage:[NSImage imageNamed:@"Delete"]];
1636     }
1637 }
1638
1639 - (void)outlineView:(NSOutlineView *)outlineView willDisplayOutlineCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item
1640 {
1641     // By default, the discolsure image gets centered vertically in the cell. We want
1642     // always at the top.
1643     if ([outlineView isItemExpanded: item])
1644         [cell setImagePosition: NSImageAbove];
1645     else
1646         [cell setImagePosition: NSImageOnly];
1647 }
1648
1649 #pragma mark -
1650 #pragma mark NSOutlineView delegate (dragging related)
1651
1652 //------------------------------------------------------------------------------------
1653 // NSTableView delegate
1654 //------------------------------------------------------------------------------------
1655
1656 #if HB_QUEUE_DRAGGING
1657 - (BOOL)outlineView:(NSOutlineView *)outlineView writeItems:(NSArray *)items toPasteboard:(NSPasteboard *)pboard
1658 {
1659     fDraggedNodes = items; // Don't retain since this is just holding temporaral drag information, and it is only used during a drag!  We could put this in the pboard actually.
1660     
1661     // Provide data for our custom type, and simple NSStrings.
1662     [pboard declareTypes:[NSArray arrayWithObjects: HBQueuePboardType, nil] owner:self];
1663
1664     // the actual data doesn't matter since DragDropSimplePboardType drags aren't recognized by anyone but us!.
1665     [pboard setData:[NSData data] forType:HBQueuePboardType]; 
1666
1667     return YES;
1668 }
1669 #endif
1670
1671 #if HB_QUEUE_DRAGGING
1672 - (NSDragOperation)outlineView:(NSOutlineView *)outlineView validateDrop:(id <NSDraggingInfo>)info proposedItem:(id)item proposedChildIndex:(int)index
1673 {
1674     // Add code here to validate the drop
1675         BOOL isOnDropTypeProposal = index == NSOutlineViewDropOnItemIndex;
1676     if (isOnDropTypeProposal)
1677         return NSDragOperationNone;
1678         
1679     return NSDragOperationGeneric;
1680 }
1681 #endif
1682
1683 #if HB_QUEUE_DRAGGING
1684 - (BOOL)outlineView:(NSOutlineView *)outlineView acceptDrop:(id <NSDraggingInfo>)info item:(id)item childIndex:(int)index
1685 {
1686 //    NSPasteboard* pboard = [info draggingPasteboard];
1687 //    NSData* rowData = [pboard dataForType:HBQueuePboardType];
1688 //    NSIndexSet* rowIndexes = [NSKeyedUnarchiver unarchiveObjectWithData:rowData];
1689 //    int dragRow = [rowIndexes firstIndex];
1690  
1691     // Move the specified row to its new location...
1692     HBJob * draggedJob = [fDraggedNodes objectAtIndex: 0];
1693     NSLog(@"dragged job = %@", draggedJob);
1694     NSLog(@"drag from location = %d", [fJobGroups indexOfObject: draggedJob]);
1695     NSLog(@"drag to location = %d", index);
1696     NSLog(@"acceptDrop");
1697     
1698     return YES;
1699 }
1700 #endif
1701
1702 @end