NSUndoManager and replacing/skipping undo steps - objective-c

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.

Related

Correctly handling undo/redo with nested method calls in Objective-C?

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!

Commit NSTextField changes before saving

I have an NSDocument app and I would like to have a NSTextField to commit the current changes to the model every time the user save (through cmd+s for example).
I don't use binding and at the moment the changes are pushed to the model in the -controlTextDidEndEditing: method. Calling the [window makeFirstResponder:nil] does push the changes to the model but also causes the control to lose focus which is not really a reasonable behaviour.
Googling around I have seen that several people suggested to use the -commitEditing method but it only applies to bindings, am I wrong?
You can just call your already defined controlTextDidEndEditing: from your save action:
-(IBAction)save:(id)sender
{
[self controlTextDidEndEditing: ...]
}
to trigger the same code you already wrote!

switching views with grand central dispatch

I've looked at a lot of topics but I still can't figure it out.
I have a UITableview which downloads its content online. Each cell has an image, and I use GCD to let the image download. The downloaded image will be saved to disk, and before each time a cell is loaded there is checked if the file already exist, if not -> gcd, nsdata etc.
All goes well if someone has a good internet connection (wifi), but if I'm going to hop from View to View (back and forth), with my crappy 3G connection, what happens is that it wants to finish its queue (about 4 cells), but already gets assigned a new one, and a new one, and a new one and eventually the user has to wait a looong time before the others are executed (which he doesnt see) before the actual UITableview gets populated. With NSLog I can see that even I'm in a different view, it's still downloading and making uiimages that were visible on the screen. Each task is approximately 100 kb, and with a slow (or even no internet connection?!) it can take a while if you have a lot.
I know it's not possible to cancel it, but I read in other topics about using a BOOL variable but I don't really get it. Even if the BOOL variable change when the user leaves the screen, the cells are already in queue right?
Is it possible that when a user taps the back button in my Navigationcontroller, so he leaves the view, I change the data the blocks in queue use (empty it), so there is nothing to download and the blocks will be executed right away (there is nothing to do). So something like, making every value in array newsitems nil? Is it possible to change the datasource, or will the blocks that are waiting already have their datasource with them while waiting?
Then there is another problem, this doesn't have effect on the the currently executed block.
Can someone point me in a good direction?
Thank you.
Prastow
You can make use of NSBlockOperation and NSOperationQueue to create a cancellable download task. You create an NSBlockOperation by giving it a block which performs some work. In your case the block would download the contents of the URL.
In your view controller, you would store a list of the operations that have been submitted to the queue. If the user decides to leave the current view, you can then call cancel on each of the pending operations to prevent any needless work from taking place. The currently running operation will run to completion however. In order to cancel the currently running operation, you need to store a weak reference to the NSOperation object in the block doing teh work. Then at appropriate intervals within the body of the block, you can check to see if the operation has been cancelled and exit early.
// Create a queue on which to run the downloads
NSOperationQueue* queue = [NSOperationQueue new];
// Create an operation without any work to do
NSBlockOperation* downloadImageOperation = [NSBlockOperation new];
// Make a weak reference to the operation. This is used to check if the operation
// has been cancelled from within the block
__weak NSBlockOperation* operation = downloadImageOperation;
// The url from which to download the image
NSURL* imageURL = [NSURL URLWithString:#"http://www.someaddress.com/image.png"];
// Give the operation some work to do
[downloadImageOperation addExecutionBlock: ^() {
// Download the image
NSData* imageData = [NSData dataWithContentsOfURL:imageURL];
// Make sure the operation was not cancelled whilst the download was in progress
if (operation.isCancelled) {
return;
}
// Do something with the image
}];
// Schedule the download by adding the download operation to the queue
[queue addOperation:imageDownloadOperation];
// As necessary
// Cancel the operation if it is not already running
[imageDownloadOperation cancel];
A good talk on this exact topic was given at WWDC this year entitled "Building Concurrent User Interfaces on iOS". You can find the video and slides here
I faced similar issues with an app I developed a while back and found that the best way to do everything you require, and more, is to use https://github.com/MugunthKumar/MKNetworkKit
It took me the best part of a day to learn and understand the conversion and then a couple more days to tweak it to exactly what I needed.
If you do decide to use it or would like a thorough overview of the capabilities start here
http://blog.mugunthkumar.com/products/ios-framework-introducing-mknetworkkit/

Update screen in Cocoa/Objective C following button press

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.

Clearing NSUndoManager's Redo stack

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];