I have a custom NSTextView implementation that automatically adjusts the font size so that the text fills the entire view.
I overwrote didChangeText to call my font size adjustment method. Works great when the user is editing text, but didChangeText (and the delegate method textDidChange:) are not called when the text view contents are set via bindings.
The font adjustment code needs to run whenever the text is set/changes, not only when it's changed by the user.
How can I detect all changes to the text in an NSTextView, even via bindings?
Note: If there's a better way to have the text fill the entire text view other than increasing the font size, let me know.
It would be better to set the font attributes into the NSAttributedString that is bound to the text view's "attributedString". In the textDidChange: delegate method, you can just recreate the NSAttributedString with the correct font attributes.
The NSTextView method didChangeText is not called when a binding updates the text (as opposed to the text view updating the model).
didChangeText is the source of the binding update. If you override it and don't call super, the binding is broken. didChangeText calls the delegate method textDidChange.
Unfortunately, didChangeText is also called rather late in the NSTextView update process - after the layout and storage delegate calls.
I found that the NSTextStorageDelegate method "didProcessEditing" was the best way to catch changes to the bound string. Although you have to be careful what changes you can make back to the textview at this point - some calls crashed.
I answered my own similar question more fully here:
NSTextView textDidChange not called through binding
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.
This seems it should be easy enough, but could anyone give me pointers on how to do this? Seems I should be subclassing NSTextView and using drawInsertionPointInRect:color:turnedOn: but how would I do this? I don't really want to do major customization maybe just a touch thicker or a touch shorter, but the question is where?
Thanks,
rc
This isn't really straight forward, since a NSSearchField is a subclass of NSTextField and not NSTextView. However, each NSTextField uses a proxy NSTextView to do the drawing, and this proxy NSTextView (called the field editor), is maintained by the current window. So, what you want to do is to create your custom NSTextView subclass, instantiate it somewhere in your window controller (or whatever you use as your windows delegate) and then create the following method: windowWillReturnFieldEditor:toObject:.
In the method you check if the toObject is your search field (or just any search field, in case you want to override it for every search field in the window), and then return your custom NSTextView, otherwise return nilfor the default field editor with the default behaviour.
I have a custom NSTextField sub-class with a custom drawRect: implementation.
The text field is part of a view inside a table cell. I programmatically bind the fontBold property of the text field.
The problem is that I have to recalculate a few things when the fontBold binding changes, but I can't figure out how to get notified when that happens.
I tried adding an observer, but that is not called. There does not seem to be a setFontBold:(BOOL) method that I could overwrite in NSTextField.
I think I figured it out:
There really is no fontBold property. I think what happens under the hood is that the binding is transformed by a value transformer into an NSFont object and what actually changes is the font attribute of NSTextField (I confirmed that setFont: is called when the fontBold binding changes). Phew, 4 hours of my life gone.
What's the minimum implementation needed to make a custom NSView with an editable text area? I assume NSTextFieldCell can be used for this. I've succeeded in drawing the cell in the view (which is straightforward), but making it editable seems to require a more complicated coordination between the view and the cell. Is there sample code available somewhere?
Update. I should have made clear that my longer-term goal is to have many more editable text areas on the same view. AFAIU it is better to use cells in that case as they are more light-weight than full-blown views. My updated question is: What's the minimum implementation needed to make a custom NSView with an editable text area using an appropriate NSCell?
What's the minimum implementation needed to make a custom NSView with an editable text area?
Make an NSView.
Put an NSTextField in it.
Remember, NSViews (custom or otherwise) can contain other NSViews, and an NSTextField is a kind of NSView.
If you don't want code outside the custom view class to know about the text field, and it probably shouldn't, the custom view can create the text field and add it to itself as a private implementation detail. To do this, simply don't expose the text field in the custom view class's #interface (aside from the instance variable declaration, which is unavoidable).
The custom view should, of course, not draw wherever it put its text field. It could draw there, but the text field would cover it.
I assume NSTextFieldCell can be used for this.
Yes, if you don't mind reimplementing NSTextField. Adding an NSTextField as a subview of your view is much easier.
If you want to make a grid of text fields (with a dynamic number of them, perhaps), use an NSMatrix of NSTextFieldCells. You can, of course, add the NSMatrix as a subview of your custom view.
If you want to edit a text cell just call editWithFrame:inView:editor:delegate:event: on the cell object. This method requires the NSEvent that started the editing, so you can only call this from an event handler. There also is selectWithFrame:inView:editor:delegate:start:length: which sets up the field editor with an selection. You can use this if you need to start the editing from outside of an event handler.
After the user is done editing you need to call endEditing: on your cell.
I have an instance of NSImageView. I have told InterfaceBuilder to allow the user to paste or drag an image into this view. This works. However, I don't see a way in the NSImageView or related documentation to get notified when this actually happens. I was expecting a delegate or some such, but I have been unable to find it. Any ideas?
This is what Cocoa Bindings does best.
Instead of talking to the view, simply have a property whose value is an image, and bind the image view's image binding to it; then, when you want to change the image in the image view yourself, all you have to do is set the value of the property, and the image view will notice the change.
The user pasting or dragging into the image view is essentially the same thing, partly in reverse: The paste or drop will change the value of the image view, which will then set the value of your property, which will cause anything else bound to it to notice the change and pick up the new value as before.
Aside from ripping out your existing send-explicit-messages-to-views code, this will require almost no code work: The only code you need is for your property. You'll hook up the Binding in IB.
See the Key-Value Coding Programming Guide and the Cocoa Bindings Programming Topics.
As for me the better way to override function
-(void)paste:(id)sender
But only if you have it in NSView based class which has NSResponder as parent