In my application, there are some actions I want to undo programmatically, without giving the user the option of clicking "Redo". Is there any way to clear the Redo stack of NSUndoManager? If not, and I were to subclass NSUndoManager, is there any way to get access to the redo stack in order to clear it? I didn't see any way to from the documentation.
Alternately, is there a way to revert the changes from the current nested undo group without it populating the Redo stack? I'm already building a nested undo group.
I ended up taking a 2-step approach. The first step was to create a dummy undo item, which clears the Redo stack. Then, I just had to remove that undo item, and both stacks are clean.
I was able to use self as the dummy undo target, since I don't have any actual undo actions associated with the class containing the code. self could be replaced with any object that doesn't contribute to the Undo stack.
The trick was calling removeAllActionsWithTarget with a delay, otherwise it doesn't have an effect.
// End the open undo grouping
[undoManager endUndoGrouping];
// Perform the undo operation, which gets pushed onto the Redo stack
[undoManager undo];
// Add a dummy Undo item to clear the Redo stack
[undoManager registerUndoWithTarget:self selector:nil object:nil];
// Remove the dummy item with a delay, pushing it to the next run loop cycle
[undoManager performSelector:#selector(removeAllActionsWithTarget:)
withObject:self
afterDelay:0.0];
[undoManager disableUndoRegistration];
[undoManager undo];
[undoManager enableUndoRegistration];
Related
Is there any way to tell undoManager that in some case the undo step has already been committed before the edit has happened?
I have an application which automatically replaces certain strings with uppercase counterparts. Trouble is, this has to be done automatically into textStorage, so when undoing the edits, text remains uppercase.
I've already written custom undo states for the operations in question, which are created in NSTextView shouldChangeTextInRange:
For example:
[[self.undoManager prepareWithInvocationTarget:self]
replaceCharactersInRange:
NSMakeRange(affectedCharRange.location + affectedCharRange.length, string.length + 1) withString:string
];
I'd like the undoManager to ignore the undo step it's going to receive after the edit.
One approach is disabling NSUndoManager from logging undo steps by using [self.undoManager disableUndoRegistration];. Then, after the operation is done, you can enable it again with [self.undoManager enableUndoRegistration].
This seems to be a bit dangerous approach because it can really mess up you undo stack.
I fixed this issue by making an extra undo step before the NSTextView undo gets registered:
[self.undoManager registerUndoWithTarget:self handler:^(id _Nonnull target) {
[self replaceCharactersInRange:undoStringRange withString:undoString];
}];
Redoing can cause harm here, though, because the whole affected string might not get registered with text view's internal methods.
I couldn't think of a better way to ask this question, so I apologize if the title makes no sense. Basically, I'm implementing Undo/Redo at the controller-level for a portion of my app, and I keep coming across this niggling issue. I have two methods:
- (void)addNote:(BSDNote *)note;
- (void)duplicateNote:(BSDNote *)note;
In the -addNote: method, a note object is added to a mutable array. I also update some other stuff, but for the purpose of my example, let's just say an object is added to an array. Before adding the object to the array however, I register an Undo operation with the document's NSUndoManager, like so:
if ( [self.undoManager levelsOfUndo] > 0 ) {
[self.undoManager endUndoGrouping];
[self.undoManager beginUndoGrouping];
}
[self.undoManager registerUndoWithTarget:self selector:#selector(deleteNote:) object:note];
[self.undoManager setActionName:NSLocalizedString(#"Add Note", #"Add Note undo action name")];
By itself, it makes sense, and everything's all good. The thing I'm unsure of is when calling the -addNote: method from within my -duplicateNote: method.
In the -duplicateNote: method, the note object is copied, then added to the mutable array using the -addNote: method. Since I'm duplicating the object, I want to add an undo operation to the stack for -duplicateNote:, not -addNote:.
The only thing I can think of is to do something like this from within the -duplicateNote: method:
[self.undoManager registerUndoWithTarget:self selector:#selector(deleteNote:) object:note];
[self.undoManager setActionName:NSLocalizedString(#"Duplicate Note", #"Duplicate Note undo action name")];
// I make a copy of the note here (noteCopy), along with other miscellaneous stuff.
[self.undoManager disableUndoRegistration];
[self addNote:noteCopy];
[self.undoManager enableUndoRegistration];
Am I crazy in thinking this is an okay way to handle this situation? The entire "grouping" of undo/redo operations kind of makes my head hurt, so I'm sure I'm overlooking something, or there is a Better Way®. If someone knows of this better way, would you mind explaining it to me (like I'm five)?
Ben, I think you're overthinking it. If the last instance created is a "duplicate note", then it would make sense to add an "undo" to the stack when the duplicateNote method executes.
Good luck!
My AppDelegate maintains a list of active window controllers to avoid ARC deallocating them too early. So I have a notification handler like this:
- (void) windowWillClose: (NSNotification*) notification {
[self performSelectorOnMainThread: #selector(removeWindowControllerInMainThread:)
withObject: windowController
waitUntilDone: NO];
}
- (void) removeWindowControllerInMainThread: (id) windowController {
[windowControllers removeObject: windowController];
}
I use the main thread because doing the handling on the notification thread risks deallocating the controller before it's ready.
Now, this works pretty well — except when there are animators currently running. I use animators in some places, through NSAnimationContext. I have looked at this QA, and the answer just isn't acceptable. Waiting for a while, just to get animation done, is really shoddy and not guaranteed to work; indeed it doesn't. I tried using performSelector:withObject:afterDelay, even with a larger delay than the current animation duration, and it still results in the animator running against nil objects.
What is the preferred way of doing controller cleanup like this? Not use NSAnimationContext but using NSAnimation instead, which has a stopAnimation method?
First, if some of your animations run indefinitely -- or for a very long time -- you're going to have to have a way to stop them.
But for things like implicit animations on views, you could simply use a completion method.
self.animating=YES;
[NSAnimationContext runAnimationGroup:^(NSAnimationContext *context){
[[v animator] setAlphaValue: 1];
} completionHandler:^{
self.animating=NO;
}];
Now, you only need to poll whether your animation is running and, if it's not running, proceed to close your window.
One nice way to do the polling is to set a timer with a fixed delay. If the animation is still running, just reset the timer and wait another interval.
Alternatively, you could send a notificaton from the completion handler.
I haven't used NSAnimationContext (always did this with NSAnimation, but mostly for historical reasons). But the typical way I like to managed things similar to this is to create short-lived retain loops.
Mark's answer is exactly the right kind of idea, but the polling is not required. The fact that you reference self in the completion handler means that self cannot deallocate prior to the completion handler running. It doesn't actually matter whether you ever read animating. ARC has to keep you around until the completion block runs because the block made a reference to you.
Another similar technique is to attach yourself to the animation context using objc_setAssociatedObject. This will retain you until the completion block runs. In the completion block, remove self as an associated object, and then you'll be free to deallocate. The nice thing about that approach is that it doesn't require a bogus extra property like animating.
And of course the final, desperate measure that is occasionally appropriate is to create short-lived self-references. For instance:
- (void)setImmortal:(BOOL)imortal {
if (immortal) {
_immortalReference = self;
}
else {
_immortalReference = nil;
}
}
I'm not advocating this last option. But it's good to know that it exists, and more importantly to know why it works.
I've run into this problem a couple of times and want to know the correct approach to take.
For an example, let's say I'm writing an iPhone app and I want a custom alert view class that uses blocks.
So I write the class, then later on in my code I go:
MyAlertView *alert = [MyAlertView alertWithBlahBlahBlah...];
[alert addButton:#"button" withBlock:^{ ... }];
[alert show];
Somewhere in the alert view class, we have
- (void)addButton:(NSString *)button withBlock:(void (^))block {
[_blocks setObject:[block copy] forKey:button];
}
- (void)show {
... drawing stuff ...
UIButton *button = ...
[button addTarget:self selector:#selector(buttonPressed:) ...];
...
}
- (void)buttonPressed:(id)sender {
((void (^)())[_blocks objectForKey:[sender title]])();
}
So, the alert view now shows up just fine. The problem is, if I tap a button, it attempts to send the buttonPressed: selector to the MyAlertView object that was displayed. The MyAlertView has, however, been removed from the superview at this time. ARC decides that because the alert view is not owned by anyone anymore, it should be deallocated, not knowing that a button needs to message it in the future. This causes a crash when the button is tapped.
What's the right way to keep the alert view in memory? I could make the MyAlertView object a property of the class that's using it, but that's kind of silly (what if I want to show two alerts at once?).
If an object were to remain in memory, and you do not have a reference to it, this is known as a memory leak. As I said in my comments, you need to keep some kind of reference to it so that a) it is not deallocated, b) you can send a message to it, and c) you can deallocate it before your class is deallocated.
The most obvious way to do this would be with a property in your class. Since you said that you don't want to do that (maybe you have a lot of them) then another possible solution would be to keep an array of cached objects that you plan on reusing and eventually deallocating.
I think you can use performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay to retain the alert view in runloop.
Actually, I just came across an wrapper implementation for UIAlertView using this skill.
Check UIAlertView input wrapper for more detail.
Quite simply, you're breaking the memory management rules. ARC doesn't change the rules, it just automates them. If you need an object to stay alive, it needs to have an owner. Every object in your app's object graph, all the way back to the application delegate, has an owner. It may not be obvious what that owner is (and sometimes the owner may be an autorelease pool), but there is one.
If you want this view to stick around, it needs to be owned by something, even if it's not "currently being used". If it's onscreen, it should be part of the view hierarchy. If it's not, the ideal owner is likely to be the object that created it.
Newbie Objective C/Cocoa question: I have an application with some data entry fields and a "do it" button. When the button is pressed, some computation takes place and output data is displayed in a table view and some text fields in the same window. What I'd like is that when the button is pressed that the text fields and the table view are both cleared while the computation takes place.
I've tried making the appropriate calls as the first few statements of the action routine for the button press, but that doesn't work. I would imagine that the runtimes don't get called to do the screen update until after my action routine is finished.
Is there a simple way to do what I want to do? Thanks.
You imagine correctly.
The usual way to do this sort of thing is to use NSObject's performSelectorInBackground:withObject: to start the heavy calculation in the background. Then once the background code finishes doing its work, use performSelectorOnMainThread:withObject:waitUntilDone: to call another selector on the main thread to update the UI (remember, UI calls may only be done from the main thread).
You're correct about the screen updates not taking place until after your routine finishes. Most drawing to the screen is queued to improve performance.
When you change the value in an NSTextField, it knows to call [self setNeedsDisplay:YES] in order to queue its need for redrawing. If you want to force it to display, you can call [textField display]. (Note that calling [textField setNeedsDisplay:YES] will not cause immediate display). Things get a bit more difficult with an NSTableView, as this -display method is unlikely to work for it.
While you could create a secondary thread to do your processing, that would create a lot of complexity that may not be worth it. You might consider using -performSelector:withObject:afterDelay: to begin your processing routine rather than calling it directly.
- (IBAction)buttonClicked:(id)sender {
[textField setStringValue:#""];
[tableView reloadData];
// instead of doing the following:
// [self processData:nil];
// do
[self performSelector:#selector(processData:) withObject:nil afterDelay:0.0];
}
- (void)processData:(id)sender {
// process the data
[textField setStringValue:#"the results"];
[tableView reloadData];
}
Using -performSelector:withObject:afterDelay: is different than calling the method directly, as it causes the method to be called not immediately, but scheduled to be called "ASAP". In many cases, your app will be able to squeeze in the updates to the UI before it can get to performing that computation method. If testing reveals this to be the case, then you can avoid having to go to the trouble of creating a secondary thread to do the processing.
If you want to force updating screen then call setNeedsDisplay from your UIView.
I would imagine that the runtimes
don't get called to do the screen
update until after my action routine
is finished.
Bingo. Your button's action method is called on the main thread, which is the same thread that is responsible for updating the user-interface. So the interface will not update until after your action method returns.
To get around this, you can split your action method into two parts. The first part makes the calls to clear your previous view and set whatever new state you want to use for rendering. The second part does the new calculations, and is moved to its own method. Then, at the end of the first part, add something roughly like:
[self performSelectorInBackground:#selector(myActionSecondPart) withObject:nil];
...to run the computation part in the background. Then your UI will update while the computation runs.