I am trying to implement a NSTableView
that looks similar to the Xcode IB object selector (bottom right panel). As shown below when a row is selected a full width horizontal line is draw above and below the selected row.
I have successfully created a subclass of NSTableRowView
and have used the isNextRowSelected property to determine whether to draw a full width separator and this almost works.
The issue is the row above the selected row is not being redrawn unless you happened to select a row and then select the row below it immediately afterwards.
How can I efficiently get the NSTableView to redraw the row above the selected row every time ?
Here is my implementation when a single row is selected
And another if a the row immediately below is now selected - which is what I want.
/// This subclass draws a partial line as the separator for unselected rows and a full width line above and below for selected rows
/// | ROW |
/// | ---------- | unselected separator
/// |------------| selected separator on row above selected row
/// | ROW |
/// |------------| selected separator
///
/// Issue: Row above selected row does not get redrawn when selected row is deselected
class OSTableRowView: NSTableRowView {
let separatorColor = NSColor(calibratedWhite: 0.35, alpha: 1)
let selectedSeparatorColor = NSColor(calibratedWhite: 0.15, alpha: 1)
let selectedFillColor = NSColor(calibratedWhite: 0.82, alpha: 1)
override func drawSeparator(in dirtyRect: NSRect) {
let yBottom = self.bounds.height
let gap: CGFloat = 4.0
let xLeft: CGFloat = 0.0
let xRight = xLeft + self.bounds.width
let lines = NSBezierPath()
/// Draw a full width separator if the item is selected or if the next row is selected
if self.isSelected || self.isNextRowSelected {
selectedSeparatorColor.setStroke()
lines.move(to: NSPoint(x: xLeft, y: yBottom))
lines.line(to: NSPoint(x: xRight, y: yBottom))
lines.lineWidth = 1.0
} else {
separatorColor.setStroke()
lines.move(to: NSPoint(x: xLeft+gap, y: yBottom))
lines.line(to: NSPoint(x: xRight-gap, y: yBottom))
lines.lineWidth = 0.0
}
lines.stroke()
}
override func drawSelection(in dirtyRect: NSRect) {
if self.selectionHighlightStyle != .none {
let selectionRect = self.bounds
selectedSeparatorColor.setStroke()
selectedFillColor.setFill()
selectionRect.fill()
}
}
}
After reading a few other posts I tried adding code to cause the preceding row to be redraw. This appears to have not effect.
func selectionShouldChange(in tableView: NSTableView) -> Bool {
let selection = tableView.selectedRow
if selection > 0 {
tableView.setNeedsDisplay(tableView.rect(ofRow: selection-1))
tableView.displayIfNeeded()
}
return true
}
And nor does this.
func tableViewSelectionDidChange(_ notification: Notification) {
guard let tableView = self.sidebarOutlineView else {
return
}
let row = tableView.selectedRow
if row > 0 {
tableView.setNeedsDisplay(tableView.rect(ofRow: row-1))
print("row-1 update rect: \(tableView.rect(ofRow: row-1))")
}
}
Seems odd that neither of these trigger redrawing of the row - am I missing something here!
EDIT: OK I found something that seems to work OKish - there is still a visible lag in the redrawing of the row above the deselected row which is not present in the XCode tableView.
var lastSelectedRow = -1 {
didSet {
guard let tableView = self.sidebarOutlineView else {
return
}
if oldValue != lastSelectedRow {
if oldValue > 0 {
if let view = tableView.rowView(atRow: oldValue-1, makeIfNecessary: false) {
view.needsDisplay = true
}
}
if lastSelectedRow > 0 {
if let view = tableView.rowView(atRow: lastSelectedRow-1, makeIfNecessary: false) {
view.needsDisplay = true
}
}
}
}
}
and then simply set the value of the variable lastSelectedRow = tableView.selectedRow
in the tableViewSelectionDidChange(:)
method.
I think perhaps the tableView needs to be subclassed to make sure that both rows are redrawn in the same update cycle.