Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to implement undo/redo with programatic change of the textValue of a NSTextView?

I created a simple demo app with a NSTextView and a button, the provided a NSTextViewDelegate to the textView and added an action:

- (IBAction)actionButtonClicked:(id)sender {
    NSString *oldText = [[[self.textView textStorage] string] copy];
    NSString *newText = @"And... ACTION!";

    [[self.textView undoManager] registerUndoWithTarget:self.textView
                                               selector:@selector(setString:)
                                                 object:oldText];
    [[self.textView undoManager] setActionName:@"ACTION"];

    [self.textView setString:newText];
}

Undo/redo works without problems, if I change text by hand. But if I change the text with the action method, undo works as expected, but redo does not work anymore (nothing happens) and the undo manager seems to be scrambled...

OK - to avoid problems with NSTextView I created a model class, bound the NSTextView to it and moved the undo/redo to the model, but this shows the same behavior as before - what the I'm doing wrong - this should be easy, shouldn't it?

#import "GFTextStore.h"

@implementation GFTextStore

@synthesize textVal = textVal_;

-(void)changeText{
    if (!undoMan_) {
        undoMan_ = [[[NSApplication sharedApplication] mainWindow] undoManager];   
    }

    NSAttributedString *oldText = [self.textVal copy];
    NSString *tempStr = [[oldText string] stringByAppendingFormat:@"\n%@",[[NSCalendarDate date]description]];
    NSAttributedString *newText = [[NSAttributedString alloc] initWithString:tempStr];

    [self setTextVal:newText];


    [undoMan_ registerUndoWithTarget:self
                            selector:@selector(setTextVal:)
                              object:oldText];

    [undoMan_ setActionName:@"ACTION"];
}
@end
like image 892
user808667 Avatar asked Nov 24 '11 21:11

user808667


3 Answers

No need to add the NSUndoManager here, just let the NSTextView do the job.

You just need to make sure you are calling the higher level methods only of NSTextView beginning with insert… and not setting the text/string of the textView or the textStorage directly:

    [self.textView insertText:newString];

If you absolutely need to use setString or other lower level methods, then you just need to add the required methods handling the textDidChange delegation: -shouldChangeTextInRange:replacementString and -didChangeText (which is done by the insert... methods btw):

if( [self.textView shouldChangeTextInRange:editedRange replacementString:editedString]) {
    // do some fancy stuff here…
    [self.textView.textStorage replaceCharactersInRange:editedRange
                          withAttributedString:myFancyNewString];
    // … and finish the editing with
    [self.textView didChangeText];
}

This automatically lets the undoManager of NSTextView kick in. I think the undoManager is preparing an undoGrouping in shouldChangeTextInRange: and invoking the undo in didChangeText:.

like image 170
auco Avatar answered Oct 16 '22 12:10

auco


-setString: is an inherited method from NSText. To handle this using NSTextView methods only so that undo is handled, just do this:

[self.textView setSelectedRange:NSMakeRange(0, [[self.textView textStorage] length])];
[self.textView insertText:@"And… ACTION!"];

Making the text change this way avoids mucking about with the undo manager at all.

like image 35
Rob Keniger Avatar answered Oct 16 '22 12:10

Rob Keniger


Let's say you want your NSTextView to create a new undo group when user hits the Enter key (Apple Pages behavior). Then you might type this code in your NSTextView subclass:

override func shouldChangeTextInRange(affectedCharRange: NSRange, replacementString: String?) -> Bool {
    super.shouldChangeTextInRange(affectedCharRange, replacementString: replacementString)
    guard replacementString != nil else { return true }

    let newLineSet = NSCharacterSet.newlineCharacterSet()
    if let newLineRange = replacementString!.rangeOfCharacterFromSet(newLineSet) {

        // check whether it's a single character (user hit Return key)
        let singleCharRange = (replacementString!.startIndex)! ..< (replacementString!.startIndex.successor())!
        if newLineRange == singleCharRange {
            self.breakUndoCoalescing()
        }
    }

    return true
}
like image 30
Vitalii Vashchenko Avatar answered Oct 16 '22 14:10

Vitalii Vashchenko