I have implemented a NSTokenField which uses some custom data objects for display of dynamic data. Delegate is set up fine and displays the correct values of the token.
I've also implemented a menu on the tokens which allows selection of the format used for display of each token. My problem is however that I'm unable to make the NSTokenField respond immediately to these changes and redraw the token with the newly selected format.
If I click outside of the NSTokenField so it resigns first responder it redraws immediately. I can also do this programmatically by explicitly setting first responder to nil. The only problem with this is that the NSTokenField looses focus - and reassigning it as first responder selects everything in the field so the user may accidentally overwrite the entire content.
So my question is whether there are any way of just triggering the NSTokenField to redraw its content without changing focus and selection?
I had the same problem and found the only workable solution was to "reset" the token field every time its contents changed. Basically this boiled down to running the following method of the view controller that handled the view with the token field:
- (void) resetTokenField {
// Force the token field to redraw itself by resetting the represented object.
id anObject = [self representedObject];
[self setRepresentedObject: nil];
[self setRepresentedObject: anObject];
[[self tokenField] setNeedsDisplay: YES];
}
The represented object is the object that holds all the tokens that are being displayed. In my case that was a chunk of richt text where tokens are basically tags.
I found NSTokenField to be a royal pain in the neck but persevered as I quite like the presentation side of it.
EDIT: My token field was bound to the tagsAsArray method of the represented object of the view controller. So basically I used Cocoa bindings to solve the problem.
It turns out that you are supposed to call setNeedsDisplay on the NSTextView object used by the NSTokenField, instead of calling setNeedsDisplay on the NSTokenField itself.
Thus the following code works: (Written in PyObjC)
text_view = token_field.cell().fieldEditorForView_(token_field)
text_view.setNeedsDisplay_(YES)
Related
I have an NSTextField UI element where the user can type into the text field and I want to drop down a list of completions beneath the text field as a "live search".
I was hoping to use the native text completions infrastructure, but when the user chooses the appropriate completion, I don't want to merely put the text into the NSTextField. The user is actually choosing one of many custom objects in an NSArray by searching on string properties of the object. When they choose, I need to know which object they chose.
Is there a way to know the index of the completion that was chosen (so that I can get the object from that index in my array)?
Or do I need to forget about using the native text completions and just populate and display a dropdown under the text field?
Could you use an NSComboBox in this situation? And perhaps subclass NSComboBoxCell to override
- (NSString *)completedString:(NSString *)substring
You could also observe changes in the NSComboBox delegate protocol to detect changes to the selected item
In the end I used an NSTokenField because of some UI appearance things that NSTokenField added for me. But I think that the extra trick I came up with (below) might also work with an NSTextField. Sorry this is kind of convoluted.
In a nutshell what I did was to generate an NSMutableDictionary (an iVar) where the keys are the full completions for the partial string in the NSTokenField and the objects are the custom objects that the completion strings represent. In other words, as I am generating the custom completion strings and putting them into an NSArray to be returned from the NSTokenFieldDelegate method tokenField:completionsForSubstring:indexOfToken:indexOfSelectedItem:, I am at the same time stuffing each of those completions and the object they represent into an NSMutableDictionary with the completion as key and the object as value.
When the user "tokenizes" the completion (by hitting Return or Tab -- i modified the tokenizing characterSet so that's all that will tokenize), the NSTokenFieldDelegate method tokenField:representedObjectForEditingString: is called. Inside there, I am able to get my object from the NSMutableDictionary by using the editingString parameter as the key: [dict objectForKey:editingString]
I think it might be possible with some wrangling in the controlTextDidChange: NSTextFieldDelegate method to do the same thing with completions on an NSTextField instead of an NSTokenField using the dictionary trick, but in order to do that, I think that you would have to have the full completion in the NSTextField, grab its stringValue and then use that as the key. In my case, I did not want the whole completion in the text field, so NSTokenField's tokenizing worked better for me.
I have several panels that contain NSTextField controls bound to properties within the File's Owner object. If the user edits a field and then presses Tab, to move to the next field, it works as expected. However if the user doesn't press Tab and just presses the OK button, the new value is not set in the File's Owner object.
In order to workaround this I have set Updates Continuously in the binding, but this must be expensive (EDIT: or at least it's inelegant).
Is there a way to force the bind update when the OK button is pressed rather than using Updates Continuously?
You're right that you don't need to use the continuously updates value option.
If you're using bindings (which you are), then what you should be doing is calling the -commitEditing method of the NSController subclass that's managing the binding. You'd normally do this in your method that closes the sheet that you're displaying.
-commitEditing tells the controller to finish editing in the active control and commit the current edits to the bound object.
It's a good idea to call this whenever you are performing a persistence operation such as a save.
The solution to this is to 'end editing' in the action method that gets called by the OK button. As the pane is a subclass of NSWindowController, the NSWindow is easily accessible, however in your code you might have to get the NSWindow via a control you have bound to the controller; for example NSWindow *window = [_someControl window].
Below is the implementation of my okPressed action method.
In summary I believe this is a better solution to setting Updated Continuously in the bound controls.
- (IBAction)okPressed:(id)sender
{
NSWindow *window = [self window];
BOOL editingEnded = [window makeFirstResponder:window];
if (!editingEnded)
{
logwrn(#"Unable to end editing");
return;
}
if (_delegateRespondsToEditComplete)
{
[_delegate detailsEditComplete:&_mydetails];
}
}
Although this is really old, I absolutely disagree with the assumption that this question is based on.
Countinously updating the binding is absolutely not expensive. I guess you might think this updates the value continuously, understanding as "regularly based on some interval".
But this is not true. This just means it updates whenever you change the bound value. This means, when you type something in a textView, it would update as you write; this is what you'd want in this situation.
When a user adds a new managed object, it shows up in a table, which scrolls down to the new entry, and the name of the new object (a default value) goes into editing mode.
I need to check if the name of the new object is unique in the datastore, so I can't use a formatter for this. I think the perfect moment where I should validate this, is whenever the user tries to commit the entry's name value, using textShouldEndEditing:.
I subclassed NSTableView and overrid following methods, just to be able to check in the log if they get called.
- (BOOL)textShouldEndEditing:(NSText *)textObject {
NSLog(#"textSHOULDendEditing fired in MyTableView");
return [super textShouldEndEditing:textObject];
}
- (BOOL)control:(NSControl *)control textShouldEndEditing:(NSText *)fieldEditor {
NSLog(#"control:textShouldEndEditing fired in MyTableView");
return YES;
}
- (void)textDidEndEditing:(NSNotification *)aNotification {
NSLog(#"textDIDEndEditing fired in MyTableView");
}
textDidEndEditing: gets called fine, but textShouldEndEditing: does not.
In the NSTableView Class Reference, under Text Delegate Methods, both methods textShouldEndEditing: and textDidEndEditing: are listed. Someone please explain why one gets called and the other doesn't.
I think the NSTableView acts as the delegate for an NSTextField that gets instantiated as a black box delegate for the NSTextFieldCell. So what is referred to as delegate methods in the NSTableView Class Reference, actually implement the text manipulating methods for the NSTextField object.
I tried to declare the NSTextFieldCell as an outlet in my NSTableView. I also tried to declare several protocols in the NSTableView.
#import <AppKit/AppKit.h>
#import <Cocoa/Cocoa.h>
#interface MyTableView : NSTableView <NSTextDelegate, NSTextFieldDelegate, NSControlTextEditingDelegate, NSTableViewDelegate, NSTableViewDataSource> {
}
#end
Don't laugh, I even tried to declare my table view as its own delegate :P
After banging my head one entire day on this issue without finding any conclusive answer in Apple documentation, I decided to share the solution I've found in case somebody else struggles with the same problem.
According to the documentation, as the original poster mentioned, the methods control:textShouldBeginEditing and control:textShouldEndEditing of NSControlTextEditingDelegate should be called directly on the delegate:
This message is sent by the control directly to its delegate object.
Furthermore, a Technical Q&A was issued by Apple with the title Detecting the start and end edit sessions of a cell in NSTableView where it's clearly stated the following:
A: How do I detect start and end edit sessions of a cell in NSTableView?
In order to detect when a user is about to start and end an edit session of a cell in NSTableView, you need to be set as the delegate of that table and implement the following NSControl delegate methods:
- (BOOL)control:(NSControl *)control textShouldBeginEditing:(NSText *)fieldEditor;
- (BOOL)control:(NSControl *)control textShouldEndEditing:(NSText *)fieldEditor;
The table forwards the delegate message it is getting from the text view on to your delegate object using the control:textShouldEndEditing: method. This way your delegate can be informed of which control the text view field editor is acting on its behalf.
I found nothing in Apple's documentation stating anything different and if someone does, a documentation pointer would really be appreciated.
In fact, this appears to be true if a cell-based NSTableView is being used. But as soon as you change the table to a view-based table, the delegate method is not called any longer on the table delegate object.
A Solution
However, some heuristic tests I performed showed that those delegate methods get called on a view-based table delegate if (and as far as I know: and only if):
The table delegate is set.
The delegate of the editable control is set.
If you remove either delegate, the methods of the NSControlTextEditingDelegate protocol will not be called.
What's unexpected according to the (only) documentation is setting the delegate of the editable control. On the other hand setting the delegate object to receive delegate notifications sounds rather intuitive to me, and that's why I tried in the first place. But there's a catch! The curious thing, though, is that that's not sufficient. If the table delegate is removed, the NSControlTextEditingDelegate methods will not be called even if the delegate of the editable control is set (which is the weirdest thing to me).
Hope this helps somebody else not to lose time on this issue.
in your question you mention the insertion of a "managed object" and that was the problem. It seems that you are using a view based table, but the textShouldEndEditing: method only gets called for cell based tables.
I overrid -(void)awakeFromInsert; in the (subclassed) managed object, to construct a unique default value for the name-property.
Also, I ended up not overriding the -(BOOL)textShouldEndEditing: method in the table view. Instead, I check if a newly entered name-property is unique in the (subclassed) managed object's -(BOOL)validate<Key>:error:.
Together, the above two strategies result in unique name-properties in all managed objects.
Maybe I could have forced the NSTextFieldCell to go into editing mode, resulting in -(BOOL)textShouldEndEditing: to get called every time.
Some remarks though:
It seems -(BOOL)textShouldEndEditing: returns NO when the -(BOOL)validate<Key>:error: returns NO.
Both -(BOOL)textShouldEndEditing: and -(BOOL)validate<Key>:error: methods are called only when the user actually makes changes to the property.
I have a Core Data project.
Basically I have an NSTableView where I add some entities (using the "add:" selector), double clicking on the TableView opens a new NSWindow where is possible to edit the entity using some NSTextFields.
Each text field is binded to an attribute of the entity.
Everything is working fine, except the fact that the entity's attributes are updated only when a textfield lose the focus.
If I write on the first text field and then I move to the second one my entry is saved, but if I write on the first text field and I close the window I lose my changes.
How can I update my core data entity as soon as I write something in the text field? Should I use textDidChange:?
--- UPDATE ---
Unfortunately [context save] doesn't work. If I understand correctly the entity is not modified until the NSTextField resign first responder.
The only working solution for now is something like:
(void)controlTextDidChange:(NSNotification *)aNotification
{
NSTextField *tf = [aNotification object];
[self.window makeFirstResponder:tf];
}
but this is quite inelegant, and in any case I also still need to re-set the cursor at the end of the NSTextField.
Setting NSContinuouslyUpdatesValueBindingOption will cause the model to update every time the text field changes, which sets the dirty flag properly and causes the document to save on quit.
I think you could use DidEndEditing or TextDidChange, another way of doing this is handeling in the window close event, but I would not recommend it.
If you don't have one already, you can set a delegate on the window and use -windowWillClose: or observe the NSWindowWillCloseNotification. You can then call [[notification object] makeFirstResponder:[window initialFirstResponder]] to set the window's first responder to its initial first responder as the window is closing. This will cause the control that is first responder (e.g. NSTextField) to resign the first responder status and the binding will save the changes.
My app allows users to attach tags to certain model objects (subclasses of NSManagedObject). The Tag class is also a subclass of NSManagedObject. I decided to use NSTokenField to display the tags, where each token holds an instance of Tag as the represented object. It all works pretty good but I'm stuck in situations where the user deletes a token as I want to check whether the associated Tag has become obsolete and should be deleted.
I was expecting a method in NSTokenFieldDelegate or NSTokenFieldCellDelegate which would allow me to intercept and check a delete action on a token.
After some googling I found this post addressing the topic. I implemented the suggested method controlTextDidChange: in my controller (the delegate of the token field). Upon inspecting the control that is passed as an argument, it revealed to be an instance of NSTokenTextView for which I cannot find any documentation (probably a private class).
Has anybody run into this and found a solution to gracefully delete tokens while maintaining the underlying model of represented objects?
EDIT
I found this as well, which seems to suggest that for some reason it just is not designed to work like the rest of us would expect.
You should be able to simulate a delete delegate by creating a token wrapper class that has a pointer back to the owner as well as the wrapped object:
#protocol TokenWrapperDelegate
-(void)tokenWasDeleted:(id)token;
#end
#interface TokenWrapper : NSObject {
id<TokenWrapperDelegate> owner;
id token;
}
-(id)initWithWrappedToken:(id)token owner:(id<TokenWrapperDelegate>)owner;
#property (nonatomic, weak) id<TokenWrapperDelegate> owner;
#property (nonatomic, strong) id token;
#end
Then have the TokenWrapper dealloc notify the owner that the token was deleted:
#implementation TokenWrapper
...
-(void)dealloc {
[owner tokenWasDeleted:self.token];
self.token = nil;
[super dealloc];
}
#end
Then in your representedObjectForEditingString callback, return an autoreleased wrapper pointing at your owner and your real token. You'll also have to make sure to change the other NSTokenField delegate callbacks to delve into the wrapper object.
Make sure the owner sets a bit to ignore these callbacks when you're manually changing the contents of the NSTokenField (like by calling setObjectValue).
I gave up (after stumbling around for more than 6 hours) on the approach of in place editing my tags using NSTokenField. I eventually ended up with a number of fragile hacks which would ripple through my application as this feature is needed in various places.
Unless somebody has some strong points to counter my current opinion, NSTokenField is a bit of an ugly monster bringing a half baked implementation to the party. Which is a shame as the presentation side of it really appeals to me...
EDIT: After some further experimenting, I settled on a reasonably acceptable compromise. I use NSTokenField in a readonly mode. It takes the relevant tags from my Core Data store and displays them as tokens. I added a menu to each token which allows the user to edit, delete or review a tag. A standard push button next to the token field allows to add a new tag. Editing and reviewing is implemented using NSPopovers. See this example:
There are still some minor issues:
The tokens tend to disappear at arbitrary times when hovering the mouse over the token field. This appears to be a bug.
As the token field only accepts NSArray for its binding, I introduced a "virtual property" named tagsAsArray that takes the associated tags and converts them from NSSet to NSArray. I think this impacts the KVO as edits of tags only show up after pressing enter or clicking outside the token field.