在不更改底层字符串的情况下在 NSTextView 中以大写形式呈现一些文本

Render some text in uppercase in an NSTextView without changing the underlying string

在 NSTextView 中,是否可以在不更改基础字符串本身的情况下将给定范围的字符串呈现为全部大写?这个想法类似于 NSLayoutManager's temporary attributes, or CSS' text-transform property.

这可能是可行的,但您将不得不自己实施此类支持。我不相信有任何内置的东西可以做到这一点。

您还必须实现 NSLayoutManager 的自定义子 class 和 NSGlyphGenerator 的自定义子class。您的自定义布局管理器 class 将具有类似于临时属性界面的界面。这是因为内置的临时属性特性不支持修改布局的属性,但是改变字符的大小写会修改布局。您将需要以某种方式存储自定义临时属性并使布局无效。因为您的自定义字形生成器将需要它们(见下文),您可能希望将临时属性存储在该对象中。

处理您的自定义属性将涉及替换不同的字形,因此我认为您需要使用自定义字形生成器。您会将 NSGlyphGenerator 的自定义子 class 的实例传递给布局管理器 glyphGenerator 属性 的 setter。您的字形生成器需要将自身插入标准实现及其字形存储对象(实际上是布局管理器,其角色是 NSGlyphStorage)。所以,你的 subclass 也会采用 NSGlyphStorage 协议。

您将覆盖唯一的字形生成器实例方法,-generateGlyphsForGlyphStorage:desiredNumberOfCharacters:glyphIndex:characterIndex:。当布局管理器调用您的字形生成器时,您对该方法的覆盖将调用 super,但会将 self 替换为 glyphStorage 参数。不过,它必须记住实例变量中的原始 glyphStorage

然后,superclass 的实现将在您的对象上调用 NSGlyphStorage 协议中的各种方法。如果您希望您的实现不做任何特别的事情,它只会调用原始的 glyphStorage 对象。但是,您想检查您的自定义属性,并且对于存在的任何 运行,替换为大写字母。这必须在 -attributedString 的实现中发生。您需要制作原始 glyphStorage(布局管理器)返回的属性字符串的可变副本,并且对于受自定义临时属性影响的任何范围,将字符替换为这些字符的本地化大写版本字符。

您需要对其进行优化,这样您就不会不断地复制和修改作为布局管理器文本存储的(可能非常大的)属性字符串。不幸的是,布局管理器和字形生成器之间相当有限的接口不会使这变得容易。文本存储将在布局管理器发生更改时调用 -textStorage:edited:range:changeInLength:invalidatedRange:,因此您可以利用它来使您可能拥有的任何缓存副本无效。

这是供多年后阅读的任何人使用的有效实现。

您只需要为您的 NSLayoutManager 设置委托并实现 shouldGenerateGlyphs:。这个例子在 Objective C 中,但应该很容易翻译成 Swift.

要仅将某些范围设为大写,您需要在 shouldGenerateGlyphs 方法中探测正确的范围。在我的实现中,我使用了自定义属性。

-(NSUInteger)layoutManager:(NSLayoutManager *)layoutManager shouldGenerateGlyphs:(const CGGlyph *)glyphs properties:(const NSGlyphProperty *)props characterIndexes:(const NSUInteger *)charIndexes font:(NSFont *)aFont forGlyphRange:(NSRange)glyphRange {

    // Somehow determine if you don't want to make this specific range uppercase
    if (notCorrectRange) return 0;

    // Get string reference
    NSUInteger location = charIndexes[0];
    NSUInteger length = glyphRange.length;
    CFStringRef str = (__bridge CFStringRef)[self.textStorage.string substringWithRange:(NSRange){ location, length }];
    
    // Create a mutable copy
    CFMutableStringRef modifiedStr = CFStringCreateMutable(NULL, CFStringGetLength(str));
    CFStringAppend(modifiedStr, str);
    
    // Make the string uppercase
    CFStringUppercase(modifiedStr, NULL);
    
    // Create the new glyphs
    CGGlyph *newGlyphs = GetGlyphsForCharacters((__bridge CTFontRef)(aFont), modifiedStr);
    [self.layoutManager setGlyphs:newGlyphs properties:props characterIndexes:charIndexes font:aFont forGlyphRange:glyphRange];
    free(newGlyphs);
        
    CFRelease(modifiedStr);
    return glyphRange.length;
}

CGGlyph* GetGlyphsForCharacters(CTFontRef font, CFStringRef string)
{
    // Get the string length and allocate buffers for characters and glyphs
    CFIndex count = CFStringGetLength(string);
 
    UniChar *characters = (UniChar *)malloc(sizeof(UniChar) * count);
    CGGlyph *glyphs = (CGGlyph *)malloc(sizeof(CGGlyph) * count);
 
    CFStringGetCharacters(string, CFRangeMake(0, count), characters);
 
    // Get the glyphs for the characters.
    CTFontGetGlyphsForCharacters(font, characters, glyphs, count);

    free(characters);
 
    return glyphs;
}