From 50a283b6d1f3ce48e3a06eceeed3a466c6259fe7 Mon Sep 17 00:00:00 2001 From: rowanbeentje Date: Sun, 15 Nov 2009 23:58:21 +0000 Subject: Implement query cancellation support within MCPKit, and add it to the task functionality: - MCPKit now supports cancelling the active query; for MySQL servers >= 5.0.0 a query kill is attempted from a new connection, and if that fails or for MySQL < 5 a reconnect is triggered. - TableDocument now supports enabling a cancel task button on the task interface, including an optional callback - Implement query cancellation for custom queries. This addresses Issue #86. - Implement query cancellation for table content loads, filters, and sorts. --- Frameworks/MCPKit/MCPFoundationKit/MCPConnection.h | 5 + Frameworks/MCPKit/MCPFoundationKit/MCPConnection.m | 138 +++++++++++++++++++-- .../English.lproj/ProgressIndicatorLayer.xib | 42 ++++--- Source/CustomQuery.m | 84 +++++++++---- Source/TableContent.h | 2 +- Source/TableContent.m | 29 ++++- Source/TableDocument.h | 6 + Source/TableDocument.m | 70 ++++++++++- Source/TableSource.m | 16 ++- 9 files changed, 327 insertions(+), 65 deletions(-) diff --git a/Frameworks/MCPKit/MCPFoundationKit/MCPConnection.h b/Frameworks/MCPKit/MCPFoundationKit/MCPConnection.h index 548425e1..0442bf9f 100644 --- a/Frameworks/MCPKit/MCPFoundationKit/MCPConnection.h +++ b/Frameworks/MCPKit/MCPFoundationKit/MCPConnection.h @@ -111,6 +111,8 @@ static inline NSData* NSStringDataUsingLossyEncoding(NSString* self, NSInteger e uint64_t connectionStartTime; BOOL retryAllowed; + BOOL queryCancelled; + BOOL queryCancelUsedReconnect; BOOL delegateQueryLogging; BOOL delegateResponseToWillQueryString; @@ -205,6 +207,9 @@ void performThreadedKeepAlive(void *ptr); - (id)queryString:(NSString *) query usingEncoding:(NSStringEncoding) encoding streamingResult:(NSInteger) streamResult; - (my_ulonglong)affectedRows; - (my_ulonglong)insertId; +- (void)cancelCurrentQuery; +- (BOOL)queryCancelled; +- (BOOL)queryCancellationUsedReconnect; // Locking - (void)lockConnection; diff --git a/Frameworks/MCPKit/MCPFoundationKit/MCPConnection.m b/Frameworks/MCPKit/MCPFoundationKit/MCPConnection.m index e55568d5..7db3ec2d 100644 --- a/Frameworks/MCPKit/MCPFoundationKit/MCPConnection.m +++ b/Frameworks/MCPKit/MCPFoundationKit/MCPConnection.m @@ -101,6 +101,8 @@ static BOOL sTruncateLongFieldInLogs = YES; connectionProxy = nil; connectionStartTime = -1; lastQueryExecutedAtTime = CGFLOAT_MAX; + queryCancelled = NO; + queryCancelUsedReconnect = NO; // Initialize ivar defaults connectionTimeout = 10; @@ -1261,6 +1263,9 @@ void performThreadedKeepAlive(void *ptr) NSInteger currentMaxAllowedPacket = -1; BOOL isQueryRetry = NO; NSString *queryErrorMessage = nil; + + // Reset the query cancelled boolean + queryCancelled = NO; // If no connection is present, return nil. if (!mConnected) { @@ -1290,7 +1295,6 @@ void performThreadedKeepAlive(void *ptr) // minimising the impact of performing lots of additional checks. if ([self timeConnected] - lastQueryExecutedAtTime > 30 && ![self checkConnection]) { - NSLog(@"returning nil!"); return nil; } @@ -1366,7 +1370,7 @@ void performThreadedKeepAlive(void *ptr) // For normal result sets, fetch the results and unlock the connection if (streamResultType == MCP_NO_STREAMING) { theResult = [[MCPResult alloc] initWithMySQLPtr:mConnection encoding:mEncoding timeZone:mTimeZone]; - [queryLock unlock]; + if (!queryCancelled || !queryCancelUsedReconnect) [queryLock unlock]; // For streaming result sets, fetch the result pointer and leave the connection locked } else if (streamResultType == MCP_FAST_STREAMING) { @@ -1394,16 +1398,23 @@ void performThreadedKeepAlive(void *ptr) // On failure, set the error messages and IDs } else { - if (streamResultType == MCP_NO_STREAMING) [queryLock unlock]; - else [self unlockConnection]; + if (!queryCancelled || !queryCancelUsedReconnect) { + if (streamResultType == MCP_NO_STREAMING) [queryLock unlock]; + else [self unlockConnection]; + } - queryErrorMessage = [[NSString alloc] initWithString:[self stringWithCString:mysql_error(mConnection)]]; - queryErrorId = mysql_errno(mConnection); - - // If the error was a connection error, retry once - if (!isQueryRetry && retryAllowed && [MCPConnection isErrorNumberConnectionError:queryErrorId]) { - isQueryRetry = YES; - continue; + if (queryCancelled) { + queryErrorMessage = [[NSString alloc] initWithString:NSLocalizedString(@"Query cancelled.", @"Query cancelled error")]; + queryErrorId = 1152; + } else { + queryErrorMessage = [[NSString alloc] initWithString:[self stringWithCString:mysql_error(mConnection)]]; + queryErrorId = mysql_errno(mConnection); + + // If the error was a connection error, retry once + if (!isQueryRetry && retryAllowed && [MCPConnection isErrorNumberConnectionError:queryErrorId]) { + isQueryRetry = YES; + continue; + } } } @@ -1464,6 +1475,104 @@ void performThreadedKeepAlive(void *ptr) return 0; } +/** + * Cancel the currently running query. This tries to kill the current query, and if that + * isn't possible, resets the connection. + */ +- (void) cancelCurrentQuery +{ + + // If not connected, return. + if (![self isConnected]) return; + + // Check whether a query is actually being performed - if not, also return. + if ([queryLock tryLock]) { + [queryLock unlock]; + return; + } + + // Set queryCancelled to prevent query retries + queryCancelled = YES; + + // For MySQL server versions >=5, try to kill the connection. This requires + // setting up a new connection, and running a KILL QUERY via it. + if ([self serverMajorVersion] >= 5) { + + MYSQL *killerConnection = mysql_init(NULL); + if (killerConnection) { + const char *theLogin = [self cStringFromString:connectionLogin]; + const char *theHost; + const char *thePass; + const char *theSocket; + void *connectionSetupStatus; + + mysql_options(killerConnection, MYSQL_OPT_CONNECT_TIMEOUT, (const void *)&connectionTimeout); + + // Set up the host, socket and password as per the connect method + if (!connectionHost || ![connectionHost length]) { + theHost = NULL; + } else { + theHost = [self cStringFromString:connectionHost]; + } + if (connectionSocket == nil || ![connectionSocket length]) { + theSocket = kMCPConnectionDefaultSocket; + } else { + theSocket = [self cStringFromString:connectionSocket]; + } + if (!connectionPassword) { + if (delegate && [delegate respondsToSelector:@selector(keychainPasswordForConnection:)]) { + thePass = [self cStringFromString:[delegate keychainPasswordForConnection:self]]; + } + } else { + thePass = [self cStringFromString:connectionPassword]; + } + + // Connect + connectionSetupStatus = mysql_real_connect(killerConnection, theHost, theLogin, thePass, NULL, connectionPort, theSocket, mConnectionFlags); + thePass = NULL; + if (connectionSetupStatus) { + NSStringEncoding killerConnectionEncoding = [MCPConnection encodingForMySQLEncoding:mysql_character_set_name(killerConnection)]; + NSString *killerQueryString = [NSString stringWithFormat:@"KILL QUERY %lu", mConnection->thread_id]; + NSData *encodedKillerQueryData = NSStringDataUsingLossyEncoding(killerQueryString, killerConnectionEncoding, 1); + const char *killerQueryCString = [encodedKillerQueryData bytes]; + unsigned long killerQueryCStringLength = [encodedKillerQueryData length]; + if (mysql_real_query(killerConnection, killerQueryCString, killerQueryCStringLength) == 0) { + mysql_close(killerConnection); + queryCancelUsedReconnect = NO; + return; + } + mysql_close(killerConnection); + } + } + } + + // Reset the connection + [self unlockConnection]; + [self reconnect]; + + // Set queryCancelled again to handle requery cleanups, and return. + queryCancelled = YES; + queryCancelUsedReconnect = YES; +} + +/** + * Return whether the last query was cancelled + */ +- (BOOL)queryCancelled +{ + return queryCancelled; +} + +/** + * If the last query was cancelled, returns whether that cancellation + * required a connection reset. If the last query was not cancelled + * the behaviour is undefined. + */ +- (BOOL)queryCancellationUsedReconnect +{ + return queryCancelUsedReconnect; +} + #pragma mark - #pragma mark Connection locking @@ -1483,6 +1592,13 @@ void performThreadedKeepAlive(void *ptr) */ - (void)unlockConnection { + + // Make sure the unlock is performed safely - eg for reconnected queries + if ([queryLock tryLock]) { + [queryLock unlock]; + return; + } + if ([NSThread isMainThread]) [queryLock unlock]; else [queryLock performSelectorOnMainThread:@selector(unlock) withObject:nil waitUntilDone:YES]; } diff --git a/Interfaces/English.lproj/ProgressIndicatorLayer.xib b/Interfaces/English.lproj/ProgressIndicatorLayer.xib index 33adda95..de3df433 100644 --- a/Interfaces/English.lproj/ProgressIndicatorLayer.xib +++ b/Interfaces/English.lproj/ProgressIndicatorLayer.xib @@ -3,7 +3,7 @@ 1050 10B504 - 732 + 740 1038.2 437.00 @@ -15,7 +15,7 @@ YES - 732 + 740 1.2.1 @@ -117,8 +117,8 @@ - -2147483358 - {{89, 4}, {81, 28}} + 290 + {{64, 4}, {132, 28}} YES @@ -217,6 +217,14 @@ 34 + + + cancelTask: + + + + 35 + @@ -259,21 +267,12 @@ YES - + - - 17 - - - YES - - - - 15 @@ -293,6 +292,15 @@ + + 17 + + + YES + + + + 18 @@ -373,7 +381,7 @@ - 34 + 37 @@ -555,6 +563,7 @@ addDatabase: analyzeTable: backForwardInHistory: + cancelTask: checkTable: checksumTable: chooseDatabase: @@ -636,6 +645,7 @@ id id id + id @@ -690,6 +700,7 @@ tableWindow tablesListInstance taskCancelButton + taskCancellationCallbackObject taskDescriptionText taskProgressIndicator taskProgressLayer @@ -754,6 +765,7 @@ NSButton id id + id NSBox id id diff --git a/Source/CustomQuery.m b/Source/CustomQuery.m index f515f335..dc0d9b76 100644 --- a/Source/CustomQuery.m +++ b/Source/CustomQuery.m @@ -388,6 +388,7 @@ MCPStreamingResult *streamingResult = nil; NSMutableString *errors = [NSMutableString string]; SEL callbackMethod = NULL; + NSString *taskButtonString; int i, totalQueriesRun = 0, totalAffectedRows = 0; double executionTime = 0; @@ -425,6 +426,13 @@ long queryCount = [queries count]; NSMutableArray *tempQueries = [NSMutableArray arrayWithCapacity:queryCount]; + // Enable task cancellation + if (queryCount > 1) + taskButtonString = NSLocalizedString(@"Stop queries", @"Stop queries string"); + else + taskButtonString = NSLocalizedString(@"Stop query", @"Stop query string"); + [tableDocumentInstance enableTaskCancellationWithTitle:taskButtonString callbackObject:nil callbackFunction:NULL]; + // Perform the supplied queries in series for ( i = 0 ; i < queryCount ; i++ ) { @@ -450,7 +458,7 @@ // If this is the last query, retrieve and store the result; otherwise, // discard the result without fully loading. - if (totalQueriesRun == queryCount) { + if (totalQueriesRun == queryCount || [mySQLConnection queryCancelled]) { // get column definitions for the result array if (cqColumnDefinition) [cqColumnDefinition release]; @@ -507,7 +515,17 @@ totalAffectedRows += [streamingResult numOfRows]; // Store any error messages - if ( ![[mySQLConnection getLastErrorMessage] isEqualToString:@""] ) { + if ( ![[mySQLConnection getLastErrorMessage] isEqualToString:@""] || [mySQLConnection queryCancelled]) { + + NSString *errorString; + if ([mySQLConnection queryCancelled]) { + if ([mySQLConnection queryCancellationUsedReconnect]) + errorString = NSLocalizedString(@"Query cancelled. Please note that to cancel the query the connection had to be reset; transactions and connection variables were reset.", @"Query cancel by resetting connection error"); + else + errorString = NSLocalizedString(@"Query cancelled.", @"Query cancelled error"); + } else { + errorString = [mySQLConnection getLastErrorMessage]; + } // If the query errored, append error to the error log for display at the end if ( queryCount > 1 ) { @@ -519,35 +537,37 @@ // Update error text for the user [errors appendString:[NSString stringWithFormat:NSLocalizedString(@"[ERROR in query %d] %@\n", @"error text when multiple custom query failed"), i+1, - [mySQLConnection getLastErrorMessage]]]; + errorString]]; [errorText setStringValue:errors]; + // ask the user to continue after detecting an error - NSAlert *alert = [[[NSAlert alloc] init] autorelease]; - [alert addButtonWithTitle:NSLocalizedString(@"Run All", @"run all button")]; - [alert addButtonWithTitle:NSLocalizedString(@"Continue", @"continue button")]; - [alert addButtonWithTitle:NSLocalizedString(@"Stop", @"stop button")]; - [alert setMessageText:NSLocalizedString(@"MySQL Error", @"mysql error message")]; - [alert setInformativeText:[mySQLConnection getLastErrorMessage]]; - [alert setAlertStyle:NSWarningAlertStyle]; - int choice = [alert runModal]; - switch (choice){ - case NSAlertFirstButtonReturn: - suppressErrorSheet = YES; - case NSAlertSecondButtonReturn: - break; - default: - if(i < queryCount-1) // output that message only if it was not the last one - [errors appendString:NSLocalizedString(@"Execution stopped!\n", @"execution stopped message")]; - i = queryCount; // break for loop; for safety reasons stop the execution of the following queries + if (![mySQLConnection queryCancelled]) { + NSAlert *alert = [[[NSAlert alloc] init] autorelease]; + [alert addButtonWithTitle:NSLocalizedString(@"Run All", @"run all button")]; + [alert addButtonWithTitle:NSLocalizedString(@"Continue", @"continue button")]; + [alert addButtonWithTitle:NSLocalizedString(@"Stop", @"stop button")]; + [alert setMessageText:NSLocalizedString(@"MySQL Error", @"mysql error message")]; + [alert setInformativeText:[mySQLConnection getLastErrorMessage]]; + [alert setAlertStyle:NSWarningAlertStyle]; + int choice = [alert runModal]; + switch (choice){ + case NSAlertFirstButtonReturn: + suppressErrorSheet = YES; + case NSAlertSecondButtonReturn: + break; + default: + if(i < queryCount-1) // output that message only if it was not the last one + [errors appendString:NSLocalizedString(@"Execution stopped!\n", @"execution stopped message")]; + i = queryCount; // break for loop; for safety reasons stop the execution of the following queries + } } - } else { [errors appendString:[NSString stringWithFormat:NSLocalizedString(@"[ERROR in query %d] %@\n", @"error text when multiple custom query failed"), i+1, - [mySQLConnection getLastErrorMessage]]]; + errorString]]; } } else { - [errors setString:[mySQLConnection getLastErrorMessage]]; + [errors setString:errorString]; } } else { // Check if table/db list needs an update @@ -557,6 +577,9 @@ if(!databaseWasChanged && [query isMatchedByRegex:@"(?i)\\b(use|drop\\s+database|drop\\s+schema)\\b\\s+."]) databaseWasChanged = YES; } + + // If the query was cancelled, end all queries. + if ([mySQLConnection queryCancelled]) break; } // Reload table list if at least one query began with drop, alter, rename, or create @@ -605,7 +628,7 @@ } // Error checking - if ( [errors length] && !queryIsTableSorter ) { + if ( [mySQLConnection queryCancelled] || ([errors length] && !queryIsTableSorter)) { // set the error text [errorText setStringValue:errors]; // select the line x of the first error if error message contains "at line x" @@ -656,7 +679,18 @@ } // Set up the status string - if ( totalQueriesRun > 1 ) { + if ( [mySQLConnection queryCancelled] ) { + if (totalQueriesRun > 1) { + [affectedRowsText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"Cancelled in query %i, after %@", @"text showing multiple queries were cancelled"), + totalQueriesRun, + [NSString stringForTimeInterval:executionTime] + ]]; + } else { + [affectedRowsText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"Cancelled after %@", @"text showing a query was cancelled"), + [NSString stringForTimeInterval:executionTime] + ]]; + } + } else if ( totalQueriesRun > 1 ) { if (totalAffectedRows==1) { [affectedRowsText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"1 row affected in total, by %i queries taking %@", @"text showing one row has been affected by multiple queries"), totalQueriesRun, diff --git a/Source/TableContent.h b/Source/TableContent.h index 889a79e5..d0cc0ec3 100644 --- a/Source/TableContent.h +++ b/Source/TableContent.h @@ -65,7 +65,7 @@ NSString *compareType; NSNumber *sortCol; BOOL isEditingRow, isEditingNewRow, isSavingRow, isDesc, setLimit; - BOOL isFiltered, isLimited, maxNumRowsIsEstimate; + BOOL isFiltered, isLimited, isInterruptedLoad, maxNumRowsIsEstimate; NSUserDefaults *prefs; int currentlyEditingRow, maxNumRows; diff --git a/Source/TableContent.m b/Source/TableContent.m index 552fbd41..d5c9e8d1 100644 --- a/Source/TableContent.m +++ b/Source/TableContent.m @@ -86,6 +86,7 @@ isFiltered = NO; isLimited = NO; + isInterruptedLoad = NO; prefs = [NSUserDefaults standardUserDefaults]; @@ -532,7 +533,10 @@ rowsToLoad = rowsToLoad - ([limitRowsField intValue]-1); if (rowsToLoad > [prefs integerForKey:SPLimitResultsValue]) rowsToLoad = [prefs integerForKey:SPLimitResultsValue]; } - + + // If within a task, allow this query to be cancelled + [tableDocumentInstance enableTaskCancellationWithTitle:NSLocalizedString(@"Stop", @"Stop load") callbackObject:nil callbackFunction:NULL]; + // Perform and process the query [tableContentView performSelectorOnMainThread:@selector(noteNumberOfRowsChanged) withObject:nil waitUntilDone:YES]; [self setUsedQuery:queryString]; @@ -541,7 +545,7 @@ [streamingResult release]; // If the result is empty, and a limit is active, reset the limit - if ([prefs boolForKey:SPLimitResults] && queryStringBeforeLimit && !tableRowsCount) { + if ([prefs boolForKey:SPLimitResults] && queryStringBeforeLimit && !tableRowsCount && ![mySQLConnection queryCancelled]) { [limitRowsField setStringValue:@"1"]; queryString = [NSMutableString stringWithFormat:@"%@ LIMIT 0,%d", queryStringBeforeLimit, [prefs integerForKey:SPLimitResultsValue]]; [self setUsedQuery:queryString]; @@ -549,6 +553,14 @@ [self processResultIntoDataStorage:streamingResult approximateRowCount:[prefs integerForKey:SPLimitResultsValue]]; [streamingResult release]; } + + if ([mySQLConnection queryCancelled] || ![[mySQLConnection getLastErrorMessage] isEqualToString:@""]) + isInterruptedLoad = YES; + else + isInterruptedLoad = NO; + + // End cancellation ability + [tableDocumentInstance disableTaskCancellation]; if ([prefs boolForKey:SPLimitResults] && ([limitRowsField intValue] > 1 @@ -821,8 +833,15 @@ NSString *rowString; NSMutableString *countString = [NSMutableString string]; + // If the result is partial due to an error or query cancellation, show a very basic count + if (isInterruptedLoad) { + if (tableRowsCount == 1) + [countString appendFormat:NSLocalizedString(@"%d row in partial load", @"text showing a single row a partially loaded result"), tableRowsCount]; + else + [countString appendFormat:NSLocalizedString(@"%d rows in partial load", @"text showing how many rows are in a partially loaded result"), tableRowsCount]; + // If no filter or limit is active, show just the count of rows in the table - if (!isFiltered && !isLimited) { + } else if (!isFiltered && !isLimited) { if (tableRowsCount == 1) [countString appendFormat:NSLocalizedString(@"%d row in table", @"text showing a single row in the result"), tableRowsCount]; else @@ -1128,7 +1147,7 @@ NSString *contextInfo = @"removerow"; - if (([tableContentView numberOfSelectedRows] == [tableContentView numberOfRows]) && !isFiltered && !isLimited) { + if (([tableContentView numberOfSelectedRows] == [tableContentView numberOfRows]) && !isFiltered && !isLimited && !isInterruptedLoad) { contextInfo = @"removeallrows"; @@ -2288,7 +2307,7 @@ BOOL checkStatusCount = NO; // For unfiltered and non-limited tables, use the result count - and update the status count - if (!isLimited && !isFiltered) { + if (!isLimited && !isFiltered && !isInterruptedLoad) { maxNumRows = tableRowsCount; maxNumRowsIsEstimate = NO; [tableDataInstance setStatusValue:[NSString stringWithFormat:@"%d", maxNumRows] forKey:@"Rows"]; diff --git a/Source/TableDocument.h b/Source/TableDocument.h index f9e96730..db4b48ae 100644 --- a/Source/TableDocument.h +++ b/Source/TableDocument.h @@ -138,6 +138,9 @@ float taskProgressValueDisplayInterval; NSTimer *taskDrawTimer; NSViewAnimation *taskFadeAnimator; + BOOL taskCanBeCancelled; + id taskCancellationCallbackObject; + SEL taskCancellationCallbackSelector; NSToolbar *mainToolbar; NSToolbarItem *chooseDatabaseToolbarItem; @@ -186,6 +189,9 @@ - (void) setTaskPercentage:(float)taskPercentage; - (void) setTaskProgressToIndeterminate; - (void) endTask; +- (void) enableTaskCancellationWithTitle:(NSString *)buttonTitle callbackObject:(id)callbackObject callbackFunction:(SEL)callbackFunction; +- (void) disableTaskCancellation; +- (IBAction) cancelTask:(id)sender; - (BOOL) isWorking; - (void) setDatabaseListIsSelectable:(BOOL)isSelectable; - (void) centerTaskWindow; diff --git a/Source/TableDocument.m b/Source/TableDocument.m index fa07e143..78df5e16 100644 --- a/Source/TableDocument.m +++ b/Source/TableDocument.m @@ -103,6 +103,9 @@ taskProgressValueDisplayInterval = 1; taskDrawTimer = nil; taskFadeAnimator = nil; + taskCanBeCancelled = NO; + taskCancellationCallbackObject = nil; + taskCancellationCallbackSelector = NULL; keyChainID = nil; } @@ -223,15 +226,17 @@ NSLog(@"Progress indicator layer could not be loaded; progress display will not function correctly."); } - // Set up the progress indicator child window and later - add to main window, change indicator color and size + // Set up the progress indicator child window and layer - add to main window, change indicator color and size + [taskProgressIndicator setForeColor:[NSColor whiteColor]]; taskProgressWindow = [[NSWindow alloc] initWithContentRect:[taskProgressLayer bounds] styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO]; [taskProgressWindow setOpaque:NO]; - [taskProgressWindow setIgnoresMouseEvents:YES]; [taskProgressWindow setBackgroundColor:[NSColor clearColor]]; [taskProgressWindow setAlphaValue:0.0]; - [[taskProgressWindow contentView] addSubview:taskProgressLayer]; + [taskProgressWindow orderWindow:NSWindowAbove relativeTo:[tableWindow windowNumber]]; [tableWindow addChildWindow:taskProgressWindow ordered:NSWindowAbove]; - [taskProgressIndicator setForeColor:[NSColor whiteColor]]; + [taskProgressWindow release]; + [taskProgressWindow setContentView:taskProgressLayer]; + [self centerTaskWindow]; } /** @@ -1258,7 +1263,7 @@ [historyControl setEnabled:NO]; databaseListIsSelectable = NO; [[NSNotificationCenter defaultCenter] postNotificationName:SPDocumentTaskStartNotification object:self]; - + // Schedule appearance of the task window in the near future taskDrawTimer = [[NSTimer scheduledTimerWithTimeInterval:0.25 target:self selector:@selector(showTaskProgressWindow:) userInfo:nil repeats:NO] retain]; } @@ -1333,6 +1338,9 @@ // Decrement the working level _isWorkingLevel--; + // Ensure cancellation interface is reset + [self disableTaskCancellation]; + // If all tasks have ended, re-enable the interface if (!_isWorkingLevel) { @@ -1357,6 +1365,57 @@ } } +/** + * Allow a task to be cancelled, enabling the button with a supplied title + * and optionally supplying a callback object and function. + */ +- (void) enableTaskCancellationWithTitle:(NSString *)buttonTitle callbackObject:(id)callbackObject callbackFunction:(SEL)callbackFunction +{ + + // If no task is active, return + if (!_isWorkingLevel) return; + + if (callbackObject && callbackFunction) { + taskCancellationCallbackObject = callbackObject; + taskCancellationCallbackSelector = callbackFunction; + } + taskCanBeCancelled = YES; + + [taskCancelButton setTitle:buttonTitle]; + [taskCancelButton setEnabled:YES]; + [taskCancelButton setHidden:NO]; +} + +/** + * Disable task cancellation. Called automatically at the end of a task. + */ +- (void) disableTaskCancellation +{ + + // If no task is active, return + if (!_isWorkingLevel) return; + + taskCanBeCancelled = NO; + taskCancellationCallbackObject = nil; + taskCancellationCallbackSelector = NULL; + [taskCancelButton setHidden:YES]; +} + +/** + * Action sent by the cancel button when it's active. + */ +- (IBAction) cancelTask:(id)sender +{ + if (!taskCanBeCancelled) return; + + [taskCancelButton setEnabled:NO]; + [mySQLConnection cancelCurrentQuery]; + + if (taskCancellationCallbackObject && taskCancellationCallbackSelector) { + [taskCancellationCallbackObject performSelector:taskCancellationCallbackSelector]; + } +} + /** * Returns whether the document is busy performing a task - allows UI or actions * to be restricted as appropriate. @@ -3553,7 +3612,6 @@ if (spfSession) [spfSession release]; if (spfDocData) [spfDocData release]; if (keyChainID) [keyChainID release]; - if (taskProgressWindow) [taskProgressWindow release]; [super dealloc]; } diff --git a/Source/TableSource.m b/Source/TableSource.m index 932d9cdd..6228e6f1 100644 --- a/Source/TableSource.m +++ b/Source/TableSource.m @@ -1033,8 +1033,20 @@ returns a dictionary containing enum/set field names as key and possible values } - (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex -{ - return [(aTableView == tableSourceView) ? [tableFields objectAtIndex:rowIndex] : [indexes objectAtIndex:rowIndex] objectForKey:[aTableColumn identifier]]; +{ + NSDictionary *theRow; + + if (aTableView == tableSourceView) { + + // Return a placeholder if the table is reloading + if (rowIndex >= [tableFields count]) return @"..."; + + theRow = [tableFields objectAtIndex:rowIndex]; + } else { + theRow = [indexes objectAtIndex:rowIndex]; + } + + return [theRow objectForKey:[aTableColumn identifier]]; } - (void)tableView:(NSTableView *)aTableView setObjectValue:(id)anObject forTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex -- cgit v1.2.3