Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to programmatically add bullet list to NSTextView

The question may sound strange but I've been struggling with it for a few days.

I have a NSTextView that can display some text with a few formatting options. One of them is the ability to turn on/off the bullet list (the easiest one) for a selection or current row.

I know that there is a orderFrontListPanel: method on NSTextView that opens the window with available list parameters to select from and edit (like in TextView when you press Menu->Format->List...). I have already figured out and implemented adding bullets by hand and the NSTextView seems to behave with them almost correctly. By saying almost I mean that it preserves tab positions, continues the list on 'enter', etc. But there are some minor glitches that dont's suit me and differs from standart implementation.

I tried to find the default way to set lists programmatically like it is done through 'List...' menu with no luck.

I ask for help, every little bit of information will be appreciated :).

P.S.: I have looked into the TextView source code, found a lot of interesting but no sign or clue how to enable lists programmatically.

Update

Still investigating. I found that when you send orderFrontListPanel: to your NSTextView and then select bullets and press enter, no special messages are sent to NSTextView. It means that the bullet list may be constructed somewhere inside this popup panel and set directly to TextView's text container...

like image 229
GregoryM Avatar asked Apr 27 '11 21:04

GregoryM


1 Answers

Two methods of programmatically adding a bulleted list to an NSTextView:

Method 1:

The following links led me to this first method, but it’s unnecessarily roundabout unless you want to use some special non-Unicode glyph for the bullet:

  • Display hidden characters in NSTextView
  • How to draw one NSGlyph, that hasn't unicode representation?
  • appendBezierPathWithGlyph fails in [NSBezierPath currentPoint]

This requires: (1) a subclassed layout manager that substitutes the bullet glyph for some arbitrary character; and (2) a paragraph style with a firstLineHeadIndent, a tab stop slightly bigger than that indent, and a headIndent for wrapped lines that combines the two.

The layout manager looks like this:

#import <Foundation/Foundation.h>

@interface TickerLayoutManager : NSLayoutManager {

// Might as well let this class hold all the fonts used by the progress ticker.
// That way they're all defined in one place, the init method.
NSFont *fontNormal;
NSFont *fontIndent; // smaller, for indented lines
NSFont *fontBold;

NSGlyph glyphBullet;
CGFloat fWidthGlyphPlusSpace;

}

@property (nonatomic, retain) NSFont *fontNormal;
@property (nonatomic, retain) NSFont *fontIndent; 
@property (nonatomic, retain) NSFont *fontBold;
@property NSGlyph glyphBullet;
@property CGFloat fWidthGlyphPlusSpace;

@end

#import "TickerLayoutManager.h"

@implementation TickerLayoutManager

@synthesize fontNormal;
@synthesize fontIndent; 
@synthesize fontBold;
@synthesize glyphBullet;
@synthesize fWidthGlyphPlusSpace;

- (id)init {
    self = [super init];
    if (self) {
        self.fontNormal = [NSFont fontWithName:@"Baskerville" size:14.0f];
        self.fontIndent = [NSFont fontWithName:@"Baskerville" size:12.0f];
        self.fontBold = [NSFont fontWithName:@"Baskerville Bold" size:14.0f];
        // Get the bullet glyph.
        self.glyphBullet = [self.fontIndent glyphWithName:@"bullet"];
        // To determine its point size, put it in a Bezier path and take its bounds.
        NSBezierPath *bezierPath = [NSBezierPath bezierPath];
        [bezierPath moveToPoint:NSMakePoint(0.0f, 0.0f)]; // prevents "No current point for line" exception
        [bezierPath appendBezierPathWithGlyph:self.glyphBullet inFont:self.fontIndent];
        NSRect rectGlyphOutline = [bezierPath bounds];
        // The bullet should be followed with a space, so get the combined size...
        NSSize sizeSpace = [@" " sizeWithAttributes:[NSDictionary dictionaryWithObject:self.fontIndent forKey:NSFontAttributeName]];
        self.fWidthGlyphPlusSpace = rectGlyphOutline.size.width + sizeSpace.width;
        // ...which is for some reason inexact. If this number is too low, your bulleted text will be thrown to the line below, so add some boost.
        self.fWidthGlyphPlusSpace *= 1.5; // 
    }

    return self;
}

- (void)drawGlyphsForGlyphRange:(NSRange)range 
                        atPoint:(NSPoint)origin {

    // The following prints only once, even though the textview's string is set 4 times, so this implementation is not too expensive.
    printf("\nCalling TickerLayoutManager's drawGlyphs method.");

    NSString *string = [[self textStorage] string];
    for (int i = range.location; i < range.length; i++) {
        // Replace all occurrences of the ">" char with the bullet glyph.
        if ([string characterAtIndex:i] == '>')
            [self replaceGlyphAtIndex:i withGlyph:self.glyphBullet];
    }

    [super drawGlyphsForGlyphRange:range atPoint:origin];
}

@end

Assign the layout manager to the textview in your window/view controller’s awakeFromNib, like this:

- (void) awakeFromNib {

    // regular setup...

    // Give the ticker display NSTextView its subclassed layout manager.
    TickerLayoutManager *newLayoutMgr = [[TickerLayoutManager alloc] init];
    NSTextContainer *textContainer = [self.txvProgressTicker textContainer];
    // Use "replaceLM" rather than "setLM," in order to keep shared relnshps intact. 
    [textContainer replaceLayoutManager:newLayoutMgr];
    [newLayoutMgr release];
    // (Note: It is possible that all text-displaying controls in this class’s window will share this text container, as they would a field editor (a textview), although the fact that the ticker display is itself a textview might isolate it. Apple's "Text System Overview" is not clear on this point.)

}

And then add a method something like this:

- (void) addProgressTickerLine:(NSString *)string 
                   inStyle:(uint8_t)uiStyle {

    // Null check.
    if (!string)
        return;

    // Prepare the font.
    // (As noted above, TickerLayoutManager holds all 3 ticker display fonts.)
    NSFont *font = nil;
    TickerLayoutManager *tickerLayoutMgr = (TickerLayoutManager *)[self.txvProgressTicker layoutManager];
    switch (uiStyle) {
        case kTickerStyleNormal:
            font = tickerLayoutMgr.fontNormal;
            break;
        case kTickerStyleIndent:
            font = tickerLayoutMgr.fontIndent;
            break;
        case kTickerStyleBold:
            font = tickerLayoutMgr.fontBold;
            break;
        default:
            font = tickerLayoutMgr.fontNormal;
            break;
    }


    // Prepare the paragraph style, to govern indentation.    
    // CAUTION: If you propertize it for re-use, make sure you don't mutate it once it has been assigned to an attributed string. (See warning in class ref.)
    // At the same time, add the initial line break and, if indented, the tab.
    NSMutableParagraphStyle *paragStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; // ALLOC
    [paragStyle setAlignment:NSLeftTextAlignment]; // default, but just in case
    if (uiStyle == kTickerStyleIndent) {
        // (The custom layout mgr will replace ‘>’ char with a bullet, so it should be followed with an extra space.)
        string = [@"\n>\t" stringByAppendingString:string];
        // Indent the first line up to where the bullet should appear.
        [paragStyle setFirstLineHeadIndent:15.0f];
        // Define a tab stop to the right of the bullet glyph.
        NSTextTab *textTabFllwgBullet = [[NSTextTab alloc] initWithType:NSLeftTabStopType location:15.0f + tickerLayoutMgr.fWidthGlyphPlusSpace];
        [paragStyle setTabStops:[NSArray arrayWithObject:textTabFllwgBullet]];  
        [textTabFllwgBullet release];
        // Set the indentation for the wrapped lines to the same place as the tab stop.
        [paragStyle setHeadIndent:15.0f + tickerLayoutMgr.fWidthGlyphPlusSpace];
    }
    else {
        string = [@"\n" stringByAppendingString:string];
    }


    // PUT IT ALL TOGETHER.
    // Combine the above into a dictionary of attributes.
    NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
                            font, NSFontAttributeName, 
                            paragStyle, NSParagraphStyleAttributeName, 
                            nil];
    // Use the attributes dictionary to make an attributed string out of the plain string.
    NSAttributedString *attrs = [[NSAttributedString alloc] initWithString:string attributes:dict]; // ALLOC
    // Append the attributed string to the ticker display.
    [[self.txvProgressTicker textStorage] appendAttributedString:attrs];

    // RELEASE
    [attrs release];
    [paragStyle release];

}

Test it out:

NSString *sTicker = NSLocalizedString(@"First normal line of ticker should wrap to left margin", @"First normal line of ticker should wrap to left margin");
[self addProgressTickerLine:sTicker inStyle:kTickerStyleNormal];
sTicker = NSLocalizedString(@"Indented ticker line should have bullet point and should wrap farther to right.", @"Indented ticker line should have bullet point and should wrap farther to right.");
[self addProgressTickerLine:sTicker inStyle:kTickerStyleIndent];
sTicker = NSLocalizedString(@"Try a second indented line, to make sure both line up.", @"Try a second indented line, to make sure both line up.");
[self addProgressTickerLine:sTicker inStyle:kTickerStyleIndent];
sTicker = NSLocalizedString(@"Final bold line", @"Final bold line");
[self addProgressTickerLine:sTicker inStyle:kTickerStyleBold];

You get this:

enter image description here

Method 2:

But the bullet is a regular Unicode char, at hex 2022. So you can put it in the string directly, and get an exact measurement, like this:

    NSString *stringWithGlyph = [NSString stringWithUTF8String:"\u2022"];
    NSString *stringWithGlyphPlusSpace = [stringWithGlyph stringByAppendingString:@" "];
    NSSize sizeGlyphPlusSpace = [stringWithGlyphPlusSpace sizeWithAttributes:[NSDictionary dictionaryWithObject:self.fontIndent forKey:NSFontAttributeName]];
    self.fWidthGlyphPlusSpace = sizeGlyphPlusSpace.width;

So there is no need for the custom layout manager. Just set the paragStyle indentations as above, and append your text string to a string holding the line return + bullet char + space (or + tab, in which case you’ll still want that tab stop).

Using a space, this produced a tighter result:

enter image description here

Want to use a character other than the bullet? Here’s a nice Unicode chart: http://www.danshort.com/unicode/

like image 53
Wienke Avatar answered Sep 27 '22 19:09

Wienke