diff options
-rw-r--r-- | Source/CMCopyTable.h | 36 | ||||
-rw-r--r-- | Source/CMCopyTable.m | 95 | ||||
-rw-r--r-- | Source/SPTableContent.h | 7 | ||||
-rw-r--r-- | Source/SPTableContent.m | 183 |
4 files changed, 294 insertions, 27 deletions
diff --git a/Source/CMCopyTable.h b/Source/CMCopyTable.h index 6baa2527..948607bc 100644 --- a/Source/CMCopyTable.h +++ b/Source/CMCopyTable.h @@ -25,6 +25,8 @@ #import <AppKit/AppKit.h> #import "SPTableView.h" +#define SP_MAX_CELL_WIDTH 200 + @class SPDataStorage; /*! @@ -112,6 +114,40 @@ */ - (void)setTableData:(SPDataStorage *)theTableStorage; +/*! + @method autodetectColumnWidthsForFont: + @abstract Autodetect and return column widths based on contents + @discussion Support autocalculating column widths for the represented data. + This uses the underlying table storage, calculates string widths, + and eventually returns an array of table column widths. + Suitable for calling on background threads, but ensure that the + data storage range in use (currently rows 1-200) won't be altered + while this accesses it. + @param The font to use when calculating widths + @result A dictionary - mapped by column identifier - of the column widths to use +*/ +- (NSDictionary *) autodetectColumnWidthsForFont:(NSFont *)theFont; + +/*! + @method autodetectWidthForColumnDefinition:usingFont:maxRows: + @abstract Autodetect and return column width based on contents + @discussion Support autocalculating column width for the represented data. + This uses the underlying table storage, and the supplied column definition, + iterating through the data and returning a reasonable column width to + display that data. + Suitable for calling on background threads, but ensure that the data + storage range in use won't be altered while being accessed. + @param A column definition for a represented column; the column to use is derived + @param The font to use when calculating widths + @param The maximum number of rows to process when looking at string lengths + @result A reasonable column width to use when displaying data +*/ +/** + * Autodetect the column width for a specified column - derived from the supplied + * column definition, using the stored data and the specified font. + */ +- (NSUInteger)autodetectWidthForColumnDefinition:(NSDictionary *)columnDefinition usingFont:(NSFont *)theFont maxRows:(NSUInteger)rowsToCheck; + @end extern NSInteger MENU_EDIT_COPY; diff --git a/Source/CMCopyTable.m b/Source/CMCopyTable.m index 8ce03586..5eab896d 100644 --- a/Source/CMCopyTable.m +++ b/Source/CMCopyTable.m @@ -446,6 +446,101 @@ NSInteger MENU_EDIT_COPY_AS_SQL = 2003; tableStorage = theTableStorage; } +/** + * Autodetect column widths for a specified font. + */ +- (NSDictionary *) autodetectColumnWidthsForFont:(NSFont *)theFont; +{ + NSMutableDictionary *columnWidths = [NSMutableDictionary dictionaryWithCapacity:[columnDefinitions count]]; + NSUInteger columnWidth; + + for (NSDictionary *columnDefinition in columnDefinitions) { + if ([[NSThread currentThread] isCancelled]) return nil; + + columnWidth = [self autodetectWidthForColumnDefinition:columnDefinition usingFont:theFont maxRows:100]; + [columnWidths setObject:[NSNumber numberWithUnsignedInteger:columnWidth] forKey:[columnDefinition objectForKey:@"datacolumnindex"]]; + } + + return columnWidths; +} + +/** + * Autodetect the column width for a specified column - derived from the supplied + * column definition, using the stored data and the specified font. + */ +- (NSUInteger)autodetectWidthForColumnDefinition:(NSDictionary *)columnDefinition usingFont:(NSFont *)theFont maxRows:(NSUInteger)rowsToCheck +{ + CGFloat columnBaseWidth; + id contentString; + NSUInteger cellWidth, maxCellWidth, i; + NSRange linebreakRange; + double rowStep; + NSUInteger columnIndex = [[columnDefinition objectForKey:@"datacolumnindex"] unsignedIntegerValue]; + NSDictionary *stringAttributes = [NSDictionary dictionaryWithObject:theFont forKey:NSFontAttributeName]; + + // Check the number of rows available to check, sampling every n rows + if ([tableStorage count] < rowsToCheck) { + rowsToCheck = [tableStorage count]; + rowStep = 1; + } else { + rowStep = floor([tableStorage count] / rowsToCheck); + } + + // Set a default padding for this column + columnBaseWidth = 24; + + // Iterate through the data store rows, checking widths + maxCellWidth = 0; + for (i = 0; i < rowsToCheck; i += rowStep) { + + // Retrieve the cell's content + contentString = [tableStorage cellDataAtRow:i column:columnIndex]; + + // Replace NULLs with their placeholder string + if ([contentString isNSNull]) { + contentString = [prefs objectForKey:SPNullValue]; + + } else { + + // Otherwise, ensure the cell is represented as a short string + if ([contentString isKindOfClass:[NSData class]]) { + contentString = [contentString shortStringRepresentationUsingEncoding:[mySQLConnection encoding]]; + } else if ([contentString length] > 500) { + contentString = [contentString substringToIndex:500]; + } + + // If any linebreaks are present, use only the visible part of the string + linebreakRange = [contentString rangeOfCharacterFromSet:[NSCharacterSet newlineCharacterSet]]; + if (linebreakRange.location != NSNotFound) { + contentString = [contentString substringToIndex:linebreakRange.location]; + } + } + + // Calculate the width, using it if it's higher than the current stored width + cellWidth = [contentString sizeWithAttributes:stringAttributes].width; + if (cellWidth > maxCellWidth) maxCellWidth = cellWidth; + if (maxCellWidth > SP_MAX_CELL_WIDTH) { + maxCellWidth = SP_MAX_CELL_WIDTH; + break; + } + } + + // If the column has a foreign key link, expand the width; and also for enums + if ([columnDefinition objectForKey:@"foreignkeyreference"]) { + maxCellWidth += 18; + } else if ([[columnDefinition objectForKey:@"typegrouping"] isEqualToString:@"enum"]) { + maxCellWidth += 8; + } + + // Add the padding + maxCellWidth += columnBaseWidth; + + // If the header width is wider than this expanded width, use it instead + cellWidth = [[columnDefinition objectForKey:@"name"] sizeWithAttributes:[NSDictionary dictionaryWithObject:[NSFont labelFontOfSize:[NSFont smallSystemFontSize]] forKey:NSFontAttributeName]].width; + if (cellWidth + 10 > maxCellWidth) maxCellWidth = cellWidth + 10; + + return maxCellWidth; +} - (void)keyDown:(NSEvent *)theEvent { diff --git a/Source/SPTableContent.h b/Source/SPTableContent.h index 7b87bc32..1899d6a6 100644 --- a/Source/SPTableContent.h +++ b/Source/SPTableContent.h @@ -99,6 +99,9 @@ NSString *filterFieldToRestore, *filterComparisonToRestore, *filterValueToRestore, *firstBetweenValueToRestore, *secondBetweenValueToRestore; NSInteger paginationViewHeight; + + NSTimer *tableLoadTimer; + NSUInteger tableLoadInterfaceUpdateInterval, tableLoadTimerTicksSinceLastUpdate, tableLoadLastRowCount; } // Table loading methods and information @@ -107,6 +110,9 @@ - (void) loadTableValues; - (NSString *) tableFilterString; - (void) updateCountText; +- (void) initTableLoadTimer; +- (void) clearTableLoadTimer; +- (void) tableLoadUpdate:(NSTimer *)theTimer; // Table interface actions - (IBAction) reloadTable:(id)sender; @@ -148,6 +154,7 @@ - (NSString *)fieldListForQuery; - (void)updateNumberOfRows; - (NSInteger)fetchNumberOfRows; +- (void)autosizeColumns; - (BOOL)saveRowOnDeselect; - (void)sortTableTaskWithColumn:(NSTableColumn *)tableColumn; diff --git a/Source/SPTableContent.m b/Source/SPTableContent.m index bf03c66e..26eeb568 100644 --- a/Source/SPTableContent.m +++ b/Source/SPTableContent.m @@ -99,6 +99,8 @@ prefs = [NSUserDefaults standardUserDefaults]; usedQuery = [[NSString alloc] initWithString:@""]; + + tableLoadTimer = nil; // Init default filters for Content Browser contentFilters = nil; @@ -208,7 +210,10 @@ [tableDataInstance getConstraints], @"constraints", nil]; [self performSelectorOnMainThread:@selector(setTableDetails:) withObject:tableDetails waitUntilDone:YES]; - + + // Init copyTable with necessary information for copying selected rows as SQL INSERT + [tableContentView setTableInstance:self withTableData:tableValues withColumns:dataColumns withTableName:selectedTable withConnection:mySQLConnection]; + // Trigger a data refresh [self loadTableValues]; @@ -231,8 +236,6 @@ // Update display if necessary [[tableContentView onMainThread] setNeedsDisplay:YES]; - // Init copyTable with necessary information for copying selected rows as SQL INSERT - [tableContentView setTableInstance:self withTableData:tableValues withColumns:dataColumns withTableName:selectedTable withConnection:mySQLConnection]; // Post the notification that the query is finished [[NSNotificationCenter defaultCenter] postNotificationOnMainThreadWithName:@"SMySQLQueryHasBeenPerformed" object:tableDocumentInstance]; @@ -689,14 +692,13 @@ NSUInteger dataColumnsCount = [dataColumns count]; BOOL *columnBlobStatuses = malloc(dataColumnsCount * sizeof(BOOL)); + // Set up the table updates timer + [[self onMainThread] initTableLoadTimer]; + // Set the column count on the data store [tableValues setColumnCount:dataColumnsCount]; CGFloat relativeTargetRowCount = 100.0/targetRowCount; - NSUInteger nextTableDisplayBoundary = 50; - BOOL tableViewRedrawn = NO; - - NSUInteger rowsProcessed = 0; NSAutoreleasePool *dataLoadingPool; NSProgressIndicator *dataLoadingIndicator = [tableDocumentInstance valueForKey:@"queryProgressBar"]; @@ -711,11 +713,12 @@ dataLoadingPool = [[NSAutoreleasePool alloc] init]; // Loop through the result rows as they become available + tableRowsCount = 0; while (tempRow = [theResult fetchNextRowAsArray]) { pthread_mutex_lock(&tableValuesLock); - if (rowsProcessed < previousTableRowsCount) { - SPDataStorageReplaceRow(tableValues, rowsProcessed, tempRow); + if (tableRowsCount < previousTableRowsCount) { + SPDataStorageReplaceRow(tableValues, tableRowsCount, tempRow); } else { SPDataStorageAddRow(tableValues, tempRow); } @@ -724,42 +727,36 @@ if ( prefsLoadBlobsAsNeeded ) { for ( i = 0 ; i < dataColumnsCount ; i++ ) { if (columnBlobStatuses[i]) { - SPDataStorageReplaceObjectAtRowAndColumn(tableValues, rowsProcessed, i, [SPNotLoaded notLoaded]); + SPDataStorageReplaceObjectAtRowAndColumn(tableValues, tableRowsCount, i, [SPNotLoaded notLoaded]); } } } - rowsProcessed++; + tableRowsCount++; pthread_mutex_unlock(&tableValuesLock); // Update the task interface as necessary if (!isFiltered) { - if (rowsProcessed < targetRowCount) { - [tableDocumentInstance setTaskPercentage:(rowsProcessed*relativeTargetRowCount)]; - } else if (rowsProcessed == targetRowCount) { + if (tableRowsCount < targetRowCount) { + [tableDocumentInstance setTaskPercentage:(tableRowsCount*relativeTargetRowCount)]; + } else if (tableRowsCount == targetRowCount) { [tableDocumentInstance setTaskPercentage:100.0]; [[tableDocumentInstance onMainThread] setTaskProgressToIndeterminateAfterDelay:YES]; } } - // Update the table view with new results every now and then - if (rowsProcessed > nextTableDisplayBoundary) { - if (rowsProcessed > tableRowsCount) tableRowsCount = rowsProcessed; - [[tableContentView onMainThread] noteNumberOfRowsChanged]; - if (!tableViewRedrawn) { - [[tableContentView onMainThread] setNeedsDisplay:YES]; - tableViewRedrawn = YES; - } - nextTableDisplayBoundary *= 2; - } - // Drain and reset the autorelease pool every ~1024 rows - if (!(rowsProcessed % 1024)) { + if (!(tableRowsCount % 1024)) { [dataLoadingPool drain]; dataLoadingPool = [[NSAutoreleasePool alloc] init]; } } - tableRowsCount = rowsProcessed; + + // Clean up the interface update timer + [[self onMainThread] clearTableLoadTimer]; + + // If the final column autoresize wasn't performed, perform it + if (tableLoadLastRowCount < 200) [[self onMainThread] autosizeColumns]; // If the reloaded table is shorter than the previous table, remove the extra values from the storage if (tableRowsCount < [tableValues count]) { @@ -1015,6 +1012,77 @@ [[countText onMainThread] setStringValue:countString]; } +/** + * Set up the table loading interface update timer. + * This should be called on the main thread. + */ +- (void) initTableLoadTimer +{ + if (tableLoadTimer) [self clearTableLoadTimer]; + tableLoadInterfaceUpdateInterval = 1; + tableLoadLastRowCount = 0; + tableLoadTimerTicksSinceLastUpdate = 0; + + tableLoadTimer = [[NSTimer scheduledTimerWithTimeInterval:0.02 target:self selector:@selector(tableLoadUpdate:) userInfo:nil repeats:YES] retain]; +} + +/** + * Invalidate and release the table loading interface update timer. + * This should be called on the main thread. + */ +- (void) clearTableLoadTimer +{ + if (tableLoadTimer) { + [tableLoadTimer invalidate]; + [tableLoadTimer release]; + tableLoadTimer = nil; + } +} + +/** + * Perform table interface updates when loading tables, based on timer + * ticks. As data becomes available, the table should be redrawn to + * show new rows - quickly at the start of the table, and then slightly + * slower after some time to avoid needless updates. + */ +- (void) tableLoadUpdate:(NSTimer *)theTimer +{ + if (tableLoadTimerTicksSinceLastUpdate < tableLoadInterfaceUpdateInterval) { + tableLoadTimerTicksSinceLastUpdate++; + return; + } + + // Check whether a table update is required, based on whether new rows are + // available to display. + if (tableRowsCount == tableLoadLastRowCount) { + return; + } + + // Update the table display + [tableContentView noteNumberOfRowsChanged]; + if (!tableLoadLastRowCount) [tableContentView setNeedsDisplay:YES]; + + // Update column widths in two cases: on very first rows displayed, and once + // more than 200 rows are present. + if (tableLoadInterfaceUpdateInterval || (tableRowsCount >= 200 && tableLoadLastRowCount < 200)) { + [self autosizeColumns]; + } + + tableLoadLastRowCount = tableRowsCount; + + // Determine whether to decrease the update frequency + switch (tableLoadInterfaceUpdateInterval) { + case 1: + tableLoadInterfaceUpdateInterval = 10; + break; + case 10: + tableLoadInterfaceUpdateInterval = 25; + break; + } + tableLoadTimerTicksSinceLastUpdate = 0; +} + + #pragma mark - #pragma mark Table interface actions @@ -2749,6 +2817,27 @@ return [[[[mySQLConnection queryString:[NSString stringWithFormat:@"SELECT COUNT(1) FROM %@", [selectedTable backtickQuotedString]]] fetchRowAsArray] objectAtIndex:0] integerValue]; } +/** + * Autosize all columns based on their content. + * Should be called on the main thread. + */ +- (void)autosizeColumns +{ + NSDictionary *columnWidths = [tableContentView autodetectColumnWidthsForFont:[NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:SPGlobalResultTableFont]]]; + [tableContentView setDelegate:nil]; + for (NSDictionary *columnDefinition in dataColumns) { + + // Skip columns with saved widths + if ([[[[prefs objectForKey:SPTableColumnWidths] objectForKey:[NSString stringWithFormat:@"%@@%@", [tableDocumentInstance database], [tableDocumentInstance host]]] objectForKey:[tablesListInstance tableName]] objectForKey:[columnDefinition objectForKey:@"name"]]) continue; + + // Otherwise set the column width + NSTableColumn *aTableColumn = [tableContentView tableColumnWithIdentifier:[columnDefinition objectForKey:@"datacolumnindex"]]; + NSUInteger targetWidth = [[columnWidths objectForKey:[columnDefinition objectForKey:@"datacolumnindex"]] unsignedIntegerValue]; + [aTableColumn setWidth:targetWidth]; + } + [tableContentView setDelegate:self]; +} + #pragma mark - #pragma mark TableView delegate methods @@ -3157,6 +3246,44 @@ return tableRowsSelectable; } +/** + * Resize a column when it's double-clicked. (10.6+) + */ +- (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 usingFont:[NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:SPGlobalResultTableFont]] maxRows:500]; + + // 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]; + } + + // 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; +} + #pragma mark - #pragma mark SplitView delegate methods @@ -3406,7 +3533,9 @@ - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; + [NSObject cancelPreviousPerformRequestsWithTarget:tableContentView]; + [self clearTableLoadTimer]; [tableValues release]; pthread_mutex_destroy(&tableValuesLock); [dataColumns release]; |