From 0020be837355c4eed3e874c32ca247ad9b7abf22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Qu=E1=BB=91c=20=C4=90=E1=BA=A1t?= Date: Mon, 30 Mar 2026 15:07:38 +0700 Subject: [PATCH 1/2] feat: add column auto-fit (double-click divider + context menu) - Implement tableView(_:sizeToFitWidthOfColumn:) delegate for native double-click-on-column-divider auto-fit (matches Finder/Numbers) - Add "Size to Fit" to column header right-click menu - Add "Size All Columns to Fit" to column header right-click menu - All three reuse existing calculateOptimalColumnWidth() logic - Works in both table data tab and SQL query results tab --- CHANGELOG.md | 1 + .../Extensions/DataGridView+Sort.swift | 65 +++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8723ffa12..672ea3a8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Visual Create Table UI with column, index, and foreign key editors (sidebar → "Create New Table...") - Real-time SQL preview with syntax highlighting for CREATE TABLE DDL - Multi-database CREATE TABLE support: MySQL, PostgreSQL, SQLite, SQL Server, ClickHouse, DuckDB +- Auto-fit column width: double-click column divider, right-click header → "Size to Fit" / "Size All Columns to Fit" ### Fixed diff --git a/TablePro/Views/Results/Extensions/DataGridView+Sort.swift b/TablePro/Views/Results/Extensions/DataGridView+Sort.swift index 11da27c7b..88e126768 100644 --- a/TablePro/Views/Results/Extensions/DataGridView+Sort.swift +++ b/TablePro/Views/Results/Extensions/DataGridView+Sort.swift @@ -24,6 +24,26 @@ extension TableViewCoordinator { onSort?(columnIndex, sortDescriptor.ascending, isMultiSort) } + // MARK: - Double-Click Column Divider Auto-Fit + + func tableView(_ tableView: NSTableView, sizeToFitWidthOfColumn columnIndex: Int) -> CGFloat { + let column = tableView.tableColumns[columnIndex] + guard column.identifier.rawValue != "__rowNumber__" else { + return column.width + } + guard let dataColumnIndex = DataGridView.columnIndex(from: column.identifier) else { + return column.width + } + + let width = cellFactory.calculateOptimalColumnWidth( + for: dataColumnIndex < rowProvider.columns.count ? rowProvider.columns[dataColumnIndex] : column.title, + columnIndex: dataColumnIndex, + rowProvider: rowProvider + ) + hasUserResizedColumns = true + return width + } + // MARK: - NSMenuDelegate (Header Context Menu) func menuNeedsUpdate(_ menu: NSMenu) { @@ -63,6 +83,17 @@ extension TableViewCoordinator { menu.addItem(NSMenuItem.separator()) + let sizeToFitItem = NSMenuItem(title: String(localized: "Size to Fit"), action: #selector(sizeColumnToFit(_:)), keyEquivalent: "") + sizeToFitItem.representedObject = columnIndex + sizeToFitItem.target = self + menu.addItem(sizeToFitItem) + + let sizeAllItem = NSMenuItem(title: String(localized: "Size All Columns to Fit"), action: #selector(sizeAllColumnsToFit(_:)), keyEquivalent: "") + sizeAllItem.target = self + menu.addItem(sizeAllItem) + + menu.addItem(NSMenuItem.separator()) + let hideItem = NSMenuItem(title: String(localized: "Hide Column"), action: #selector(hideColumn(_:)), keyEquivalent: "") hideItem.representedObject = baseName hideItem.target = self @@ -83,4 +114,38 @@ extension TableViewCoordinator { guard let columnName = sender.representedObject as? String else { return } onHideColumn?(columnName) } + + @objc func sizeColumnToFit(_ sender: NSMenuItem) { + guard let tableView, + let columnIndex = sender.representedObject as? Int, + columnIndex >= 0 && columnIndex < tableView.tableColumns.count else { return } + + let column = tableView.tableColumns[columnIndex] + guard let dataColumnIndex = DataGridView.columnIndex(from: column.identifier) else { return } + + let width = cellFactory.calculateOptimalColumnWidth( + for: dataColumnIndex < rowProvider.columns.count ? rowProvider.columns[dataColumnIndex] : column.title, + columnIndex: dataColumnIndex, + rowProvider: rowProvider + ) + column.width = width + hasUserResizedColumns = true + } + + @objc func sizeAllColumnsToFit(_ sender: NSMenuItem) { + guard let tableView else { return } + + for column in tableView.tableColumns { + guard column.identifier.rawValue != "__rowNumber__", + let dataColumnIndex = DataGridView.columnIndex(from: column.identifier) else { continue } + + let width = cellFactory.calculateOptimalColumnWidth( + for: dataColumnIndex < rowProvider.columns.count ? rowProvider.columns[dataColumnIndex] : column.title, + columnIndex: dataColumnIndex, + rowProvider: rowProvider + ) + column.width = width + } + hasUserResizedColumns = true + } } From e0524062491ba0ca30d2bd6a632ecb08902c0af8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Qu=E1=BB=91c=20=C4=90=E1=BA=A1t?= Date: Mon, 30 Mar 2026 15:16:34 +0700 Subject: [PATCH 2/2] fix: use uncapped width calculation for user-initiated auto-fit --- .../Views/Results/DataGridCellFactory.swift | 27 +++++++++++++++++++ .../Extensions/DataGridView+Sort.swift | 4 +-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/TablePro/Views/Results/DataGridCellFactory.swift b/TablePro/Views/Results/DataGridCellFactory.swift index f67f82eb3..6c402e661 100644 --- a/TablePro/Views/Results/DataGridCellFactory.swift +++ b/TablePro/Views/Results/DataGridCellFactory.swift @@ -403,6 +403,33 @@ final class DataGridCellFactory { return min(max(maxWidth, Self.minColumnWidth), Self.maxColumnWidth) } + + /// Calculate column width to fit content without max-width or max-chars caps. + /// Used for user-initiated "Size to Fit" (double-click divider, context menu). + func calculateFitToContentWidth( + for columnName: String, + columnIndex: Int, + rowProvider: InMemoryRowProvider + ) -> CGFloat { + let headerCharCount = (columnName as NSString).length + var maxWidth = CGFloat(headerCharCount) * ThemeEngine.shared.dataGridFonts.monoCharWidth * 0.75 + 48 + + let totalRows = rowProvider.totalRowCount + let columnCount = rowProvider.columns.count + let effectiveSampleCount = columnCount > 50 ? 10 : Self.sampleRowCount + let step = max(1, totalRows / effectiveSampleCount) + let charWidth = ThemeEngine.shared.dataGridFonts.monoCharWidth + + for i in stride(from: 0, to: totalRows, by: step) { + guard let value = rowProvider.value(atRow: i, column: columnIndex) else { continue } + + let charCount = (value as NSString).length + let cellWidth = CGFloat(charCount) * charWidth + 16 + maxWidth = max(maxWidth, cellWidth) + } + + return max(maxWidth, Self.minColumnWidth) + } } // MARK: - NSFont Extension diff --git a/TablePro/Views/Results/Extensions/DataGridView+Sort.swift b/TablePro/Views/Results/Extensions/DataGridView+Sort.swift index 88e126768..7bf4235e5 100644 --- a/TablePro/Views/Results/Extensions/DataGridView+Sort.swift +++ b/TablePro/Views/Results/Extensions/DataGridView+Sort.swift @@ -35,7 +35,7 @@ extension TableViewCoordinator { return column.width } - let width = cellFactory.calculateOptimalColumnWidth( + let width = cellFactory.calculateFitToContentWidth( for: dataColumnIndex < rowProvider.columns.count ? rowProvider.columns[dataColumnIndex] : column.title, columnIndex: dataColumnIndex, rowProvider: rowProvider @@ -123,7 +123,7 @@ extension TableViewCoordinator { let column = tableView.tableColumns[columnIndex] guard let dataColumnIndex = DataGridView.columnIndex(from: column.identifier) else { return } - let width = cellFactory.calculateOptimalColumnWidth( + let width = cellFactory.calculateFitToContentWidth( for: dataColumnIndex < rowProvider.columns.count ? rowProvider.columns[dataColumnIndex] : column.title, columnIndex: dataColumnIndex, rowProvider: rowProvider