Undo support of NSTextStorage
Undo support of NSTextStorage
- Subject: Undo support of NSTextStorage
- From: Jesse Grosjean <email@hidden>
- Date: Fri, 2 May 2003 22:37:32 -0400
I wrote a while ago asking if anyone know how to add undo support to
NSTextStorage. I didn't get any responses, but now I'm further along
and hope someone can explain to me what I'm doing wrong. Simple undo
(char by char) is easy, but I would like to be able to merge
appropriate edits and that's where I'm having difficulties with the
undo stack.
Here is what I have so far. Seems to work for me on paper, but I'm
doing something wrong and corupting the undo stack. The basic idea here
is to commit partial edits (ones that might be merged with later edits)
to a separate HBTextStorageEdit object. This is done so that I have a
"handle" on these edits and can remove them from the undo stack when
they need to be merged with later edits. When a situation arrises where
the edits cannot be merged they are commited to the undo stack. By this
"commit" I mean that the edits are placed on the undo stack with the
NSTextStorage as the target, they won't be removed or merged with other
edits after this commit.
So what do I need to do to get this thing working?
@interface HBTextStorage : NSTextStorage {
NSMutableAttributedString *_textContents;
HBTextStorageEdit *_editInProgress;
}
@end
@implementation HBTextStorage
- ( NSString *)string {
return [_textContents string];
}
- (void)addAttribute:( NSString *)name value:(id)value range:( NSRange
)aRange {
[_textContents addAttribute:name value:value range:aRange]; //
cover bug listed on cocoadev
}
- ( NSDictionary *)attributesAtIndex:(unsigned)index
effectiveRange:(NSRangePointer)aRange {
return [_textContents attributesAtIndex:index
effectiveRange:aRange];
}
- (void)setAttributes:( NSDictionary *)attributes range:( NSRange
)aRange {
[[self textContents] setAttributes:attributes range:aRange];
[self edited:NSTextStorageEditedAttributes range:aRange
changeInLength:0];
}
- (void)replaceCharactersInRange:( NSRange )aRange withString:(
NSString *)aString {
NSUndoManager *undoManager = [self undoManager];
NSRange insertedRange = NSMakeRange (aRange.location, [aString
length]);
int origLen = [self length];
BOOL commitThisChangeToUndoStack = NO;
if ([undoManager isUndoing] || [undoManager isRedoing]) {
[_editInProgress release];
_editInProgress = nil;
commitThisChangeToUndoStack = YES;
} else {
[undoManager removeAllActionsWithTarget:_editInProgress];
// 1. try to merge changes with edit in progress
if (![_editInProgress merge:aString range:aRange]) {
// 2. commit current edit if can't merge
[_editInProgress commitEditToUndoStack];
[_editInProgress release];
_editInProgress = nil;
BOOL singleInsert = [HBTextStorageEdit
isSingleCharInsert:aString range:aRange];
BOOL singleDelete = [HBTextStorageEdit
isSingleCharDelete:aString range:aRange];
// 3. start new edit in progress
if (singleInsert || singleDelete) {
_editInProgress = [[HBTextStorageEdit alloc]
initWithString:aString range:aRange target:self];
[[undoManager
prepareWithInvocationTarget:_editInProgress] undoEdit];
// 4. commit edit
} else {
commitThisChangeToUndoStack = YES;
}
}
if (_editInProgress) {
[[undoManager prepareWithInvocationTarget:_editInProgress]
undoEdit];
[undoManager setActionName:NSLocalizedString(@"Typing",
@"")];
}
}
if (commitThisChangeToUndoStack) { NSString *replacedString =
[[self string] substringWithRange:aRange];
[[undoManager prepareWithInvocationTarget:self]
replaceCharactersInRange:insertedRange withString:replacedString];
[undoManager setActionName:NSLocalizedString(@"Typing", @"")];
}
[[self textContents] replaceCharactersInRange:aRange
withString:aString];
[self edited:NSTextStorageEditedCharacters range:aRange
changeInLength:[self length] - origLen];
}
@end
@interface HBTextStorageEdit : NSObject {
HBTextStorage *_target;
NSRange _editRange;
NSString *_replacedString;
}
+ (BOOL)isSingleCharInsert:( NSString *)edit range:( NSRange )range;
+ (BOOL)isSingleCharDelete:( NSString *)edit range:( NSRange )range;
- (id)initWithString:( NSString *)string range:( NSRange )range
target:(HBTextStorage *)target;
- (void)undoEdit;
- (void)commitEditToUndoStack;
- (BOOL)merge:( NSString *)edit range:( NSRange )range;
@end
@implementation HBTextStorageEdit
+ (BOOL)isSingleCharInsert:( NSString *)edit range:( NSRange )range {
return range.length == 0 && [edit length] == 1;
}
+ (BOOL)isSingleCharDelete:( NSString *)edit range:( NSRange )range {
return range.length == 1 && [edit length] == 0;
}
- (id)initWithString:( NSString *)string range:( NSRange )range
target:(HBTextStorage *)target {
if (self = [super init]) {
_target = target;
_editRange.location = range.location;
if (range.length == 0) {
_editRange.length = 1;
_replacedString = @"";
} else {
_editRange.length = 0;
_replacedString = [[[target string]
substringWithRange:range] retain];
}
}
return self;
}
- (void)dealloc {
[_replacedString release];
[super dealloc];
}
- (void)undoEdit {
[[_target undoManager] removeAllActionsWithTarget:self];
[_target replaceCharactersInRange:_editRange
withString:_replacedString];
}
- (void)commitEditToUndoStack {
NSUndoManager *undoManager = [_target undoManager];
[[_target undoManager] removeAllActionsWithTarget:self];
[[undoManager prepareWithInvocationTarget:_target]
replaceCharactersInRange:_editRange withString:_replacedString];
[undoManager setActionName:NSLocalizedString(@"Typing", @"")];
}
- (BOOL)merge:( NSString *)edit range:( NSRange )range {
int currentCursor = _editRange.location + _editRange.length;
BOOL singleInsert = range.location == currentCursor &&
[HBTextStorageEdit isSingleCharInsert:edit range:range];
BOOL singleDelete = range.location == currentCursor - 1 &&
[HBTextStorageEdit isSingleCharDelete:edit range:range];
if (singleInsert) {
_editRange.length++;
return YES;
} else if (singleDelete) {
if (_editRange.length > 0) {
_editRange.length--;
} else { NSString *temp = [[_target string]
substringWithRange:range];
[_replacedString autorelease];
_replacedString = [[temp
stringByAppendingString:_replacedString] retain];
_editRange.location--;
}
return YES;
}
return NO;
}
@end
Thanks for any help. Sorry for the long post but I think this code can
be useful to a number of people so it's worthwhile to get it working
correctly.
----------------------------------------------------------------
Jesse Grosjean / Hog Bay Software, Quality Mac Software
163 Westway RD #204
Greenbelt, MD 20770
email@hidden
http://www.hogbay.com/software
_______________________________________________
cocoa-dev mailing list | email@hidden
Help/Unsubscribe/Archives:
http://www.lists.apple.com/mailman/listinfo/cocoa-dev
Do not post admin requests to the list. They will be ignored.