Can I force NSTextField to use ASCII quotes instead of unicode? - xcode

I have an NSTextField to collect a format string, which is used to create some text that is then saved to a file. If I enter double quotes (") in the NSTextField, the string from NSTextField.stringValue is encoded as 0xe2 0x80 0x9d, which is unicode for double quotes. If I try to save the resulting string to a file, using NSASCIIStringEncoding, it complains that the encoding fails. Can I force NSTextEdit to use the ASCII character for double quotes, 0x22? I've tried lossy conversion:
[NSString dataUsingEncoding: NSASCIIStringEncoding
allowLossyConversion: YES]
but its solution is to remove the quote altogether.
I've tried changing the "Use smart quotes and dashes" setting in System Preferences --> Keyboard --> Text

While an NSTextField is being edited, it has an NSTextView subview that handles the actual editing. This subview is called the “field editor”.
NSTextView has an automaticQuoteSubstitutionEnabled property. You want to turn this off for the field editor.
You can do this by creating a subclass of NSTextFieldCell and overriding the setUpFieldEditorAttributes method. In Swift:
class MyTextFieldCell: NSTextFieldCell {
override func setUpFieldEditorAttributes(_ textObj: NSText) -> NSText {
super.setUpFieldEditorAttributes(textObj)
if let textView = textObj as? NSTextView {
textView.isAutomaticQuoteSubstitutionEnabled = false
}
return textObj
}
}
In your xib, select the cell of your text field and set its custom class to MyTextFieldCell.
By default, each window has a single field editor shared by all text fields in the window. So using this custom cell subclass will have the effect of leaving smart quotes turned off for all text fields in that window after that one text field has had keyboard focus. If that's not what you want, then you need to use a separate field editor just for the one text field that you want to be free of smart quotes.
To use a separate field editor for the special text field, you have two choices:
Create an NSWindow subclass. In the subclass, override fieldEditor:forObject: to return a separate NSTextView for the fields where you want to disable smart quotes, and return [super fieldEditor:createFlag forObject:object] for normal field.
Give your window a delegate (if it doesn't have one already). In the delegate, implement windowWillReturnFieldEditor:toObject: to return a separate NSTextView if the field (the client argument) should have smart quotes disabled. Return nil for other fields.
If you go with a separate-field-editor solution, you probably don't have to use a custom NSTextFieldCell subclass. You can just set the special field editor's isAutomaticQuoteSubstitutionEnabled to false before returning it.

Related

Highlight a selection in NSTextField

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

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.

NSAttributedString in NSTextView not showing up unless you scroll

I'm trying to set an attributed string in an NSTextView, and if the text is taller than the text view, the text doesn't show up at all unless you scroll the text box.
There aren't many posts about attributed strings in NSTextView, so maybe I'm just doing something wrong. Here's how I'm setting the text:
[[self.textView textStorage] appendAttributedString:attributedString];
You have to programmatically tell the NSTextView it has to make the new string visible:
NSUInteger length = [[self.textView string] length];
[self.textView scrollRangeToVisible:NSMakeRange(length,0)];
This should work independent of if the appended string is a "simple" string or an attributed string.
Call -beginEditing before making a series of changes to the text storage and -endEditing afterward. See Text System Storage Layer Overview: Changing Text Storage.

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.

How to determine whether an NSSearchField/NSTextField has input focus?

How can I determine whether an NSSearchField/NSTextField has input focus?
The previous answer is wrong, because NSTextField / NSSearchField do not themselves become the first responder and handle edited text. Instead, they use the window's field editor, which is an NSTextView that is shared between all fields on the window (since only one of them can have focus at a time).
You need to see if the first responder is an NSText, and if so, if the search field / text field is its delegate.
NSResponder *firstResponder = [[NSApp keyWindow] firstResponder];
if ([firstResponder isKindOfClass:[NSText class]] && [(id)firstResponder delegate] == mySearchField) {
NSLog(#"Yup.");
}
While Greg Titus' answer probably works, I think the following is a better way:
BOOL isFirstResponder = mySearchField.currentEditor == mySearchField.window.firstResponder;
AppKit uses a “field editor” (which is an NSTextView) to handle the actual editing in an NSTextField (or an NSSearchField or an NSSecureTextField). While your text field has keyboard focus for its window, it has the field editor as a subview, and the window's first responder is the field editor.
So in general, you can check whether the text field has a field editor:
if textField.currentEditor() != nil {
// textField has the keyboard focus
} else {
// textField does not have the keyboard focus
}
However, when you move the focus out of the text field (by pressing the tab key or clicking in another text field), the text field posts an NSControl.textDidEndEditingNotification (Objective-C: NSControlTextDidEndEditingNotification). If the text field has a delegate, and the delegate implements the control(_:controlTextDidEndEditing:) method of the NSControlTextEditingDelegate protocol, then the delegate method is also called for the notification.
While this notification is being delivered (including calling the delegate method), the text field still has the field editor as a subview, and the field editor's delegate is still set to the text field. So if you don't want to consider the text field to still have the keyboard focus while in the notification handler (or delegate method), then testing the field editor will give the wrong answer.
(You might think this is a weird thing to test, because after all AppKit is sending you a notification that the text field is no longer the keyboard focus, so why do you need to ask? But maybe your notification handler calls into some other method that wants to check, and you don't want to have to pass in a flag saying “oh by the way the text field is/isn't the keyboard focus right now”.)
Okay, so anyway, before sending the NSControl.textDidEndEditingNotification, AppKit changes the text field's window's first responder. So you can check whether the text field has a field editor, and whether that field editor is the first responder of its window. While you are handling the NSControl.textDidEndEditingNotification, this test will report that the text field doesn't have the keyboard focus.
extension NSTextField {
public var hasKeyboardFocus: Bool {
guard
let editor = currentEditor(),
editor == window?.firstResponder
else { return false }
return true
}
}

Resources