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.
Related
I have a custom NSTextField and I'd like to detect double clicks by the user in the text field. My goal: I want to be able to double click on a parenthesis in an expression, such as "(2+2) = 4" and have it select everything inside the matching parentheses. Thought I could do this with...
- (void)textView:(NSTextView *)textView doubleClickedOnCell:(id <NSTextAttachmentCell>)cell inRect:(NSRect)cellFrame atIndex:(NSUInteger)charIndex;
but it never gets called in my custom NSTextField.
Then I thought I could override -mouseDown, but that isn't getting called either. I'm stumped. Any suggestions for what should be an easy function to implement.
Thanks!
Philip
A text field does not handling editing, as such. When a text field has focus, a text view is added to the window, overlapping the area of the text field. This is called the "field editor" and it is responsible for handling editing.
It seems the most likely place for you to change the behavior of a double-click is in the text storage object used by that text view. NSTextStorage inherits from NSMutableAttributedString which inherits from NSAttributedString which has a -doubleClickAtIndex: method. That method returns the range of the text that should be selected by a double-click at a particular index.
So, you'll want to implement a subclass of NSTextStorage that overrides that method and returns a different result in some circumstances. NSTextStorage is a semi-abstract base class of a class cluster. Subclassing it requires a bit more than usual. You have to implement the primitive methods of NSAttributedString and NSMutableAttributedString. See the docs about it.
There are a few places to customize the field editor by replacing its text storage object with an instance of your class:
You could implement a custom subclass of NSTextFieldCell. Set your text field to use this as its cell. In your subclass, override -fieldEditorForView:. In your override, instantiate an NSTextView. Obtain its layoutManager and call -replaceTextStorage: on that, passing it an instance of your custom text storage class. (This is easier than putting together the hierarchy of objects that is involved with text editing, although you could do that yourself.) Set the fieldEditor property of the text view to true and return it.
In your window delegate, implement -windowWillReturnFieldEditor:toObject:. Create, configure, and return an NSTextView using your custom text storage, as above.
it is simple just use this class to detect double tap
final class doubleClickableTextField : NSTextField {
override func mouseDown(with event: NSEvent) {
super.mouseDown(with: event)
if (event.clickCount == 2){
// do the work here
self.isEditable = true
}
}
}
The answer from Ken Thomases here is correct in its analysis of the issue regarding the field editor and how to replace it, but the solution it then recommends – replacing the NSTextStorage of the field editor – is not the correct solution, according to Apple. In their doc they specifically recommend that for delimiter-balancing the selectionRangeForProposedRange:granularity: method should be used. Once you have a custom field editor going, as per Ken's answer, you should therefore use the solution for NSTextView here, applied to a custom NSTextView subclass that you use for your field editor.
In case it is of interest, using NSTextStorage's doubleClickAtIndex: method for delimiter-balancing is probably the wrong solution for several trivial reasons: (1) because Apple says so, (2) because subclassing NSTextStorage is complicated and error-prone, and (3) because NSTextView provides a method specifically intended for the purpose of doing things like delimiter-balancing. But it is also wrong for a non-trivial reason: (4) that doubleClickAtIndex: is documented as "Returns the range of characters that form a word (or other linguistic unit) surrounding the given index, taking language characteristics into account". So doubleClickAtIndex: is really about how the linguistic units of the text (i.e. words) are defined, and redefining those in some way to make delimiter-balancing work would probably break other aspects of word-level text processing. For example, I would guess that it would be pretty tricky to make double-click-drag (dragging out a selection word by word) work properly if you have overridden doubleClickAtIndex: to do delimiter balancing. Cocoa may use doubleClickAtIndex: for other aspects of word-finding too, and may add more uses of it in the future. Since a delimiter-balanced section of text is not a "word", who knows what weirdness might result.
What I want to accomplish seems like it should be fairly straightforward. I have placed a sample project here.
I have a NSArrayController filled with an array of NSDictionaries.
[[self controller] addObject:#{ #"name" : #"itemA", #"part" : #"partA" }];
[[self controller] addObject:#{ #"name" : #"itemB", #"part" : #"partB" }];
[[self controller] addObject:#{ #"name" : #"itemC", #"part" : #"partC" }];
I am populating a NSPopupButton with the items in this array based on the 'name' key. This is easily accomplished with the following bindings
I would then like to populate a NSTextField with the text in the 'part' key based on the current selection of the NSPopupButton. I have setup the following binding:
With these bindings alone, the text field does display 'partC'.
However, if I change the value of the NSPopupMenu, what the text field shows does not change.
I thought this would simply be a matter of setting up the 'Selected Object' binding on the NSPopupButton
but that isn't working. I end up with the proxy object in my menu for some strange reason (providing the reason why would be a bonus).
So, what do I need to do to make this work?
Don't use "Selected Object" in this case. Bind the pop-up's "Selected Index" binding to the NSArrayController's selectionIndex Controller Key. Tried it out on your sample project and it works.
EDIT:
You asked why it's appropriate to use selectionIndex over selectedObject. First some background:
When binding a popup menu, there are three virtual "Collections" you can bind: Content is the abstract "list of things that should be in the menu" -- you must always specify Content. If you specify neither Content Objects, nor Content Values, then the collection of values bound to Content will be used as the "objects" and the strings returned by their -description methods will be used as the "values". In other words, the Content Values are the strings displayed in the pop-up and the Content Objects are the things they correspond to (which are possibly not strings, and which might not have a -description method suitable for generating the text in the pop-up). What's important to realize here is that there are potentially three different 'virtual arrays' in play here: The array for Content, the array for Content Objects (which may be different) and the array for Content Values (which may also be different). They will all have the same number of values, and typically, the Content Objects and Content Values will be functions (in the mathematical sense) of the corresponding items in the Content array.
The next thing that's important to realize is that part of NSArrayController's purpose in life is to keep track of the user's selection. This is only mildly (if at all) interesting in the case of a pop-up, but starts to become far more interesting in the case of an NSTableView. Internally, NSArrayController keeps track of this by keeping an NSIndexSet containing the indexes in the Content array that are selected at any given time. From there, selection state is exposed in several different ways for your convenience:
selectionIndexes is as described - an NSIndexSet containing the indexes of the selected items in the Content array
selectionIndex is a convenient option for applications that do not support multiple selection. It can be thought of as being equivalent to arrayController.selectionIndexes.firstIndex.
selectedObject is also useful in single selection cases, and corresponds conceptually to ContentObjectsArray[arrayController.selectionIndexes.firstIndex]
selection returns a special object (opaque to the consumer) that brokers reads and writes back to the underlying object (or objects in the case of multiple selection) in the Content Array of the array controller. It exists to enable editing multiple objects at a time in multiple selection cases, and to provide support for other special cases. (You should think of this property as read-only; Since its type is opaque to the consumer, you could never make a suitable new value to write into it. It's meaningful to make calls like: -[arrayController.selection setValue: myObject forKey: #"modelKey"], but it's not meaningful to make calls like -[arrayController setValue: myObject forKey: #"selection"]
With that understanding of the selection property, let's take a step back and see why it's not the right thing to use in this case. NSPopUpButton tries to be smart: You've provided it with a list of things that should be in the menu via the Content and Content Values bindings. Then you've additionally told it that you want to bind its Selected Object to the the NSArrayController's selection property. You're probably thinking of this as a "write only" binding -- i.e. "Dear pop-up, please take the user's selection and push it into the arrayController", but the binding is really bi-directional. So when the bindings refresh, the popup first populates the menu with all the items from the Content/Content Values bindings, and then it says, "Oh, you say the value at arrayController.selection is my Selected Object. That's odd -- it's not in the list of things bound with my Content/Content Values bindings. I'd better add it to the list for you! I'll do that by calling -description on it, and plunking that string into the menu for you." But the object you get from that Selected Object binding is the opaque selection object described above (and you can see from the outcome that it is of class _NSControllerObjectProxy, a private-to-AppKit class as hinted by the leading underscore).
In sum, that's why binding your popup's Selected Object binding to the array controller's selection controller key is the wrong thing to do here. Sad to say, but as I'm sure you've discovered, the documentation for Cocoa bindings only begins to scratch the surface, so don't feel bad. I've been working with Cocoa bindings pretty much daily, in a large-scale project, for several years now, and I still feel like there are a lot of use cases I don't yet fully understand.
I'm a little rusty on my Cocoa, so bear with me on terminology and such.
I want to write something that is essentially a reverse spell checker. As in, if a word is spell correctly, then for random words it changes it to a misspelled version. Harmless prank to play on someone.
So then, my main hitch is that I have no idea how to do this (major problem, I know). I like the way that textedit performs on-the-fly spellchecking, but I'd like to incorporate that sort of behavior into the generic behavior of the NSTextField. Is there some way for an application to be notified whenever a character is input into an NSTextField?
EDIT: My aim is to make this system-wide, as in any NSTextField in the system would get this behavior as a matter of inheritance. I'm open to some serious hacking here.
To answer your question: attach a delegate to the NSTextField control, and implement
- (void)controlTextDidChange:(NSNotification *)aNotification
Note that NSTextField uses the shared field editor to edit text, which is a NSText object. You might be able to manipulate that to do what you want.
I like the idea! This should be fairly easy to do. First you have to decide if you want to use NSTextField or NSTextView. TextEdit.app uses NSTextView which is appropriate for more extensive word processing-type tasks. NSTextField is more for smaller, minimally-formatted chunks of text. There's lots of ways to tackle this, but I'll give you a couple.
For NSTextField, set your controller object to be the delegate for the text field and override the controlTextDidChange: method. Whenever the user types a character into the text field, you'll get this message. You can then modify the field's string to introduce the misspelled word.
For NSTextView, you can activate spell checking and use the text view's delegate method textView:didCheckTextInRange:types:options:results:orthography:wordCount:. You should be able to modify the results of the spell check.
This question is similar to this one: How do I use an NSFormatter subclass with an NSPopUpButton
As mentioned in that question, it seems like the 'formatter' used by the cell of a NSPopUpButton doesn't seem to work. I'm wondering if this is expected, or if there is actually a purpose to setting the formatter of a NSPopUpButton.
Right now, I have a NSPopUpButton whose "Content Objects" are bound to the arrangedObjects of a NSArrayController whose "Content Array" is a NSArray of NSNumbers. Setting the formatter of the NSPopUpButton cell to a simple NSNumberFormatter which formats NSNumbers in a currency format doesn't work; the pop up menu displays the numbers un-formatted.
I'm wondering how I can format strings displayed in the pop up menu of an NSPopUpButton? I feel like this should be fairly straight-forward; having to use a value transformer, or a special value for the display path, seems like overkill and should be easier.
Thanks in advance.
If the cell won't honor the formatter, then you could provide an alternative property like -formattedCost as opposed to -cost. Nothing fancy is needed since a popup button's menu items are not user-editable.
Your -formattedCost property would use a shared NSNumberFormatter instance and return the properly-formatted string of -cost.
- (NSString *)formattedCost
{
return [mySharedCurrencyFormatter stringFromNumber:[self cost]];
}
The "formattedCost" property is what you'd bind to for display. Additional caveats: you'll want to register the "formattedCost" key as being dependent on the "cost" key. That way, when costs are changed, your popup will update (because that triggers "formattedCost" to change as well).
I have an NSTextView that contains data for the user to edit, but I want to surround it with a header and footer of non-editable data to give the user an idea of context.
I don't think an NSTextView can handle the concept of mixed editable/non-editable data, so I've come up with a few ideas.
a) Use text attachments with a custom cell to draw the header and footers.
b) Have 3 NSTextViews within the NSScrollView.
c) Use attributes to determine what cannot be edited,and use the delegate methods to prevent editing, this is probably my favourite, as it's probably the least intrusive.
Am I missing anything, any better ideas?
The NSTextView delegate method -textView:shouldChangeTextInRange:replacementString: will let you do this. You can "just say NO" to change. ;-)
Update / Elaboration (November, 2015)
To elaborate based on the comments on this answer, the idea is to use your own custom attributes on the attributed string your text view is editing. Beyond the standard attributes, you can specify your own attribute name (any NSString) and PLIST-compatible object as the value for that name.
For example, if you wanted to designate a range of text as "uneditable", you could add an attribute for that range with an attribute named (for example) #"TextIsEditableAttributeName" with an NSNumber with a BOOL value of YES or NO: [NSNumber NO] or #( NO ) (to use ObjC number boxing - same result: an NSNumber instance). Later, when the text view asks its delegate if it should change text in range, you can inspect the range for the presence of your #"TextIsEditableAttributeName" attribute.
Really, there's only a need to assign an attribute to ranges that aren't editable, so you don't even have to check for the value. You could just put an empty NSData instance there for a placeholder so the attribute has a value. Your attribute name could be #"EditingLocked" or something. This means you only have to check for the presence of the #"EditingLocked" attribute anywhere in the proposed range and return NO when the text view asks. This would catch overlapped selection (if you allow selection for copying the non-editable text) of editable vs. non-editable ranges.
This same approach, of course, can work for -textView:willChangeSelectionFromCharacterRanges:toCharacterRanges:, another delegate method that allows you to return a "corrected" array of range values for selection. If you don't want to allow non-editable text to be selected, you can "cut out" the ranges described by any instances of your #"EditingLocked" attribute you find in the proposed ranges.
I hope this helps.