Vitaliy Vashchenko Vitaliy Vashchenko - 7 months ago 72
Swift Question

NSTextView selection highlights all characters even line indents

Can't find any clue how to manage this.

By default, NSTextView selection highlights the whole size of its text container. It ignores line spacing, head or tail indents etc. But in Pages app selection doesn't highlight those ancillary parts, it highlight characters ONLY. And it highlights all the height of the line even if text container's height is smaller (paragraph spacing before and after).

I want to implement that behavior but can't understand where to begin. I've searched here, I've searched Apple docs, I've tried sample projects. Nothing.

Maybe someone can guide me in the right direction? Thanks!

Answer

I found that hamstergene's answer isn't correct. In fact NSTextView highlights its text container lines all over their frames.

So, if you use paragraph head indents then there will be empty space leading the text highlighted. And if you select EOL character then the tail of the text container will be highlighted.

My solution was to nullify head and tail indents of the paragraph style (I cache them in the private variable to and put them back when my text storage is accessed for printing) and simply adjust frame of the text container line via overrided lineFragmentRectForProposedRect: atIndex: writingDirection: remainingRect method of my NSTextContainer subclass.

public override func lineFragmentRectForProposedRect(proposedRect: NSRect, atIndex characterIndex: Int, writingDirection baseWritingDirection: NSWritingDirection, remainingRect: UnsafeMutablePointer<NSRect>) -> NSRect {
    var rect = super.lineFragmentRectForProposedRect(proposedRect, atIndex: characterIndex, writingDirection: baseWritingDirection, remainingRect: remainingRect)

    let indent = // get cached indent wherever you stored it
    rect.origin.x = indent
    return rect
}

One hint here: adjusting line frame isn't helping for excluding selection of the tail of the line (when selecting end-of-line character). And that's where the text container's exclusionPaths property comes in hand.

I just overrided the lineFragmentRectForGlyphAtIndex: effectiveRange: withoutAdditionalLayout method in my NSLayoutManager subclass. And just calculating tailIndent like that (code is still in process of improving:)

public override func lineFragmentRectForGlyphAtIndex(glyphIndex: Int, effectiveRange effectiveGlyphRange: NSRangePointer, withoutAdditionalLayout flag: Bool) -> NSRect {
    let rect = super.lineFragmentRectForGlyphAtIndex(glyphIndex, effectiveRange: effectiveGlyphRange, withoutAdditionalLayout: flag)

    // calculate line width to constrain the text container
    let lineFragment = self.lineFragmentUsedRectForGlyphAtIndex(glyphIndex, effectiveRange: nil, withoutAdditionalLayout: true)
    if let textContainer = textContainerForGlyphAtIndex(glyphIndex, effectiveRange: nil, withoutAdditionalLayout: true) {
        let tailIndent = textContainer.size.width - (lineFragment.width + lineFragment.origin.x)

        // create exclusion path
        let exclusionRect = NSMakeRect(lineFragment.width, lineFragment.origin.y, tailIndent, lineFragment.height)
        let rectanglePath = NSBezierPath.init(rect: exclusionRect)

        // and add it only if there's no exclusion path for the current line (otherwise we'll get an infinity loop
        if let _ = textContainer.exclusionPaths.indexOf({$0.bounds == exclusionRect}) {
        } else {
            textContainer.exclusionPaths.append(rectanglePath)
        }
    }
    return rect
}

It worked like a charm.

P.S.: end-of-line character I show in drawGlyphsForGlyphRange: atPoint method of my NSLayoutManager subclass. Just need to make sure this character is selected before showing it. Something like that:

public override func drawGlyphsForGlyphRange(glyphsToShow: NSRange, atPoint origin: NSPoint) {
    // show end of line character for selected text
    if let textView = firstTextView {
        for value in textView.selectedRanges {
            let range = value.rangeValue
            if range.contains(glyphsToShow) && range.length > 0 {
                let lengthToRedraw = NSMaxRange(range)
                for index in range.location ..< lengthToRedraw {
                    if (textStorage!.string as NSString).characterAtIndex(index) == 0x0A {
                        var pointToDrawAt = self.locationForGlyphAtIndex(index)
                        let glyphFragment = self.lineFragmentUsedRectForGlyphAtIndex(index, effectiveRange: nil, withoutAdditionalLayout: true)
                        pointToDrawAt.x += glyphFragment.origin.x
                        pointToDrawAt.y = glyphFragment.origin.y
                        let endOfLine = "¶" as NSString
                        endOfLine.drawAtPoint(pointToDrawAt, withAttributes:self.firstTextView!.typingAttributes)
                    }
                }
            }
        }
    }
    super.drawGlyphsForGlyphRange(glyphsToShow, atPoint: origin)
}

P.P.S.: all of the code above is still in process of refining. But it gives the perspective on the solution to create such a behavior of NSTextView. At least it working almost fine :)

Comments