// // $Id$ // // SPExportController.m // sequel-pro // // Created by Ben Perry (benperry.com.au) on 21/02/09. // Modified by Stuart Connolly (stuconnolly.com) // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA // // More info at <http://code.google.com/p/sequel-pro/> #import <MCPKit/MCPKit.h> #import "SPExportController.h" #import "SPExportInitializer.h" #import "SPTablesList.h" #import "SPTableData.h" #import "SPTableContent.h" #import "SPArrayAdditions.h" #import "SPStringAdditions.h" #import "SPConstants.h" #import "SPGrowlController.h" @interface SPExportController (PrivateAPI) - (void)_toggleExportButton:(id)uiStateDict; - (void)_toggleExportButtonOnBackgroundThread; - (void)_toggleExportButtonWithBool:(NSNumber *)enable; - (void)_resizeWindowByHeightDelta:(NSInteger)delta; @end @implementation SPExportController @synthesize connection; @synthesize exportToMultipleFiles; @synthesize exportCancelled; #pragma mark - #pragma mark Initialization /** * Initializes an instance of SPExportController */ - (id)init { if (self = [super initWithWindowNibName:@"ExportDialog"]) { [self setExportCancelled:NO]; [self setExportToMultipleFiles:YES]; exportType = 0; exportTableCount = 0; currentTableExportIndex = 0; exportFilename = @""; exportTypeLabel = @""; createCustomFilename = NO; sqlPreviousConnectionEncodingViaLatin1 = NO; tables = [[NSMutableArray alloc] init]; exporters = [[NSMutableArray alloc] init]; operationQueue = [[NSOperationQueue alloc] init]; showAdvancedView = NO; heightOffset = 0; windowMinWidth = [[self window] minSize].width; windowMinHeigth = [[self window] minSize].height; prefs = [NSUserDefaults standardUserDefaults]; // Default filename tokens availableFilenameTokens = @"host,database,table,date,time"; } return self; } /** * Upon awakening select the first toolbar item */ - (void)awakeFromNib { // Set the current toolbar item currentToolbarItem = [[exportToolbar items] objectAtIndex:0]; // Upon awakening select the SQL tab [exportToolbar setSelectedItemIdentifier:[currentToolbarItem itemIdentifier]]; // Select the 'selected tables' option [exportInputMatrix selectCellAtRow:2 column:0]; } #pragma mark - #pragma mark IB action methods /** * Display the export window allowing the user to select what and of what type to export. */ - (void)export { [self exportTables:nil asFormat:0]; } /** * Displays the export window with the supplied tables and export type/format selected. */ - (void)exportTables:(NSArray *)exportTables asFormat:(SPExportType)format { [self refreshTableList:self]; if (exportTables && format) { // Select the correct tab according to the supplied export type [exportToolbar setSelectedItemIdentifier:[[[exportToolbar items] objectAtIndex:(format - 1)] itemIdentifier]]; // Select the 'selected tables' source option [exportInputMatrix selectCellAtRow:2 column:0]; // Disable all tables for (NSMutableArray *table in tables) { [table replaceObjectAtIndex:1 withObject:[NSNumber numberWithBool:NO]]; [table replaceObjectAtIndex:2 withObject:[NSNumber numberWithBool:NO]]; [table replaceObjectAtIndex:3 withObject:[NSNumber numberWithBool:NO]]; } // Select the supplied tables for (NSMutableArray *table in tables) { for (NSString *exportTable in exportTables) { if ([exportTable isEqualToString:[table objectAtIndex:0]]) { [table replaceObjectAtIndex:1 withObject:[NSNumber numberWithBool:YES]]; [table replaceObjectAtIndex:2 withObject:[NSNumber numberWithBool:YES]]; [table replaceObjectAtIndex:3 withObject:[NSNumber numberWithBool:YES]]; } } } [exportTableList reloadData]; // Ensure interface validation [self switchTab:[[exportToolbar items] objectAtIndex:(format - 1)]]; } NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDesktopDirectory, NSUserDomainMask, YES); // If found the set the default path to the user's desktop, otherwise use their home directory [exportPathField setStringValue:([paths count] > 0) ? [paths objectAtIndex:0] : NSHomeDirectory()]; [NSApp beginSheet:[self window] modalForWindow:[tableDocumentInstance parentWindow] modalDelegate:self didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) contextInfo:nil]; } /** * Opens the errors sheet and displays the supplied errors string. */ - (void)openExportErrorsSheetWithString:(NSString *)errors { [errorsTextView setString:@""]; [errorsTextView setString:errors]; [NSApp beginSheet:errorsWindow modalForWindow:[tableDocumentInstance parentWindow] modalDelegate:self didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) contextInfo:nil]; } /** * Displays the export finished Growl notification. */ - (void)displayExportFinishedGrowlNotification { // Export finished Growl notification [[SPGrowlController sharedGrowlController] notifyWithTitle:@"Export Finished" description:[NSString stringWithFormat:NSLocalizedString(@"Finished exporting to %@", @"description for finished exporting growl notification"), exportFilename] document:tableDocumentInstance notificationName:@"Export Finished"]; } /** * Expands the custom filename format based on the selected tokens. */ - (NSString *)expandCustomFilenameFormatFromString:(NSString *)format usingTableName:(NSString *)table { NSMutableString *string = [NSMutableString stringWithString:format]; NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; [dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4]; [dateFormatter setDateStyle:NSDateFormatterShortStyle]; [dateFormatter setTimeStyle:NSDateFormatterNoStyle]; [string replaceOccurrencesOfString:@"host" withString:[tableDocumentInstance host] options:NSLiteralSearch range:NSMakeRange(0, [string length])]; [string replaceOccurrencesOfString:@"database" withString:[tableDocumentInstance database] options:NSLiteralSearch range:NSMakeRange(0, [string length])]; if (table) { [string replaceOccurrencesOfString:@"table" withString:table options:NSLiteralSearch range:NSMakeRange(0, [string length])]; } else { [string replaceOccurrencesOfString:@"table" withString:@"" options:NSLiteralSearch range:NSMakeRange(0, [string length])]; } [string replaceOccurrencesOfString:@"date" withString:[dateFormatter stringFromDate:[NSDate date]] options:NSLiteralSearch range:NSMakeRange(0, [string length])]; [dateFormatter setDateStyle:NSDateFormatterNoStyle]; [dateFormatter setTimeStyle:NSDateFormatterShortStyle]; [string replaceOccurrencesOfString:@"time" withString:[dateFormatter stringFromDate:[NSDate date]] options:NSLiteralSearch range:NSMakeRange(0, [string length])]; // Strip comma separators [string replaceOccurrencesOfString:@"," withString:@"" options:NSLiteralSearch range:NSMakeRange(0, [string length])]; // Replace colons with hyphens [string replaceOccurrencesOfString:@":" withString:@"-" options:NSLiteralSearch range:NSMakeRange(0, [string length])]; [dateFormatter release]; return string; } /** * Closes the export dialog. */ - (IBAction)closeSheet:(id)sender { if ([sender window] == [self window]) { // Close the advanced options view if it's open [exportAdvancedOptionsView setHidden:YES]; [exportAdvancedOptionsViewButton setState:NSOffState]; [self _resizeWindowByHeightDelta:0]; } [NSApp endSheet:[sender window] returnCode:[sender tag]]; [[sender window] orderOut:self]; } /** * Change the selected toolbar item. */ - (IBAction)switchTab:(id)sender { if ([sender isKindOfClass:[NSToolbarItem class]]) { currentToolbarItem = sender; NSString *label = [[currentToolbarItem label] uppercaseString]; [exportTabBar selectTabViewItemWithIdentifier:[label lowercaseString]]; BOOL isSQL = [label isEqualToString:@"SQL"]; BOOL isCSV = [label isEqualToString:@"CSV"]; BOOL isXML = [label isEqualToString:@"XML"]; BOOL isHTML = [label isEqualToString:@"HTML"]; BOOL isPDF = [label isEqualToString:@"PDF"]; BOOL isDot = [label isEqualToString:@"DOT"]; BOOL disable = (isCSV || isXML || isHTML || isPDF || isDot); [exportFilePerTableCheck setHidden:(isSQL || isDot)]; [exportFilePerTableNote setHidden:(isSQL || isDot)]; [exportTableList setEnabled:(!isDot)]; [exportSelectAllTablesButton setEnabled:(!isDot)]; [exportDeselectAllTablesButton setEnabled:(!isDot)]; [exportRefreshTablesButton setEnabled:(!isDot)]; [[exportInputMatrix cellAtRow:2 column:0] setEnabled:(!isDot)]; if (isDot) { // Disable all source checkboxes [[exportInputMatrix cellAtRow:0 column:0] setEnabled:NO]; [[exportInputMatrix cellAtRow:1 column:0] setEnabled:NO]; } else { // Enable/disable the 'filtered result' and 'query result' options [[exportInputMatrix cellAtRow:0 column:0] setEnabled:((disable) && ([[tableContentInstance currentResult] count] > 1))]; [[exportInputMatrix cellAtRow:1 column:0] setEnabled:((disable) && ([[customQueryInstance currentResult] count] > 1))]; } [[exportTableList tableColumnWithIdentifier:@"structure"] setHidden:disable]; [[exportTableList tableColumnWithIdentifier:@"drop"] setHidden:disable]; [[[exportTableList tableColumnWithIdentifier:@"content"] headerCell] setStringValue:(disable) ? @"" : @"C"]; [exportCSVNULLValuesAsTextField setStringValue:[prefs stringForKey:SPNullValue]]; } } /** * Enables/disables and shows/hides various interface controls depending on the selected item. */ - (IBAction)switchInput:(id)sender { if ([sender isKindOfClass:[NSMatrix class]]) { BOOL isSelectedTables = ([[sender selectedCell] tag] == SPTableExport); [exportFilePerTableCheck setHidden:(!isSelectedTables)]; [exportFilePerTableNote setHidden:(!isSelectedTables)]; [exportTableList setEnabled:isSelectedTables]; [exportSelectAllTablesButton setEnabled:isSelectedTables]; [exportDeselectAllTablesButton setEnabled:isSelectedTables]; [exportRefreshTablesButton setEnabled:isSelectedTables]; availableFilenameTokens = ([[sender selectedCell] tag] == SPQueryExport) ? @"host,database,date,time" : @"host,database,table,date,time"; } } /** * Cancel's the export operation by stopping the current table export loop and marking any current SPExporter * NSOperation subclasses as cancelled. */ - (IBAction)cancelExport:(id)sender { [self setExportCancelled:YES]; // Cancel all of the currently running operations [operationQueue cancelAllOperations]; // Close the progress sheet [NSApp endSheet:exportProgressWindow returnCode:0]; [exportProgressWindow orderOut:self]; } /** * Opens the open panel when user selects to change the output path. */ - (IBAction)changeExportOutputPath:(id)sender { [exportCustomFilenameTokenField setStringValue:@""]; [exportCustomFilenameTokensField setStringValue:availableFilenameTokens]; NSOpenPanel *panel = [NSOpenPanel openPanel]; [panel setCanChooseFiles:NO]; [panel setCanChooseDirectories:YES]; [panel setCanCreateDirectories:YES]; [panel setAccessoryView:exportCustomFilenameView]; NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDesktopDirectory, NSUserDomainMask, YES); [panel beginSheetForDirectory:([paths count] > 0) ? [paths objectAtIndex:0] : NSHomeDirectory() file:nil modalForWindow:[self window] modalDelegate:self didEndSelector:@selector(savePanelDidEnd:returnCode:contextInfo:) contextInfo:nil]; } /** * Refreshes the table list. */ - (IBAction)refreshTableList:(id)sender { NSUInteger i; [tables removeAllObjects]; // For all modes, retrieve table and view names NSArray *tablesAndViews = [tablesListInstance allTableAndViewNames]; for (id itemName in tablesAndViews) { [tables addObject:[NSMutableArray arrayWithObjects: itemName, [NSNumber numberWithBool:YES], [NSNumber numberWithBool:YES], [NSNumber numberWithBool:YES], [NSNumber numberWithInt:SPTableTypeTable], nil]]; } // For SQL only, add procedures and functions if ([[[currentToolbarItem label] lowercaseString] isEqualToString:@"sql"]) { NSArray *procedures = [tablesListInstance allProcedureNames]; for (id procName in procedures) { [tables addObject:[NSMutableArray arrayWithObjects: procName, [NSNumber numberWithBool:YES], [NSNumber numberWithBool:YES], [NSNumber numberWithBool:YES], [NSNumber numberWithInt:SPTableTypeProc], nil]]; } NSArray *functions = [tablesListInstance allFunctionNames]; for (id funcName in functions) { [tables addObject:[NSMutableArray arrayWithObjects: funcName, [NSNumber numberWithBool:YES], [NSNumber numberWithBool:YES], [NSNumber numberWithBool:YES], [NSNumber numberWithInt:SPTableTypeFunc], nil]]; } } [exportTableList reloadData]; } /** * Selects or de-selects all tables. */ - (IBAction)selectDeselectAllTables:(id)sender { [self refreshTableList:self]; for (NSMutableArray *table in tables) { [table replaceObjectAtIndex:2 withObject:[NSNumber numberWithBool:[sender tag]]]; } [exportTableList reloadData]; [self _toggleExportButtonOnBackgroundThread]; } /** * Toggles the state of the custom filename format token fields. */ - (IBAction)toggleCustomFilenameFormat:(id)sender { [exportCustomFilenameTokenField setEnabled:[sender state]]; [exportCustomFilenameTokensField setEnabled:[sender state]]; } /** * Toggles the display of the advanced options box. */ - (IBAction)toggleAdvancedExportOptionsView:(id)sender { showAdvancedView = !showAdvancedView; if (showAdvancedView) { [exportAdvancedOptionsViewButton setState:NSOnState]; [self _resizeWindowByHeightDelta:([exportAdvancedOptionsView frame].size.height + 10)]; [exportAdvancedOptionsView setHidden:NO]; } else { [exportAdvancedOptionsViewButton setState:NSOffState]; [self _resizeWindowByHeightDelta:0]; [exportAdvancedOptionsView setHidden:YES]; } } /** * Toggles the export button when choosing to include or table structures in an SQL export. */ - (IBAction)toggleSQLIncludeStructure:(id)sender { [[exportTableList tableColumnWithIdentifier:@"structure"] setHidden:(![sender state])]; [self _toggleExportButtonOnBackgroundThread]; } /** * Toggles the export button when choosing to include or exclude table contents in an SQL export. */ - (IBAction)toggleSQLIncludeContent:(id)sender { [sender setTag:[sender state]]; [self selectDeselectAllTables:sender]; [self _toggleExportButtonOnBackgroundThread]; } /** * Toggles the export button when choosing to include or exclude table drop syntax in an SQL export. */ - (IBAction)toggleSQLIncludeDropSyntax:(id)sender { [[exportTableList tableColumnWithIdentifier:@"drop"] setHidden:(![sender state])]; [self _toggleExportButtonOnBackgroundThread]; } #pragma mark - #pragma mark Table view datasource methods - (NSInteger)numberOfRowsInTableView:(NSTableView *)aTableView; { return [tables count]; } - (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)rowIndex { return NSArrayObjectAtIndex([tables objectAtIndex:rowIndex], [exportTableList columnWithIdentifier:[tableColumn identifier]]); } - (void)tableView:(NSTableView *)tableView setObjectValue:(id)anObject forTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)rowIndex { [[tables objectAtIndex:rowIndex] replaceObjectAtIndex:[exportTableList columnWithIdentifier:[tableColumn identifier]] withObject:anObject]; [self _toggleExportButtonOnBackgroundThread]; } #pragma mark - #pragma mark Table view delegate methods - (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(NSInteger)rowIndex { return (tableView != exportTableList); } - (BOOL)tableView:(NSTableView *)tableView shouldTrackCell:(NSCell *)cell forTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)rowIndex { return (tableView == exportTableList); } - (void)tableView:(NSTableView *)tableView willDisplayCell:(id)aCell forTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)rowIndex { [aCell setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; } #pragma mark - #pragma mark Toolbar delegate methods - (NSArray *)toolbarSelectableItemIdentifiers:(NSToolbar *)toolbar { NSMutableArray *items = [NSMutableArray array]; for (NSToolbarItem *item in [toolbar items]) { [items addObject:[item itemIdentifier]]; } return items; } #pragma mark - #pragma mark Text field delegate methods - (void)controlTextDidChange:(NSNotification *)notification { if ([notification object] == exportCustomFilenameTokenField) { // Create the table name, but since this is only an example, use the first table in the list NSString *filename = [self expandCustomFilenameFormatFromString:[exportCustomFilenameTokenField stringValue] usingTableName:[[tablesListInstance tables] objectAtIndex:1]]; [exportCustomFilenameExampleTextField setStringValue:[NSString stringWithFormat:@"%@: %@", NSLocalizedString(@"Example", @"example label"), filename]]; } } #pragma mark - #pragma mark Other /** * Invoked when the user dismissing the export dialog and starts the export process if required. */ - (void)sheetDidEnd:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo { // Perform the export if (returnCode == NSOKButton) { // Initialize the export after half a second to give the export sheet a chance to close [self performSelector:@selector(initializeExportUsingSelectedOptions) withObject:nil afterDelay:0.5]; } } /** * Invoked when the user dismisses the save panel. Updates the selected directory if they clicked OK. */ - (void)savePanelDidEnd:(NSSavePanel *)panel returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo { if (returnCode == NSOKButton) { [exportPathField setStringValue:[panel directory]]; } } #pragma mark - /** * Dealloc */ - (void)dealloc { [tables release], tables = nil; [exporters release], exporters = nil; [operationQueue release], operationQueue = nil; if (sqlPreviousConnectionEncoding) [sqlPreviousConnectionEncoding release], sqlPreviousConnectionEncoding = nil; [super dealloc]; } #pragma mark - #pragma mark Private API /** * Enables or disables the export button based on the state of various interface controls. */ - (void)_toggleExportButton:(id)uiStateDict { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; BOOL enable; NSString *format = [uiStateDict objectForKey:@"ExportFormat"]; BOOL isSQL = [format isEqualToString:@"SQL"]; BOOL isCSV = [format isEqualToString:@"CSV"]; BOOL isXML = [format isEqualToString:@"XML"]; BOOL isHTML = [format isEqualToString:@"HTML"]; BOOL isPDF = [format isEqualToString:@"PDF"]; if (isCSV || isXML || isHTML || isPDF) { enable = NO; // Only enable the button if at least one table is selected for (NSArray *table in tables) { if ([NSArrayObjectAtIndex(table, 2) boolValue]) { enable = YES; break; } } } else if (isSQL) { BOOL structureEnabled = [uiStateDict objectForKey:@"SQLExportStructureEnabled"]; BOOL contentEnabled = [uiStateDict objectForKey:@"SQLExportContentEnabled"]; BOOL dropEnabled = [uiStateDict objectForKey:@"SQLExportDropEnabled"]; // Disable if all are unchecked if ((!contentEnabled) && (!structureEnabled) && (!dropEnabled)) { enable = NO; } // Disable if structure is unchecked, but content and drop are as dropping a table then trying to insert // into it is obviously an error else if (contentEnabled && (!structureEnabled) && (dropEnabled)) { enable = NO; } else { enable = (contentEnabled || (structureEnabled || dropEnabled)); } } [self performSelectorOnMainThread:@selector(_toggleExportButtonWithBool:) withObject:[NSNumber numberWithBool:enable] waitUntilDone:NO]; [pool release]; } /** * */ - (void)_toggleExportButtonOnBackgroundThread { NSMutableDictionary *uiStateDict = [[NSMutableDictionary alloc] init]; [uiStateDict setObject:[[currentToolbarItem label] uppercaseString] forKey:@"ExportFormat"]; [uiStateDict setObject:[NSNumber numberWithInteger:[exportSQLIncludeStructureCheck state]] forKey:@"SQLExportStructureEnabled"]; [uiStateDict setObject:[NSNumber numberWithInteger:[exportSQLIncludeContentCheck state]] forKey:@"SQLExportContentEnabled"]; [uiStateDict setObject:[NSNumber numberWithInteger:[exportSQLIncludeDropSyntaxCheck state]] forKey:@"SQLExportDropEnabled"]; [NSThread detachNewThreadSelector:@selector(_toggleExportButton:) toTarget:self withObject:uiStateDict]; [uiStateDict release]; } /** * Enables or disables the export button based on the supplied number (boolean). */ - (void)_toggleExportButtonWithBool:(NSNumber *)enable { [exportButton setEnabled:[enable boolValue]]; } /** * Resizes the export window's height by the supplied delta, while retaining the position of * all interface controls. */ - (void)_resizeWindowByHeightDelta:(NSInteger)delta { NSUInteger scrollMask = [exportTablelistScrollView autoresizingMask]; NSUInteger buttonBarMask = [exportTableListButtonBar autoresizingMask]; NSUInteger tabBarMask = [exportTabBar autoresizingMask]; NSUInteger buttonMask = [exportAdvancedOptionsViewButton autoresizingMask]; NSUInteger textFieldMask = [exportAdvancedOptionsViewLabelButton autoresizingMask]; NSUInteger advancedViewMask = [exportAdvancedOptionsView autoresizingMask]; NSRect frame = [[self window] frame]; [exportTablelistScrollView setAutoresizingMask:NSViewNotSizable | NSViewMinYMargin]; [exportTableListButtonBar setAutoresizingMask:NSViewNotSizable | NSViewMinYMargin]; [exportTabBar setAutoresizingMask:NSViewNotSizable | NSViewMinYMargin]; [exportAdvancedOptionsViewButton setAutoresizingMask:NSViewNotSizable | NSViewMinYMargin]; [exportAdvancedOptionsViewLabelButton setAutoresizingMask:NSViewNotSizable | NSViewMinYMargin]; [exportAdvancedOptionsView setAutoresizingMask:NSViewNotSizable | NSViewMinYMargin]; NSInteger newMinHeight = (windowMinHeigth - heightOffset + delta < windowMinHeigth) ? windowMinHeigth : windowMinHeigth - heightOffset + delta; [[self window] setMinSize:NSMakeSize(windowMinWidth, newMinHeight)]; frame.origin.y += heightOffset; frame.size.height -= heightOffset; heightOffset = delta; frame.origin.y -= heightOffset; frame.size.height += heightOffset; [[self window] setFrame:frame display:YES animate:YES]; [exportTablelistScrollView setAutoresizingMask:scrollMask]; [exportTableListButtonBar setAutoresizingMask:buttonBarMask]; [exportTabBar setAutoresizingMask:tabBarMask]; [exportAdvancedOptionsViewButton setAutoresizingMask:buttonMask]; [exportAdvancedOptionsViewLabelButton setAutoresizingMask:textFieldMask]; [exportAdvancedOptionsView setAutoresizingMask:advancedViewMask]; } @end