Sub-Classing Nstextstorage Causes Significant Memory Issues

Sub-Classing NSTextStorage Causes Significant Memory Issues

Wow.... weird, it got fixed when I changed the type of storage to NSTextStorage....

typealias PropertyList = [String : AnyObject]

class BMTextStorage : NSTextStorage {

overrride var string: String {
return storage.string
}

private var storage = NSTextStorage()

override func attributesAtIndex(location: Int, effectiveRange range: NSRangePointer) -> PropertyList {
return storage.attributesAtIndex(location, effectiveRange: range)
}

override func replaceCharactersInRange(range: NSRange, withString str: String) {
storage.replaceCharactersInRange(range, withString: str)
edited([.EditedAttributes, .EditedCharacters], range: range, changeInLength: str.length - range.length)
}

override func setAttributes(attrs: PropertyList?, range: NSRange) {
storage.setAttributes(attrs, range: range)
edited([.EditedAttributes], range: range, changeInLength: 0)
}

override func processEditing() {
super.processEditing()
}

}

NSTextView customizing double click selection

First of all, contrary to a previous answer, NSTextView's selectionRangeForProposedRange:granularity: method is not the correct place to override to achieve this. In Apple's "Cocoa Text Architecture" doc (https://developer.apple.com/library/prerelease/mac/documentation/TextFonts/Conceptual/CocoaTextArchitecture/TextEditing/TextEditing.html – see the "Subclassing NSTextView" section) Apple states explicitly "These mechanisms aren’t meant for changing language word definitions (such as what’s selected by a double click)." I'm not sure why Apple feels that way, but I suspect it is because selectionRangeForProposedRange:granularity: does not get any information regarding what part of the proposed range is the initial click point, versus what part is a place the user dragged to; making double-click-drags behave correctly might be hard to do with an override of this method. Perhaps there are other issues as well, I don't know; the doc is a bit cryptic. Perhaps Apple plans to make changes to the selection mechanism later that would break such overrides. Perhaps there are other aspects of defining what a "word" is that overriding here fails to address. Who knows; but it is generally a good idea to follow Apple's instructions when they make a statement like this.

Oddly, Apple's doc goes on to say "That detail of selection is handled at a lower (and currently private) level of the text system." I think that is outdated, because in fact the needed support does exist: the doubleClickAtIndex: method on NSAttributedString (in the NSAttributedStringKitAdditions category). This method is used (in the NSTextStorage subclass of NSAttributedString) by the Cocoa text system to determine word boundaries. Subclassing NSTextStorage is a bit tricky, so I'll provide a full implementation here for a subclass called MyTextStorage. Much of this code for subclassing NSTextStorage comes from Ali Ozer at Apple.

In MyTextStorage .h:

@interface MyTextStorage : NSTextStorage
- (id)init;
- (id)initWithAttributedString:(NSAttributedString *)attrStr;
@end

In MyTextStorage.m:

@interface MyTextStorage ()
{
NSMutableAttributedString *contents;
}
@end

@implementation MyTextStorage

- (id)initWithAttributedString:(NSAttributedString *)attrStr
{
if (self = [super init])
{
contents = attrStr ? [attrStr mutableCopy] : [[NSMutableAttributedString alloc] init];
}
return self;
}

- init
{
return [self initWithAttributedString:nil];
}

- (void)dealloc
{
[contents release];
[super dealloc];
}

// The next set of methods are the primitives for attributed and mutable attributed string...

- (NSString *)string
{
return [contents string];
}

- (NSDictionary *)attributesAtIndex:(NSUInteger)location effectiveRange:(NSRange *)range
{
return [contents attributesAtIndex:location effectiveRange:range];
}

- (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str
{
NSUInteger origLen = [self length];
[contents replaceCharactersInRange:range withString:str];
[self edited:NSTextStorageEditedCharacters range:range changeInLength:[self length] - origLen];
}

- (void)setAttributes:(NSDictionary *)attrs range:(NSRange)range
{
[contents setAttributes:attrs range:range];
[self edited:NSTextStorageEditedAttributes range:range changeInLength:0];
}

// And now the actual reason for this subclass: to provide code-aware word selection behavior

- (NSRange)doubleClickAtIndex:(NSUInteger)location
{
// Start by calling super to get a proposed range. This is documented to raise if location >= [self length]
// or location < 0, so in the code below we can assume that location indicates a valid character position.
NSRange superRange = [super doubleClickAtIndex:location];
NSString *string = [self string];

// If the user has actually double-clicked a period, we want to just return the range of the period.
if ([string characterAtIndex:location] == '.')
return NSMakeRange(location, 1);

// The case where super's behavior is wrong involves the dot operator; x.y should not be considered a word.
// So we check for a period before or after the anchor position, and trim away the periods and everything
// past them on both sides. This will correctly handle longer sequences like foo.bar.baz.is.a.test.
NSRange candidateRangeBeforeLocation = NSMakeRange(superRange.location, location - superRange.location);
NSRange candidateRangeAfterLocation = NSMakeRange(location + 1, NSMaxRange(superRange) - (location + 1));
NSRange periodBeforeRange = [string rangeOfString:@"." options:NSBackwardsSearch range:candidateRangeBeforeLocation];
NSRange periodAfterRange = [string rangeOfString:@"." options:(NSStringCompareOptions)0 range:candidateRangeAfterLocation];

if (periodBeforeRange.location != NSNotFound)
{
// Change superRange to start after the preceding period; fix its length so its end remains unchanged.
superRange.length -= (periodBeforeRange.location + 1 - superRange.location);
superRange.location = periodBeforeRange.location + 1;
}

if (periodAfterRange.location != NSNotFound)
{
// Change superRange to end before the following period
superRange.length -= (NSMaxRange(superRange) - periodAfterRange.location);
}

return superRange;
}

@end

And then the last part is actually using your custom subclass in your textview. If you have an NSTextView subclass as well, you can do this in its awakeFromNib method; otherwise, do this wherever else you get a chance, right after your nib loads; in the awakeFromNib call for a related window or controller, for example, or simply after your call to load the nib that contains the textview. In any case, you want to do this (where textview is your NSTextView object):

[[textview layoutManager] replaceTextStorage:[[[MyTextStorage alloc] init] autorelease]];

And with that, you should be good to go, unless I've made a mistake in transcibing this!

Finally, note that there is another method in NSAttributedString, nextWordFromIndex:forward:, that is used by Cocoa's text system when the user moves the insertion point to the next/previous word. If you want that sort of thing to follow the same word definition, you will need to subclass it as well. For my application I did not do that – I wanted next/previous word to move over whole a.b.c.d sequences (or more accurately I just didn't care) – so I don't have an implementation of that to share here. Left as an exercise for the reader.

Cocoa class member variable allocated inside function call nil unless forced to init/load

I am not familiar with cocoa that much but I think the problem is ARC (Automatic reference counting).

NSTextView* textView = [[NSTextView alloc] initWithFrame:containerFrame textContainer:textContainer];

In the .h file of NSTextContainer you can see NSTextView is a weak reference type.

Sample Image

So after returning from the function it gets deallocated

But if you make the textView an instance variable of TestMainView it works as expected.
Not really sure why it also works if you force it though. ~~(Maybe compiler optimisation?)~~

It seems forcing i.e calling

if (textContainer.textView) {

is triggering retain/autorelease calls so until the next autorelease drain call, textview is still alive.(I am guessing it does not get drained until awakeFromNib function returns). The reason why it works is that you are adding the textView to the view hierarchy(a strong reference) before autorelease pool releases it.

how to alloc a NSTextStorage with pre-allocated malloc'ed storage

NSTextStorage is documented as subclassable. As long as you implement the required primitives (listed in the Subclassing Notes), you're free to implement the backing storage any way you like.



Related Topics



Leave a reply



Submit