diff options
Diffstat (limited to 'Source/SPDatabaseDocument.m')
-rw-r--r-- | Source/SPDatabaseDocument.m | 1190 |
1 files changed, 1185 insertions, 5 deletions
diff --git a/Source/SPDatabaseDocument.m b/Source/SPDatabaseDocument.m index a82632d0..d591121d 100644 --- a/Source/SPDatabaseDocument.m +++ b/Source/SPDatabaseDocument.m @@ -34,7 +34,6 @@ #import "SPConnectionHandler.h" #import "SPConnectionControllerInitializer.h" #import "SPTablesList.h" -#import "SPTableStructure.h" #import "SPDatabaseStructure.h" #import "SPFileHandle.h" #import "SPKeychain.h" @@ -54,7 +53,6 @@ #import "SPTableData.h" #import "SPDatabaseData.h" #import "SPDatabaseStructure.h" -#import "SPAppController.h" #import "SPExtendedTableInfo.h" #import "SPHistoryController.h" #import "SPPreferenceController.h" @@ -72,9 +70,6 @@ #import "SPCopyTable.h" #import "SPServerSupport.h" #import "SPTooltip.h" -#import "SPDatabaseViewController.h" -#import "SPBundleHTMLOutputController.h" -#import "SPConnectionDelegate.h" #import "SPThreadAdditions.h" #import "RegexKitLite.h" #import "SPTextView.h" @@ -83,6 +78,18 @@ #import "SPGotoDatabaseController.h" #import "SPFunctions.h" #import "SPCreateDatabaseInfo.h" +#ifndef SP_CODA /* headers */ +#import "SPAppController.h" +#import "SPBundleHTMLOutputController.h" +#endif +#import "SPTableTriggers.h" +#ifdef SP_CODA /* headers */ +#import "SPTableStructure.h" +#import "SPTableStructureLoading.h" +#endif +#import "SPPrintAccessory.h" +#import "MGTemplateEngine.h" +#import "ICUTemplateMatcher.h" #import <SPMySQL/SPMySQL.h> @@ -108,6 +115,18 @@ static int64_t SPDatabaseDocumentInstanceCounter = 0; - (void)_addPreferenceObservers; - (void)_removePreferenceObservers; +#pragma mark - SPDatabaseViewControllerPrivateAPI + +- (void)_loadTabTask:(NSNumber *)tabViewItemIndexNumber; +- (void)_loadTableTask; + +#pragma mark - SPConnectionDelegate + +- (void) closeAndDisconnect; + +- (NSString *)keychainPasswordForConnection:(SPMySQLConnection *)connection; +- (NSString *)keychainPasswordForSSHConnection:(SPMySQLConnection *)connection; + @end @implementation SPDatabaseDocument @@ -121,6 +140,7 @@ static int64_t SPDatabaseDocumentInstanceCounter = 0; @synthesize databaseStructureRetrieval; @synthesize processID; @synthesize instanceId; +@synthesize dbTablesTableView = dbTablesTableView; #pragma mark - @@ -6524,6 +6544,1166 @@ static int64_t SPDatabaseDocumentInstanceCounter = 0; [prefs removeObserver:[SPQueryController sharedQueryController] forKeyPath:SPDisplayTableViewVerticalGridlines]; } +#pragma mark - SPDatabaseViewController + +#pragma mark Getters + +#ifndef SP_CODA /* getters */ +/** + * Returns the master database view, containing the tables list and views for + * table setup and contents. + */ +- (NSView *)databaseView +{ + return parentView; +} +#endif + +/** + * Returns the name of the currently selected table/view/procedure/function. + */ +- (NSString *)table +{ + return selectedTableName; +} + +/** + * Returns the currently selected table type, or -1 if no table or multiple tables are selected + */ +- (SPTableType)tableType +{ + return selectedTableType; +} + +/** + * Returns YES if table source has already been loaded + */ +- (BOOL)structureLoaded +{ + return structureLoaded; +} + +/** + * Returns YES if table content has already been loaded + */ +- (BOOL)contentLoaded +{ + return contentLoaded; +} + +/** + * Returns YES if table status has already been loaded + */ +- (BOOL)statusLoaded +{ + return statusLoaded; +} + +#ifndef SP_CODA /* toolbar ibactions */ + +#pragma mark - +#pragma mark Tab view control and delegate methods + +//WARNING: Might be called from code in background threads +- (IBAction)viewStructure:(id)sender +{ + // Cancel the selection if currently editing a view and unable to save + if (![[self onMainThread] couldCommitCurrentViewActions]) { + [[mainToolbar onMainThread] setSelectedItemIdentifier:*SPViewModeToMainToolbarMap[[prefs integerForKey:SPLastViewMode]]]; + return; + } + + [[tableTabView onMainThread] selectTabViewItemAtIndex:0]; + [[mainToolbar onMainThread] setSelectedItemIdentifier:SPMainToolbarTableStructure]; + [spHistoryControllerInstance updateHistoryEntries]; + + [prefs setInteger:SPStructureViewMode forKey:SPLastViewMode]; +} + +- (IBAction)viewContent:(id)sender +{ + // Cancel the selection if currently editing a view and unable to save + if (![self couldCommitCurrentViewActions]) { + [mainToolbar setSelectedItemIdentifier:*SPViewModeToMainToolbarMap[[prefs integerForKey:SPLastViewMode]]]; + return; + } + + [tableTabView selectTabViewItemAtIndex:1]; + [mainToolbar setSelectedItemIdentifier:SPMainToolbarTableContent]; + [spHistoryControllerInstance updateHistoryEntries]; + + [prefs setInteger:SPContentViewMode forKey:SPLastViewMode]; +} + +- (IBAction)viewQuery:(id)sender +{ + // Cancel the selection if currently editing a view and unable to save + if (![self couldCommitCurrentViewActions]) { + [mainToolbar setSelectedItemIdentifier:*SPViewModeToMainToolbarMap[[prefs integerForKey:SPLastViewMode]]]; + return; + } + + [tableTabView selectTabViewItemAtIndex:2]; + [mainToolbar setSelectedItemIdentifier:SPMainToolbarCustomQuery]; + [spHistoryControllerInstance updateHistoryEntries]; + + // Set the focus on the text field + [parentWindow makeFirstResponder:customQueryTextView]; + + [prefs setInteger:SPQueryEditorViewMode forKey:SPLastViewMode]; +} + +- (IBAction)viewStatus:(id)sender +{ + // Cancel the selection if currently editing a view and unable to save + if (![self couldCommitCurrentViewActions]) { + [mainToolbar setSelectedItemIdentifier:*SPViewModeToMainToolbarMap[[prefs integerForKey:SPLastViewMode]]]; + return; + } + + [tableTabView selectTabViewItemAtIndex:3]; + [mainToolbar setSelectedItemIdentifier:SPMainToolbarTableInfo]; + [spHistoryControllerInstance updateHistoryEntries]; + + if ([[self table] length]) { + [extendedTableInfoInstance loadTable:[self table]]; + } + + [parentWindow makeFirstResponder:[extendedTableInfoInstance valueForKeyPath:@"tableCreateSyntaxTextView"]]; + + [prefs setInteger:SPTableInfoViewMode forKey:SPLastViewMode]; +} + +- (IBAction)viewRelations:(id)sender +{ + // Cancel the selection if currently editing a view and unable to save + if (![self couldCommitCurrentViewActions]) { + [mainToolbar setSelectedItemIdentifier:*SPViewModeToMainToolbarMap[[prefs integerForKey:SPLastViewMode]]]; + return; + } + + [tableTabView selectTabViewItemAtIndex:4]; + [mainToolbar setSelectedItemIdentifier:SPMainToolbarTableRelations]; + [spHistoryControllerInstance updateHistoryEntries]; + + [prefs setInteger:SPRelationsViewMode forKey:SPLastViewMode]; +} + +- (IBAction)viewTriggers:(id)sender +{ + // Cancel the selection if currently editing a view and unable to save + if (![self couldCommitCurrentViewActions]) { + [mainToolbar setSelectedItemIdentifier:*SPViewModeToMainToolbarMap[[prefs integerForKey:SPLastViewMode]]]; + return; + } + + [tableTabView selectTabViewItemAtIndex:5]; + [mainToolbar setSelectedItemIdentifier:SPMainToolbarTableTriggers]; + [spHistoryControllerInstance updateHistoryEntries]; + + [prefs setInteger:SPTriggersViewMode forKey:SPLastViewMode]; +} +#endif + +/** + * Mark the structure tab for refresh when it's next switched to, + * or reload the view if it's currently active + */ +- (void)setStructureRequiresReload:(BOOL)reload +{ + BOOL reloadRequired = reload; + +#ifndef SP_CODA + if ([self currentlySelectedView] == SPTableViewStructure) { + reloadRequired = NO; + } +#endif + + if (reloadRequired && selectedTableName) { + [tableSourceInstance loadTable:selectedTableName]; + } + else { + structureLoaded = !reload; + } +} + +/** + * Mark the content tab for refresh when it's next switched to, + * or reload the view if it's currently active + */ +- (void)setContentRequiresReload:(BOOL)reload +{ + if (reload && selectedTableName +#ifndef SP_CODA /* check which tab is selected */ + && [self currentlySelectedView] == SPTableViewContent +#endif + ) { + [tableContentInstance loadTable:selectedTableName]; + } + else { + contentLoaded = !reload; + } +} + +/** + * Mark the extended tab info for refresh when it's next switched to, + * or reload the view if it's currently active + */ +- (void)setStatusRequiresReload:(BOOL)reload +{ + if (reload && selectedTableName +#ifndef SP_CODA /* check which tab is selected */ + && [self currentlySelectedView] == SPTableViewStatus +#endif + ) { + [[extendedTableInfoInstance onMainThread] loadTable:selectedTableName]; + } + else { + statusLoaded = !reload; + } +} + +/** + * Mark the relations tab for refresh when it's next switched to, + * or reload the view if it's currently active + */ +- (void)setRelationsRequiresReload:(BOOL)reload +{ + if (reload && selectedTableName +#ifndef SP_CODA /* check which tab is selected */ + && [self currentlySelectedView] == SPTableViewRelations +#endif + ) { + [[tableRelationsInstance onMainThread] refreshRelations:self]; + } + else { + relationsLoaded = !reload; + } +} + +#ifndef SP_CODA /* !!! respond to tab change */ +/** + * Triggers a task to update the newly selected tab view, ensuring + * the data is fully loaded and up-to-date. + */ +- (void)tabView:(NSTabView *)aTabView didSelectTabViewItem:(NSTabViewItem *)tabViewItem +{ + [self startTaskWithDescription:[NSString stringWithFormat:NSLocalizedString(@"Loading %@...", @"Loading table task string"), [self table]]]; + + // We can't pass aTabView or tabViewItem UI objects to a bg thread, but since the change should already + // be done in *did*SelectTabViewItem we can just ask the tab view for the current selection index and use that + SPTableViewType newView = [self currentlySelectedView]; + + if ([NSThread isMainThread]) { + [NSThread detachNewThreadWithName:SPCtxt(@"SPDatabaseDocument view load task",self) + target:self + selector:@selector(_loadTabTask:) + object:@(newView)]; + } + else { + [self _loadTabTask:@(newView)]; + } +} +#endif + +#pragma mark - +#pragma mark Table control + +/** + * Loads a specified table into the database view, and ensures it's selected in + * the tables list. Passing a table name of nil will deselect any currently selected + * table, but will leave multiple selections intact. + * If this method is supplied with the currently selected name, a reload rather than + * a load will be triggered. + */ +- (void)loadTable:(NSString *)aTable ofType:(SPTableType)aTableType +{ + // Ensure a connection is still present + if (![mySQLConnection isConnected]) return; + + // If the supplied table name was nil, clear the views. + if (!aTable) { + + // Update the selected table name and type + if (selectedTableName) SPClear(selectedTableName); + + selectedTableType = SPTableTypeNone; + + // Clear the views + [[tablesListInstance onMainThread] setSelectionState:nil]; + [tableSourceInstance loadTable:nil]; + [tableContentInstance loadTable:nil]; +#ifndef SP_CODA /* [extendedTableInfoInstance loadTable:] */ + [[extendedTableInfoInstance onMainThread] loadTable:nil]; + [[tableTriggersInstance onMainThread] resetInterface]; + [[tableRelationsInstance onMainThread] refreshRelations:self]; +#endif + structureLoaded = NO; + contentLoaded = NO; + statusLoaded = NO; + triggersLoaded = NO; + relationsLoaded = NO; + +#ifndef SP_CODA + // Update the window title + [self updateWindowTitle:self]; + + // Add a history entry + [spHistoryControllerInstance updateHistoryEntries]; +#endif + + // Notify listeners of the table change + [[NSNotificationCenter defaultCenter] postNotificationOnMainThreadWithName:SPTableChangedNotification object:self]; + + return; + } + + BOOL isReloading = (selectedTableName && [selectedTableName isEqualToString:aTable]); + + // Store the new name + if (selectedTableName) [selectedTableName release]; + + selectedTableName = [[NSString alloc] initWithString:aTable]; + selectedTableType = aTableType; + + // Start a task + if (isReloading) { + [self startTaskWithDescription:NSLocalizedString(@"Reloading...", @"Reloading table task string")]; + } + else { + [self startTaskWithDescription:[NSString stringWithFormat:NSLocalizedString(@"Loading %@...", @"Loading table task string"), aTable]]; + } + + // Update the tables list interface - also updates menus to reflect the selected table type + [[tablesListInstance onMainThread] setSelectionState:[NSDictionary dictionaryWithObjectsAndKeys:aTable, @"name", [NSNumber numberWithInteger:aTableType], @"type", nil]]; + + // If on the main thread, fire up a thread to deal with view changes and data loading; + // if already on a background thread, make the changes on the existing thread. + if ([NSThread isMainThread]) { + [NSThread detachNewThreadWithName:SPCtxt(@"SPDatabaseDocument table load task",self) + target:self + selector:@selector(_loadTableTask) + object:nil]; + } + else { + [self _loadTableTask]; + } +} + +/** + * In a threaded task, ensure that the supplied tab is loaded - + * usually as a result of switching to it. + */ +- (void)_loadTabTask:(NSNumber *)tabViewItemIndexNumber +{ + NSAutoreleasePool *tabLoadPool = [[NSAutoreleasePool alloc] init]; + + // If anything other than a single table or view is selected, don't proceed. + if (![self table] || ([tablesListInstance tableType] != SPTableTypeTable && [tablesListInstance tableType] != SPTableTypeView)) + { + [self endTask]; + [tabLoadPool drain]; + return; + } + + // Get the tab view index and ensure the associated view is loaded + SPTableViewType selectedTabViewIndex = [tabViewItemIndexNumber integerValue]; + + switch (selectedTabViewIndex) { + case SPTableViewStructure: + if (!structureLoaded) { + [tableSourceInstance loadTable:selectedTableName]; + structureLoaded = YES; + } + break; + case SPTableViewContent: + if (!contentLoaded) { + [tableContentInstance loadTable:selectedTableName]; + contentLoaded = YES; + } + break; +#ifndef SP_CODA /* case SPTableViewStatus: case SPTableViewTriggers: */ + case SPTableViewStatus: + if (!statusLoaded) { + [[extendedTableInfoInstance onMainThread] loadTable:selectedTableName]; + statusLoaded = YES; + } + break; + case SPTableViewTriggers: + if (!triggersLoaded) { + [[tableTriggersInstance onMainThread] loadTriggers]; + triggersLoaded = YES; + } + break; + case SPTableViewRelations: + if (!relationsLoaded) { + [[tableRelationsInstance onMainThread] refreshRelations:self]; + relationsLoaded = YES; + } + break; +#endif + } + + [self endTask]; + + [tabLoadPool drain]; +} + + +/** + * In a threaded task, load the currently selected table/view/proc/function. + */ +- (void)_loadTableTask +{ + NSAutoreleasePool *loadPool = [[NSAutoreleasePool alloc] init]; + NSString *tableEncoding = nil; + +#ifndef SP_CODA /* Update the window title */ + // Update the window title + [self updateWindowTitle:self]; +#endif + + // Reset table information caches and mark that all loaded views require their data reloading + [tableDataInstance resetAllData]; + + structureLoaded = NO; + contentLoaded = NO; + statusLoaded = NO; + triggersLoaded = NO; + relationsLoaded = NO; + + // Ensure status and details are fetched using UTF8 + NSString *previousEncoding = [mySQLConnection encoding]; + BOOL changeEncoding = ![previousEncoding isEqualToString:@"utf8"]; + + if (changeEncoding) { + [mySQLConnection storeEncodingForRestoration]; + [mySQLConnection setEncoding:@"utf8"]; + } + + // Cache status information on the working thread + [tableDataInstance updateStatusInformationForCurrentTable]; + + // Check the current encoding against the table encoding to see whether + // an encoding change and reset is required. This also caches table information on + // the working thread. + if( selectedTableType == SPTableTypeView || selectedTableType == SPTableTypeTable) { + + // tableEncoding == nil indicates that there was an error while retrieving table data + tableEncoding = [tableDataInstance tableEncoding]; + + // If encoding is set to Autodetect, update the connection character set encoding + // based on the newly selected table's encoding - but only if it differs from the current encoding. + if ([[[NSUserDefaults standardUserDefaults] objectForKey:SPDefaultEncoding] intValue] == SPEncodingAutodetect) { + if (tableEncoding != nil && ![tableEncoding isEqualToString:previousEncoding]) { + [self setConnectionEncoding:tableEncoding reloadingViews:NO]; + changeEncoding = NO; + } + } + } + + if (changeEncoding) [mySQLConnection restoreStoredEncoding]; + + // Notify listeners of the table change now that the state is fully set up. + [[NSNotificationCenter defaultCenter] postNotificationOnMainThreadWithName:SPTableChangedNotification object:self]; + +#ifndef SP_CODA /* [spHistoryControllerInstance restoreViewStates] */ + + // Restore view states as appropriate + [spHistoryControllerInstance restoreViewStates]; +#endif + + // Load the currently selected view if looking at a table or view + if (tableEncoding && (selectedTableType == SPTableTypeView || selectedTableType == SPTableTypeTable)) + { +#ifndef SP_CODA /* load everything */ + NSInteger selectedTabViewIndex = [[self onMainThread] currentlySelectedView]; + + switch (selectedTabViewIndex) { + case SPTableViewStructure: +#endif + [tableSourceInstance loadTable:selectedTableName]; + structureLoaded = YES; +#ifndef SP_CODA /* load everything */ + break; + case SPTableViewContent: +#endif + [tableContentInstance loadTable:selectedTableName]; + contentLoaded = YES; +#ifndef SP_CODA /* load everything */ + break; + case SPTableViewStatus: + [[extendedTableInfoInstance onMainThread] loadTable:selectedTableName]; + statusLoaded = YES; + break; + case SPTableViewTriggers: + [[tableTriggersInstance onMainThread] loadTriggers]; + triggersLoaded = YES; + break; + case SPTableViewRelations: + [[tableRelationsInstance onMainThread] refreshRelations:self]; + relationsLoaded = YES; + break; + } +#endif + } + + // Clear any views which haven't been loaded as they weren't visible. Note + // that this should be done after reloading visible views, instead of clearing all + // views, to reduce UI operations and avoid resetting state unnecessarily. + // Some views (eg TableRelations) make use of the SPTableChangedNotification and + // so don't require manual clearing. + if (!structureLoaded) [tableSourceInstance loadTable:nil]; + if (!contentLoaded) [tableContentInstance loadTable:nil]; + if (!statusLoaded) [[extendedTableInfoInstance onMainThread] loadTable:nil]; + if (!triggersLoaded) [[tableTriggersInstance onMainThread] resetInterface]; + + // If the table row counts an inaccurate and require updating, trigger an update - no + // action will be performed if not necessary + [tableDataInstance updateAccurateNumberOfRowsForCurrentTableForcingUpdate:NO]; + +#ifndef SP_CODA /* show Create Table syntax */ + // Update the "Show Create Syntax" window if it's already opened + // according to the selected table/view/proc/func + if ([[[self onMainThread] getCreateTableSyntaxWindow] isVisible]) { + [[self onMainThread] showCreateTableSyntax:self]; + } + + // Add a history entry + [spHistoryControllerInstance updateHistoryEntries]; +#endif + // Empty the loading pool and exit the thread + [self endTask]; + +#ifndef SP_CODA /* triggered commands */ + NSArray *triggeredCommands = [SPAppDelegate bundleCommandsForTrigger:SPBundleTriggerActionTableChanged]; + + for(NSString* cmdPath in triggeredCommands) + { + NSArray *data = [cmdPath componentsSeparatedByString:@"|"]; + NSMenuItem *aMenuItem = [[[NSMenuItem alloc] init] autorelease]; + [aMenuItem setTag:0]; + [aMenuItem setToolTip:[data objectAtIndex:0]]; + + // For HTML output check if corresponding window already exists + BOOL stopTrigger = NO; + if([(NSString*)[data objectAtIndex:2] length]) { + BOOL correspondingWindowFound = NO; + NSString *uuid = [data objectAtIndex:2]; + for(id win in [NSApp windows]) { + if([[[[win delegate] class] description] isEqualToString:@"SPBundleHTMLOutputController"]) { + if([[[win delegate] windowUUID] isEqualToString:uuid]) { + correspondingWindowFound = YES; + break; + } + } + } + if(!correspondingWindowFound) stopTrigger = YES; + } + if(!stopTrigger) { + id firstResponder = [[NSApp keyWindow] firstResponder]; + if([[data objectAtIndex:1] isEqualToString:SPBundleScopeGeneral]) { + [[SPAppDelegate onMainThread] executeBundleItemForApp:aMenuItem]; + } + else if([[data objectAtIndex:1] isEqualToString:SPBundleScopeDataTable]) { + if([[[firstResponder class] description] isEqualToString:@"SPCopyTable"]) + [[firstResponder onMainThread] executeBundleItemForDataTable:aMenuItem]; + } + else if([[data objectAtIndex:1] isEqualToString:SPBundleScopeInputField]) { + if([firstResponder isKindOfClass:[NSTextView class]]) + [[firstResponder onMainThread] executeBundleItemForInputField:aMenuItem]; + } + } + } +#endif + + [loadPool drain]; +} + +#pragma mark - SPMySQLConnection delegate methods + +/** + * Invoked when the framework is about to perform a query. + */ +- (void)willQueryString:(NSString *)query connection:(id)connection +{ +#ifndef SP_CODA + if ([prefs boolForKey:SPConsoleEnableLogging]) { + if ((_queryMode == SPInterfaceQueryMode && [prefs boolForKey:SPConsoleEnableInterfaceLogging]) || + (_queryMode == SPCustomQueryQueryMode && [prefs boolForKey:SPConsoleEnableCustomQueryLogging]) || + (_queryMode == SPImportExportQueryMode && [prefs boolForKey:SPConsoleEnableImportExportLogging])) + { + [[SPQueryController sharedQueryController] showMessageInConsole:query connection:[self name] database:[self database]]; + } + } +#endif +} + +/** + * Invoked when the query just executed by the framework resulted in an error. + */ +- (void)queryGaveError:(NSString *)error connection:(id)connection +{ +#ifndef SP_CODA + if ([prefs boolForKey:SPConsoleEnableLogging] && [prefs boolForKey:SPConsoleEnableErrorLogging]) { + [[SPQueryController sharedQueryController] showErrorInConsole:error connection:[self name] database:[self database]]; + } +#endif +} + +/** + * Invoked when the current connection needs a password from the Keychain. + */ +- (NSString *)keychainPasswordForConnection:(SPMySQLConnection *)connection +{ + // If no keychain item is available, return an empty password + if (![connectionController connectionKeychainItemName]) return nil; + + // Otherwise, pull the password from the keychain using the details from this connection + SPKeychain *keychain = [[SPKeychain alloc] init]; + + NSString *password = [keychain getPasswordForName:[connectionController connectionKeychainItemName] account:[connectionController connectionKeychainItemAccount]]; + + [keychain release]; + + return password; +} + +/** + * Invoked when the current connection needs a ssh password from the Keychain. + * This isn't actually part of the SPMySQLConnection delegate protocol, but is here + * due to its similarity to the previous method. + */ +- (NSString *)keychainPasswordForSSHConnection:(SPMySQLConnection *)connection +{ + // If no keychain item is available, return an empty password + if (![connectionController connectionKeychainItemName]) return @""; + + // Otherwise, pull the password from the keychain using the details from this connection + SPKeychain *keychain = [[SPKeychain alloc] init]; + + NSString *connectionSSHKeychainItemName = [[keychain nameForSSHForFavoriteName:[connectionController name] id:[self keyChainID]] retain]; + NSString *connectionSSHKeychainItemAccount = [[keychain accountForSSHUser:[connectionController sshUser] sshHost:[connectionController sshHost]] retain]; + NSString *sshPassword = [keychain getPasswordForName:connectionSSHKeychainItemName account:connectionSSHKeychainItemAccount]; + + if (!sshPassword || ![sshPassword length]) { + sshPassword = @""; + } + + if (connectionSSHKeychainItemName) [connectionSSHKeychainItemName release]; + if (connectionSSHKeychainItemAccount) [connectionSSHKeychainItemAccount release]; + + [keychain release]; + + return sshPassword; +} + +/** + * Invoked when an attempt was made to execute a query on the current connection, but the connection is not + * actually active. + */ +- (void)noConnectionAvailable:(id)connection +{ + SPOnewayAlertSheet( + NSLocalizedString(@"No connection available", @"no connection available message"), + [self parentWindow], + NSLocalizedString(@"An error has occured and there doesn't seem to be a connection available.", @"no connection available informatie message") + ); +} + +/** + * Invoked when the connection fails and the framework needs to know how to proceed. + */ +- (SPMySQLConnectionLostDecision)connectionLost:(id)connection +{ + SPMySQLConnectionLostDecision connectionErrorCode = SPMySQLConnectionLostDisconnect; + + // Only display the reconnect dialog if the window is visible + if ([self parentWindow] && [[self parentWindow] isVisible]) { + + // Ensure the window isn't miniaturized + if ([[self parentWindow] isMiniaturized]) [[self parentWindow] deminiaturize:self]; + +#ifndef SP_CODA + // Ensure the window and tab are frontmost + [self makeKeyDocument]; +#endif + + // Display the connection error dialog and wait for the return code + [NSApp beginSheet:connectionErrorDialog modalForWindow:[self parentWindow] modalDelegate:self didEndSelector:nil contextInfo:nil]; + connectionErrorCode = (SPMySQLConnectionLostDecision)[NSApp runModalForWindow:connectionErrorDialog]; + + [NSApp endSheet:connectionErrorDialog]; + [connectionErrorDialog orderOut:nil]; + + // If 'disconnect' was selected, trigger a window close. + if (connectionErrorCode == SPMySQLConnectionLostDisconnect) { + [self performSelectorOnMainThread:@selector(closeAndDisconnect) withObject:nil waitUntilDone:YES]; + } + } + + return connectionErrorCode; +} + +/** + * Invoke to display an informative but non-fatal error directly to the user. + */ +- (void)showErrorWithTitle:(NSString *)theTitle message:(NSString *)theMessage +{ + if ([[self parentWindow] isVisible]) { + SPOnewayAlertSheet(theTitle, [self parentWindow], theMessage); + } +} + +/** + * Invoked when user dismisses the error sheet displayed as a result of the current connection being lost. + */ +- (IBAction)closeErrorConnectionSheet:(id)sender +{ + [NSApp stopModalWithCode:[sender tag]]; +} + +/** + * Close the connection - should be performed on the main thread. + */ +- (void) closeAndDisconnect +{ +#ifndef SP_CODA + NSWindow *theParentWindow = [self parentWindow]; + + _isConnected = NO; + + if ([[[self parentTabViewItem] tabView] numberOfTabViewItems] == 1) { + [theParentWindow orderOut:self]; + [theParentWindow setAlphaValue:0.0f]; + [theParentWindow performSelector:@selector(close) withObject:nil afterDelay:1.0]; + } + else { + [[[self parentTabViewItem] tabView] performSelector:@selector(removeTabViewItem:) withObject:[self parentTabViewItem] afterDelay:0.5]; + [theParentWindow performSelector:@selector(makeKeyAndOrderFront:) withObject:nil afterDelay:0.6]; + } + + [self parentTabDidClose]; +#endif +} + +#pragma mark - SPPrintController + +/** + * WebView delegate method. + */ +- (void)webView:(WebView *)sender didFinishLoadForFrame:(WebFrame *)frame +{ + // Because we need the webFrame loaded (for preview), we've moved the actual printing here + NSPrintInfo *printInfo = [NSPrintInfo sharedPrintInfo]; + + NSSize paperSize = [printInfo paperSize]; + NSRect printableRect = [printInfo imageablePageBounds]; + + // Calculate page margins + CGFloat marginL = printableRect.origin.x; + CGFloat marginR = paperSize.width - (printableRect.origin.x + printableRect.size.width); + CGFloat marginB = printableRect.origin.y; + CGFloat marginT = paperSize.height - (printableRect.origin.y + printableRect.size.height); + + // Make sure margins are symetric and positive + CGFloat marginLR = MAX(0, MAX(marginL, marginR)); + CGFloat marginTB = MAX(0, MAX(marginT, marginB)); + + // Set the margins + [printInfo setLeftMargin:marginLR]; + [printInfo setRightMargin:marginLR]; + [printInfo setTopMargin:marginTB]; + [printInfo setBottomMargin:marginTB]; + + [printInfo setHorizontalPagination:NSFitPagination]; + [printInfo setVerticalPagination:NSAutoPagination]; + [printInfo setVerticallyCentered:NO]; + + NSPrintOperation *op = [NSPrintOperation printOperationWithView:[[[printWebView mainFrame] frameView] documentView] printInfo:printInfo]; + + // Perform the print operation on a background thread + [op setCanSpawnSeparateThread:YES]; + + // Add the ability to select the orientation to print panel + NSPrintPanel *printPanel = [op printPanel]; + + [printPanel setOptions:[printPanel options] + NSPrintPanelShowsOrientation + NSPrintPanelShowsScaling + NSPrintPanelShowsPaperSize]; + + SPPrintAccessory *printAccessory = [[SPPrintAccessory alloc] initWithNibName:@"PrintAccessory" bundle:nil]; + + [printAccessory setPrintView:printWebView]; + [printPanel addAccessoryController:printAccessory]; + + [[NSPageLayout pageLayout] addAccessoryController:printAccessory]; + [printAccessory release]; + + [op setPrintPanel:printPanel]; + + [op runOperationModalForWindow:[self parentWindow] + delegate:self + didRunSelector:nil + contextInfo:nil]; + + if ([self isWorking]) [self endTask]; +} + +/** + * Loads the print document interface. The actual printing is done in the doneLoading delegate. + */ +- (IBAction)printDocument:(id)sender +{ + // Only display warning for the 'Table Content' view + if ([self currentlySelectedView] == SPTableViewContent) { + + NSInteger rowLimit = [prefs integerForKey:SPPrintWarningRowLimit]; + + // Result count minus one because the first element is the column names + NSInteger resultRows = ([[tableContentInstance currentResult] count] - 1); + + if (resultRows > rowLimit) { + + NSNumberFormatter *numberFormatter = [[[NSNumberFormatter alloc] init] autorelease]; + + [numberFormatter setNumberStyle:NSNumberFormatterDecimalStyle]; + + NSAlert *alert = [NSAlert alertWithMessageText:NSLocalizedString(@"Continue to print?", @"continue to print message") + defaultButton:NSLocalizedString(@"Print", @"print button") + alternateButton:NSLocalizedString(@"Cancel", @"cancel button") + otherButton:nil + informativeTextWithFormat:NSLocalizedString(@"Are you sure you want to print the current content view of the table '%@'?\n\nIt currently contains %@ rows, which may take a significant amount of time to print.", @"continue to print informative message"), [self table], [numberFormatter stringFromNumber:[NSNumber numberWithLongLong:resultRows]]]; + + NSArray *buttons = [alert buttons]; + + // Change the alert's cancel button to have the key equivalent of return + [[buttons objectAtIndex:0] setKeyEquivalent:@"p"]; + [[buttons objectAtIndex:0] setKeyEquivalentModifierMask:NSCommandKeyMask]; + [[buttons objectAtIndex:1] setKeyEquivalent:@"\r"]; + + [alert beginSheetModalForWindow:[self parentWindow] modalDelegate:self didEndSelector:@selector(printWarningDidEnd:returnCode:contextInfo:) contextInfo:NULL]; + + return; + } + } + + [self startPrintDocumentOperation]; +} + +/** + * Called when the print warning dialog is dismissed. + */ +- (void)printWarningDidEnd:(id)sheet returnCode:(NSInteger)returnCode contextInfo:(NSString *)contextInfo +{ + if (returnCode == NSAlertDefaultReturn) { + [self startPrintDocumentOperation]; + } +} + +/** + * Starts tge print document operation by spawning a new thread if required. + */ +- (void)startPrintDocumentOperation +{ + [self startTaskWithDescription:NSLocalizedString(@"Generating print document...", @"generating print document status message")]; + + BOOL isTableInformation = ([self currentlySelectedView] == SPTableViewStatus); + + if ([NSThread isMainThread]) { + printThread = [[NSThread alloc] initWithTarget:self selector:(isTableInformation) ? @selector(generateTableInfoHTMLForPrinting) : @selector(generateHTMLForPrinting) object:nil]; + [printThread setName:@"SPDatabaseDocument document generator"]; + + [self enableTaskCancellationWithTitle:NSLocalizedString(@"Cancel", @"cancel button") callbackObject:self callbackFunction:@selector(generateHTMLForPrintingCallback)]; + + [printThread start]; + } + else { + (isTableInformation) ? [self generateTableInfoHTMLForPrinting] : [self generateHTMLForPrinting]; + } +} + +/** + * HTML generation thread callback method. + */ +- (void)generateHTMLForPrintingCallback +{ + [self setTaskDescription:NSLocalizedString(@"Cancelling...", @"cancelling task status message")]; + + // Cancel the print thread + [printThread cancel]; +} + +/** + * Loads the supplied HTML string in the print WebView. + */ +- (void)loadPrintWebViewWithHTMLString:(NSString *)HTMLString +{ + [[printWebView mainFrame] loadHTMLString:HTMLString baseURL:nil]; + + if (printThread) SPClear(printThread); +} + +/** + * Generates the HTML for the current view that is being printed. + */ +- (void)generateHTMLForPrinting +{ + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + + // Set up template engine with your chosen matcher + MGTemplateEngine *engine = [MGTemplateEngine templateEngine]; + + [engine setMatcher:[ICUTemplateMatcher matcherWithTemplateEngine:engine]]; + + NSMutableDictionary *connection = [self connectionInformation]; + + NSString *heading = @""; + NSArray *rows, *indexes, *indexColumns = nil; + + NSArray *columns = [self columnNames]; + + NSMutableDictionary *printData = [NSMutableDictionary dictionary]; + + SPTableViewType view = [self currentlySelectedView]; + + // Table source view + if (view == SPTableViewStructure) { + + NSDictionary *tableSource = [tableSourceInstance tableSourceForPrinting]; + + NSInteger tableType = [tablesListInstance tableType]; + + switch (tableType) { + case SPTableTypeTable: + heading = NSLocalizedString(@"Table Structure", @"table structure print heading"); + break; + case SPTableTypeView: + heading = NSLocalizedString(@"View Structure", @"view structure print heading"); + break; + } + + rows = [[NSArray alloc] initWithArray: + [[tableSource objectForKey:@"structure"] objectsAtIndexes: + [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(1, [[tableSource objectForKey:@"structure"] count] - 1)]] + ]; + + indexes = [[NSArray alloc] initWithArray: + [[tableSource objectForKey:@"indexes"] objectsAtIndexes: + [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(1, [[tableSource objectForKey:@"indexes"] count] - 1)]] + ]; + + indexColumns = [[tableSource objectForKey:@"indexes"] objectAtIndex:0]; + + [printData setObject:rows forKey:@"rows"]; + [printData setObject:indexes forKey:@"indexes"]; + [printData setObject:indexColumns forKey:@"indexColumns"]; + + if ([indexes count]) [printData setObject:@1 forKey:@"hasIndexes"]; + + [rows release]; + [indexes release]; + } + // Table content view + else if (view == SPTableViewContent) { + + NSArray *data = [tableContentInstance currentDataResultWithNULLs:NO hideBLOBs:YES]; + + heading = NSLocalizedString(@"Table Content", @"table content print heading"); + + rows = [[NSArray alloc] initWithArray: + [data objectsAtIndexes: + [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(1, [data count] - 1)]] + ]; + + [printData setObject:rows forKey:@"rows"]; + [connection setValue:[tableContentInstance usedQuery] forKey:@"query"]; + + [rows release]; + } + // Custom query view + else if (view == SPTableViewCustomQuery) { + + NSArray *data = [customQueryInstance currentResult]; + + heading = NSLocalizedString(@"Query Result", @"query result print heading"); + + rows = [[NSArray alloc] initWithArray: + [data objectsAtIndexes: + [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(1, [data count] - 1)]] + ]; + + [printData setObject:rows forKey:@"rows"]; + [connection setValue:[customQueryInstance usedQuery] forKey:@"query"]; + + [rows release]; + } + // Table relations view + else if (view == SPTableViewRelations) { + + NSArray *data = [tableRelationsInstance relationDataForPrinting]; + + heading = NSLocalizedString(@"Table Relations", @"toolbar item label for switching to the Table Relations tab"); + + rows = [[NSArray alloc] initWithArray: + [data objectsAtIndexes: + [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(1, ([data count] - 1))]] + ]; + + [printData setObject:rows forKey:@"rows"]; + + [rows release]; + } + // Table triggers view + else if (view == SPTableViewTriggers) { + + NSArray *data = [tableTriggersInstance triggerDataForPrinting]; + + heading = NSLocalizedString(@"Table Triggers", @"toolbar item label for switching to the Table Triggers tab"); + + rows = [[NSArray alloc] initWithArray: + [data objectsAtIndexes: + [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(1, ([data count] - 1))]] + ]; + + [printData setObject:rows forKey:@"rows"]; + + [rows release]; + } + + [engine setObject:connection forKey:@"c"]; + + [printData setObject:heading forKey:@"heading"]; + [printData setObject:columns forKey:@"columns"]; + [printData setObject:([prefs boolForKey:SPUseMonospacedFonts]) ? SPDefaultMonospacedFontName : @"Lucida Grande" forKey:@"font"]; + [printData setObject:([prefs boolForKey:SPDisplayTableViewVerticalGridlines]) ? @"1px solid #CCCCCC" : @"none" forKey:@"gridlines"]; + + NSString *HTMLString = [engine processTemplateInFileAtPath:[[NSBundle mainBundle] pathForResource:SPHTMLPrintTemplate ofType:@"html"] withVariables:printData]; + + // Check if the operation has been cancelled + if ((printThread != nil) && (![NSThread isMainThread]) && ([printThread isCancelled])) { + [self endTask]; + [pool drain]; + + [NSThread exit]; + return; + } + + [self performSelectorOnMainThread:@selector(loadPrintWebViewWithHTMLString:) withObject:HTMLString waitUntilDone:NO]; + + [pool drain]; +} + +/** + * Generates the HTML for the table information view that is to be printed. + */ +- (void)generateTableInfoHTMLForPrinting +{ + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + + // Set up template engine with your chosen matcher + MGTemplateEngine *engine = [MGTemplateEngine templateEngine]; + + [engine setMatcher:[ICUTemplateMatcher matcherWithTemplateEngine:engine]]; + + NSMutableDictionary *connection = [self connectionInformation]; + NSMutableDictionary *printData = [NSMutableDictionary dictionary]; + + NSString *heading = NSLocalizedString(@"Table Information", @"table information print heading"); + + [engine setObject:connection forKey:@"c"]; + [engine setObject:[[extendedTableInfoInstance onMainThread] tableInformationForPrinting] forKey:@"i"]; + + [printData setObject:heading forKey:@"heading"]; + [printData setObject:[[NSUnarchiver unarchiveObjectWithData:[prefs objectForKey:SPCustomQueryEditorFont]] fontName] forKey:@"font"]; + + NSString *HTMLString = [engine processTemplateInFileAtPath:[[NSBundle mainBundle] pathForResource:SPHTMLTableInfoPrintTemplate ofType:@"html"] withVariables:printData]; + + // Check if the operation has been cancelled + if ((printThread != nil) && (![NSThread isMainThread]) && ([printThread isCancelled])) { + [self endTask]; + [pool drain]; + + [NSThread exit]; + return; + } + + [self performSelectorOnMainThread:@selector(loadPrintWebViewWithHTMLString:) withObject:HTMLString waitUntilDone:NO]; + + [pool drain]; +} + +/** + * Returns an array of columns for whichever view is being printed. + */ +- (NSArray *)columnNames +{ + NSArray *columns = nil; + + SPTableViewType view = [self currentlySelectedView]; + + // Table source view + if ((view == SPTableViewStructure) && ([[tableSourceInstance tableSourceForPrinting] count] > 0)) { + + columns = [[NSArray alloc] initWithArray:[[[tableSourceInstance tableSourceForPrinting] objectForKey:@"structure"] objectAtIndex:0] copyItems:YES]; + } + // Table content view + else if ((view == SPTableViewContent) && ([[tableContentInstance currentResult] count] > 0)) { + + columns = [[NSArray alloc] initWithArray:[[tableContentInstance currentResult] objectAtIndex:0] copyItems:YES]; + } + // Custom query view + else if ((view == SPTableViewCustomQuery) && ([[customQueryInstance currentResult] count] > 0)) { + + columns = [[NSArray alloc] initWithArray:[[customQueryInstance currentResult] objectAtIndex:0] copyItems:YES]; + } + // Table relations view + else if ((view == SPTableViewRelations) && ([[tableRelationsInstance relationDataForPrinting] count] > 0)) { + + columns = [[NSArray alloc] initWithArray:[[tableRelationsInstance relationDataForPrinting] objectAtIndex:0] copyItems:YES]; + } + // Table triggers view + else if ((view == SPTableViewTriggers) && ([[tableTriggersInstance triggerDataForPrinting] count] > 0)) { + + columns = [[NSArray alloc] initWithArray:[[tableTriggersInstance triggerDataForPrinting] objectAtIndex:0] copyItems:YES]; + } + + if (columns) [columns autorelease]; + + return columns; +} + +/** + * Generates a dictionary of connection information that is used for printing. + */ +- (NSMutableDictionary *)connectionInformation +{ + NSString *versionForPrint = [NSString stringWithFormat:@"%@ %@ (%@ %@)", + [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleName"], + [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"], + NSLocalizedString(@"build", @"build label"), + [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"] + ]; + + NSMutableDictionary *connection = [NSMutableDictionary dictionary]; + + if ([[self user] length]) { + [connection setValue:[self user] forKey:@"username"]; + } + + if ([[self table] length]) { + [connection setValue:[self table] forKey:@"table"]; + } + + if ([connectionController port] && [[connectionController port] length]) { + [connection setValue:[connectionController port] forKey:@"port"]; + } + + [connection setValue:[self host] forKey:@"hostname"]; + [connection setValue:selectedDatabase forKey:@"database"]; + [connection setValue:versionForPrint forKey:@"version"]; + + return connection; +} + #pragma mark - - (void)dealloc |