OSDN Git Service

MacGui: add HB_AMIXDOWN_AC3 to HB QueueController so it reports the new hybrid sound...
[handbrake-jp/handbrake-jp-git.git] / macosx / HBQueueController.mm
index d9d62e4..594bce0 100644 (file)
@@ -8,16 +8,24 @@
 #include "Controller.h"
 #import "HBImageAndTextCell.h"
 
-// UNI_QUEUE turns on the feature where the first item in the queue NSTableView is the
-// current job followed by the jobs in hblib's queue. In this scheme, fCurrentJobPane
-// disappers.
-#define HB_UNI_QUEUE 0             // <--- NOT COMPLETELY FUNCTIONAL YET
-
 #define HB_ROW_HEIGHT_TITLE_ONLY           17.0
 
 // Pasteboard type for or drag operations
 #define HBQueuePboardType            @"HBQueuePboardType"
 
+//------------------------------------------------------------------------------------
+// Job ID Utilities
+//------------------------------------------------------------------------------------
+
+int MakeJobID(int jobGroupID, int sequenceNum)
+{
+    return jobGroupID<<16 | sequenceNum;
+}
+
+bool IsFirstPass(int jobID)
+{
+    return LoWord(jobID) == 0;
+}
 
 //------------------------------------------------------------------------------------
 // NSMutableAttributedString (HBAdditions)
     [super viewDidEndLiveResize];
 }
 
-@end
-
-//------------------------------------------------------------------------------------
-#pragma mark -
-#pragma mark Job group functions
-//------------------------------------------------------------------------------------
-// These could be part of hblib if we think hblib should have knowledge of groups.
-// For now, I see groups as a metaphor that HBQueueController provides.
-
-/**
- * Returns the number of jobs groups in the queue.
- * @param h Handle to hb_handle_t.
- * @return Number of job groups.
- */
-static int hb_group_count(hb_handle_t * h)    
+#if HB_QUEUE_DRAGGING
+- (NSImage *)dragImageForRowsWithIndexes:(NSIndexSet *)dragRows tableColumns:(NSArray *)tableColumns event:(NSEvent*)dragEvent offset:(NSPointPointer)dragImageOffset
 {
-    hb_job_t * job;
-    int count = 0;
-    int index = 0;
-    while( ( job = hb_job( h, index++ ) ) )
-    {
-        if (job->sequence_id == 0)
-            count++;
-    }
-    return count;
+    // Set the fIsDragging flag so that other's know that a drag operation is being
+    // performed.
+       fIsDragging = YES;
+
+    // By default, NSTableView only drags an image of the first column. Change this to
+    // drag an image of the queue's icon and desc columns.
+    NSArray * cols = [NSArray arrayWithObjects: [self tableColumnWithIdentifier:@"icon"], [self tableColumnWithIdentifier:@"desc"], nil];
+    return [super dragImageForRowsWithIndexes:dragRows tableColumns:cols event:dragEvent offset:dragImageOffset];
 }
+#endif
 
-/**
- * Returns handle to the first job in the i-th group within the job list.
- * @param h Handle to hb_handle_t.
- * @param i Index of group.
- * @returns Handle to hb_job_t of desired job.
- */
-static hb_job_t * hb_group(hb_handle_t * h, int i)    
+#if HB_QUEUE_DRAGGING
+- (void) mouseDown:(NSEvent *)theEvent
 {
-    hb_job_t * job;
-    int count = 0;
-    int index = 0;
-    while( ( job = hb_job( h, index++ ) ) )
-    {
-        if (job->sequence_id == 0)
-        {
-            if (count == i)
-                return job;
-            count++;
-        }
-    }
-    return NULL;
+    // After a drag operation, reset fIsDragging back to NO. This is really the only way
+    // for us to detect when a drag has finished. You can't do it in acceptDrop because
+    // that won't be called if the dragged item is released outside the view.
+    [super mouseDown:theEvent];
+       fIsDragging = NO;
 }
+#endif
 
-/**
- * Removes a groups of jobs from the job list.
- * @param h Handle to hb_handle_t.
- * @param job Handle to the first job in the group.
- */
-static void hb_rem_group( hb_handle_t * h, hb_job_t * job )
+#if HB_QUEUE_DRAGGING
+- (BOOL) isDragging;
 {
-    // Find job in list
-    hb_job_t * j;
-    int index = 0;
-    while( ( j = hb_job( h, index ) ) )
-    {
-        if (j == job)
-        {
-            // Delete this job plus the following ones in the sequence
-            hb_rem( h, job );
-            while( ( j = hb_job( h, index ) ) && (j->sequence_id != 0) )
-                hb_rem( h, j );
-            return;
-        }
-        else
-            index++;
-    }
+    return fIsDragging;
 }
+#endif
 
-/**
- * Returns handle to the next job after the given job.
- * @param h Handle to hb_handle_t.
- * @param job Handle to the a job in the group.
- * @returns Handle to hb_job_t of desired job or NULL if no such job.
- */
-static hb_job_t * hb_next_job( hb_handle_t * h, hb_job_t * job )
-{
-    hb_job_t * j = NULL;
-    int index = 0;
-    while( ( j = hb_job( h, index++ ) ) )
-    {
-        if (j == job)
-            return hb_job( h, index );
-    }
-    return NULL;
-}
+@end
 
 #pragma mark -
 
@@ -151,34 +100,122 @@ static hb_job_t * hb_next_job( hb_handle_t * h, hb_job_t * job )
 // HBJob
 //------------------------------------------------------------------------------------
 
+static NSMutableParagraphStyle * _descriptionParagraphStyle = NULL;
+static NSDictionary* _detailAttribute = NULL;
+static NSDictionary* _detailBoldAttribute = NULL;
+static NSDictionary* _titleAttribute = NULL;
+static NSDictionary* _shortHeightAttribute = NULL;
+
 @implementation HBJob
 
-+ (HBJob*) jobWithJob: (hb_job_t *) job
++ (HBJob*) jobWithLibhbJob: (hb_job_t *) job
 {
-    return [[[HBJob alloc] initWithJob:job] autorelease];
+    return [[[HBJob alloc] initWithLibhbJob:job] autorelease];
 }
 
-- (id) initWithJob: (hb_job_t *) job
+- (id) initWithLibhbJob: (hb_job_t *) job
 {
     if (self = [super init])
     {
-        // job is not owned by HBJob. It does not get dealloacted when HBJob is released.
-        hbJob = job;
+        sequence_id = job->sequence_id;
+
+        chapter_start = job->chapter_start;
+        chapter_end = job->chapter_end;
+        chapter_markers = job->chapter_markers;
+        memcpy(crop, job->crop, sizeof(crop));
+        deinterlace = job->deinterlace;
+        width = job->width;
+        height = job->height;
+        keep_ratio = job->keep_ratio;
+        grayscale = job->grayscale;
+        pixel_ratio = job->pixel_ratio;
+        pixel_aspect_width = job->pixel_aspect_width;
+        pixel_aspect_height = job->pixel_aspect_height;
+        vcodec = job->vcodec;
+        vquality = job->vquality;
+        vbitrate = job->vbitrate;
+        vrate = job->vrate;
+        vrate_base = job->vrate_base;
+        pass = job->pass;
+        h264_level = job->h264_level;
+        crf = job->crf;
+        if (job->x264opts)
+            x264opts = [[NSString stringWithUTF8String:job->x264opts] retain];
+        memcpy(audio_mixdowns, job->audio_mixdowns, sizeof(audio_mixdowns));
+        acodec = job->acodec;
+        abitrate = job->abitrate;
+        arate = job->arate;
+        subtitle = job->subtitle;
+        mux = job->mux;
+        if (job->file)
+            file = [[NSString stringWithUTF8String:job->file] retain];
+        if (job->title->name)
+            titleName = [[NSString stringWithUTF8String:job->title->name] retain];
+        titleIndex = job->title->index;
+        titleWidth = job->title->width;
+        titleHeight = job->title->height;
+        if (job->subtitle >= 0)
+        {
+            hb_subtitle_t * aSubtitle = (hb_subtitle_t *) hb_list_item(job->title->list_subtitle, job->subtitle);
+            if (aSubtitle)
+                subtitleLang = [[NSString stringWithUTF8String:aSubtitle->lang] retain];
+        }
+
+        // Calculate and store output dimensions and anamorphic dimensions
+        if (pixel_ratio == 1) // Original PAR Implementation, now called Strict Anamorphic
+        {
+            output_width = titleWidth - crop[2] - crop[3];
+            output_height = titleHeight - crop[0] - crop[1];
+            anamorphic_width = output_width * pixel_aspect_width / pixel_aspect_height;
+            anamorphic_height = output_height;
+        }
+        else if (pixel_ratio == 2) // Loose Anamorphic
+        {
+            // call hb_set_anamorphic_size to do a "dry run" to get the values to be
+            // used by libhb for loose anamorphic.
+            int par_width, par_height;
+            hb_set_anamorphic_size(job, &output_width, &output_height, &par_width, &par_height);
+            anamorphic_width = output_width * par_width / par_height;
+            anamorphic_height = output_height;
+        }
+        else    // No Anamorphic
+        {
+            output_width = width;
+            output_height = height;
+            anamorphic_width = 0;       // not needed for this case
+            anamorphic_height = 0;      // not needed for this case
+        }
+        
     }
-    return self; 
+    return self;
+}
+
+- (void) dealloc
+{
+    // jobGroup is a weak reference and does not need to be deleted
+    [x264opts release];
+    [file release];
+    [titleName release];
+    [subtitleLang release];
+    [super dealloc];
+}
+
+- (HBJobGroup *) jobGroup
+{
+    return jobGroup;
 }
 
-- (hb_job_t*) job
+- (void) setJobGroup: (HBJobGroup *)aJobGroup
 {
-    return hbJob;
+    // This is a weak reference. We don't retain or release it.
+    jobGroup = aJobGroup;
 }
 
 //------------------------------------------------------------------------------------
 // Generate string to display in UI.
 //------------------------------------------------------------------------------------
 
-- (NSMutableAttributedString *) attributedDescriptionWithHBHandle: (hb_handle_t *)handle
-                               withIcon: (BOOL)withIcon
+- (NSMutableAttributedString *) attributedDescriptionWithIcon: (BOOL)withIcon
                               withTitle: (BOOL)withTitle
                            withPassName: (BOOL)withPassName
                          withFormatInfo: (BOOL)withFormatInfo
@@ -192,34 +229,12 @@ static hb_job_t * hb_next_job( hb_handle_t * h, hb_job_t * job )
 {
     NSMutableAttributedString * finalString = [[[NSMutableAttributedString alloc] initWithString: @""] autorelease];
     
-    hb_title_t * title = hbJob->title;
-    
     // Attributes
-    static NSMutableParagraphStyle * ps = NULL;
-    if (!ps)
-    {
-        ps = [[[NSParagraphStyle defaultParagraphStyle] mutableCopy] retain];
-        [ps setHeadIndent: 40.0];
-        [ps setParagraphSpacing: 1.0];
-        [ps setTabStops:[NSArray array]];    // clear all tabs
-        [ps addTabStop: [[[NSTextTab alloc] initWithType: NSLeftTabStopType location: 20.0] autorelease]];
-    }
-
-    static NSDictionary* detailAttribute = [[NSDictionary dictionaryWithObjectsAndKeys:
-                [NSFont systemFontOfSize:10.0], NSFontAttributeName,
-                ps, NSParagraphStyleAttributeName,
-                nil] retain];
-    static NSDictionary* detailBoldAttribute = [[NSDictionary dictionaryWithObjectsAndKeys:
-                [NSFont boldSystemFontOfSize:10.0], NSFontAttributeName,
-                ps, NSParagraphStyleAttributeName,
-                nil] retain];
-    static NSDictionary* titleAttribute = [[NSDictionary dictionaryWithObjectsAndKeys:
-                [NSFont systemFontOfSize:[NSFont systemFontSize]], NSFontAttributeName,
-                ps, NSParagraphStyleAttributeName,
-                nil] retain];
-    static NSDictionary* shortHeightAttribute = [[NSDictionary dictionaryWithObjectsAndKeys:
-                [NSFont systemFontOfSize:2.0], NSFontAttributeName,
-                nil] retain];
+    NSMutableParagraphStyle * ps = [HBJob descriptionParagraphStyle];
+    NSDictionary* detailAttr = [HBJob descriptionDetailAttribute];
+    NSDictionary* detailBoldAttr = [HBJob descriptionDetailBoldAttribute];
+    NSDictionary* titleAttr = [HBJob descriptionTitleAttribute];
+    NSDictionary* shortHeightAttr = [HBJob descriptionShortHeightAttribute];
 
     // Title with summary
     if (withTitle)
@@ -244,15 +259,15 @@ static hb_job_t * hb_next_job( hb_handle_t * h, hb_job_t * job )
     
         // Note: use title->name instead of title->dvd since name is just the chosen
         // folder, instead of dvd which is the full path
-        [finalString appendString:[NSString stringWithUTF8String:title->name] withAttributes:titleAttribute];
+        [finalString appendString:titleName withAttributes:titleAttr];
         
         NSString * summaryInfo;
     
-        NSString * chapterString = (hbJob->chapter_start == hbJob->chapter_end) ?
-                [NSString stringWithFormat:@"Chapter %d", hbJob->chapter_start] :
-                [NSString stringWithFormat:@"Chapters %d through %d", hbJob->chapter_start, hbJob->chapter_end];
+        NSString * chapterString = (chapter_start == chapter_end) ?
+                [NSString stringWithFormat:@"Chapter %d", chapter_start] :
+                [NSString stringWithFormat:@"Chapters %d through %d", chapter_start, chapter_end];
 
-        BOOL hasIndepthScan = (hbJob->pass == -1);
+        BOOL hasIndepthScan = (pass == -1);
         int numVideoPasses = 0;
 
         // To determine number of video passes, we need to skip past the subtitle scan.
@@ -260,30 +275,29 @@ static hb_job_t * hb_next_job( hb_handle_t * h, hb_job_t * job )
         {
             // When job is the one currently being processed, then the next in its group
             // is the the first job in the queue.
-            hb_job_t * nextjob;
-            if (hbJob == hb_current_job(handle))
-                nextjob = hb_job(handle, 0);
-            else
-                nextjob = hb_next_job(handle, hbJob);
+            HBJob * nextjob = nil;
+            unsigned int index = [jobGroup indexOfJob:self];
+            if (index != NSNotFound)
+                nextjob = [jobGroup jobAtIndex:index+1];
             if (nextjob)    // Overly cautious in case there is no next job!
                 numVideoPasses = MIN( 2, nextjob->pass + 1 );
         }
         else
-            numVideoPasses = MIN( 2, hbJob->pass + 1 );
+            numVideoPasses = MIN( 2, pass + 1 );
 
         if (hasIndepthScan && numVideoPasses == 1)
-            summaryInfo = [NSString stringWithFormat: @"  (Title %d, %@, Deep Scan, Single Video Pass)", title->index, chapterString];
+            summaryInfo = [NSString stringWithFormat: @"  (Title %d, %@, Deep Scan, Single Video Pass)", titleIndex, chapterString];
         else if (hasIndepthScan && numVideoPasses > 1)
-            summaryInfo = [NSString stringWithFormat: @"  (Title %d, %@, Deep Scan, %d Video Passes)", title->index, chapterString, numVideoPasses];
+            summaryInfo = [NSString stringWithFormat: @"  (Title %d, %@, Deep Scan, %d Video Passes)", titleIndex, chapterString, numVideoPasses];
         else if (numVideoPasses == 1)
-            summaryInfo = [NSString stringWithFormat: @"  (Title %d, %@, Single Video Pass)", title->index, chapterString];
+            summaryInfo = [NSString stringWithFormat: @"  (Title %d, %@, Single Video Pass)", titleIndex, chapterString];
         else
-            summaryInfo = [NSString stringWithFormat: @"  (Title %d, %@, %d Video Passes)", title->index, chapterString, numVideoPasses];
+            summaryInfo = [NSString stringWithFormat: @"  (Title %d, %@, %d Video Passes)", titleIndex, chapterString, numVideoPasses];
 
-        [finalString appendString:[NSString stringWithFormat:@"%@\n", summaryInfo] withAttributes:detailAttribute];
+        [finalString appendString:[NSString stringWithFormat:@"%@\n", summaryInfo] withAttributes:detailAttr];
         
         // Insert a short-in-height line to put some white space after the title
-        [finalString appendString:@"\n" withAttributes:shortHeightAttribute];
+        [finalString appendString:@"\n" withAttributes:shortHeightAttr];
     }
     
     // End of title stuff
@@ -295,7 +309,7 @@ static hb_job_t * hb_next_job( hb_handle_t * h, hb_job_t * job )
         if (withIcon)
         {
             NSString * imageName;
-            switch (hbJob->pass)
+            switch (pass)
             {
                 case -1: imageName = @"JobPassSubtitleSmall"; break;
                 case  0: imageName = @"JobPassFirstSmall"; break;
@@ -321,11 +335,11 @@ static hb_job_t * hb_next_job( hb_handle_t * h, hb_job_t * job )
         }
     
         NSString * jobPassName;
-        if (hbJob->pass == -1)
+        if (pass == -1)
             jobPassName = NSLocalizedString (@"Deep Scan", nil);
         else
         {
-            int passNum = MAX( 1, hbJob->pass );
+            int passNum = MAX( 1, pass );
             if (passNum == 0)
                 jobPassName = NSLocalizedString (@"1st Pass", nil);
             else if (passNum == 1)
@@ -335,7 +349,7 @@ static hb_job_t * hb_next_job( hb_handle_t * h, hb_job_t * job )
             else
                 jobPassName = [NSString stringWithFormat: NSLocalizedString(@"Pass %d", nil), passNum];
         }
-        [finalString appendString:[NSString stringWithFormat:@"%@\n", jobPassName] withAttributes:detailBoldAttribute];
+        [finalString appendString:[NSString stringWithFormat:@"%@\n", jobPassName] withAttributes:detailBoldAttr];
     }
 
     // Video Codec needed by FormatInfo and withVideoInfo
@@ -343,15 +357,15 @@ static hb_job_t * hb_next_job( hb_handle_t * h, hb_job_t * job )
     if (withFormatInfo || withVideoInfo)
     {
         // 2097152
-        /* Video Codec settings (Encoder in the gui) */
-        if (hbJob->vcodec == HB_VCODEC_FFMPEG)
+        // Video Codec settings (Encoder in the gui)
+        if (vcodec == HB_VCODEC_FFMPEG)
             jobVideoCodec = @"FFmpeg"; // HB_VCODEC_FFMPEG
-        else if (hbJob->vcodec == HB_VCODEC_XVID)
+        else if (vcodec == HB_VCODEC_XVID)
             jobVideoCodec = @"XviD"; // HB_VCODEC_XVID
-        else if (hbJob->vcodec == HB_VCODEC_X264)
+        else if (vcodec == HB_VCODEC_X264)
         {
-            /* Deterimine for sure how we are now setting iPod uuid atom */
-            if (hbJob->h264_level) // We are encoding for iPod
+            // Deterimine for sure how we are now setting iPod uuid atom
+            if (h264_level) // We are encoding for iPod
                 jobVideoCodec = @"x264 (H.264 iPod)"; // HB_VCODEC_X264    
             else
                 jobVideoCodec = @"x264 (H.264 Main)"; // HB_VCODEC_X264
@@ -364,13 +378,13 @@ static hb_job_t * hb_next_job( hb_handle_t * h, hb_job_t * job )
     NSString * jobAudioCodec = nil;
     if (withFormatInfo || withAudioInfo)
     {
-        if (hbJob->acodec == 256)
+        if (acodec == 256)
             jobAudioCodec = @"AAC"; // HB_ACODEC_FAAC
-        else if (hbJob->acodec == 512)
+        else if (acodec == 512)
             jobAudioCodec = @"MP3"; // HB_ACODEC_LAME
-        else if (hbJob->acodec == 1024)
+        else if (acodec == 1024)
             jobAudioCodec = @"Vorbis"; // HB_ACODEC_VORBIS
-        else if (hbJob->acodec == 2048)
+        else if (acodec == 2048)
             jobAudioCodec = @"AC3"; // HB_ACODEC_AC3
     }
     if (jobAudioCodec == nil)
@@ -381,59 +395,54 @@ static hb_job_t * hb_next_job( hb_handle_t * h, hb_job_t * job )
     {
         NSString * jobFormatInfo;
         // Muxer settings (File Format in the gui)
-        if (hbJob->mux == 65536 || hbJob->mux == 131072 || hbJob->mux == 1048576)
+        if (mux == 65536 || mux == 131072 || mux == 1048576)
             jobFormatInfo = @"MP4"; // HB_MUX_MP4,HB_MUX_PSP,HB_MUX_IPOD
-        else if (hbJob->mux == 262144)
+        else if (mux == 262144)
             jobFormatInfo = @"AVI"; // HB_MUX_AVI
-        else if (hbJob->mux == 524288)
+        else if (mux == 524288)
             jobFormatInfo = @"OGM"; // HB_MUX_OGM
-        else if (hbJob->mux == 2097152)
+        else if (mux == 2097152)
             jobFormatInfo = @"MKV"; // HB_MUX_MKV
         else
             jobFormatInfo = @"unknown";
                 
-        if (hbJob->chapter_markers == 1)
+        if (chapter_markers == 1)
             jobFormatInfo = [NSString stringWithFormat:@"%@ Container, %@ Video + %@ Audio, Chapter Markers\n", jobFormatInfo, jobVideoCodec, jobAudioCodec];
         else
             jobFormatInfo = [NSString stringWithFormat:@"%@ Container, %@ Video + %@ Audio\n", jobFormatInfo, jobVideoCodec, jobAudioCodec];
             
-        [finalString appendString: @"Format: " withAttributes:detailBoldAttribute];
-        [finalString appendString: jobFormatInfo withAttributes:detailAttribute];
+        [finalString appendString: @"Format: " withAttributes:detailBoldAttr];
+        [finalString appendString: jobFormatInfo withAttributes:detailAttr];
     }
 
     if (withDestination)
     {
-        [finalString appendString: @"Destination: " withAttributes:detailBoldAttribute];
-        [finalString appendString:[NSString stringWithFormat:@"%@\n", [NSString stringWithUTF8String:hbJob->file]] withAttributes:detailAttribute];
+        [finalString appendString: @"Destination: " withAttributes:detailBoldAttr];
+        [finalString appendString:[NSString stringWithFormat:@"%@\n", file] withAttributes:detailAttr];
     }
 
 
     if (withPictureInfo)
     {
         NSString * jobPictureInfo;
-        /*integers for picture values deinterlace, crop[4], keep_ratio, grayscale, pixel_ratio, pixel_aspect_width, pixel_aspect_height,
-         maxWidth, maxHeight */
-        if (hbJob->pixel_ratio == 1)
-        {
-            int titlewidth = title->width - hbJob->crop[2] - hbJob->crop[3];
-            int displayparwidth = titlewidth * hbJob->pixel_aspect_width / hbJob->pixel_aspect_height;
-            int displayparheight = title->height - hbJob->crop[0] - hbJob->crop[1];
-            jobPictureInfo = [NSString stringWithFormat:@"%dx%d (%dx%d Anamorphic)", displayparwidth, displayparheight, hbJob->width, displayparheight];
-        }
+        if (pixel_ratio == 1) // Original PAR Implementation, now called Strict Anamorphic
+            jobPictureInfo = [NSString stringWithFormat:@"%d x %d (%d x %d Strict Anamorphic)", output_width, output_height, anamorphic_width, anamorphic_height];
+        else if (pixel_ratio == 2) // Loose Anamorphic
+            jobPictureInfo = [NSString stringWithFormat:@"%d x %d (%d x %d Loose Anamorphic)", output_width, output_height, anamorphic_width, anamorphic_height];
         else
-            jobPictureInfo = [NSString stringWithFormat:@"%dx%d", hbJob->width, hbJob->height];
-        if (hbJob->keep_ratio == 1)
+            jobPictureInfo = [NSString stringWithFormat:@"%d x %d", output_width, output_height];
+        if (keep_ratio == 1)
             jobPictureInfo = [jobPictureInfo stringByAppendingString:@" Keep Aspect Ratio"];
         
-        if (hbJob->grayscale == 1)
+        if (grayscale == 1)
             jobPictureInfo = [jobPictureInfo stringByAppendingString:@", Grayscale"];
         
-        if (hbJob->deinterlace == 1)
+        if (deinterlace == 1)
             jobPictureInfo = [jobPictureInfo stringByAppendingString:@", Deinterlace"];
         if (withIcon)   // implies indent the info
-            [finalString appendString: @"\t" withAttributes:detailBoldAttribute];
-        [finalString appendString: @"Picture: " withAttributes:detailBoldAttribute];
-        [finalString appendString:[NSString stringWithFormat:@"%@\n", jobPictureInfo] withAttributes:detailAttribute];
+            [finalString appendString: @"\t" withAttributes:detailBoldAttr];
+        [finalString appendString: @"Picture: " withAttributes:detailBoldAttr];
+        [finalString appendString:[NSString stringWithFormat:@"%@\n", jobPictureInfo] withAttributes:detailAttr];
     }
     
     if (withVideoInfo)
@@ -441,48 +450,48 @@ static hb_job_t * hb_next_job( hb_handle_t * h, hb_job_t * job )
         NSString * jobVideoQuality;
         NSString * jobVideoDetail;
         
-        if (hbJob->vquality <= 0 || hbJob->vquality >= 1)
-            jobVideoQuality = [NSString stringWithFormat:@"%d kbps", hbJob->vbitrate];
+        if (vquality <= 0 || vquality >= 1)
+            jobVideoQuality = [NSString stringWithFormat:@"%d kbps", vbitrate];
         else
         {
             NSNumber * vidQuality;
-            vidQuality = [NSNumber numberWithInt:hbJob->vquality * 100];
+            vidQuality = [NSNumber numberWithInt:vquality * 100];
             // this is screwed up kind of. Needs to be formatted properly.
-            if (hbJob->crf == 1)
+            if (crf == 1)
                 jobVideoQuality = [NSString stringWithFormat:@"%@%% CRF", vidQuality];            
             else
                 jobVideoQuality = [NSString stringWithFormat:@"%@%% CQP", vidQuality];
         }
         
-        if (hbJob->vrate_base == 1126125)
+        if (vrate_base == 1126125)
         {
-            /* NTSC FILM 23.976 */
+            // NTSC FILM 23.976
             jobVideoDetail = [NSString stringWithFormat:@"%@, %@, 23.976 fps", jobVideoCodec, jobVideoQuality];
         }
-        else if (hbJob->vrate_base == 900900)
+        else if (vrate_base == 900900)
         {
-            /* NTSC 29.97 */
+            // NTSC 29.97
             jobVideoDetail = [NSString stringWithFormat:@"%@, %@, 29.97 fps", jobVideoCodec, jobVideoQuality];
         }
         else
         {
-            /* Everything else */
-            jobVideoDetail = [NSString stringWithFormat:@"%@, %@, %d fps", jobVideoCodec, jobVideoQuality, hbJob->vrate / hbJob->vrate_base];
+            // Everything else
+            jobVideoDetail = [NSString stringWithFormat:@"%@, %@, %d fps", jobVideoCodec, jobVideoQuality, vrate / vrate_base];
         }
         if (withIcon)   // implies indent the info
-            [finalString appendString: @"\t" withAttributes:detailBoldAttribute];
-        [finalString appendString: @"Video: " withAttributes:detailBoldAttribute];
-        [finalString appendString:[NSString stringWithFormat:@"%@\n", jobVideoDetail] withAttributes:detailAttribute];
+            [finalString appendString: @"\t" withAttributes:detailBoldAttr];
+        [finalString appendString: @"Video: " withAttributes:detailBoldAttr];
+        [finalString appendString:[NSString stringWithFormat:@"%@\n", jobVideoDetail] withAttributes:detailAttr];
     }
     
     if (withx264Info)
     {
-        if (hbJob->vcodec == HB_VCODEC_X264 && hbJob->x264opts)
+        if (vcodec == HB_VCODEC_X264 && x264opts)
         {
             if (withIcon)   // implies indent the info
-                [finalString appendString: @"\t" withAttributes:detailBoldAttribute];
-            [finalString appendString: @"x264 Options: " withAttributes:detailBoldAttribute];
-            [finalString appendString:[NSString stringWithFormat:@"%@\n", [NSString stringWithUTF8String:hbJob->x264opts]] withAttributes:detailAttribute];
+                [finalString appendString: @"\t" withAttributes:detailBoldAttr];
+            [finalString appendString: @"x264 Options: " withAttributes:detailBoldAttr];
+            [finalString appendString:[NSString stringWithFormat:@"%@\n", x264opts] withAttributes:detailAttr];
         }
     }
 
@@ -492,52 +501,52 @@ static hb_job_t * hb_next_job( hb_handle_t * h, hb_job_t * job )
         if ([jobAudioCodec isEqualToString: @"AC3"])
             jobAudioInfo = [NSString stringWithFormat:@"%@, Pass-Through", jobAudioCodec];
         else
-            jobAudioInfo = [NSString stringWithFormat:@"%@, %d kbps, %d Hz", jobAudioCodec, hbJob->abitrate, hbJob->arate];
+            jobAudioInfo = [NSString stringWithFormat:@"%@, %d kbps, %d Hz", jobAudioCodec, abitrate, arate];
         
-        /* we now get the audio mixdown info for each of the two gui audio tracks */
-        /* lets do it the long way here to get a handle on things.
-            Hardcoded for two tracks for gui: audio_mixdowns[i] audio_mixdowns[i] */
+        // we now get the audio mixdown info for each of the two gui audio tracks
+        // lets do it the long way here to get a handle on things.
+        // Hardcoded for two tracks for gui: audio_mixdowns[i] audio_mixdowns[i]
         int ai; // counter for each audios [] , macgui only allows for two audio tracks currently
         for( ai = 0; ai < 2; ai++ )
         {
-            if (hbJob->audio_mixdowns[ai] == HB_AMIXDOWN_MONO)
+            if (audio_mixdowns[ai] == HB_AMIXDOWN_MONO)
                 jobAudioInfo = [jobAudioInfo stringByAppendingString:[NSString stringWithFormat:@", Track %d: Mono", ai + 1]];
-            if (hbJob->audio_mixdowns[ai] == HB_AMIXDOWN_STEREO)
+            if (audio_mixdowns[ai] == HB_AMIXDOWN_STEREO)
                 jobAudioInfo = [jobAudioInfo stringByAppendingString:[NSString stringWithFormat:@", Track %d: Stereo", ai + 1]];
-            if (hbJob->audio_mixdowns[ai] == HB_AMIXDOWN_DOLBY)
+            if (audio_mixdowns[ai] == HB_AMIXDOWN_DOLBY)
                 jobAudioInfo = [jobAudioInfo stringByAppendingString:[NSString stringWithFormat:@", Track %d: Dolby Surround", ai + 1]];
-            if (hbJob->audio_mixdowns[ai] == HB_AMIXDOWN_DOLBYPLII)
+            if (audio_mixdowns[ai] == HB_AMIXDOWN_DOLBYPLII)
                 jobAudioInfo = [jobAudioInfo stringByAppendingString:[NSString stringWithFormat:@", Track %d: Dolby Pro Logic II", ai + 1]];
-            if (hbJob->audio_mixdowns[ai] == HB_AMIXDOWN_6CH)
-                jobAudioInfo = [jobAudioInfo stringByAppendingString:[NSString stringWithFormat:@", Track %d: 6-channel discreet", ai + 1]];
+            if (audio_mixdowns[ai] == HB_AMIXDOWN_AC3)
+                jobAudioInfo = [jobAudioInfo stringByAppendingString:[NSString stringWithFormat:@", Pass-Through", ai + 1]];
+                
         }
         if (withIcon)   // implies indent the info
-            [finalString appendString: @"\t" withAttributes:detailBoldAttribute];
-        [finalString appendString: @"Audio: " withAttributes:detailBoldAttribute];
-        [finalString appendString:[NSString stringWithFormat:@"%@\n", jobAudioInfo] withAttributes:detailAttribute];
+            [finalString appendString: @"\t" withAttributes:detailBoldAttr];
+        [finalString appendString: @"Audio: " withAttributes:detailBoldAttr];
+        [finalString appendString:[NSString stringWithFormat:@"%@\n", jobAudioInfo] withAttributes:detailAttr];
     }
     
     if (withSubtitleInfo)
     {
-        // hbJob->subtitle can == -1 in two cases:
+        // subtitle scan == -1 in two cases:
         // autoselect: when pass == -1
         // none: when pass != -1
-        if ((hbJob->subtitle == -1) && (hbJob->pass == -1))
+        if ((subtitle == -1) && (pass == -1))
         {
             if (withIcon)   // implies indent the info
-                [finalString appendString: @"\t" withAttributes:detailBoldAttribute];
-            [finalString appendString: @"Subtitles: " withAttributes:detailBoldAttribute];
-            [finalString appendString: @"Autoselect " withAttributes:detailAttribute];
+                [finalString appendString: @"\t" withAttributes:detailBoldAttr];
+            [finalString appendString: @"Subtitles: " withAttributes:detailBoldAttr];
+            [finalString appendString: @"Autoselect " withAttributes:detailAttr];
         }
-        else if (hbJob->subtitle >= 0)
+        else if (subtitle >= 0)
         {
-            hb_subtitle_t * subtitle = (hb_subtitle_t *) hb_list_item( title->list_subtitle, 0 );
-            if (subtitle)
+            if (subtitleLang)
             {
                 if (withIcon)   // implies indent the info
-                    [finalString appendString: @"\t" withAttributes:detailBoldAttribute];
-                [finalString appendString: @"Subtitles: " withAttributes:detailBoldAttribute];
-                [finalString appendString: [NSString stringWithCString: subtitle->lang] withAttributes:detailAttribute];
+                    [finalString appendString: @"\t" withAttributes:detailBoldAttr];
+                [finalString appendString: @"Subtitles: " withAttributes:detailBoldAttr];
+                [finalString appendString: subtitleLang   withAttributes:detailAttr];
             }
         }
     }
@@ -549,6 +558,59 @@ static hb_job_t * hb_next_job( hb_handle_t * h, hb_job_t * job )
     return finalString;
 }
 
++ (NSMutableParagraphStyle *) descriptionParagraphStyle
+{
+    if (!_descriptionParagraphStyle)
+    {
+        _descriptionParagraphStyle = [[[NSParagraphStyle defaultParagraphStyle] mutableCopy] retain];
+        [_descriptionParagraphStyle setHeadIndent: 40.0];
+        [_descriptionParagraphStyle setParagraphSpacing: 1.0];
+        [_descriptionParagraphStyle setTabStops:[NSArray array]];    // clear all tabs
+        [_descriptionParagraphStyle addTabStop: [[[NSTextTab alloc] initWithType: NSLeftTabStopType location: 20.0] autorelease]];
+    }
+    return _descriptionParagraphStyle;
+}
+
++ (NSDictionary *) descriptionDetailAttribute
+{
+    if (!_detailAttribute)
+        _detailAttribute = [[NSDictionary dictionaryWithObjectsAndKeys:
+                [NSFont systemFontOfSize:10.0], NSFontAttributeName,
+                _descriptionParagraphStyle, NSParagraphStyleAttributeName,
+                nil] retain];
+    return _detailAttribute;
+}
+
++ (NSDictionary *) descriptionDetailBoldAttribute
+{
+    if (!_detailBoldAttribute)
+        _detailBoldAttribute = [[NSDictionary dictionaryWithObjectsAndKeys:
+                [NSFont boldSystemFontOfSize:10.0], NSFontAttributeName,
+                _descriptionParagraphStyle, NSParagraphStyleAttributeName,
+                nil] retain];
+    return _detailBoldAttribute;
+}
+
++ (NSDictionary *) descriptionTitleAttribute
+{
+    if (!_titleAttribute)
+        _titleAttribute = [[NSDictionary dictionaryWithObjectsAndKeys:
+                [NSFont systemFontOfSize:[NSFont systemFontSize]], NSFontAttributeName,
+                _descriptionParagraphStyle, NSParagraphStyleAttributeName,
+                nil] retain];
+    return _titleAttribute;
+}
+
++ (NSDictionary *) descriptionShortHeightAttribute
+{
+    if (!_shortHeightAttribute)
+        _shortHeightAttribute = [[NSDictionary dictionaryWithObjectsAndKeys:
+                [NSFont systemFontOfSize:2.0], NSFontAttributeName,
+                nil] retain];
+    return _shortHeightAttribute;
+}
+
+
 @end
 
 #pragma mark -
@@ -557,6 +619,9 @@ static hb_job_t * hb_next_job( hb_handle_t * h, hb_job_t * job )
 // HBJobGroup
 //------------------------------------------------------------------------------------
 
+// Notification sent from HBJobGroup setStatus whenever the status changes.
+NSString *HBJobGroupStatusNotification = @"HBJobGroupStatusNotification";
+
 @implementation HBJobGroup
 
 + (HBJobGroup *) jobGroup;
@@ -571,12 +636,14 @@ static hb_job_t * hb_next_job( hb_handle_t * h, hb_job_t * job )
         fJobs = [[NSMutableArray arrayWithCapacity:0] retain];
         fDescription = [[NSMutableAttributedString alloc] initWithString: @""];
         [self setNeedsDescription: NO];
+        fStatus = HBStatusNone;
     }
     return self; 
 }
 
 - (void) dealloc
 {
+    [fPresetName release];
     [fJobs release];
     [super dealloc];
 }
@@ -588,6 +655,7 @@ static hb_job_t * hb_next_job( hb_handle_t * h, hb_job_t * job )
 
 - (void) addJob: (HBJob *)aJob
 {
+    [aJob setJobGroup:self];
     [fJobs addObject: aJob];
     [self setNeedsDescription: YES];
     fLastDescriptionHeight = 0;
@@ -614,16 +682,44 @@ static hb_job_t * hb_next_job( hb_handle_t * h, hb_job_t * job )
     fNeedsDescription = flag;
 }
 
-- (void) updateDescriptionWithHBHandle: (hb_handle_t *)handle
+- (void) updateDescription
 {
+    fNeedsDescription = NO;
+
     [fDescription deleteCharactersInRange: NSMakeRange(0, [fDescription length])]; 
 
+    if ([self count] == 0)
+    {
+        NSAssert(NO, @" jobgroup with no jobs");
+        return;
+    }
+    
     HBJob * job = [self jobAtIndex:0];
     
-    [fDescription appendAttributedString: [job attributedDescriptionWithHBHandle: handle
-                             withIcon: NO
+    // append the title
+    [fDescription appendAttributedString: [job attributedDescriptionWithIcon: NO
                             withTitle: YES
                          withPassName: NO
+                       withFormatInfo: NO
+                      withDestination: NO
+                      withPictureInfo: NO
+                        withVideoInfo: NO
+                         withx264Info: NO
+                        withAudioInfo: NO
+                     withSubtitleInfo: NO]];
+
+    // append the preset name
+    if ([fPresetName length])
+    {
+        [fDescription appendString:@"Preset: " withAttributes:[HBJob descriptionDetailBoldAttribute]];
+        [fDescription appendString:fPresetName withAttributes:[HBJob descriptionDetailAttribute]];
+        [fDescription appendString:@"\n" withAttributes:[HBJob descriptionDetailAttribute]];
+    }
+    
+    // append the format and destinaton
+    [fDescription appendAttributedString: [job attributedDescriptionWithIcon: NO
+                            withTitle: NO
+                         withPassName: NO
                        withFormatInfo: YES
                       withDestination: YES
                       withPictureInfo: NO
@@ -632,16 +728,16 @@ static hb_job_t * hb_next_job( hb_handle_t * h, hb_job_t * job )
                         withAudioInfo: NO
                      withSubtitleInfo: NO]];
 
+
     static NSAttributedString * carriageReturn = [[NSAttributedString alloc] initWithString:@"\n"];
     
     NSEnumerator * e = [self jobEnumerator];
     while ( (job = [e nextObject]) )
     {
-        int pass = [job job]->pass;
+        int pass = job->pass;
         [fDescription appendAttributedString:carriageReturn];
         [fDescription appendAttributedString:
-            [job attributedDescriptionWithHBHandle: handle
-                                 withIcon: YES
+            [job attributedDescriptionWithIcon: YES
                                 withTitle: NO
                              withPassName: YES
                            withFormatInfo: NO
@@ -653,24 +749,23 @@ static hb_job_t * hb_next_job( hb_handle_t * h, hb_job_t * job )
                          withSubtitleInfo: YES]];
     }
     
-    fNeedsDescription = NO;
 }
 
-- (NSMutableAttributedString *) attributedDescriptionWithHBHandle: (hb_handle_t *)handle
+- (NSMutableAttributedString *) attributedDescription
 {
     if (fNeedsDescription)
-        [self updateDescriptionWithHBHandle: handle];
+        [self updateDescription];
     return fDescription;
 }
 
-- (float) heightOfDescriptionForWidth:(float)width withHBHandle: (hb_handle_t *)handle
+- (float) heightOfDescriptionForWidth:(float)width
 {
     // Try to return the cached value if no changes have happened since the last time
     if ((width == fLastDescriptionWidth) && (fLastDescriptionHeight != 0) && !fNeedsDescription)
         return fLastDescriptionHeight;
     
     if (fNeedsDescription)
-        [self updateDescriptionWithHBHandle: handle];
+        [self updateDescription];
 
     // Calculate the height    
     NSRect bounds = [fDescription boundingRectWithSize:NSMakeSize(width, 10000) options:NSStringDrawingUsesLineFragmentOrigin];
@@ -697,6 +792,46 @@ static hb_job_t * hb_next_job( hb_handle_t * h, hb_job_t * job )
     return fLastDescriptionHeight;
 }
 
+- (void) setStatus: (HBQueueJobGroupStatus)status
+{
+    // Create a dictionary with the old status
+    NSDictionary * userInfo = [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:self->fStatus] forKey:@"HBOldJobGroupStatus"];
+
+    self->fStatus = status;
+    
+    // Send notification with old status
+    [[NSNotificationCenter defaultCenter] postNotificationName:HBJobGroupStatusNotification object:self userInfo:userInfo];
+}
+
+- (HBQueueJobGroupStatus) status
+{
+    return self->fStatus;
+}
+
+- (void) setPresetName: (NSString *)name
+{
+    [name retain];
+    [fPresetName release];
+    fPresetName = name;
+}
+
+- (NSString *) presetName
+{
+    return fPresetName;
+}
+
+- (NSString *) name
+{
+    HBJob * firstJob = [self jobAtIndex:0];
+    return firstJob ? firstJob->titleName : nil;
+}
+
+- (NSString *) destinationPath
+{
+    HBJob * firstJob = [self jobAtIndex:0];
+    return firstJob ? firstJob->file : nil;
+}
+
 @end
 
 
@@ -707,6 +842,7 @@ static NSString*    HBQueueToolbar                            = @"HBQueueToolbar
 static NSString*    HBQueueStartCancelToolbarIdentifier       = @"HBQueueStartCancelToolbarIdentifier";
 static NSString*    HBQueuePauseResumeToolbarIdentifier       = @"HBQueuePauseResumeToolbarIdentifier";
 
+#pragma mark -
 
 @implementation HBQueueController
 
@@ -725,6 +861,13 @@ static NSString*    HBQueuePauseResumeToolbarIdentifier       = @"HBQueuePauseRe
             nil]];
 
         fJobGroups = [[NSMutableArray arrayWithCapacity:0] retain];
+
+        BOOL loadSucceeded = [NSBundle loadNibNamed:@"Queue" owner:self] && fQueueWindow;
+        NSAssert(loadSucceeded, @"Could not open Queue nib");
+        NSAssert(fQueueWindow, @"fQueueWindow not found in Queue nib");
+        
+        // Register for HBJobGroup status changes
+        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(jobGroupStatusNotification:) name:HBJobGroupStatusNotification object:nil];
     }
     return self; 
 }
@@ -734,14 +877,16 @@ static NSString*    HBQueuePauseResumeToolbarIdentifier       = @"HBQueuePauseRe
 //------------------------------------------------------------------------------------
 - (void)dealloc
 {
-    [fAnimation release];
-    
     // clear the delegate so that windowWillClose is not attempted
     if ([fQueueWindow delegate] == self)
         [fQueueWindow setDelegate:nil];
     
     [fJobGroups release];
+    [fCurrentJobGroup release];
     [fSavedExpandedItems release];
+    [fSavedSelectedItems release];
+
+    [[NSNotificationCenter defaultCenter] removeObserver:self];
 
     [super dealloc];
 }
@@ -762,24 +907,38 @@ static NSString*    HBQueuePauseResumeToolbarIdentifier       = @"HBQueuePauseRe
     fHBController = controller;
 }
 
+#pragma mark -
+#pragma mark - Getting the currently processing job group
+
 //------------------------------------------------------------------------------------
-// Displays and brings the queue window to the front
+// Returns the HBJobGroup that is currently being encoded; nil if no encoding is
+// occurring.
 //------------------------------------------------------------------------------------
-- (IBAction) showQueueWindow: (id)sender
+- (HBJobGroup *) currentJobGroup;
 {
-    if (!fQueueWindow)
-    {
-        BOOL loadSucceeded = [NSBundle loadNibNamed:@"Queue" owner:self] && fQueueWindow;
-        NSAssert(loadSucceeded, @"Could not open Queue nib file");
-    }
+    return fCurrentJobGroup;
+}
+
+//------------------------------------------------------------------------------------
+// Returns the HBJob (pass) that is currently being encoded; nil if no encoding is
+// occurring.
+//------------------------------------------------------------------------------------
+- (HBJob *) currentJob
+{
+    return fCurrentJob;
+}
 
-    [self updateQueueUI];
-    [self updateCurrentJobUI];
+#pragma mark -
 
+//------------------------------------------------------------------------------------
+// Displays and brings the queue window to the front
+//------------------------------------------------------------------------------------
+- (IBAction) showQueueWindow: (id)sender
+{
     [fQueueWindow makeKeyAndOrderFront: self];
-
     [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"QueueWindowIsOpen"];
 }
+
 //------------------------------------------------------------------------------------
 // Show or hide the current job pane (fCurrentJobPane).
 //------------------------------------------------------------------------------------
@@ -818,50 +977,177 @@ static NSString*    HBQueuePauseResumeToolbarIdentifier       = @"HBQueuePauseRe
         [NSValue valueWithRect:queueFrame], NSViewAnimationEndFrameKey,
         nil];
 
-    if (!fAnimation)
-        fAnimation = [[NSViewAnimation alloc] initWithViewAnimations:nil];
-
-    [fAnimation setViewAnimations:[NSArray arrayWithObjects:dict1, dict2, nil]];
-    [fAnimation setDuration:0.25];
-    [fAnimation setAnimationBlockingMode:NSAnimationBlocking]; // prevent user from resizing the window during an animation
-    [fAnimation startAnimation];
+    NSViewAnimation * anAnimation = [[[NSViewAnimation alloc] initWithViewAnimations:nil] autorelease];
+    [anAnimation setViewAnimations:[NSArray arrayWithObjects:dict1, dict2, nil]];
+    [anAnimation setDuration:0.25];
+    [anAnimation setAnimationBlockingMode:NSAnimationBlocking]; // prevent user from resizing the window during an animation
+    [anAnimation startAnimation];
+    
     fCurrentJobPaneShown = showPane;
 }
 
 //------------------------------------------------------------------------------------
-// Rebuilds the contents of fJobGroups which is a hierarchy of HBJobGroup and HBJobs.
+// Sets fCurrentJobGroup to a new job group.
 //------------------------------------------------------------------------------------
-- (void)rebuildJobGroups
+- (void) setCurrentJobGroup: (HBJobGroup *)aJobGroup
 {
-    [fJobGroups autorelease];
-    fJobGroups = [[NSMutableArray arrayWithCapacity:0] retain];
+    if (aJobGroup)
+        [aJobGroup setStatus: HBStatusWorking];
+
+    [aJobGroup retain];
+    [fCurrentJobGroup release];
+    fCurrentJobGroup = aJobGroup;
+}
 
-    HBJobGroup * aJobGroup = [HBJobGroup jobGroup];
+#pragma mark - Finding job groups
 
-    hb_job_t * nextJob = hb_group( fHandle, 0 );
-    while( nextJob )
+//------------------------------------------------------------------------------------
+// Returns the first pending job with a specified destination path or nil if no such
+// job exists.
+//------------------------------------------------------------------------------------
+- (HBJobGroup *) pendingJobGroupWithDestinationPath: (NSString *)path
+{
+    HBJobGroup * aJobGroup;
+    NSEnumerator * groupEnum = [fJobGroups objectEnumerator];
+    while ( (aJobGroup = [groupEnum nextObject]) )
+    {
+        if ([[aJobGroup destinationPath] isEqualToString: path])
+            return aJobGroup;
+    }
+    return nil;
+}
+
+//------------------------------------------------------------------------------------
+// Locates and returns a HBJob whose sequence_id matches a specified value.
+//------------------------------------------------------------------------------------
+- (HBJob *) findJobWithID: (int)aJobID
+{
+    HBJobGroup * aJobGroup;
+    NSEnumerator * groupEnum = [fJobGroups objectEnumerator];
+    while ( (aJobGroup = [groupEnum nextObject]) )
     {
-        if (nextJob->sequence_id == 0)
+        HBJob * job;
+        NSEnumerator * jobEnum = [aJobGroup jobEnumerator];
+        while ( (job = [jobEnum nextObject]) )
         {
-            // Encountered a new group. Add the current one to fJobGroups and then start a new one.
-            if ([aJobGroup count] > 0)
-            {
-                [fJobGroups addObject: aJobGroup];
-                aJobGroup = [HBJobGroup jobGroup];
-            }
+            if (job->sequence_id == aJobID)
+                return job;
         }
-        [aJobGroup addJob: [HBJob jobWithJob:nextJob]];
-        nextJob = hb_next_job (fHandle, nextJob);
     }
-    if ([aJobGroup count] > 0)
+    return nil;
+}
+
+//------------------------------------------------------------------------------------
+// Locates and returns a libhb job whose sequence_id matches a specified value.
+//------------------------------------------------------------------------------------
+- (hb_job_t *) findLibhbJobWithID: (int)aJobID
+{
+    hb_job_t * job;
+    int index = 0;
+    while( ( job = hb_job( fHandle, index++ ) ) )
+    {
+        if (job->sequence_id == aJobID)
+            return job;
+    }
+    return nil;
+}
+
+#pragma mark -
+#pragma mark Queue Counts
+
+//------------------------------------------------------------------------------------
+// Sets a flag indicating that the values for fPendingCount, fCompletedCount,
+// fCanceledCount, and fWorkingCount need to be recalculated.
+//------------------------------------------------------------------------------------
+- (void) setJobGroupCountsNeedUpdating: (BOOL)flag
+{
+    fJobGroupCountsNeedUpdating = flag;
+}
+
+//------------------------------------------------------------------------------------
+// Recalculates and stores new values in fPendingCount, fCompletedCount,
+// fCanceledCount, and fWorkingCount.
+//------------------------------------------------------------------------------------
+- (void) recalculateJobGroupCounts
+{
+    fPendingCount = 0;
+    fCompletedCount = 0;
+    fCanceledCount = 0;
+    fWorkingCount = 0;
+
+    NSEnumerator * groupEnum = [fJobGroups objectEnumerator];
+    HBJobGroup * aJobGroup;
+    while ( (aJobGroup = [groupEnum nextObject]) )
     {
-        [fJobGroups addObject:aJobGroup];
+        switch ([aJobGroup status])
+        {
+            case HBStatusNone:
+                // We don't track these.
+                break;
+            case HBStatusPending:
+                fPendingCount++;
+                break;
+            case HBStatusCompleted:
+                fCompletedCount++;
+                break;
+            case HBStatusCanceled:
+                fCanceledCount++;
+                break;
+            case HBStatusWorking:
+                fWorkingCount++;
+                break;
+        }
     }
+    fJobGroupCountsNeedUpdating = NO;
 }
 
 //------------------------------------------------------------------------------------
-// Saves the state of the items that are currently expanded. Calling restoreOutlineViewState
-// will restore the state of all items to match what was saved by saveOutlineViewState.
+// Returns the number of job groups whose status is HBStatusPending.
+//------------------------------------------------------------------------------------
+- (unsigned int) pendingCount
+{
+    if (fJobGroupCountsNeedUpdating)
+        [self recalculateJobGroupCounts];
+    return fPendingCount;
+}
+
+//------------------------------------------------------------------------------------
+// Returns the number of job groups whose status is HBStatusCompleted.
+//------------------------------------------------------------------------------------
+- (unsigned int) completedCount
+{
+    if (fJobGroupCountsNeedUpdating)
+        [self recalculateJobGroupCounts];
+    return fCompletedCount;
+}
+
+//------------------------------------------------------------------------------------
+// Returns the number of job groups whose status is HBStatusCanceled.
+//------------------------------------------------------------------------------------
+- (unsigned int) canceledCount
+{
+    if (fJobGroupCountsNeedUpdating)
+        [self recalculateJobGroupCounts];
+    return fCanceledCount;
+}
+
+//------------------------------------------------------------------------------------
+// Returns the number of job groups whose status is HBStatusWorking.
+//------------------------------------------------------------------------------------
+- (unsigned int) workingCount
+{
+    if (fJobGroupCountsNeedUpdating)
+        [self recalculateJobGroupCounts];
+    return fWorkingCount;
+}
+
+#pragma mark -
+#pragma mark UI Updating
+
+//------------------------------------------------------------------------------------
+// Saves the state of the items that are currently expanded and selected. Calling
+// restoreOutlineViewState will restore the state of all items to match what was saved
+// by saveOutlineViewState. Nested calls to saveOutlineViewState are not supported.
 //------------------------------------------------------------------------------------
 - (void) saveOutlineViewState
 {
@@ -870,34 +1156,33 @@ static NSString*    HBQueuePauseResumeToolbarIdentifier       = @"HBQueuePauseRe
     else
         [fSavedExpandedItems removeAllIndexes];
     
-    // NB: This code is stuffing the address of each job into an index set. While it
-    // works 99.9% of the time, it's not the ideal solution. We need unique ids in
-    // each job, possibly using the existing sequence_id field. Could use the high
-    // word as a unique encode id and the low word the sequence number.
+    // This code stores the sequence_id of the first job of each job group into an
+    // index set. This is sufficient to identify each group uniquely.
     
     HBJobGroup * aJobGroup;
     NSEnumerator * e = [fJobGroups objectEnumerator];
     while ( (aJobGroup = [e nextObject]) )
     {
         if ([fOutlineView isItemExpanded: aJobGroup])
-            [fSavedExpandedItems addIndex: (unsigned int)[[aJobGroup jobAtIndex:0] job]];
+            [fSavedExpandedItems addIndex: [aJobGroup jobAtIndex:0]->sequence_id];
     }
     
-    // Save the selection also. This is really UGLY code. Since I have to rebuild the
-    // entire outline hierachy every time hblib changes its job list, there's no easy
-    // way for me to remember the selection state other than saving off the first
-    // hb_job_t item in each selected group. This is done by saving the object's
-    // address. This could go away if I'd save a unique id in each job object.
-
-    int selection = [fOutlineView selectedRow];
-    if (selection == -1)
-        fSavedSelectedItem = 0;
+    // Save the selection also.
+
+    if (!fSavedSelectedItems)
+        fSavedSelectedItems = [[NSMutableIndexSet alloc] init];
     else
+        [fSavedSelectedItems removeAllIndexes];
+
+    NSIndexSet * selectedRows = [fOutlineView selectedRowIndexes];
+    int row = [selectedRows firstIndex];
+    while (row != NSNotFound)
     {
-        HBJobGroup * jobGroup = [fOutlineView itemAtRow: selection];
-        fSavedSelectedItem = (unsigned int)[[jobGroup jobAtIndex:0] job];
+        aJobGroup = [fOutlineView itemAtRow: row];
+        [fSavedSelectedItems addIndex: [aJobGroup jobAtIndex:0]->sequence_id];
+        row = [selectedRows indexGreaterThanIndex: row];
     }
-    
+
 }
 
 //------------------------------------------------------------------------------------
@@ -912,42 +1197,105 @@ static NSString*    HBQueuePauseResumeToolbarIdentifier       = @"HBQueuePauseRe
         NSEnumerator * e = [fJobGroups objectEnumerator];
         while ( (aJobGroup = [e nextObject]) )
         {
-            hb_job_t * j = [[aJobGroup jobAtIndex:0] job];
-            if ([fSavedExpandedItems containsIndex: (unsigned int)j])
+            HBJob * job = [aJobGroup jobAtIndex:0];
+            if (job && [fSavedExpandedItems containsIndex: job->sequence_id])
                 [fOutlineView expandItem: aJobGroup];
         }
     }
     
-    if (fSavedSelectedItem)
+    if (fSavedSelectedItems)
     {
-        // Ugh. Have to cycle through each row looking for the previously selected job.
-        // See the explanation in saveExpandedItems about the logic here.
-        
-        // Find out what hb_job_t was selected
-        hb_job_t * j = (hb_job_t *)fSavedSelectedItem;
-        
-        int rowToSelect = -1;
-        for (int i = 0; i < [fOutlineView numberOfRows]; i++)
+        NSMutableIndexSet * rowsToSelect = [[[NSMutableIndexSet alloc] init] autorelease];
+        HBJobGroup * aJobGroup;
+        NSEnumerator * e = [fJobGroups objectEnumerator];
+        int i = 0;
+        while ( (aJobGroup = [e nextObject]) )
         {
-            HBJobGroup * jobGroup = [fOutlineView itemAtRow: i];
-            // Test to see if the group's first job is a match
-            if ([[jobGroup jobAtIndex:0] job] == j)
-            {
-                rowToSelect = i;
-                break;
-            }
+            HBJob * job = [aJobGroup jobAtIndex:0];
+            if (job && [fSavedSelectedItems containsIndex: job->sequence_id])
+                [rowsToSelect addIndex: i];
+            i++;
         }
-        if (rowToSelect == -1)
+        if ([rowsToSelect count] == 0)
             [fOutlineView deselectAll: nil];
         else
-            [fOutlineView selectRow:rowToSelect byExtendingSelection:NO];
+            [fOutlineView selectRowIndexes:rowsToSelect byExtendingSelection:NO];
+    }
+}
+
+//------------------------------------------------------------------------------------
+// Marks the icon region of a job group in the queue view as needing display.
+//------------------------------------------------------------------------------------
+- (void) updateJobGroupIconInQueue:(HBJobGroup*)aJobGroup
+{
+    int row = [fOutlineView rowForItem: aJobGroup];
+    int col = [fOutlineView columnWithIdentifier: @"icon"];
+    if (row != -1 && col != -1)
+    {
+        NSRect frame = [fOutlineView frameOfCellAtColumn:col row:row];
+        [fOutlineView setNeedsDisplayInRect: frame];
+    }
+}
+
+//------------------------------------------------------------------------------------
+// Marks the entire region of a job group in the queue view as needing display.
+//------------------------------------------------------------------------------------
+- (void) updateJobGroupInQueue:(HBJobGroup*)aJobGroup
+{
+    int row = [fOutlineView rowForItem: aJobGroup];
+    if (row != -1)
+    {
+        NSRect frame = [fOutlineView rectOfRow:row];
+        [fOutlineView setNeedsDisplayInRect: frame];
+    }
+}
+
+//------------------------------------------------------------------------------------
+// If a job is currently processing, its job icon in the queue outline view is
+// animated to its next state.
+//------------------------------------------------------------------------------------
+- (void) animateCurrentJobGroupInQueue:(NSTimer*)theTimer
+{
+    if (fCurrentJobGroup)
+    {
+        fAnimationIndex++;
+        fAnimationIndex %= 6;   // there are 6 animation images; see outlineView:objectValueForTableColumn:byItem: below.
+        [self updateJobGroupIconInQueue: fCurrentJobGroup];
+    }
+}
+
+//------------------------------------------------------------------------------------
+// Starts animating the job icon of the currently processing job in the queue outline
+// view.
+//------------------------------------------------------------------------------------
+- (void) startAnimatingCurrentJobGroupInQueue
+{
+    if (!fAnimationTimer)
+        fAnimationTimer = [[NSTimer scheduledTimerWithTimeInterval:1.0/12.0     // 1/12 because there are 6 images in the animation cycle
+                target:self
+                selector:@selector(animateCurrentJobGroupInQueue:)
+                userInfo:nil
+                repeats:YES] retain];
+}
+
+//------------------------------------------------------------------------------------
+// Stops animating the job icon of the currently processing job in the queue outline
+// view.
+//------------------------------------------------------------------------------------
+- (void) stopAnimatingCurrentJobGroupInQueue
+{
+    if (fAnimationTimer && [fAnimationTimer isValid])
+    {
+        [fAnimationTimer invalidate];
+        [fAnimationTimer release];
+        fAnimationTimer = nil;
     }
 }
 
 //------------------------------------------------------------------------------------
 // Generate string to display in UI.
 //------------------------------------------------------------------------------------
-- (NSString *) progressStatusStringForJob: (hb_job_t *)job state: (hb_state_t *)s
+- (NSString *) progressStatusStringForJob: (HBJob *)job state: (hb_state_t *)s
 {
     if (s->state == HB_STATE_WORKING)
     {
@@ -987,7 +1335,7 @@ static NSString*    HBQueuePauseResumeToolbarIdentifier       = @"HBQueuePauseRe
 //------------------------------------------------------------------------------------
 // Generate string to display in UI.
 //------------------------------------------------------------------------------------
-- (NSString *) progressTimeRemainingStringForJob: (hb_job_t *)job state: (hb_state_t *)s
+- (NSString *) progressTimeRemainingStringForJob: (HBJob *)job state: (hb_state_t *)s
 {
     if (s->state == HB_STATE_WORKING)
     {
@@ -1057,6 +1405,18 @@ static NSString*    HBQueuePauseResumeToolbarIdentifier       = @"HBQueuePauseRe
 }
 
 //------------------------------------------------------------------------------------
+// Refresh progress bar (fProgressTextField) from current state.
+//------------------------------------------------------------------------------------
+- (void) updateProgressTextForJob: (HBJob *)job state: (hb_state_t *)s
+{
+    NSString * statusMsg = [self progressStatusStringForJob:job state:s];
+    NSString * timeMsg = [self progressTimeRemainingStringForJob:job state:s];
+    if ([timeMsg length] > 0)
+        statusMsg = [NSString stringWithFormat:@"%@ - %@", statusMsg, timeMsg];
+    [fProgressTextField setStringValue:statusMsg];
+}
+
+//------------------------------------------------------------------------------------
 // Refresh progress bar (fProgressBar) from current state.
 //------------------------------------------------------------------------------------
 - (void) updateProgressBarWithState: (hb_state_t *)s
@@ -1081,8 +1441,12 @@ static NSString*    HBQueuePauseResumeToolbarIdentifier       = @"HBQueuePauseRe
     else if (s->state == HB_STATE_WORKDONE)
     {
         [fProgressBar setIndeterminate:NO];
+        [fProgressBar stopAnimation:nil];
         [fProgressBar setDoubleValue:0.0];
     }
+    
+    else
+        [fProgressBar stopAnimation:nil];    // just in case in was animating
 }
 
 //------------------------------------------------------------------------------------
@@ -1091,169 +1455,209 @@ static NSString*    HBQueuePauseResumeToolbarIdentifier       = @"HBQueuePauseRe
 - (void)updateQueueCountField
 {
     NSString * msg;
-    int jobCount;
-    
-    jobCount = fHandle ? hb_group_count(fHandle) : 0;
-    if (jobCount == 1)
-        msg = NSLocalizedString(@"1 pending encode", nil);
-    else
-        msg = [NSString stringWithFormat:NSLocalizedString(@"%d pending encodes", nil), jobCount];
+    int jobCount = [fJobGroups count];
+    int pendingCount = [self pendingCount];
+    if (jobCount == 0)
+        msg = NSLocalizedString(@"No encodes", nil);
+    else if ((jobCount == 1) && (pendingCount == 0))
+        msg = NSLocalizedString(@"1 encode", nil);
+    else if (jobCount == pendingCount)  // ie, all jobs listed are pending
+    {
+        if (jobCount == 1)
+            msg = NSLocalizedString(@"1 pending encode", nil);
+        else
+            msg = [NSString stringWithFormat:NSLocalizedString(@"%d pending encodes", nil), pendingCount];
+    }
+    else    // some completed, some pending
+        msg = [NSString stringWithFormat:NSLocalizedString(@"%d encodes (%d pending)", nil), jobCount, pendingCount];
 
     [fQueueCountField setStringValue:msg];
 }
 
 //------------------------------------------------------------------------------------
 // Refresh the UI in the current job pane. Should be called whenever the current job
-// being processed has changed or when progress has changed.
+// being processed has changed.
 //------------------------------------------------------------------------------------
-- (void)updateCurrentJobUI
+- (void)updateCurrentJobDescription
 {
-    hb_state_t s;
-    hb_job_t * job = nil;
-    
-    if (fHandle)
-    {
-        hb_get_state2( fHandle, &s );
-        job = hb_current_job(fHandle);
-    }
-
-    if (job)
+    if (fCurrentJob)
     {
-        if (fLastKnownCurrentJob != job)
+        switch (fCurrentJob->pass)
         {
-            HBJob * currentJob = [HBJob jobWithJob: job];
-            
-            switch (job->pass)
-            {
-                case -1:  // Subtitle scan
-                    [fJobDescTextField setAttributedStringValue:
-                        [currentJob attributedDescriptionWithHBHandle:fHandle
-                                     withIcon: NO
-                                    withTitle: YES
-                                 withPassName: YES
-                               withFormatInfo: NO
-                              withDestination: NO
-                              withPictureInfo: NO
-                                withVideoInfo: NO
-                                 withx264Info: NO
-                                withAudioInfo: NO
-                             withSubtitleInfo: YES]];
-                    break;
-                    
-                case 1:  // video 1st pass
-                    [fJobDescTextField setAttributedStringValue:
-                        [currentJob attributedDescriptionWithHBHandle:fHandle
-                                     withIcon: NO
-                                    withTitle: YES
-                                 withPassName: YES
-                               withFormatInfo: NO
-                              withDestination: NO
-                              withPictureInfo: YES
-                                withVideoInfo: YES
-                                 withx264Info: YES
-                                withAudioInfo: NO
-                             withSubtitleInfo: NO]];
-                    break;
-                
-                case 0:  // single pass
-                case 2:  // video 2nd pass + audio
-                    [fJobDescTextField setAttributedStringValue:
-                        [currentJob attributedDescriptionWithHBHandle:fHandle
-                                     withIcon: NO
-                                    withTitle: YES
-                                 withPassName: YES
-                               withFormatInfo: NO
-                              withDestination: NO
-                              withPictureInfo: YES
-                                withVideoInfo: YES
-                                 withx264Info: YES
-                                withAudioInfo: YES
-                             withSubtitleInfo: YES]];
-                    break;
+            case -1:  // Subtitle scan
+                [fJobDescTextField setAttributedStringValue:
+                    [fCurrentJob attributedDescriptionWithIcon: NO
+                                withTitle: YES
+                             withPassName: YES
+                           withFormatInfo: NO
+                          withDestination: NO
+                          withPictureInfo: NO
+                            withVideoInfo: NO
+                             withx264Info: NO
+                            withAudioInfo: NO
+                         withSubtitleInfo: YES]];
+                break;
                 
-                default: // unknown
-                    [fJobDescTextField setAttributedStringValue:
-                        [currentJob attributedDescriptionWithHBHandle:fHandle
-                                     withIcon: NO
-                                    withTitle: YES
-                                 withPassName: YES
-                               withFormatInfo: NO
-                              withDestination: NO
-                              withPictureInfo: YES
-                                withVideoInfo: YES
-                                 withx264Info: YES
-                                withAudioInfo: YES
-                             withSubtitleInfo: YES]];
-            }
-
-            [self showCurrentJobPane:YES];
-            [fJobIconView setImage: [NSImage imageNamed:@"JobLarge"]];
+            case 1:  // video 1st pass
+                [fJobDescTextField setAttributedStringValue:
+                    [fCurrentJob attributedDescriptionWithIcon: NO
+                                withTitle: YES
+                             withPassName: YES
+                           withFormatInfo: NO
+                          withDestination: NO
+                          withPictureInfo: YES
+                            withVideoInfo: YES
+                             withx264Info: YES
+                            withAudioInfo: NO
+                         withSubtitleInfo: NO]];
+                break;
+            
+            case 0:  // single pass
+            case 2:  // video 2nd pass + audio
+                [fJobDescTextField setAttributedStringValue:
+                    [fCurrentJob attributedDescriptionWithIcon: NO
+                                withTitle: YES
+                             withPassName: YES
+                           withFormatInfo: NO
+                          withDestination: NO
+                          withPictureInfo: YES
+                            withVideoInfo: YES
+                             withx264Info: YES
+                            withAudioInfo: YES
+                         withSubtitleInfo: YES]];
+                break;
+            
+            default: // unknown
+                [fJobDescTextField setAttributedStringValue:
+                    [fCurrentJob attributedDescriptionWithIcon: NO
+                                withTitle: YES
+                             withPassName: YES
+                           withFormatInfo: NO
+                          withDestination: NO
+                          withPictureInfo: YES
+                            withVideoInfo: YES
+                             withx264Info: YES
+                            withAudioInfo: YES
+                         withSubtitleInfo: YES]];
         }
-
-        NSString * statusMsg = [self progressStatusStringForJob:job state:&s];
-        NSString * timeMsg = [self progressTimeRemainingStringForJob:job state:&s];
-        if ([timeMsg length] > 0)
-            statusMsg = [NSString stringWithFormat:@"%@ - %@", statusMsg, timeMsg];
-        [fProgressTextField setStringValue:statusMsg];
-        [self updateProgressBarWithState:&s];
     }
+    
     else
-    {
-        [fJobDescTextField setStringValue:NSLocalizedString(@"No job processing", nil)];
+        [fJobDescTextField setStringValue: @"No encodes pending"];
+}
 
-        [self showCurrentJobPane:NO];
-        [fProgressBar stopAnimation:nil];    // just in case in was animating
-    }
-        
-    fLastKnownCurrentJob = job;
+//------------------------------------------------------------------------------------
+// Refresh the UI in the current job pane. Should be called whenever the current job
+// being processed has changed or when progress has changed.
+//------------------------------------------------------------------------------------
+- (void)updateCurrentJobProgress
+{
+    hb_state_t s;
+    hb_get_state2( fHandle, &s );
+    [self updateProgressTextForJob: fCurrentJob state: &s];
+    [self updateProgressBarWithState:&s];
 }
 
 //------------------------------------------------------------------------------------
-// Refresh the UI in the queue pane. Should be called whenever the content of HB's job
-// list has changed so that HBQueueController can sync up.
+// Notifies HBQueuecontroller that the contents of fJobGroups is about to be modified.
+// HBQueuecontroller remembers the state of the UI (selection and expanded items).
 //------------------------------------------------------------------------------------
-- (void)updateQueueUI
+- (void) beginEditingJobGroupsArray
 {
     [self saveOutlineViewState];
-    [self rebuildJobGroups];
+}
+
+//------------------------------------------------------------------------------------
+// Notifies HBQueuecontroller that modifications to fJobGroups as indicated by a prior
+// call to beginEditingJobGroupsArray have been completed. HBQueuecontroller reloads
+// the queue view and restores the state of the UI (selection and expanded items).
+//------------------------------------------------------------------------------------
+- (void) endEditingJobGroupsArray
+{
+    [self setJobGroupCountsNeedUpdating:YES];
     [fOutlineView noteNumberOfRowsChanged];
     [fOutlineView reloadData];
     [self restoreOutlineViewState];    
     [self updateQueueCountField];
 }
 
+#pragma mark -
+#pragma mark Actions
+
 //------------------------------------------------------------------------------------
-// Deletes the selected job from HB and the queue UI
+// Deletes the selected jobs from HB and the queue UI
 //------------------------------------------------------------------------------------
-- (IBAction)removeSelectedJob: (id)sender
+- (IBAction)removeSelectedJobGroups: (id)sender
 {
     if (!fHandle) return;
     
-    int row = [sender selectedRow];
-    if (row != -1)
+    NSIndexSet * selectedRows = [fOutlineView selectedRowIndexes];
+    int row = [selectedRows firstIndex];
+    if (row != NSNotFound)
     {
-#if HB_UNI_QUEUE
-        if (row == 0)
+        [self beginEditingJobGroupsArray];
+        while (row != NSNotFound)
         {
-            [self cancelCurrentJob:sender];
+            HBJobGroup * jobGroup = [fOutlineView itemAtRow: row];
+            switch ([jobGroup status])
+            {
+                case HBStatusCompleted:
+                case HBStatusCanceled:
+                    [fJobGroups removeObject: jobGroup];
+                    break;
+                case HBStatusWorking:
+                    [self cancelCurrentJob: sender];
+                    break;
+                case HBStatusPending:
+                    // Remove from libhb
+                    HBJob * job;
+                    NSEnumerator * e = [jobGroup jobEnumerator];
+                    while (job = [e nextObject])
+                    {
+                        hb_job_t * libhbJob = [self findLibhbJobWithID:job->sequence_id];
+                        if (libhbJob)
+                            hb_rem( fHandle, libhbJob );
+                    }
+                    // Remove from our list
+                    [fJobGroups removeObject: jobGroup];
+                    break;
+                case HBStatusNone:
+                    break;
+            }
+        
+            row = [selectedRows indexGreaterThanIndex: row];
         }
-        else
+        [self endEditingJobGroupsArray];
+    } 
+}
+
+//------------------------------------------------------------------------------------
+// Reveals the file icons in the Finder of the selected job groups.
+//------------------------------------------------------------------------------------
+- (IBAction)revealSelectedJobGroups: (id)sender
+{
+    if (!fHandle) return;
+    
+    NSIndexSet * selectedRows = [fOutlineView selectedRowIndexes];
+    int row = [selectedRows firstIndex];
+    if (row != NSNotFound)
+    {
+        while (row != NSNotFound)
         {
-            row--;
-            hb_rem_group( fHandle, hb_group( fHandle, row ) );
+            HBJobGroup * jobGroup = [fOutlineView itemAtRow: row];
+            if ([[jobGroup destinationPath] length])
+                [[NSWorkspace sharedWorkspace] selectFile:[jobGroup destinationPath] inFileViewerRootedAtPath:nil];
+        
+            row = [selectedRows indexGreaterThanIndex: row];
         }
-#else
-        HBJobGroup * jobGroup = [fOutlineView itemAtRow: row];
-        hb_job_t * job = [[jobGroup jobAtIndex: 0] job];
-        hb_rem_group( fHandle, job );
-#endif
-        [self updateQueueUI];
-    }
+    } 
 }
 
 //------------------------------------------------------------------------------------
-// Prompts user if the want to cancel encoding of current job. If so, doCancelCurrentJob
-// gets called.
+// Calls HBController Cancel: which displays an alert asking user if they want to
+// cancel encoding of current job. cancelCurrentJob: returns immediately after posting
+// the alert. Later, when the user acknowledges the alert, HBController will call
+// libhb to cancel the job.
 //------------------------------------------------------------------------------------
 - (IBAction)cancelCurrentJob: (id)sender
 {
@@ -1273,12 +1677,12 @@ static NSString*    HBQueuePauseResumeToolbarIdentifier       = @"HBQueuePauseRe
     if ((s.state == HB_STATE_PAUSED) || (s.state == HB_STATE_WORKING) || (s.state == HB_STATE_MUXING))
         [fHBController Cancel: fQueuePane]; // sender == fQueuePane so that warning alert shows up on queue window
 
-    else if (hb_group_count(fHandle) > 0)
+    else if ([self pendingCount] > 0)
         [fHBController doRip];
 }
 
 //------------------------------------------------------------------------------------
-// Toggles the pause/resume state of hblib
+// Toggles the pause/resume state of libhb
 //------------------------------------------------------------------------------------
 - (IBAction)togglePauseResume: (id)sender
 {
@@ -1293,6 +1697,142 @@ static NSString*    HBQueuePauseResumeToolbarIdentifier       = @"HBQueuePauseRe
         hb_pause (fHandle);
 }
 
+#pragma mark -
+#pragma mark Synchronizing with libhb 
+
+//------------------------------------------------------------------------------------
+// Queues a job group. The job group's status is set to HBStatusPending.
+//------------------------------------------------------------------------------------
+- (void) addJobGroup: (HBJobGroup *) aJobGroup
+{
+    NSAssert(![fJobGroups containsObject:aJobGroup], @"Duplicate job group");
+    [aJobGroup setStatus:HBStatusPending];
+    
+    [self beginEditingJobGroupsArray];
+    [fJobGroups addObject:aJobGroup];
+    [self endEditingJobGroupsArray];
+}
+
+//------------------------------------------------------------------------------------
+// Notifies HBQueueController that libhb's current job has changed
+//------------------------------------------------------------------------------------
+- (void)currentJobChanged: (HBJob *) currentJob
+{
+    [currentJob retain];
+    [fCurrentJob release];
+    fCurrentJob = currentJob;
+
+    // Log info about the preset name. We do this for each job, since libhb logs each
+    // job separately. The preset name is found in the job's job group object.
+    if (fCurrentJob && [fCurrentJob jobGroup] && ([[[fCurrentJob jobGroup] presetName] length] > 0))
+        [fHBController writeToActivityLog: "Using preset: %s", [[[fCurrentJob jobGroup] presetName] UTF8String]];
+
+    // Check to see if this is also a change in Job Group
+    
+    HBJobGroup * theJobGroup = [currentJob jobGroup];
+    if ((theJobGroup == nil) || (theJobGroup != fCurrentJobGroup))     // no more job groups or start of a new group
+    {
+        // Previous job has completed
+        if (fCurrentJobGroup)
+        {
+            // Update the status of the job that just finished. If the user canceled,
+            // the status will have already been set to canceled by libhbWillStop. So
+            // all other cases are assumed to be a successful encode. BTW, libhb
+            // doesn't currently report errors back to the GUI.
+            if ([fCurrentJobGroup status] != HBStatusCanceled)
+                [fCurrentJobGroup setStatus:HBStatusCompleted];
+        }
+        
+        // Set the new group
+        [self setCurrentJobGroup: theJobGroup];
+    
+        // Update the UI
+        [self updateCurrentJobDescription];
+        [self updateCurrentJobProgress];
+        [self showCurrentJobPane: fCurrentJobGroup != nil];
+        if (fCurrentJobGroup)
+            [self startAnimatingCurrentJobGroupInQueue];
+        else
+            [self stopAnimatingCurrentJobGroupInQueue];
+    }
+    
+    else    // start a new job/pass in the same group
+    {
+        // Update the UI
+        [self updateCurrentJobDescription];
+        [self updateCurrentJobProgress];
+    }
+
+}
+
+//------------------------------------------------------------------------------------
+// Notifies HBQueueController that hb_stop is about to be called. This signals us that
+// the current job is going to be canceled and deleted. This is somewhat of a hack to
+// let HBQueueController know when a job group has been cancelled. Otherwise, we'd
+// have no way of knowing if a job was canceled or completed sucessfully.
+//------------------------------------------------------------------------------------
+- (void)libhbWillStop
+{
+    if (fCurrentJobGroup)
+        [fCurrentJobGroup setStatus: HBStatusCanceled];
+}
+
+//------------------------------------------------------------------------------------
+// Notifies HBQueueController that libhb's state has changed
+//------------------------------------------------------------------------------------
+- (void)libhbStateChanged: (hb_state_t &)state
+{
+    switch( state.state )
+    {
+        case HB_STATE_WORKING:
+        {
+            //NSLog(@"job = %x; job_cur = %d; job_count = %d", state.param.working.sequence_id, state.param.working.job_cur, state.param.working.job_count);
+            // First check to see if libhb has moved on to another job. We get no direct
+            // message when this happens, so we have to detect it ourself. The new job could
+            // be either just the next job in the current group, or the start of a new group.
+            if (fCurrentJobID != state.param.working.sequence_id)
+            {
+                fCurrentJobID = state.param.working.sequence_id;
+                HBJob * currentJob = [self findJobWithID:fCurrentJobID];
+                [self currentJobChanged: currentJob];
+            }
+
+            if (fCurrentJob)
+            {
+                [self updateCurrentJobProgress];
+                [self startAnimatingCurrentJobGroupInQueue];
+            }
+            break;
+        }
+
+        case HB_STATE_MUXING:
+        {
+            [self updateCurrentJobProgress];
+            break;
+        }
+
+        case HB_STATE_PAUSED:
+        {
+            [self updateCurrentJobProgress];
+            [self stopAnimatingCurrentJobGroupInQueue];
+            break;
+        }
+
+        case HB_STATE_WORKDONE:
+        {
+            // HB_STATE_WORKDONE means that libhb has finished processing all the jobs
+            // in *its* queue. This message is NOT sent as each individual job is
+            // completed.
+
+            [self currentJobChanged: nil];
+            fCurrentJobID = 0;
+            break;
+        }
+
+    }
+
+}
+
 #if HB_OUTLINE_METRIC_CONTROLS
 static float spacingWidth = 3.0;
 - (IBAction)imageSpacingChanged: (id)sender;
@@ -1307,6 +1847,21 @@ static float spacingWidth = 3.0;
 }
 #endif
 
+#pragma mark -
+
+//------------------------------------------------------------------------------------
+// Receives notification whenever an HBJobGroup's status is changed.
+//------------------------------------------------------------------------------------
+- (void) jobGroupStatusNotification:(NSNotification *)notification
+{
+    [self setJobGroupCountsNeedUpdating: YES];
+//    HBQueueJobGroupStatus oldStatus = (HBQueueJobGroupStatus) [[[notification userInfo] objectForKey:@"HBOldJobGroupStatus"] intValue];
+    HBJobGroup * jobGroup = [notification object];
+    if (jobGroup)
+        [self updateJobGroupInQueue:jobGroup];
+    [self updateQueueCountField];
+}
+
 
 #pragma mark -
 #pragma mark Toolbar
@@ -1347,36 +1902,36 @@ static float spacingWidth = 3.0;
     if ([itemIdentifier isEqual: HBQueueStartCancelToolbarIdentifier])
     {
         toolbarItem = [[[NSToolbarItem alloc] initWithItemIdentifier: itemIdentifier] autorelease];
-               
+        
         // Set the text label to be displayed in the toolbar and customization palette 
-               [toolbarItem setLabel: @"Start"];
-               [toolbarItem setPaletteLabel: @"Start/Cancel"];
-               
-               // Set up a reasonable tooltip, and image
-               [toolbarItem setToolTip: @"Start Encoding"];
-               [toolbarItem setImage: [NSImage imageNamed: @"Play"]];
-               
-               // Tell the item what message to send when it is clicked 
-               [toolbarItem setTarget: self];
-               [toolbarItem setAction: @selector(toggleStartCancel:)];
-       }
+        [toolbarItem setLabel: @"Start"];
+        [toolbarItem setPaletteLabel: @"Start/Cancel"];
+        
+        // Set up a reasonable tooltip, and image
+        [toolbarItem setToolTip: @"Start Encoding"];
+        [toolbarItem setImage: [NSImage imageNamed: @"Play"]];
+        
+        // Tell the item what message to send when it is clicked 
+        [toolbarItem setTarget: self];
+        [toolbarItem setAction: @selector(toggleStartCancel:)];
+    }
     
     if ([itemIdentifier isEqual: HBQueuePauseResumeToolbarIdentifier])
     {
         toolbarItem = [[[NSToolbarItem alloc] initWithItemIdentifier: itemIdentifier] autorelease];
-               
+        
         // Set the text label to be displayed in the toolbar and customization palette 
-               [toolbarItem setLabel: @"Pause"];
-               [toolbarItem setPaletteLabel: @"Pause/Resume"];
-               
-               // Set up a reasonable tooltip, and image
-               [toolbarItem setToolTip: @"Pause Encoding"];
-               [toolbarItem setImage: [NSImage imageNamed: @"Pause"]];
-               
-               // Tell the item what message to send when it is clicked 
-               [toolbarItem setTarget: self];
-               [toolbarItem setAction: @selector(togglePauseResume:)];
-       }
+        [toolbarItem setLabel: @"Pause"];
+        [toolbarItem setPaletteLabel: @"Pause/Resume"];
+        
+        // Set up a reasonable tooltip, and image
+        [toolbarItem setToolTip: @"Pause Encoding"];
+        [toolbarItem setImage: [NSImage imageNamed: @"Pause"]];
+        
+        // Tell the item what message to send when it is clicked 
+        [toolbarItem setTarget: self];
+        [toolbarItem setAction: @selector(togglePauseResume:)];
+    }
     
     return toolbarItem;
 }
@@ -1407,10 +1962,10 @@ static float spacingWidth = 3.0;
     return [NSArray arrayWithObjects:
         HBQueueStartCancelToolbarIdentifier,
         HBQueuePauseResumeToolbarIdentifier,
-               NSToolbarCustomizeToolbarItemIdentifier,
-               NSToolbarFlexibleSpaceItemIdentifier,
+        NSToolbarCustomizeToolbarItemIdentifier,
+        NSToolbarFlexibleSpaceItemIdentifier,
         NSToolbarSpaceItemIdentifier,
-               NSToolbarSeparatorItemIdentifier,
+        NSToolbarSeparatorItemIdentifier,
         nil];
 }
 
@@ -1435,26 +1990,26 @@ static float spacingWidth = 3.0;
         {
             enable = YES;
             [toolbarItem setImage:[NSImage imageNamed: @"Stop"]];
-                       [toolbarItem setLabel: @"Stop"];
-                       [toolbarItem setToolTip: @"Stop Encoding"];
+            [toolbarItem setLabel: @"Stop"];
+            [toolbarItem setToolTip: @"Stop Encoding"];
         }
 
-        else if (hb_count(fHandle) > 0)
+        else if ([self pendingCount] > 0)
         {
             enable = YES;
             [toolbarItem setImage:[NSImage imageNamed: @"Play"]];
-                       [toolbarItem setLabel: @"Start"];
-                       [toolbarItem setToolTip: @"Start Encoding"];
+            [toolbarItem setLabel: @"Start"];
+            [toolbarItem setToolTip: @"Start Encoding"];
         }
 
         else
         {
             enable = NO;
             [toolbarItem setImage:[NSImage imageNamed: @"Play"]];
-                       [toolbarItem setLabel: @"Start"];
-                       [toolbarItem setToolTip: @"Start Encoding"];
+            [toolbarItem setLabel: @"Start"];
+            [toolbarItem setToolTip: @"Start Encoding"];
         }
-       }
+    }
     
     if ([[toolbarItem itemIdentifier] isEqual: HBQueuePauseResumeToolbarIdentifier])
     {
@@ -1462,27 +2017,27 @@ static float spacingWidth = 3.0;
         {
             enable = YES;
             [toolbarItem setImage:[NSImage imageNamed: @"Play"]];
-                       [toolbarItem setLabel: @"Resume"];
-                       [toolbarItem setToolTip: @"Resume Encoding"];
+            [toolbarItem setLabel: @"Resume"];
+            [toolbarItem setToolTip: @"Resume Encoding"];
        }
         
         else if ((s.state == HB_STATE_WORKING) || (s.state == HB_STATE_MUXING))
         {
             enable = YES;
             [toolbarItem setImage:[NSImage imageNamed: @"Pause"]];
-                       [toolbarItem setLabel: @"Pause"];
-                       [toolbarItem setToolTip: @"Pause Encoding"];
+            [toolbarItem setLabel: @"Pause"];
+            [toolbarItem setToolTip: @"Pause Encoding"];
         }
         else
         {
             enable = NO;
             [toolbarItem setImage:[NSImage imageNamed: @"Pause"]];
-                       [toolbarItem setLabel: @"Pause"];
-                       [toolbarItem setToolTip: @"Pause Encoding"];
+            [toolbarItem setLabel: @"Pause"];
+            [toolbarItem setToolTip: @"Pause Encoding"];
         }
-       }
+    }
     
-       return enable;
+    return enable;
 }
 
 #pragma mark -
@@ -1508,7 +2063,6 @@ static float spacingWidth = 3.0;
     // Don't allow autoresizing of main column, else the "delete" column will get
     // pushed out of view.
     [fOutlineView setAutoresizesOutlineColumn: NO];
-    [fOutlineView setIndentationPerLevel:21];
 
 #if HB_OUTLINE_METRIC_CONTROLS
     [fIndentation setHidden: NO];
@@ -1520,6 +2074,8 @@ static float spacingWidth = 3.0;
     // Show/hide UI elements
     fCurrentJobPaneShown = YES;     // it's shown in the nib
     [self showCurrentJobPane:NO];
+
+    [self updateQueueCountField];
 }
 
 
@@ -1532,6 +2088,37 @@ static float spacingWidth = 3.0;
 }
 
 #pragma mark -
+
+- (void)moveObjectsInArray:(NSMutableArray *)array fromIndexes:(NSIndexSet *)indexSet toIndex:(unsigned)insertIndex
+{
+    unsigned index = [indexSet lastIndex];
+    unsigned aboveInsertIndexCount = 0;
+    
+    while (index != NSNotFound)
+    {
+        unsigned removeIndex;
+        
+        if (index >= insertIndex)
+        {
+            removeIndex = index + aboveInsertIndexCount;
+            aboveInsertIndexCount++;
+        }
+        else
+        {
+            removeIndex = index;
+            insertIndex--;
+        }
+        
+        id object = [[array objectAtIndex:removeIndex] retain];
+        [array removeObjectAtIndex:removeIndex];
+        [array insertObject:object atIndex:insertIndex];
+        [object release];
+        
+        index = [indexSet indexLessThanIndex:index];
+    }
+}
+
+#pragma mark -
 #pragma mark NSOutlineView delegate
 
 - (id)outlineView:(NSOutlineView *)outlineView child:(int)index ofItem:(id)item
@@ -1551,6 +2138,18 @@ static float spacingWidth = 3.0;
     return YES;
 }
 
+- (BOOL)outlineView:(NSOutlineView *)outlineView shouldExpandItem:(id)item
+{
+    // Our outline view has no levels, but we can still expand every item. Doing so
+    // just makes the row taller. See heightOfRowByItem below.
+#if HB_QUEUE_DRAGGING
+       // Don't autoexpand while dragging, since we can't drop into the items
+       return ![(HBQueueOutlineView*)outlineView isDragging];
+#else
+       return YES;
+#endif
+}
+
 - (int)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item
 {
     // Our outline view has no levels, so number of children will be zero for all
@@ -1589,10 +2188,14 @@ static float spacingWidth = 3.0;
             return [item lastDescriptionHeight];
         
         float width = [[outlineView tableColumnWithIdentifier: @"desc"] width];
-        // Column width is NOT what is ultimately used
-        width -= 47;    // 26 pixels for disclosure triangle, 20 for icon, 1 for intercell spacing
+        // Column width is NOT what is ultimately used. I can't quite figure out what
+        // width to use for calculating text metrics. No matter how I tweak this value,
+        // there are a few conditions in which the drawn text extends below the bounds
+        // of the row cell. In previous versions, which ran under Tiger, I was
+        // reducing width by 47 pixles.
+        width -= 2;     // (?) for intercell spacing
         
-        float height = [item heightOfDescriptionForWidth: width withHBHandle: fHandle];
+        float height = [item heightOfDescriptionForWidth: width];
         return height;
     }
     else
@@ -1601,8 +2204,29 @@ static float spacingWidth = 3.0;
 
 - (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item
 {
+       // nb: The "desc" column is currently an HBImageAndTextCell. However, we are longer
+       // using the image portion of the cell so we could switch back to a regular NSTextFieldCell.
+       
     if ([[tableColumn identifier] isEqualToString:@"desc"])
-        return [item attributedDescriptionWithHBHandle: fHandle];
+        return [item attributedDescription];
+    else if ([[tableColumn identifier] isEqualToString:@"icon"])
+    {
+        switch ([(HBJobGroup*)item status])
+        {
+            case HBStatusCanceled:
+                return [NSImage imageNamed:@"EncodeCanceled"];
+                break;
+            case HBStatusCompleted:
+                return [NSImage imageNamed:@"EncodeComplete"];
+                break;
+            case HBStatusWorking:
+                return [NSImage imageNamed: [NSString stringWithFormat: @"EncodeWorking%d", fAnimationIndex]];
+                break;
+            default:
+                return [NSImage imageNamed:@"JobSmall"];
+                break;
+        }
+    }
     else
         return @"";
 }
@@ -1617,22 +2241,39 @@ static float spacingWidth = 3.0;
         [cell setImageSpacing: theSize];
 #endif
         
+               // nb: The "desc" column is currently an HBImageAndTextCell. However, we are longer
+               // using the image portion of the cell so we could switch back to a regular NSTextFieldCell.
+
         // Set the image here since the value returned from outlineView:objectValueForTableColumn: didn't specify the image part
-        [cell setImage:[NSImage imageNamed:@"JobSmall"]];
+        [cell setImage:nil];
     }
     
-    else if ([[tableColumn identifier] isEqualToString:@"delete"])
+    else if ([[tableColumn identifier] isEqualToString:@"action"])
     {
-        // The Delete action can only be applied for group items, not indivdual jobs.
         [cell setEnabled: YES];
         BOOL highlighted = [outlineView isRowSelected:[outlineView rowForItem: item]] && [[outlineView window] isKeyWindow] && ([[outlineView window] firstResponder] == outlineView);
-        if (highlighted)
+        if ([(HBJobGroup*)item status] == HBStatusCompleted)
         {
-            [cell setImage:[NSImage imageNamed:@"DeleteHighlight"]];
-            [cell setAlternateImage:[NSImage imageNamed:@"DeleteHighlightPressed"]];
+            [cell setAction: @selector(revealSelectedJobGroups:)];
+            if (highlighted)
+            {
+                [cell setImage:[NSImage imageNamed:@"RevealHighlight"]];
+                [cell setAlternateImage:[NSImage imageNamed:@"RevealHighlightPressed"]];
+            }
+            else
+                [cell setImage:[NSImage imageNamed:@"Reveal"]];
         }
         else
-            [cell setImage:[NSImage imageNamed:@"Delete"]];
+        {
+            [cell setAction: @selector(removeSelectedJobGroups:)];
+            if (highlighted)
+            {
+                [cell setImage:[NSImage imageNamed:@"DeleteHighlight"]];
+                [cell setAlternateImage:[NSImage imageNamed:@"DeleteHighlightPressed"]];
+            }
+            else
+                [cell setImage:[NSImage imageNamed:@"Delete"]];
+        }
     }
 }
 
@@ -1656,8 +2297,19 @@ static float spacingWidth = 3.0;
 #if HB_QUEUE_DRAGGING
 - (BOOL)outlineView:(NSOutlineView *)outlineView writeItems:(NSArray *)items toPasteboard:(NSPasteboard *)pboard
 {
-    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.
-    
+       // Dragging is only allowed of the pending items.
+       NSEnumerator * e = [items objectEnumerator];
+       HBJobGroup * group;
+       while ( (group = [e nextObject]) )
+       {
+               if ([group status] != HBStatusPending)
+                       return NO;
+       }
+       
+    // 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.
+    fDraggedNodes = items;
+       
     // Provide data for our custom type, and simple NSStrings.
     [pboard declareTypes:[NSArray arrayWithObjects: HBQueuePboardType, nil] owner:self];
 
@@ -1671,11 +2323,25 @@ static float spacingWidth = 3.0;
 #if HB_QUEUE_DRAGGING
 - (NSDragOperation)outlineView:(NSOutlineView *)outlineView validateDrop:(id <NSDraggingInfo>)info proposedItem:(id)item proposedChildIndex:(int)index
 {
-    // Add code here to validate the drop
-       BOOL isOnDropTypeProposal = index == NSOutlineViewDropOnItemIndex;
+       // Don't allow dropping ONTO an item since they can't really contain any children.
+    BOOL isOnDropTypeProposal = index == NSOutlineViewDropOnItemIndex;
     if (isOnDropTypeProposal)
         return NSDragOperationNone;
-        
+
+       // Don't allow dropping INTO an item since they can't really contain any children.
+       if (item != nil)
+       {
+               index = [fOutlineView rowForItem: item] + 1;
+               item = nil;
+       }
+
+       // Prevent dragging into the completed or current job.
+       int firstPendingIndex = [fCompleted count];
+       if (fCurrentJobGroup)
+               firstPendingIndex++;
+       index = MAX (index, firstPendingIndex);
+       
+       [outlineView setDropItem:item dropChildIndex:index];
     return NSDragOperationGeneric;
 }
 #endif
@@ -1683,20 +2349,24 @@ static float spacingWidth = 3.0;
 #if HB_QUEUE_DRAGGING
 - (BOOL)outlineView:(NSOutlineView *)outlineView acceptDrop:(id <NSDraggingInfo>)info item:(id)item childIndex:(int)index
 {
-//    NSPasteboard* pboard = [info draggingPasteboard];
-//    NSData* rowData = [pboard dataForType:HBQueuePboardType];
-//    NSIndexSet* rowIndexes = [NSKeyedUnarchiver unarchiveObjectWithData:rowData];
-//    int dragRow = [rowIndexes firstIndex];
-    // Move the specified row to its new location...
-    HBJob * draggedJob = [fDraggedNodes objectAtIndex: 0];
-    NSLog(@"dragged job = %@", draggedJob);
-    NSLog(@"drag from location = %d", [fJobGroups indexOfObject: draggedJob]);
-    NSLog(@"drag to location = %d", index);
-    NSLog(@"acceptDrop");
+    NSMutableIndexSet *moveItems = [NSMutableIndexSet indexSet];
     
+    id obj;
+    NSEnumerator *enumerator = [fDraggedNodes objectEnumerator];
+    while (obj = [enumerator nextObject])
+    {
+        [moveItems addIndex:[fJobGroups indexOfObject:obj]];
+    }
+
+    // Rearrange the data and view
+    [self saveOutlineViewState];
+    [self moveObjectsInArray:fJobGroups fromIndexes:moveItems toIndex: index];
+    [fOutlineView reloadData];
+    [self restoreOutlineViewState];
+        
     return YES;
 }
 #endif
 
+
 @end