add resizable columns and rows to markdown table view
This commit is contained in:
parent
8f4f1fddc7
commit
96262ca9ef
|
|
@ -321,81 +321,129 @@ class MarkdownTableView: NSView, NSTextFieldDelegate {
|
|||
var onTableChanged: ((ParsedTable) -> Void)?
|
||||
|
||||
private var cellFields: [[NSTextField]] = []
|
||||
private let cellHeight: CGFloat = 26
|
||||
private let cellPadding: CGFloat = 4
|
||||
private let headerHeight: CGFloat = 28
|
||||
private var columnWidths: [CGFloat] = []
|
||||
private var rowHeights: [CGFloat] = []
|
||||
private var tableWidth: CGFloat = 0
|
||||
|
||||
private let minColWidth: CGFloat = 40
|
||||
private let minRowHeight: CGFloat = 24
|
||||
private let defaultHeaderHeight: CGFloat = 28
|
||||
private let defaultCellHeight: CGFloat = 26
|
||||
private let dividerHitZone: CGFloat = 6
|
||||
|
||||
private enum DragMode { case none, column(Int), row(Int) }
|
||||
private var dragMode: DragMode = .none
|
||||
private var dragStartPoint: NSPoint = .zero
|
||||
private var dragStartSize: CGFloat = 0
|
||||
|
||||
init(table: ParsedTable, width: CGFloat) {
|
||||
self.table = table
|
||||
self.tableWidth = width
|
||||
super.init(frame: .zero)
|
||||
wantsLayer = true
|
||||
layer?.backgroundColor = Theme.current.base.cgColor
|
||||
layer?.cornerRadius = 4
|
||||
layer?.borderWidth = 1
|
||||
layer?.borderColor = Theme.current.surface2.cgColor
|
||||
buildGrid(width: width)
|
||||
initSizes(width: width)
|
||||
buildGrid()
|
||||
setupTrackingArea()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
private func buildGrid(width: CGFloat) {
|
||||
private func initSizes(width: CGFloat) {
|
||||
let colCount = table.headers.count
|
||||
guard colCount > 0 else { return }
|
||||
let available = width - CGFloat(colCount + 1)
|
||||
let colW = available / CGFloat(colCount)
|
||||
columnWidths = Array(repeating: max(colW, minColWidth), count: colCount)
|
||||
|
||||
rowHeights = []
|
||||
rowHeights.append(defaultHeaderHeight)
|
||||
for _ in 0..<table.rows.count {
|
||||
rowHeights.append(defaultCellHeight)
|
||||
}
|
||||
}
|
||||
|
||||
private var totalHeight: CGFloat {
|
||||
rowHeights.reduce(0, +)
|
||||
}
|
||||
|
||||
private func columnX(for col: Int) -> CGFloat {
|
||||
var x: CGFloat = 1
|
||||
for i in 0..<col {
|
||||
x += columnWidths[i] + 1
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
private func rowY(for row: Int) -> CGFloat {
|
||||
let th = totalHeight
|
||||
var y = th
|
||||
for i in 0...row {
|
||||
y -= rowHeights[i]
|
||||
}
|
||||
return y
|
||||
}
|
||||
|
||||
private func buildGrid() {
|
||||
subviews.forEach { $0.removeFromSuperview() }
|
||||
cellFields = []
|
||||
|
||||
let colCount = table.headers.count
|
||||
guard colCount > 0 else { return }
|
||||
let colWidth = (width - CGFloat(colCount + 1)) / CGFloat(colCount)
|
||||
let totalRows = 1 + table.rows.count
|
||||
let totalHeight = headerHeight + CGFloat(table.rows.count) * cellHeight
|
||||
let th = totalHeight
|
||||
let totalWidth = columnX(for: colCount) + 1
|
||||
|
||||
frame.size = NSSize(width: width, height: totalHeight)
|
||||
frame.size = NSSize(width: totalWidth, height: th)
|
||||
|
||||
// Header row
|
||||
let headerBg = NSView(frame: NSRect(x: 0, y: totalHeight - headerHeight, width: width, height: headerHeight))
|
||||
let headerBg = NSView(frame: NSRect(x: 0, y: th - rowHeights[0], width: totalWidth, height: rowHeights[0]))
|
||||
headerBg.wantsLayer = true
|
||||
headerBg.layer?.backgroundColor = Theme.current.surface0.cgColor
|
||||
addSubview(headerBg)
|
||||
|
||||
var headerFields: [NSTextField] = []
|
||||
for (col, header) in table.headers.enumerated() {
|
||||
let x = CGFloat(col) * colWidth + CGFloat(col + 1)
|
||||
let field = makeCell(text: header, frame: NSRect(x: x, y: totalHeight - headerHeight + 2, width: colWidth, height: headerHeight - 4), isHeader: true, row: -1, col: col)
|
||||
let x = columnX(for: col)
|
||||
let h = rowHeights[0]
|
||||
let field = makeCell(text: header, frame: NSRect(x: x, y: th - h + 2, width: columnWidths[col], height: h - 4), isHeader: true, row: -1, col: col)
|
||||
addSubview(field)
|
||||
headerFields.append(field)
|
||||
}
|
||||
cellFields.append(headerFields)
|
||||
|
||||
// Data rows
|
||||
for (rowIdx, row) in table.rows.enumerated() {
|
||||
var rowFields: [NSTextField] = []
|
||||
let y = totalHeight - headerHeight - CGFloat(rowIdx + 1) * cellHeight
|
||||
let y = rowY(for: rowIdx + 1)
|
||||
let h = rowHeights[rowIdx + 1]
|
||||
for (col, cell) in row.enumerated() where col < colCount {
|
||||
let x = CGFloat(col) * colWidth + CGFloat(col + 1)
|
||||
let field = makeCell(text: cell, frame: NSRect(x: x, y: y + 2, width: colWidth, height: cellHeight - 4), isHeader: false, row: rowIdx, col: col)
|
||||
let x = columnX(for: col)
|
||||
let field = makeCell(text: cell, frame: NSRect(x: x, y: y + 2, width: columnWidths[col], height: h - 4), isHeader: false, row: rowIdx, col: col)
|
||||
addSubview(field)
|
||||
rowFields.append(field)
|
||||
}
|
||||
while rowFields.count < colCount {
|
||||
let col = rowFields.count
|
||||
let x = CGFloat(col) * colWidth + CGFloat(col + 1)
|
||||
let field = makeCell(text: "", frame: NSRect(x: x, y: y + 2, width: colWidth, height: cellHeight - 4), isHeader: false, row: rowIdx, col: col)
|
||||
let x = columnX(for: col)
|
||||
let field = makeCell(text: "", frame: NSRect(x: x, y: y + 2, width: columnWidths[col], height: h - 4), isHeader: false, row: rowIdx, col: col)
|
||||
addSubview(field)
|
||||
rowFields.append(field)
|
||||
}
|
||||
cellFields.append(rowFields)
|
||||
}
|
||||
|
||||
// Grid lines
|
||||
let totalRows = 1 + table.rows.count
|
||||
for i in 1..<colCount {
|
||||
let x = CGFloat(i) * (colWidth + 1)
|
||||
let line = NSView(frame: NSRect(x: x, y: 0, width: 1, height: totalHeight))
|
||||
let x = columnX(for: i) - 1
|
||||
let line = NSView(frame: NSRect(x: x, y: 0, width: 1, height: th))
|
||||
line.wantsLayer = true
|
||||
line.layer?.backgroundColor = Theme.current.surface2.cgColor
|
||||
addSubview(line)
|
||||
}
|
||||
for i in 0..<totalRows {
|
||||
let y = totalHeight - headerHeight - CGFloat(i) * cellHeight
|
||||
let line = NSView(frame: NSRect(x: 0, y: y, width: width, height: 1))
|
||||
let lineY = (i == 0) ? th - rowHeights[0] : rowY(for: i)
|
||||
let line = NSView(frame: NSRect(x: 0, y: lineY, width: totalWidth, height: 1))
|
||||
line.wantsLayer = true
|
||||
line.layer?.backgroundColor = Theme.current.surface2.cgColor
|
||||
addSubview(line)
|
||||
|
|
@ -426,6 +474,110 @@ class MarkdownTableView: NSView, NSTextFieldDelegate {
|
|||
return field
|
||||
}
|
||||
|
||||
// MARK: - Resize hit detection
|
||||
|
||||
private func columnDivider(at point: NSPoint) -> Int? {
|
||||
let colCount = table.headers.count
|
||||
for i in 1..<colCount {
|
||||
let divX = columnX(for: i) - 1
|
||||
if abs(point.x - divX) <= dividerHitZone {
|
||||
return i - 1
|
||||
}
|
||||
}
|
||||
let lastX = columnX(for: colCount)
|
||||
if abs(point.x - lastX) <= dividerHitZone {
|
||||
return colCount - 1
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func rowDivider(at point: NSPoint) -> Int? {
|
||||
let totalRows = 1 + table.rows.count
|
||||
for i in 0..<totalRows {
|
||||
let divY: CGFloat
|
||||
if i == 0 {
|
||||
divY = totalHeight - rowHeights[0]
|
||||
} else {
|
||||
divY = rowY(for: i)
|
||||
}
|
||||
if abs(point.y - divY) <= dividerHitZone {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MARK: - Tracking area
|
||||
|
||||
private func setupTrackingArea() {
|
||||
for area in trackingAreas { removeTrackingArea(area) }
|
||||
let area = NSTrackingArea(
|
||||
rect: bounds,
|
||||
options: [.mouseMoved, .mouseEnteredAndExited, .activeInKeyWindow, .inVisibleRect],
|
||||
owner: self,
|
||||
userInfo: nil
|
||||
)
|
||||
addTrackingArea(area)
|
||||
}
|
||||
|
||||
override func mouseMoved(with event: NSEvent) {
|
||||
let pt = convert(event.locationInWindow, from: nil)
|
||||
if columnDivider(at: pt) != nil {
|
||||
NSCursor.resizeLeftRight.set()
|
||||
} else if rowDivider(at: pt) != nil {
|
||||
NSCursor.resizeUpDown.set()
|
||||
} else {
|
||||
NSCursor.arrow.set()
|
||||
}
|
||||
}
|
||||
|
||||
override func mouseExited(with event: NSEvent) {
|
||||
NSCursor.arrow.set()
|
||||
}
|
||||
|
||||
override func mouseDown(with event: NSEvent) {
|
||||
let pt = convert(event.locationInWindow, from: nil)
|
||||
if let col = columnDivider(at: pt) {
|
||||
dragMode = .column(col)
|
||||
dragStartPoint = pt
|
||||
dragStartSize = columnWidths[col]
|
||||
return
|
||||
}
|
||||
if let row = rowDivider(at: pt) {
|
||||
dragMode = .row(row)
|
||||
dragStartPoint = pt
|
||||
dragStartSize = rowHeights[row]
|
||||
return
|
||||
}
|
||||
dragMode = .none
|
||||
super.mouseDown(with: event)
|
||||
}
|
||||
|
||||
override func mouseDragged(with event: NSEvent) {
|
||||
let pt = convert(event.locationInWindow, from: nil)
|
||||
switch dragMode {
|
||||
case .column(let col):
|
||||
let delta = pt.x - dragStartPoint.x
|
||||
columnWidths[col] = max(minColWidth, dragStartSize + delta)
|
||||
buildGrid()
|
||||
case .row(let row):
|
||||
let delta = dragStartPoint.y - pt.y
|
||||
rowHeights[row] = max(minRowHeight, dragStartSize + delta)
|
||||
buildGrid()
|
||||
case .none:
|
||||
super.mouseDragged(with: event)
|
||||
}
|
||||
}
|
||||
|
||||
override func mouseUp(with event: NSEvent) {
|
||||
if case .none = dragMode {
|
||||
super.mouseUp(with: event)
|
||||
}
|
||||
dragMode = .none
|
||||
}
|
||||
|
||||
// MARK: - Cell editing
|
||||
|
||||
func controlTextDidEndEditing(_ obj: Notification) {
|
||||
guard let field = obj.object as? NSTextField else { return }
|
||||
let tag = field.tag
|
||||
|
|
@ -464,8 +616,8 @@ class MarkdownTableView: NSView, NSTextFieldDelegate {
|
|||
table.rows.append(Array(repeating: "", count: table.headers.count))
|
||||
onTableChanged?(table)
|
||||
|
||||
let width = frame.width
|
||||
buildGrid(width: width)
|
||||
rowHeights.append(defaultCellHeight)
|
||||
buildGrid()
|
||||
needsLayout = true
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue