From e5aa4302f8655a08d7fa7542893db009a6920689 Mon Sep 17 00:00:00 2001 From: rowanbeentje Date: Thu, 12 Aug 2010 01:15:44 +0000 Subject: Implement column autosizing for the Content View: - Add automatic column sizing (for columns without saved widths) as part of the value loading process - Rework table updates to be timer based, for time-based and more regular updates. This improves speed and allows tables to update more consistently. This results in overall smoother table loads, faster table loads, and autosizing columns. This partially implements Issues #271 and #272. Column autosizing will likely be tweaked, and this will all also be extended to Custom Query views in a future patch. --- Source/CMCopyTable.h | 36 ++++++++++ Source/CMCopyTable.m | 95 +++++++++++++++++++++++++ Source/SPTableContent.h | 7 ++ 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 #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]; -- cgit v1.2.3