aboutsummaryrefslogtreecommitdiffstats
path: root/Source/SPDatabaseDocument.m
diff options
context:
space:
mode:
Diffstat (limited to 'Source/SPDatabaseDocument.m')
-rw-r--r--Source/SPDatabaseDocument.m1190
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