I have certain custom text attributes that are used in my NSTextStorage to which I would like to add temporary attributes via the NSLayoutManager. For example, say I have a custom "NoteColor" text attribute associated with an NSColor object ([text addAttribute:@"NoteColor value:[NSColor redColor] range:range]); any range of text with this attribute I want to be drawn using a temporary attribute - for instance, having its NSForegroundAttributeName using the colour associated with the attribute.
What is the best and most efficient way of doing this?
In the past, I have reapplied the temporary attributes manually using NSLayoutManager's -setTemporaryAttribute:... every time the text is changed, for instance. That obviously isn't ideal, and more recently I have taken to overriding NSLayoutManager's -temporaryAttributesAtCharacterIndex:effectiveRange: to do this. For instance:
- (NSDictionary *)temporaryAttributesAtCharacterIndex:(unsigned)charIndex effectiveRange:(NSRangePointer)effectiveCharRange
{
NSDictionary *result = [super temporaryAttributesAtCharacterIndex:charIndex effectiveRange:effectiveCharRange];
NSTextStorage *textStorage = [self textStorage];
NSUInteger textLength = [textStorage length];
if (charIndex >= textLength)
return result;
NSMutableDictionary *mutableResult = nil;
NSDictionary *attribs = [textStorage attributesAtIndex:charIndex effectiveRange:NULL];
NSColor *color = [attribs objectForKey:@"NoteColor"];
if (color != nil)
{
mutableResult = [NSMutableDictionary dictionaryWithDictionary:result];
[mutableResult setObject:color forKey:NSForegroundColorAttributeName];
result = mutableResult;
}
return result;
}
(The above typed in my e-mail program as an example.)
However, this too seems incredibly inefficient. The above example would work fine, but as soon as I've checked for three or four custom attributes, typing slows down.
Is there a better way of doing this?
Thanks and all the best,
Keith
_______________________________________________
Cocoa-dev mailing list (Coco...@lists.apple.com)
Please do not post admin requests or moderator comments to the list.
Contact the moderators at cocoa-dev-admins(at)lists.apple.com
Help/Unsubscribe/Update your Subscription:
http://lists.apple.com/mailman/options/cocoa-dev/cocoa-dev-garchive-98506%40googlegroups.com
This email sent to cocoa-dev-ga...@googlegroups.com
> I have certain custom text attributes that are used in my
> NSTextStorage to which I would like to add temporary attributes via
> the NSLayoutManager.
What version of OSX are you testing under? Under Leopard there's a bug
in -[NSLayoutManager
temporaryAttribute:atCharacterIndex:longestEffectiveRange:inRange:]
that calculates effective ranges that are too short. For specific test
cases this caused big inefficiencies in the text system. I believe
this bug is fixed in Snow Leopard.
> more recently I have taken to overriding NSLayoutManager's -
> temporaryAttributesAtCharacterIndex:effectiveRange:
If this is too slow, then I'd look to using some kind of cache for
your calculations. But really, NSLayoutManager's temporary attributes
are already a cache; one likely to be specifically designed for high
performance index/run access. I think your original idea of setting
temporary attributes whenever text changes would be the most efficient.
Perhaps you're recalculating too much, too often? I don't know the
access patterns for temporary attributes, but I would guess they are
only queried when associated text is displayed on screen. If that's
the case, you could fix them up lazily, eg: whenever text changes just
note down that the attributes are dirty in that range. Your temporary
attribute methods in your NSLayoutManager subclass can then ensure
that temporary attributes are not dirty before they are returned.
If none of that is efficient enough, you could rig up a NSTextStorage
subclass that has two sets of attributes: one set for private use and
another derived set which only the layout system sees.
Hopefully some of that helps,
~Martin
Previously I was using the shouldChangeTextInRange and textDidChange text view delegate methods to calculate the exact portion of text that had been added, and then my text view iterated through the changed text looking for specific custom attributes and applying temporary attributes to them. The main problem in that method seemed to be caused by removing temporary attributes rather than adding them (-removeTemporaryAttribute... seems to be very slow and use up a lot of CPU if called to often), so maybe I can optimise that.
Another idea that occurs to me following your text storage suggestion is that I could create a text storage subclass that actually knows about its layout manager and uses it to assign temporary attributes along with standard attributes, though I'm not so sure about that one.
I hadn't thought of how -temporaryAttributesAtIndex:... might be doing a lot of caching internally, though, but it does makes sense, and would explain why trying to add temporary attributes in an override of this is so inefficient.
I wish there was an easier way of doing this, something in the layout manager itself so that you could just tell the layout manager, "if the text has this attribute, use this temporary attribute". Something like:
- (void)setUsesTemporaryAttributeName:(NSString *)name forAttributeName:(NSString *)name options:(NSDictionary *)opt;
Oh well!
Thanks again and all the best,
Keith
--- On Thu, 10/1/09, Martin Wierschin <mar...@nisus.com> wrote:
> From: Martin Wierschin <mar...@nisus.com>
> Subject: Re: NSLayoutManager and best override point for temporary attributes
> To: "Keith Blount" <keith...@yahoo.com>
> Cc: coco...@lists.apple.com
> Date: Thursday, October 1, 2009, 1:50 AM
> Hi Keith,
>
> > I have certain custom text attributes that are used in
> my NSTextStorage to which I would like to add temporary
> attributes via the NSLayoutManager.
>
> What version of OSX are you testing under? Under Leopard
> there's a bug in -[NSLayoutManager
> temporaryAttribute:atCharacterIndex:longestEffectiveRange:inRange:]
> that calculates effective ranges that are too short. For
> specific test cases this caused big inefficiencies in the
> text system. I believe this bug is fixed in Snow Leopard.
>
> > more recently I have taken to overriding
> NSLayoutManager's
> -temporaryAttributesAtCharacterIndex:effectiveRange:
Looking at this again, would NSLayoutManager's
-textStorage:edited:range:changeInLength:invalidatedRange:
method be a good candidate for overriding to add temporary attributes? The text storage calls this whenever it's edited and provides it with the new range of characters. So it seems that I could add any temporary attributes to the range that gets passed in here, checking that changeInLength > 0.
Or, what if in the -addAttribute:..., -setAttributes:... and removeAttribute:... methods of my NSTextStorage, I called through to all of the text storage's layout managers to ask them to add the temporary attribute if necessary, after -edited:range:changeInLength: gets called?
Many thanks again!
All the best,
Keith
--- On Thu, 10/1/09, Martin Wierschin <mar...@nisus.com> wrote:
> From: Martin Wierschin <mar...@nisus.com>
> Subject: Re: NSLayoutManager and best override point for temporary attributes
> To: "Keith Blount" <keith...@yahoo.com>
> Cc: coco...@lists.apple.com
> Date: Thursday, October 1, 2009, 1:50 AM
> Hi Keith,
>
> > I have certain custom text attributes that are used in
> my NSTextStorage to which I would like to add temporary
> attributes via the NSLayoutManager.
>
> What version of OSX are you testing under? Under Leopard
> there's a bug in -[NSLayoutManager
> temporaryAttribute:atCharacterIndex:longestEffectiveRange:inRange:]
> that calculates effective ranges that are too short. For
> specific test cases this caused big inefficiencies in the
> text system. I believe this bug is fixed in Snow Leopard.
>
> > more recently I have taken to overriding
> NSLayoutManager's
> -temporaryAttributesAtCharacterIndex:effectiveRange:
If your custom attributes modifies just the graphics state (don't
affect the layout), you can override -[NSLayoutManager
showPackedGlyphs:length:glyphRange:atPoint:font:color:printingAdjustment
:].
The method is the bottleneck for calling CG APIs. You can query the
text attribute using glyphRange.location and see if there is one of
your custom attributes.
You should be able to customize alpha, color, blending mode,
compositing mode, clipping, etc.
Aki
> http://lists.apple.com/mailman/options/cocoa-dev/aki%40apple.com
>
> This email sent to a...@apple.com
Many thanks for your reply, much appreciated. Would you mind giving me a little more information on how to override this method? The docs are a little sparse in this regard. For instance, if I try passing a different colour into super's method, it has no effect; instead, it seems that I need to use [color set] before calling super (which would seem to be hinted at by the docs, as they say "if for any reason you modify the set color or font..."). Do I call [customColor set] using my own colour, then call super, then call [color set] using the passed-in colour after calling super?
My custom attributes set only the colour, so this should be fine. (Although I'm not sure how I would use this method if I wanted, say, the underline to be a different colour from the text, which I need for certain link attributes.)
Many thanks again and all the best,
Keith
--- On Fri, 10/2/09, Aki Inoue <a...@apple.com> wrote:
> if necessary, after -edited:range:changeInLength: gets
You can do:
NSGraphicsContext *context = [NSGraphicsContext currentContext];
[context saveGraphicsState];
[yourCustomColor set];
[super showPackedGlyphs:...];
[context restoreGraphicsState];
> My custom attributes set only the colour, so this should be fine.
> (Although I'm not sure how I would use this method if I wanted, say,
> the underline to be a different colour from the text, which I need
> for certain link attributes.)
There is no rendering primitive method like this for underlines. You
still need to use temporary attributes for that purpose. I believe
the temporary attribute approach is efficient enough for that kind of
usage patterns.
Aki
> You can do:
> NSGraphicsContext *context = [NSGraphicsContext
> currentContext];
> [context saveGraphicsState];
> [yourCustomColor set];
> [super showPackedGlyphs:...];
> [context restoreGraphicsState];
>
This works perfectly, thank you.
> There is no rendering primitive method like this for
> underlines. You still need to use temporary attributes
> for that purpose. I believe the temporary attribute
> approach is efficient enough for that kind of usage
> patterns.
Returning to the original question for the underlines, is the most efficient approach to override -temporaryAttributesAtIndex... and check for the underline + custom attribute there and modify the returned dictionary, or to use -addTemporaryAttributes: on any changed text every time it is changed (e.g. in -textStorage:edited:range:changeInLength:invalidatedRange:)?
Thanks again!
Keith
I spent the weekend on this and the only thing I'm still having problems with is finding the best place to apply the underline and strikethrough temporary attributes so that they get applied automatically and efficiently (using the showPackedGlyphs:... method worked perfectly for the text itself - thank you Aki!).
I tried saving the edited range of text in the text storage and then having my layout manager check this from:
-textStorage:edited:range:changeInLength:invalidatedRange:
Then my layout manager adds temp attribs in this method. Turns out that this is a Bad Thing (which I should have realised beforehand), as it can cause glyph generation before the text storage has ended editing. D'oh.
Another solution that occurs to me is to have the text storage tell all of its layout managers to apply any temporary attributes after it has processed editing, e.g:
- (void)processEditing
{
[super processEditing];
if (dirtyRange.length == 0)
return;
NSEnumerator *e = [[self layoutManagers] objectEnumerator];
NSLayoutManager *lm;
while (lm = [e nextObject])
[lm applyTemporaryAttributesToDirtyRangeIfNecessary:dirtyRange];
dirtyRange = NSMakeRange(NSNotFound,0);
}
I'm still unsure that this is the best route to take, though. (As I say, I previously did all of this stuff in the text view delegate, but that meant duplicating code for each text view and was hugely inefficient; and overriding NSLayoutManager's -temporaryAttributesAtRange: was just too slow.)
In fact, does anyone know how NSTextView handles keeping the temporary attributes up to date for NSLinkAttributeName? Essentially I'm just trying to do the same but with a custom attribute instead of NSLinkAttributeName. The problem is simply that if you apply a temporary attribute to a range of text, typing in that range doesn't take on the temporary attribute (because temporary attributes are handled by the layout manager and so the text view knows nothing about them presumably). But NSTextView or NSLayoutManager (actually presumably the latter) has some internal magic that handles applying the linkTextAttributes (which are, according to the docs, just temporary attributes too) to any typed text with NSLinkAttributeName applied. All I'm trying to achieve is the same thing but with a custom attribute, and it seems more difficult than it should be... But I'm probably just making it so.
Many thanks and all the best,
Keith
> I tried saving the edited range of text in the text storage and then
> having my layout manager check this from:
>
> -textStorage:edited:range:changeInLength:invalidatedRange:
>
> Then my layout manager adds temp attribs in this method. Turns out
> that this is a Bad Thing (which I should have realised beforehand),
> as it can cause glyph generation before the text storage has ended
> editing. D'oh.
I haven't tried fixing temporary attributes in NSTextStorage -
fixFontAttributeInRange:, but that might be worth a try.
Ross
Many thanks for the suggestion.
>> I tried saving the edited range of text in the text storage and then
>> having my layout manager check this from:
>>
>> -textStorage:edited:range:changeInLength:invalidatedRange:
>>
>> Then my layout manager adds temp attribs in this method. Turns out
>> that this is a Bad Thing (which I should have realised beforehand),
>> as it can cause glyph generation before the text storage has ended
>> editing. D'oh.
> I haven't tried fixing temporary attributes in NSTextStorage -
> fixFontAttributeInRange:, but that might be worth a try.
Although it sounds like it should work, it doesn't unfortunately. Something very strange happens if you try to apply temporary attributes from -fixAttrubutesInRange: (or -fixFontAttributeInRange:) - the temporary attributes don't get displayed at all.
This seems way harder than it should be. I've now spent the best part of a week on essentially just trying to find the best way to apply temporary attributes to typed text without slowing everything down or causing crashes by intercepting things at the wrong time.
Although NSLayoutManager's -showPackedGlyphs:... provides the perfect override point for applying a different display colour to glyphs, there seems to be no good intercept point for changing the colour of underlines or strikethroughs.
For now, I have reverted to overriding -temporaryAttributesAtCharacterIndex:effectiveRange: and only changing the result if there is an underline or strikethrough at the passed-in index. Given that 10.6 provides a new delegate method, -layoutManager:shouldUseTemporaryAttributes:forDrawingToScreen:atCharacterIndex:effectiveRange:, which allows you to adjust temp attribs, I am guessing that this is the intended override after all, but that it is just hugely inefficient if you change the result too much... Though I'm unconvinced. Only more profiling will tell. :)
Many thanks again and all the best,
Keith