// // SPTableContentDelegate.m // sequel-pro // // Created by Stuart Connolly (stuconnolly.com) on March 20, 2012. // Copyright (c) 2012 Stuart Connolly. All rights reserved. // // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation // files (the "Software"), to deal in the Software without // restriction, including without limitation the rights to use, // copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following // conditions: // // The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. // // More info at <https://github.com/sequelpro/sequelpro> #import "SPTableContentDelegate.h" #import "SPTableContentFilter.h" #ifndef SP_CODA /* headers */ #import "SPAppController.h" #endif #import "SPDatabaseDocument.h" #import "SPDataStorage.h" #import "SPGeometryDataView.h" #import "SPTooltip.h" #import "SPTablesList.h" #ifndef SP_CODA /* headers */ #import "SPBundleHTMLOutputController.h" #endif #import "SPCopyTable.h" #import "SPAlertSheets.h" #import "SPTableData.h" #import "SPFieldEditorController.h" #import "SPThreadAdditions.h" #import "SPTextAndLinkCell.h" #import <pthread.h> #import <SPMySQL/SPMySQL.h> @interface SPTableContent (SPDeclaredAPI) - (BOOL)cancelRowEditing; @end @implementation SPTableContent (SPTableContentDelegate) #pragma mark - #pragma mark TableView delegate methods /** * Sorts the tableView by the clicked column. If clicked twice, order is altered to descending. * Performs the task in a new thread if necessary. */ - (void)tableView:(NSTableView*)tableView didClickTableColumn:(NSTableColumn *)tableColumn { if ([selectedTable isEqualToString:@""] || !selectedTable || tableView != tableContentView) return; // Prevent sorting while the table is still loading if ([tableDocumentInstance isWorking]) return; // Start the task [tableDocumentInstance startTaskWithDescription:NSLocalizedString(@"Sorting table...", @"Sorting table task description")]; if ([NSThread isMainThread]) { [NSThread detachNewThreadWithName:@"SPTableContent table sort task" target:self selector:@selector(sortTableTaskWithColumn:) object:tableColumn]; } else { [self sortTableTaskWithColumn:tableColumn]; } } - (void)tableViewSelectionDidChange:(NSNotification *)aNotification { // Check our notification object is our table content view if ([aNotification object] != tableContentView) return; isFirstChangeInView = YES; [addButton setEnabled:([tablesListInstance tableType] == SPTableTypeTable)]; // If we are editing a row, attempt to save that row - if saving failed, reselect the edit row. if (isEditingRow && [tableContentView selectedRow] != currentlyEditingRow && ![self saveRowOnDeselect]) return; if (![tableDocumentInstance isWorking]) { // Update the row selection count // and update the status of the delete/duplicate buttons if([tablesListInstance tableType] == SPTableTypeTable) { if ([tableContentView numberOfSelectedRows] > 0) { [duplicateButton setEnabled:([tableContentView numberOfSelectedRows] == 1)]; [removeButton setEnabled:YES]; } else { [duplicateButton setEnabled:NO]; [removeButton setEnabled:NO]; } } else { [duplicateButton setEnabled:NO]; [removeButton setEnabled:NO]; } } [self updateCountText]; #ifndef SP_CODA /* triggered commands */ NSArray *triggeredCommands = [SPAppDelegate bundleCommandsForTrigger:SPBundleTriggerActionTableRowChanged]; 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) { if ([[data objectAtIndex:1] isEqualToString:SPBundleScopeGeneral]) { [[SPAppDelegate onMainThread] executeBundleItemForApp:aMenuItem]; } else if ([[data objectAtIndex:1] isEqualToString:SPBundleScopeDataTable]) { if ([[[[[NSApp mainWindow] firstResponder] class] description] isEqualToString:@"SPCopyTable"]) { [[[[NSApp mainWindow] firstResponder] onMainThread] executeBundleItemForDataTable:aMenuItem]; } } else if ([[data objectAtIndex:1] isEqualToString:SPBundleScopeInputField]) { if ([[[NSApp mainWindow] firstResponder] isKindOfClass:[NSTextView class]]) { [[[[NSApp mainWindow] firstResponder] onMainThread] executeBundleItemForInputField:aMenuItem]; } } } } #endif } /** * Saves the new column size in the preferences. */ - (void)tableViewColumnDidResize:(NSNotification *)notification { // Check our notification object is our table content view if ([notification object] != tableContentView) return; // Sometimes the column has no identifier. I can't figure out what is causing it, so we just skip over this item if (![[[notification userInfo] objectForKey:@"NSTableColumn"] identifier]) return; NSMutableDictionary *tableColumnWidths; NSString *database = [NSString stringWithFormat:@"%@@%@", [tableDocumentInstance database], [tableDocumentInstance host]]; NSString *table = [tablesListInstance tableName]; // Get tableColumnWidths object #ifndef SP_CODA if ([prefs objectForKey:SPTableColumnWidths] != nil ) { tableColumnWidths = [NSMutableDictionary dictionaryWithDictionary:[prefs objectForKey:SPTableColumnWidths]]; } else { #endif tableColumnWidths = [NSMutableDictionary dictionary]; #ifndef SP_CODA } #endif // Get the database object if ([tableColumnWidths objectForKey:database] == nil) { [tableColumnWidths setObject:[NSMutableDictionary dictionary] forKey:database]; } else { [tableColumnWidths setObject:[NSMutableDictionary dictionaryWithDictionary:[tableColumnWidths objectForKey:database]] forKey:database]; } // Get the table object if ([[tableColumnWidths objectForKey:database] objectForKey:table] == nil) { [[tableColumnWidths objectForKey:database] setObject:[NSMutableDictionary dictionary] forKey:table]; } else { [[tableColumnWidths objectForKey:database] setObject:[NSMutableDictionary dictionaryWithDictionary:[[tableColumnWidths objectForKey:database] objectForKey:table]] forKey:table]; } // Save column size [[[tableColumnWidths objectForKey:database] objectForKey:table] setObject:[NSNumber numberWithDouble:[(NSTableColumn *)[[notification userInfo] objectForKey:@"NSTableColumn"] width]] forKey:[[[[notification userInfo] objectForKey:@"NSTableColumn"] headerCell] stringValue]]; #ifndef SP_CODA [prefs setObject:tableColumnWidths forKey:SPTableColumnWidths]; #endif } /** * Confirm whether to allow editing of a row. Returns YES by default, unless the multipleLineEditingButton is in * the ON state, or for blob or text fields - in those cases opens a sheet for editing instead and returns NO. */ - (BOOL)tableView:(NSTableView *)tableView shouldEditTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)rowIndex { if ([tableDocumentInstance isWorking]) return NO; #ifndef SP_CODA if (tableView == filterTableView) { return (filterTableIsSwapped && [[tableColumn identifier] integerValue] == 0) ? NO : YES; } else #endif if (tableView == tableContentView) { // Ensure that row is editable since it could contain "(not loaded)" columns together with // issue that the table has no primary key NSString *wherePart = [NSString stringWithString:[self argumentForRow:[tableContentView selectedRow]]]; if ([wherePart length] == 0) return NO; // If the selected cell hasn't been loaded, load it. if ([[tableValues cellDataAtRow:rowIndex column:[[tableColumn identifier] integerValue]] isSPNotLoaded]) { // Only get the data for the selected column, not all of them NSString *query = [NSString stringWithFormat:@"SELECT %@ FROM %@ WHERE %@", [[[tableColumn headerCell] stringValue] backtickQuotedString], [selectedTable backtickQuotedString], wherePart]; SPMySQLResult *tempResult = [mySQLConnection queryString:query]; if (![tempResult numberOfRows]) { SPBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, [tableDocumentInstance parentWindow], self, nil, nil, NSLocalizedString(@"Couldn't load the row. Reload the table to be sure that the row exists and use a primary key for your table.", @"message of panel when loading of row failed")); return NO; } NSArray *tempRow = [tempResult getRowAsArray]; [tableValues replaceObjectInRow:rowIndex column:[[tableContentView tableColumns] indexOfObject:tableColumn] withObject:[tempRow objectAtIndex:0]]; [tableContentView reloadData]; } // Open the editing sheet if required if ([tableContentView shouldUseFieldEditorForRow:rowIndex column:[[tableColumn identifier] integerValue] checkWithLock:NULL]) { // Retrieve the column definition NSDictionary *columnDefinition = [cqColumnDefinition objectAtIndex:[[tableColumn identifier] integerValue]]; BOOL isBlob = [tableDataInstance columnIsBlobOrText:[[tableColumn headerCell] stringValue]]; // A table is per definition editable BOOL isFieldEditable = YES; // Check for Views if field is editable if ([tablesListInstance tableType] == SPTableTypeView) { NSArray *editStatus = [self fieldEditStatusForRow:rowIndex andColumn:[[tableColumn identifier] integerValue]]; isFieldEditable = [[editStatus objectAtIndex:0] integerValue] == 1; } NSUInteger fieldLength = 0; NSString *fieldEncoding = nil; BOOL allowNULL = YES; NSString *fieldType = [columnDefinition objectForKey:@"type"]; if ([columnDefinition objectForKey:@"char_length"]) { fieldLength = [[columnDefinition objectForKey:@"char_length"] integerValue]; } if ([columnDefinition objectForKey:@"null"]) { allowNULL = (![[columnDefinition objectForKey:@"null"] integerValue]); } if ([columnDefinition objectForKey:@"charset_name"] && ![[columnDefinition objectForKey:@"charset_name"] isEqualToString:@"binary"]) { fieldEncoding = [columnDefinition objectForKey:@"charset_name"]; } if (fieldEditor) SPClear(fieldEditor); fieldEditor = [[SPFieldEditorController alloc] init]; [fieldEditor setEditedFieldInfo:[NSDictionary dictionaryWithObjectsAndKeys: [[tableColumn headerCell] stringValue], @"colName", [self usedQuery], @"usedQuery", @"content", @"tableSource", nil]]; [fieldEditor setTextMaxLength:fieldLength]; [fieldEditor setFieldType:fieldType == nil ? @"" : fieldType]; [fieldEditor setFieldEncoding:fieldEncoding == nil ? @"" : fieldEncoding]; [fieldEditor setAllowNULL:allowNULL]; id cellValue = [tableValues cellDataAtRow:rowIndex column:[[tableColumn identifier] integerValue]]; if ([cellValue isNSNull]) { cellValue = [NSString stringWithString:[prefs objectForKey:SPNullValue]]; } if ([[columnDefinition objectForKey:@"typegrouping"] isEqualToString:@"binary"] && [prefs boolForKey:SPDisplayBinaryDataAsHex]) { [fieldEditor setTextMaxLength:[[self tableView:tableContentView objectValueForTableColumn:tableColumn row:rowIndex] length]]; } NSInteger editedColumn = 0; for (NSTableColumn* col in [tableContentView tableColumns]) { if ([[col identifier] isEqualToString:[tableColumn identifier]]) break; editedColumn++; } [fieldEditor editWithObject:cellValue fieldName:[[tableColumn headerCell] stringValue] usingEncoding:[mySQLConnection stringEncoding] isObjectBlob:isBlob isEditable:isFieldEditable withWindow:[tableDocumentInstance parentWindow] sender:self contextInfo:[NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithInteger:rowIndex], @"rowIndex", [NSNumber numberWithInteger:editedColumn], @"columnIndex", [NSNumber numberWithBool:isFieldEditable], @"isFieldEditable", nil]]; return NO; } return YES; } return YES; } /** * Enable drag from tableview */ - (BOOL)tableView:(NSTableView *)tableView writeRowsWithIndexes:(NSIndexSet *)rows toPasteboard:(NSPasteboard*)pboard { if (tableView == tableContentView) { NSString *tmp; // By holding ⌘, ⇧, or/and ⌥ copies selected rows as SQL INSERTS // otherwise \t delimited lines if ([[NSApp currentEvent] modifierFlags] & (NSCommandKeyMask|NSShiftKeyMask|NSAlternateKeyMask)) { tmp = [tableContentView rowsAsSqlInsertsOnlySelectedRows:YES]; } else { tmp = [tableContentView draggedRowsAsTabString]; } #warning code inside "if" is unreachable if (!tmp && [tmp length]) { [pboard declareTypes:@[NSTabularTextPboardType, NSStringPboardType] owner:nil]; [pboard setString:tmp forType:NSStringPboardType]; [pboard setString:tmp forType:NSTabularTextPboardType]; return YES; } } return NO; } /** * Disable row selection while the document is working. */ - (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(NSInteger)rowIndex { #ifndef SP_CODA if (tableView == filterTableView) { return YES; } else #endif return tableView == tableContentView ? tableRowsSelectable : YES; } /** * Resize a column when it's double-clicked (10.6+ only). */ - (CGFloat)tableView:(NSTableView *)tableView sizeToFitWidthOfColumn:(NSInteger)columnIndex { NSTableColumn *theColumn = [[tableView tableColumns] objectAtIndex:columnIndex]; NSDictionary *columnDefinition = [dataColumns objectAtIndex:[[theColumn identifier] integerValue]]; // Get the column width NSUInteger targetWidth = [tableContentView autodetectWidthForColumnDefinition:columnDefinition maxRows:500]; #ifndef SP_CODA // Clear any saved widths for the column NSString *dbKey = [NSString stringWithFormat:@"%@@%@", [tableDocumentInstance database], [tableDocumentInstance host]]; NSString *tableKey = [tablesListInstance tableName]; NSMutableDictionary *savedWidths = [NSMutableDictionary dictionaryWithDictionary:[prefs objectForKey:SPTableColumnWidths]]; NSMutableDictionary *dbDict = [NSMutableDictionary dictionaryWithDictionary:[savedWidths objectForKey:dbKey]]; NSMutableDictionary *tableDict = [NSMutableDictionary dictionaryWithDictionary:[dbDict objectForKey:tableKey]]; if ([tableDict objectForKey:[columnDefinition objectForKey:@"name"]]) { [tableDict removeObjectForKey:[columnDefinition objectForKey:@"name"]]; if ([tableDict count]) { [dbDict setObject:[NSDictionary dictionaryWithDictionary:tableDict] forKey:tableKey]; } else { [dbDict removeObjectForKey:tableKey]; } if ([dbDict count]) { [savedWidths setObject:[NSDictionary dictionaryWithDictionary:dbDict] forKey:dbKey]; } else { [savedWidths removeObjectForKey:dbKey]; } [prefs setObject:[NSDictionary dictionaryWithDictionary:savedWidths] forKey:SPTableColumnWidths]; } #endif // Return the width, while the delegate is empty to prevent column resize notifications [tableContentView setDelegate:nil]; [tableContentView performSelector:@selector(setDelegate:) withObject:self afterDelay:0.1]; return targetWidth; } /** * This function changes the text color of text/blob fields which are null or not yet loaded to gray */ - (void)tableView:(SPCopyTable *)tableView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)rowIndex { #ifndef SP_CODA if (tableView == filterTableView) { if (filterTableIsSwapped && [[tableColumn identifier] integerValue] == 0) { [cell setDrawsBackground:YES]; [cell setBackgroundColor:lightGrayColor]; } else { [cell setDrawsBackground:NO]; } return; } else #endif if (tableView == tableContentView) { if (![cell respondsToSelector:@selector(setTextColor:)]) return; BOOL cellIsNullOrUnloaded = NO; BOOL cellIsLinkCell = [cell isMemberOfClass:[SPTextAndLinkCell class]]; NSUInteger columnIndex = [[tableColumn identifier] integerValue]; // If user wants to edit 'cell' set text color to black and return to avoid // writing in gray if value was NULL if ([tableView editedColumn] != -1 && [tableView editedRow] == rowIndex && (NSUInteger)[[NSArrayObjectAtIndex([tableView tableColumns], [tableView editedColumn]) identifier] integerValue] == columnIndex) { [cell setTextColor:blackColor]; if (cellIsLinkCell) [cell setLinkActive:NO]; return; } // While the table is being loaded, additional validation is required - data // locks must be used to avoid crashes, and indexes higher than the available // rows or columns may be requested. Use gray to indicate loading in these cases. if (isWorking) { pthread_mutex_lock(&tableValuesLock); if (rowIndex < (NSInteger)tableRowsCount && columnIndex < [tableValues columnCount]) { cellIsNullOrUnloaded = [tableValues cellIsNullOrUnloadedAtRow:rowIndex column:columnIndex]; } pthread_mutex_unlock(&tableValuesLock); } else { cellIsNullOrUnloaded = [tableValues cellIsNullOrUnloadedAtRow:rowIndex column:columnIndex]; } if (cellIsNullOrUnloaded) { [cell setTextColor:rowIndex == [tableContentView selectedRow] ? whiteColor : lightGrayColor]; } else { [cell setTextColor:blackColor]; } NSDictionary *columnDefinition = [[(id <SPDatabaseContentViewDelegate>)[tableContentView delegate] dataColumnDefinitions] objectAtIndex:columnIndex]; NSString *columnType = [columnDefinition objectForKey:@"typegrouping"]; // Find a more reliable way of doing this check if ([columnType isEqualToString:@"binary"] && [prefs boolForKey:SPDisplayBinaryDataAsHex] && [[self tableView:tableContentView objectValueForTableColumn:tableColumn row:rowIndex] hasPrefix:@"0x"]) { [cell setTextColor:rowIndex == [tableContentView selectedRow] ? whiteColor : blueColor]; } // Disable link arrows for the currently editing row and for any NULL or unloaded cells if (cellIsLinkCell) { if (cellIsNullOrUnloaded || [tableView editedRow] == rowIndex) { [cell setLinkActive:NO]; } else { [cell setLinkActive:YES]; } } } } #ifndef SP_CODA /** * Show the table cell content as tooltip * * - for text displays line breaks and tabs as well * - if blob data can be interpret as image data display the image as transparent thumbnail * (up to now using base64 encoded HTML data). */ - (NSString *)tableView:(NSTableView *)tableView toolTipForCell:(id)aCell rect:(NSRectPointer)rect tableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row mouseLocation:(NSPoint)mouseLocation { if (tableView == filterTableView) { return nil; } else if (tableView == tableContentView) { if ([[aCell stringValue] length] < 2 || [tableDocumentInstance isWorking]) return nil; // Suppress tooltip if another toolip is already visible, mainly displayed by a Bundle command // TODO has to be improved for (id win in [NSApp orderedWindows]) { if ([[[[win contentView] class] description] isEqualToString:@"WebView"]) return nil; } NSImage *image; NSPoint pos = [NSEvent mouseLocation]; pos.y -= 20; id theValue = nil; // While the table is being loaded, additional validation is required - data // locks must be used to avoid crashes, and indexes higher than the available // rows or columns may be requested. Return "..." to indicate loading in these // cases. if (isWorking) { pthread_mutex_lock(&tableValuesLock); if (row < (NSInteger)tableRowsCount && [[tableColumn identifier] integerValue] < (NSInteger)[tableValues columnCount]) { theValue = [[SPDataStorageObjectAtRowAndColumn(tableValues, row, [[tableColumn identifier] integerValue]) copy] autorelease]; } pthread_mutex_unlock(&tableValuesLock); if (!theValue) theValue = @"..."; } else { theValue = SPDataStorageObjectAtRowAndColumn(tableValues, row, [[tableColumn identifier] integerValue]); } if (theValue == nil) return nil; if ([theValue isKindOfClass:[NSData class]]) { image = [[[NSImage alloc] initWithData:theValue] autorelease]; if (image) { [SPTooltip showWithObject:image atLocation:pos ofType:@"image"]; return nil; } } else if ([theValue isKindOfClass:[SPMySQLGeometryData class]]) { SPGeometryDataView *v = [[SPGeometryDataView alloc] initWithCoordinates:[theValue coordinates]]; image = [v thumbnailImage]; if (image) { [SPTooltip showWithObject:image atLocation:pos ofType:@"image"]; [v release]; return nil; } [v release]; } // Show the cell string value as tooltip (including line breaks and tabs) // by using the cell's font [SPTooltip showWithObject:[aCell stringValue] atLocation:pos ofType:@"text" displayOptions:[NSDictionary dictionaryWithObjectsAndKeys: [[aCell font] familyName], @"fontname", [NSString stringWithFormat:@"%f",[[aCell font] pointSize]], @"fontsize", nil]]; return nil; } return nil; } #endif #ifndef SP_CODA /* SplitView delegate methods */ #pragma mark - #pragma mark SplitView delegate methods - (BOOL)splitView:(NSSplitView *)sender canCollapseSubview:(NSView *)subview { return NO; } /** * Set a minimum size for the filter text area. */ - (CGFloat)splitView:(NSSplitView *)sender constrainMaxCoordinate:(CGFloat)proposedMax ofSubviewAt:(NSInteger)offset { return proposedMax - 180; } /** * Set a minimum size for the field list and action area. */ - (CGFloat)splitView:(NSSplitView *)sender constrainMinCoordinate:(CGFloat)proposedMin ofSubviewAt:(NSInteger)offset { return proposedMin + 225; } /** * Improve default resizing and resize only the filter text area by default. */ - (void)splitView:(NSSplitView *)sender resizeSubviewsWithOldSize:(NSSize)oldSize { NSSize newSize = [sender frame].size; NSView *leftView = [[sender subviews] objectAtIndex:0]; NSView *rightView = [[sender subviews] objectAtIndex:1]; float dividerThickness = [sender dividerThickness]; NSRect leftFrame = [leftView frame]; NSRect rightFrame = [rightView frame]; // Resize height of both views leftFrame.size.height = newSize.height; rightFrame.size.height = newSize.height; // Only resize the right view's width - unless the constraint has been reached if (rightFrame.size.width > 180 || newSize.width > oldSize.width) { rightFrame.size.width = newSize.width - leftFrame.size.width - dividerThickness; } else { leftFrame.size.width = newSize.width - rightFrame.size.width - dividerThickness; } rightFrame.origin.x = leftFrame.size.width + dividerThickness; [leftView setFrame:leftFrame]; [rightView setFrame:rightFrame]; } #endif #pragma mark - #pragma mark Control delegate methods - (void)controlTextDidChange:(NSNotification *)notification { #ifndef SP_CODA if ([notification object] == filterTableView) { NSString *string = [[[[notification userInfo] objectForKey:@"NSFieldEditor"] textStorage] string]; if (string && [string length]) { if (lastEditedFilterTableValue) [lastEditedFilterTableValue release]; lastEditedFilterTableValue = [[NSString stringWithString:string] retain]; } [self updateFilterTableClause:string]; } #endif } /** * If the user selected a table cell which is a blob field and tried to edit it * cancel the fieldEditor, display the field editor sheet instead for editing * and re-enable the fieldEditor after editing. */ - (BOOL)control:(NSControl *)control textShouldBeginEditing:(NSText *)aFieldEditor { if (control != tableContentView) return YES; NSUInteger row, column; BOOL shouldBeginEditing = YES; row = [tableContentView editedRow]; column = [tableContentView editedColumn]; // If cell editing mode and editing request comes // from the keyboard show an error tooltip // or bypass if numberOfPossibleUpdateRows == 1 if ([tableContentView isCellEditingMode]) { NSArray *editStatus = [self fieldEditStatusForRow:row andColumn:[[NSArrayObjectAtIndex([tableContentView tableColumns], column) identifier] integerValue]]; NSInteger numberOfPossibleUpdateRows = [[editStatus objectAtIndex:0] integerValue]; NSPoint pos = [[tableDocumentInstance parentWindow] convertBaseToScreen:[tableContentView convertPoint:[tableContentView frameOfCellAtColumn:column row:row].origin toView:nil]]; pos.y -= 20; switch (numberOfPossibleUpdateRows) { case -1: [SPTooltip showWithObject:kCellEditorErrorNoMultiTabDb atLocation:pos ofType:@"text"]; shouldBeginEditing = NO; break; case 0: [SPTooltip showWithObject:[NSString stringWithFormat:kCellEditorErrorNoMatch, selectedTable] atLocation:pos ofType:@"text"]; shouldBeginEditing = NO; break; case 1: shouldBeginEditing = YES; break; default: [SPTooltip showWithObject:[NSString stringWithFormat:kCellEditorErrorTooManyMatches, (long)numberOfPossibleUpdateRows] atLocation:pos ofType:@"text"]; shouldBeginEditing = NO; } } // Open the field editor sheet if required if ([tableContentView shouldUseFieldEditorForRow:row column:column checkWithLock:NULL]) { [tableContentView setFieldEditorSelectedRange:[aFieldEditor selectedRange]]; // Cancel editing [control abortEditing]; // Call the field editor sheet [self tableView:tableContentView shouldEditTableColumn:NSArrayObjectAtIndex([tableContentView tableColumns], column) row:row]; // send current event to field editor sheet if ([NSApp currentEvent]) { [NSApp sendEvent:[NSApp currentEvent]]; } return NO; } return shouldBeginEditing; } /** * Trap the enter, escape, tab and arrow keys, overriding default behaviour and continuing/ending editing, * only within the current row. */ - (BOOL)control:(NSControl<NSControlTextEditingDelegate> *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)command { // Check firstly if SPCopyTable can handle command if ([control control:control textView:textView doCommandBySelector:command]) return YES; // Trap the escape key if ([[control window] methodForSelector:command] == [[control window] methodForSelector:@selector(cancelOperation:)]) { // Abort editing [control abortEditing]; if ((SPCopyTable*)control == tableContentView) { [self cancelRowEditing]; } return YES; } return NO; } #pragma mark - #pragma mark Database content view delegate methods - (NSString *)usedQuery { return usedQuery; } /** * Retrieve the data column definitions */ - (NSArray *)dataColumnDefinitions { return dataColumns; } @end