'Render some text in uppercase in an NSTextView without changing the underlying string
In an NSTextView, is it possible to render a given range of a string as all-caps, without changing the underlying string itself? The idea is similar to NSLayoutManager's temporary attributes, or CSS' text-transform property.
Solution 1:[1]
It might be possible, but you're going to have to implement such support yourself. I don't believe there's anything built in to do that.
You would have to implement a custom subclass of NSLayoutManager and a custom subclass of NSGlyphGenerator, too. Your custom layout manager class would have an interface similar to the temporary attributes interface. That's because the built-in temporary attributes feature doesn't support attributes that modify layout, but changing the case of characters will modify layout. You will need to store the custom temporary attributes somehow and invalidate layout. Because your custom glyph generator will need them (see below), you may wish to store the temporary attributes in that object.
Handling your custom attribute will involve substituting different glyphs, so I think you need to use a custom glyph generator. You'd pass an instance of your custom subclass of NSGlyphGenerator to the setter of the layout manager's glyphGenerator property. Your glyph generator will need to interpose itself between the standard implementation and its glyph storage object (which is actually the layout manager in its role as an NSGlyphStorage). So, your subclass would also adopt the NSGlyphStorage protocol.
You will override the sole glyph generator instance method, -generateGlyphsForGlyphStorage:desiredNumberOfCharacters:glyphIndex:characterIndex:. When the layout manager calls your glyph generator, your override of that method will call through to super, but will substitute self for the glyphStorage parameter. It will have to remember the original glyphStorage in an instance variable, though.
Then, the superclass's implementation will call various methods from the NSGlyphStorage protocol on your object. If you wanted your implementation to do nothing special, it would just call through to the original glyphStorage object. However, you want to check for your custom attribute and, for any run where it's present, substitute capital letters. This has to happen in the implementation of -attributedString. You will need to make a mutable copy of the attribute string returned by the original glyphStorage (which is the layout manager) and, for any ranges affected by your custom temporary attribute, replace the characters with the localized uppercase versions of those characters.
You will want to optimize this so you're not constantly duplicating and modifying the (possibly very large) attributed string that is the text storage of the layout manager. Unfortunately, the rather limited interfaces between the layout manager and glyph generator won't make this easy. The text storage will call -textStorage:edited:range:changeInLength:invalidatedRange: on the layout manager when it has been changed, so you can leverage that to invalidate any cached copy you may have.
Solution 2:[2]
Here's a working implementation for anyone reading this years later.
You only need to set a delegate for your NSLayoutManager and implement shouldGenerateGlyphs:. This example is in Objective C, but should be easily translated into Swift.
To make only some ranges uppercase, you need to probe the correct range in your shouldGenerateGlyphs method. In my implementation, I used custom attributes.
-(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;
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Ken Thomases |
| Solution 2 |
