OSDN Git Service

MacGui: Fix errant encode done growl alert and send to MetaX function when finishing...
[handbrake-jp/handbrake-jp-git.git] / macosx / HBQueueController.mm
index 320a75a..555ba5c 100644 (file)
     It may be used under the terms of the GNU General Public License. */
 
 #include "HBQueueController.h"
+#include "Controller.h"
+#import "HBImageAndTextCell.h"
 
-/**
- * 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)    
+#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)
 {
-    hb_job_t * job;
-    int count = 0;
-    int index = 0;
-    while( ( job = hb_job( h, index++ ) ) )
-    {
-        if (job->sequence_id == 0)
-            count++;
-    }
-    return count;
+    return jobGroupID<<16 | sequenceNum;
 }
 
-/**
- * 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)    
+bool IsFirstPass(int jobID)
 {
-    hb_job_t * job;
-    int count = 0;
-    int index = 0;
-    while( ( job = hb_job( h, index++ ) ) )
+    return LoWord(jobID) == 0;
+}
+
+//------------------------------------------------------------------------------------
+// NSMutableAttributedString (HBAdditions)
+//------------------------------------------------------------------------------------
+
+@interface NSMutableAttributedString (HBAdditions)
+- (void) appendString: (NSString*)aString withAttributes: (NSDictionary *)aDictionary;
+@end
+
+@implementation NSMutableAttributedString (HBAdditions)
+- (void) appendString: (NSString*)aString withAttributes: (NSDictionary *)aDictionary
+{
+    NSAttributedString * s = [[[NSAttributedString alloc]
+        initWithString: aString
+        attributes: aDictionary] autorelease];
+    [self appendAttributedString: s];
+}
+@end
+
+//------------------------------------------------------------------------------------
+#pragma mark -
+//------------------------------------------------------------------------------------
+
+@implementation HBQueueOutlineView
+
+- (void)viewDidEndLiveResize
+{
+    // Since we disabled calculating row heights during a live resize, force them to
+    // recalculate now.
+    [self noteHeightOfRowsWithIndexesChanged:
+            [NSIndexSet indexSetWithIndexesInRange: NSMakeRange(0, [self numberOfRows])]];
+    [super viewDidEndLiveResize];
+}
+
+#if HB_QUEUE_DRAGGING
+- (NSImage *)dragImageForRowsWithIndexes:(NSIndexSet *)dragRows tableColumns:(NSArray *)tableColumns event:(NSEvent*)dragEvent offset:(NSPointPointer)dragImageOffset
+{
+    // 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
+
+#if HB_QUEUE_DRAGGING
+- (void) mouseDown:(NSEvent *)theEvent
+{
+    // 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
+
+#if HB_QUEUE_DRAGGING
+- (BOOL) isDragging;
+{
+    return fIsDragging;
+}
+#endif
+
+@end
+
+#pragma mark -
+
+//------------------------------------------------------------------------------------
+// HBJob
+//------------------------------------------------------------------------------------
+
+static NSMutableParagraphStyle * _descriptionParagraphStyle = NULL;
+static NSDictionary* _detailAttribute = NULL;
+static NSDictionary* _detailBoldAttribute = NULL;
+static NSDictionary* _titleAttribute = NULL;
+static NSDictionary* _shortHeightAttribute = NULL;
+
+@implementation HBJob
+
++ (HBJob*) jobWithLibhbJob: (hb_job_t *) job
+{
+    return [[[HBJob alloc] initWithLibhbJob:job] autorelease];
+}
+
+- (id) initWithLibhbJob: (hb_job_t *) job
+{
+    if (self = [super init])
     {
-        if (job->sequence_id == 0)
+        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];
+        /* So, with the advent of job->list_audio's I decided why not just use an NSString and concatanate
+         all of the info we need for all of the audio values to display right into an NSString here ? So I
+         did. I have no idea why we are reading libhb stuff just to display it in the queue gui. So here we
+         are with a huge string. But its easy to change and saves alot of messing about. Maybe we move a bunch
+         of other display stuff into strings for display for each job. It's not like they have to actually do
+         anything.*/
+        hb_audio_config_t * audio;
+        NSString * thisJobAudioCodecs = [NSString stringWithFormat:@""];
+        NSString * thisJobAudioInfo = [NSString stringWithFormat:@""];
+        for( int i = 0; i < hb_list_count(job->list_audio); i++ )
         {
-            if (count == i)
-                return job;
-            count++;
+           audio = (hb_audio_config_t *) hb_list_audio_config_item( job->list_audio, i );
+            /* Output Codec */
+            NSString *outputCodec;
+            if (audio->out.codec == HB_ACODEC_AC3)
+                outputCodec = @"AC3";
+            else if (audio->out.codec == HB_ACODEC_FAAC)
+                outputCodec = @"AAC";
+            else if (audio->out.codec == HB_ACODEC_LAME)
+                outputCodec = @"MP3";
+            else if (audio->out.codec == HB_ACODEC_VORBIS)
+                outputCodec = @"Vorbis";
+            else
+                outputCodec = @"Unknown Codec";       
+            /* Add the codec to the audio codecs list ( We should check against dupes)*/
+            thisJobAudioCodecs = [thisJobAudioCodecs stringByAppendingString:[NSString stringWithFormat:@" %@,",outputCodec]];
+            if (i > 0)
+            {
+                /* Insert a line break so that we get each track on a separate line */
+                /* Wicked HACK alert!!, use 18 whitespaces to align offset in display for list > 2 to offset "Audio" in the queue display
+                 Please Fix Me because this is embarrassing (but it works) */
+                thisJobAudioInfo = [thisJobAudioInfo stringByAppendingString:@"\n                  "];
+            }
+            /* Detailed Job audio track info*/
+            /* Track Number and Mixdown Info */
+            if (audio->out.mixdown == HB_ACODEC_AC3)// Remember for ac3 passthru the mixdown uses the source codec
+                thisJobAudioInfo = [thisJobAudioInfo stringByAppendingString:[NSString stringWithFormat:@"Track %d: Source: %@ Output: %@, Pass-Thru", i + 1, [NSString stringWithUTF8String:audio->lang.description], outputCodec]];
+            else if (audio->out.mixdown == HB_AMIXDOWN_MONO)
+                thisJobAudioInfo = [thisJobAudioInfo stringByAppendingString:[NSString stringWithFormat:@"Track %d: Source: %@ Output: %@, Mono", i + 1, [NSString stringWithUTF8String:audio->lang.description], outputCodec]];
+            else if (audio->out.mixdown == HB_AMIXDOWN_STEREO)
+                thisJobAudioInfo = [thisJobAudioInfo stringByAppendingString:[NSString stringWithFormat:@"Track %d: Source: %@ Output: %@, Stereo", i + 1, [NSString stringWithUTF8String:audio->lang.description], outputCodec]];
+            else if (audio->out.mixdown == HB_AMIXDOWN_DOLBY)
+                thisJobAudioInfo = [thisJobAudioInfo stringByAppendingString:[NSString stringWithFormat:@"Track %d: Source: %@ Output: %@, Dolby Surround", i + 1, [NSString stringWithUTF8String:audio->lang.description], outputCodec]];
+            else if (audio->out.mixdown == HB_AMIXDOWN_DOLBYPLII)
+                thisJobAudioInfo = [thisJobAudioInfo stringByAppendingString:[NSString stringWithFormat:@"Track %d: Source: %@ Output: %@, Dolby Pro Logic II", i + 1, [NSString stringWithUTF8String:audio->lang.description], outputCodec]];
+            else if (audio->out.mixdown == HB_AMIXDOWN_6CH)
+                thisJobAudioInfo = [thisJobAudioInfo stringByAppendingString:[NSString stringWithFormat:@"Track %d: Source: %@ Output: %@, 6 Channel Discreet", i + 1, [NSString stringWithUTF8String:audio->lang.description], outputCodec]];
+            else
+                thisJobAudioInfo = [thisJobAudioInfo stringByAppendingString:[NSString stringWithFormat:@"Track %d: Source: %@ Output: Unknown Codec Info", i + 1, [NSString stringWithUTF8String:audio->lang.description]]];
+            
+            thisJobAudioInfo = [thisJobAudioInfo stringByAppendingString:[NSString stringWithFormat:@", %d kbps, %d Hz", audio->out.bitrate, audio->out.samplerate]];
+            
+        }
+        audioinfo_summary = [[NSString stringWithFormat:@"%@",thisJobAudioInfo]retain];
+        audioinfo_codecs = [[NSString stringWithFormat:@"%@",thisJobAudioCodecs]retain];
+        
+        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 NULL;
+    return self;
 }
 
-/**
- * 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 )
+- (void) dealloc
 {
-    // Find job in list
-    hb_job_t * j;
-    int index = 0;
-    while( ( j = hb_job( h, index ) ) )
+    // jobGroup is a weak reference and does not need to be deleted
+    [x264opts release];
+    [file release];
+    [titleName release];
+    [subtitleLang release];
+    [audioinfo_summary release];
+    [audioinfo_codecs release];
+    [super dealloc];
+}
+
+- (HBJobGroup *) jobGroup
+{
+    return jobGroup;
+}
+
+- (void) setJobGroup: (HBJobGroup *)aJobGroup
+{
+    // This is a weak reference. We don't retain or release it.
+    jobGroup = aJobGroup;
+}
+
+//------------------------------------------------------------------------------------
+// Generate string to display in UI.
+//------------------------------------------------------------------------------------
+
+- (NSMutableAttributedString *) attributedDescriptionWithIcon: (BOOL)withIcon
+                              withTitle: (BOOL)withTitle
+                           withPassName: (BOOL)withPassName
+                         withFormatInfo: (BOOL)withFormatInfo
+                        withDestination: (BOOL)withDestination
+                        withPictureInfo: (BOOL)withPictureInfo
+                          withVideoInfo: (BOOL)withVideoInfo
+                           withx264Info: (BOOL)withx264Info
+                          withAudioInfo: (BOOL)withAudioInfo
+                       withSubtitleInfo: (BOOL)withSubtitleInfo
+
+{
+    NSMutableAttributedString * finalString = [[[NSMutableAttributedString alloc] initWithString: @""] autorelease];
+    
+    // Attributes
+    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)
     {
-        if (j == job)
+        if (withIcon)
+        {
+            NSFileWrapper * wrapper = [[[NSFileWrapper alloc] initWithPath:[[NSBundle mainBundle] pathForImageResource: @"JobSmall"]] autorelease];
+            NSTextAttachment * imageAttachment = [[[NSTextAttachment alloc] initWithFileWrapper:wrapper] autorelease];
+
+            NSDictionary* imageAttributes = [NSDictionary dictionaryWithObjectsAndKeys:
+                            [NSNumber numberWithFloat: -2.0], NSBaselineOffsetAttributeName,
+                            imageAttachment, NSAttachmentAttributeName,
+                            ps, NSParagraphStyleAttributeName,
+                            nil];
+
+            NSAttributedString * imageAsString = [[[NSAttributedString alloc]
+                    initWithString: [NSString stringWithFormat:@"%C%C", NSAttachmentCharacter, NSTabCharacter]
+                    attributes: imageAttributes] autorelease];
+
+            [finalString appendAttributedString:imageAsString];
+        }
+    
+        // 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:titleName withAttributes:titleAttr];
+        
+        NSString * summaryInfo;
+    
+        NSString * chapterString = (chapter_start == chapter_end) ?
+                [NSString stringWithFormat:@"Chapter %d", chapter_start] :
+                [NSString stringWithFormat:@"Chapters %d through %d", chapter_start, chapter_end];
+
+        BOOL hasIndepthScan = (pass == -1);
+        int numVideoPasses = 0;
+
+        // To determine number of video passes, we need to skip past the subtitle scan.
+        if (hasIndepthScan)
+        {
+            // When job is the one currently being processed, then the next in its group
+            // is the the first job in the queue.
+            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, pass + 1 );
+
+        if (hasIndepthScan && numVideoPasses == 1)
+            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)", titleIndex, chapterString, numVideoPasses];
+        else if (numVideoPasses == 1)
+            summaryInfo = [NSString stringWithFormat: @"  (Title %d, %@, Single Video Pass)", titleIndex, chapterString];
+        else
+            summaryInfo = [NSString stringWithFormat: @"  (Title %d, %@, %d Video Passes)", titleIndex, chapterString, numVideoPasses];
+
+        [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:shortHeightAttr];
+    }
+    
+    // End of title stuff
+    
+
+    // Pass Name
+    if (withPassName)
+    {
+        if (withIcon)
+        {
+            NSString * imageName;
+            switch (pass)
+            {
+                case -1: imageName = @"JobPassSubtitleSmall"; break;
+                case  0: imageName = @"JobPassFirstSmall"; break;
+                case  1: imageName = @"JobPassFirstSmall"; break;
+                case  2: imageName = @"JobPassSecondSmall"; break;
+                default: imageName = @"JobPassUnknownSmall"; break;
+            }
+
+            NSFileWrapper * wrapper = [[[NSFileWrapper alloc] initWithPath:[[NSBundle mainBundle] pathForImageResource: imageName]] autorelease];
+            NSTextAttachment * imageAttachment = [[[NSTextAttachment alloc] initWithFileWrapper:wrapper] autorelease];
+
+            NSDictionary* imageAttributes = [NSDictionary dictionaryWithObjectsAndKeys:
+                            [NSNumber numberWithFloat: -2.0], NSBaselineOffsetAttributeName,
+                            imageAttachment, NSAttachmentAttributeName,
+                            ps, NSParagraphStyleAttributeName,
+                            nil];
+
+            NSAttributedString * imageAsString = [[[NSAttributedString alloc]
+                    initWithString: [NSString stringWithFormat:@"%C%C", NSAttachmentCharacter, NSTabCharacter]
+                    attributes: imageAttributes] autorelease];
+
+            [finalString appendAttributedString:imageAsString];
+        }
+    
+        NSString * jobPassName;
+        if (pass == -1)
+            jobPassName = NSLocalizedString (@"Deep Scan", nil);
+        else
+        {
+            int passNum = MAX( 1, pass );
+            if (passNum == 0)
+                jobPassName = NSLocalizedString (@"1st Pass", nil);
+            else if (passNum == 1)
+                jobPassName = NSLocalizedString (@"1st Pass", nil);
+            else if (passNum == 2)
+                jobPassName = NSLocalizedString (@"2nd Pass", nil);
+            else
+                jobPassName = [NSString stringWithFormat: NSLocalizedString(@"Pass %d", nil), passNum];
+        }
+        [finalString appendString:[NSString stringWithFormat:@"%@\n", jobPassName] withAttributes:detailBoldAttr];
+    }
+
+    // Video Codec needed by FormatInfo and withVideoInfo
+    NSString * jobVideoCodec = nil;
+    if (withFormatInfo || withVideoInfo)
+    {
+        // 2097152
+        // Video Codec settings (Encoder in the gui)
+        if (vcodec == HB_VCODEC_FFMPEG)
+            jobVideoCodec = @"FFmpeg"; // HB_VCODEC_FFMPEG
+        else if (vcodec == HB_VCODEC_XVID)
+            jobVideoCodec = @"XviD"; // HB_VCODEC_XVID
+        else if (vcodec == HB_VCODEC_X264)
+        {
+            // 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
+        }
+    }
+    if (jobVideoCodec == nil)
+        jobVideoCodec = @"unknown";
+    
+    // Audio Codec needed by FormatInfo and AudioInfo
+    NSString * jobAudioCodec = nil;
+    if (withFormatInfo || withAudioInfo)
+    {
+        if (acodec == 256)
+            jobAudioCodec = @"AAC"; // HB_ACODEC_FAAC
+        else if (acodec == 512)
+            jobAudioCodec = @"MP3"; // HB_ACODEC_LAME
+        else if (acodec == 1024)
+            jobAudioCodec = @"Vorbis"; // HB_ACODEC_VORBIS
+        else if (acodec == 2048)
+            jobAudioCodec = @"AC3"; // HB_ACODEC_AC3
+    }
+    if (jobAudioCodec == nil)
+        jobAudioCodec = @"unknown";
+
+
+    if (withFormatInfo)
+    {
+        NSString * jobFormatInfo;
+        // Muxer settings (File Format in the gui)
+        if (mux == 65536 || mux == 131072 || mux == 1048576)
+            jobFormatInfo = @"MP4"; // HB_MUX_MP4,HB_MUX_PSP,HB_MUX_IPOD
+        else if (mux == 262144)
+            jobFormatInfo = @"AVI"; // HB_MUX_AVI
+        else if (mux == 524288)
+            jobFormatInfo = @"OGM"; // HB_MUX_OGM
+        else if (mux == 2097152)
+            jobFormatInfo = @"MKV"; // HB_MUX_MKV
+        else
+            jobFormatInfo = @"unknown";
+                
+        if (chapter_markers == 1)
+            jobFormatInfo = [NSString stringWithFormat:@"%@ Container, %@ Video + %@ Audio, Chapter Markers\n", jobFormatInfo, jobVideoCodec, audioinfo_codecs];
+        else
+            jobFormatInfo = [NSString stringWithFormat:@"%@ Container, %@ Video + %@ Audio\n", jobFormatInfo, jobVideoCodec, audioinfo_codecs];
+            
+        [finalString appendString: @"Format: " withAttributes:detailBoldAttr];
+        [finalString appendString: jobFormatInfo withAttributes:detailAttr];
+    }
+
+    if (withDestination)
+    {
+        [finalString appendString: @"Destination: " withAttributes:detailBoldAttr];
+        [finalString appendString:[NSString stringWithFormat:@"%@\n", file] withAttributes:detailAttr];
+    }
+
+
+    if (withPictureInfo)
+    {
+        NSString * jobPictureInfo;
+        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:@"%d x %d", output_width, output_height];
+        if (keep_ratio == 1)
+            jobPictureInfo = [jobPictureInfo stringByAppendingString:@" Keep Aspect Ratio"];
+        
+        if (grayscale == 1)
+            jobPictureInfo = [jobPictureInfo stringByAppendingString:@", Grayscale"];
+        
+        if (deinterlace == 1)
+            jobPictureInfo = [jobPictureInfo stringByAppendingString:@", Deinterlace"];
+        if (withIcon)   // implies indent the info
+            [finalString appendString: @"\t" withAttributes:detailBoldAttr];
+        [finalString appendString: @"Picture: " withAttributes:detailBoldAttr];
+        [finalString appendString:[NSString stringWithFormat:@"%@\n", jobPictureInfo] withAttributes:detailAttr];
+    }
+    
+    if (withVideoInfo)
+    {
+        NSString * jobVideoQuality;
+        NSString * jobVideoDetail;
+        
+        if (vquality <= 0 || vquality >= 1)
+            jobVideoQuality = [NSString stringWithFormat:@"%d kbps", vbitrate];
+        else
+        {
+            NSNumber * vidQuality;
+            vidQuality = [NSNumber numberWithInt:vquality * 100];
+            // this is screwed up kind of. Needs to be formatted properly.
+            if (crf == 1)
+                jobVideoQuality = [NSString stringWithFormat:@"%@%% CRF", vidQuality];            
+            else
+                jobVideoQuality = [NSString stringWithFormat:@"%@%% CQP", vidQuality];
+        }
+        
+        if (vrate_base == 1126125)
         {
-            // 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;
+            // NTSC FILM 23.976
+            jobVideoDetail = [NSString stringWithFormat:@"%@, %@, 23.976 fps", jobVideoCodec, jobVideoQuality];
+        }
+        else if (vrate_base == 900900)
+        {
+            // NTSC 29.97
+            jobVideoDetail = [NSString stringWithFormat:@"%@, %@, 29.97 fps", jobVideoCodec, jobVideoQuality];
         }
         else
-            index++;
+        {
+            // Everything else
+            jobVideoDetail = [NSString stringWithFormat:@"%@, %@, %d fps", jobVideoCodec, jobVideoQuality, vrate / vrate_base];
+        }
+        if (withIcon)   // implies indent the info
+            [finalString appendString: @"\t" withAttributes:detailBoldAttr];
+        [finalString appendString: @"Video: " withAttributes:detailBoldAttr];
+        [finalString appendString:[NSString stringWithFormat:@"%@\n", jobVideoDetail] withAttributes:detailAttr];
     }
+    
+    if (withx264Info)
+    {
+        if (vcodec == HB_VCODEC_X264 && x264opts)
+        {
+            if (withIcon)   // implies indent the info
+                [finalString appendString: @"\t" withAttributes:detailBoldAttr];
+            [finalString appendString: @"x264 Options: " withAttributes:detailBoldAttr];
+            [finalString appendString:[NSString stringWithFormat:@"%@\n", x264opts] withAttributes:detailAttr];
+        }
+    }
+
+    if (withAudioInfo)
+    {
+        if (withIcon)   // implies indent the info
+            [finalString appendString: @"\t" withAttributes:detailBoldAttr];
+        [finalString appendString: @"Audio: " withAttributes:detailBoldAttr];
+        [finalString appendString:[NSString stringWithFormat:@"%@\n", audioinfo_summary] withAttributes:detailAttr];
+    }
+    
+    if (withSubtitleInfo)
+    {
+        // subtitle scan == -1 in two cases:
+        // autoselect: when pass == -1
+        // none: when pass != -1
+        if ((subtitle == -1) && (pass == -1))
+        {
+            if (withIcon)   // implies indent the info
+                [finalString appendString: @"\t" withAttributes:detailBoldAttr];
+            [finalString appendString: @"Subtitles: " withAttributes:detailBoldAttr];
+            [finalString appendString: @"Autoselect " withAttributes:detailAttr];
+        }
+        else if (subtitle >= 0)
+        {
+            if (subtitleLang)
+            {
+                if (withIcon)   // implies indent the info
+                    [finalString appendString: @"\t" withAttributes:detailBoldAttr];
+                [finalString appendString: @"Subtitles: " withAttributes:detailBoldAttr];
+                [finalString appendString: subtitleLang   withAttributes:detailAttr];
+            }
+        }
+    }
+    
+    
+    if ([[finalString string] hasSuffix: @"\n"])
+        [finalString deleteCharactersInRange: NSMakeRange([[finalString string] length]-1, 1)];
+    
+    return finalString;
 }
 
-/**
- * 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 )
++ (NSMutableParagraphStyle *) descriptionParagraphStyle
 {
-    hb_job_t * j = NULL;
-    int index = 0;
-    while( ( j = hb_job( h, index++ ) ) )
+    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 -
+
+//------------------------------------------------------------------------------------
+// HBJobGroup
+//------------------------------------------------------------------------------------
+
+// Notification sent from HBJobGroup setStatus whenever the status changes.
+NSString *HBJobGroupStatusNotification = @"HBJobGroupStatusNotification";
+
+@implementation HBJobGroup
+
++ (HBJobGroup *) jobGroup;
+{
+    return [[[HBJobGroup alloc] init] autorelease];
+}
+
+- (id) init
+{
+    if (self = [super init])
+    {
+        fJobs = [[NSMutableArray arrayWithCapacity:0] retain];
+        fDescription = [[NSMutableAttributedString alloc] initWithString: @""];
+        [self setNeedsDescription: NO];
+        fStatus = HBStatusNone;
+    }
+    return self; 
+}
+
+- (void) dealloc
+{
+    [fPresetName release];
+    [fJobs release];
+    [super dealloc];
+}
+
+- (unsigned int) count
+{
+    return [fJobs count];
+}
+
+- (void) addJob: (HBJob *)aJob
+{
+    [aJob setJobGroup:self];
+    [fJobs addObject: aJob];
+    [self setNeedsDescription: YES];
+    fLastDescriptionHeight = 0;
+    fLastDescriptionWidth = 0;
+}
+
+- (HBJob *) jobAtIndex: (unsigned)index
+{
+    return [fJobs objectAtIndex: index];
+}
+
+- (unsigned) indexOfJob: (HBJob *)aJob;
+{
+    return [fJobs indexOfObject: aJob];
+}
+
+- (NSEnumerator *) jobEnumerator
+{
+    return [fJobs objectEnumerator];
+}
+
+- (void) setNeedsDescription: (BOOL)flag
+{
+    fNeedsDescription = flag;
+}
+
+- (void) updateDescription
+{
+    fNeedsDescription = NO;
+
+    [fDescription deleteCharactersInRange: NSMakeRange(0, [fDescription length])]; 
+
+    if ([self count] == 0)
     {
-        if (j == job)
-            return hb_job( h, index+1 );
+        NSAssert(NO, @" jobgroup with no jobs");
+        return;
     }
-    return NULL;
+    
+    HBJob * job = [self jobAtIndex:0];
+    
+    // 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
+                        withVideoInfo: NO
+                         withx264Info: NO
+                        withAudioInfo: NO
+                     withSubtitleInfo: NO]];
+
+
+    static NSAttributedString * carriageReturn = [[NSAttributedString alloc] initWithString:@"\n"];
+    
+    NSEnumerator * e = [self jobEnumerator];
+    while ( (job = [e nextObject]) )
+    {
+        int pass = job->pass;
+        [fDescription appendAttributedString:carriageReturn];
+        [fDescription appendAttributedString:
+            [job attributedDescriptionWithIcon: YES
+                                withTitle: NO
+                             withPassName: YES
+                           withFormatInfo: NO
+                          withDestination: NO
+                          withPictureInfo: pass != -1
+                            withVideoInfo: pass != -1
+                             withx264Info: pass != -1
+                            withAudioInfo: pass == 0 || pass == 2
+                         withSubtitleInfo: YES]];
+    }
+    
+}
+
+- (NSMutableAttributedString *) attributedDescription
+{
+    if (fNeedsDescription)
+        [self updateDescription];
+    return fDescription;
+}
+
+- (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 updateDescription];
+
+    // Calculate the height    
+    NSRect bounds = [fDescription boundingRectWithSize:NSMakeSize(width, 10000) options:NSStringDrawingUsesLineFragmentOrigin];
+    fLastDescriptionHeight = bounds.size.height + 6.0; // add some border to bottom
+    fLastDescriptionWidth = width;
+    return fLastDescriptionHeight;
+
+/* supposedly another way to do this, in case boundingRectWithSize isn't working
+    NSTextView* tmpView = [[NSTextView alloc] initWithFrame:NSMakeRect(0, 0, width, 1)];
+    [[tmpView textStorage] setAttributedString:aString];
+    [tmpView setHorizontallyResizable:NO];
+    [tmpView setVerticallyResizable:YES];
+//    [[tmpView textContainer] setHeightTracksTextView: YES];
+//    [[tmpView textContainer] setContainerSize: NSMakeSize(width, 10000)];
+    [tmpView sizeToFit];
+    float height = [tmpView frame].size.height;
+    [tmpView release];
+    return height;
+*/
+}
+
+- (float) lastDescriptionHeight
+{
+    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
+
+
 #pragma mark -
 
 // Toolbar identifiers
-static NSString*    HBQueueToolbar                            = @"HBQueueToolbar";
-static NSString*    HBStartPauseResumeToolbarIdentifier       = @"HBStartPauseResumeToolbarIdentifier";
-static NSString*    HBShowDetailToolbarIdentifier             = @"HBShowDetailToolbarIdentifier";
-static NSString*    HBShowGroupsToolbarIdentifier             = @"HBShowGroupsToolbarIdentifier";
+static NSString*    HBQueueToolbar                            = @"HBQueueToolbar1";
+static NSString*    HBQueueStartCancelToolbarIdentifier       = @"HBQueueStartCancelToolbarIdentifier";
+static NSString*    HBQueuePauseResumeToolbarIdentifier       = @"HBQueuePauseResumeToolbarIdentifier";
 
+#pragma mark -
 
 @implementation HBQueueController
 
@@ -115,59 +890,91 @@ static NSString*    HBShowGroupsToolbarIdentifier             = @"HBShowGroupsTo
             @"YES",     @"QueueShowsJobsAsGroups",
             nil]];
 
-        fShowsDetail = [[NSUserDefaults standardUserDefaults] boolForKey:@"QueueShowsDetail"];
-        fShowsJobsAsGroups = [[NSUserDefaults standardUserDefaults] boolForKey:@"QueueShowsJobsAsGroups"];
+        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; 
 }
 
 //------------------------------------------------------------------------------------
-// dealloc
+// dealloc
+//------------------------------------------------------------------------------------
+- (void)dealloc
+{
+    // 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];
+}
+
+//------------------------------------------------------------------------------------
+// Receive HB handle
+//------------------------------------------------------------------------------------
+- (void)setHandle: (hb_handle_t *)handle
+{
+    fHandle = handle;
+}
+
+//------------------------------------------------------------------------------------
+// Receive HBController
+//------------------------------------------------------------------------------------
+- (void)setHBController: (HBController *)controller
+{
+    fHBController = controller;
+}
+
+#pragma mark -
+#pragma mark - Getting the currently processing job group
+
+//------------------------------------------------------------------------------------
+// Returns the HBJobGroup that is currently being encoded; nil if no encoding is
+// occurring.
 //------------------------------------------------------------------------------------
-- (void)dealloc
+- (HBJobGroup *) currentJobGroup;
 {
-    [fAnimation release];
-    
-    // clear the delegate so that windowWillClose is not attempted
-    if ([fQueueWindow delegate] == self)
-        [fQueueWindow setDelegate:nil];
-    
-    [super dealloc];
+    return fCurrentJobGroup;
 }
 
 //------------------------------------------------------------------------------------
-// Receive HB handle
+// Returns the HBJob (pass) that is currently being encoded; nil if no encoding is
+// occurring.
 //------------------------------------------------------------------------------------
-- (void)setHandle: (hb_handle_t *)handle
+- (HBJob *) currentJob
 {
-    fHandle = handle;
+    return fCurrentJob;
 }
 
+#pragma mark -
+
 //------------------------------------------------------------------------------------
 // Displays and brings the queue window to the front
 //------------------------------------------------------------------------------------
 - (IBAction) showQueueWindow: (id)sender
 {
-    if (!fQueueWindow)
-    {
-        BOOL loadSucceeded = [NSBundle loadNibNamed:@"Queue" owner:self] && fQueueWindow;
-        NSAssert(loadSucceeded, @"Could not open Queue nib file");
-    }
-
-    [self updateQueueUI];
-    [self updateCurrentJobUI];
-
     [fQueueWindow makeKeyAndOrderFront: self];
-
     [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"QueueWindowIsOpen"];
 }
+
 //------------------------------------------------------------------------------------
 // Show or hide the current job pane (fCurrentJobPane).
 //------------------------------------------------------------------------------------
 - (void) showCurrentJobPane: (BOOL)showPane
 {
-    if (showPane != fCurrentJobHidden)
+    if (showPane == fCurrentJobPaneShown)
         return;
     
     // Things to keep in mind:
@@ -200,302 +1007,331 @@ static NSString*    HBShowGroupsToolbarIdentifier             = @"HBShowGroupsTo
         [NSValue valueWithRect:queueFrame], NSViewAnimationEndFrameKey,
         nil];
 
-    if (!fAnimation)
-        fAnimation = [[NSViewAnimation alloc] initWithViewAnimations:nil];
+    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;
+}
+
+//------------------------------------------------------------------------------------
+// Sets fCurrentJobGroup to a new job group.
+//------------------------------------------------------------------------------------
+- (void) setCurrentJobGroup: (HBJobGroup *)aJobGroup
+{
+    if (aJobGroup)
+        [aJobGroup setStatus: HBStatusWorking];
 
-    [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];
-    fCurrentJobHidden = !showPane;
+    [aJobGroup retain];
+    [fCurrentJobGroup release];
+    fCurrentJobGroup = aJobGroup;
 }
 
+#pragma mark - Finding job groups
+
 //------------------------------------------------------------------------------------
-// Enables or disables the display of detail information for each job.
+// Returns the first pending job with a specified destination path or nil if no such
+// job exists.
 //------------------------------------------------------------------------------------
-- (void)setShowsDetail: (BOOL)showsDetail
+- (HBJobGroup *) pendingJobGroupWithDestinationPath: (NSString *)path
 {
-    fShowsDetail = showsDetail;
-    
-    [[NSUserDefaults standardUserDefaults] setBool:showsDetail forKey:@"QueueShowsDetail"];
-    [[NSUserDefaults standardUserDefaults] synchronize];
+    HBJobGroup * aJobGroup;
+    NSEnumerator * groupEnum = [fJobGroups objectEnumerator];
+    while ( (aJobGroup = [groupEnum nextObject]) )
+    {
+        if ([[aJobGroup destinationPath] isEqualToString: path])
+            return aJobGroup;
+    }
+    return nil;
+}
 
-    // clumsy - have to update UI
-    [fDetailCheckbox setState:showsDetail ? NSOnState : NSOffState];
-    
-    [fTaskView setRowHeight:showsDetail ? 110.0 : 17.0];
-    if ([fTaskView selectedRow] != -1)
-        [fTaskView scrollRowToVisible:[fTaskView selectedRow]];
+//------------------------------------------------------------------------------------
+// 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]) )
+    {
+        HBJob * job;
+        NSEnumerator * jobEnum = [aJobGroup jobEnumerator];
+        while ( (job = [jobEnum nextObject]) )
+        {
+            if (job->sequence_id == aJobID)
+                return job;
+        }
+    }
+    return nil;
 }
 
 //------------------------------------------------------------------------------------
-// Enables or disables the grouping of job passes into one item in the UI.
+// Locates and returns a libhb job whose sequence_id matches a specified value.
 //------------------------------------------------------------------------------------
-- (void)setShowsJobsAsGroups: (BOOL)showsGroups
+- (hb_job_t *) findLibhbJobWithID: (int)aJobID
 {
-    fShowsJobsAsGroups = showsGroups;
-    
-    [[NSUserDefaults standardUserDefaults] setBool:showsGroups forKey:@"QueueShowsJobsAsGroups"];
-    [[NSUserDefaults standardUserDefaults] synchronize];
+    hb_job_t * job;
+    int index = 0;
+    while( ( job = hb_job( fHandle, index++ ) ) )
+    {
+        if (job->sequence_id == aJobID)
+            return job;
+    }
+    return nil;
+}
 
-    // clumsy - have to update UI
-    [fJobGroupsCheckbox setState:showsGroups ? NSOnState : NSOffState];
-    
-    [self updateQueueUI];
-    if ([fTaskView selectedRow] != -1)
-        [fTaskView scrollRowToVisible:[fTaskView selectedRow]];
+#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;
 }
 
 //------------------------------------------------------------------------------------
-// Generates a multi-line text string that includes the job name on the first line
-// followed by details of the job on subsequent lines. If the text is to be drawn as
-// part of a highlighted cell, set isHighlighted to true. The returned string may
-// contain multiple fonts and paragraph formating.
+// Recalculates and stores new values in fPendingCount, fCompletedCount,
+// fCanceledCount, and fWorkingCount.
 //------------------------------------------------------------------------------------
-- (NSAttributedString *)attributedDescriptionForJob: (hb_job_t *)job
-                                         withDetail: (BOOL)detail
-                                   withHighlighting: (BOOL)highlighted
+- (void) recalculateJobGroupCounts
 {
-    NSMutableAttributedString * finalString;   // the return value
-    NSAttributedString* anAttributedString;    // a temp string for building up attributed substrings
-    NSMutableString* aMutableString;           // a temp string for non-attributed substrings
-    hb_title_t * title = job->title;
-    
-    NSMutableParagraphStyle *ps = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
-    [ps setLineBreakMode:NSLineBreakByClipping];
+    fPendingCount = 0;
+    fCompletedCount = 0;
+    fCanceledCount = 0;
+    fWorkingCount = 0;
+
+    NSEnumerator * groupEnum = [fJobGroups objectEnumerator];
+    HBJobGroup * aJobGroup;
+    while ( (aJobGroup = [groupEnum nextObject]) )
+    {
+        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;
+}
 
-    static NSDictionary* detailAttribute = [[NSDictionary dictionaryWithObjectsAndKeys:
-                [NSFont systemFontOfSize:10.0], NSFontAttributeName,
-                [NSColor darkGrayColor], NSForegroundColorAttributeName,
-                ps, NSParagraphStyleAttributeName,
-                nil] retain];
-    static NSDictionary* detailHighlightedAttribute = [[NSDictionary dictionaryWithObjectsAndKeys:
-                [NSFont systemFontOfSize:10.0], NSFontAttributeName,
-                [NSColor whiteColor], NSForegroundColorAttributeName,
-                ps, NSParagraphStyleAttributeName,
-                nil] retain];
-    static NSDictionary* titleAttribute = [[NSDictionary dictionaryWithObject:
-                [NSFont systemFontOfSize:[NSFont systemFontSize]] forKey:NSFontAttributeName] retain];
+//------------------------------------------------------------------------------------
+// Returns the number of job groups whose status is HBStatusPending.
+//------------------------------------------------------------------------------------
+- (unsigned int) pendingCount
+{
+    if (fJobGroupCountsNeedUpdating)
+        [self recalculateJobGroupCounts];
+    return fPendingCount;
+}
 
-    finalString = [[[NSMutableAttributedString alloc] init] autorelease];
+//------------------------------------------------------------------------------------
+// Returns the number of job groups whose status is HBStatusCompleted.
+//------------------------------------------------------------------------------------
+- (unsigned int) completedCount
+{
+    if (fJobGroupCountsNeedUpdating)
+        [self recalculateJobGroupCounts];
+    return fCompletedCount;
+}
 
-    // Title, in bold
-    // Show the name of the source Note: use title->name instead of title->dvd since
-    // name is just the chosen folder, instead of dvd which is the full path
-    anAttributedString = [[[NSAttributedString alloc] initWithString:[NSString stringWithUTF8String:title->name] attributes:titleAttribute] autorelease];
-    [finalString appendAttributedString:anAttributedString];
+//------------------------------------------------------------------------------------
+// Returns the number of job groups whose status is HBStatusCanceled.
+//------------------------------------------------------------------------------------
+- (unsigned int) canceledCount
+{
+    if (fJobGroupCountsNeedUpdating)
+        [self recalculateJobGroupCounts];
+    return fCanceledCount;
+}
 
-    if (!detail)
-        return finalString;
-        
-    // Other info in plain
-    aMutableString = [NSMutableString stringWithCapacity:200];
-    
-    BOOL jobGroups = [[NSUserDefaults standardUserDefaults] boolForKey:@"QueueShowsJobsAsGroups"];
+//------------------------------------------------------------------------------------
+// Returns the number of job groups whose status is HBStatusWorking.
+//------------------------------------------------------------------------------------
+- (unsigned int) workingCount
+{
+    if (fJobGroupCountsNeedUpdating)
+        [self recalculateJobGroupCounts];
+    return fWorkingCount;
+}
 
-    // The subtitle scan doesn't contain all the stuff we need (like x264opts).
-    // So grab the next job in the group for display purposes.
-    if (jobGroups && job->pass == -1)
-    {
-        // 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 (job == hb_current_job(fHandle))
-            nextjob = hb_job(fHandle, 0);
-        else
-            nextjob = hb_next_job(fHandle, job);
-        if (nextjob)    // Overly cautious in case there is no next job!
-            job = nextjob;
-    }
+#pragma mark -
+#pragma mark UI Updating
 
-    NSString * chapterString = (job->chapter_start == job->chapter_end) ?
-            [NSString stringWithFormat:@"Chapter %d", job->chapter_start] :
-            [NSString stringWithFormat:@"Chapters %d through %d", job->chapter_start, job->chapter_end];
+//------------------------------------------------------------------------------------
+// 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
+{
+    if (!fSavedExpandedItems)
+        fSavedExpandedItems = [[NSMutableIndexSet alloc] init];
+    else
+        [fSavedExpandedItems removeAllIndexes];
     
-    // Scan pass
-    if (job->pass == -1)
+    // 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]) )
     {
-        [aMutableString appendString:[NSString stringWithFormat:
-                @"\nTitle %d, %@, Pass: Scan", title->index, chapterString]];
+        if ([fOutlineView isItemExpanded: aJobGroup])
+            [fSavedExpandedItems addIndex: [aJobGroup jobAtIndex:0]->sequence_id];
     }
+    
+    // Save the selection also.
 
-    // Normal pass
+    if (!fSavedSelectedItems)
+        fSavedSelectedItems = [[NSMutableIndexSet alloc] init];
     else
+        [fSavedSelectedItems removeAllIndexes];
+
+    NSIndexSet * selectedRows = [fOutlineView selectedRowIndexes];
+    int row = [selectedRows firstIndex];
+    while (row != NSNotFound)
     {
-        if (jobGroups)
-            [aMutableString appendString:[NSString stringWithFormat:
-                    @"\nTitle %d, %@, %d-Pass",
-                    title->index, chapterString, MIN( 2, job->pass + 1 )]];
-        else
-            [aMutableString appendString:[NSString stringWithFormat:
-                    @"\nTitle %d, %@, Pass %d of %d",
-                    title->index, chapterString, MAX( 1, job->pass ), MIN( 2, job->pass + 1 )]];
-        
+        aJobGroup = [fOutlineView itemAtRow: row];
+        [fSavedSelectedItems addIndex: [aJobGroup jobAtIndex:0]->sequence_id];
+        row = [selectedRows indexGreaterThanIndex: row];
+    }
 
-        NSString * jobFormat;
-        NSString * jobPictureDetail;
-        NSString * jobVideoDetail;
-        NSString * jobVideoCodec;
-        NSString * jobVideoQuality;
-        NSString * jobAudioDetail;
-        NSString * jobAudioCodec;
-
-        /* Muxer settings (File Format in the gui) */
-        if (job->mux == 65536 || job->mux == 131072 || job->mux == 1048576)
-            jobFormat = @"MP4"; // HB_MUX_MP4,HB_MUX_PSP,HB_MUX_IPOD
-        else if (job->mux == 262144)
-            jobFormat = @"AVI"; // HB_MUX_AVI
-        else if (job->mux == 524288)
-            jobFormat = @"OGM"; // HB_MUX_OGM
-        else if (job->mux == 2097152)
-            jobFormat = @"MKV"; // HB_MUX_MKV
-        else
-            jobFormat = @"unknown";
-        
-        // 2097152
-        /* Video Codec settings (Encoder in the gui) */
-        if (job->vcodec == 1)
-            jobVideoCodec = @"FFmpeg"; // HB_VCODEC_FFMPEG
-        else if (job->vcodec == 2)
-            jobVideoCodec = @"XviD"; // HB_VCODEC_XVID
-        else if (job->vcodec == 4)
-        {
-            /* Deterimine for sure how we are now setting iPod uuid atom */
-            if (job->h264_level) // We are encoding for iPod
-                jobVideoCodec = @"x264 (H.264 iPod)"; // HB_VCODEC_X264    
-            else
-                jobVideoCodec = @"x264 (H.264 Main)"; // HB_VCODEC_X264
-        }
-        else
-            jobVideoCodec = @"unknown";
-        
-        /* Audio Codecs (Second half of Codecs in the gui) */
-        if (job->acodec == 256)
-            jobAudioCodec = @"AAC"; // HB_ACODEC_FAAC
-        else if (job->acodec == 512)
-            jobAudioCodec = @"MP3"; // HB_ACODEC_LAME
-        else if (job->acodec == 1024)
-            jobAudioCodec = @"Vorbis"; // HB_ACODEC_VORBIS
-        else if (job->acodec == 2048)
-            jobAudioCodec = @"AC3"; // HB_ACODEC_AC3
-        else
-            jobAudioCodec = @"unknown";
-        /* Show Basic File info */
-        if (job->chapter_markers == 1)
-            [aMutableString appendString:[NSString stringWithFormat:@"\nFormat: %@ Container, %@ Video + %@ Audio, Chapter Markers", jobFormat, jobVideoCodec, jobAudioCodec]];
-        else
-            [aMutableString appendString:[NSString stringWithFormat:@"\nFormat: %@ Container, %@ Video + %@ Audio", jobFormat, jobVideoCodec, jobAudioCodec]];
-            
-        /*Picture info*/
-        /*integers for picture values deinterlace, crop[4], keep_ratio, grayscale, pixel_ratio, pixel_aspect_width, pixel_aspect_height,
-         maxWidth, maxHeight */
-        if (job->pixel_ratio == 1)
-        {
-            int titlewidth = title->width - job->crop[2] - job->crop[3];
-            int displayparwidth = titlewidth * job->pixel_aspect_width / job->pixel_aspect_height;
-            int displayparheight = title->height - job->crop[0] - job->crop[1];
-            jobPictureDetail = [NSString stringWithFormat:@"Picture: %dx%d (%dx%d Anamorphic)", displayparwidth, displayparheight, job->width, displayparheight];
-        }
-        else
-            jobPictureDetail = [NSString stringWithFormat:@"Picture: %dx%d", job->width, job->height];
-        if (job->keep_ratio == 1)
-            jobPictureDetail = [jobPictureDetail stringByAppendingString:@" Keep Aspect Ratio"];
-        
-        if (job->grayscale == 1)
-            jobPictureDetail = [jobPictureDetail stringByAppendingString:@", Grayscale"];
-        
-        if (job->deinterlace == 1)
-            jobPictureDetail = [jobPictureDetail stringByAppendingString:@", Deinterlace"];
-        /* Show Picture info */    
-        [aMutableString appendString:[NSString stringWithFormat:@"\n%@", jobPictureDetail]];
-        
-        /* Detailed Video info */
-        if (job->vquality <= 0 || job->vquality >= 1)
-            jobVideoQuality =[NSString stringWithFormat:@"%d kbps", job->vbitrate];
-        else
-        {
-            NSNumber * vidQuality;
-            vidQuality = [NSNumber numberWithInt:job->vquality * 100];
-            /* this is screwed up kind of. Needs to be formatted properly */
-            if (job->crf == 1)
-                jobVideoQuality =[NSString stringWithFormat:@"%@%% CRF", vidQuality];            
-            else
-                jobVideoQuality =[NSString stringWithFormat:@"%@%% CQP", vidQuality];
-        }
-        
-        if (job->vrate_base == 1126125)
-        {
-            /* NTSC FILM 23.976 */
-            jobVideoDetail = [NSString stringWithFormat:@"Video: %@, %@, 23.976 fps", jobVideoCodec, jobVideoQuality];
-        }
-        else if (job->vrate_base == 900900)
+}
+
+//------------------------------------------------------------------------------------
+// Restores the expanded state of items in the outline view to match those saved by a
+// previous call to saveOutlineViewState.
+//------------------------------------------------------------------------------------
+- (void) restoreOutlineViewState
+{
+    if (fSavedExpandedItems)
+    {
+        HBJobGroup * aJobGroup;
+        NSEnumerator * e = [fJobGroups objectEnumerator];
+        while ( (aJobGroup = [e nextObject]) )
         {
-            /* NTSC 29.97 */
-            jobVideoDetail = [NSString stringWithFormat:@"Video: %@, %@, 29.97 fps", jobVideoCodec, jobVideoQuality];
+            HBJob * job = [aJobGroup jobAtIndex:0];
+            if (job && [fSavedExpandedItems containsIndex: job->sequence_id])
+                [fOutlineView expandItem: aJobGroup];
         }
-        else
+    }
+    
+    if (fSavedSelectedItems)
+    {
+        NSMutableIndexSet * rowsToSelect = [[[NSMutableIndexSet alloc] init] autorelease];
+        HBJobGroup * aJobGroup;
+        NSEnumerator * e = [fJobGroups objectEnumerator];
+        int i = 0;
+        while ( (aJobGroup = [e nextObject]) )
         {
-            /* Everything else */
-            jobVideoDetail = [NSString stringWithFormat:@"Video: %@, %@, %d fps", jobVideoCodec, jobVideoQuality, job->vrate / job->vrate_base];
+            HBJob * job = [aJobGroup jobAtIndex:0];
+            if (job && [fSavedSelectedItems containsIndex: job->sequence_id])
+                [rowsToSelect addIndex: i];
+            i++;
         }
-        
-        /* Add the video detail string to the job filed in the window */
-        [aMutableString appendString:[NSString stringWithFormat:@"\n%@", jobVideoDetail]];
-        
-        /* if there is an x264 option string, lets add it here*/
-        /*NOTE: Due to size, lets get this in a tool tip*/
-        
-        if (job->x264opts)
-            [aMutableString appendString:[NSString stringWithFormat:@"\nx264 Options: %@", [NSString stringWithUTF8String:job->x264opts]]];
-        
-        /* Audio Detail */
-        if ([jobAudioCodec isEqualToString: @"AC3"])
-            jobAudioDetail = [NSString stringWithFormat:@"Audio: %@, Pass-Through", jobAudioCodec];
+        if ([rowsToSelect count] == 0)
+            [fOutlineView deselectAll: nil];
         else
-            jobAudioDetail = [NSString stringWithFormat:@"Audio: %@, %d kbps, %d Hz", jobAudioCodec, job->abitrate, job->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] */
-        int ai; // counter for each audios [] , macgui only allows for two audio tracks currently
-        for( ai = 0; ai < 2; ai++ )
-        {
-            if (job->audio_mixdowns[ai] == HB_AMIXDOWN_MONO)
-                jobAudioDetail = [jobAudioDetail stringByAppendingString:[NSString stringWithFormat:@", Track %d: Mono",ai + 1]];
-            if (job->audio_mixdowns[ai] == HB_AMIXDOWN_STEREO)
-                jobAudioDetail = [jobAudioDetail stringByAppendingString:[NSString stringWithFormat:@", Track %d: Stereo",ai + 1]];
-            if (job->audio_mixdowns[ai] == HB_AMIXDOWN_DOLBY)
-                jobAudioDetail = [jobAudioDetail stringByAppendingString:[NSString stringWithFormat:@", Track %d: Dolby Surround",ai + 1]];
-            if (job->audio_mixdowns[ai] == HB_AMIXDOWN_DOLBYPLII)
-                jobAudioDetail = [jobAudioDetail stringByAppendingString:[NSString stringWithFormat:@", Track %d: Dolby Pro Logic II",ai + 1]];
-            if (job->audio_mixdowns[ai] == HB_AMIXDOWN_6CH)
-                jobAudioDetail = [jobAudioDetail stringByAppendingString:[NSString stringWithFormat:@", Track %d: 6-channel discreet",ai + 1]];
-        }
-        
-        /* Add the Audio detail string to the job filed in the window */
-        [aMutableString appendString:[NSString stringWithFormat: @"\n%@", jobAudioDetail]];
-        
-        /*Destination Field */
-        [aMutableString appendString:[NSString stringWithFormat:@"\nDestination: %@", [NSString stringWithUTF8String:job->file]]];
+            [fOutlineView selectRowIndexes:rowsToSelect byExtendingSelection:NO];
     }
-    
-    anAttributedString = [[[NSAttributedString alloc] initWithString:aMutableString attributes:highlighted ? detailHighlightedAttribute : detailAttribute] autorelease];
-    [finalString appendAttributedString:anAttributedString];
+}
 
-            
-    return finalString;
+//------------------------------------------------------------------------------------
+// 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)
     {
         NSString * msg;
         if (job->pass == -1)
-            msg = NSLocalizedString( @"Analyzing subtitles", nil );
+            msg = NSLocalizedString( @"Deep Scan", nil );
         else if (job->pass == 1)
             msg = NSLocalizedString( @"Analyzing video", nil );
         else if ((job->pass == 0) ||  (job->pass == 2))
@@ -529,7 +1365,7 @@ static NSString*    HBShowGroupsToolbarIdentifier             = @"HBShowGroupsTo
 //------------------------------------------------------------------------------------
 // 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)
     {
@@ -599,6 +1435,18 @@ static NSString*    HBShowGroupsToolbarIdentifier             = @"HBShowGroupsTo
 }
 
 //------------------------------------------------------------------------------------
+// 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
@@ -607,12 +1455,7 @@ static NSString*    HBShowGroupsToolbarIdentifier             = @"HBShowGroupsTo
     {
         #define p s->param.working
         [fProgressBar setIndeterminate:NO];
-
-        BOOL jobGroups = [[NSUserDefaults standardUserDefaults] boolForKey:@"QueueShowsJobsAsGroups"];
-        float progress_total = jobGroups ?
-                100.0 * ( p.progress + p.job_cur - 1 ) / p.job_count :
-                100.0 * p.progress;
-
+        float progress_total = 100.0 * ( p.progress + p.job_cur - 1 ) / p.job_count;
         [fProgressBar setDoubleValue:progress_total];
         #undef p
     }
@@ -628,278 +1471,459 @@ static NSString*    HBShowGroupsToolbarIdentifier             = @"HBShowGroupsTo
     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
 }
 
 //------------------------------------------------------------------------------------
-// Refresh start/pause button (fStartPauseButton) from current state.
+// Refresh queue count text field (fQueueCountField).
 //------------------------------------------------------------------------------------
-- (void) updateStartPauseButton
+- (void)updateQueueCountField
 {
-
-// ************* THIS METHOD CAN DISAPPEAR. THE BUTTON IS NOW HIDDEN AND CAN BE DELETED
-    if (!fHandle) return;
-
-    hb_state_t s;
-    hb_get_state2 (fHandle, &s);
-
-    if (s.state == HB_STATE_PAUSED)
-    {
-        [fStartPauseButton setEnabled:YES];
-//        [fStartPauseButton setTitle:NSLocalizedString(@"Resume", nil)];
-        [fStartPauseButton setImage:[NSImage imageNamed: @"Play"]];
-   }
-    
-    else if ((s.state == HB_STATE_WORKING) || (s.state == HB_STATE_MUXING))
+    NSString * msg;
+    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
     {
-        [fStartPauseButton setEnabled:YES];
-//        [fStartPauseButton setTitle:NSLocalizedString(@"Pause", nil)];
-        [fStartPauseButton setImage:[NSImage imageNamed: @"Pause"]];
+        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];
+}
 
-    else if (hb_count(fHandle) > 0)
+//------------------------------------------------------------------------------------
+// Refresh the UI in the current job pane. Should be called whenever the current job
+// being processed has changed.
+//------------------------------------------------------------------------------------
+- (void)updateCurrentJobDescription
+{
+    if (fCurrentJob)
     {
-        [fStartPauseButton setEnabled:YES];
-//        [fStartPauseButton setTitle:NSLocalizedString(@"Start", nil)];
-        [fStartPauseButton setImage:[NSImage imageNamed: @"Play"]];
+        switch (fCurrentJob->pass)
+        {
+            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;
+                
+            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]];
+        }
     }
-
     else
     {
-        [fStartPauseButton setEnabled:NO];
-//        [fStartPauseButton setTitle:NSLocalizedString(@"Start", nil)];
-        [fStartPauseButton setImage:[NSImage imageNamed: @"Play"]];
+        [fJobDescTextField setStringValue: @"No encodes pending"];
+    
     }
+    
 }
 
 //------------------------------------------------------------------------------------
-// Refresh queue count text field (fQueueCountField).
+// 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)updateQueueCountField
+- (void)updateCurrentJobProgress
 {
-    NSString * msg;
-    int jobCount;
-    BOOL jobGroups = [[NSUserDefaults standardUserDefaults] boolForKey:@"QueueShowsJobsAsGroups"];
-    
-    if (jobGroups)
-    {
-        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];
-    }
-    else
-    {
-        jobCount = fHandle ? hb_count(fHandle) : 0;
-        if (jobCount == 1)
-            msg = NSLocalizedString(@"1 pending pass", nil);
-        else
-            msg = [NSString stringWithFormat:NSLocalizedString(@"%d pending passes", nil), jobCount];
+    hb_state_t s;
+    hb_get_state2( fHandle, &s );
+    [self updateProgressTextForJob: fCurrentJob state: &s];
+    [self updateProgressBarWithState:&s];
+}
 
-    }
+//------------------------------------------------------------------------------------
+// Notifies HBQueuecontroller that the contents of fJobGroups is about to be modified.
+// HBQueuecontroller remembers the state of the UI (selection and expanded items).
+//------------------------------------------------------------------------------------
+- (void) beginEditingJobGroupsArray
+{
+    [self saveOutlineViewState];
+}
 
-    [fQueueCountField setStringValue:msg];
+//------------------------------------------------------------------------------------
+// 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
+
 //------------------------------------------------------------------------------------
-// Refresh the UI in the current job pane. Should be called whenever the current job
-// being processed has changed or when progress has changed.
+// Deletes the selected jobs from HB and the queue UI
 //------------------------------------------------------------------------------------
-- (void)updateCurrentJobUI
+- (IBAction)removeSelectedJobGroups: (id)sender
 {
-    hb_state_t s;
-    hb_job_t * job = nil;
+    if (!fHandle) return;
     
-    if (fHandle)
+    NSIndexSet * selectedRows = [fOutlineView selectedRowIndexes];
+    int row = [selectedRows firstIndex];
+    if (row != NSNotFound)
     {
-        hb_get_state( fHandle, &s );
-        job = hb_current_job(fHandle);
-    }
+        [self beginEditingJobGroupsArray];
+        while (row != NSNotFound)
+        {
+            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];
+        }
+        [self endEditingJobGroupsArray];
+    } 
+}
 
-    if (job)
+//------------------------------------------------------------------------------------
+// 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)
     {
-        [fJobDescTextField setAttributedStringValue:[self attributedDescriptionForJob:job withDetail:YES withHighlighting:NO]];
-
-        [self showCurrentJobPane:YES];
-        [fJobIconView setImage: fShowsJobsAsGroups ? [NSImage imageNamed:@"JobLarge"] : [NSImage imageNamed:@"JobPassLarge"] ];
+        while (row != NSNotFound)
+        {
+            HBJobGroup * jobGroup = [fOutlineView itemAtRow: row];
+            if ([[jobGroup destinationPath] length])
+                [[NSWorkspace sharedWorkspace] selectFile:[jobGroup destinationPath] inFileViewerRootedAtPath:nil];
         
-        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)];
-
-        [self showCurrentJobPane:NO];
-        [fProgressBar stopAnimation:nil];    // just in case in was animating
-    }
-
-    // Gross hack. Also update start/pause button. Have to do it here since we don't
-    // have any other periodic chance to update the button.
-    [self updateStartPauseButton];
+            row = [selectedRows indexGreaterThanIndex: row];
+        }
+    } 
 }
 
 //------------------------------------------------------------------------------------
-// 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.
+// 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.
 //------------------------------------------------------------------------------------
-- (void)updateQueueUI
+- (IBAction)cancelCurrentJob: (id)sender
 {
-    [fTaskView noteNumberOfRowsChanged];
-    [fTaskView reloadData];
-    
-    [self updateQueueCountField];
-    [self updateStartPauseButton];
+    [fHBController Cancel:sender];
 }
 
 //------------------------------------------------------------------------------------
-// Deletes the selected job from HB and the queue UI
+// Starts or cancels the processing of jobs depending on the current state
 //------------------------------------------------------------------------------------
-- (IBAction)removeSelectedJob: (id)sender
+- (IBAction)toggleStartCancel: (id)sender
 {
     if (!fHandle) return;
     
-    int row = [sender selectedRow];
-    if (row != -1)
-    {
-        BOOL jobGroups = [[NSUserDefaults standardUserDefaults] boolForKey:@"QueueShowsJobsAsGroups"];
-        if (jobGroups)
-            hb_rem_group( fHandle, hb_group( fHandle, row ) );
-        else
-            hb_rem( fHandle, hb_job( fHandle, row ) );
-        [self updateQueueUI];
-    }
+    hb_state_t s;
+    hb_get_state2 (fHandle, &s);
+
+    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 ([self pendingCount] > 0)
+        [fHBController doRip];
 }
 
 //------------------------------------------------------------------------------------
-// Prompts user if the want to cancel encoding of current job. If so, hb_stop gets
-// called.
+// Toggles the pause/resume state of libhb
 //------------------------------------------------------------------------------------
-- (IBAction)cancelCurrentJob: (id)sender
+- (IBAction)togglePauseResume: (id)sender
 {
     if (!fHandle) return;
     
-    hb_job_t * job = hb_current_job(fHandle);
-    if (!job) return;
+    hb_state_t s;
+    hb_get_state2 (fHandle, &s);
 
-    // If command key is down, don't prompt
-    BOOL hasCmdKeyMask = ([[NSApp currentEvent] modifierFlags] & NSCommandKeyMask) != 0;
-    if (hasCmdKeyMask)
-        hb_stop(fHandle);
-    else
-    {
-        NSString * alertTitle = [NSString stringWithFormat:NSLocalizedString(@"Do you want to stop processing of %@?", nil),
-                [NSString stringWithUTF8String:job->title->name]];
-        
-        NSBeginCriticalAlertSheet(
-                alertTitle,
-                NSLocalizedString(@"Stop Processing", nil), NSLocalizedString(@"Keep Processing", nil), nil, fQueueWindow, self,
-                @selector(cancelCurrentJob:returnCode:contextInfo:), nil, nil,
-                NSLocalizedString(@"Your movie will be lost if you don't continue processing.", nil),
-                [NSString stringWithUTF8String:job->title->name]);
-
-        // cancelCurrentJob:returnCode:contextInfo: will be called when the dialog is dismissed
-    }
+    if (s.state == HB_STATE_PAUSED)
+        hb_resume (fHandle);
+    else if ((s.state == HB_STATE_WORKING) || (s.state == HB_STATE_MUXING))
+        hb_pause (fHandle);
 }
 
-- (void) cancelCurrentJob: (NSWindow *)sheet returnCode: (int)returnCode contextInfo: (void *)contextInfo
-{
-    if (returnCode == NSAlertDefaultReturn)
-        hb_stop(fHandle);
-}
+#pragma mark -
+#pragma mark Synchronizing with libhb 
 
 //------------------------------------------------------------------------------------
-// Enables or disables the display of detail information for each job based on the 
-// state of the sender.
+// Queues a job group. The job group's status is set to HBStatusPending.
 //------------------------------------------------------------------------------------
-- (IBAction)detailChanged: (id)sender
+- (void) addJobGroup: (HBJobGroup *) aJobGroup
 {
-    if ([sender isMemberOfClass:[NSButton class]])
-    {
-        BOOL detail = [sender state] == NSOnState;
-        [[NSUserDefaults standardUserDefaults] setBool:detail forKey:@"QueueShowsDetail"];
-
-        [self setShowsDetail:detail];
-    }
+    NSAssert(![fJobGroups containsObject:aJobGroup], @"Duplicate job group");
+    [aJobGroup setStatus:HBStatusPending];
+    
+    [self beginEditingJobGroupsArray];
+    [fJobGroups addObject:aJobGroup];
+    [self endEditingJobGroupsArray];
 }
 
 //------------------------------------------------------------------------------------
-// Enables or disables the display of job groups based on the state of the sender.
+// Notifies HBQueueController that libhb's current job has changed
 //------------------------------------------------------------------------------------
-- (IBAction)jobGroupsChanged: (id)sender
+- (void)currentJobChanged: (HBJob *) currentJob
 {
-    if ([sender isMemberOfClass:[NSButton class]])
+    /* if the job has a destination path, lets perform finished job notifications in fHBController 
+     * We call this here so that we pickup the last job in the queue and single encodes before fCurrentJob
+     * is released. So for the first job and the beginning of single encodes we check for the existence
+     * of a valid fCurrentJob jobGroup
+     */
+    [currentJob retain];
+    /* We need to compare the job group to determine if this is the end of a job group
+     * or just the end of a job within a group to keep from sending encode done notification
+     * after the first pass in a two pass encode
+     */
+    HBJobGroup * theJobGroupCheck = [currentJob jobGroup];
+    if ((theJobGroupCheck == nil) || (theJobGroupCheck != fCurrentJobGroup))
     {
-        BOOL groups = [sender state] == NSOnState;
-        [[NSUserDefaults standardUserDefaults] setBool:groups forKey:@"QueueShowsJobsAsGroups"];
-
-        [self setShowsJobsAsGroups:groups];
+        /* we need to make sure that we are not at the beginning of a queue and also that the job hasn't
+         * been cancelled
+         */
+        if ([[fCurrentJob jobGroup] destinationPath] && [fCurrentJobGroup status] != HBStatusCanceled)
+        {
+            /* send encode messages to fHBController. User prefs are grokked there. */
+            [fHBController showGrowlDoneNotification: [[fCurrentJob jobGroup] destinationPath]];
+            [fHBController sendToMetaX: [[fCurrentJob jobGroup] destinationPath]];
+        }
     }
-    else if ([sender isMemberOfClass:[NSSegmentedControl class]])
-    {
-        BOOL groups = [sender selectedSegment] == 0;
-        [[NSUserDefaults standardUserDefaults] setBool:groups forKey:@"QueueShowsJobsAsGroups"];
+    [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]];
 
-        [self setShowsJobsAsGroups:groups];
+    // 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 if ([sender isMemberOfClass:[NSMatrix class]])
+    
+    else    // start a new job/pass in the same group
     {
-        BOOL groups = [sender selectedColumn] == 0;
-        [[NSUserDefaults standardUserDefaults] setBool:groups forKey:@"QueueShowsJobsAsGroups"];
-
-        [self setShowsJobsAsGroups:groups];
+        // Update the UI
+        [self updateCurrentJobDescription];
+        [self updateCurrentJobProgress];
     }
+
 }
 
 //------------------------------------------------------------------------------------
-// Toggles the Shows Detail setting.
+// 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.
 //------------------------------------------------------------------------------------
-- (IBAction)toggleShowsDetail: (id)sender
+- (void)libhbWillStop
 {
-    [self setShowsDetail:!fShowsDetail];
+    if (fCurrentJobGroup)
+        [fCurrentJobGroup setStatus: HBStatusCanceled];
 }
 
 //------------------------------------------------------------------------------------
-// Toggles the Shows Jobs As Groups setting.
+// Notifies HBQueueController that libhb's state has changed
 //------------------------------------------------------------------------------------
-- (IBAction)toggleShowsJobsAsGroups: (id)sender
+- (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;
+{
+    spacingWidth = [sender floatValue];
+    [fOutlineView setNeedsDisplay: YES];
+}
+- (IBAction)indentChanged: (id)sender
 {
-    [self setShowsJobsAsGroups:!fShowsJobsAsGroups];
+    [fOutlineView setIndentationPerLevel: [sender floatValue]];
+    [fOutlineView setNeedsDisplay: YES];
 }
+#endif
+
+#pragma mark -
 
 //------------------------------------------------------------------------------------
-// Toggles the processing of jobs on or off depending on the current state
+// Receives notification whenever an HBJobGroup's status is changed.
 //------------------------------------------------------------------------------------
-- (IBAction)toggleStartPause: (id)sender
+- (void) jobGroupStatusNotification:(NSNotification *)notification
 {
-    if (!fHandle) return;
-    
-    hb_state_t s;
-    hb_get_state2 (fHandle, &s);
-
-    if (s.state == HB_STATE_PAUSED)
-        hb_resume (fHandle);
-    else if ((s.state == HB_STATE_WORKING) || (s.state == HB_STATE_MUXING))
-        hb_pause (fHandle);
-    else
-    {
-        BOOL jobGroups = [[NSUserDefaults standardUserDefaults] boolForKey:@"QueueShowsJobsAsGroups"];
-        if (jobGroups)
-        {
-            if (hb_group_count(fHandle) > 0)
-                hb_start (fHandle);
-        }
-        else if (hb_count(fHandle) > 0)
-            hb_start (fHandle);
-    }    
+    [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
 
@@ -936,109 +1960,39 @@ static NSString*    HBShowGroupsToolbarIdentifier             = @"HBShowGroupsTo
     
     NSToolbarItem *toolbarItem = nil;
     
-    if ([itemIdentifier isEqual: HBStartPauseResumeToolbarIdentifier])
+    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/Pause"];
-               
-               // 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(toggleStartPause:)];
-       }
-    
-    else if ([itemIdentifier isEqual: HBShowDetailToolbarIdentifier])
-    {
-        toolbarItem = [[[NSToolbarItem alloc] initWithItemIdentifier: itemIdentifier] autorelease];
-               
+        
         // Set the text label to be displayed in the toolbar and customization palette 
-               [toolbarItem setLabel: @"Show Detail"];
-               [toolbarItem setPaletteLabel: @"Show Detail"];
-               
-               // Set up a reasonable tooltip, and image
-               [toolbarItem setToolTip: @"Show Detail"];
-               [toolbarItem setImage: [NSImage imageNamed: @"Info"]];
-               
-               // Tell the item what message to send when it is clicked 
-               [toolbarItem setTarget: self];
-               [toolbarItem setAction: @selector(toggleShowsDetail:)];
-       }
+        [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:)];
+    }
     
-    else if ([itemIdentifier isEqual: HBShowGroupsToolbarIdentifier])
+    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: @"View"];
-               [toolbarItem setPaletteLabel: @"View"];
-               
-               // Set up a reasonable tooltip, and image
-               [toolbarItem setToolTip: @"View"];
-//             [toolbarItem setImage: [NSImage imageNamed: @"Disc"]];
+        [toolbarItem setLabel: @"Pause"];
+        [toolbarItem setPaletteLabel: @"Pause/Resume"];
         
+        // Set up a reasonable tooltip, and image
+        [toolbarItem setToolTip: @"Pause Encoding"];
+        [toolbarItem setImage: [NSImage imageNamed: @"Pause"]];
         
-        NSButtonCell * buttonCell = [[[NSButtonCell alloc] initImageCell:nil] autorelease];
-        [buttonCell setBezelStyle:NSShadowlessSquareBezelStyle];//NSShadowlessSquareBezelStyle
-        [buttonCell setButtonType:NSToggleButton];
-        [buttonCell setBordered:NO];
-        [buttonCell setImagePosition:NSImageOnly];
-
-        NSMatrix * matrix = [[[NSMatrix alloc] initWithFrame:NSMakeRect(0,0,54,25)
-                mode:NSRadioModeMatrix
-                prototype:buttonCell
-                numberOfRows:1
-                numberOfColumns:2] autorelease];
-        [matrix setCellSize:NSMakeSize(27, 25)];
-        [matrix setIntercellSpacing:NSMakeSize(0, 0)];
-        [matrix selectCellAtRow:0 column:(fShowsJobsAsGroups ? 0 : 1)];
-
-        buttonCell = [matrix cellAtRow:0 column:0];
-        [buttonCell setTitle:@""];
-        [buttonCell setImage:[NSImage imageNamed: @"Encodes"]];
-        [buttonCell setAlternateImage:[NSImage imageNamed: @"EncodesPressed"]];
-        buttonCell = [matrix cellAtRow:0 column:1];
-        [buttonCell setTitle:@""];
-        [buttonCell setImage:[NSImage imageNamed: @"Passes"]];
-        [buttonCell setAlternateImage:[NSImage imageNamed: @"PassesPressed"]];
-        [toolbarItem setMinSize: [matrix frame].size];
-        [toolbarItem setMaxSize: [matrix frame].size];
-               [toolbarItem setView: matrix];
-
-/*
-        NSSegmentedControl * segControl = [[[NSSegmentedControl alloc] initWithFrame:NSMakeRect(0,0,20,20)] autorelease];
-        [[segControl cell] setControlSize:NSSmallControlSize];
-        [segControl setSegmentCount:2];
-        [segControl setLabel:@"Encodes" forSegment:0];
-        [segControl setLabel:@"Passes" forSegment:1];
-        [segControl setImage:[NSImage imageNamed:@"Delete"] forSegment:0];
-        [segControl setImage:[NSImage imageNamed:@"Delete"] forSegment:1];
-        [segControl setSelectedSegment: (fShowsJobsAsGroups ? 0 : 1)];
-        [segControl sizeToFit];
-        [toolbarItem setMinSize: [segControl frame].size];
-        [toolbarItem setMaxSize: [segControl frame].size];
-               [toolbarItem setView: segControl];
-*/
-
-/*
-        NSButton * button = [[[NSButton alloc] initWithFrame:NSMakeRect(0,0,20,20)] autorelease];
-        [button setButtonType:NSSwitchButton];
-        [button setTitle:@""];
-        [button setState: fShowsJobsAsGroups ? NSOnState : NSOffState];
-        [toolbarItem setMinSize: NSMakeSize(20,20)];
-        [toolbarItem setMaxSize: NSMakeSize(20,20)];
-               [toolbarItem setView: button];
-*/
-               
-               // Tell the item what message to send when it is clicked 
-               [toolbarItem setTarget: self];
-               [toolbarItem setAction: @selector(jobGroupsChanged:)];
-       }
+        // Tell the item what message to send when it is clicked 
+        [toolbarItem setTarget: self];
+        [toolbarItem setAction: @selector(togglePauseResume:)];
+    }
     
     return toolbarItem;
 }
@@ -1052,10 +2006,8 @@ static NSString*    HBShowGroupsToolbarIdentifier             = @"HBShowGroupsTo
     // toolbar by default.
     
     return [NSArray arrayWithObjects:
-        HBStartPauseResumeToolbarIdentifier,
-               NSToolbarSeparatorItemIdentifier,
-               HBShowGroupsToolbarIdentifier,
-        HBShowDetailToolbarIdentifier,
+        HBQueueStartCancelToolbarIdentifier,
+        HBQueuePauseResumeToolbarIdentifier,
         nil];
 }
 
@@ -1069,13 +2021,12 @@ static NSString*    HBShowGroupsToolbarIdentifier             = @"HBShowGroupsTo
     // separator. So, every allowed item must be explicitly listed.
 
     return [NSArray arrayWithObjects:
-        HBStartPauseResumeToolbarIdentifier,
-               HBShowGroupsToolbarIdentifier,
-        HBShowDetailToolbarIdentifier,
-               NSToolbarCustomizeToolbarItemIdentifier,
-               NSToolbarFlexibleSpaceItemIdentifier,
+        HBQueueStartCancelToolbarIdentifier,
+        HBQueuePauseResumeToolbarIdentifier,
+        NSToolbarCustomizeToolbarItemIdentifier,
+        NSToolbarFlexibleSpaceItemIdentifier,
         NSToolbarSpaceItemIdentifier,
-               NSToolbarSeparatorItemIdentifier,
+        NSToolbarSeparatorItemIdentifier,
         nil];
 }
 
@@ -1094,82 +2045,60 @@ static NSString*    HBShowGroupsToolbarIdentifier             = @"HBShowGroupsTo
     hb_state_t s;
     hb_get_state2 (fHandle, &s);
 
-    if ([[toolbarItem itemIdentifier] isEqual: HBStartPauseResumeToolbarIdentifier])
+    if ([[toolbarItem itemIdentifier] isEqual: HBQueueStartCancelToolbarIdentifier])
     {
-        if (s.state == HB_STATE_PAUSED)
-        {
-            enable = YES;
-            [toolbarItem setImage:[NSImage imageNamed: @"Play"]];
-                       [toolbarItem setLabel: @"Resume"];
-                       [toolbarItem setPaletteLabel: @"Resume"];
-                       [toolbarItem setToolTip: @"Resume Encoding"];
-       }
-        
-        else if ((s.state == HB_STATE_WORKING) || (s.state == HB_STATE_MUXING))
+        if ((s.state == HB_STATE_PAUSED) || (s.state == HB_STATE_WORKING) || (s.state == HB_STATE_MUXING))
         {
             enable = YES;
-            [toolbarItem setImage:[NSImage imageNamed: @"Pause"]];
-                       [toolbarItem setLabel: @"Pause"];
-                       [toolbarItem setPaletteLabel: @"Pause"];
-                       [toolbarItem setToolTip: @"Pause Encoding"];
+            [toolbarItem setImage:[NSImage imageNamed: @"Stop"]];
+            [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 setPaletteLabel: @"Start"];
-                       [toolbarItem setToolTip: @"Start Encoding"];
+            [toolbarItem setLabel: @"Start"];
+            [toolbarItem setToolTip: @"Start Encoding"];
         }
 
         else
         {
             enable = NO;
             [toolbarItem setImage:[NSImage imageNamed: @"Play"]];
-                       [toolbarItem setLabel: @"Start"];
-                       [toolbarItem setPaletteLabel: @"Start"];
-                       [toolbarItem setToolTip: @"Start Encoding"];
-        }
-       }
-    
-/* not used because HBShowGroupsToolbarIdentifier is now a custom view
-    else if ([[toolbarItem itemIdentifier] isEqual: HBShowGroupsToolbarIdentifier])
-    {
-        enable = hb_count(fHandle) > 0;
-        if (fShowsJobsAsGroups)
-        {
-            [toolbarItem setLabel: @"View Passes"];
-            [toolbarItem setPaletteLabel: @"View Passes"];
-            [toolbarItem setToolTip: @"Displays items in the queue as individual passes"];
-        }
-        else
-        {
-            [toolbarItem setLabel: @"View Encodes"];
-            [toolbarItem setPaletteLabel: @"View Encodes"];
-            [toolbarItem setToolTip: @"Displays items in the queue as encodes"];
+            [toolbarItem setLabel: @"Start"];
+            [toolbarItem setToolTip: @"Start Encoding"];
         }
     }
-*/
     
-    else if ([[toolbarItem itemIdentifier] isEqual: HBShowDetailToolbarIdentifier])
+    if ([[toolbarItem itemIdentifier] isEqual: HBQueuePauseResumeToolbarIdentifier])
     {
-        enable = hb_count(fHandle) > 0;
-        if (fShowsDetail)
+        if (s.state == HB_STATE_PAUSED)
+        {
+            enable = YES;
+            [toolbarItem setImage:[NSImage imageNamed: @"Play"]];
+            [toolbarItem setLabel: @"Resume"];
+            [toolbarItem setToolTip: @"Resume Encoding"];
+       }
+        
+        else if ((s.state == HB_STATE_WORKING) || (s.state == HB_STATE_MUXING))
         {
-            [toolbarItem setLabel: @"Hide Detail"];
-            [toolbarItem setPaletteLabel: @"Hide Detail"];
-            [toolbarItem setToolTip: @"Displays detailed information in the queue"];
+            enable = YES;
+            [toolbarItem setImage:[NSImage imageNamed: @"Pause"]];
+            [toolbarItem setLabel: @"Pause"];
+            [toolbarItem setToolTip: @"Pause Encoding"];
         }
         else
         {
-            [toolbarItem setLabel: @"Show Detail"];
-            [toolbarItem setPaletteLabel: @"Show Detail"];
-            [toolbarItem setToolTip: @"Displays detailed information in the queue"];
+            enable = NO;
+            [toolbarItem setImage:[NSImage imageNamed: @"Pause"]];
+            [toolbarItem setLabel: @"Pause"];
+            [toolbarItem setToolTip: @"Pause Encoding"];
         }
     }
-
-       return enable;
+    
+    return enable;
 }
 
 #pragma mark -
@@ -1185,11 +2114,29 @@ static NSString*    HBShowGroupsToolbarIdentifier             = @"HBShowGroupsTo
         [fQueueWindow center];
     [fQueueWindow setFrameAutosaveName: @"Queue"];
     [fQueueWindow setExcludedFromWindowsMenu:YES];
-    
+
+#if HB_QUEUE_DRAGGING
+    [fOutlineView registerForDraggedTypes: [NSArray arrayWithObject:HBQueuePboardType] ];
+    [fOutlineView setDraggingSourceOperationMask:NSDragOperationEvery forLocal:YES];
+    [fOutlineView setVerticalMotionCanBeginDrag: YES];
+#endif
+
+    // Don't allow autoresizing of main column, else the "delete" column will get
+    // pushed out of view.
+    [fOutlineView setAutoresizesOutlineColumn: NO];
+
+#if HB_OUTLINE_METRIC_CONTROLS
+    [fIndentation setHidden: NO];
+    [fSpacing setHidden: NO];
+    [fIndentation setIntValue:[fOutlineView indentationPerLevel]];  // debug
+    [fSpacing setIntValue:3];       // debug
+#endif
+
     // Show/hide UI elements
-    [self setShowsDetail:fShowsDetail];
-    [self setShowsJobsAsGroups:fShowsJobsAsGroups];
+    fCurrentJobPaneShown = YES;     // it's shown in the nib
     [self showCurrentJobPane:NO];
+
+    [self updateQueueCountField];
 }
 
 
@@ -1202,77 +2149,285 @@ static NSString*    HBShowGroupsToolbarIdentifier             = @"HBShowGroupsTo
 }
 
 #pragma mark -
-#pragma mark NSTableView delegate
 
-//------------------------------------------------------------------------------------
-// NSTableView delegate
-//------------------------------------------------------------------------------------
-- (int)numberOfRowsInTableView: (NSTableView *)aTableView
+- (void)moveObjectsInArray:(NSMutableArray *)array fromIndexes:(NSIndexSet *)indexSet toIndex:(unsigned)insertIndex
 {
-    BOOL jobGroups = [[NSUserDefaults standardUserDefaults] boolForKey:@"QueueShowsJobsAsGroups"];
-    if (jobGroups)
-        return hb_group_count(fHandle);
-    else
-        return hb_count(fHandle);
+    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];
+    }
 }
 
-//------------------------------------------------------------------------------------
-// NSTableView delegate
-//------------------------------------------------------------------------------------
-- (id)tableView: (NSTableView *)aTableView
-      objectValueForTableColumn: (NSTableColumn *)aTableColumn
-                            row: (int)rowIndex
+#pragma mark -
+#pragma mark NSOutlineView delegate
+
+- (id)outlineView:(NSOutlineView *)outlineView child:(int)index ofItem:(id)item
 {
-    if (!fHandle)
-        return @"";    // fatal error!
-        
-    hb_job_t * job;
+    if (item == nil)
+        return [fJobGroups objectAtIndex:index];
+    
+    // We are only one level deep, so we can't be asked about children
+    NSAssert (NO, @"HBQueueController outlineView:child:ofItem: can't handle nested items.");
+    return nil;
+}
+
+- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(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.
+    return YES;
+}
 
-    BOOL jobGroups = [[NSUserDefaults standardUserDefaults] boolForKey:@"QueueShowsJobsAsGroups"];
-    if (jobGroups)
-        job = hb_group(fHandle, rowIndex);
+- (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
+    // top-level items.
+    if (item == nil)
+        return [fJobGroups count];
     else
-        job = hb_job(fHandle, rowIndex);
+        return 0;
+}
+
+- (void)outlineViewItemDidCollapse:(NSNotification *)notification
+{
+    id item = [[notification userInfo] objectForKey:@"NSObject"];
+    int row = [fOutlineView rowForItem:item];
+    [fOutlineView noteHeightOfRowsWithIndexesChanged:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(row,1)]];
+}
 
-    if (!job)
-        return @"";    // fatal error!
+- (void)outlineViewItemDidExpand:(NSNotification *)notification
+{
+    id item = [[notification userInfo] objectForKey:@"NSObject"];
+    int row = [fOutlineView rowForItem:item];
+    [fOutlineView noteHeightOfRowsWithIndexesChanged:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(row,1)]];
+}
 
-    if ([[aTableColumn identifier] isEqualToString:@"desc"])
+- (float)outlineView:(NSOutlineView *)outlineView heightOfRowByItem:(id)item
+{
+    if ([outlineView isItemExpanded: item])
     {
-        BOOL highlighted = [aTableView isRowSelected:rowIndex] && [[aTableView window] isKeyWindow] && ([[aTableView window] firstResponder] == aTableView);
-        return [self attributedDescriptionForJob:job withDetail:fShowsDetail withHighlighting:highlighted];    
+        // Short-circuit here if in a live resize primarily to fix a bug but also to
+        // increase resposivness during a resize. There's a bug in NSTableView that
+        // causes row heights to get messed up if you try to change them during a live
+        // resize. So if in a live resize, simply return the previously calculated
+        // height. The row heights will get fixed up after the resize because we have
+        // implemented viewDidEndLiveResize to force all of them to be recalculated.
+        if ([outlineView inLiveResize] && [item lastDescriptionHeight] > 0)
+            return [item lastDescriptionHeight];
+        
+        float width = [[outlineView tableColumnWithIdentifier: @"desc"] width];
+        // 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];
+        return height;
     }
-    
-    else if ([[aTableColumn identifier] isEqualToString:@"delete"])
-        return @"";
-
-    else if ([[aTableColumn identifier] isEqualToString:@"icon"])
-        return fShowsJobsAsGroups ? [NSImage imageNamed:@"JobSmall"] : [NSImage imageNamed:@"JobPassSmall"];
+    else
+        return HB_ROW_HEIGHT_TITLE_ONLY;
+}
 
-    return @"";
+- (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 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 @"";
 }
 
-//------------------------------------------------------------------------------------
-// NSTableView delegate
-//------------------------------------------------------------------------------------
-- (void)tableView: (NSTableView *)aTableView
-        willDisplayCell: (id)aCell
-         forTableColumn: (NSTableColumn *)aTableColumn
-                    row: (int)rowIndex
+- (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item
 {
-    if ([[aTableColumn identifier] isEqualToString:@"delete"])
+    if ([[tableColumn identifier] isEqualToString:@"desc"])
+    {
+#if HB_OUTLINE_METRIC_CONTROLS
+        NSSize theSize = [cell imageSpacing];
+        theSize.width = spacingWidth;
+        [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:nil];
+    }
+    
+    else if ([[tableColumn identifier] isEqualToString:@"action"])
     {
-        BOOL highlighted = [aTableView isRowSelected:rowIndex] && [[aTableView window] isKeyWindow] && ([[aTableView window] firstResponder] == aTableView);
-        if (highlighted)
+        [cell setEnabled: YES];
+        BOOL highlighted = [outlineView isRowSelected:[outlineView rowForItem: item]] && [[outlineView window] isKeyWindow] && ([[outlineView window] firstResponder] == outlineView);
+        if ([(HBJobGroup*)item status] == HBStatusCompleted)
         {
-            [aCell setImage:[NSImage imageNamed:@"DeleteHighlight"]];
-            [aCell 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
         {
-            [aCell 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"]];
         }
     }
 }
 
+- (void)outlineView:(NSOutlineView *)outlineView willDisplayOutlineCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item
+{
+    // By default, the discolsure image gets centered vertically in the cell. We want
+    // always at the top.
+    if ([outlineView isItemExpanded: item])
+        [cell setImagePosition: NSImageAbove];
+    else
+        [cell setImagePosition: NSImageOnly];
+}
+
+#pragma mark -
+#pragma mark NSOutlineView delegate (dragging related)
+
+//------------------------------------------------------------------------------------
+// NSTableView delegate
+//------------------------------------------------------------------------------------
+
+#if HB_QUEUE_DRAGGING
+- (BOOL)outlineView:(NSOutlineView *)outlineView writeItems:(NSArray *)items toPasteboard:(NSPasteboard *)pboard
+{
+       // 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];
+
+    // the actual data doesn't matter since DragDropSimplePboardType drags aren't recognized by anyone but us!.
+    [pboard setData:[NSData data] forType:HBQueuePboardType]; 
+
+    return YES;
+}
+#endif
+
+#if HB_QUEUE_DRAGGING
+- (NSDragOperation)outlineView:(NSOutlineView *)outlineView validateDrop:(id <NSDraggingInfo>)info proposedItem:(id)item proposedChildIndex:(int)index
+{
+       // 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
+
+#if HB_QUEUE_DRAGGING
+- (BOOL)outlineView:(NSOutlineView *)outlineView acceptDrop:(id <NSDraggingInfo>)info item:(id)item childIndex:(int)index
+{
+    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