为什么在NSRulerView中绘制的NSAttributedString会与NSTextView一起滚动,但是在其中绘制的NSRect不会?

时间:2018-05-04 21:24:34

标签: swift macos cocoa nstextview

我尝试使用带有行号的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))
    }
}

看起来像这样:

Line Numbers

但是如果我在TextView中滚动,那么使用NSAttributedString.draw(at :)绘制的行号与文本一起滚动,但背景不会滚动:

Background Scroll

我该如何解决这个问题?

0 个答案:

没有答案