diff options
Diffstat (limited to 'Source')
46 files changed, 1010 insertions, 334 deletions
diff --git a/Source/SPCSVExporter.m b/Source/SPCSVExporter.m index e5d945c3..651f0ffd 100644 --- a/Source/SPCSVExporter.m +++ b/Source/SPCSVExporter.m @@ -95,8 +95,7 @@ } // Check that we have all the required info before starting the export - if ((![self csvOutputFieldNames]) || - (![self csvFieldSeparatorString]) || + if ((![self csvFieldSeparatorString]) || (![self csvEscapeString]) || (![self csvLineEndingString])) { @@ -123,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 d1329758..4ff7ee31 100644 --- a/Source/SPConstants.h +++ b/Source/SPConstants.h @@ -237,6 +237,8 @@ extern NSString *SPDefaultMonospacedFontName; extern NSString *SPDefaultPasteboardDragType; extern NSString *SPFavoritesPasteboardDragType; extern NSString *SPContentFilterPasteboardDragType; +extern NSString *SPNavigatorPasteboardDragType; +extern NSString *SPNavigatorTableDataPasteboardDragType; // File extensions extern NSString *SPFileExtensionDefault; @@ -284,6 +286,7 @@ extern NSString *SPNullValue; extern NSString *SPGlobalResultTableFont; extern NSString *SPFilterTableDefaultOperator; extern NSString *SPFilterTableDefaultOperatorLastItems; +extern NSString *SPAlphabeticalTableSorting; // Favorites Prefpane extern NSString *SPFavorites; @@ -342,6 +345,7 @@ extern NSString *SPTableInformationPanelCollapsed; extern NSString *SPTableColumnWidths; extern NSString *SPProcessListTableColumnWidths; extern NSString *SPProcessListShowProcessID; +extern NSString *SPProcessListShowFullProcessList; extern NSString *SPProcessListEnableAutoRefresh; extern NSString *SPProcessListAutoRrefreshInterval; extern NSString *SPFavoritesSortedBy; @@ -363,6 +367,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 d1b4bc45..f8e3fe4f 100644 --- a/Source/SPConstants.m +++ b/Source/SPConstants.m @@ -38,6 +38,8 @@ NSString *SPDefaultMonospacedFontName = @"Monaco"; NSString *SPDefaultPasteboardDragType = @"SequelProPasteboard"; NSString *SPFavoritesPasteboardDragType = @"SPFavoritesPasteboard"; NSString *SPContentFilterPasteboardDragType = @"SPContentFilterPasteboard"; +NSString *SPNavigatorPasteboardDragType = @"SPNavigatorPasteboardDragType"; +NSString *SPNavigatorTableDataPasteboardDragType = @"SPNavigatorTableDataPasteboardDragType"; // File extensions NSString *SPFileExtensionDefault = @"spf"; @@ -90,6 +92,7 @@ NSString *SPNullValue = @"SPNullValue"; NSString *SPGlobalResultTableFont = @"GlobalResultTableFont"; NSString *SPFilterTableDefaultOperator = @"FilterTableDefaultOperator"; NSString *SPFilterTableDefaultOperatorLastItems = @"FilterTableDefaultOperatorLastItems"; +NSString *SPAlphabeticalTableSorting = @"AlphabeticalTableSorting"; // Favorites Prefpane NSString *SPFavorites = @"favorites"; @@ -148,6 +151,7 @@ NSString *SPTableInformationPanelCollapsed = @"TableInformationPanelCollap NSString *SPTableColumnWidths = @"tableColumnWidths"; NSString *SPProcessListTableColumnWidths = @"ProcessListTableColumnWidths"; NSString *SPProcessListShowProcessID = @"ProcessListShowProcessID"; +NSString *SPProcessListShowFullProcessList = @"ProcessListShowFullProcessList"; NSString *SPProcessListEnableAutoRefresh = @"ProcessListEnableAutoRefresh"; NSString *SPProcessListAutoRrefreshInterval = @"ProcessListAutoRrefreshInterval"; NSString *SPFavoritesSortedBy = @"FavoritesSortedBy"; @@ -169,6 +173,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/SPContentFilterManager.m b/Source/SPContentFilterManager.m index 7b24d6e1..57ff7dac 100644 --- a/Source/SPContentFilterManager.m +++ b/Source/SPContentFilterManager.m @@ -233,10 +233,10 @@ // Duplicate a selected filter if sender == self if(sender == self) - filter = [NSMutableDictionary dictionaryWithObjects:[NSArray arrayWithObjects:[[contentFilterNameTextField stringValue] stringByAppendingFormat:@" Copy"], [contentFilterTextView string], nil] forKeys:[NSArray arrayWithObjects:@"MenuLabel", @"Clause", nil]]; + filter = [NSMutableDictionary dictionaryWithObjects:[NSArray arrayWithObjects:[NSString stringWithFormat:NSLocalizedString(@"%@ Copy",@"Content Filter Manager : Initial name of copied filter"),[contentFilterNameTextField stringValue]], [contentFilterTextView string], nil] forKeys:[NSArray arrayWithObjects:@"MenuLabel", @"Clause", nil]]; // Add a new filter else - filter = [NSMutableDictionary dictionaryWithObjects:[NSArray arrayWithObjects:@"New Filter", @"", @"", nil] forKeys:[NSArray arrayWithObjects:@"MenuLabel", @"Clause", @"ConjunctionLabel", nil]]; + filter = [NSMutableDictionary dictionaryWithObjects:[NSArray arrayWithObjects:NSLocalizedString(@"New Filter",@"Content Filter Manager : Initial name for new filter"), @"", @"", nil] forKeys:[NSArray arrayWithObjects:@"MenuLabel", @"Clause", @"ConjunctionLabel", nil]]; if([contentFilterTableView numberOfSelectedRows] > 0) { insertIndex = [[contentFilterTableView selectedRowIndexes] lastIndex]+1; diff --git a/Source/SPCustomQuery.h b/Source/SPCustomQuery.h index 8a1ad793..f001d364 100644 --- a/Source/SPCustomQuery.h +++ b/Source/SPCustomQuery.h @@ -52,10 +52,9 @@ @class SPCopyTable, SPQueryFavoriteManager, SPDataStorage, NSSplitView, SPFieldEditorController; #endif -#ifndef SP_REFACTOR -@interface SPCustomQuery : NSObject -#else -@interface SPCustomQuery : NSObject <NSTableViewDataSource, NSWindowDelegate, NSTableViewDelegate> +@interface SPCustomQuery : NSObject +#ifdef SP_REFACTOR +<NSTableViewDataSource, NSWindowDelegate, NSTableViewDelegate> #endif { IBOutlet id tableDocumentInstance; diff --git a/Source/SPCustomQuery.m b/Source/SPCustomQuery.m index a67ec5f3..26810323 100644 --- a/Source/SPCustomQuery.m +++ b/Source/SPCustomQuery.m @@ -1980,11 +1980,13 @@ if (aTableView == customQueryView) { NSDictionary *columnDefinition; + NSString *columnTypeGroup; // Retrieve the column defintion for(id c in cqColumnDefinition) { if([[c objectForKey:@"datacolumnindex"] isEqualToNumber:[aTableColumn identifier]]) { columnDefinition = [NSDictionary dictionaryWithDictionary:c]; + columnTypeGroup = [columnDefinition objectForKey:@"typegrouping"]; break; } } @@ -2018,13 +2020,16 @@ } else { if ( [[anObject description] isEqualToString:@"CURRENT_TIMESTAMP"] ) { newObject = @"CURRENT_TIMESTAMP"; - } else if([anObject isEqualToString:[prefs stringForKey:SPNullValue]]) { + } else if ([anObject isEqualToString:[prefs stringForKey:SPNullValue]] + || (([columnTypeGroup isEqualToString:@"float"] || [columnTypeGroup isEqualToString:@"integer"]) + && [[anObject description] isEqualToString:@""])) + { newObject = @"NULL"; - } else if ([[columnDefinition objectForKey:@"typegrouping"] isEqualToString:@"geometry"]) { + } else if ([columnTypeGroup isEqualToString:@"geometry"]) { newObject = [(NSString*)anObject getGeomFromTextString]; - } else if ([[columnDefinition objectForKey:@"typegrouping"] isEqualToString:@"bit"]) { + } else if ([columnTypeGroup isEqualToString:@"bit"]) { newObject = [NSString stringWithFormat:@"b'%@'", ((![[anObject description] length] || [[anObject description] isEqualToString:@"0"]) ? @"0" : [anObject description])]; - } else if ([[columnDefinition objectForKey:@"typegrouping"] isEqualToString:@"date"] + } else if ([columnTypeGroup isEqualToString:@"date"] && [[anObject description] isEqualToString:@"NOW()"]) { newObject = @"NOW()"; } else { @@ -3868,6 +3873,10 @@ // Call the field editor sheet [self tableView:customQueryView shouldEditTableColumn:NSArrayObjectAtIndex([customQueryView tableColumns], column) row:row]; + // send current event to field editor sheet + if([NSApp currentEvent]) + [NSApp sendEvent:[NSApp currentEvent]]; + return NO; } diff --git a/Source/SPDataImport.h b/Source/SPDataImport.h index f6e51299..b9810201 100644 --- a/Source/SPDataImport.h +++ b/Source/SPDataImport.h @@ -107,6 +107,8 @@ NSMutableIndexSet *geometryFieldsMapIndex; NSMutableArray *bitFields; NSMutableIndexSet *bitFieldsMapIndex; + NSMutableArray *nullableNumericFields; + NSMutableIndexSet *nullableNumericFieldsMapIndex; NSSavePanel *currentExportPanel; } diff --git a/Source/SPDataImport.m b/Source/SPDataImport.m index 03a1afb7..d1b2b67a 100644 --- a/Source/SPDataImport.m +++ b/Source/SPDataImport.m @@ -69,6 +69,8 @@ geometryFieldsMapIndex = [[NSMutableIndexSet alloc] init]; bitFields = [[NSMutableArray alloc] init]; bitFieldsMapIndex = [[NSMutableIndexSet alloc] init]; + nullableNumericFields = [[NSMutableArray alloc] init]; + nullableNumericFieldsMapIndex = [[NSMutableIndexSet alloc] init]; fieldMappingArray = nil; fieldMappingGlobalValueArray = nil; fieldMappingTableColumnNames = nil; @@ -712,6 +714,7 @@ NSUInteger csvRowsPerQuery = 50; NSUInteger csvRowsThisQuery; NSUInteger fileTotalLength = 0; + BOOL fileIsCompressed; NSInteger rowsImported = 0; NSInteger dataBufferLength = 0; NSInteger dataBufferPosition = 0; @@ -729,6 +732,8 @@ [geometryFieldsMapIndex removeAllIndexes]; [bitFields removeAllObjects]; [bitFieldsMapIndex removeAllIndexes]; + [nullableNumericFields removeAllObjects]; + [nullableNumericFieldsMapIndex removeAllIndexes]; // Start the notification timer to allow notifications to be shown even if frontmost for long queries [[SPGrowlController sharedGrowlController] setVisibilityForNotificationName:@"Import Finished"]; @@ -746,9 +751,14 @@ return; } - // Grab the file length + // Grab the file length and status fileTotalLength = (NSUInteger)[[[[NSFileManager defaultManager] attributesOfItemAtPath:filename error:NULL] objectForKey:NSFileSize] longLongValue]; if (!fileTotalLength) fileTotalLength = 1; + fileIsCompressed = [csvFileHandle isCompressed]; + + // If importing a bzipped file, use indeterminate progress bars as no progress is available + BOOL useIndeterminate = NO; + if ([csvFileHandle compressionFormat] == SPBzip2Compression) useIndeterminate = YES; // Reset progress interface [errorsView setString:@""]; @@ -926,12 +936,22 @@ } // Reset progress interface and open the progress sheet - [[singleProgressBar onMainThread] setIndeterminate:NO]; + [[singleProgressBar onMainThread] setIndeterminate:useIndeterminate]; [[singleProgressBar onMainThread] setMaxValue:fileTotalLength]; [[singleProgressBar onMainThread] startAnimation:self]; [[NSApp onMainThread] beginSheet:singleProgressSheet modalForWindow:[tableDocumentInstance parentWindow] modalDelegate:self didEndSelector:nil contextInfo:nil]; [[singleProgressSheet onMainThread] makeKeyWindow]; + // Set up index sets for use during row enumeration + for (i = 0; i < [fieldMappingArray count]; i++) { + if ([NSArrayObjectAtIndex(fieldMapperOperator, i) integerValue] == 0) { + NSString *fieldName = NSArrayObjectAtIndex(fieldMappingTableColumnNames, i); + if ([nullableNumericFields containsObject:fieldName]) { + [nullableNumericFieldsMapIndex addIndex:i]; + } + } + } + // Set up the field names import string for INSERT or REPLACE INTO [insertBaseString appendString:csvImportHeaderString]; if(!importMethodIsUpdate) { @@ -1062,9 +1082,15 @@ rowsImported++; csvRowsThisQuery++; - [singleProgressBar setDoubleValue:[[parsePositions objectAtIndex:i] doubleValue]]; - [singleProgressText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"Imported %@ of %@", @"SQL import progress text"), - [NSString stringForByteSize:[[parsePositions objectAtIndex:i] longValue]], [NSString stringForByteSize:fileTotalLength]]]; + if (fileIsCompressed) { + [singleProgressBar setDoubleValue:[csvFileHandle realDataReadLength]]; + [singleProgressText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"Imported %@ of CSV data", @"CSV import progress text where total size is unknown"), + [NSString stringForByteSize:[[parsePositions objectAtIndex:i] longValue]]]]; + } else { + [singleProgressBar setDoubleValue:[[parsePositions objectAtIndex:i] doubleValue]]; + [singleProgressText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"Imported %@ of %@", @"SQL import progress text"), + [NSString stringForByteSize:[[parsePositions objectAtIndex:i] longValue]], [NSString stringForByteSize:fileTotalLength]]]; + } } } @@ -1089,15 +1115,27 @@ (long)(rowsImported+1),[mySQLConnection getLastErrorMessage]]; } rowsImported++; - [singleProgressBar setDoubleValue:[[parsePositions objectAtIndex:i] doubleValue]]; - [singleProgressText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"Imported %@ of %@", @"SQL import progress text"), - [NSString stringForByteSize:[[parsePositions objectAtIndex:i] longValue]], [NSString stringForByteSize:fileTotalLength]]]; + if (fileIsCompressed) { + [singleProgressBar setDoubleValue:[csvFileHandle realDataReadLength]]; + [singleProgressText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"Imported %@ of CSV data", @"CSV import progress text where total size is unknown"), + [NSString stringForByteSize:[[parsePositions objectAtIndex:i] longValue]]]]; + } else { + [singleProgressBar setDoubleValue:[[parsePositions objectAtIndex:i] doubleValue]]; + [singleProgressText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"Imported %@ of %@", @"SQL import progress text"), + [NSString stringForByteSize:[[parsePositions objectAtIndex:i] longValue]], [NSString stringForByteSize:fileTotalLength]]]; + } } } else { rowsImported += csvRowsThisQuery; - [singleProgressBar setDoubleValue:[[parsePositions objectAtIndex:csvRowsThisQuery-1] doubleValue]]; - [singleProgressText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"Imported %@ of %@", @"SQL import progress text"), - [NSString stringForByteSize:[[parsePositions objectAtIndex:csvRowsThisQuery-1] longValue]], [NSString stringForByteSize:fileTotalLength]]]; + if (fileIsCompressed) { + [singleProgressBar setDoubleValue:[csvFileHandle realDataReadLength]]; + [singleProgressText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"Imported %@ of CSV data", @"CSV import progress text where total size is unknown"), + [NSString stringForByteSize:[[parsePositions objectAtIndex:csvRowsThisQuery-1] longValue]]]]; + } else { + [singleProgressBar setDoubleValue:[[parsePositions objectAtIndex:csvRowsThisQuery-1] doubleValue]]; + [singleProgressText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"Imported %@ of %@", @"SQL import progress text"), + [NSString stringForByteSize:[[parsePositions objectAtIndex:csvRowsThisQuery-1] longValue]], [NSString stringForByteSize:fileTotalLength]]]; + } } // Update the arrays @@ -1263,12 +1301,15 @@ targetTableDetails = [selectedTableData informationForTable:selectedTableTarget]; [selectedTableData release]; - // Store all field names which are of typegrouping 'geometry' and 'bit' + // Store all field names which are of typegrouping 'geometry' and 'bit', and check if + // numeric columns can hold NULL values to map empty strings to. for(NSDictionary *field in [targetTableDetails objectForKey:@"columns"]) { if([[field objectForKey:@"typegrouping"] isEqualToString:@"geometry"]) [geometryFields addObject:[field objectForKey:@"name"]]; if([[field objectForKey:@"typegrouping"] isEqualToString:@"bit"]) [bitFields addObject:[field objectForKey:@"name"]]; + if(([[field objectForKey:@"typegrouping"] isEqualToString:@"float"] || [[field objectForKey:@"typegrouping"] isEqualToString:@"integer"]) && [[field objectForKey:@"null"] boolValue]) + [nullableNumericFields addObject:[field objectForKey:@"name"]]; } [importFieldNamesSwitch setState:[fieldMapperController importFieldNamesHeader]]; @@ -1485,8 +1526,10 @@ if ([cellData isSPNotLoaded]) cellData = NSArrayObjectAtIndex(fieldMappingTableDefaultValues, i); - if (cellData == [NSNull null]) { + // Insert a NULL if the cell is an NSNull, or is a nullable numeric field and empty + if (cellData == [NSNull null] || ([nullableNumericFieldsMapIndex containsIndex:i] && [[cellData description] isEqualToString:@""])) { [valueString appendString:@"NULL"]; + } else { // Apply GeomFromText() for each geometry field if([geometryFields count] && [geometryFieldsMapIndex containsIndex:i]) { @@ -1650,7 +1693,9 @@ if (geometryFields) [geometryFields release]; if (geometryFieldsMapIndex) [geometryFieldsMapIndex release]; if (bitFields) [bitFields release]; + if (nullableNumericFields) [nullableNumericFields release]; if (bitFieldsMapIndex) [bitFieldsMapIndex release]; + if (nullableNumericFieldsMapIndex) [nullableNumericFieldsMapIndex release]; if (lastFilename) [lastFilename release]; if (prefs) [prefs release]; diff --git a/Source/SPDatabaseDocument.h b/Source/SPDatabaseDocument.h index d70a7a40..ebdac213 100644 --- a/Source/SPDatabaseDocument.h +++ b/Source/SPDatabaseDocument.h @@ -48,14 +48,14 @@ SPTablesList, SPTableStructure, SPTableContent, SPTableData, SPServerSupport; #endif // IBOutlets - SPTablesList* tablesListInstance; - SPTableStructure* tableSourceInstance; - SPTableContent* tableContentInstance; + IBOutlet SPTablesList* tablesListInstance; + IBOutlet SPTableStructure* tableSourceInstance; + IBOutlet SPTableContent* tableContentInstance; IBOutlet id tableRelationsInstance; IBOutlet id tableTriggersInstance; IBOutlet id customQueryInstance; IBOutlet id tableDumpInstance; - SPTableData* tableDataInstance; + IBOutlet SPTableData* tableDataInstance; IBOutlet id extendedTableInfoInstance; IBOutlet id databaseDataInstance; IBOutlet id spHistoryControllerInstance; diff --git a/Source/SPDatabaseDocument.m b/Source/SPDatabaseDocument.m index d8eaa100..04d4b3f3 100644 --- a/Source/SPDatabaseDocument.m +++ b/Source/SPDatabaseDocument.m @@ -78,7 +78,10 @@ #import "NSNotificationAdditions.h" #endif -@interface SPDatabaseDocument (PrivateAPI) +// Constants +static NSString *SPCreateSyntx = @"SPCreateSyntax"; + +@interface SPDatabaseDocument () #ifndef SP_REFACTOR /* method decls */ - (void)_addDatabase; @@ -2135,7 +2138,7 @@ [panel setAllowsOtherFileTypes:YES]; [panel setCanSelectHiddenExtension:YES]; - [panel beginSheetForDirectory:nil file:@"CreateSyntax" modalForWindow:createTableSyntaxWindow modalDelegate:self didEndSelector:@selector(savePanelDidEnd:returnCode:contextInfo:) contextInfo:@"CreateSyntax"]; + [panel beginSheetForDirectory:nil file:@"CreateSyntax" modalForWindow:createTableSyntaxWindow modalDelegate:self didEndSelector:@selector(savePanelDidEnd:returnCode:contextInfo:) contextInfo:SPCreateSyntx]; } /** @@ -2716,7 +2719,7 @@ if([[NSApp delegate] sessionURL]) filename = [[[[NSApp delegate] sessionURL] absoluteString] lastPathComponent]; else - filename = [NSString stringWithFormat:@"%@", @"session"]; + filename = [NSString stringWithFormat:NSLocalizedString(@"Session",@"Initial filename for 'Save session' file")]; contextInfo = @"saveSession"; } @@ -3175,7 +3178,6 @@ */ - (IBAction)openDatabaseInNewTab:(id)sender { - // Add a new tab to the window [[parentWindow windowController] addNewConnection:self]; @@ -3282,11 +3284,11 @@ } } - if ([menuItem action] == @selector(import:) || - [menuItem action] == @selector(removeDatabase:) || - [menuItem action] == @selector(copyDatabase:) || - [menuItem action] == @selector(renameDatabase:) || - [menuItem action] == @selector(openDatabaseInNewTab:) || + if ([menuItem action] == @selector(import:) || + [menuItem action] == @selector(removeDatabase:) || + [menuItem action] == @selector(copyDatabase:) || + [menuItem action] == @selector(renameDatabase:) || + [menuItem action] == @selector(openDatabaseInNewTab:) || [menuItem action] == @selector(refreshTables:)) { return ([self database] != nil); @@ -3420,12 +3422,12 @@ - (void)savePanelDidEnd:(NSSavePanel *)sheet returnCode:(NSInteger)returnCode contextInfo:(NSString *)contextInfo { if (returnCode == NSOKButton) { - if ([contextInfo isEqualToString:@"CreateSyntax"]) { + if ([contextInfo isEqualToString:SPCreateSyntx]) { NSString *createSyntax = [createTableSyntaxTextView string]; if ([createSyntax length] > 0) { - NSString *output = [NSString stringWithFormat:@"-- Create syntax for '%@'\n\n%@\n", [self table], createSyntax]; + NSString *output = [NSString stringWithFormat:@"-- %@ '%@'\n\n%@\n", NSLocalizedString(@"Create syntax for", @"create syntax for table comment"), [self table], createSyntax]; [output writeToFile:[sheet filename] atomically:YES encoding:NSUTF8StringEncoding error:NULL]; } @@ -3577,7 +3579,7 @@ /** * Return the identifier for the currently selected toolbar item, or nil if none is selected. */ -- (NSString *)selectedToolbarItemIdentifier; +- (NSString *)selectedToolbarItemIdentifier { return [mainToolbar selectedItemIdentifier]; } @@ -4902,6 +4904,20 @@ return; } + if([command isEqualToString:@"RunQueryInQueryEditor"]) { + NSString *queryFileName = [NSString stringWithFormat:@"%@%@", SPURLSchemeQueryInputPathHeader, docProcessID]; + NSFileManager *fm = [NSFileManager defaultManager]; + BOOL isDir; + if([fm fileExistsAtPath:queryFileName isDirectory:&isDir] && !isDir) { + NSError *inError = nil; + NSString *query = [NSString stringWithContentsOfFile:queryFileName encoding:NSUTF8StringEncoding error:&inError]; + [fm removeItemAtPath:queryFileName error:nil]; + if(inError == nil && query && [query length]) { + [customQueryInstance performQueries:[NSArray arrayWithObject:query] withCallback:NULL]; + } + } + return; + } if([command isEqualToString:@"CreateSyntaxForTables"]) { @@ -5603,10 +5619,6 @@ return tablesListInstance; } -@end - -@implementation SPDatabaseDocument (PrivateAPI) - #ifndef SP_REFACTOR /* whole database operations */ - (void)_copyDatabase diff --git a/Source/SPEditorPreferencePane.m b/Source/SPEditorPreferencePane.m index 51ffacf6..08e91936 100644 --- a/Source/SPEditorPreferencePane.m +++ b/Source/SPEditorPreferencePane.m @@ -37,7 +37,7 @@ static NSString *SPDefaultColorSchemeName = @"Default"; static NSString *SPDefaultColorSchemeNameLC = @"default"; static NSString *SPCustomColorSchemeName = @"User-defined"; static NSString *SPCustomColorSchemeNameLC = @"user-defined"; -static NSString *SPDefaultExportColourSchemeName = @"MyTheme"; +#define SP_EXPORT_COLOR_SCHEME_NAME_STRING NSLocalizedString(@"MyTheme",@"Preferences : Themes : Initial filename for 'Export'") @interface SPEditorPreferencePane (PrivateAPI) @@ -136,7 +136,7 @@ static NSString *SPDefaultExportColourSchemeName = @"MyTheme"; [panel setCanCreateDirectories:YES]; [panel beginSheetForDirectory:nil - file:[SPDefaultExportColourSchemeName stringByAppendingPathExtension:SPColorThemeFileExtension] + file:[SP_EXPORT_COLOR_SCHEME_NAME_STRING stringByAppendingPathExtension:SPColorThemeFileExtension] modalForWindow:[[self view] window] modalDelegate:self didEndSelector:@selector(panelDidEnd:returnCode:contextInfo:) 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 2780df54..0ff78c1f 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 @@ -43,11 +44,17 @@ static const NSString *SPTableViewStructureColumnID = @"structure"; static const NSString *SPTableViewContentColumnID = @"content"; static const NSString *SPTableViewDropColumnID = @"drop"; +static const NSString *SPSQLExportStructureEnabled = @"SQLExportStructureEnabled"; +static const NSString *SPSQLExportContentEnabled = @"SQLExportContentEnabled"; +static const NSString *SPSQLExportDropEnabled = @"SQLExportDropEnabled"; + @interface SPExportController (PrivateAPI) - (void)_switchTab; - (void)_checkForDatabaseChanges; - (void)_displayExportTypeOptions:(BOOL)display; +- (void)_updateExportFormatInformation; +- (void)_updateExportAdvancedOptionsLabel; - (void)_toggleExportButton:(id)uiStateDict; - (void)_toggleExportButtonOnBackgroundThread; @@ -127,12 +134,21 @@ static const NSString *SPTableViewDropColumnID = @"drop"; // 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]; @@ -154,10 +170,13 @@ static const NSString *SPTableViewDropColumnID = @"drop"; // 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]; + [self refreshTableList:nil]; [exporters removeAllObjects]; [exportFiles removeAllObjects]; @@ -194,6 +213,7 @@ static const NSString *SPTableViewDropColumnID = @"drop"; // Ensure interface validation [self _switchTab]; + [self _updateExportAdvancedOptionsLabel]; [NSApp beginSheet:[self window] modalForWindow:[tableDocumentInstance parentWindow] @@ -377,7 +397,7 @@ static const NSString *SPTableViewDropColumnID = @"drop"; [panel setCanChooseDirectories:YES]; [panel setCanCreateDirectories:YES]; - [panel beginSheetForDirectory:nil + [panel beginSheetForDirectory:[exportPathField stringValue] file:nil modalForWindow:[self window] modalDelegate:self @@ -390,6 +410,21 @@ static const NSString *SPTableViewDropColumnID = @"drop"; */ - (IBAction)refreshTableList:(id)sender { + NSMutableDictionary *tableDict = [[NSMutableDictionary alloc] init]; + + // Before refreshing the list, preserve the user's table selection, but only if it was triggered by the UI. + if (sender) { + for (NSMutableArray *item in tables) + { + [tableDict setObject:[NSArray arrayWithObjects: + [item objectAtIndex:1], + [item objectAtIndex:2], + [item objectAtIndex:3], + nil] + forKey:[item objectAtIndex:0]]; + } + } + [tables removeAllObjects]; // For all modes, retrieve table and view names @@ -434,7 +469,28 @@ static const NSString *SPTableViewDropColumnID = @"drop"; } } + if (sender) { + // Restore the user's table selection + for (NSUInteger i = 0; i < [tables count]; i++) + { + NSMutableArray *oldSelection = [tableDict objectForKey:[[tables objectAtIndex:i] objectAtIndex:0]]; + + if (oldSelection) { + + NSMutableArray *newItem = [[NSMutableArray alloc] initWithArray:oldSelection]; + + [newItem insertObject:[[tables objectAtIndex:i] objectAtIndex:0] atIndex:0]; + + [tables replaceObjectAtIndex:i withObject:newItem]; + + [newItem release]; + } + } + } + [exportTableList reloadData]; + + [tableDict release]; } /** @@ -445,7 +501,7 @@ static const NSString *SPTableViewDropColumnID = @"drop"; BOOL toggleStructure = NO; BOOL toggleDropTable = NO; - [self refreshTableList:self]; + [self refreshTableList:nil]; // Determine whether the structure and drop items should also be toggled if (exportType == SPSQLExport) { @@ -463,7 +519,8 @@ static const NSString *SPTableViewDropColumnID = @"drop"; } [exportTableList reloadData]; - + + [self _updateExportFormatInformation]; [self _toggleExportButtonOnBackgroundThread]; } @@ -487,14 +544,6 @@ static const NSString *SPTableViewDropColumnID = @"drop"; [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")]; - } } /** @@ -524,6 +573,7 @@ static const NSString *SPTableViewDropColumnID = @"drop"; [exportAdvancedOptionsViewButton setState:showAdvancedView]; [exportAdvancedOptionsView setHidden:(!showAdvancedView)]; + [self _updateExportAdvancedOptionsLabel]; [self _resizeWindowForAdvancedOptionsViewByHeightDelta:(showAdvancedView) ? ([exportAdvancedOptionsView frame].size.height + 10) : 0]; } @@ -558,6 +608,14 @@ static const NSString *SPTableViewDropColumnID = @"drop"; } /** + * 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 @@ -578,7 +636,20 @@ static const NSString *SPTableViewDropColumnID = @"drop"; { // 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. @@ -615,6 +686,7 @@ static const NSString *SPTableViewDropColumnID = @"drop"; { if (returnCode == NSOKButton) { [exportPathField setStringValue:[panel directory]]; + [prefs setObject:[panel directory] forKey:SPExportLastDirectory]; } } @@ -730,7 +802,8 @@ static const NSString *SPTableViewDropColumnID = @"drop"; [self _displayExportTypeOptions:(isSQL || isCSV || isXML || isDot)]; [self updateAvailableExportFilenameTokens]; - if (!showCustomFilenameView) [self updateDisplayedExportFilename]; + [self updateDisplayedExportFilename]; + [self _updateExportFormatInformation]; } /** @@ -789,6 +862,62 @@ static const NSString *SPTableViewDropColumnID = @"drop"; } /** + * 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. @@ -805,9 +934,9 @@ static const NSString *SPTableViewDropColumnID = @"drop"; BOOL isHTML = (exportType == SPHTMLExport); BOOL isPDF = (exportType == SPPDFExport); - BOOL structureEnabled = [[uiStateDict objectForKey:@"SQLExportStructureEnabled"] integerValue]; - BOOL contentEnabled = [[uiStateDict objectForKey:@"SQLExportContentEnabled"] integerValue]; - BOOL dropEnabled = [[uiStateDict objectForKey:@"SQLExportDropEnabled"] integerValue]; + BOOL structureEnabled = [[uiStateDict objectForKey:SPSQLExportStructureEnabled] integerValue]; + BOOL contentEnabled = [[uiStateDict objectForKey:SPSQLExportContentEnabled] integerValue]; + BOOL dropEnabled = [[uiStateDict objectForKey:SPSQLExportDropEnabled] integerValue]; if (isCSV || isXML || isHTML || isPDF || (isSQL && ((!structureEnabled) || (!dropEnabled)))) { enable = NO; @@ -864,9 +993,9 @@ static const NSString *SPTableViewDropColumnID = @"drop"; { NSMutableDictionary *uiStateDict = [[NSMutableDictionary alloc] init]; - [uiStateDict setObject:[NSNumber numberWithInteger:[exportSQLIncludeStructureCheck state]] forKey:@"SQLExportStructureEnabled"]; - [uiStateDict setObject:[NSNumber numberWithInteger:[exportSQLIncludeContentCheck state]] forKey:@"SQLExportContentEnabled"]; - [uiStateDict setObject:[NSNumber numberWithInteger:[exportSQLIncludeDropSyntaxCheck state]] forKey:@"SQLExportDropEnabled"]; + [uiStateDict setObject:[NSNumber numberWithInteger:[exportSQLIncludeStructureCheck state]] forKey:SPSQLExportStructureEnabled]; + [uiStateDict setObject:[NSNumber numberWithInteger:[exportSQLIncludeContentCheck state]] forKey:SPSQLExportContentEnabled]; + [uiStateDict setObject:[NSNumber numberWithInteger:[exportSQLIncludeDropSyntaxCheck state]] forKey:SPSQLExportDropEnabled]; [NSThread detachNewThreadSelector:@selector(_toggleExportButton:) toTarget:self withObject:uiStateDict]; 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..b23d0b78 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 files 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 f1868210..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,10 +288,13 @@ [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]) { + [exportFilename setString:[exportFilename stringByAppendingPathExtension:[self currentDefaultExportFileExtension]]]; + } - [exportFilename setString:[exportFilename stringByAppendingPathExtension:[self currentDefaultExportFileExtension]]]; - file = [SPExportFile exportFileAtPath:[[exportPathField stringValue] stringByAppendingPathComponent:exportFilename]]; [exportFiles addObject:file]; @@ -302,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]]; } @@ -371,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]]; @@ -465,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]]; @@ -519,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/SPFieldEditorController.m b/Source/SPFieldEditorController.m index bf48ec80..7bdd2eb9 100644 --- a/Source/SPFieldEditorController.m +++ b/Source/SPFieldEditorController.m @@ -225,10 +225,10 @@ // Init according bit check boxes NSUInteger i = 0; NSUInteger maxBit = (NSUInteger)((maxTextLength > 64) ? 64 : maxTextLength); - if([bitSheetNULLButton state] == NSOffState) + if([bitSheetNULLButton state] == NSOffState && maxBit <= [(NSString*)sheetEditData length]) for( i = 0; i<maxBit; i++ ) [[self valueForKeyPath:[NSString stringWithFormat:@"bitSheetBitButton%ld", i]] - setState:([sheetEditData characterAtIndex:(maxBit-i-1)] == '1') ? NSOnState : NSOffState]; + setState:([(NSString*)sheetEditData characterAtIndex:(maxBit-i-1)] == '1') ? NSOnState : NSOffState]; for( i = maxBit; i<64; i++ ) [[self valueForKeyPath:[NSString stringWithFormat:@"bitSheetBitButton%ld", i]] setEnabled:NO]; diff --git a/Source/SPIndexesController.h b/Source/SPIndexesController.h index c56ff4eb..65cbb207 100644 --- a/Source/SPIndexesController.h +++ b/Source/SPIndexesController.h @@ -23,7 +23,7 @@ // // More info at <http://code.google.com/p/sequel-pro/> -@class SPDatabaseDocument, SPTablesList, SPTableData, SPTableStructure, MCPConnection, BWAnchoredButtonBar; +@class SPDatabaseDocument, SPTablesList, SPTableData, SPTableStructure, SPTableView, MCPConnection, BWAnchoredButtonBar; @interface SPIndexesController : NSWindowController { @@ -34,7 +34,7 @@ IBOutlet SPTableData *tableData; // Index table view - IBOutlet NSTableView *indexesTableView; + IBOutlet SPTableView *indexesTableView; IBOutlet NSButton *addIndexButton; IBOutlet NSButton *removeIndexButton; diff --git a/Source/SPIndexesController.m b/Source/SPIndexesController.m index 9c0c03a6..989ec02c 100644 --- a/Source/SPIndexesController.m +++ b/Source/SPIndexesController.m @@ -31,6 +31,7 @@ #import <MCPKit/MCPKit.h> #import "SPDatabaseDocument.h" #import "SPTablesList.h" +#import "SPTableView.h" #import "SPDatabaseViewController.h" #import "SPTableStructure.h" @@ -111,6 +112,9 @@ static const NSString *SPNewIndexKeyBlockSize = @"IndexKeyBlockSize"; BOOL useMonospacedFont = NO; #endif + // Set the double-click action in blank areas of the table to create new rows + [indexesTableView setEmptyDoubleClickAction:@selector(addIndex:)]; + for (NSTableColumn *indexColumn in [indexesTableView tableColumns]) { [[indexColumn dataCell] setFont:(useMonospacedFont) ? [NSFont fontWithName:SPDefaultMonospacedFontName size:[NSFont smallSystemFontSize]] : [NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; @@ -134,6 +138,10 @@ static const NSString *SPNewIndexKeyBlockSize = @"IndexKeyBlockSize"; */ - (IBAction)addIndex:(id)sender { + + // Check whether table editing is permitted (necessary as some actions - eg table double-click - bypass validation) + if ([dbDocument isWorking] || [tablesList tableType] != SPTableTypeTable) return; + // Check whether a save of the current field row is required. if (![tableStructure saveRowOnDeselect]) return; diff --git a/Source/SPNavigatorController.m b/Source/SPNavigatorController.m index be24948b..1c17dd25 100644 --- a/Source/SPNavigatorController.m +++ b/Source/SPNavigatorController.m @@ -39,11 +39,6 @@ static SPNavigatorController *sharedNavigatorController = nil; -#ifndef SP_REFACTOR /* pasteboard types */ -#define DragFromNavigatorPboardType @"SPDragFromNavigatorPboardType" -#define DragTableDataFromNavigatorPboardType @"SPDragTableDataFromNavigatorPboardType" -#endif - @implementation SPNavigatorController #ifndef SP_REFACTOR /* unused sort func */ @@ -146,7 +141,7 @@ static NSComparisonResult compareStrings(NSString *s1, NSString *s2, void* conte prefs = [NSUserDefaults standardUserDefaults]; [self setWindowFrameAutosaveName:@"SPNavigator"]; - [outlineSchema2 registerForDraggedTypes:[NSArray arrayWithObjects:DragTableDataFromNavigatorPboardType, DragFromNavigatorPboardType, NSStringPboardType, nil]]; + [outlineSchema2 registerForDraggedTypes:[NSArray arrayWithObjects:SPNavigatorTableDataPasteboardDragType, SPNavigatorPasteboardDragType, NSStringPboardType, nil]]; [outlineSchema2 setDraggingSourceOperationMask:NSDragOperationEvery forLocal:YES]; [outlineSchema2 setDraggingSourceOperationMask:NSDragOperationEvery forLocal:NO]; @@ -1108,7 +1103,7 @@ static NSComparisonResult compareStrings(NSString *s1, NSString *s2, void* conte - (BOOL)outlineView:(NSOutlineView *)outlineView writeItems:(NSArray *)items toPasteboard:(NSPasteboard *)pboard { // Provide data for our custom type, and simple NSStrings. - [pboard declareTypes:[NSArray arrayWithObjects:DragTableDataFromNavigatorPboardType, DragFromNavigatorPboardType, NSStringPboardType, nil] owner:self]; + [pboard declareTypes:[NSArray arrayWithObjects:SPNavigatorTableDataPasteboardDragType, SPNavigatorPasteboardDragType, NSStringPboardType, nil] owner:self]; // Collect the actual schema paths without leading connection ID NSMutableArray *draggedItems = [NSMutableArray array]; @@ -1125,7 +1120,7 @@ static NSComparisonResult compareStrings(NSString *s1, NSString *s2, void* conte NSKeyedArchiver *archiver = [[[NSKeyedArchiver alloc] initForWritingWithMutableData:arraydata] autorelease]; [archiver encodeObject:draggedItems forKey:@"itemdata"]; [archiver finishEncoding]; - [pboard setData:arraydata forType:DragFromNavigatorPboardType]; + [pboard setData:arraydata forType:SPNavigatorPasteboardDragType]; if([draggedItems count] == 1) { NSArray *pathComponents = [[draggedItems objectAtIndex:0] componentsSeparatedByString:SPUniqueSchemaDelimiter]; @@ -1134,7 +1129,7 @@ static NSComparisonResult compareStrings(NSString *s1, NSString *s2, void* conte [pboard setString:[NSString stringWithFormat:@"CREATE TABLE IF NOT EXISTS %@ SELECT * FROM %@", [[pathComponents lastObject] backtickQuotedString], [pathComponents componentsJoinedByPeriodAndBacktickQuoted] - ] forType:DragTableDataFromNavigatorPboardType]; + ] forType:SPNavigatorTableDataPasteboardDragType]; } } // For external destinations provide a comma separated string diff --git a/Source/SPProcessListController.h b/Source/SPProcessListController.h index 3fc66c78..df5f48a6 100644 --- a/Source/SPProcessListController.h +++ b/Source/SPProcessListController.h @@ -58,6 +58,7 @@ - (IBAction)killProcessQuery:(id)sender; - (IBAction)killProcessConnection:(id)sender; - (IBAction)toggleShowProcessID:(id)sender; +- (IBAction)toggeleShowFullProcessList:(id)sender; - (IBAction)toggleProcessListAutoRefresh:(id)sender; - (IBAction)setAutoRefreshInterval:(id)sender; - (IBAction)setCustomAutoRefreshInterval:(id)sender; diff --git a/Source/SPProcessListController.m b/Source/SPProcessListController.m index e3323666..b0242a35 100644 --- a/Source/SPProcessListController.m +++ b/Source/SPProcessListController.m @@ -65,6 +65,8 @@ static NSString *SPTableViewIDColumnIdentifier = @"Id"; autoRefreshTimer = nil; processListThreadRunning = NO; + showFullProcessList = [prefs boolForKey:SPProcessListShowFullProcessList]; + processes = [[NSMutableArray alloc] init]; prefs = [NSUserDefaults standardUserDefaults]; @@ -291,6 +293,16 @@ static NSString *SPTableViewIDColumnIdentifier = @"Id"; } /** + * Toggles the display of the FULL process list. + */ +- (IBAction)toggeleShowFullProcessList:(id)sender +{ + showFullProcessList = (!showFullProcessList); + + [self refreshProcessList:self]; +} + +/** * Toggles whether or not auto refresh is enabled. */ - (IBAction)toggleProcessListAutoRefresh:(id)sender @@ -312,7 +324,7 @@ static NSString *SPTableViewIDColumnIdentifier = @"Id"; } /** - * + * Displays the set custom auto-refresh interval sheet. */ - (IBAction)setCustomAutoRefreshInterval:(id)sender { @@ -678,7 +690,7 @@ static NSString *SPTableViewIDColumnIdentifier = @"Id"; // Get processes if ([connection isConnected]) { - MCPResult *processList = [connection listProcesses]; + MCPResult *processList = (showFullProcessList) ? [connection queryString:@"SHOW FULL PROCESSLIST"] : [connection listProcesses]; [processList setReturnDataAsStrings:YES]; diff --git a/Source/SPQueryController.m b/Source/SPQueryController.m index 475b96bc..3a003f82 100644 --- a/Source/SPQueryController.m +++ b/Source/SPQueryController.m @@ -295,7 +295,7 @@ static SPQueryController *sharedQueryController = nil; [panel setAccessoryView:saveLogView]; - [panel beginSheetForDirectory:nil file:@"untitled" modalForWindow:[self window] modalDelegate:self didEndSelector:@selector(savePanelDidEnd:returnCode:contextInfo:) contextInfo:NULL]; + [panel beginSheetForDirectory:nil file:NSLocalizedString(@"ConsoleLog",@"Console : Save as : Initial filename") modalForWindow:[self window] modalDelegate:self didEndSelector:@selector(savePanelDidEnd:returnCode:contextInfo:) contextInfo:NULL]; } /** 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/Source/SPServerVariablesController.m b/Source/SPServerVariablesController.m index dedda57b..f2eb60e0 100644 --- a/Source/SPServerVariablesController.m +++ b/Source/SPServerVariablesController.m @@ -138,7 +138,7 @@ #pragma mark Other methods /** - * Displays the process list sheet attached to the supplied window. + * Displays the server variables sheet attached to the supplied window. */ - (void)displayServerVariablesSheetAttachedToWindow:(NSWindow *)window { @@ -253,7 +253,7 @@ #pragma mark Text field delegate methods /** - * Apply the filter string to the current process list. + * Apply the filter string to the current variables list. */ - (void)controlTextDidChange:(NSNotification *)notification { diff --git a/Source/SPTableContent.h b/Source/SPTableContent.h index 2b125acd..9045772e 100644 --- a/Source/SPTableContent.h +++ b/Source/SPTableContent.h @@ -31,10 +31,9 @@ @class SPTableData, SPDatabaseDocument, SPTablesList; -#ifndef SP_REFACTOR @interface SPTableContent : NSObject -#else -@interface SPTableContent : NSObject <NSTableViewDelegate, NSTableViewDataSource> +#ifdef SP_REFACTOR +<NSTableViewDelegate, NSTableViewDataSource> #endif { IBOutlet SPDatabaseDocument *tableDocumentInstance; diff --git a/Source/SPTableContent.m b/Source/SPTableContent.m index c9b8c145..df4eccf7 100644 --- a/Source/SPTableContent.m +++ b/Source/SPTableContent.m @@ -51,8 +51,10 @@ #import "SPBundleHTMLOutputController.h" #import "SPCustomQuery.h" -@interface SPTableContent (Private) +@interface SPTableContent () + - (BOOL)cancelRowEditing; + @end @implementation SPTableContent @@ -163,6 +165,9 @@ [tableContentView setGridStyleMask:([prefs boolForKey:SPDisplayTableViewVerticalGridlines]) ? NSTableViewSolidVerticalGridLineMask : NSTableViewGridNone]; #endif + // Set the double-click action in blank areas of the table to create new rows + [tableContentView setEmptyDoubleClickAction:@selector(addRow:)]; + // Load the pagination view, keeping references to the top-level objects for later release NSArray *paginationViewTopLevelObjects = nil; NSNib *nibLoader = [[NSNib alloc] initWithNibNamed:@"ContentPaginationView" bundle:[NSBundle mainBundle]]; @@ -218,7 +223,6 @@ */ - (void)loadTable:(NSString *)aTable { - // Abort the reload if the user is still editing a row if ( isEditingRow ) return; @@ -226,6 +230,7 @@ // If no table has been supplied, clear the table interface and return if (!aTable || [aTable isEqualToString:@""]) { [self performSelectorOnMainThread:@selector(setTableDetails:) withObject:nil waitUntilDone:YES]; + return; } @@ -233,6 +238,8 @@ // while retrieving table data), or if the Rows variable is null, clear and return if (![tableDataInstance tableEncoding] || [[[tableDataInstance statusValues] objectForKey:@"Rows"] isNSNull]) { [self performSelectorOnMainThread:@selector(setTableDetails:) withObject:nil waitUntilDone:YES]; + [tableDocumentInstance performSelectorOnMainThread:@selector(endTask) withObject:nil waitUntilDone:YES]; + return; } @@ -606,10 +613,8 @@ // Enable and initialize filter fields (with tags for position of menu item and field position) [fieldField setEnabled:YES]; [fieldField removeAllItems]; - [fieldField addItemsWithTitles:columnNames]; - for ( i = 0 ; i < [fieldField numberOfItems] ; i++ ) { - [[fieldField itemAtIndex:i] setTag:i]; - } + NSArray *columnTitles = ([prefs boolForKey:SPAlphabeticalTableSorting])? [columnNames sortedArrayUsingSelector:@selector(compare:)] : columnNames; + [fieldField addItemsWithTitles:columnTitles]; [compareField setEnabled:YES]; [self setCompareTypes:self]; [argumentField setEnabled:YES]; @@ -1332,7 +1337,6 @@ */ - (IBAction)filterTable:(id)sender { - if(sender == filterTableFilterButton) activeFilter = 1; else if([sender isKindOfClass:[NSString class]] && [(NSString *)sender length]) { @@ -1655,6 +1659,9 @@ NSMutableArray *newRow = [NSMutableArray array]; NSUInteger i; + // Check whether table editing is permitted (necessary as some actions - eg table double-click - bypass validation) + if ([tableDocumentInstance isWorking] || [tablesListInstance tableType] != SPTableTypeTable) return; + // Check whether a save of the current row is required. if ( ![self saveRowOnDeselect] ) return; @@ -1665,9 +1672,13 @@ } else if ([[column objectForKey:@"default"] isEqualToString:@""] && ![[column objectForKey:@"null"] boolValue] && ([[column objectForKey:@"typegrouping"] isEqualToString:@"float"] - || [[column objectForKey:@"typegrouping"] isEqualToString:@"integer"])) + || [[column objectForKey:@"typegrouping"] isEqualToString:@"integer"] + || [[column objectForKey:@"typegrouping"] isEqualToString:@"bit"])) { [newRow addObject:@"0"]; + } else if ([[column objectForKey:@"typegrouping"] isEqualToString:@"bit"] && [[column objectForKey:@"default"] hasPrefix:@"b'"] && [(NSString*)[column objectForKey:@"default"] length] > 3) { + // remove leading b' and final ' + [newRow addObject:[[[column objectForKey:@"default"] substringFromIndex:2] substringToIndex:[(NSString*)[column objectForKey:@"default"] length]-3]]; } else { [newRow addObject:[column objectForKey:@"default"]]; } @@ -2340,8 +2351,8 @@ [compareField removeAllItems]; NSString *fieldTypeGrouping; - if([[tableDataInstance columnWithName:[[fieldField selectedItem] title]] objectForKey:@"typegrouping"]) - fieldTypeGrouping = [NSString stringWithString:[[tableDataInstance columnWithName:[[fieldField selectedItem] title]] objectForKey:@"typegrouping"]]; + if([[tableDataInstance columnWithName:[fieldField titleOfSelectedItem]] objectForKey:@"typegrouping"]) + fieldTypeGrouping = [NSString stringWithString:[[tableDataInstance columnWithName:[fieldField titleOfSelectedItem]] objectForKey:@"typegrouping"]]; else return; @@ -2387,7 +2398,7 @@ } else { compareType = @""; NSBeep(); - NSLog(@"ERROR: unknown type for comparision: %@, in %@", [[tableDataInstance columnWithName:[[fieldField selectedItem] title]] objectForKey:@"type"], fieldTypeGrouping); + NSLog(@"ERROR: unknown type for comparision: %@, in %@", [[tableDataInstance columnWithName:[fieldField titleOfSelectedItem]] objectForKey:@"type"], fieldTypeGrouping); } // Add IS NULL and IS NOT NULL as they should always be available @@ -3324,7 +3335,7 @@ theDictionary = [NSDictionary dictionaryWithObjectsAndKeys: [self tableFilterString], @"menuLabel", - [[fieldField selectedItem] title], @"filterField", + [fieldField titleOfSelectedItem], @"filterField", [[compareField selectedItem] title], @"filterComparison", [NSNumber numberWithInteger:[[compareField selectedItem] tag]], @"filterComparisonTag", [argumentField stringValue], @"filterValue", @@ -4573,6 +4584,10 @@ // Call the field editor sheet [self tableView:tableContentView shouldEditTableColumn:NSArrayObjectAtIndex([tableContentView tableColumns], column) row:row]; + // send current event to field editor sheet + if([NSApp currentEvent]) + [NSApp sendEvent:[NSApp currentEvent]]; + return NO; } @@ -4635,16 +4650,23 @@ */ - (BOOL)validateMenuItem:(NSMenuItem *)menuItem { + SEL action = [menuItem action]; + // Remove row - if ([menuItem action] == @selector(removeRow:)) { - [menuItem setTitle:([tableContentView numberOfSelectedRows] > 1) ? @"Delete Rows" : @"Delete Row"]; + if (action == @selector(removeRow:)) { + [menuItem setTitle:([tableContentView numberOfSelectedRows] > 1) ? NSLocalizedString(@"Delete Rows", @"delete rows menu item plural") : NSLocalizedString(@"Delete Row", @"delete row menu item singular")]; return ([tableContentView numberOfSelectedRows] > 0 && [tablesListInstance tableType] == SPTableTypeTable); } // Duplicate row - if ([menuItem action] == @selector(copyRow:)) { - return ([tableContentView numberOfSelectedRows] == 1 && [tablesListInstance tableType] == SPTableTypeTable); + if (action == @selector(copyRow:)) { + return (([tableContentView numberOfSelectedRows]) == 1 && ([tablesListInstance tableType] == SPTableTypeTable)); + } + + // Add new row + if (action == @selector(addRow:)) { + return ((![tableContentView numberOfSelectedRows]) && ([tablesListInstance tableType] == SPTableTypeTable)); } return YES; diff --git a/Source/SPTableRelations.h b/Source/SPTableRelations.h index 3fbef811..a71b151a 100644 --- a/Source/SPTableRelations.h +++ b/Source/SPTableRelations.h @@ -25,6 +25,8 @@ #import <MCPKit/MCPKit.h> +@class SPTableView; + @interface SPTableRelations : NSObject { IBOutlet id tableDocumentInstance; @@ -37,7 +39,7 @@ IBOutlet NSButton *removeRelationButton; IBOutlet NSButton *refreshRelationsButton; IBOutlet NSTextField *labelTextField; - IBOutlet NSTableView *relationsTableView; + IBOutlet SPTableView *relationsTableView; IBOutlet NSPanel *addRelationPanel; IBOutlet NSBox *addRelationTableBox; @@ -51,6 +53,7 @@ MCPConnection *connection; NSMutableArray *relationData; + NSUserDefaults *prefs; } @property (readonly) NSMutableArray *relationData; diff --git a/Source/SPTableRelations.m b/Source/SPTableRelations.m index fb833d94..2266229a 100644 --- a/Source/SPTableRelations.m +++ b/Source/SPTableRelations.m @@ -27,6 +27,7 @@ #import "SPDatabaseDocument.h" #import "SPTablesList.h" #import "SPTableData.h" +#import "SPTableView.h" #import "SPAlertSheets.h" @interface SPTableRelations (PrivateAPI) @@ -48,6 +49,7 @@ { if ((self = [super init])) { relationData = [[NSMutableArray alloc] init]; + prefs = [NSUserDefaults standardUserDefaults]; } return self; @@ -61,6 +63,9 @@ // Set the table relation view's vertical gridlines if required [relationsTableView setGridStyleMask:([[NSUserDefaults standardUserDefaults] boolForKey:SPDisplayTableViewVerticalGridlines]) ? NSTableViewSolidVerticalGridLineMask : NSTableViewGridNone]; + // Set the double-click action in blank areas of the table to create new rows + [relationsTableView setEmptyDoubleClickAction:@selector(addRelation:)]; + // Set the strutcture and index view's font BOOL useMonospacedFont = [[NSUserDefaults standardUserDefaults] boolForKey:SPUseMonospacedFonts]; @@ -163,11 +168,16 @@ */ - (IBAction)addRelation:(id)sender { + + // Check whether table editing is permitted (necessary as some actions - eg table double-click - bypass validation) + if ([tableDocumentInstance isWorking] || [tablesListInstance tableType] != SPTableTypeTable) return; + // Set up the controls [addRelationTableBox setTitle:[NSString stringWithFormat:NSLocalizedString(@"Table: %@", @"Add Relation sheet title, showing table name"), [tablesListInstance tableName]]]; [columnPopUpButton removeAllItems]; - [columnPopUpButton addItemsWithTitles:[tableDataInstance columnNames]]; + NSArray *columnTitles = ([prefs boolForKey:SPAlphabeticalTableSorting])? [[tableDataInstance columnNames] sortedArrayUsingSelector:@selector(compare:)] : [tableDataInstance columnNames]; + [columnPopUpButton addItemsWithTitles:columnTitles]; [refTablePopUpButton removeAllItems]; @@ -540,7 +550,8 @@ // Add the valid columns if ([validColumns count] > 0) { - [refColumnPopUpButton addItemsWithTitles:validColumns]; + NSArray *columnTitles = ([prefs boolForKey:SPAlphabeticalTableSorting])? [validColumns sortedArrayUsingSelector:@selector(compare:)] : validColumns; + [refColumnPopUpButton addItemsWithTitles:columnTitles]; [refColumnPopUpButton setEnabled:YES]; [confirmAddRelationButton setEnabled:YES]; diff --git a/Source/SPTableStructure.h b/Source/SPTableStructure.h index 81905410..40b665b2 100644 --- a/Source/SPTableStructure.h +++ b/Source/SPTableStructure.h @@ -25,7 +25,7 @@ #import <MCPKit/MCPKit.h> -@class SPDatabaseDocument, SPTableFieldValidation, SPTableData, SPDatabaseData, SPTablesList, SPIndexesController; +@class SPDatabaseDocument, SPTableFieldValidation, SPTableData, SPDatabaseData, SPTablesList, SPIndexesController, SPTableView; @interface SPTableStructure : NSObject { @@ -43,7 +43,7 @@ IBOutlet id resetAutoIncrementSheet; IBOutlet id resetAutoIncrementValue; IBOutlet id resetAutoIncrementLine; - IBOutlet id tableSourceView; + IBOutlet SPTableView *tableSourceView; IBOutlet id addFieldButton; IBOutlet id copyFieldButton; IBOutlet id removeFieldButton; @@ -54,7 +54,7 @@ IBOutlet id addIndexButton; IBOutlet id removeIndexButton; IBOutlet id refreshIndexesButton; - IBOutlet id indexesTableView; + IBOutlet SPTableView *indexesTableView; IBOutlet NSSplitView *tablesIndexesSplitView; IBOutlet NSButton *indexesShowButton; diff --git a/Source/SPTableStructure.m b/Source/SPTableStructure.m index 1af8bc62..ebbb9f7e 100644 --- a/Source/SPTableStructure.m +++ b/Source/SPTableStructure.m @@ -29,6 +29,7 @@ #import "SPTableInfo.h" #import "SPTablesList.h" #import "SPTableData.h" +#import "SPTableView.h" #import "SPDatabaseData.h" #import "SPSQLParser.h" #import "SPAlertSheets.h" @@ -83,6 +84,10 @@ // Set the structure and index view's vertical gridlines if required [tableSourceView setGridStyleMask:([prefs boolForKey:SPDisplayTableViewVerticalGridlines]) ? NSTableViewSolidVerticalGridLineMask : NSTableViewGridNone]; #endif + + // Set the double-click action in blank areas of the table to create new rows + [tableSourceView setEmptyDoubleClickAction:@selector(addField:)]; + #ifndef SP_REFACTOR /* set font from prefs */ // Set the strutcture and index view's font [tableSourceView setFont:([prefs boolForKey:SPUseMonospacedFonts]) ? [NSFont fontWithName:SPDefaultMonospacedFontName size:[NSFont smallSystemFontSize]] : [NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; @@ -474,6 +479,10 @@ */ - (IBAction)addField:(id)sender { + + // Check whether table editing is permitted (necessary as some actions - eg table double-click - bypass validation) + if ([tableDocumentInstance isWorking] || [tablesListInstance tableType] != SPTableTypeTable) return; + // Check whether a save of the current row is required. if ( ![self saveRowOnDeselect] ) return; diff --git a/Source/SPTableStructureDelegate.m b/Source/SPTableStructureDelegate.m index e7f9d3be..4e04ca05 100644 --- a/Source/SPTableStructureDelegate.m +++ b/Source/SPTableStructureDelegate.m @@ -28,6 +28,7 @@ #import "SPDatabaseData.h" #import "SPDatabaseViewController.h" #import "SPTableData.h" +#import "SPTableView.h" #import "SPTableFieldValidation.h" @implementation SPTableStructure (SPTableStructureDelegate) @@ -515,7 +516,7 @@ } // Check if UNSIGNED and ZEROFILL is allowed else if([[aTableColumn identifier] isEqualToString:@"zerofill"] || [[aTableColumn identifier] isEqualToString:@"unsigned"]) { - [aCell setEnabled:([fieldValidation isFieldTypeNumeric:theRowType])]; + [aCell setEnabled:([fieldValidation isFieldTypeNumeric:theRowType] && ![theRowType isEqualToString:@"BIT"])]; } // Check if BINARY is allowed else if([[aTableColumn identifier] isEqualToString:@"binary"]) { diff --git a/Source/SPTableTriggers.h b/Source/SPTableTriggers.h index 70eeeb2e..5cc8d59d 100644 --- a/Source/SPTableTriggers.h +++ b/Source/SPTableTriggers.h @@ -25,6 +25,8 @@ #import <MCPKit/MCPKit.h> +@class SPTableView; + @interface SPTableTriggers : NSObject { IBOutlet id tableDocumentInstance; @@ -36,7 +38,7 @@ IBOutlet NSButton *addTriggerButton; IBOutlet NSButton *removeTriggerButton; IBOutlet NSButton *refreshTriggersButton; - IBOutlet NSTableView *triggersTableView; + IBOutlet SPTableView *triggersTableView; IBOutlet NSPanel *addTriggerPanel; IBOutlet NSTextField *labelTextField; diff --git a/Source/SPTableTriggers.m b/Source/SPTableTriggers.m index bc5fc4f6..742579d8 100644 --- a/Source/SPTableTriggers.m +++ b/Source/SPTableTriggers.m @@ -27,6 +27,7 @@ #import "SPDatabaseDocument.h" #import "SPTablesList.h" #import "SPTableData.h" +#import "SPTableView.h" #import "SPAlertSheets.h" #import "SPServerSupport.h" @@ -76,6 +77,9 @@ static const NSString *SPTriggerSQLMode = @"TriggerSQLMode"; // Set the table triggers view's vertical gridlines if required [triggersTableView setGridStyleMask:([[NSUserDefaults standardUserDefaults] boolForKey:SPDisplayTableViewVerticalGridlines]) ? NSTableViewSolidVerticalGridLineMask : NSTableViewGridNone]; + // Set the double-click action in blank areas of the table to create new rows + [triggersTableView setEmptyDoubleClickAction:@selector(addTrigger:)]; + // Set the strutcture and index view's font BOOL useMonospacedFont = [[NSUserDefaults standardUserDefaults] boolForKey:SPUseMonospacedFonts]; @@ -263,6 +267,10 @@ static const NSString *SPTriggerSQLMode = @"TriggerSQLMode"; */ - (IBAction)addTrigger:(id)sender { + + // Check whether table editing is permitted (necessary as some actions - eg table double-click - bypass validation) + if ([tableDocumentInstance isWorking] || [tablesListInstance tableType] != SPTableTypeTable) return; + [NSApp beginSheet:addTriggerPanel modalForWindow:[tableDocumentInstance parentWindow] modalDelegate:self diff --git a/Source/SPTableView.h b/Source/SPTableView.h index e39df82f..11656e5b 100644 --- a/Source/SPTableView.h +++ b/Source/SPTableView.h @@ -25,8 +25,11 @@ @interface SPTableView : NSTableView { BOOL tabEditingDisabled; + SEL emptyDoubleClickAction; } @property (readwrite, assign) BOOL tabEditingDisabled; +- (void)setEmptyDoubleClickAction:(SEL)aSelector; + @end diff --git a/Source/SPTableView.m b/Source/SPTableView.m index d678fef3..9139bb79 100644 --- a/Source/SPTableView.m +++ b/Source/SPTableView.m @@ -34,10 +34,34 @@ @end +@interface SPTableView (PrivateAPI) + +- (void)_doubleClickAction; + +@end + + @implementation SPTableView @synthesize tabEditingDisabled; +- (id) init +{ + if ((self = [super init])) { + emptyDoubleClickAction = NULL; + } + return self; +} + +- (void) awakeFromNib +{ + [super setDoubleAction:@selector(_doubleClickAction)]; + if ([NSTableView instancesRespondToSelector:@selector(awakeFromNib)]) + [super awakeFromNib]; +} + +#pragma mark - + /** * Right-click at row will select that row before ordering out the contextual menu * if not more than one row is selected. @@ -193,4 +217,25 @@ } } +- (void)setEmptyDoubleClickAction:(SEL)aSelector +{ + emptyDoubleClickAction = aSelector; +} + +@end + + +@implementation SPTableView (PrivateAPI) + +/** + * On a double click, determine whether the action was in the empty area + * of the current table; if so, perform the assigned emptyDoubleClick action. + */ +- (void)_doubleClickAction +{ + if ([super clickedRow] == -1 && [super clickedColumn] == -1 && emptyDoubleClickAction) { + [[self delegate] performSelector:emptyDoubleClickAction]; + } +} + @end diff --git a/Source/SPTablesList.h b/Source/SPTablesList.h index 914295a5..50a5f4c0 100644 --- a/Source/SPTablesList.h +++ b/Source/SPTablesList.h @@ -39,7 +39,7 @@ @interface SPTablesList : NSObject { - SPDatabaseDocument* tableDocumentInstance; + IBOutlet SPDatabaseDocument* tableDocumentInstance; #ifndef SP_REFACTOR /* ivars */ IBOutlet id tableSourceInstance; IBOutlet id tableContentInstance; @@ -81,6 +81,7 @@ IBOutlet NSMenuItem *separatorTableMenuItem; IBOutlet NSMenuItem *showCreateSyntaxMenuItem; IBOutlet NSMenuItem *separatorTableMenuItem2; + IBOutlet NSMenuItem *separatorTableMenuItem3; #endif MCPConnection *mySQLConnection; @@ -95,6 +96,7 @@ IBOutlet NSMenuItem *separatorTableContextMenuItem; IBOutlet NSMenuItem *showCreateSyntaxContextMenuItem; IBOutlet NSMenuItem *separatorTableContextMenuItem2; + IBOutlet NSMenuItem *separatorTableContextMenuItem3; #endif NSMutableArray *tables; diff --git a/Source/SPTablesList.m b/Source/SPTablesList.m index 5ccb9bae..5054798c 100644 --- a/Source/SPTablesList.m +++ b/Source/SPTablesList.m @@ -49,13 +49,20 @@ #import "SPWindowController.h" #import "SPAppController.h" -@interface SPTablesList (PrivateAPI) +// Constants +static NSString *SPAddRow = @"SPAddRow"; +static NSString *SPAddNewTable = @"SPAddNewTable"; +static NSString *SPRemoveTable = @"SPRemoveTable"; +static NSString *SPTruncateTable = @"SPTruncateTable"; +static NSString *SPDuplicateTable = @"SPDuplicateTable"; -- (void)removeTable; -- (void)truncateTable; -- (void)addTable; -- (void)copyTable; -- (void)renameTableOfType:(SPTableType)tableType from:(NSString *)oldTableName to:(NSString *)newTableName; +@interface SPTablesList () + +- (void)_removeTable; +- (void)_truncateTable; +- (void)_addTable; +- (void)_copyTable; +- (void)_renameTableOfType:(SPTableType)tableType from:(NSString *)oldTableName to:(NSString *)newTableName; @end #endif @@ -383,7 +390,7 @@ modalForWindow:[tableDocumentInstance parentWindow] modalDelegate:self didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) - contextInfo:@"addTable"]; + contextInfo:SPAddNewTable]; } /** @@ -471,7 +478,7 @@ [alert setInformativeText:[NSString stringWithFormat:NSLocalizedString(@"Are you sure you want to delete the selected %@? This operation cannot be undone.", @"delete tables/views informative message"), tblTypes]]; } - [alert beginSheetModalForWindow:[tableDocumentInstance parentWindow] modalDelegate:self didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) contextInfo:@"removeRow"]; + [alert beginSheetModalForWindow:[tableDocumentInstance parentWindow] modalDelegate:self didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) contextInfo:SPRemoveTable]; } /** @@ -518,7 +525,7 @@ modalForWindow:[tableDocumentInstance parentWindow] modalDelegate:self didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) - contextInfo:@"copyTable"]; + contextInfo:SPDuplicateTable]; } /** @@ -604,7 +611,7 @@ [alert setInformativeText:NSLocalizedString(@"Are you sure you want to delete ALL records in the selected tables? This operation cannot be undone.", @"truncate tables informative message")]; } - [alert beginSheetModalForWindow:[tableDocumentInstance parentWindow] modalDelegate:self didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) contextInfo:@"truncateTable"]; + [alert beginSheetModalForWindow:[tableDocumentInstance parentWindow] modalDelegate:self didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) contextInfo:SPTruncateTable]; } /** @@ -658,27 +665,27 @@ else if ([sheet respondsToSelector:@selector(window)]) [[sheet window] orderOut:nil]; - if ([contextInfo isEqualToString:@"addRow"]) { + if ([contextInfo isEqualToString:SPAddRow]) { alertSheetOpened = NO; } - else if ([contextInfo isEqualToString:@"removeRow"]) { + else if ([contextInfo isEqualToString:SPRemoveTable]) { if (returnCode == NSAlertDefaultReturn) { - [self performSelector:@selector(removeTable) withObject:nil afterDelay:0.0]; + [self performSelector:@selector(_removeTable) withObject:nil afterDelay:0.0]; } } - else if ([contextInfo isEqualToString:@"truncateTable"]) { + else if ([contextInfo isEqualToString:SPTruncateTable]) { if (returnCode == NSAlertDefaultReturn) { - [self truncateTable]; + [self _truncateTable]; } } - else if ([contextInfo isEqualToString:@"addTable"]) { + else if ([contextInfo isEqualToString:SPAddNewTable]) { if (returnCode == NSOKButton) { - [self addTable]; + [self _addTable]; } } - else if ([contextInfo isEqualToString:@"copyTable"]) { + else if ([contextInfo isEqualToString:SPDuplicateTable]) { if (returnCode == NSOKButton) { - [self copyTable]; + [self _copyTable]; } } } @@ -826,6 +833,7 @@ // Context menu [renameTableContextMenuItem setHidden:YES]; [openTableInNewTabContextMenuItem setHidden:YES]; + [separatorTableContextMenuItem3 setHidden:YES]; [duplicateTableContextMenuItem setHidden:YES]; [separatorTableContextMenuItem setHidden:YES]; [separatorTableContextMenuItem2 setHidden:NO]; @@ -835,6 +843,7 @@ // 'Gear' menu [renameTableMenuItem setHidden:YES]; [openTableInNewTabMenuItem setHidden:YES]; + [separatorTableMenuItem3 setHidden:YES]; [duplicateTableMenuItem setHidden:YES]; [separatorTableMenuItem setHidden:YES]; [separatorTableMenuItem2 setHidden:NO]; @@ -923,6 +932,7 @@ [truncateTableButton setHidden:YES]; [removeTableMenuItem setTitle:NSLocalizedString(@"Delete View", @"delete view menu title")]; [openTableInNewTabMenuItem setHidden:NO]; + [separatorTableMenuItem3 setHidden:NO]; [openTableInNewTabMenuItem setTitle:NSLocalizedString(@"Open View in New Tab", @"open view in new table title")]; [showCreateSyntaxMenuItem setHidden:NO]; [showCreateSyntaxMenuItem setTitle:NSLocalizedString(@"Show Create View Syntax...", @"show create view syntax menu item")]; @@ -934,6 +944,7 @@ [truncateTableContextMenuItem setHidden:YES]; [removeTableContextMenuItem setTitle:NSLocalizedString(@"Delete View", @"delete view menu title")]; [openTableInNewTabContextMenuItem setHidden:NO]; + [separatorTableContextMenuItem3 setHidden:NO]; [openTableInNewTabContextMenuItem setTitle:NSLocalizedString(@"Open View in New Tab", @"open view in new table title")]; [showCreateSyntaxContextMenuItem setHidden:NO]; [showCreateSyntaxContextMenuItem setTitle:NSLocalizedString(@"Show Create View Syntax...", @"show create view syntax menu item")]; @@ -965,6 +976,7 @@ [removeTableMenuItem setTitle:NSLocalizedString(@"Delete Table", @"delete table menu title")]; [openTableInNewTabMenuItem setHidden:NO]; [openTableInNewTabMenuItem setTitle:NSLocalizedString(@"Open Table in New Tab", @"open table in new table title")]; + [separatorTableMenuItem3 setHidden:NO]; [showCreateSyntaxMenuItem setHidden:NO]; [showCreateSyntaxMenuItem setTitle:NSLocalizedString(@"Show Create Table Syntax...", @"show create table syntax menu item")]; @@ -976,6 +988,7 @@ [truncateTableContextMenuItem setTitle:NSLocalizedString(@"Truncate Table", @"truncate table menu title")]; [removeTableContextMenuItem setTitle:NSLocalizedString(@"Delete Table", @"delete table menu title")]; [openTableInNewTabContextMenuItem setHidden:NO]; + [separatorTableContextMenuItem3 setHidden:NO]; [openTableInNewTabContextMenuItem setTitle:NSLocalizedString(@"Open Table in New Tab", @"open table in new table title")]; [showCreateSyntaxContextMenuItem setHidden:NO]; [showCreateSyntaxContextMenuItem setTitle:NSLocalizedString(@"Show Create Table Syntax...", @"show create table syntax menu item")]; @@ -1000,6 +1013,7 @@ [removeTableMenuItem setTitle:NSLocalizedString(@"Delete Procedure", @"delete proc menu title")]; [openTableInNewTabMenuItem setHidden:NO]; [openTableInNewTabMenuItem setTitle:NSLocalizedString(@"Open Procedure in New Tab", @"open procedure in new table title")]; + [separatorTableMenuItem3 setHidden:NO]; [showCreateSyntaxMenuItem setHidden:NO]; [showCreateSyntaxMenuItem setTitle:NSLocalizedString(@"Show Create Procedure Syntax...", @"show create proc syntax menu item")]; @@ -1010,6 +1024,7 @@ [truncateTableContextMenuItem setHidden:YES]; [removeTableContextMenuItem setTitle:NSLocalizedString(@"Delete Procedure", @"delete proc menu title")]; [openTableInNewTabContextMenuItem setHidden:NO]; + [separatorTableContextMenuItem3 setHidden:NO]; [openTableInNewTabContextMenuItem setTitle:NSLocalizedString(@"Open Procedure in New Tab", @"open procedure in new table title")]; [showCreateSyntaxContextMenuItem setHidden:NO]; [showCreateSyntaxContextMenuItem setTitle:NSLocalizedString(@"Show Create Procedure Syntax...", @"show create proc syntax menu item")]; @@ -1033,6 +1048,7 @@ [truncateTableButton setHidden:YES]; [removeTableMenuItem setTitle:NSLocalizedString(@"Delete Function", @"delete func menu title")]; [openTableInNewTabMenuItem setHidden:NO]; + [separatorTableMenuItem3 setHidden:NO]; [openTableInNewTabMenuItem setTitle:NSLocalizedString(@"Open Function in New Tab", @"open function in new table title")]; [showCreateSyntaxMenuItem setHidden:NO]; [showCreateSyntaxMenuItem setTitle:NSLocalizedString(@"Show Create Function Syntax...", @"show create func syntax menu item")]; @@ -1044,6 +1060,7 @@ [truncateTableContextMenuItem setHidden:YES]; [removeTableContextMenuItem setTitle:NSLocalizedString(@"Delete Function", @"delete func menu title")]; [openTableInNewTabContextMenuItem setHidden:NO]; + [separatorTableContextMenuItem3 setHidden:NO]; [openTableInNewTabContextMenuItem setTitle:NSLocalizedString(@"Open Function in New Tab", @"open function in new table title")]; [showCreateSyntaxContextMenuItem setHidden:NO]; [showCreateSyntaxContextMenuItem setTitle:NSLocalizedString(@"Show Create Function Syntax...", @"show create func syntax menu item")]; @@ -1449,7 +1466,7 @@ @try { // first: update the database - [self renameTableOfType:selectedTableType from:selectedTableName to:newTableName]; + [self _renameTableOfType:selectedTableType from:selectedTableName to:newTableName]; // second: update the table list if (isTableListFiltered) { @@ -1672,8 +1689,8 @@ NSPasteboard *pboard = [info draggingPasteboard]; // tables were dropped coming from the Navigator - if ( [[pboard types] containsObject:@"SPDragTableDataFromNavigatorPboardType"] ) { - NSString *query = [pboard stringForType:@"SPDragTableDataFromNavigatorPboardType"]; + if ( [[pboard types] containsObject:SPNavigatorTableDataPasteboardDragType] ) { + NSString *query = [pboard stringForType:SPNavigatorTableDataPasteboardDragType]; if(!query) return NO; [mySQLConnection queryString:query]; @@ -1984,7 +2001,7 @@ object:tableDocumentInstance]; - [tablesListView registerForDraggedTypes:[NSArray arrayWithObjects:@"SPDragTableDataFromNavigatorPboardType", nil]]; + [tablesListView registerForDraggedTypes:[NSArray arrayWithObjects:SPNavigatorTableDataPasteboardDragType, nil]]; } #endif @@ -2016,14 +2033,11 @@ #endif #ifndef SP_REFACTOR /* operations performed on whole tables */ -@end - -@implementation SPTablesList (PrivateAPI) /** * Removes the selected object (table, view, procedure, function, etc.) from the database and tableView. */ -- (void)removeTable +- (void)_removeTable { NSIndexSet *indexes = [tablesListView selectedRowIndexes]; [tablesListView selectRowIndexes:[NSIndexSet indexSet] byExtendingSelection:NO]; @@ -2117,7 +2131,7 @@ /** * Trucates the selected table(s). */ -- (void)truncateTable +- (void)_truncateTable { NSIndexSet *indexes = [tablesListView selectedRowIndexes]; @@ -2159,7 +2173,7 @@ /** * Adds a new table table to the database using the selected character set encoding and storage engine. */ -- (void)addTable +- (void)_addTable { NSString *charSetStatement = @""; NSString *engineStatement = @""; @@ -2244,7 +2258,7 @@ SPBeginAlertSheet(NSLocalizedString(@"Error adding new table", @"error adding new table message"), NSLocalizedString(@"OK", @"OK button"), nil, nil, [tableDocumentInstance parentWindow], self, - @selector(sheetDidEnd:returnCode:contextInfo:), @"addRow", + @selector(sheetDidEnd:returnCode:contextInfo:), SPAddRow, [NSString stringWithFormat:NSLocalizedString(@"An error occurred while trying to add the new table '%@'.\n\nMySQL said: %@", @"error adding new table informative message"), tableName, [mySQLConnection getLastErrorMessage]]); if (changeEncoding) [mySQLConnection restoreStoredEncoding]; @@ -2258,7 +2272,7 @@ /** * Copies the currently selected object (table, view, procedure, function, etc.). */ -- (void)copyTable +- (void)_copyTable { NSString *tableType = @""; @@ -2444,7 +2458,7 @@ * This function ONLY changes the database. It does NOT refresh the views etc. * CAREFUL: This function raises an exception if renaming fails, and does not show an error message. */ -- (void)renameTableOfType:(SPTableType)tableType from:(NSString *)oldTableName to:(NSString *)newTableName +- (void)_renameTableOfType:(SPTableType)tableType from:(NSString *)oldTableName to:(NSString *)newTableName { // check if the name really changed if ([oldTableName isEqualToString:newTableName]) return; @@ -2456,17 +2470,21 @@ // this code should be improved in case we find out that something uses table names like mytable-1, mytable-2, etc. NSString* tempTableName; int tempNumber; - for(tempNumber=2; tempNumber<100; tempNumber++) { + + for (tempNumber=2; tempNumber<100; tempNumber++) + { tempTableName = [NSString stringWithFormat:@"%@-%d",selectedTableName,tempNumber]; if ([self isTableNameValid:tempTableName forType:tableType]) break; } + if (tempNumber==100) { // we couldn't find a temporary name [NSException raise:@"No Tempname found" format:NSLocalizedString(@"An error occured while renaming '%@'. No temporary name could be found. Please try renaming to something else first.", @"rename table error - no temporary name found"), oldTableName]; } - [self renameTableOfType:tableType from:oldTableName to:tempTableName]; - [self renameTableOfType:tableType from:tempTableName to:newTableName]; + [self _renameTableOfType:tableType from:oldTableName to:tempTableName]; + [self _renameTableOfType:tableType from:tempTableName to:newTableName]; + return; } @@ -2478,6 +2496,7 @@ if ([mySQLConnection queryErrored]) { [NSException raise:@"MySQL Error" format:NSLocalizedString(@"An error occured while renaming '%@'.\n\nMySQL said: %@", @"rename table error informative message"), oldTableName, [mySQLConnection getLastErrorMessage]]; } + return; } @@ -2524,65 +2543,4 @@ } #endif -/** - * Check tableName for length and if the tableName doesn't match - * against current database table/view names (case-insensitive). - */ -- (BOOL)isTableNameValid:(NSString *)tableName forType:(SPTableType)tableType -{ - return [self isTableNameValid:tableName forType:tableType ignoringSelectedTable:NO]; -} - -/** - * Check tableName for length and if the tableName doesn't match - * against current database table/view names (case-insensitive). - */ -- (BOOL)isTableNameValid:(NSString *)tableName forType:(SPTableType)tableType ignoringSelectedTable:(BOOL)ignoreSelectedTable -{ - BOOL isValid = YES; - - // delete trailing whitespaces since 'foo ' or ' ' are not valid table names - NSString *fieldStr = [tableName stringByMatching:@"(.*?)\\s*$" capture:1]; - NSString *lowercaseFieldStr = [fieldStr lowercaseString]; - - // If table name has trailing whitespaces return 'no valid' - if([fieldStr length] != [tableName length]) return NO; - - // empty table names are invalid - if([fieldStr length] == 0) return NO; - - - NSArray *similarTables; - switch (tableType) { - case SPTableTypeView: - case SPTableTypeTable: - similarTables = [self allTableAndViewNames]; - break; - case SPTableTypeProc: - similarTables = [self allProcedureNames]; - break; - case SPTableTypeFunc: - similarTables = [self allFunctionNames]; - break; - default: - // if some other table type is given, just return yes - // better a mysql error than not being able to change something at all - return YES; - } - - for(id table in similarTables) { - //compare case insensitive here - if([lowercaseFieldStr isEqualToString:[table lowercaseString]]) { - if (ignoreSelectedTable) { - // if table is the selectedTable, ignore it - // we must compare CASE SENSITIVE here! - if ([table isEqualToString:selectedTableName]) continue; - } - isValid = NO; - break; - } - } - return isValid; -} - @end diff --git a/Source/SPTextView.h b/Source/SPTextView.h index bc4a3eb8..3309ed1d 100644 --- a/Source/SPTextView.h +++ b/Source/SPTextView.h @@ -32,10 +32,9 @@ @class SPNarrowDownCompletion, SPDatabaseDocument, SPTablesList, SPCustomQuery; -#ifndef SP_REFACTOR @interface SPTextView : NSTextView -#else -@interface SPTextView : NSTextView <NSTextStorageDelegate> +#ifdef SP_REFACTOR +<NSTextStorageDelegate> #endif { IBOutlet SPDatabaseDocument *tableDocumentInstance; diff --git a/Source/SPTextView.m b/Source/SPTextView.m index fa01709e..cb0e4d9e 100644 --- a/Source/SPTextView.m +++ b/Source/SPTextView.m @@ -3444,13 +3444,13 @@ NSInteger _alphabeticSort(id string1, id string2, void *reverse) } // Insert selected items coming from the Navigator - if ( [[pboard types] containsObject:@"SPDragFromNavigatorPboardType"] ) { + if ( [[pboard types] containsObject:SPNavigatorPasteboardDragType] ) { NSPoint draggingLocation = [sender draggingLocation]; draggingLocation = [self convertPoint:draggingLocation fromView:nil]; NSUInteger characterIndex = [self characterIndexOfPoint:draggingLocation]; [self setSelectedRange:NSMakeRange(characterIndex,0)]; - NSKeyedUnarchiver *unarchiver = [[[NSKeyedUnarchiver alloc] initForReadingWithData:[pboard dataForType:@"SPDragFromNavigatorPboardType"]] autorelease]; + NSKeyedUnarchiver *unarchiver = [[[NSKeyedUnarchiver alloc] initForReadingWithData:[pboard dataForType:SPNavigatorPasteboardDragType]] autorelease]; NSArray *draggedItems = [[NSArray alloc] initWithArray:(NSArray *)[unarchiver decodeObjectForKey:@"itemdata"]]; [unarchiver finishDecoding]; diff --git a/Source/SPWindowController.h b/Source/SPWindowController.h index 5626c580..ae1e9d27 100644 --- a/Source/SPWindowController.h +++ b/Source/SPWindowController.h @@ -37,11 +37,11 @@ } // Database connection management -- (IBAction) addNewConnection:(id)sender; -- (IBAction) moveSelectedTabInNewWindow:(id)sender; -- (SPDatabaseDocument *) selectedTableDocument; -- (void) updateSelectedTableDocument; -- (void) updateAllTabTitles:(id)sender; +- (IBAction)addNewConnection:(id)sender; +- (IBAction)moveSelectedTabInNewWindow:(id)sender; +- (SPDatabaseDocument *)selectedTableDocument; +- (void)updateSelectedTableDocument; +- (void)updateAllTabTitles:(id)sender; - (IBAction)closeTab:(id)sender; - (IBAction)selectNextDocumentTab:(id)sender; - (IBAction)selectPreviousDocumentTab:(id)sender; diff --git a/Source/SPWindowController.m b/Source/SPWindowController.m index e9b73491..ed18335d 100644 --- a/Source/SPWindowController.m +++ b/Source/SPWindowController.m @@ -42,7 +42,7 @@ /** * awakeFromNib */ -- (void) awakeFromNib +- (void)awakeFromNib { selectedTableDocument = nil; @@ -67,9 +67,13 @@ [tabBar setTearOffStyle:PSMTabBarTearOffAlphaWindow]; [tabBar setUsesSafariStyleDragging:YES]; - // hook up add tab button + // Hook up add tab button [tabBar setCreateNewTabTarget:self]; [tabBar setCreateNewTabAction:@selector(addNewConnection:)]; + + // Set the double click target and action + [tabBar setDoubleClickTarget:self]; + [tabBar setDoubleClickAction:@selector(openDatabaseInNewTab)]; // Retrieve references to the 'Close Window' and 'Close Tab' menus. These are updated as window focus changes. closeWindowMenuItem = [[[[NSApp mainMenu] itemWithTag:SPMainMenuFile] submenu] itemWithTag:1003]; @@ -100,7 +104,6 @@ */ - (IBAction) addNewConnection:(id)sender { - // Create a new database connection view SPDatabaseDocument *newTableDocument = [[SPDatabaseDocument alloc] init]; [newTableDocument setParentWindowController:self]; @@ -160,14 +163,14 @@ */ - (IBAction) closeTab:(id)sender { - // Return if the selected tab shouldn't be closed if (![selectedTableDocument parentTabShouldClose]) return; // If there are multiple tabs, close the front tab. if ([tabView numberOfTabViewItems] > 1) { [tabView removeTabViewItem:[tabView selectedTabViewItem]]; - } else { + } + else { [[self window] performClose:self]; } } @@ -326,11 +329,22 @@ } } + - (void)setHideForSingleTab:(BOOL)hide { [tabBar setHideForSingleTab:hide]; } +/** + * Opens the current connection in a new tab, but only if it's already connected. + */ +- (void)openDatabaseInNewTab +{ + if ([selectedTableDocument database]) { + [selectedTableDocument openDatabaseInNewTab:self]; + } +} + #pragma mark - #pragma mark Tab view delegate methods @@ -572,6 +586,20 @@ } /** + * Displays the current tab's context menu. + */ +- (NSMenu *)tabView:(NSTabView *)aTabView menuForTabViewItem:(NSTabViewItem *)tabViewItem +{ + NSMenu *menu = [[NSMenu alloc] init]; + + [menu addItemWithTitle:NSLocalizedString(@"Close Tab", @"close tab context menu item") action:@selector(closeTab:) keyEquivalent:@""]; + [menu insertItem:[NSMenuItem separatorItem] atIndex:1]; + [menu addItemWithTitle:NSLocalizedString(@"Open in New Tab", @"open connection in new tab context menu item") action:@selector(openDatabaseInNewTab:) keyEquivalent:@""]; + + return [menu autorelease]; +} + +/** * When tab drags start, show all the tab bars. This allows adding tabs to windows * containing only one tab - where the bar is normally hidden. */ |