2015-10-15 19:35:03 +02:00
/ * HBChapterTitlesController . m $
2014-07-15 18:30:51 +00:00
This file is part of the HandBrake source code .
Homepage : < http : // handbrake . fr / > .
It may be used under the terms of the GNU General Public License . * /
2019-11-01 21:34:54 +01:00
2014-07-15 18:30:51 +00:00
# import "HBChapterTitlesController.h"
2019-07-29 07:34:38 +02:00
# import "HBPreferencesKeys.h"
2017-11-12 10:56:38 +01:00
@ import HandBrakeKit ;
2014-07-15 18:30:51 +00:00
2016-08-22 14:38:01 +02:00
@ interface NSArray ( HBCSVAdditions )
+ ( nullable NSArray < NSArray < NSString * > * > * ) HB_arrayWithContentsOfCSVURL : ( NSURL * ) url ;
@ end
@ implementation NSArray ( HBCSVAdditions )
// CSV parsing examples
// CSV Record :
// one , two , three
// Fields :
// < one >
// < two >
// < three >
// CSV Record :
// one , two , three
// Fields :
// < one >
// < two >
// < three >
// CSV Record :
// one , "2,345" , three
// Fields :
// < one >
// < 2 , 345 >
// < three >
// CSV record :
// one , "John said, " "Hello there." "" , three
// Explanation : inside a quoted field , two double quotes in a row count
// as an escaped double quote in the field data .
// Fields :
// < one >
// < John said , "Hello there." >
// < three >
2019-08-12 10:19:00 +02:00
+ ( nullable NSArray < NSArray < NSString * > * > * ) HB_arrayWithContentsOfCSVURL : ( NSURL * ) url
2016-08-22 14:38:01 +02:00
{
NSString * str = [ [ NSString alloc ] initWithContentsOfURL : url encoding : NSUTF8StringEncoding error : NULL ] ;
if ( str = = nil )
{
return nil ;
}
NSMutableString * csvString = [ str mutableCopy ] ;
[ csvString replaceOccurrencesOfString : @ "\r\n" withString : @ "\n" options : NSLiteralSearch range : NSMakeRange ( 0 , csvString . length ) ] ;
[ csvString replaceOccurrencesOfString : @ "\r" withString : @ "\n" options : NSLiteralSearch range : NSMakeRange ( 0 , csvString . length ) ] ;
if ( ! csvString )
{
return 0 ;
}
if ( [ csvString characterAtIndex : 0 ] = = 0 xFEFF )
{
[ csvString deleteCharactersInRange : NSMakeRange ( 0 , 1 ) ] ;
}
if ( [ csvString characterAtIndex : [ csvString length ] -1 ] ! = ' \ n ' )
{
[ csvString appendFormat : @ "%c" , ' \ n ' ] ;
}
NSScanner * sc = [ NSScanner scannerWithString : csvString ] ;
sc . charactersToBeSkipped = nil ;
NSMutableArray * csvArray = [ NSMutableArray array ] ;
[ csvArray addObject : [ NSMutableArray array ] ] ;
NSCharacterSet * commaNewlineCS = [ NSCharacterSet characterSetWithCharactersInString : @ ",\n" ] ;
while ( sc . scanLocation < csvString . length )
{
if ( [ sc scanString : @ "\" " intoString : NULL ] )
{
// Quoted field
NSMutableString * field = [ NSMutableString string ] ;
BOOL done = NO ;
NSString * quotedString ;
// Scan until we get to the end double quote or the EOF .
while ( ! done && sc . scanLocation < csvString . length )
{
if ( [ sc scanUpToString : @ "\" " intoString : & quotedString ] )
{
[ field appendString : quotedString ] ;
}
if ( [ sc scanString : @ "\" \ "" intoString : NULL ] )
{
// Escaped double quote inside the quoted string .
[ field appendString : @ "\" " ] ;
}
else
{
done = YES ;
}
}
if ( sc . scanLocation < csvString . length )
{
+ + sc . scanLocation ;
BOOL nextIsNewline = [ sc scanString : @ "\n" intoString : NULL ] ;
BOOL nextIsComma = NO ;
if ( ! nextIsNewline )
{
nextIsComma = [ sc scanString : @ "," intoString : NULL ] ;
}
if ( nextIsNewline || nextIsComma )
{
[ [ csvArray lastObject ] addObject : field ] ;
if ( nextIsNewline && sc . scanLocation < csvString . length )
{
[ csvArray addObject : [ NSMutableArray array ] ] ;
}
}
else
{
// Quoted fields must be immediately followed by a comma or newline .
return nil ;
}
}
else
{
// No close quote found before EOF , so file is invalid CSV .
return nil ;
}
}
else
{
NSString * field ;
[ sc scanUpToCharactersFromSet : commaNewlineCS intoString : & field ] ;
BOOL nextIsNewline = [ sc scanString : @ "\n" intoString : NULL ] ;
BOOL nextIsComma = NO ;
if ( ! nextIsNewline )
{
nextIsComma = [ sc scanString : @ "," intoString : NULL ] ;
}
if ( nextIsNewline || nextIsComma )
{
[ [ csvArray lastObject ] addObject : field ] ;
if ( nextIsNewline && sc . scanLocation < csvString . length )
{
[ csvArray addObject : [ NSMutableArray array ] ] ;
}
}
}
}
return csvArray ;
}
@ end
2014-07-15 18:30:51 +00:00
@ interface HBChapterTitlesController ( ) < NSTableViewDataSource , NSTableViewDelegate >
2019-08-12 10:19:00 +02:00
@ property ( nonatomic , weak ) IBOutlet NSTableView * table ;
2016-08-22 14:38:01 +02:00
@ property ( nonatomic , readwrite , strong ) NSArray < HBChapter * > * chapterTitles ;
2014-12-22 17:12:16 +00:00
2014-07-15 18:30:51 +00:00
@ end
@ implementation HBChapterTitlesController
- ( instancetype ) init
{
self = [ super initWithNibName : @ "ChaptersTitles" bundle : nil ] ;
if ( self )
{
2014-12-22 17:12:16 +00:00
_chapterTitles = [ [ NSMutableArray alloc ] init ] ;
2014-07-15 18:30:51 +00:00
}
return self ;
}
2014-12-21 06:34:10 +00:00
- ( void ) setJob : ( HBJob * ) job
2014-07-15 18:30:51 +00:00
{
2014-12-22 17:12:16 +00:00
_job = job ;
self . chapterTitles = job . chapterTitles ;
2014-07-15 18:30:51 +00:00
}
2018-07-10 20:06:09 +02:00
- ( void ) viewDidLoad
2017-11-12 10:56:38 +01:00
{
2018-07-10 20:06:09 +02:00
[ super viewDidLoad ] ;
2017-11-12 10:56:38 +01:00
self . table . doubleAction = @ selector ( doubleClickAction : ) ;
}
2015-10-15 19:35:03 +02:00
/ * *
* Method to edit the next chapter when the user presses Return .
2015-10-20 18:55:08 +02:00
* We queue the action on the runloop to avoid interfering
2015-10-15 19:35:03 +02:00
* with the chain of events that handles the edit .
* /
- ( void ) controlTextDidEndEditing : ( NSNotification * ) notification
2014-07-15 18:30:51 +00:00
{
2015-10-20 18:55:08 +02:00
NSTableView * chapterTable = self . table ;
2017-12-11 14:58:15 +01:00
NSInteger column = [ self . table columnForView : [ notification object ] ] ;
2015-10-20 18:55:08 +02:00
NSInteger row = [ self . table rowForView : [ notification object ] ] ;
2014-07-15 18:30:51 +00:00
NSInteger textMovement ;
// Edit the cell in the next row , same column
row + + ;
2015-04-09 19:43:33 +00:00
textMovement = [ [ notification userInfo ] [ @ "NSTextMovement" ] integerValue ] ;
2015-10-15 19:35:03 +02:00
if ( textMovement = = NSReturnTextMovement && row < chapterTable . numberOfRows )
2014-07-15 18:30:51 +00:00
{
2015-10-15 19:35:03 +02:00
NSArray * info = @ [ chapterTable , @ ( column ) , @ ( row ) ] ;
// The delay is unimportant ; editNextRow : won ' t be called until the responder
// chain finishes because the event loop containing the timer is on this thread
2014-07-15 18:30:51 +00:00
[ self performSelector : @ selector ( editNextRow : ) withObject : info afterDelay : 0.0 ] ;
}
}
2015-10-15 19:35:03 +02:00
- ( void ) editNextRow : ( id ) objects
2014-07-15 18:30:51 +00:00
{
2015-04-09 19:43:33 +00:00
NSTableView * chapterTable = objects [ 0 ] ;
NSInteger column = [ objects [ 1 ] integerValue ] ;
NSInteger row = [ objects [ 2 ] integerValue ] ;
2014-07-15 18:30:51 +00:00
2015-10-15 19:35:03 +02:00
if ( row >= 0 && row < chapterTable . numberOfRows )
2014-07-15 18:30:51 +00:00
{
[ chapterTable selectRowIndexes : [ NSIndexSet indexSetWithIndex : row ] byExtendingSelection : NO ] ;
[ chapterTable editColumn : column row : row withEvent : nil select : YES ] ;
}
}
2017-11-12 10:56:38 +01:00
- ( IBAction ) doubleClickAction : ( NSTableView * ) sender
{
if ( sender . clickedRow > -1 ) {
NSTableColumn * column = sender . tableColumns [ sender . clickedColumn ] ;
if ( [ column . identifier isEqualToString : @ "title" ] ) {
// edit the cell
[ sender editColumn : sender . clickedColumn
row : sender . clickedRow
withEvent : nil
select : YES ] ;
}
}
}
2015-10-15 19:35:03 +02:00
# pragma mark - Chapter Files Import / Export
2014-07-15 18:30:51 +00:00
2016-08-22 14:38:01 +02:00
- ( BOOL ) importChaptersFromURL : ( NSURL * ) URL error : ( NSError * * ) outError
{
NSArray < NSArray < NSString * > * > * csvData = [ NSArray HB_arrayWithContentsOfCSVURL : URL ] ;
if ( csvData . count = = self . chapterTitles . count )
{
NSUInteger i = 0 ;
for ( NSArray < NSString * > * lineFields in csvData )
{
if ( lineFields . count < 2 || [ lineFields [ 0 ] integerValue ] ! = i + 1 )
{
if ( NULL ! = outError )
{
2018-06-09 10:06:52 +02:00
* outError = [ NSError errorWithDomain : @ "HBError" code : 0 userInfo : @ { NSLocalizedDescriptionKey : NSLocalizedString ( @ "Invalid chapters CSV file" , @ "Chapters import -> invalid CSV description" ) ,
NSLocalizedRecoverySuggestionErrorKey : NSLocalizedString ( @ "The CSV file is not a valid chapters CSV file." , @ "Chapters import -> invalid CSV recovery suggestion" ) } ] ;
2016-08-22 14:38:01 +02:00
}
return NO ;
}
i + + ;
}
NSUInteger j = 0 ;
for ( NSArray < NSString * > * lineFields in csvData )
{
[ self . chapterTitles [ j ] setTitle : lineFields [ 1 ] ] ;
j + + ;
}
return YES ;
}
if ( NULL ! = outError )
{
2018-06-09 10:06:52 +02:00
* outError = [ NSError errorWithDomain : @ "HBError" code : 0 userInfo : @ { NSLocalizedDescriptionKey : NSLocalizedString ( @ "Incorrect line count" , @ "Chapters import -> invalid CSV line count description" ) ,
NSLocalizedRecoverySuggestionErrorKey : NSLocalizedString ( @ "The line count in the chapters CSV file does not match the number of chapters in the movie." , @ "Chapters import -> invalid CSV line count recovery suggestion" ) } ] ;
2016-08-22 14:38:01 +02:00
}
return NO ;
}
2015-10-15 19:35:03 +02:00
- ( IBAction ) browseForChapterFile : ( id ) sender
2014-07-15 18:30:51 +00:00
{
2015-10-15 19:35:03 +02:00
// We get the current file name and path from the destination field here
2019-07-29 07:34:38 +02:00
NSURL * sourceDirectory = [ NSUserDefaults . standardUserDefaults URLForKey : HBLastDestinationDirectoryURL ] ;
2014-07-15 18:30:51 +00:00
2015-10-15 19:35:03 +02:00
// Open a panel to let the user choose the file
2014-07-15 18:30:51 +00:00
NSOpenPanel * panel = [ NSOpenPanel openPanel ] ;
2016-08-22 14:38:01 +02:00
panel . allowedFileTypes = @ [ @ "csv" , @ "txt" ] ;
2015-10-15 19:35:03 +02:00
panel . directoryURL = sourceDirectory ;
2014-07-15 18:30:51 +00:00
2015-10-15 19:35:03 +02:00
[ panel beginSheetModalForWindow : self . view . window completionHandler : ^ ( NSInteger result )
{
2018-06-10 09:12:18 +02:00
if ( result = = NSModalResponseOK )
2014-07-15 18:30:51 +00:00
{
2016-08-22 14:38:01 +02:00
NSError * error ;
if ( [ self importChaptersFromURL : panel . URL error : & error ] = = NO )
2014-07-15 18:30:51 +00:00
{
2016-08-22 14:38:01 +02:00
[ self presentError : error ] ;
2014-07-15 18:30:51 +00:00
}
2023-07-07 07:30:02 +02:00
[ panel . URL stopAccessingSecurityScopedResource ] ;
2014-07-15 18:30:51 +00:00
}
} ] ;
}
2015-10-15 19:35:03 +02:00
- ( IBAction ) browseForChapterFileSave : ( id ) sender
2014-07-15 18:30:51 +00:00
{
2019-07-29 07:34:38 +02:00
NSURL * destinationDirectory = [ NSUserDefaults . standardUserDefaults URLForKey : HBLastDestinationDirectoryURL ] ;
2014-07-15 18:30:51 +00:00
NSSavePanel * panel = [ NSSavePanel savePanel ] ;
2015-10-15 19:35:03 +02:00
panel . allowedFileTypes = @ [ @ "csv" ] ;
panel . directoryURL = destinationDirectory ;
2022-01-11 09:18:47 +01:00
panel . nameFieldStringValue = self . job . destinationFileName . stringByDeletingPathExtension ;
2014-07-15 18:30:51 +00:00
2015-10-15 19:35:03 +02:00
[ panel beginSheetModalForWindow : self . view . window completionHandler : ^ ( NSInteger result )
{
2018-06-10 09:12:18 +02:00
if ( result = = NSModalResponseOK )
2014-07-15 18:30:51 +00:00
{
2015-10-15 19:35:03 +02:00
NSError * saveError ;
NSMutableString * csv = [ NSMutableString string ] ;
2014-07-15 18:30:51 +00:00
2015-10-15 19:35:03 +02:00
NSInteger idx = 0 ;
for ( HBChapter * chapter in self . chapterTitles )
{
// put each chapter title from the table into the array
2016-08-22 14:38:01 +02:00
[ csv appendFormat : @ "%ld," , idx + 1 ] ;
2015-10-15 19:35:03 +02:00
idx + + ;
2020-08-26 11:53:14 -04:00
NSString * sanitizedTitle = [ chapter . title stringByReplacingOccurrencesOfString : @ "\" " withString:@" \ "\" " ] ;
2016-08-22 14:38:01 +02:00
// If the title contains any commas or quotes , add quotes
2020-08-26 11:53:14 -04:00
if ( [ sanitizedTitle containsString : @ "," ] || [ sanitizedTitle containsString : @ "\" " ] )
2016-08-22 14:38:01 +02:00
{
[ csv appendString : @ "\" " ] ;
2020-08-26 11:53:14 -04:00
[ csv appendString : sanitizedTitle ] ;
2016-08-22 14:38:01 +02:00
[ csv appendString : @ "\" " ] ;
}
else
{
2020-08-26 11:53:14 -04:00
[ csv appendString : sanitizedTitle ] ;
2016-08-22 14:38:01 +02:00
}
2015-10-15 19:35:03 +02:00
[ csv appendString : @ "\n" ] ;
}
2014-07-15 18:30:51 +00:00
2015-10-15 19:35:03 +02:00
[ csv deleteCharactersInRange : NSMakeRange ( csv . length - 1 , 1 ) ] ;
2014-07-15 18:30:51 +00:00
2015-10-15 19:35:03 +02:00
// try to write it to where the user wanted
if ( ! [ csv writeToURL : panel . URL
atomically : YES
encoding : NSUTF8StringEncoding
error : & saveError ] )
2014-07-15 18:30:51 +00:00
{
[ panel close ] ;
[ [ NSAlert alertWithError : saveError ] runModal ] ;
}
2023-07-07 07:30:02 +02:00
[ panel . URL stopAccessingSecurityScopedResource ] ;
2014-07-15 18:30:51 +00:00
}
} ] ;
}
@ end