How to implement undo / redo with programmatically changing textValue NSTextView?

I created a simple demo application with NSTextView and button, provided NSTextViewDelegate for textView and added 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 the text manually. But if I change the text using the action method, the undo will work as expected, but the replay will no longer work (nothing happens), and the undo manager seems to be scrambled ...

OK - to avoid problems with NSTextView, I created a model class, linked an NSTextView to it and moved the undo / redo to the model, but it shows the same behavior as before - what I am doing wrong - it should be easy, right?

 #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 
+6
cocoa undo-redo nstextview
source share
3 answers

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

You just need to make sure that you only call higher-level methods from NSTextView, starting from the insert ... and not setting the text / line textView or textStorage directly:

  [self.textView insertText:newString]; 

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

 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 allows you to run undoManager from NSTextView. I think that undoManager prepares undoGrouping in shouldChangeTextInRange: and causes the cancellation in didChangeText :.

+7
source share

-setString: is an inherited method from NSText . To handle this using NSTextView methods only to handle the handle, simply do the following:

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

Changing the text in this way avoids smoothing with the undo manager.

+5
source share

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

 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 a single character (user hit Return key) let singleCharRange = (replacementString!.startIndex)! ..< (replacementString!.startIndex.successor())! if newLineRange == singleCharRange { self.breakUndoCoalescing() } } return true } 
+1
source share

All Articles