/* $Id: Controller.mm,v 1.10 2003/10/13 23:09:56 titer Exp $
This file is part of the HandBrake source code.
Homepage: .
It may be used under the terms of the GNU General Public License. */
#include
#include
#include
#include
#include
#include "Controller.h"
#include "Manager.h"
@implementation HBController
- (void) applicationDidFinishLaunching: (NSNotification *) notification
{
/* Init libhb */
fManager = new HBManager( true );
/* Update the GUI every 1/10 sec */
fDie = false;
[NSTimer scheduledTimerWithTimeInterval: 0.1
target: self selector: @selector( UpdateIntf: )
userInfo: nil repeats: YES];
}
- (NSApplicationTerminateReply) applicationShouldTerminate:
(NSApplication *) app
{
/* Clean up */
fDie = true;
delete fManager;
return NSTerminateNow;
}
- (void) awakeFromNib
{
[fDVDPopUp removeAllItems];
[fScanProgress setStyle: NSProgressIndicatorSpinningStyle];
[fScanProgress setDisplayedWhenStopped: NO];
[fAudioBitratePopUp removeAllItems];
[fAudioBitratePopUp addItemWithTitle: @"32"];
[fAudioBitratePopUp addItemWithTitle: @"64"];
[fAudioBitratePopUp addItemWithTitle: @"96"];
[fAudioBitratePopUp addItemWithTitle: @"128"];
[fAudioBitratePopUp addItemWithTitle: @"160"];
[fAudioBitratePopUp addItemWithTitle: @"192"];
[fAudioBitratePopUp addItemWithTitle: @"224"];
[fAudioBitratePopUp addItemWithTitle: @"256"];
[fAudioBitratePopUp addItemWithTitle: @"288"];
[fAudioBitratePopUp addItemWithTitle: @"320"];
[fAudioBitratePopUp selectItemWithTitle: @"128"];
char string[1024]; memset( string, 0, 1024 );
snprintf( string, 1024, "%s/Desktop/Movie.avi", getenv( "HOME" ) );
[fFileField setStringValue: [NSString stringWithCString: string]];
/* Show the scan view */
[fWindow setContentSize: [fScanView frame].size];
[fWindow setContentView: fScanView];
[fWindow center];
/* Detect DVD drives */
fLastDVDDetection = GetDate();
[self DetectDrives];
[self ScanMatrixChanged: self];
}
- (BOOL) windowShouldClose: (id) sender
{
/* Stop the application when the user closes the window */
[NSApp terminate: self];
return YES;
}
- (IBAction) BrowseDVD: (id) sender
{
/* Open a panel to let the user choose and update the text field */
NSOpenPanel * panel = [NSOpenPanel openPanel];
[panel setAllowsMultipleSelection: NO];
[panel setCanChooseFiles: NO];
[panel setCanChooseDirectories: YES ];
[panel beginSheetForDirectory: nil file: nil types: nil
modalForWindow: fWindow modalDelegate: self
didEndSelector: @selector( BrowseDVDDone:returnCode:contextInfo: )
contextInfo: nil];
}
- (void) BrowseDVDDone: (NSOpenPanel *) sheet
returnCode: (int) returnCode contextInfo: (void *) contextInfo
{
if( returnCode == NSOKButton )
{
[fDVDFolderField setStringValue:
[[sheet filenames] objectAtIndex: 0]];
}
}
- (IBAction) VideoMatrixChanged: (id) sender;
{
if( ![fVideoMatrix selectedRow] )
{
[fCustomBitrateField setEnabled: YES];
[fTargetSizeField setEnabled: NO];
}
else
{
[fCustomBitrateField setEnabled: NO];
[fTargetSizeField setEnabled: YES];
[fTargetSizeField UpdateBitrate];
}
}
- (IBAction) BrowseFile: (id) sender
{
/* Open a panel to let the user choose and update the text field */
NSSavePanel * panel = [NSSavePanel savePanel];
[panel beginSheetForDirectory: nil file: nil
modalForWindow: fWindow modalDelegate: self
didEndSelector: @selector( BrowseFileDone:returnCode:contextInfo: )
contextInfo: nil];
}
- (void) BrowseFileDone: (NSSavePanel *) sheet
returnCode: (int) returnCode contextInfo: (void *) contextInfo
{
if( returnCode == NSOKButton )
{
[fFileField setStringValue: [sheet filename]];
}
}
- (IBAction) Scan: (id) sender
{
/* Ask the manager to start scanning the specified volume */
if( ![fScanMatrix selectedRow] )
{
/* DVD drive */
fManager->ScanVolumes( (char*) [[fDVDPopUp titleOfSelectedItem]
cString] );
}
else
{
/* DVD folder */
fManager->ScanVolumes( (char*) [[fDVDFolderField stringValue]
cString] );
}
}
- (IBAction) ShowPicturePanel: (id) sender
{
HBTitle * title = (HBTitle*)
fTitleList->ItemAt( [fTitlePopUp indexOfSelectedItem] );
[fPictureGLView SetManager: fManager];
[fPictureGLView SetTitle: title];
fPicture = 0;
[fPictureGLView ShowPicture: fPicture];
[fWidthStepper setValueWraps: NO];
[fWidthStepper setIncrement: 16];
[fWidthStepper setMinValue: 16];
[fWidthStepper setMaxValue: title->fOutWidthMax];
[fWidthStepper setIntValue: title->fOutWidth];
[fWidthField setIntValue: title->fOutWidth];
[fTopStepper setValueWraps: NO];
[fTopStepper setIncrement: 2];
[fTopStepper setMinValue: 0];
[fTopStepper setMaxValue: title->fInHeight / 4];
[fTopStepper setIntValue: title->fTopCrop];
[fTopField setIntValue: title->fTopCrop];
[fBottomStepper setValueWraps: NO];
[fBottomStepper setIncrement: 2];
[fBottomStepper setMinValue: 0];
[fBottomStepper setMaxValue: title->fInHeight / 4];
[fBottomStepper setIntValue: title->fBottomCrop];
[fBottomField setIntValue: title->fBottomCrop];
[fLeftStepper setValueWraps: NO];
[fLeftStepper setIncrement: 2];
[fLeftStepper setMinValue: 0];
[fLeftStepper setMaxValue: title->fInWidth / 4];
[fLeftStepper setIntValue: title->fLeftCrop];
[fLeftField setIntValue: title->fLeftCrop];
[fRightStepper setValueWraps: NO];
[fRightStepper setIncrement: 2];
[fRightStepper setMinValue: 0];
[fRightStepper setMaxValue: title->fInWidth / 4];
[fRightStepper setIntValue: title->fRightCrop];
[fRightField setIntValue: title->fRightCrop];
char string[1024]; memset( string, 0, 1024 );
sprintf( string, "Final size: %dx%d",
title->fOutWidth, title->fOutHeight );
[fInfoField setStringValue: [NSString stringWithCString: string]];
/* Resize the panel */
NSSize newSize;
/* XXX */
newSize.width = 762 /*fPicturePanelSize.width*/ +
title->fOutWidthMax - 720;
newSize.height = 740 /*fPicturePanelSize.height*/ +
title->fOutHeightMax - 576;
[fPicturePanel setContentSize: newSize];
[NSApp beginSheet: fPicturePanel modalForWindow: fWindow
modalDelegate: nil didEndSelector: nil contextInfo: nil];
[NSApp runModalForWindow: fPicturePanel];
[NSApp endSheet: fPicturePanel];
[fPicturePanel orderOut: self];
}
- (IBAction) ClosePanel: (id) sender
{
[NSApp stopModal];
}
- (IBAction) Rip: (id) sender
{
/* Rip or Cancel ? */
if( [[fRipButton title] compare: @"Cancel" ] == NSOrderedSame )
{
[self Cancel: self];
return;
}
if( [fCustomBitrateField intValue] < 256 )
{
NSBeginCriticalAlertSheet( @"Invalid video bitrate", @"Ooops",
nil, nil, fWindow, self, nil, nil, nil,
@"Video bitrate is too low !" );
return;
}
if( [fCustomBitrateField intValue] > 8192 )
{
NSBeginCriticalAlertSheet( @"Invalid video bitrate", @"Ooops",
nil, nil, fWindow, self, nil, nil, nil,
@"Video bitrate is too high !" );
return;
}
if( [fLanguagePopUp indexOfSelectedItem] ==
[fSecondaryLanguagePopUp indexOfSelectedItem] )
{
NSBeginCriticalAlertSheet( @"Invalid secondary language",
@"Ooops", nil, nil, fWindow, self, nil, nil, nil,
@"Do you _really_ want to encode the same audio track twice?" );
return;
}
FILE * file;
if( ( file = fopen( [[fFileField stringValue] cString], "r" ) ) )
{
fclose( file );
NSBeginCriticalAlertSheet( @"File already exists",
@"Nooo", @"Yes, go ahead!", nil, fWindow, self,
@selector( OverwriteAlertDone:returnCode:contextInfo: ),
nil, nil,
[NSString stringWithFormat: @"Do you want to overwrite %s ?",
[[fFileField stringValue] cString]] );
return;
}
[self _Rip];
}
- (void) OverwriteAlertDone: (NSWindow *) sheet
returnCode: (int) returnCode contextInfo: (void *) contextInfo
{
if( returnCode == NSAlertAlternateReturn )
{
[self _Rip];
}
}
- (void) _Rip
{
/* Get the specified title & audio track(s) */
HBTitle * title = (HBTitle*)
fTitleList->ItemAt( [fTitlePopUp indexOfSelectedItem] );
HBAudio * audio1 = (HBAudio*)
title->fAudioList->ItemAt( [fLanguagePopUp indexOfSelectedItem] );
HBAudio * audio2 = (HBAudio*)
title->fAudioList->ItemAt( [fSecondaryLanguagePopUp
indexOfSelectedItem] );
/* Use user settings */
title->fBitrate = [fCustomBitrateField intValue];
title->fTwoPass = ( [fTwoPassCheck state] == NSOnState );
audio1->fOutBitrate = [[fAudioBitratePopUp titleOfSelectedItem]
intValue];
if( audio2 )
{
audio2->fOutBitrate =
[[fAudioBitratePopUp titleOfSelectedItem] intValue];
}
/* Let libhb do the job */
fManager->StartRip( title, audio1, audio2,
(char*) [[fFileField stringValue] cString] );
}
- (IBAction) Cancel: (id) sender
{
fManager->StopRip();
}
- (IBAction) Suspend: (id) sender
{
if( [[fSuspendButton title] compare: @"Resume" ] == NSOrderedSame )
{
[self Resume: self];
return;
}
fManager->SuspendRip();
}
- (IBAction) Resume: (id) sender
{
fManager->ResumeRip();
}
- (IBAction) PreviousPicture: (id) sender
{
if( fPicture > 0 )
{
fPicture--;
[fPictureGLView ShowPicture: fPicture];
}
}
- (IBAction) NextPicture: (id) sender
{
if( fPicture < 9 )
{
fPicture++;
[fPictureGLView ShowPicture: fPicture];
}
}
- (IBAction) UpdatePicture: (id) sender
{
HBTitle * title = (HBTitle*)
fTitleList->ItemAt( [fTitlePopUp indexOfSelectedItem] );
title->fOutWidth = [fWidthStepper intValue];
title->fDeinterlace = ( [fDeinterlaceCheck state] == NSOnState );
title->fTopCrop = [fTopStepper intValue];
title->fBottomCrop = [fBottomStepper intValue];
title->fLeftCrop = [fLeftStepper intValue];
title->fRightCrop = [fRightStepper intValue];
[fPictureGLView ShowPicture: fPicture];
[fWidthStepper setIntValue: title->fOutWidth];
[fTopStepper setIntValue: title->fTopCrop];
[fBottomStepper setIntValue: title->fBottomCrop];
[fLeftStepper setIntValue: title->fLeftCrop];
[fRightStepper setIntValue: title->fRightCrop];
[fWidthField setIntValue: [fWidthStepper intValue]];
[fTopField setIntValue: [fTopStepper intValue]];
[fBottomField setIntValue: [fBottomStepper intValue]];
[fLeftField setIntValue: [fLeftStepper intValue]];
[fRightField setIntValue: [fRightStepper intValue]];
char string[1024]; memset( string, 0, 1024 );
sprintf( string, "Final size: %dx%d",
title->fOutWidth, title->fOutHeight );
[fInfoField setStringValue: [NSString stringWithCString: string]];
}
- (void) UpdateIntf: (NSTimer *) timer
{
if( fDie )
{
[timer invalidate];
return;
}
/* Update DVD popup */
if( [fWindow contentView] == fScanView &&
GetDate() > fLastDVDDetection + 2000000 )
{
[self DetectDrives];
fLastDVDDetection = GetDate();
}
/* Ask libhb about what's happening now */
if( fManager->NeedUpdate() )
{
HBStatus status = fManager->GetStatus();
switch( status.fMode )
{
case HB_MODE_NEED_VOLUME:
break;
case HB_MODE_SCANNING:
{
[fScanMatrix setEnabled: NO];
[fDVDPopUp setEnabled: NO];
[fDVDFolderField setEnabled: NO];
[fScanBrowseButton setEnabled: NO];
[fScanProgress startAnimation: self];
[fScanButton setEnabled: NO];
char string[1024]; memset( string, 0, 1024 );
if( status.fScannedTitle )
{
sprintf( string, "Scanning %s, title %d...",
status.fScannedVolume,
status.fScannedTitle );
}
else
{
sprintf( string, "Opening %s...",
status.fScannedVolume );
}
[fScanStatusField setStringValue:
[NSString stringWithCString: string]];
break;
}
case HB_MODE_INVALID_VOLUME:
{
[fScanMatrix setEnabled: YES];
[self ScanMatrixChanged: self];
[fScanProgress stopAnimation: self];
[fScanButton setEnabled: YES];
[fScanStatusField setStringValue:
@"Invalid volume, try again" ];
break;
}
case HB_MODE_READY_TO_RIP:
{
fTitleList = status.fTitleList;
/* Show a temporary empty view while the window
resizing animation */
[fWindow setContentView: fTempView ];
/* Actually resize it */
NSRect newFrame;
newFrame = [NSWindow contentRectForFrameRect: [fWindow frame]
styleMask: [fWindow styleMask]];
newFrame.origin.y += newFrame.size.height -
[fRipView frame].size.height;
newFrame.size.height = [fRipView frame].size.height;
newFrame.size.width = [fRipView frame].size.width;
newFrame = [NSWindow frameRectForContentRect: newFrame
styleMask: [fWindow styleMask]];
[fWindow setFrame: newFrame display: YES animate: YES];
/* Show the new GUI */
[fWindow setContentView: fRipView ];
[fSuspendButton setEnabled: NO];
[fTitlePopUp removeAllItems];
HBTitle * title;
for( uint32_t i = 0; i < fTitleList->CountItems(); i++ )
{
title = (HBTitle*) fTitleList->ItemAt( i );
char string[1024]; memset( string, 0, 1024 );
sprintf( string, "%d (%02lld:%02lld:%02lld)",
title->fIndex, title->fLength / 3600,
( title->fLength % 3600 ) / 60,
title->fLength % 60 );
[[fTitlePopUp menu] addItemWithTitle:
[NSString stringWithCString: string]
action: nil keyEquivalent: @""];
}
[self TitlePopUpChanged: self];
break;
}
case HB_MODE_ENCODING:
{
[fTitlePopUp setEnabled: NO];
[fVideoCodecPopUp setEnabled: NO];
[fVideoMatrix setEnabled: NO];
[fCustomBitrateField setEnabled: NO];
[fTargetSizeField setEnabled: NO];
[fTwoPassCheck setEnabled: NO];
[fCropButton setEnabled: NO];
[fLanguagePopUp setEnabled: NO];
[fSecondaryLanguagePopUp setEnabled: NO];
[fAudioCodecPopUp setEnabled: NO];
[fAudioBitratePopUp setEnabled: NO];
[fFileFormatPopUp setEnabled: NO];
[fFileBrowseButton setEnabled: NO];
[fSuspendButton setEnabled: YES];
[fSuspendButton setTitle: @"Suspend"];
[fRipButton setTitle: @"Cancel"];
if( !status.fPosition )
{
[fRipStatusField setStringValue: @"Starting..."];
[fRipProgress setIndeterminate: YES];
[fRipProgress startAnimation: self];;
}
else
{
char string[1024];
memset( string, 0, 1024 );
sprintf( string, "Encoding: %.2f %%",
100 * status.fPosition );
[fRipStatusField setStringValue:
[NSString stringWithCString: string]];
memset( string, 0, 1024 );
sprintf( string,
"Speed: %.2f fps (%02d:%02d:%02d remaining)",
status.fFrameRate,
status.fRemainingTime / 3600,
( status.fRemainingTime % 3600 ) / 60,
status.fRemainingTime % 60 );
[fRipInfoField setStringValue:
[NSString stringWithCString: string]];
[fRipProgress setIndeterminate: NO];
[fRipProgress setDoubleValue: 100 * status.fPosition];
}
break;
}
case HB_MODE_SUSPENDED:
{
char string[1024]; memset( string, 0, 1024 );
sprintf( string, "Encoding: %.2f %% (PAUSED)",
100 * status.fPosition ) ;
[fRipStatusField setStringValue:
[NSString stringWithCString: string]];
[fRipInfoField setStringValue: @""];
[fRipProgress setDoubleValue: 100 * status.fPosition];
[fSuspendButton setTitle: @"Resume"];
break;
}
case HB_MODE_STOPPING:
[fRipStatusField setStringValue: @"Stopping..."];
[fRipInfoField setStringValue: @""];
[fRipProgress setIndeterminate: YES];
[fRipProgress startAnimation: self];;
break;
case HB_MODE_DONE:
case HB_MODE_CANCELED:
case HB_MODE_ERROR:
[fRipProgress setIndeterminate: NO];
if( status.fMode == HB_MODE_DONE )
{
[fRipProgress setDoubleValue: 100];
[fRipStatusField setStringValue: @"Done." ];
NSBeep();
[NSApp requestUserAttention: NSInformationalRequest];
[NSApp beginSheet: fDonePanel
modalForWindow: fWindow modalDelegate: nil
didEndSelector: nil contextInfo: nil];
[NSApp runModalForWindow: fDonePanel];
[NSApp endSheet: fDonePanel];
[fDonePanel orderOut: self];
}
else if( status.fMode == HB_MODE_CANCELED )
{
[fRipProgress setDoubleValue: 0];
[fRipStatusField setStringValue: @"Canceled." ];
}
else
{
[fRipProgress setDoubleValue: 0];
switch( status.fError )
{
case HB_ERROR_A52_SYNC:
[fRipStatusField setStringValue:
@"An error occured (corrupted AC3 data)." ];
break;
case HB_ERROR_AVI_WRITE:
[fRipStatusField setStringValue:
@"An error occured (could not write to file)." ];
break;
case HB_ERROR_DVD_OPEN:
[fRipStatusField setStringValue:
@"An error occured (could not open device)." ];
break;
case HB_ERROR_DVD_READ:
[fRipStatusField setStringValue:
@"An error occured (DVD read failed)." ];
break;
case HB_ERROR_MP3_INIT:
[fRipStatusField setStringValue:
@"An error occured (could not init MP3 encoder)." ];
break;
case HB_ERROR_MP3_ENCODE:
[fRipStatusField setStringValue:
@"An error occured (MP3 encoder failed)." ];
break;
case HB_ERROR_MPEG4_INIT:
[fRipStatusField setStringValue:
@"An error occured (could not init MPEG4 encoder)." ];
break;
}
}
[fRipInfoField setStringValue: @""];
[fTitlePopUp setEnabled: YES];
[fVideoCodecPopUp setEnabled: YES];
[fVideoMatrix setEnabled: YES];
[fTwoPassCheck setEnabled: YES];
[fCropButton setEnabled: YES];
[fLanguagePopUp setEnabled: YES];
[fSecondaryLanguagePopUp setEnabled: YES];
[fAudioCodecPopUp setEnabled: YES];
[fAudioBitratePopUp setEnabled: YES];
[fFileFormatPopUp setEnabled: YES];
[fFileBrowseButton setEnabled: YES];
[fSuspendButton setEnabled: NO];
[fSuspendButton setTitle: @"Suspend"];
[fRipButton setTitle: @"Rip"];
[self VideoMatrixChanged: self];
/* Warn the finder to update itself */
[[NSWorkspace sharedWorkspace] noteFileSystemChanged:
[fFileField stringValue]];
break;
default:
break;
}
}
}
- (void) DetectDrives
{
/* Scan DVD drives (stolen from VLC) */
io_object_t next_media;
mach_port_t master_port;
kern_return_t kern_result;
io_iterator_t media_iterator;
CFMutableDictionaryRef classes_to_match;
kern_result = IOMasterPort( MACH_PORT_NULL, &master_port );
if( kern_result != KERN_SUCCESS )
{
return;
}
classes_to_match = IOServiceMatching( kIODVDMediaClass );
if( classes_to_match == NULL )
{
return;
}
CFDictionarySetValue( classes_to_match, CFSTR( kIOMediaEjectable ),
kCFBooleanTrue );
kern_result =
IOServiceGetMatchingServices( master_port, classes_to_match,
&media_iterator );
if( kern_result != KERN_SUCCESS )
{
return;
}
NSMutableArray * drivesList;
drivesList = [NSMutableArray arrayWithCapacity: 1];
next_media = IOIteratorNext( media_iterator );
if( next_media != NULL )
{
char psz_buf[0x32];
size_t dev_path_length;
CFTypeRef str_bsd_path;
do
{
str_bsd_path =
IORegistryEntryCreateCFProperty( next_media,
CFSTR( kIOBSDName ),
kCFAllocatorDefault,
0 );
if( str_bsd_path == NULL )
{
IOObjectRelease( next_media );
continue;
}
snprintf( psz_buf, sizeof(psz_buf), "%s%c", _PATH_DEV, 'r' );
dev_path_length = strlen( psz_buf );
if( CFStringGetCString( (CFStringRef) str_bsd_path,
(char*)&psz_buf + dev_path_length,
sizeof(psz_buf) - dev_path_length,
kCFStringEncodingASCII ) )
{
[drivesList addObject:
[NSString stringWithCString: psz_buf]];
}
CFRelease( str_bsd_path );
IOObjectRelease( next_media );
} while( ( next_media = IOIteratorNext( media_iterator ) ) != NULL );
}
IOObjectRelease( media_iterator );
/* Refresh only if a change occured */
if( [drivesList count] == (unsigned) [fDVDPopUp numberOfItems] )
{
bool isSame = true;
for( unsigned i = 0; i < [drivesList count]; i++ )
{
if( ![[drivesList objectAtIndex: i] isEqualToString:
[fDVDPopUp itemTitleAtIndex: i]] )
{
isSame = false;
break;
}
}
if( isSame )
{
return;
}
}
[fDVDPopUp removeAllItems];
for( unsigned i = 0; i < [drivesList count]; i++ )
{
[[fDVDPopUp menu] addItemWithTitle:
[drivesList objectAtIndex: i] action: nil
keyEquivalent: @""];
}
[self ScanMatrixChanged: self];
}
- (IBAction) ScanMatrixChanged: (id) sender
{
if( ![fScanMatrix selectedRow] )
{
[fDVDPopUp setEnabled: YES];
[fDVDFolderField setEnabled: NO];
[fScanBrowseButton setEnabled: NO];
[fScanButton setEnabled: ( [fDVDPopUp selectedItem] != nil )];
}
else
{
[fDVDPopUp setEnabled: NO];
[fDVDFolderField setEnabled: YES];
[fScanBrowseButton setEnabled: YES];
[fScanButton setEnabled: YES];
}
}
- (IBAction) TitlePopUpChanged: (id) sender
{
HBTitle * title = (HBTitle*)
fTitleList->ItemAt( [fTitlePopUp indexOfSelectedItem] );
[fLanguagePopUp removeAllItems];
[fSecondaryLanguagePopUp removeAllItems];
HBAudio * audio;
for( uint32_t i = 0; i < title->fAudioList->CountItems(); i++ )
{
audio = (HBAudio*) title->fAudioList->ItemAt( i );
/* We cannot use NSPopUpButton's addItemWithTitle because
it checks for duplicate entries */
[[fLanguagePopUp menu] addItemWithTitle:
[NSString stringWithCString: audio->fDescription]
action: nil keyEquivalent: @""];
[[fSecondaryLanguagePopUp menu] addItemWithTitle:
[NSString stringWithCString: audio->fDescription]
action: nil keyEquivalent: @""];
}
[fSecondaryLanguagePopUp addItemWithTitle: @"None"];
[fSecondaryLanguagePopUp selectItemWithTitle: @"None"];
[fSecondaryLanguagePopUp setEnabled:
( title->fAudioList->CountItems() > 1 )];
[fTargetSizeField SetHBTitle: title];
if( [fVideoMatrix selectedRow] )
{
[fTargetSizeField UpdateBitrate];
}
}
- (IBAction) AudioPopUpChanged: (id) sender
{
if( [fVideoMatrix selectedRow] )
{
[fTargetSizeField UpdateBitrate];
}
}
@end