NSTextField edit coalescing - cocoa

I am writing a Cocoa app which contains several NSTextFields. I am trying to propagate changes to the text in the text field to my model layer, but since I am registering an Undo action when the model is updated, this makes it a bit more complicated than simply listening for controlTextDidChange:.
If I hook onto the delegate method controlTextDidChange:, then it gets called once for every key pressed. If I update the model then, the model will treat each separate keypress as a separate Undo event. This is not desirable. What I want is to be notified when the default "Undo Typing" action is registered with the Undo manager, so I can update the model then. The NSTextField registers one (and only one) such event, when I pause typing for a moment. Unfortunately, I have no idea when this occurs.
I have tried listening for controlTextDidEndEditing:, and updating the model in that delegate method, but then I have to explicitly tab out of the text field before the model will update. Since I archive my model object directly in my NSDocument subclass, doing this will result in data loss for the user if they save the document after changing the contents of the text field, but don't tab out of the text field first, because the model would not have been updated in time. Therefore, this technique will not work.
I have also tried hooking onto the text field's field editor (during control:textShouldBeginEditing:) and listening for textDidEndEditing:, but then I find that this simply doesn't fire, so the model won't be updated, causing data loss as described in the previous paragraph. Can anyone help me figure this out? Thanks!

By default, Apple NSTextField leaves dangling edits as you mentioned.
Because of this problem, amongst others, I subclassed NSTextField and NSApplication to handle text field editing the way it should be handled: Pressing 'Enter', clicking outside the field, and other actions should end editing and produce the Undo entry.
Also, I highly recommend GCUndoManager replacement for NSUndoManager.

Related

Manage Undo across multiple NSTextFields in the same window

While Undo for multiple NSTextViews in a window work persistently, the same is not the case for NSTextField controls by default.
I like to make Undo behave the same for NSTextField as it works with NSTextView.
I have so far found out that NSTextField uses its own private NSUndoManager, which does enable and handle the Undo/Redo menus autmatically, but without using the window's shared undoManager, and without marking the connected document dirty ("edited"): If I provide a private NSUndoManager instance to an NSTextField, it'll behave the same.
So I've tried, by overwriting undoManagerForTextView:, to have the NSTextFields in the window use the window's shared undoManager. That will indeed lead to the effect that the text fields share the same undo stack, and I can still issue an Undo after editing and then leaving a specific text field.
However, the problem is that it'll then crash, because it appears that the Undo gets applied to the wrong (i.e. the currently active) text field and not to the one the undo action was created from.
I've provided a simple example here: https://github.com/tempelmann/Undo_shared_NSTextFields
It's a freshly created document based app, with two text fields and two text views in the main window. To enable the shared undo manager for the text fields, I've created a subclass ("CustomTextField") of NSTextField that overrides the following function to provide the shared undo manager:
-(NSUndoManager *)undoManagerForTextView:(NSTextView *)view { // NSTextViewDelegate
return view.window.undoManager;
}
And the text fields in the storyboard are set to this subclass instead of NSTextField.
When run, it looks like this:
Now, if you edit the first and the second text view, and then use Undo twice, it'll undo the changes in each text view just fine.
But if you try the same with the two text fields, e.g. by replacing the longer preset texts in the fields with something shorter (such as "1" and "2"), you'll find that Undo replaces the text in the same active text field twice:
How do I make Undo in the text fields behave correctly, i.e. the same as the text views do?

Trying to see the delegate of an NSTextView of an NSCell

I am having a devil of a time trying to figure out how to get the address of the Text Field Editor (NSTextView) of an NSCell—NSFormCell and NSTextFieldCell in particular? NSCell does not have a property to access it. I did figure out the editor is not allocated until one is actually editing the field.
I want to set the delegate so I can capture keystrokes for auto-completion.
By default, there's a single field editor for each window. Even if a control or cell uses a custom field editor, it's still vended by the window. You would call -[NSWindow fieldEditor:forObject:] to obtain the field editor for a given control.
However, the delegate of the field editor is always set to the control on whose behalf it is working. Setting the delegate to something else is likely to break things. So, you would typically use a custom subclass of the control and implement your delegate methods there.
Finally, controlling completions is normally done using -textView:completions:forPartialWordRange:indexOfSelectedItem: in the text view delegate, not by capturing keystrokes.

Custom UITableViewCell with UITextField editingDidEnd race condition

I have UITableViewController with Edit/Save button as BarItem. I have a custom cell which displays UITextField in edit mode. I am able to save modified text when I move focus to another cell etc. no problem. However my problem begins when I attempt to modify a field and press Save button that triggers setEditing:NO save etc. What I believe is happening is a race condition where Save action is triggered before editingDidEnd is processed and as a result I am not saving all of the data.
Any suggestions on how to handle this? Am I to go through all visible cells to save all of the data on save? I can definitely do that, but am I going to get into same problem with scroll and edit button clicked? Is there a better way to flush messaging queue?
Given that I have no takers, I think I am stuck with what I know:
in done/save (i.e. setEditing:NO) handling go through all cells and save their data
Keep updating data every time there is a change i.e. in Value Changed perhaps
attempt using UITextFieldDelegate and textFieldShouldEndEditing but I am not sure this would work since I will probably run into the same problem that I have.
I believe endEditing inside setEditing will solve the problem as it supposed to resign first responders for text fields.

Cocoa bindings only update when window focus changes

I am using MonoMac to build a desktop download manager for Mac in C#.
My XIB has a Table View, whose columns are bound to an NSArrayController. The array controller is connected to my Main Window Controller through an IBOutlet. The array holds a bunch of HttpDownload objects, which derive from NSObject. These HttpDownload objects contain properties such as TotalSize, TotalDownloaded, Bandwidth, etc. I have decorated these properties with an [Export] attribute.
In the controller I add some HttpDownload objects to the NSArrayController using the AddObject method. A background process, started with Task.Factory.StartNew() begins the download asynchronously and updates the bound properties such as TotalDownloaded and Bandwidth as data is received.
I can see these new values being reflected in the Table View, but only once I've "forced" a UI update, for instance by causing the window to lose focus, gain focus, or by clicking on a button within the window.
I have tried setting Continuously Updates Value in IB, but this makes no difference (and reading the docs, I didn't think it should).
Does anyone know to make the UI update the bound values in "real time", instead of only when a window event occurs?
I figured this out shortly after I posted this question.
It seems that we need to manually call WillChangeValue() and DidChangeValue() for at least one of the keys that are being updated, for instance, when I updated the total downloaded:
WillChangeValue("DownloadedBytes");
DownloadedBytes += bytesRead;
DidChangeValue("DownloadedBytes");
In my case, calling these methods for just one of the updated keys seems to be enough to force an update of all the bound values.
For reference, in Objective-C these selectors are called [self willChangeValueForKey:#"keyname"] and [self didChangeValueForKey:#"keyname"].

How to get notifications of NSView isHidden changes?

I am building a Cocoa desktop application. I want to know when a NSView's isHidden status has changed. So far using target/action doesn't help, and I can't find anything in NSNotification for this task. I would like to avoid overriding the setHidden method, because then I'll have to override all the NSView derived class that I am using.
UPDATE: I ended up using KVO. The path for "isHidden" is "hidden", probably because the setter is "setHidden".
You could use Key-Value Observing to observe the isHidden property of the NSView(s). When you receive a change notification from one of these views, you can check if it or one of its superviews is hidden with -isHiddenOrHasHiddenAncestor.
A word of warning: getting Key-Value Observing right is slightly tricky. I would highly recommend reading this post by Michael Ash, or using the -[NSObject gtm_addObserver:forKeyPath:selector:userInfo:options] method from the NSObject+KeyValueObserving category from the Google Toolbox for Mac.
More generally, one can override viewWillMoveToWindow: or the other related methods in NSView to tell when a view will actually be showing (i.e. it's window is in the window display list AND the view is not hidden). Thus the dependency on KVO for the 'hidden' key used above is removed, which only works if setIsHidden has been called on that view. In the override, 'window' (or [self window]) will indicate whether the view is being put into a visible view hierarchy (window is non-nil) or being taken out of it (window is nil).
I use it for example to start/stop a timer to update a control from online data periodically - when I only want to update while the control is visible.
Could you override the setter method for the hidden property so that it will trigger some custom notification within your application?

Resources