我尝试使用带有行号的NSTextView。当TextView的选择在此行时,行号的背景应该有另一种颜色。这个代码很好用(我在https://github.com/daizhirui/macOS-Development/tree/master/LineNumberView/line-number-text-view-master找到它并添加了背景代码):
/// Defines the width of the gutter view.
fileprivate let GUTTER_WIDTH: CGFloat = 40
/// Adds line numbers to a NSTextField.
class LineNumberGutter: NSRulerView {
/// Holds the background color.
internal var backgroundColor: NSColor {
didSet {
self.needsDisplay = true
}
}
/// Holds the text color.
internal var foregroundColor: NSColor {
didSet {
self.needsDisplay = true
}
}
/// Initializes a LineNumberGutter with the given attributes.
///
/// - parameter textView: NSTextView to attach the LineNumberGutter to.
/// - parameter foregroundColor: Defines the foreground color.
/// - parameter backgroundColor: Defines the background color.
///
/// - returns: An initialized LineNumberGutter object.
init(withTextView textView: NSTextView, foregroundColor: NSColor, backgroundColor: NSColor) {
// Set the color preferences.
self.backgroundColor = backgroundColor
self.foregroundColor = foregroundColor
// Make sure everything's set up properly before initializing properties.
super.init(scrollView: textView.enclosingScrollView, orientation: .verticalRuler)
// Set the rulers clientView to the supplied textview.
self.clientView = textView
// Define the ruler's width.
self.ruleThickness = GUTTER_WIDTH
}
/// Initializes a default LineNumberGutter, attached to the given textView.
/// Default foreground color: hsla(0, 0, 0, 0.55);
/// Default background color: hsla(0, 0, 0.95, 1);
///
/// - parameter textView: NSTextView to attach the LineNumberGutter to.
///
/// - returns: An initialized LineNumberGutter object.
convenience init(withTextView textView: NSTextView) {
let fg = NSColor(calibratedHue: 0, saturation: 0, brightness: 0, alpha: 1)
let bg = NSColor(calibratedHue: 0, saturation: 0, brightness: 0, alpha: 1)
// Call the designated initializer.
self.init(withTextView: textView, foregroundColor: fg, backgroundColor: bg)
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/// Draws the line numbers.
///
/// - parameter rect: NSRect to draw the gutter view in.
override func drawHashMarksAndLabels(in rect: NSRect) {
// Set the current background color...
self.backgroundColor.setFill()
// ...and fill the given rect.
rect.fill()
// Unwrap the clientView, the layoutManager and the textContainer, since we'll
// them sooner or later.
guard let textView = self.clientView as? NSTextView, let layoutManager = textView.layoutManager, let textContainer = textView.textContainer else { return }
let content = textView.string
//I added the following lines
let nsString = content as NSString
let selectedRange = textView.selectedRange()
var newlineCountBeforeSelection:Int?
if selectedRange.location != NSNotFound {
let textBeforeSelection = nsString.substring(to: selectedRange.location)
newlineCountBeforeSelection = textBeforeSelection.countOccurrences(ofCharacter: "\n")
}
// Get the range of the currently visible glyphs.
let visibleGlyphsRange = layoutManager.glyphRange(forBoundingRect: textView.visibleRect, in: textContainer)
// Check how many lines are out of the current bounding rect.
var lineNumber = 1
do {
// Define a regular expression to find line breaks.
let newlineRegex = try NSRegularExpression(pattern: "\n", options: [])
// Check how many lines are out of view; From the glyph at index 0
// to the first glyph in the visible rect.
lineNumber += newlineRegex.numberOfMatches(in: content, options: [], range: NSMakeRange(0, visibleGlyphsRange.location))
} catch {
return
}
// Get the index of the first glyph in the visible rect, as starting point...
var firstGlyphOfLineIndex = visibleGlyphsRange.location
// ...then loop through all visible glyphs, line by line.
while firstGlyphOfLineIndex < NSMaxRange(visibleGlyphsRange) {
// Get the character range of the line we're currently in.
let charRangeOfLine = (content as NSString).lineRange(for: NSRange(location: layoutManager.characterIndexForGlyph(at: firstGlyphOfLineIndex), length: 0))
// Get the glyph range of the line we're currently in.
let glyphRangeOfLine = layoutManager.glyphRange(forCharacterRange: charRangeOfLine, actualCharacterRange: nil)
var firstGlyphOfRowIndex = firstGlyphOfLineIndex
var lineWrapCount = 0
// Loop through all rows (soft wraps) of the current line.
while firstGlyphOfRowIndex < NSMaxRange(glyphRangeOfLine) {
// The effective range of glyphs within the current line.
var effectiveRange = NSRange(location: 0, length: 0)
// Get the rect for the current line fragment.
let lineRect = layoutManager.lineFragmentRect(forGlyphAt: firstGlyphOfRowIndex, effectiveRange: &effectiveRange, withoutAdditionalLayout: true)
// Draw the current line number;
// When lineWrapCount > 0 the current line spans multiple rows.
if lineWrapCount == 0 {
//This lines draw the background
if let newlineCountBeforeSelection = newlineCountBeforeSelection, (lineNumber - 1) == newlineCountBeforeSelection {
let rect = NSRect(x: 0, y: lineRect.minY, width: GUTTER_WIDTH, height: lineRect.height)
NSColor.selectedBackground.setFill()
rect.fill()
}
self.drawLineNumber(num: lineNumber, atYPosition: lineRect.minY)
} else {
break
}
// Move to the next row.
firstGlyphOfRowIndex = NSMaxRange(effectiveRange)
lineWrapCount += 1
}
// Move to the next line.
firstGlyphOfLineIndex = NSMaxRange(glyphRangeOfLine)
lineNumber += 1
}
// Draw another line number for the extra line fragment.
if let _ = layoutManager.extraLineFragmentTextContainer {
//This lines draw the background
if let newlineCountBeforeSelection = newlineCountBeforeSelection, (lineNumber - 1) == newlineCountBeforeSelection {
let rect = NSRect(x: 0, y: layoutManager.extraLineFragmentRect.minY, width: GUTTER_WIDTH, height: layoutManager.extraLineFragmentRect.height)
NSColor.selectedBackground.setFill()
rect.fill()
}
self.drawLineNumber(num: lineNumber, atYPosition: layoutManager.extraLineFragmentRect.minY)
}
}
func drawLineNumber(num: Int, atYPosition yPos: CGFloat) {
// Unwrap the text view.
guard let textView = self.clientView as? NSTextView, let font = textView.font else { return }
// Define attributes for the attributed string.
let attrs = [NSAttributedStringKey.font: font, NSAttributedStringKey.foregroundColor: self.foregroundColor]
// Define the attributed string.
let attributedString = NSAttributedString(string: "\(num)", attributes: attrs)
// Get the NSZeroPoint from the text view.
let relativePoint = self.convert(NSPoint.zero, from: textView)
// Calculate the x position, within the gutter.
let xPosition = GUTTER_WIDTH - (attributedString.size().width + 5)
// Draw the attributed string to the calculated point.
attributedString.draw(at: NSPoint(x: xPosition, y: relativePoint.y + yPos - 4))
}
}
看起来像这样:
但是如果我在TextView中滚动,那么使用NSAttributedString.draw(at :)绘制的行号与文本一起滚动,但背景不会滚动:
我该如何解决这个问题?