//
// $Id$
//
// SPTableStructureDelegate.m
// sequel-pro
//
// Created by Stuart Connolly (stuconnolly.com) on October 26, 2010
// Copyright (c) 2010 Stuart Connolly. All rights reserved.
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
// More info at
#import "SPTableStructureDelegate.h"
#import "SPAlertSheets.h"
@implementation SPTableStructure (SPTableStructureDelegate)
#pragma mark -
#pragma mark Table view datasource methods
- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
{
return [tableFields count];
}
- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)rowIndex
{
// Return a placeholder if the table is reloading
if (rowIndex >= [tableFields count]) return @"...";
if([[tableColumn identifier] isEqualToString:@"collation"]) {
NSInteger idx = 0;
if((idx = [[NSArrayObjectAtIndex(tableFields,rowIndex) objectForKey:@"encoding"] integerValue]) > 0) {
NSString *enc = [[encodingPopupCell itemAtIndex:idx] title];
NSInteger start = [enc rangeOfString:@"("].location+1;
NSInteger end = [enc length] - start - 1;
collations = [databaseDataInstance getDatabaseCollationsForEncoding:[enc substringWithRange:NSMakeRange(start, end)]];
} else {
if([tableDataInstance tableEncoding] != nil) {
collations = [databaseDataInstance getDatabaseCollationsForEncoding:[tableDataInstance tableEncoding]];
} else {
collations = [NSArray array];
}
}
[[tableColumn dataCell] removeAllItems];
if ([collations count] > 0) {
[[tableColumn dataCell] addItemWithTitle:@""];
// Populate collation popup button
for (NSDictionary *collation in collations)
[[tableColumn dataCell] addItemWithTitle:[collation objectForKey:@"COLLATION_NAME"]];
}
}
return [NSArrayObjectAtIndex(tableFields, rowIndex) objectForKey:[tableColumn identifier]];
}
- (void)tableView:(NSTableView *)aTableView setObjectValue:(id)anObject forTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex
{
// Make sure that the drag operation is for the right table view
if (aTableView != tableSourceView) return;
if (!isEditingRow) {
[oldRow setDictionary:[tableFields objectAtIndex:rowIndex]];
isEditingRow = YES;
currentlyEditingRow = rowIndex;
}
NSDictionary *currentRow = [tableFields objectAtIndex:rowIndex];
// Reset collation if encoding was changed
if([[aTableColumn identifier] isEqualToString:@"encoding"]) {
if([[currentRow objectForKey:@"encoding"] integerValue] != [anObject integerValue]) {
[currentRow setObject:[NSNumber numberWithInteger:0] forKey:@"collation"];
[tableSourceView reloadData];
}
}
// Reset collation if BINARY was set to 1 since BINARY sets collation to *_bin
else if([[aTableColumn identifier] isEqualToString:@"binary"]) {
if([[currentRow objectForKey:@"binary"] integerValue] != [anObject integerValue]) {
if([anObject integerValue] == 1) {
[currentRow setObject:[NSNumber numberWithInteger:0] forKey:@"collation"];
}
[tableSourceView reloadData];
}
}
// Reset collation if BINARY was set to 1 since BINARY sets collation to *_bin
else if([[aTableColumn identifier] isEqualToString:@"Extra"]) {
if(![[currentRow objectForKey:@"Extra"] isEqualToString:anObject]) {
if([[[currentRow objectForKey:@"Extra"] uppercaseString] isEqualToString:@"AUTO_INCREMENT"]) {
[currentRow setObject:[NSNumber numberWithInteger:0] forKey:@"null"];
}
[tableSourceView reloadData];
}
}
// Reset default to "" if field doesn't allow NULL and current default is set to NULL
else if([[aTableColumn identifier] isEqualToString:@"null"]) {
if([[currentRow objectForKey:@"null"] integerValue] != [anObject integerValue]) {
if([anObject integerValue] == 0) {
if([[currentRow objectForKey:@"default"] isEqualToString:[prefs objectForKey:SPNullValue]])
[currentRow setObject:@"" forKey:@"default"];
}
[tableSourceView reloadData];
}
}
// Store new value but not if user choose "---" for type and reset values if required
if([[aTableColumn identifier] isEqualToString:@"type"]) {
if(anObject && [(NSString*)anObject length] && ![(NSString*)anObject hasPrefix:@"--"]) {
[currentRow setObject:[(NSString*)anObject uppercaseString] forKey:@"type"];
// If type is BLOB or TEXT reset DEFAULT since these field types don't allow a default
if([[currentRow objectForKey:@"type"] hasSuffix:@"TEXT"] || [[currentRow objectForKey:@"type"] hasSuffix:@"BLOB"] || [self _isFieldTypeGeometry:[currentRow objectForKey:@"type"]]) {
[currentRow setObject:@"" forKey:@"default"];
[currentRow setObject:@"" forKey:@"length"];
}
[tableSourceView reloadData];
}
}
else {
[currentRow setObject:(anObject) ? anObject : @"" forKey:[aTableColumn identifier]];
}
}
/**
* Confirm whether to allow editing of a row. Returns YES by default, but NO for views.
*/
- (BOOL)tableView:(NSTableView *)aTableView shouldEditTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex
{
if ([tableDocumentInstance isWorking]) return NO;
// Return NO for views
if ([tablesListInstance tableType] == SPTableTypeView) return NO;
return YES;
}
/**
Begin a drag and drop operation from the table - copy a single dragged row to the drag pasteboard.
*/
- (BOOL)tableView:(NSTableView *)aTableView writeRowsWithIndexes:(NSIndexSet *)rows toPasteboard:(NSPasteboard*)pboard
{
// Make sure that the drag operation is started from the right table view
if (aTableView != tableSourceView) return NO;
// Check whether a save of the current field row is required.
if ( ![self saveRowOnDeselect] ) return NO;
if ([rows count] == 1) {
[pboard declareTypes:[NSArray arrayWithObject:SPDefaultPasteboardDragType] owner:nil];
[pboard setString:[[NSNumber numberWithInteger:[rows firstIndex]] stringValue] forType:SPDefaultPasteboardDragType];
return YES;
}
else {
return NO;
}
}
/**
Determine whether to allow a drag and drop operation on this table - for the purposes of drag reordering,
validate that the original source is of the correct type and within the same table, and that the drag
would result in a position change.
*/
- (NSDragOperation)tableView:(NSTableView*)tableView validateDrop:(id )info proposedRow:(NSInteger)row
proposedDropOperation:(NSTableViewDropOperation)operation
{
//make sure that the drag operation is for the right table view
if (tableView!=tableSourceView) return NO;
NSArray *pboardTypes = [[info draggingPasteboard] types];
NSInteger originalRow;
// Ensure the drop is of the correct type
if (operation == NSTableViewDropAbove && row != -1 && [pboardTypes containsObject:SPDefaultPasteboardDragType]) {
// Ensure the drag originated within this table
if ([info draggingSource] == tableView) {
originalRow = [[[info draggingPasteboard] stringForType:SPDefaultPasteboardDragType] integerValue];
if (row != originalRow && row != (originalRow+1)) {
return NSDragOperationMove;
}
}
}
return NSDragOperationNone;
}
/**
* Having validated a drop, perform the field/column reordering to match.
*/
- (BOOL)tableView:(NSTableView*)tableView acceptDrop:(id )info row:(NSInteger)destinationRowIndex dropOperation:(NSTableViewDropOperation)operation
{
//make sure that the drag operation is for the right table view
if (tableView!=tableSourceView) return NO;
NSInteger originalRowIndex;
NSMutableString *queryString;
NSDictionary *originalRow;
// Extract the original row position from the pasteboard and retrieve the details
originalRowIndex = [[[info draggingPasteboard] stringForType:SPDefaultPasteboardDragType] integerValue];
originalRow = [[NSDictionary alloc] initWithDictionary:[tableFields objectAtIndex:originalRowIndex]];
[[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryWillBePerformed" object:tableDocumentInstance];
// Begin construction of the reordering query
queryString = [NSMutableString stringWithFormat:@"ALTER TABLE %@ MODIFY COLUMN %@ %@", [selectedTable backtickQuotedString],
[[originalRow objectForKey:@"name"] backtickQuotedString],
[[originalRow objectForKey:@"type"] uppercaseString]];
// Add the length parameter if necessary
if ( [originalRow objectForKey:@"length"] && ![[originalRow objectForKey:@"length"] isEqualToString:@""]) {
[queryString appendFormat:@"(%@)", [originalRow objectForKey:@"length"]];
}
NSString *fieldEncoding = @"";
if([[originalRow objectForKey:@"encoding"] integerValue] > 0) {
NSString *enc = [[encodingPopupCell itemAtIndex:[[originalRow objectForKey:@"encoding"] integerValue]] title];
NSInteger start = [enc rangeOfString:@"("].location+1;
NSInteger end = [enc length] - start - 1;
fieldEncoding = [enc substringWithRange:NSMakeRange(start, end)];
[queryString appendFormat:@" CHARACTER SET %@", fieldEncoding];
}
if(![fieldEncoding length] && [tableDataInstance tableEncoding]) {
fieldEncoding = [tableDataInstance tableEncoding];
}
if([fieldEncoding length] && [[originalRow objectForKey:@"collation"] integerValue] > 0) {
NSArray *theCollations = [databaseDataInstance getDatabaseCollationsForEncoding:fieldEncoding];
NSString *col = [[theCollations objectAtIndex:[[originalRow objectForKey:@"collation"] integerValue]-1] objectForKey:@"COLLATION_NAME"];
[queryString appendFormat:@" COLLATE %@", col];
}
// Add unsigned, zerofill, binary, not null if necessary
if ([[originalRow objectForKey:@"unsigned"] integerValue]) {
[queryString appendString:@" UNSIGNED"];
}
if ([[originalRow objectForKey:@"zerofill"] integerValue]) {
[queryString appendString:@" ZEROFILL"];
}
if ([[originalRow objectForKey:@"binary"] integerValue]) {
[queryString appendString:@" BINARY"];
}
if (![[originalRow objectForKey:@"null"] integerValue]) {
[queryString appendString:@" NOT NULL"];
}
if (![[originalRow objectForKey:@"Extra"] isEqualToString:@"None"] ) {
[queryString appendString:@" "];
[queryString appendString:[[originalRow objectForKey:@"Extra"] uppercaseString]];
}
BOOL isTimestampType = [[[originalRow objectForKey:@"type"] lowercaseString] isEqualToString:@"timestamp"];
// Add the default value, skip it for auto_increment
if([originalRow objectForKey:@"Extra"] && ![[originalRow objectForKey:@"Extra"] isEqualToString:@"auto_increment"]) {
if ([[originalRow objectForKey:@"default"] isEqualToString:[prefs objectForKey:SPNullValue]]) {
if ([[originalRow objectForKey:@"null"] integerValue] == 1) {
[queryString appendString:(isTimestampType) ? @" NULL DEFAULT NULL" : @" DEFAULT NULL"];
}
}
else if (isTimestampType && ([[[originalRow objectForKey:@"default"] uppercaseString] isEqualToString:@"CURRENT_TIMESTAMP"]) ) {
[queryString appendString:@" DEFAULT CURRENT_TIMESTAMP"];
}
else {
[queryString appendFormat:@" DEFAULT '%@'", [mySQLConnection prepareString:[originalRow objectForKey:@"default"]]];
}
}
// Any column comments
if ([[originalRow objectForKey:@"comment"] length]) {
[queryString appendFormat:@" COMMENT '%@'", [mySQLConnection prepareString:[originalRow objectForKey:@"comment"]]];
}
// Unparsed details - column formats, storage, reference definitions
if ([originalRow objectForKey:@"unparsed"]) {
[queryString appendString:[originalRow objectForKey:@"unparsed"]];
}
// Add the new location
if ( destinationRowIndex == 0 ){
[queryString appendString:@" FIRST"];
} else {
[queryString appendFormat:@" AFTER %@",
[[[tableFields objectAtIndex:destinationRowIndex-1] objectForKey:@"name"] backtickQuotedString]];
}
// Run the query; report any errors, or reload the table on success
[mySQLConnection queryString:queryString];
if ([mySQLConnection queryErrored]) {
SPBeginAlertSheet(NSLocalizedString(@"Error moving field", @"error moving field message"), NSLocalizedString(@"OK", @"OK button"), nil, nil, [tableDocumentInstance parentWindow], self, nil, nil,
[NSString stringWithFormat:NSLocalizedString(@"An error occurred while trying to move the field.\n\nMySQL said: %@", @"error moving field informative message"), [mySQLConnection getLastErrorMessage]]);
}
else {
[tableDataInstance resetAllData];
[tablesListInstance setStatusRequiresReload:YES];
[self loadTable:selectedTable];
// Mark the content table cache for refresh
[tablesListInstance setContentRequiresReload:YES];
if ( originalRowIndex < destinationRowIndex ) {
[tableSourceView selectRowIndexes:[NSIndexSet indexSetWithIndex:destinationRowIndex-1] byExtendingSelection:NO];
} else {
[tableSourceView selectRowIndexes:[NSIndexSet indexSetWithIndex:destinationRowIndex] byExtendingSelection:NO];
}
}
[[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryHasBeenPerformed" object:tableDocumentInstance];
[originalRow release];
return YES;
}
#pragma mark -
#pragma mark Table view delegate methods
- (BOOL)tableView:(NSTableView *)aTableView shouldSelectRow:(NSInteger)rowIndex
{
// If we are editing a row, attempt to save that row - if saving failed, do not select the new row.
if (isEditingRow && ![self addRowToDB]) return NO;
return YES;
}
/**
* Performs various interface validation
*/
- (void)tableViewSelectionDidChange:(NSNotification *)aNotification
{
id object = [aNotification object];
// Check for which table view the selection changed
if (object == tableSourceView) {
// If we are editing a row, attempt to save that row - if saving failed, reselect the edit row.
if (isEditingRow && [tableSourceView selectedRow] != currentlyEditingRow && ![self saveRowOnDeselect]) return;
[copyFieldButton setEnabled:YES];
// Check if there is currently a field selected and change button state accordingly
if ([tableSourceView numberOfSelectedRows] > 0 && [tablesListInstance tableType] == SPTableTypeTable) {
[removeFieldButton setEnabled:YES];
} else {
[removeFieldButton setEnabled:NO];
[copyFieldButton setEnabled:NO];
}
// If the table only has one field, disable the remove button. This removes the need to check that the user
// is attempting to remove the last field in a table in removeField: above, but leave it in just in case.
if ([tableSourceView numberOfRows] == 1) {
[removeFieldButton setEnabled:NO];
}
}
}
/**
* Traps enter and esc and make/cancel editing without entering next row
*/
- (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)command
{
NSInteger row, column;
row = [tableSourceView editedRow];
column = [tableSourceView editedColumn];
// Trap the tab key, selecting the next item in the line
if ( [textView methodForSelector:command] == [textView methodForSelector:@selector(insertTab:)] && [tableSourceView numberOfColumns] - 1 == column)
{
//save current line
[[control window] makeFirstResponder:control];
if ( [self addRowToDB] && [textView methodForSelector:command] == [textView methodForSelector:@selector(insertTab:)] ) {
if ( row < ([tableSourceView numberOfRows] - 1) ) {
[tableSourceView selectRowIndexes:[NSIndexSet indexSetWithIndex:row+1] byExtendingSelection:NO];
[tableSourceView editColumn:0 row:row+1 withEvent:nil select:YES];
} else {
[tableSourceView selectRowIndexes:[NSIndexSet indexSetWithIndex:0] byExtendingSelection:NO];
[tableSourceView editColumn:0 row:0 withEvent:nil select:YES];
}
}
return YES;
}
// Trap shift-tab key
else if ( [textView methodForSelector:command] == [textView methodForSelector:@selector(insertBacktab:)] && column < 1)
{
if ( [self addRowToDB] && [textView methodForSelector:command] == [textView methodForSelector:@selector(insertBacktab:)] ) {
[[control window] makeFirstResponder:control];
if ( row > 0) {
[tableSourceView selectRowIndexes:[NSIndexSet indexSetWithIndex:row-1] byExtendingSelection:NO];
[tableSourceView editColumn:([tableFields count]-1) row:row-1 withEvent:nil select:YES];
} else {
[tableSourceView selectRowIndexes:[NSIndexSet indexSetWithIndex:([tableFields count]-1)] byExtendingSelection:NO];
[tableSourceView editColumn:([tableFields count]-1) row:([tableSourceView numberOfRows]-1) withEvent:nil select:YES];
}
}
return YES;
}
// Trap the enter key, triggering a save
else if ( [textView methodForSelector:command] == [textView methodForSelector:@selector(insertNewline:)] )
{
// Suppress enter for non-text fields to allow selecting of chosen items from comboboxes or popups
if(![[[[[[tableSourceView tableColumns] objectAtIndex:column] dataCell] class] description] isEqualToString:@"NSTextFieldCell"])
return YES;
[[control window] makeFirstResponder:control];
[self addRowToDB];
[tableSourceView selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO];
[[tableDocumentInstance parentWindow] makeFirstResponder:tableSourceView];
return YES;
}
// Trap escape, aborting the edit and reverting the row
else if ( [[control window] methodForSelector:command] == [[control window] methodForSelector:@selector(cancelOperation:)] )
{
[control abortEditing];
[self cancelRowEditing];
return YES;
}
else {
return NO;
}
}
/**
* Modify cell display by disabling table cells when a view is selected, meaning structure/index
* is uneditable and do cell validation due to row's field type.
*/
- (void)tableView:(NSTableView *)tableView willDisplayCell:(id)aCell forTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex
{
//make sure that the message is from the right table view
if (tableView != tableSourceView) return;
if ([tablesListInstance tableType] == SPTableTypeView) {
[aCell setEnabled:NO];
}
else {
// validate cell against current field type
NSDictionary *theRow = NSArrayObjectAtIndex(tableFields, rowIndex);
NSString *theRowType = @"";
if(theRowType = [theRow objectForKey:@"type"])
theRowType = [[theRowType stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] uppercaseString];
// Only string fields allow encoding settings
if(([[aTableColumn identifier] isEqualToString:@"encoding"])) {
[aCell setEnabled:([self _isFieldTypeString:theRowType] && ![theRowType hasSuffix:@"BINARY"] && ![theRowType hasSuffix:@"BLOB"])];
}
// Only string fields allow collation settings and string field is not set to BINARY since BINARY sets the collation to *_bin
else if([[aTableColumn identifier] isEqualToString:@"collation"]){
[aCell setEnabled:([self _isFieldTypeString:theRowType] && [[theRow objectForKey:@"binary"] integerValue] == 0 && ![theRowType hasSuffix:@"BINARY"] && ![theRowType hasSuffix:@"BLOB"])];
}
// Check if UNSIGNED and ZEROFILL is allowed
else if([[aTableColumn identifier] isEqualToString:@"zerofill"] || [[aTableColumn identifier] isEqualToString:@"unsigned"]) {
[aCell setEnabled:([self _isFieldTypeNumeric:theRowType])];
}
// Check if BINARY is allowed
else if([[aTableColumn identifier] isEqualToString:@"binary"]) {
[aCell setEnabled:([self _isFieldTypeAllowBinary:theRowType])];
}
// TEXT, BLOB, and GEOMETRY fields don't allow a DEFAULT
else if([[aTableColumn identifier] isEqualToString:@"default"]) {
[aCell setEnabled:([theRowType hasSuffix:@"TEXT"] || [theRowType hasSuffix:@"BLOB"] || [self _isFieldTypeGeometry:theRowType]) ? NO : YES];
}
// Check allow NULL
else if([[aTableColumn identifier] isEqualToString:@"null"]) {
[aCell setEnabled:([[theRow objectForKey:@"Key"] isEqualToString:@"PRI"] || [[[theRow objectForKey:@"Extra"] uppercaseString] isEqualToString:@"AUTO_INCREMENT"]) ? NO : YES];
}
// TEXT, BLOB, date, and GEOMETRY fields don't allow a length
else if([[aTableColumn identifier] isEqualToString:@"length"]) {
[aCell setEnabled:([theRowType hasSuffix:@"TEXT"] || [theRowType hasSuffix:@"BLOB"] || [self _isFieldTypeDate:theRowType] || [self _isFieldTypeGeometry:theRowType]) ? NO : YES];
}
else {
[aCell setEnabled:YES];
}
}
}
#pragma mark -
#pragma mark Split view delegate methods
- (BOOL)splitView:(NSSplitView *)sender canCollapseSubview:(NSView *)subview
{
return YES;
}
- (CGFloat)splitView:(NSSplitView *)sender constrainMaxCoordinate:(CGFloat)proposedMax ofSubviewAt:(NSInteger)offset
{
return proposedMax - 130;
}
- (CGFloat)splitView:(NSSplitView *)sender constrainMinCoordinate:(CGFloat)proposedMin ofSubviewAt:(NSInteger)offset
{
return proposedMin + 130;
}
- (NSRect)splitView:(NSSplitView *)splitView additionalEffectiveRectOfDividerAtIndex:(NSInteger)dividerIndex
{
return [structureGrabber convertRect:[structureGrabber bounds] toView:splitView];
}
- (void)splitViewDidResizeSubviews:(NSNotification *)aNotification
{
if ([aNotification object] == tablesIndexesSplitView) {
NSView *indexesView = [[tablesIndexesSplitView subviews] objectAtIndex:1];
if ([tablesIndexesSplitView isSubviewCollapsed:indexesView]) {
[indexesShowButton setHidden:NO];
}
else {
[indexesShowButton setHidden:YES];
}
}
}
#pragma mark -
#pragma mark Combo box delegate methods
- (id)comboBoxCell:(NSComboBoxCell *)aComboBoxCell objectValueForItemAtIndex:(NSInteger)index
{
return NSArrayObjectAtIndex(typeSuggestions, index);
}
- (NSInteger)numberOfItemsInComboBoxCell:(NSComboBoxCell *)aComboBoxCell
{
return [typeSuggestions count];
}
/**
* Allow completion of field data types of lowercased input.
*/
- (NSString *)comboBoxCell:(NSComboBoxCell *)aComboBoxCell completedString:(NSString *)uncompletedString
{
if ([uncompletedString hasPrefix:@"-"]) return @"";
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF BEGINSWITH[c] %@", [uncompletedString uppercaseString]];
NSArray *result = [typeSuggestions filteredArrayUsingPredicate:predicate];
if (result && [result count]) return [result objectAtIndex:0];
return @"";
}
@end