Highlight a selection in NSTextField - macos

I want to be able to highlight a portion of text in an NSTextField but I've not been able to Google a way of doing this.
I have defined an NSRange but I cannot find a way of using this range to highlight the text. The only thing I have turned up is textField.selectText but this supposedly highlights the whole field.
I'm using Swift 2.

You may have noticed that an NSTextField only shows a selection range when it has focus, i.e., is the first responder. In this case, editing is handled by an NSTextView called the field editor. So, make sure the field has focus (e.g., by using the makeFirstResponder: method of NSWindow), then use the NSControl method currentEditor to get the field editor, and then you can use the NSText method setSelectedRange:.
ObjC
NSText* fieldEditor = [myField currentEditor];
[fieldEditor setSelectedRange: mySelRange];
Swift
let fieldEditor = textfield.currentEditor()
fieldEditor?.selectedRange = range

Related

how to detect double Click in NSTextField

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.

How do I update the expansion tooltip size after calling NSTextField setStringValue:?

When a view contains an NSTextField with the expansion tooltip enabled and the text doesn't fit, then the user hovers the cursor over the field, OS X shows an expansion tooltip. If you then call setStringValue: to change the text content of the NSTextField, the expansion tooltip's size is not updated. For instance, if the original text was 100 characters long but the new text is only 50 characters long, hovering over the new text will show an expansion tooltip large enough for 100 characters containing the new text. This is true even if the new string fits entirely in the NSTextField, which normally would prevent an expansion tooltip appearing.
The opposite also occurs: If the original string fits within the NSTextField, no expansion tooltip appears. If the new string does not fit within the NSTextField, no expansion tooltip appears even though it should.
Internally, the NSTextField's NSTextFieldCell implements the NSCell method expansionFrameWithFrame:inView:. Something (I'm not sure what) calls this once, and seems to cache the result. Setting a new string using setStringValue: does not cause this function to be called again.
Calling setNeedsDisplay on the NSTextField after calling setStringValue: does not fix this.
So how do I get AppKit to resize the expansion tooltip?
After a great deal of experimentation, I found two methods to fix this.
The very difficult way is to delete and recreate the entire NSTextField each time the text changes. This is laborious because NSTextField doesn't conform to the NSCopying protocol, so you have to use an NSArchiver and an NSUnarchiver to duplicate the original NSTextField, and even then some attributes are not copied, such as constraints.
The easy way is to hide and un-hide the NSTextField.
NSTextField *textField;
{...}
[textField setStringValue:newText];
[textField setHidden:YES];
[textField setHidden:NO];
This makes AppKit call expansionFrameWithFrame:inView: on the NSTextField's NSTextFieldCell, which properly updates the expansion tooltip's presence and size.
[textField resetCursorRects] seems to cause expansionFrameWithFrame:inView: to be called on the NSTextFieldCell as well. Tested only in macOS 10.14.

Get index of NSTextField completion

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.

NSTextField : How to draw background only when focused

I put a textfield in a window, and I want the textfield draw background only when focused.
I know that all the controls in the window share one field editor.
I tried subclass nstextfield and implement becomeFirstResponder and resignFirstResponder.
And tried use custom singleton editor for the window .
Any one know how to achieve this?
In the NSWindow ,every textfield or button share one instance of field editor(a singleton NSTextView instance),so when you click the textfield, textfield become firstResponser first,and then quickly pass it to the shared field editor. So when the textfield lost focus ,the resignFirstResponder of the textfield will never be called(because the field editor is the FirstResponder now).
You can look at fieldEditor:forObject: in NSWindow API.
http://developer.apple.com/library/mac/#documentation/Cocoa/Reference/ApplicationKit/Classes/NSWindow_Class/Reference/Reference.html#//apple_ref/occ/instm/NSWindow/fieldEditor:forObject:
SOLUTION:
(Thanks , Michael Gorbach)
In my window controller
- (id)windowWillReturnFieldEditor:(NSWindow *)sender toObject:(id)anObject
{
NSText *text = [sender fieldEditor:YES forObject:self];
if(text&&[anObject isKindOfClass:[MyCustomTextField class]])
{
[text setBackgroundColor:[NSColor whiteColor]];
[text setDrawsBackground:YES];
}
return text;
}
I just did this recently, in a tableView. You need to use a custom cell and fieldEditor. Specifically, you need to call setDrawsbackground:YES on the NSText/NSTextView object that is the field editor, and setBackground: to configure your color of choice. There are two places to set up a custom field editor.
One is to implement setUpFieldEditorAttributes: on a custom NSTextFieldCell subclass that you have configured your NSTextField to use, and another is to use the window or window delegate method windowWillReturnFieldEditor:toObject:.
Note that if the first method doesn't work for a particular setting, sometimes you need to use the second, because it gets in earlier in the codepath.

Text formatting in NSTextField

I have a graphics app that uses a NSTextField instance for in-place text editing, this feature was added long time ago and I never had a reason to check it, however, I have recently received a report: text filed doesn't allow text formatting. Format -> Text menu subitems are all disabled, so there is no way to set the paragraph of a text item.
The question: how should I set up the NSTextField to support paragraph editing? I'm sure it did work before since I have some projects with formatted text and NSTextField was there since the app was born. Did I miss something with system/XCode updates?
My NSTextField is multiline, editable, allowed to edit text attributes.
If someone will face this in the future, I can describe the problem in details:
NSTextView refuses to apply formatting if there is no ruler used (with recent OS update, I suppose)
NSTextField itself does no text editing, it uses shared NSTextView instance driven by the owning NSWindow
Default text editor from the NSWindow doesn't use rulers.
This results disabled text formatting when using NSTextField.
Solution is to subclass the NSWindow:
#implementation MyWindow
- (NSText *)fieldEditor:(BOOL)createWhenNeeded forObject:(id)anObject
{
NSText* text = [super fieldEditor:createWhenNeeded forObject:anObject];
if ([text isKindOfClass:[NSTextView class]])
[(NSTextView *)text setUsesRuler:YES];
return text;
}
#end
And voila, formatting is back.

Resources