1

I'm trying to show some extra symbols next to lines in NSTextView, based on text attributes.

I have successfully subclassed NSLayoutManager, but it seems that layout manager can't draw outside the area set by textContainerInset.

Because my text view can potentially have a very long strings, I'm hoping to keep the drawing connected to displaying glyphs. Is there a way to trick the layout manager to be able to draw inside the content insets — or is there another method I use instead of drawGlyphsForGlyphRange?

I have tried calling super before and after drawing, as well as storing and not storing graphics state. I also attempted setDrawsOutsideLineFragment:YES for the glyphs, but with no luck.

Things like Xcode editor itself uses change markers, so I know that this is somehow doable, but it's very possible I'm looking from the wrong place.

My drawing method, simplified:

- (void)drawGlyphsForGlyphRange:(NSRange)glyphsToShow atPoint:(NSPoint)origin {
    [super drawGlyphsForGlyphRange:glyphsToShow atPoint:origin];
    
    NSTextStorage *textStorage = self.textStorage;
    NSTextContainer *textContainer = self.textContainers[0];
    
    NSRange glyphRange = glyphsToShow;
    NSSize offset = self.textContainers.firstObject.textView.textContainerInset;
        
    while (glyphRange.length > 0) {
        NSRange charRange = [self characterRangeForGlyphRange:glyphRange actualGlyphRange:NULL], attributeCharRange, attributeGlyphRange;
        
        id attribute = [textStorage attribute:@"Revision" atIndex:charRange.location longestEffectiveRange:&attributeCharRange inRange:charRange];
        attributeGlyphRange = [self glyphRangeForCharacterRange:attributeCharRange actualCharacterRange:NULL];
        attributeGlyphRange = NSIntersectionRange(attributeGlyphRange, glyphRange);

        if (attribute != nil) {
            [NSGraphicsContext saveGraphicsState];
            
            NSRect boundingRect = [self boundingRectForGlyphRange:attributeGlyphRange
                                                  inTextContainer:textContainer];
            
            // Find the top of the revision
            NSPoint point = NSMakePoint(offset.width - 20, offset.height + boundingRect.origin.y + 1.0);
            
            NSString *marker = @"*";
            
            [marker drawAtPoint:point withAttributes:@{
                NSForegroundColorAttributeName: NSColor.blackColor;
            }];
                                   
            [NSGraphicsContext restoreGraphicsState];
        }
                
        glyphRange.length = NSMaxRange(glyphRange) - NSMaxRange(attributeGlyphRange);
        glyphRange.location = NSMaxRange(attributeGlyphRange);
    }
}
Tritonal
  • 607
  • 4
  • 16
  • How about a vertical ruler? See [NSRulerView how to correctly align line numbers with main text](https://stackoverflow.com/questions/40672218/nsrulerview-how-to-correctly-align-line-numbers-with-main-text) – Willeke Feb 13 '22 at 00:54
  • AFAIK, a vertical `NSRulerView` cannot be positioned anywhere else except on the left side. I tried several hacks to make it invisible and move it around, but with no luck. However, looking at how ruler views work and are updated, I think it might be possible to create a similar view of my own, and have `drawGlyphsForGlyphRange` update its contents when necessary. – Tritonal Feb 13 '22 at 12:03

1 Answers1

2

The answer was much more simple than I anticipated.

You can set lineFragmentPadding for the associated NSTextContainer to make more room for drawing in the margins. This has to be taken into account when setting insets for the text container.

Tritonal
  • 607
  • 4
  • 16