如何以编程方式将项目符号列表添加到NSTextView

问题描述:

这个问题听起来很奇怪,但是我已经为之苦苦挣扎了几天.

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

我有一个NSTextView,它可以显示一些带有一些格式设置选项的文本.其中之一就是可以打开/关闭所选行或当前行的项目符号列表(最简单的一项).

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.

我知道NSTextView上有一个orderFrontListPanel:方法,该方法打开带有可用列表参数的窗口以供选择和编辑(就像在按Menu-> Format-> List ...时在TextView中一样). 我已经弄清楚并实现了手动添加项目符号的方法,而NSTextView似乎在几乎上表现正确.说几乎"是指它保留制表符的位置,在输入"后继续显示列表,等等.但是有些小故障不适合我,与标准实现有所不同.

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.

我试图找到以编程方式设置列表的默认方式,就像通过"List ..."菜单完成操作一样.

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 .:我研究了TextView源代码,发现了很多有趣的东西,但没有迹象或线索来了解如何以编程方式启用列表.

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.

更新

仍在调查中.我发现,当您将orderFrontListPanel:发送到您的NSTextView,然后选择项目符号并按Enter键时,没有特殊消息发送到NSTextView.这意味着项目符号列表可以构造在此弹出面板内的某个位置,并直接设置为TextView的文本容器...

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...

以编程方式将项目符号列表添加到NSTextView的两种方法:

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

方法1:

以下链接将我带到了第一种方法,但是除非您想为项目符号使用一些特殊的非Unicode字形,否则它是不必要的回旋:

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]

这需要:(1)子类化的布局管理器,它用项目符号字形代替某些任意字符; (2)具有firstLineHeadIndent的段落样式,一个比该缩进稍大的制表位和一个将两者结合在一起的换行的headIndent.

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.

布局管理器如下所示:

#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

将布局管理器分配给窗口/视图控制器awakeFromNib中的textview,如下所示:

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];

}

测试一下:

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];

您得到了:

方法2:

但是项目符号是2022十六进制的常规Unicode字符.因此,您可以将其直接放入字符串中,并获得精确的度量值,如下所示:

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;

因此不需要自定义布局管理器.只需如上所述设置paragStyle缩进,然后将文本字符串附加到包含回车符+项目符号字符+空格(或+制表符,在这种情况下,您仍然希望该制表符停止)的字符串即可.

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:

是否要使用子弹头以外的其他字符?这是一个不错的Unicode图表: http://www.danshort.com/unicode/

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