How to programmatically add bullet list to NSTextView

后端 未结 1 1743
说谎
说谎 2021-02-03 14:49

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 the

相关标签:
1条回答
  • 2021-02-03 15:21

    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/

    0 讨论(0)
提交回复
热议问题