diff options
author | rowanbeentje <rowan@beent.je> | 2011-05-07 15:31:54 +0000 |
---|---|---|
committer | rowanbeentje <rowan@beent.je> | 2011-05-07 15:31:54 +0000 |
commit | 0c2a225a68ef1512e51ff3a48fef1fa13eacce60 (patch) | |
tree | 624b65144aac2f2a5d57172f8dd7625a5a8e236a | |
parent | 47a1c49e95403e6da6c9e0ae979144fd5c1dff08 (diff) | |
download | sequelpro-0c2a225a68ef1512e51ff3a48fef1fa13eacce60.tar.gz sequelpro-0c2a225a68ef1512e51ff3a48fef1fa13eacce60.tar.bz2 sequelpro-0c2a225a68ef1512e51ff3a48fef1fa13eacce60.zip |
Implement a large number of minor fixes and improvements to export functionality, including an overhaul of filename NSTokenField functionality:
- Improve the NSTokenField used for export filenames: only tokenise reserved tokens, don't tokenise reserved words which are parts of other words, allow the comma to be used, update tokenisation during typing, and prevent whitespace triming.
- Save the last selected export path, and make the path selection button open a dialog to the selected directory
- Save the export filename and restore on future uses of the export dialog (only if the name contains placeholder tokens, so one-off export names aren't saved)
- If the advanced options are collapsed, display a summary of the selected options next to the disclosure triangle
- Display a small warning in the corner of the window if the export file cannot be imported into Sequel Pro, to warn those people attempting to back up their databases in XML
- Clarify and improve the export warning dialog if files already exist or could not be created; make the simpler file-exists cases reflect OS-style dialogs, alter wording based on the number of files that failed and how they failed, and only show the "replace" or "skip" type buttons if it makes sense to do so.
- Fix a mutation-during-enumeration error when skipping files
- If "Cancel" is chosen in the export file creation replace/error dialog, redisplay the export sheet with the previous selection still active
- Add support for year, month and day tokens in the filename token list
- Don't allow blank custom filenames, before or after tokenisation, as this can cause problems - instead fall back to default filenames in those cases
- Only append the extension if one hasn't been set - on all export formats, extending r3284
- If exporting to multiple files option is enabled but only one table is selected, supply that table name for filename table tokens
- Update the progress bar to reflect update progress when exporting CSV data
- Fix a bug causing exports to hang if the low-memory advanced option was set and content was selected to export and any empty tables were encountered
- Save memory use and compression advanced export settings across sessions
- Update localisable strings
-rw-r--r-- | Interfaces/English.lproj/ExportDialog.xib | 149 | ||||
-rw-r--r-- | Resources/English.lproj/Localizable.strings | bin | 213204 -> 219438 bytes | |||
-rw-r--r-- | Source/SPCSVExporter.m | 1 | ||||
-rw-r--r-- | Source/SPConstants.h | 2 | ||||
-rw-r--r-- | Source/SPConstants.m | 2 | ||||
-rw-r--r-- | Source/SPExportController.h | 2 | ||||
-rw-r--r-- | Source/SPExportController.m | 127 | ||||
-rw-r--r-- | Source/SPExportControllerDelegate.m | 76 | ||||
-rw-r--r-- | Source/SPExportFileNameTokenObject.h | 34 | ||||
-rw-r--r-- | Source/SPExportFileNameTokenObject.m | 48 | ||||
-rw-r--r-- | Source/SPExportFileUtilities.m | 142 | ||||
-rw-r--r-- | Source/SPExportFilenameUtilities.h | 4 | ||||
-rw-r--r-- | Source/SPExportFilenameUtilities.m | 174 | ||||
-rw-r--r-- | Source/SPExportInitializer.m | 58 | ||||
-rw-r--r-- | Source/SPSQLExporter.m | 23 | ||||
-rw-r--r-- | sequel-pro.xcodeproj/project.pbxproj | 6 |
16 files changed, 718 insertions, 130 deletions
diff --git a/Interfaces/English.lproj/ExportDialog.xib b/Interfaces/English.lproj/ExportDialog.xib index 50a42201..43d07e90 100644 --- a/Interfaces/English.lproj/ExportDialog.xib +++ b/Interfaces/English.lproj/ExportDialog.xib @@ -2,10 +2,10 @@ <archive type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="7.10"> <data> <int key="IBDocument.SystemTarget">1050</int> - <string key="IBDocument.SystemVersion">10J567</string> - <string key="IBDocument.InterfaceBuilderVersion">804</string> + <string key="IBDocument.SystemVersion">10J869</string> + <string key="IBDocument.InterfaceBuilderVersion">851</string> <string key="IBDocument.AppKitVersion">1038.35</string> - <string key="IBDocument.HIToolboxVersion">462.00</string> + <string key="IBDocument.HIToolboxVersion">461.00</string> <object class="NSMutableDictionary" key="IBDocument.PluginVersions"> <bool key="EncodedWithXMLCoder">YES</bool> <object class="NSArray" key="dict.sortedKeys"> @@ -15,14 +15,14 @@ </object> <object class="NSMutableArray" key="dict.values"> <bool key="EncodedWithXMLCoder">YES</bool> - <string>804</string> - <string>1.2.2</string> + <string>851</string> + <string>1.2.5</string> </object> </object> <object class="NSMutableArray" key="IBDocument.EditedObjectIDs"> <bool key="EncodedWithXMLCoder">YES</bool> <integer value="1086"/> - <integer value="1"/> + <integer value="1305"/> </object> <object class="NSArray" key="IBDocument.PluginDependencies"> <bool key="EncodedWithXMLCoder">YES</bool> @@ -424,6 +424,25 @@ <int key="NSPeriodicInterval">75</int> </object> </object> + <object class="NSTextField" id="912734518"> + <reference key="NSNextResponder" ref="13817034"/> + <int key="NSvFlags">290</int> + <string key="NSFrame">{{17, 20}, {274, 14}}</string> + <reference key="NSSuperview" ref="13817034"/> + <bool key="NSEnabled">YES</bool> + <object class="NSTextFieldCell" key="NSCell" id="73683384"> + <int key="NSCellFlags">68288064</int> + <int key="NSCellFlags2">272765952</int> + <string key="NSContents"/> + <reference key="NSSupport" ref="26"/> + <reference key="NSControlView" ref="912734518"/> + <reference key="NSBackgroundColor" ref="683790803"/> + <object class="NSColor" key="NSTextColor"> + <int key="NSColorSpace">3</int> + <bytes key="NSWhite">MC41MDQwMzIyNTgxAA</bytes> + </object> + </object> + </object> </object> <string key="NSFrameSize">{517, 480}</string> <reference key="NSSuperview"/> @@ -2790,6 +2809,14 @@ AAMAAAABAAEAAAFTAAMAAAAEAAAFwgAAAAAACAAIAAgACAABAAEAAQABA</bytes> <reference key="NSSuperview"/> <string key="NSClassName">NSView</string> </object> + <object class="NSUserDefaultsController" id="965845845"> + <object class="NSMutableArray" key="NSDeclaredKeys"> + <bool key="EncodedWithXMLCoder">YES</bool> + <string>SPExportCompressionLastValue</string> + <string>SPExportMemoryModeLastValue</string> + </object> + <bool key="NSSharedInstance">YES</bool> + </object> </object> <object class="IBObjectContainer" key="IBDocument.Objects"> <object class="NSMutableArray" key="connectionRecords"> @@ -3410,6 +3437,54 @@ AAMAAAABAAEAAAFTAAMAAAAEAAAFwgAAAAAACAAIAAgACAABAAEAAQABA</bytes> </object> <int key="connectionID">1377</int> </object> + <object class="IBConnectionRecord"> + <object class="IBOutletConnection" key="connection"> + <string key="label">exportFormatInfoText</string> + <reference key="source" ref="1001"/> + <reference key="destination" ref="912734518"/> + </object> + <int key="connectionID">1380</int> + </object> + <object class="IBConnectionRecord"> + <object class="IBActionConnection" key="connection"> + <string key="label">toggleNewFilePerTable:</string> + <reference key="source" ref="1001"/> + <reference key="destination" ref="238794017"/> + </object> + <int key="connectionID">1381</int> + </object> + <object class="IBConnectionRecord"> + <object class="IBBindingConnection" key="connection"> + <string key="label">selectedIndex: values.SPExportCompressionLastValue</string> + <reference key="source" ref="257072199"/> + <reference key="destination" ref="965845845"/> + <object class="NSNibBindingConnector" key="connector"> + <reference key="NSSource" ref="257072199"/> + <reference key="NSDestination" ref="965845845"/> + <string key="NSLabel">selectedIndex: values.SPExportCompressionLastValue</string> + <string key="NSBinding">selectedIndex</string> + <string key="NSKeyPath">values.SPExportCompressionLastValue</string> + <int key="NSNibBindingConnectorVersion">2</int> + </object> + </object> + <int key="connectionID">1387</int> + </object> + <object class="IBConnectionRecord"> + <object class="IBBindingConnection" key="connection"> + <string key="label">value: values.SPExportMemoryModeLastValue</string> + <reference key="source" ref="829070828"/> + <reference key="destination" ref="965845845"/> + <object class="NSNibBindingConnector" key="connector"> + <reference key="NSSource" ref="829070828"/> + <reference key="NSDestination" ref="965845845"/> + <string key="NSLabel">value: values.SPExportMemoryModeLastValue</string> + <string key="NSBinding">value</string> + <string key="NSKeyPath">values.SPExportMemoryModeLastValue</string> + <int key="NSNibBindingConnectorVersion">2</int> + </object> + </object> + <int key="connectionID">1389</int> + </object> </object> <object class="IBMutableOrderedSet" key="objectRecords"> <object class="NSArray" key="orderedObjects"> @@ -3459,6 +3534,7 @@ AAMAAAABAAEAAAFTAAMAAAAEAAAFwgAAAAAACAAIAAgACAABAAEAAQABA</bytes> <reference ref="184955131"/> <reference ref="793270283"/> <reference ref="443463899"/> + <reference ref="912734518"/> </object> <reference key="parent" ref="834889278"/> </object> @@ -4976,6 +5052,25 @@ AAMAAAABAAEAAAFTAAMAAAAEAAAFwgAAAAAACAAIAAgACAABAAEAAQABA</bytes> <reference key="object" ref="464364038"/> <reference key="parent" ref="227266718"/> </object> + <object class="IBObjectRecord"> + <int key="objectID">1378</int> + <reference key="object" ref="912734518"/> + <object class="NSMutableArray" key="children"> + <bool key="EncodedWithXMLCoder">YES</bool> + <reference ref="73683384"/> + </object> + <reference key="parent" ref="13817034"/> + </object> + <object class="IBObjectRecord"> + <int key="objectID">1379</int> + <reference key="object" ref="73683384"/> + <reference key="parent" ref="912734518"/> + </object> + <object class="IBObjectRecord"> + <int key="objectID">1385</int> + <reference key="object" ref="965845845"/> + <reference key="parent" ref="0"/> + </object> </object> </object> <object class="NSMutableDictionary" key="flattenedProperties"> @@ -5206,6 +5301,8 @@ AAMAAAABAAEAAAFTAAMAAAAEAAAFwgAAAAAACAAIAAgACAABAAEAAQABA</bytes> <string>1375.ImportedFromIB2</string> <string>1375.notes</string> <string>1376.IBPluginDependency</string> + <string>1378.IBPluginDependency</string> + <string>1379.IBPluginDependency</string> <string>2.IBPluginDependency</string> <string>2.IBUserGuides</string> <string>294.IBEditorWindowLastContentRect</string> @@ -5271,15 +5368,15 @@ AAMAAAABAAEAAAFTAAMAAAAEAAAFwgAAAAAACAAIAAgACAABAAEAAQABA</bytes> <object class="NSMutableArray" key="dict.values"> <bool key="EncodedWithXMLCoder">YES</bool> <string>com.apple.InterfaceBuilder.CocoaPlugin</string> - <string>{{354, 248}, {517, 480}}</string> + <string>{{206, 286}, {517, 480}}</string> <string>com.apple.InterfaceBuilder.CocoaPlugin</string> <boolean value="NO"/> <boolean value="NO"/> - <string>{{354, 248}, {517, 480}}</string> + <string>{{206, 286}, {517, 480}}</string> <boolean value="NO"/> <boolean value="YES"/> <string>{517, 480}</string> - <string>{{872, 645}, {517, 359}}</string> + <string>{{428, 497}, {517, 359}}</string> <string>com.apple.InterfaceBuilder.CocoaPlugin</string> <string>com.apple.InterfaceBuilder.CocoaPlugin</string> <object class="NSMutableDictionary"> @@ -5643,6 +5740,8 @@ AAMAAAABAAEAAAFTAAMAAAAEAAAFwgAAAAAACAAIAAgACAABAAEAAQABA</bytes> </object> <string>com.apple.InterfaceBuilder.CocoaPlugin</string> <string>com.apple.InterfaceBuilder.CocoaPlugin</string> + <string>com.apple.InterfaceBuilder.CocoaPlugin</string> + <string>com.apple.InterfaceBuilder.CocoaPlugin</string> <object class="NSMutableArray"> <bool key="EncodedWithXMLCoder">YES</bool> </object> @@ -5730,7 +5829,7 @@ AAMAAAABAAEAAAFTAAMAAAAEAAAFwgAAAAAACAAIAAgACAABAAEAAQABA</bytes> </object> </object> <nil key="sourceID"/> - <int key="maxID">1377</int> + <int key="maxID">1389</int> </object> <object class="IBClassDescriber" key="IBDocument.Classes"> <object class="NSMutableArray" key="referencedPartialClassDescriptions"> @@ -6013,6 +6112,7 @@ AAMAAAABAAEAAAFTAAMAAAAEAAAFwgAAAAAACAAIAAgACAABAAEAAQABA</bytes> <string>switchInput:</string> <string>toggleAdvancedExportOptionsView:</string> <string>toggleCustomFilenameFormatView:</string> + <string>toggleNewFilePerTable:</string> <string>toggleSQLIncludeContent:</string> <string>toggleSQLIncludeDropSyntax:</string> <string>toggleSQLIncludeStructure:</string> @@ -6035,6 +6135,7 @@ AAMAAAABAAEAAAFTAAMAAAAEAAAFwgAAAAAACAAIAAgACAABAAEAAQABA</bytes> <string>id</string> <string>id</string> <string>id</string> + <string>id</string> </object> </object> <object class="NSMutableDictionary" key="actionInfosByName"> @@ -6052,6 +6153,7 @@ AAMAAAABAAEAAAFTAAMAAAAEAAAFwgAAAAAACAAIAAgACAABAAEAAQABA</bytes> <string>switchInput:</string> <string>toggleAdvancedExportOptionsView:</string> <string>toggleCustomFilenameFormatView:</string> + <string>toggleNewFilePerTable:</string> <string>toggleSQLIncludeContent:</string> <string>toggleSQLIncludeDropSyntax:</string> <string>toggleSQLIncludeStructure:</string> @@ -6104,6 +6206,10 @@ AAMAAAABAAEAAAFTAAMAAAAEAAAFwgAAAAAACAAIAAgACAABAAEAAQABA</bytes> <string key="candidateClassName">id</string> </object> <object class="IBActionInfo"> + <string key="name">toggleNewFilePerTable:</string> + <string key="candidateClassName">id</string> + </object> + <object class="IBActionInfo"> <string key="name">toggleSQLIncludeContent:</string> <string key="candidateClassName">id</string> </object> @@ -6148,6 +6254,7 @@ AAMAAAABAAEAAAFTAAMAAAAEAAAFwgAAAAAACAAIAAgACAABAAEAAQABA</bytes> <string>exportExcelSheetOrFilePerTableMatrix</string> <string>exportFilePerTableCheck</string> <string>exportFilenameDividerBox</string> + <string>exportFormatInfoText</string> <string>exportInputPopUpButton</string> <string>exportOptionsTabBar</string> <string>exportOutputCompressionFormatPopupButton</string> @@ -6206,6 +6313,7 @@ AAMAAAABAAEAAAFTAAMAAAAEAAAFwgAAAAAACAAIAAgACAABAAEAAQABA</bytes> <string>NSMatrix</string> <string>NSButton</string> <string>NSBox</string> + <string>NSTextField</string> <string>NSPopUpButton</string> <string>NSTabView</string> <string>NSPopUpButton</string> @@ -6267,6 +6375,7 @@ AAMAAAABAAEAAAFTAAMAAAAEAAAFwgAAAAAACAAIAAgACAABAAEAAQABA</bytes> <string>exportExcelSheetOrFilePerTableMatrix</string> <string>exportFilePerTableCheck</string> <string>exportFilenameDividerBox</string> + <string>exportFormatInfoText</string> <string>exportInputPopUpButton</string> <string>exportOptionsTabBar</string> <string>exportOutputCompressionFormatPopupButton</string> @@ -6395,6 +6504,10 @@ AAMAAAABAAEAAAFTAAMAAAAEAAAFwgAAAAAACAAIAAgACAABAAEAAQABA</bytes> <string key="candidateClassName">NSBox</string> </object> <object class="IBToOneOutletInfo"> + <string key="name">exportFormatInfoText</string> + <string key="candidateClassName">NSTextField</string> + </object> + <object class="IBToOneOutletInfo"> <string key="name">exportInputPopUpButton</string> <string key="candidateClassName">NSPopUpButton</string> </object> @@ -6727,6 +6840,14 @@ AAMAAAABAAEAAAFTAAMAAAAEAAAFwgAAAAAACAAIAAgACAABAAEAAQABA</bytes> </object> </object> <object class="IBPartialClassDescription"> + <string key="className">NSController</string> + <string key="superclassName">NSObject</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSController.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> <string key="className">NSFormatter</string> <string key="superclassName">NSObject</string> <object class="IBClassDescriptionSource" key="sourceIdentifier"> @@ -7315,6 +7436,14 @@ AAMAAAABAAEAAAFTAAMAAAAEAAAFwgAAAAAACAAIAAgACAABAAEAAQABA</bytes> </object> </object> <object class="IBPartialClassDescription"> + <string key="className">NSUserDefaultsController</string> + <string key="superclassName">NSController</string> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBFrameworkSource</string> + <string key="minorKey">AppKit.framework/Headers/NSUserDefaultsController.h</string> + </object> + </object> + <object class="IBPartialClassDescription"> <string key="className">NSView</string> <object class="IBClassDescriptionSource" key="sourceIdentifier"> <string key="majorKey">IBFrameworkSource</string> diff --git a/Resources/English.lproj/Localizable.strings b/Resources/English.lproj/Localizable.strings Binary files differindex 4ffd1476..9c49eab5 100644 --- a/Resources/English.lproj/Localizable.strings +++ b/Resources/English.lproj/Localizable.strings diff --git a/Source/SPCSVExporter.m b/Source/SPCSVExporter.m index 36ccbf58..651f0ffd 100644 --- a/Source/SPCSVExporter.m +++ b/Source/SPCSVExporter.m @@ -122,6 +122,7 @@ // Make a streaming request for the data if the data array isn't set if ((![self csvDataArray]) && [self csvTableName]) { + totalRows = [[[[connection queryString:[NSString stringWithFormat:@"SELECT COUNT(1) FROM %@", [[self csvTableName] backtickQuotedString]]] fetchRowAsArray] objectAtIndex:0] integerValue]; streamingResult = [connection streamingQueryString:[NSString stringWithFormat:@"SELECT * FROM %@", [[self csvTableName] backtickQuotedString]] useLowMemoryBlockingStreaming:[self exportUsingLowMemoryBlockingStreaming]]; } diff --git a/Source/SPConstants.h b/Source/SPConstants.h index e35adb75..df66951a 100644 --- a/Source/SPConstants.h +++ b/Source/SPConstants.h @@ -370,6 +370,8 @@ extern NSString *SPCSVFieldImportMappingAlignment; extern NSString *SPImportClipboardTempFileNamePrefix; extern NSString *SPSQLExportUseCompression; extern NSString *SPNoBOMforSQLdumpFile; +extern NSString *SPExportLastDirectory; +extern NSString *SPExportFilenameFormat; // Misc extern NSString *SPContentFilters; diff --git a/Source/SPConstants.m b/Source/SPConstants.m index 30ae2bfd..abb27c1c 100644 --- a/Source/SPConstants.m +++ b/Source/SPConstants.m @@ -176,6 +176,8 @@ NSString *SPCSVFieldImportMappingAlignment = @"CSVFieldImportMappingAlignm NSString *SPImportClipboardTempFileNamePrefix = @"/tmp/_SP_ClipBoard_Import_File_"; NSString *SPSQLExportUseCompression = @"SQLExportUseCompression"; NSString *SPNoBOMforSQLdumpFile = @"NoBOMforSQLdumpFile"; +NSString *SPExportLastDirectory = @"SPExportLastDirectory"; +NSString *SPExportFilenameFormat = @"SPExportFilenameFormat"; // Misc NSString *SPContentFilters = @"ContentFilters"; diff --git a/Source/SPExportController.h b/Source/SPExportController.h index f6503cb2..0130d508 100644 --- a/Source/SPExportController.h +++ b/Source/SPExportController.h @@ -74,6 +74,7 @@ IBOutlet NSWindow *exportProgressWindow; IBOutlet NSTextField *exportProgressTitle; IBOutlet NSTextField *exportProgressText; + IBOutlet NSTextField *exportFormatInfoText; IBOutlet NSProgressIndicator *exportProgressIndicator; // Custom filename view @@ -254,6 +255,7 @@ - (IBAction)toggleSQLIncludeStructure:(id)sender; - (IBAction)toggleSQLIncludeContent:(id)sender; - (IBAction)toggleSQLIncludeDropSyntax:(id)sender; +- (IBAction)toggleNewFilePerTable:(id)sender; - (void)savePanelDidEnd:(NSSavePanel *)panel returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo; diff --git a/Source/SPExportController.m b/Source/SPExportController.m index 374ff158..f7825050 100644 --- a/Source/SPExportController.m +++ b/Source/SPExportController.m @@ -34,6 +34,7 @@ #import "SPExportFile.h" #import "SPAlertSheets.h" #import "SPExportFilenameUtilities.h" +#import "SPExportFileNameTokenObject.h" #import "SPDatabaseDocument.h" // Constants @@ -52,6 +53,8 @@ static const NSString *SPSQLExportDropEnabled = @"SQLExportDropEnabled"; - (void)_switchTab; - (void)_checkForDatabaseChanges; - (void)_displayExportTypeOptions:(BOOL)display; +- (void)_updateExportFormatInformation; +- (void)_updateExportAdvancedOptionsLabel; - (void)_toggleExportButton:(id)uiStateDict; - (void)_toggleExportButtonOnBackgroundThread; @@ -131,12 +134,21 @@ static const NSString *SPSQLExportDropEnabled = @"SQLExportDropEnabled"; // Set the progress indicator's max value [exportProgressIndicator setMaxValue:(NSInteger)[exportProgressIndicator bounds].size.width]; - - NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDesktopDirectory, NSAllDomainsMask, 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()]; - + + // If a directory has previously been selected, reselect it + if ([prefs objectForKey:SPExportLastDirectory]) { + [exportPathField setStringValue:[prefs objectForKey:SPExportLastDirectory]]; + } else { + + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDesktopDirectory, NSAllDomainsMask, 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()]; + } + + // Empty the tokenizing character set for the filename field + [exportCustomFilenameTokenField setTokenizingCharacterSet:[NSCharacterSet characterSetWithCharactersInString:@""]]; + // Accept Core Animation [exportOptionsTabBar wantsLayer]; [exportTablelistScrollView wantsLayer]; @@ -158,7 +170,10 @@ static const NSString *SPSQLExportDropEnabled = @"SQLExportDropEnabled"; // Select the correct tab [exportTypeTabBar selectTabViewItemAtIndex:format]; - // Set the default export filename + // Restore the export filename if it exists, and update the display + if ([prefs objectForKey:SPExportFilenameFormat]) { + [exportCustomFilenameTokenField setObjectValue:[NSKeyedUnarchiver unarchiveObjectWithData:[prefs objectForKey:SPExportFilenameFormat]]]; + } [self updateDisplayedExportFilename]; [self refreshTableList:self]; @@ -198,6 +213,7 @@ static const NSString *SPSQLExportDropEnabled = @"SQLExportDropEnabled"; // Ensure interface validation [self _switchTab]; + [self _updateExportAdvancedOptionsLabel]; [NSApp beginSheet:[self window] modalForWindow:[tableDocumentInstance parentWindow] @@ -381,7 +397,7 @@ static const NSString *SPSQLExportDropEnabled = @"SQLExportDropEnabled"; [panel setCanChooseDirectories:YES]; [panel setCanCreateDirectories:YES]; - [panel beginSheetForDirectory:nil + [panel beginSheetForDirectory:[exportPathField stringValue] file:nil modalForWindow:[self window] modalDelegate:self @@ -467,7 +483,8 @@ static const NSString *SPSQLExportDropEnabled = @"SQLExportDropEnabled"; } [exportTableList reloadData]; - + + [self _updateExportFormatInformation]; [self _toggleExportButtonOnBackgroundThread]; } @@ -491,14 +508,6 @@ static const NSString *SPSQLExportDropEnabled = @"SQLExportDropEnabled"; [exportCustomFilenameView setHidden:(!showCustomFilenameView)]; [self _resizeWindowForCustomFilenameViewByHeightDelta:(showCustomFilenameView) ? [exportCustomFilenameView frame].size.height : 0]; - - // On close update the displayed filename - if (!showCustomFilenameView) { - [self updateDisplayedExportFilename]; - } - else { - [exportCustomFilenameViewLabelButton setTitle:NSLocalizedString(@"Customize Filename", @"default customize file name label")]; - } } /** @@ -528,6 +537,7 @@ static const NSString *SPSQLExportDropEnabled = @"SQLExportDropEnabled"; [exportAdvancedOptionsViewButton setState:showAdvancedView]; [exportAdvancedOptionsView setHidden:(!showAdvancedView)]; + [self _updateExportAdvancedOptionsLabel]; [self _resizeWindowForAdvancedOptionsViewByHeightDelta:(showAdvancedView) ? ([exportAdvancedOptionsView frame].size.height + 10) : 0]; } @@ -562,6 +572,14 @@ static const NSString *SPSQLExportDropEnabled = @"SQLExportDropEnabled"; } /** + * Toggles whether XML and CSV files should be combined into a single file. + */ +- (IBAction)toggleNewFilePerTable:(id)sender +{ + [self _updateExportFormatInformation]; +} + +/** * Opens the export sheet, selecting custom query as the export source. */ - (IBAction)exportCustomQueryResultAsFormat:(id)sender @@ -582,7 +600,20 @@ static const NSString *SPSQLExportDropEnabled = @"SQLExportDropEnabled"; { // Perform the export if (returnCode == NSOKButton) { - + + // Check whether to save the export filename. Save it if it's not blank and contains at least one + // token - this suggests it's not a one-off filename + if (![exportCustomFilenameTokenField stringValue]) { + [prefs removeObjectForKey:SPExportFilenameFormat]; + } else { + BOOL saveFilename = NO; + NSArray *representedObjects = [exportCustomFilenameTokenField objectValue]; + for (id aToken in representedObjects) { + if ([aToken isKindOfClass:[SPExportFileNameTokenObject class]]) saveFilename = YES; + } + if (saveFilename) [prefs setObject:[NSKeyedArchiver archivedDataWithRootObject:representedObjects] forKey:SPExportFilenameFormat]; + } + // If we are about to perform a table export, cache the current number of tables within the list, // refresh the list and then compare the numbers to accommodate situations where new tables are // added by external applications. @@ -619,6 +650,7 @@ static const NSString *SPSQLExportDropEnabled = @"SQLExportDropEnabled"; { if (returnCode == NSOKButton) { [exportPathField setStringValue:[panel directory]]; + [prefs setObject:[panel directory] forKey:SPExportLastDirectory]; } } @@ -734,7 +766,8 @@ static const NSString *SPSQLExportDropEnabled = @"SQLExportDropEnabled"; [self _displayExportTypeOptions:(isSQL || isCSV || isXML || isDot)]; [self updateAvailableExportFilenameTokens]; - if (!showCustomFilenameView) [self updateDisplayedExportFilename]; + [self updateDisplayedExportFilename]; + [self _updateExportFormatInformation]; } /** @@ -793,6 +826,62 @@ static const NSString *SPSQLExportDropEnabled = @"SQLExportDropEnabled"; } /** + * Updates the information note in the window based on the current export settings. + */ +- (void)_updateExportFormatInformation +{ + NSString *noteText = @""; + + // If the selected format is XML, Dot, or multiple tables in one CSV file, display a warning note. + switch (exportType) { + case SPCSVExport: + if ([exportFilePerTableCheck state]) break; + NSUInteger numberOfTables = 0; + for (NSMutableArray *eachTable in tables) { + if ([[eachTable objectAtIndex:2] boolValue]) numberOfTables++; + } + if (numberOfTables <= 1) break; + case SPXMLExport: + case SPDotExport: + noteText = NSLocalizedString(@"Import of the selected data is currently not supported.", @"Export file format cannot be imported warning"); + break; + default: + break; + } + + [exportFormatInfoText setStringValue:noteText]; +} + +/** + * Update the export advanced options label to show a summary if the options are hidden. + */ +- (void)_updateExportAdvancedOptionsLabel +{ + if (showAdvancedView) { + [exportAdvancedOptionsViewLabelButton setTitle:NSLocalizedString(@"Advanced", @"Advanced options short title")]; + return; + } + + NSMutableArray *optionsSummary = [NSMutableArray array]; + + if ([exportProcessLowMemoryButton state]) { + [optionsSummary addObject:NSLocalizedString(@"Low memory", @"Low memory export summary")]; + } else { + [optionsSummary addObject:NSLocalizedString(@"Standard memory", @"Standard memory export summary")]; + } + + if ([exportOutputCompressionFormatPopupButton indexOfSelectedItem] == SPNoCompression) { + [optionsSummary addObject:NSLocalizedString(@"no compression", @"No compression export summary - within a sentence")]; + } else if ([exportOutputCompressionFormatPopupButton indexOfSelectedItem] == SPGzipCompression) { + [optionsSummary addObject:NSLocalizedString(@"Gzip compression", @"Gzip compression export summary - within a sentence")]; + } else if ([exportOutputCompressionFormatPopupButton indexOfSelectedItem] == SPBzip2Compression) { + [optionsSummary addObject:NSLocalizedString(@"bzip2 compression", @"bzip2 compression export summary - within a sentence")]; + } + + [exportAdvancedOptionsViewLabelButton setTitle:[NSString stringWithFormat:@"%@ (%@)", NSLocalizedString(@"Advanced", @"Advanced options short title"), [optionsSummary componentsJoinedByString:@", "]]]; +} + +/** * Enables or disables the export button based on the state of various interface controls. * * @param uiStateDict A dictionary containing the state of various UI controls. diff --git a/Source/SPExportControllerDelegate.m b/Source/SPExportControllerDelegate.m index 8f4977a6..e8e9f1c6 100644 --- a/Source/SPExportControllerDelegate.m +++ b/Source/SPExportControllerDelegate.m @@ -25,11 +25,13 @@ #import "SPExportControllerDelegate.h" #import "SPExportFilenameUtilities.h" +#import "SPExportFileNameTokenObject.h" // Defined to suppress warnings @interface SPExportController (SPExportControllerPrivateAPI) - (void)_toggleExportButtonOnBackgroundThread; +- (void)_updateExportFormatInformation; - (void)_switchTab; @end @@ -54,6 +56,7 @@ [[tables objectAtIndex:rowIndex] replaceObjectAtIndex:[exportTableList columnWithIdentifier:[tableColumn identifier]] withObject:anObject]; [self _toggleExportButtonOnBackgroundThread]; + [self _updateExportFormatInformation]; } #pragma mark - @@ -80,6 +83,79 @@ } #pragma mark - +#pragma mark Token field delegate methods + +/** + * Use the default token style for matched tokens, plain text for all other text. + */ +- (NSTokenStyle)tokenField:(NSTokenField *)tokenField styleForRepresentedObject:(id)representedObject +{ + if ([representedObject isKindOfClass:[SPExportFileNameTokenObject class]]) return NSDefaultTokenStyle; + + return NSPlainTextTokenStyle; +} + +/** + * Take the default suggestion of new tokens - all untokenized text, as no tokenizing character is set - and + * split into many shorter tokens, using non-alphanumeric characters as (preserved) breaks. This preserves + * all supplied characters and allows tokens to be typed. + */ +- (NSArray *)tokenField:(NSTokenField *)tokenField shouldAddObjects:(NSArray *)tokens atIndex:(NSUInteger)index +{ + NSMutableArray *processedTokens = [NSMutableArray array]; + NSUInteger i, j; + NSCharacterSet *alphanumericSet = [NSCharacterSet alphanumericCharacterSet]; + + for (NSString *inputToken in tokens) { + j = 0; + for (i = 0; i < [inputToken length]; i++) { + if (![alphanumericSet characterIsMember:[inputToken characterAtIndex:i]]) { + if (i > j) { + [processedTokens addObject:[self tokenObjectForString:[inputToken substringWithRange:NSMakeRange(j, i-j)]]]; + } + [processedTokens addObject:[inputToken substringWithRange:NSMakeRange(i, 1)]]; + j = i+1; + } + } + if (j < i) { + [processedTokens addObject:[self tokenObjectForString:[inputToken substringWithRange:NSMakeRange(j, i-j)]]]; + } + } + + return processedTokens; +} + +- (NSString *)tokenField:(NSTokenField *)tokenField displayStringForRepresentedObject:(id)representedObject +{ + if ([representedObject isKindOfClass:[SPExportFileNameTokenObject class]]) { + return [(SPExportFileNameTokenObject *)representedObject tokenContent]; + } + + return representedObject; +} + +/** + * Return the editing string untouched - implementing this method prevents whitespace trimming. + */ +- (id)tokenField:(NSTokenField *)tokenField representedObjectForEditingString:(NSString *)editingString +{ + return editingString; +} + +/** + * During text entry into the token field, update the displayed filename and also + * trigger tokenization after a short delay. + */ +- (void)controlTextDidChange:(NSNotification *)aNotification +{ + if ([aNotification object] == exportCustomFilenameTokenField) { + [self updateDisplayedExportFilename]; + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(tokenizeCustomFilenameTokenField) object:nil]; + [self performSelector:@selector(tokenizeCustomFilenameTokenField) withObject:nil afterDelay:0.5]; + } +} + +#pragma mark - #pragma mark Combo box delegate methods - (void)comboBoxSelectionDidChange:(NSNotification *)notification diff --git a/Source/SPExportFileNameTokenObject.h b/Source/SPExportFileNameTokenObject.h new file mode 100644 index 00000000..24022745 --- /dev/null +++ b/Source/SPExportFileNameTokenObject.h @@ -0,0 +1,34 @@ +// +// $Id$ +// +// SPExportFileNameTokenObject.h +// sequel-pro +// +// Created by Rowan Beentje on 3rd May 2011. +// +// 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 <Cocoa/Cocoa.h> + + +@interface SPExportFileNameTokenObject : NSObject<NSCoding> { + NSString *tokenContent; +} + +@property(retain) NSString *tokenContent; + +@end diff --git a/Source/SPExportFileNameTokenObject.m b/Source/SPExportFileNameTokenObject.m new file mode 100644 index 00000000..9e589f79 --- /dev/null +++ b/Source/SPExportFileNameTokenObject.m @@ -0,0 +1,48 @@ +// +// $Id$ +// +// SPExportFileNameTokenObject.m +// sequel-pro +// +// Created by Rowan Beentje on 3rd May 2011. +// +// 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 "SPExportFileNameTokenObject.h" + + +@implementation SPExportFileNameTokenObject + +@synthesize tokenContent; + +#pragma mark - +#pragma mark NSCoding compatibility + +- (id)initWithCoder:(NSCoder *)decoder +{ + if ((self = [super init])) { + [self setTokenContent:[decoder decodeObjectForKey:@"TokenContent"]]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)encoder +{ + [encoder encodeObject:[self tokenContent] forKey:@"TokenContent"]; +} + +@end diff --git a/Source/SPExportFileUtilities.m b/Source/SPExportFileUtilities.m index efa824f8..2a490307 100644 --- a/Source/SPExportFileUtilities.m +++ b/Source/SPExportFileUtilities.m @@ -30,6 +30,17 @@ #import "SPDatabaseDocument.h" #import "SPCustomQuery.h" +typedef enum +{ + SPExportErrorCancelExport = 0, + SPExportErrorReplaceFiles = 1, + SPExportErrorSkipErrorFiles = 2 +} SPExportErrorChoice; + +@interface SPExportController (SPExportFileUtilitiesPrivateAPI) + - (void)_reopenExportSheet; +@end + @implementation SPExportController (SPExportFileUtilities) /** @@ -119,17 +130,18 @@ - (void)errorCreatingExportFileHandles:(NSArray *)files { // Get the number of files that already exists as well as couldn't be created because of other reasons - NSUInteger i = 0; + NSUInteger filesAlreadyExisting = 0; + NSUInteger filesFailed = 0; for (SPExportFile *file in files) { if ([file exportFileHandleStatus] == SPExportFileHandleExists) { - i++; - } + filesAlreadyExisting++; + // For file handles that we failed to create for some unknown reason, ignore them and remove any // exporters that are associated with them. - else if ([file exportFileHandleStatus] == SPExportFileHandleFailed) { - + } else if ([file exportFileHandleStatus] == SPExportFileHandleFailed) { + filesFailed++; for (SPExporter *exporter in exporters) { if ([[exporter exportOutputFile] isEqualTo:file]) { @@ -139,27 +151,72 @@ } } - // If all the files failed, show a simplified export dialog + NSAlert *alert = [[NSAlert alloc] init]; + [alert setAlertStyle:NSCriticalAlertStyle]; - // If only some of the files failed, show an export dialog with the option to ignore the failed files. + // If files failed because they already existed, show a OS-like dialog. + if (filesAlreadyExisting) { - // For single files, show a dialog very close to the OS dialog - if (i > 0) { - - NSAlert *alert = [NSAlert alertWithMessageText:NSLocalizedString(@"Error creating export files", @"export file handle creation error message") - defaultButton:NSLocalizedString(@"Ignore", @"ignore button") - alternateButton:NSLocalizedString(@"Overwrite", @"overwrite button") - otherButton:NSLocalizedString(@"Cancel", @"cancel button") - informativeTextWithFormat:NSLocalizedString(@"One or more errors occurred while attempting to create the export files. Those that failed to be created for unknown reasons will be ignored.\n\nHow would you like to proceed with the files that already exist at the location you have chosen to export to?", @"export file handle creation error informative message")]; - - [alert setAlertStyle:NSCriticalAlertStyle]; - - // Close the progress sheet - [NSApp endSheet:exportProgressWindow returnCode:0]; - [exportProgressWindow orderOut:self]; + // Set up a string for use if files had to be skipped. + NSString *additionalErrors = filesFailed ? NSLocalizedString(@"\n\n(In addition, one or more errors occurred while attempting to create the export files: %lu could not be created. These will be ignored.", @"Additional export file errors") : @""; + + if (filesAlreadyExisting == 1) { + [alert setMessageText:[NSString stringWithFormat:NSLocalizedString(@"“%@” already exists. Do you want to replace it?", @"Export file already exists message"), [[[files objectAtIndex:0] exportFilePath] lastPathComponent]]]; + [alert setInformativeText:[NSString stringWithFormat:@"%@%@", NSLocalizedString(@"A file with the same name already exists in the target folder. Replacing it will overwrite its current contents.", @"Export file already exists explanatory text"), additionalErrors]]; + } else if (filesAlreadyExisting == [exportFiles count]) { + [alert setMessageText:[NSString stringWithFormat:NSLocalizedString(@"All the export files already exist. Do you want to replace them?", @"All export files already exist message")]]; + [alert setInformativeText:[NSString stringWithFormat:@"%@%@", NSLocalizedString(@"Files with the same names already exist in the target folder. Replacing them will overwrite their current contents.", @"All export files already exist explanatory text"), additionalErrors]]; + } else { + [alert setMessageText:[NSString stringWithFormat:NSLocalizedString(@"%lu files already exist. Do you want to replace them?", @"Export file already exists message"), filesAlreadyExisting]]; + [alert setInformativeText:[NSString stringWithFormat:@"%@%@", [NSString stringWithFormat:NSLocalizedString(@"%lu files with the same names already exist in the target folder. Replacing them will overwrite their current contents.", @"Some export files already exist explanatory text"), filesAlreadyExisting], additionalErrors]]; + } + + [alert addButtonWithTitle:NSLocalizedString(@"Replace", @"Replace button")]; + [[[alert buttons] objectAtIndex:0] setTag:SPExportErrorReplaceFiles]; + [[[alert buttons] objectAtIndex:0] setKeyEquivalent:@"r"]; + [[[alert buttons] objectAtIndex:0] setKeyEquivalentModifierMask:NSCommandKeyMask]; + + [alert addButtonWithTitle:NSLocalizedString(@"Cancel", @"cancel button")]; + [[[alert buttons] objectAtIndex:1] setTag:SPExportErrorCancelExport]; + [[[alert buttons] objectAtIndex:1] setKeyEquivalent:@"\r"]; - [alert beginSheetModalForWindow:[tableDocumentInstance parentWindow] modalDelegate:self didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:) contextInfo:files]; + if ((filesAlreadyExisting + filesFailed) != [exportFiles count]) { + [alert addButtonWithTitle:NSLocalizedString(@"Skip existing", @"skip existing button")]; + [[[alert buttons] objectAtIndex:2] setTag:SPExportErrorSkipErrorFiles]; + [[[alert buttons] objectAtIndex:2] setKeyEquivalent:@"s"]; + [[[alert buttons] objectAtIndex:2] setKeyEquivalentModifierMask:NSCommandKeyMask]; + } + + // If one or multiple files failed, but only due to unhandled errors, show a short dialog + } else { + if (filesFailed == 1) { + [alert setMessageText:[NSString stringWithFormat:NSLocalizedString(@"“%@” could not be created", @"Export file creation error title"), [[[files objectAtIndex:0] exportFilePath] lastPathComponent]]]; + [alert setInformativeText:NSLocalizedString(@"An unhandled error occurred when attempting to create the export file. Please check the details and try again.", @"Export file creation error explanatory text")]; + } else if (filesFailed == [exportFiles count]) { + [alert setMessageText:[NSString stringWithFormat:NSLocalizedString(@"No files could be created", @"All export files creation error title")]]; + [alert setInformativeText:NSLocalizedString(@"An unhandled error occurred when attempting to create each of the export files. Please check the details and try again.", @"All export files creation error explanatory text")]; + } else { + [alert setMessageText:[NSString stringWithFormat:NSLocalizedString(@"%lu files could not be created", @"Export files creation error title"), filesFailed]]; + [alert setInformativeText:[NSString stringWithFormat:NSLocalizedString(@"An unhandled error occurred when attempting to create %lu of the export files. Please check the details and try again.", @"Export files creation error explanatory text"), filesFailed]]; + } + + + [alert addButtonWithTitle:NSLocalizedString(@"Cancel", @"cancel button")]; + [[[alert buttons] objectAtIndex:0] setTag:SPExportErrorCancelExport]; + + if (filesFailed != [exportFiles count]) { + [alert addButtonWithTitle:NSLocalizedString(@"Skip problems", @"skip problems button")]; + [[[alert buttons] objectAtIndex:1] setTag:SPExportErrorSkipErrorFiles]; + [[[alert buttons] objectAtIndex:1] setKeyEquivalent:@"s"]; + [[[alert buttons] objectAtIndex:1] setKeyEquivalentModifierMask:NSCommandKeyMask]; + } } + + // Close the progress sheet + [NSApp endSheet:exportProgressWindow returnCode:0]; + [exportProgressWindow orderOut:self]; + + [alert beginSheetModalForWindow:[tableDocumentInstance parentWindow] modalDelegate:self didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:) contextInfo:files]; } /** @@ -170,14 +227,17 @@ NSArray *files = (NSArray *)contextInfo; // Ignore the files that exist and remove the associated exporters - if (returnCode == NSAlertDefaultReturn) { + if (returnCode == SPExportErrorSkipErrorFiles) { - for (SPExportFile *file in files) - { - for (SPExporter *exporter in exporters) - { + for (SPExportFile *file in files) { + + // Use a numerically controlled loop to avoid mutating the collection while enumerating + NSUInteger i; + for (i = 0; i < [exporters count]; i++) { + SPExporter *exporter = [exporters objectAtIndex:i]; if ([[exporter exportOutputFile] isEqualTo:file]) { - [exporters removeObject:exporter]; + [exporters removeObjectAtIndex:i]; + i--; } } } @@ -187,6 +247,9 @@ // If we're now left with no exporters, cancel the export operation if ([exporters count] == 0) { [exportFiles removeAllObjects]; + + // Trigger restoration of the export interface + [self performSelector:@selector(_reopenExportSheet) withObject:nil afterDelay:0.1]; } else { // Start the export after a short delay to give this sheet a chance to close @@ -194,7 +257,7 @@ } } // Overwrite the files and continue - else if (returnCode == NSAlertAlternateReturn) { + else if (returnCode == SPExportErrorReplaceFiles) { for (SPExportFile *file in files) { @@ -220,7 +283,7 @@ } // Cancel the entire export operation - else if (returnCode == NSAlertOtherReturn) { + else if (returnCode == SPExportErrorCancelExport) { // Loop the cached export files and remove those we've already created for (SPExportFile *file in exportFiles) @@ -233,7 +296,26 @@ // Finally get rid of all the exporters and files [exportFiles removeAllObjects]; [exporters removeAllObjects]; + + // Trigger restoration of the export interface + [self performSelector:@selector(_reopenExportSheet) withObject:nil afterDelay:0.1]; } } @end + +@implementation SPExportController (SPExportFileUtilitiesPrivateAPI) + +/** + * Re-open the export sheet without resetting the interface - for use on error. + */ +- (void)_reopenExportSheet +{ + [NSApp beginSheet:[self window] + modalForWindow:[tableDocumentInstance parentWindow] + modalDelegate:self + didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) + contextInfo:nil]; +} + +@end diff --git a/Source/SPExportFilenameUtilities.h b/Source/SPExportFilenameUtilities.h index 7bbcea25..6007f49e 100644 --- a/Source/SPExportFilenameUtilities.h +++ b/Source/SPExportFilenameUtilities.h @@ -36,8 +36,10 @@ - (void)updateDisplayedExportFilename; - (void)updateAvailableExportFilenameTokens; +- (id)tokenObjectForString:(NSString *)stringToTokenize; +- (void)tokenizeCustomFilenameTokenField; - (NSString *)generateDefaultExportFilename; - (NSString *)currentDefaultExportFileExtension; -- (NSString *)expandCustomFilenameFormatFromString:(NSString *)format usingTableName:(NSString *)table; +- (NSString *)expandCustomFilenameFormatUsingTableName:(NSString *)table; @end diff --git a/Source/SPExportFilenameUtilities.m b/Source/SPExportFilenameUtilities.m index a485eb6b..a4330e36 100644 --- a/Source/SPExportFilenameUtilities.m +++ b/Source/SPExportFilenameUtilities.m @@ -26,6 +26,7 @@ #import "SPExportFilenameUtilities.h" #import "SPTablesList.h" #import "SPDatabaseViewController.h" +#import "SPExportFileNameTokenObject.h" @implementation SPExportController (SPExportFilenameUtilities) @@ -41,9 +42,9 @@ // Get the current export file extension NSString *extension = [self currentDefaultExportFileExtension]; - filename = [self expandCustomFilenameFormatFromString:[exportCustomFilenameTokenField stringValue] usingTableName:[[tablesListInstance tables] objectAtIndex:1]]; + filename = [self expandCustomFilenameFormatUsingTableName:[[tablesListInstance tables] objectAtIndex:1]]; - if ([extension length] > 0) filename = [filename stringByAppendingPathExtension:extension]; + if (![[filename pathExtension] length] && [extension length] > 0) filename = [filename stringByAppendingPathExtension:extension]; } else { filename = [self generateDefaultExportFilename]; @@ -57,7 +58,81 @@ */ - (void)updateAvailableExportFilenameTokens { - [exportCustomFilenameTokensField setStringValue:((exportSource == SPQueryExport) || (exportType == SPDotExport)) ? NSLocalizedString(@"host,database,date,time", @"custom export filename tokens without table") : NSLocalizedString(@"host,database,table,date,time", @"default custom export filename tokens")]; + [exportCustomFilenameTokensField setStringValue:((exportSource == SPQueryExport) || (exportType == SPDotExport)) ? NSLocalizedString(@"host,database,date,year,month,day,time", @"custom export filename tokens without table") : NSLocalizedString(@"host,database,table,date,year,month,day,time", @"default custom export filename tokens")]; +} + +/** + * Take a supplied string and return the token for it - a SPExportFileNameTokenObject if the token + * has been recognized, or the supplied NSString if unmatched. + */ +- (id)tokenObjectForString:(NSString *)stringToTokenize +{ + if ([[exportCustomFilenameTokensField objectValue] containsObject:stringToTokenize]) { + SPExportFileNameTokenObject *newToken = [[SPExportFileNameTokenObject alloc] init]; + [newToken setTokenContent:stringToTokenize]; + return [newToken autorelease]; + } + + return stringToTokenize; +} + +/** + * Tokenize the filename field. + * This is called on a delay after text entry to update the tokens during text entry. + * There's no API to perform tokenizing, but the same result can be achieved by using the return key; + * however, this only works if the cursor is after text, not after a token. + */ +- (void)tokenizeCustomFilenameTokenField +{ + NSCharacterSet *nonAlphanumericSet = [[NSCharacterSet alphanumericCharacterSet] invertedSet]; + NSArray *validTokens = [exportCustomFilenameTokensField objectValue]; + + if ([exportCustomFilenameTokenField currentEditor] == nil) return; + + NSRange selectedRange = [[exportCustomFilenameTokenField currentEditor] selectedRange]; + if (selectedRange.location == NSNotFound) return; + if (selectedRange.length > 0) return; + + // Retrieve the object value of the token field. This consists of plain text and recognised tokens interspersed. + NSArray *representedObjects = [exportCustomFilenameTokenField objectValue]; + + // Walk through the strings - not the tokens - and determine whether any need tokenizing + BOOL tokenizingRequired = NO; + for (id representedObject in representedObjects) { + if ([representedObject isKindOfClass:[SPExportFileNameTokenObject class]]) continue; + NSArray *tokenParts = [representedObject componentsSeparatedByCharactersInSet:nonAlphanumericSet]; + for (NSString *tokenPart in tokenParts) { + if ([validTokens containsObject:tokenPart]) { + tokenizingRequired = YES; + break; + } + } + } + + // If no tokenizing is required, don't process any further. + if (!tokenizingRequired) return; + + // Detect where the cursor is currently located. If it's at the end of a token, also return - + // or the enter key would result in closing the sheet. + NSUInteger stringPosition = 0; + for (id representedObject in representedObjects) { + if ([representedObject isKindOfClass:[SPExportFileNameTokenObject class]]) { + stringPosition++; + } else { + stringPosition += [(NSString *)representedObject length]; + } + if (selectedRange.location <= stringPosition) { + if ([representedObject isKindOfClass:[SPExportFileNameTokenObject class]]) return; + break; + } + } + + // All conditions met - synthesize the return key to trigger tokenization. + NSEvent *tokenizingEvent = [NSEvent keyEventWithType:NSKeyDown location:NSMakePoint(0,0) modifierFlags:0 timestamp:0 windowNumber:[[exportCustomFilenameTokenField window] windowNumber] context:[NSGraphicsContext currentContext] characters:nil charactersIgnoringModifiers:nil isARepeat:NO keyCode:0x24]; + [[NSApplication sharedApplication] postEvent:tokenizingEvent atStart:NO]; + + // Update the filename preview + [self updateDisplayedExportFilename]; } /** @@ -129,57 +204,59 @@ /** * Expands the custom filename format based on the selected tokens. + * Uses the current custom filename field as a data source. * - * @param format The filename format that is to be expanded. * @param table A table name to be used within the expanded filename. * * @return The expanded filename. */ -- (NSString *)expandCustomFilenameFormatFromString:(NSString *)format usingTableName:(NSString *)table +- (NSString *)expandCustomFilenameFormatUsingTableName:(NSString *)table { - NSMutableString *string = [NSMutableString stringWithString:format]; - - NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; + NSMutableString *string = [NSMutableString string]; + NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; [dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4]; - - [dateFormatter setDateStyle:NSDateFormatterShortStyle]; - [dateFormatter setTimeStyle:NSDateFormatterNoStyle]; - - [string replaceOccurrencesOfString:NSLocalizedString(@"host", @"export filename host token") - withString:[tableDocumentInstance host] - options:NSLiteralSearch - range:NSMakeRange(0, [string length])]; - - [string replaceOccurrencesOfString:NSLocalizedString(@"database", @"export filename database token") - withString:[tableDocumentInstance database] - options:NSLiteralSearch - range:NSMakeRange(0, [string length])]; - - [string replaceOccurrencesOfString:NSLocalizedString(@"table", @"table") - withString:(table) ? table : @"" - options:NSLiteralSearch - range:NSMakeRange(0, [string length])]; - - [string replaceOccurrencesOfString:NSLocalizedString(@"date", @"export filename date token") - withString:[dateFormatter stringFromDate:[NSDate date]] - options:NSLiteralSearch - range:NSMakeRange(0, [string length])]; - - [dateFormatter setDateStyle:NSDateFormatterNoStyle]; - [dateFormatter setTimeStyle:NSDateFormatterShortStyle]; - - [string replaceOccurrencesOfString:NSLocalizedString(@"time", @"export filename time token") - 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])]; - + + // Walk through the token field, appending token replacements or strings + NSArray *representedFilenameParts = [exportCustomFilenameTokenField objectValue]; + for (id filenamePart in representedFilenameParts) { + if ([filenamePart isKindOfClass:[SPExportFileNameTokenObject class]]) { + NSString *tokenContent = [filenamePart tokenContent]; + + if ([tokenContent isEqualToString:NSLocalizedString(@"host", @"export filename host token")]) { + [string appendString:[tableDocumentInstance host]]; + + } else if ([tokenContent isEqualToString:NSLocalizedString(@"database", @"export filename database token")]) { + [string appendString:[tableDocumentInstance database]]; + + } else if ([tokenContent isEqualToString:NSLocalizedString(@"table", @"table")]) { + [string appendString:(table) ? table : @""]; + + } else if ([tokenContent isEqualToString:NSLocalizedString(@"date", @"export filename date token")]) { + [dateFormatter setDateStyle:NSDateFormatterShortStyle]; + [dateFormatter setTimeStyle:NSDateFormatterNoStyle]; + [string appendString:[dateFormatter stringFromDate:[NSDate date]]]; + + } else if ([tokenContent isEqualToString:NSLocalizedString(@"year", @"export filename date token")]) { + [string appendString:[[NSDate date] descriptionWithCalendarFormat:@"%Y" timeZone:nil locale:nil]]; + + } else if ([tokenContent isEqualToString:NSLocalizedString(@"month", @"export filename date token")]) { + [string appendString:[[NSDate date] descriptionWithCalendarFormat:@"%m" timeZone:nil locale:nil]]; + + } else if ([tokenContent isEqualToString:NSLocalizedString(@"day", @"export filename date token")]) { + [string appendString:[[NSDate date] descriptionWithCalendarFormat:@"%d" timeZone:nil locale:nil]]; + + } else if ([tokenContent isEqualToString:NSLocalizedString(@"time", @"export filename time token")]) { + [dateFormatter setDateStyle:NSDateFormatterNoStyle]; + [dateFormatter setTimeStyle:NSDateFormatterShortStyle]; + [string appendString:[dateFormatter stringFromDate:[NSDate date]]]; + + } + } else { + [string appendString:filenamePart]; + } + } + // Replace colons with hyphens [string replaceOccurrencesOfString:@":" withString:@"-" @@ -193,7 +270,10 @@ range:NSMakeRange(0, [string length])]; [dateFormatter release]; - + + // Don't allow empty strings - if an empty string resulted, revert to the default string + if (![string length]) [string setString:[self generateDefaultExportFilename]]; + return string; } diff --git a/Source/SPExportInitializer.m b/Source/SPExportInitializer.m index 32ce7ff5..f9d23b63 100644 --- a/Source/SPExportInitializer.m +++ b/Source/SPExportInitializer.m @@ -42,6 +42,7 @@ #import "SPExportFile.h" #import "SPExportFileUtilities.h" #import "SPExportFilenameUtilities.h" +#import "SPExportFileNameTokenObject.h" @implementation SPExportController (SPExportInitializer) @@ -209,9 +210,16 @@ // If the user has selected to only export to a single file or this is a filtered or custom query // export, create the single file now and assign it to all subsequently created exporters. if ((![self exportToMultipleFiles]) || (exportSource == SPFilteredExport) || (exportSource == SPQueryExport)) { + NSString *selectedTableName = nil; + if (exportSource == SPTableExport && [exportTables count] == 1) selectedTableName = [exportTables objectAtIndex:0]; + + [exportFilename setString:(createCustomFilename) ? [self expandCustomFilenameFormatUsingTableName:selectedTableName] : [self generateDefaultExportFilename]]; + + // Only append the extension if necessary + if (![[exportFilename pathExtension] length]) { + [exportFilename setString:[exportFilename stringByAppendingPathExtension:[self currentDefaultExportFileExtension]]]; + } - [exportFilename setString:(createCustomFilename) ? [self expandCustomFilenameFormatFromString:[exportCustomFilenameTokenField stringValue] usingTableName:nil] : [self generateDefaultExportFilename]]; - singleExportFile = [SPExportFile exportFileAtPath:[[exportPathField stringValue] stringByAppendingPathComponent:exportFilename]]; } @@ -280,7 +288,7 @@ [sqlExporter setSqlExportTables:exportTables]; // Create custom filename if required - [exportFilename setString:(createCustomFilename) ? [self expandCustomFilenameFormatFromString:[exportCustomFilenameTokenField stringValue] usingTableName:nil] : [self generateDefaultExportFilename]]; + [exportFilename setString:(createCustomFilename) ? [self expandCustomFilenameFormatUsingTableName:nil] : [self generateDefaultExportFilename]]; // Only append the extension if necessary if (![[exportFilename pathExtension] length]) { @@ -305,9 +313,16 @@ // If the user has selected to only export to a single file or this is a filtered or custom query // export, create the single file now and assign it to all subsequently created exporters. if ((![self exportToMultipleFiles]) || (exportSource == SPFilteredExport) || (exportSource == SPQueryExport)) { + NSString *selectedTableName = nil; + if (exportSource == SPTableExport && [exportTables count] == 1) selectedTableName = [exportTables objectAtIndex:0]; - [exportFilename setString:(createCustomFilename) ? [self expandCustomFilenameFormatFromString:[exportCustomFilenameTokenField stringValue] usingTableName:nil] : [self generateDefaultExportFilename]]; + [exportFilename setString:(createCustomFilename) ? [self expandCustomFilenameFormatUsingTableName:selectedTableName] : [self generateDefaultExportFilename]]; + // Only append the extension if necessary + if (![[exportFilename pathExtension] length]) { + [exportFilename setString:[exportFilename stringByAppendingPathExtension:[self currentDefaultExportFileExtension]]]; + } + singleExportFile = [SPExportFile exportFileAtPath:[[exportPathField stringValue] stringByAppendingPathComponent:exportFilename]]; } @@ -374,13 +389,16 @@ // Create custom filename if required if (createCustomFilename) { - [exportFilename setString:[self expandCustomFilenameFormatFromString:[exportCustomFilenameTokenField stringValue] usingTableName:nil]]; + [exportFilename setString:[self expandCustomFilenameFormatUsingTableName:nil]]; } else { [exportFilename setString:[tableDocumentInstance database]]; } - [exportFilename setString:[exportFilename stringByAppendingPathExtension:[self currentDefaultExportFileExtension]]]; + // Only append the extension if necessary + if (![[exportFilename pathExtension] length]) { + [exportFilename setString:[exportFilename stringByAppendingPathExtension:[self currentDefaultExportFileExtension]]]; + } file = [SPExportFile exportFileAtPath:[[exportPathField stringValue] stringByAppendingPathComponent:exportFilename]]; @@ -468,19 +486,27 @@ if (createCustomFilename) { // Create custom filename based on the selected format - [exportFilename setString:[self expandCustomFilenameFormatFromString:[exportCustomFilenameTokenField stringValue] usingTableName:table]]; + [exportFilename setString:[self expandCustomFilenameFormatUsingTableName:table]]; // If the user chose to use a custom filename format and we exporting to multiple files, make // sure the table name is included to ensure the output files are unique. if (exportTableCount > 1) { - [exportFilename setString:([[exportCustomFilenameTokenField stringValue] rangeOfString:@"table" options:NSLiteralSearch].location == NSNotFound) ? [exportFilename stringByAppendingFormat:@"_%@", table] : exportFilename]; + BOOL tableNameInTokens = NO; + NSArray *representedObjects = [exportCustomFilenameTokenField objectValue]; + for (id representedObject in representedObjects) { + if ([representedObject isKindOfClass:[SPExportFileNameTokenObject class]] && [[representedObject tokenContent] isEqualToString:NSLocalizedString(@"table", @"table")]) tableNameInTokens = YES; + } + [exportFilename setString:(tableNameInTokens ? exportFilename : [exportFilename stringByAppendingFormat:@"_%@", table])]; } } else { [exportFilename setString:(dataArray) ? [tableDocumentInstance database] : table]; } - [exportFilename setString:[exportFilename stringByAppendingPathExtension:[self currentDefaultExportFileExtension]]]; + // Only append the extension if necessary + if (![[exportFilename pathExtension] length]) { + [exportFilename setString:[exportFilename stringByAppendingPathExtension:[self currentDefaultExportFileExtension]]]; + } SPExportFile *file = [SPExportFile exportFileAtPath:[[exportPathField stringValue] stringByAppendingPathComponent:exportFilename]]; @@ -522,19 +548,27 @@ if (createCustomFilename) { // Create custom filename based on the selected format - [exportFilename setString:[self expandCustomFilenameFormatFromString:[exportCustomFilenameTokenField stringValue] usingTableName:table]]; + [exportFilename setString:[self expandCustomFilenameFormatUsingTableName:table]]; // If the user chose to use a custom filename format and we exporting to multiple files, make // sure the table name is included to ensure the output files are unique. if (exportTableCount > 1) { - [exportFilename setString:([[exportCustomFilenameTokenField stringValue] rangeOfString:@"table" options:NSLiteralSearch].location == NSNotFound) ? [exportFilename stringByAppendingFormat:@"_%@", table] : exportFilename]; + BOOL tableNameInTokens = NO; + NSArray *representedObjects = [exportCustomFilenameTokenField objectValue]; + for (id representedObject in representedObjects) { + if ([representedObject isKindOfClass:[SPExportFileNameTokenObject class]] && [[representedObject tokenContent] isEqualToString:NSLocalizedString(@"table", @"table")]) tableNameInTokens = YES; + } + [exportFilename setString:(tableNameInTokens ? exportFilename : [exportFilename stringByAppendingFormat:@"_%@", table])]; } } else { [exportFilename setString:(dataArray) ? [tableDocumentInstance database] : table]; } - [exportFilename setString:[exportFilename stringByAppendingPathExtension:[self currentDefaultExportFileExtension]]]; + // Only append the extension if necessary + if (![[exportFilename pathExtension] length]) { + [exportFilename setString:[exportFilename stringByAppendingPathExtension:[self currentDefaultExportFileExtension]]]; + } SPExportFile *file = [SPExportFile exportFileAtPath:[[exportPathField stringValue] stringByAppendingPathComponent:exportFilename]]; diff --git a/Source/SPSQLExporter.m b/Source/SPSQLExporter.m index 3dfed305..4c7903fc 100644 --- a/Source/SPSQLExporter.m +++ b/Source/SPSQLExporter.m @@ -302,15 +302,16 @@ rowCount = [NSArrayObjectAtIndex(rowArray, 0) integerValue]; - // Set up a result set in streaming mode - streamingResult = [[connection streamingQueryString:[NSString stringWithFormat:@"SELECT * FROM %@", [tableName backtickQuotedString]] useLowMemoryBlockingStreaming:([self exportUsingLowMemoryBlockingStreaming])] retain]; - - NSArray *fieldNames = [streamingResult fetchFieldNames]; - - // Inform the delegate that we are about to start writing data for the current table - [delegate performSelectorOnMainThread:@selector(sqlExportProcessWillBeginWritingData:) withObject:self waitUntilDone:NO]; - if (rowCount) { + + // Set up a result set in streaming mode + streamingResult = [[connection streamingQueryString:[NSString stringWithFormat:@"SELECT * FROM %@", [tableName backtickQuotedString]] useLowMemoryBlockingStreaming:([self exportUsingLowMemoryBlockingStreaming])] retain]; + + NSArray *fieldNames = [streamingResult fetchFieldNames]; + + // Inform the delegate that we are about to start writing data for the current table + [delegate performSelectorOnMainThread:@selector(sqlExportProcessWillBeginWritingData:) withObject:self waitUntilDone:NO]; + queryLength = 0; // Lock the table for writing and disable keys if supported @@ -474,6 +475,9 @@ // Drain the autorelease pool [sqlExportPool release]; + + // Release the result set + [streamingResult release]; } if ([connection queryErrored]) { @@ -484,9 +488,6 @@ dataUsingEncoding:NSUTF8StringEncoding]]; } } - - // Release the result set - [streamingResult release]; } queryResult = [connection queryString:[NSString stringWithFormat:@"/*!50003 SHOW TRIGGERS WHERE `Table` = %@ */;", [tableName tickQuotedString]]]; diff --git a/sequel-pro.xcodeproj/project.pbxproj b/sequel-pro.xcodeproj/project.pbxproj index 09c0af68..6ae3a9e1 100644 --- a/sequel-pro.xcodeproj/project.pbxproj +++ b/sequel-pro.xcodeproj/project.pbxproj @@ -217,6 +217,7 @@ 5822D3091061833C00CE2157 /* SPCSVParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 5822D3081061833C00CE2157 /* SPCSVParser.m */; }; 582A01E9107C0C170027D42B /* SPNotLoaded.m in Sources */ = {isa = PBXBuildFile; fileRef = 582A01E8107C0C170027D42B /* SPNotLoaded.m */; }; 582A05A9108A5CCF0027D42B /* ProgressIndicatorLayer.xib in Resources */ = {isa = PBXBuildFile; fileRef = 582A05A7108A5CCF0027D42B /* ProgressIndicatorLayer.xib */; }; + 582F02311370B52600B30621 /* SPExportFileNameTokenObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 582F02301370B52600B30621 /* SPExportFileNameTokenObject.m */; }; 583B77D4103870C800B21F7E /* MCPStreamingResult.m in Sources */ = {isa = PBXBuildFile; fileRef = 583B779810386B0200B21F7E /* MCPStreamingResult.m */; }; 583CA21512EC8B2200C9E763 /* SPWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = 583CA21412EC8B2200C9E763 /* SPWindow.m */; }; 583CE52D11725642008F148E /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 583CE52C11725642008F148E /* libz.dylib */; }; @@ -910,6 +911,8 @@ 582A01E7107C0C170027D42B /* SPNotLoaded.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPNotLoaded.h; sourceTree = "<group>"; }; 582A01E8107C0C170027D42B /* SPNotLoaded.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPNotLoaded.m; sourceTree = "<group>"; }; 582A05A8108A5CCF0027D42B /* English */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = English; path = Interfaces/English.lproj/ProgressIndicatorLayer.xib; sourceTree = "<group>"; }; + 582F022F1370B52600B30621 /* SPExportFileNameTokenObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPExportFileNameTokenObject.h; sourceTree = "<group>"; }; + 582F02301370B52600B30621 /* SPExportFileNameTokenObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPExportFileNameTokenObject.m; sourceTree = "<group>"; }; 583B779710386B0200B21F7E /* MCPStreamingResult.h */ = {isa = PBXFileReference; explicitFileType = sourcecode.c.h; fileEncoding = 4; path = MCPStreamingResult.h; sourceTree = "<group>"; }; 583B779810386B0200B21F7E /* MCPStreamingResult.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MCPStreamingResult.m; sourceTree = "<group>"; }; 583CA21312EC8B2200C9E763 /* SPWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPWindow.h; sourceTree = "<group>"; }; @@ -2216,6 +2219,8 @@ 17AF787A11FC41C00073D043 /* SPExportFilenameUtilities.m */, 175EC63312733B36009A7C0F /* SPExportControllerDelegate.h */, 175EC63412733B36009A7C0F /* SPExportControllerDelegate.m */, + 582F022F1370B52600B30621 /* SPExportFileNameTokenObject.h */, + 582F02301370B52600B30621 /* SPExportFileNameTokenObject.m */, 17F90E451210B41100274C98 /* Model */, 173C836C11AAD24300B8B084 /* Exporters */, 173C837C11AAD2C500B8B084 /* Delegate Protocols */, @@ -3199,6 +3204,7 @@ BC5750D512A6233900911BA2 /* SPActivityTextFieldCell.m in Sources */, BC0ED3DA12A9196C00088461 /* SPChooseMenuItemDialog.m in Sources */, 583CA21512EC8B2200C9E763 /* SPWindow.m in Sources */, + 582F02311370B52600B30621 /* SPExportFileNameTokenObject.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; |