How to draw line number with NSRulerView?
Displaying line numbers with NSTextView, written with Objective-C, clean and simple. Sorry, I know this is an old question frequently asked, but I can't find an answer here or by google searching, there's several project, e.g, https://github.com/scottharwell/NoodleKit, can't work, and https://github.com/krzyzanowskim/STTextView, etc, when I converted one of the sample working Swift code to OC, the same code can't work as expected, drawHashMarksAndLabels() never called in the subclass of NSRulerView, the from code: https://github.com/yichizhang/NSTextView-LineNumberView, the skeleton of the converted code:
===1. subclass NSRulerView
@interface PERulerView: NSRulerView
- (void)drawHashMarksAndLabels:(NSRect)rect{
//never called!!!
}
- (id)initWithTextView:(NSTextView *)textView {
NSScrollView *aScrollView = [textView enclosingScrollView];
self = [super initWithScrollView:aScrollView orientation: NSVerticalRuler];
if ( self != nil) {
self.clientView = textView;
self.ruleThickness = 40;
}
return self;
}
===2. setup RulerView
@implementation ViewController
@property (nonatomic, strong) IBOutlet PETextView *textView;
- (void)viewDidLoad {
[self.textView initRulerView];
}
===3. create RulerView
@interface PETextView: NSTextView
- (void)initRulerView{
rulerView = [[PERulerView alloc] initWithTextView: self];
NSScrollView *scrollView = [self enclosingScrollView];
[scrollView setVerticalRulerView: rulerView];
[scrollView setHasHorizontalRuler: NO];
[scrollView setHasVerticalRuler: YES];
[scrollView setRulersVisible: YES];
self.postsFrameChangedNotifications = YES;
[[NSNotificationCenter defaultCenter] addObserver: self
selector: @selector(textViewFrameDidChange:)
name: NSViewFrameDidChangeNotification
object: self];
[[NSNotificationCenter defaultCenter] addObserver: self
selector: @selector(textViewTextDidChange:)
name: NSTextDidChangeNotification
object: self];
}
- (void)textViewFrameDidChange: (NSNotification *)notif{
[rulerView setNeedsDisplay: YES];
}
- (void)textViewTextDidChange: (NSNotification *)notif{
[rulerView setNeedsDisplay: YES];
}
===3. end
===4. draw code
- (void)doDrawHashMarksAndLabels:(NSRect)rect{
if(isDrawing) return;
NSTextView *textView = (NSTextView *)[self clientView];
NSLayoutManager *layoutManager = [textView layoutManager];
if(layoutManager == nil) return;
isDrawing = YES;
NSLog(@"doDrawHashMarksAndLabels");
CGPoint relativePoint = [self convertPoint: NSZeroPoint fromView: textView];
NSMutableDictionary *attr = [NSMutableDictionary new];
attr[NSFontAttributeName] = textView.font;
attr[NSForegroundColorAttributeName] = [NSColor redColor];
static void(^drawLineNumber)(NSDictionary *a, NSString *lineNumberString, CGFloat y) = nil;
if (!drawLineNumber) {
drawLineNumber = ^(NSDictionary *a, NSString *lineNumberString, CGFloat y) {
NSAttributedString *attString = [[NSAttributedString alloc] initWithString: lineNumberString attributes: a];
NSSize stringSize = [lineNumberString sizeWithAttributes: a];
// Calculate the x position, within the gutter.
CGFloat x = 40 - stringSize.width;
// Draw the attributed string to the calculated point.
[attString drawAtPoint: NSMakePoint(x, y)];
NSLog(@"drawLineNumber %@.", lineNumberString);
};
}
NSRange visibleGlyphRange = [layoutManager glyphRangeForBoundingRect: textView.visibleRect inTextContainer: textView.textContainer];
NSUInteger firstVisibleGlyphCharacterIndex = [layoutManager characterIndexForGlyphAtIndex: visibleGlyphRange.location];
NSRegularExpression *newLineRegex = [[NSRegularExpression alloc] initWithPattern: @"\n" options:NSRegularExpressionCaseInsensitive error: nil];
NSRange matchRage = NSMakeRange(0, firstVisibleGlyphCharacterIndex);
NSArray *matches = [newLineRegex matchesInString:textView.string options:NSMatchingReportProgress range: matchRage];
NSUInteger lineNumber = [matches count] + 1;
NSLog(@"lineNumber: %ld", lineNumber);
NSUInteger glyphIndexForStringLine = visibleGlyphRange.location;
// Go through each line in the string.
while( glyphIndexForStringLine < NSMaxRange(visibleGlyphRange) ){
// Range of current line in the string.
NSUInteger characterIndex = [layoutManager characterIndexForGlyphAtIndex: glyphIndexForStringLine];
NSRange characterRangeForStringLine = [textView.string lineRangeForRange:NSMakeRange(characterIndex, 0)];
NSRange glyphRangeForStringLine = [layoutManager glyphRangeForCharacterRange:characterRangeForStringLine
actualCharacterRange: nil];
NSUInteger glyphIndexForGlyphLine = glyphIndexForStringLine;
NSUInteger glyphLineCount = 0;
while ( glyphIndexForGlyphLine < NSMaxRange(glyphRangeForStringLine) ) {
// See if the current line in the string spread across
// several lines of glyphs
NSRange effectiveRange = NSMakeRange(0, 0);
// Range of current "line of glyphs". If a line is wrapped,
// then it will have more than one "line of glyphs"
NSRect lineRect = [layoutManager lineFragmentRectForGlyphAtIndex:glyphIndexForGlyphLine
effectiveRange: &effectiveRange withoutAdditionalLayout: YES];
if (glyphLineCount > 0 ){
drawLineNumber(attr, @"-", NSMinY(lineRect)+relativePoint.y);
} else {
drawLineNumber(attr, [NSString stringWithFormat: @"%ld", lineNumber], NSMinY(lineRect)+relativePoint.y);
}
// Move to next glyph line
glyphLineCount += 1;
glyphIndexForGlyphLine = NSMaxRange(effectiveRange);
}
glyphIndexForStringLine = NSMaxRange(glyphRangeForStringLine);
lineNumber += 1;
}
// Draw line number for the extra line at the end of the text
if([layoutManager extraLineFragmentTextContainer] != nil){
drawLineNumber(attr, [NSString stringWithFormat: @"%ld", lineNumber], NSMinY(layoutManager.extraLineFragmentRect)+relativePoint.y);
}
isDrawing = NO;
}
- (void)drawHashMarksAndLabels:(NSRect)rect{
NSLog(@"drawHashMarksAndLabels");
id obj = [self clientView];
if ([obj isKindOfClass:[NSTextView class]]){
[self doDrawHashMarksAndLabels: rect];
}
}
===4. end
The env: Xcode 16.2 on macOS Sonoma 14.6.1, and the screenshot of the result:
drawHashMarksAndLabels
does not exist in AppKit. The method is called drawHashMarksAndLabels(in:)
in Swift and drawHashMarksAndLabelsInRect:
in Objective-C. Note the InRect
at the end.
Replace
- (void)drawHashMarksAndLabels:(NSRect)rect{
by
- (void)drawHashMarksAndLabelsInRect:(NSRect)rect {
Documentation drawHashMarksAndLabelsInRect: