In a seemingly trivial setup, I encounter an undesired interplay between a selectable NSTextField and an NSColorPanel that I cannot get rid of and that drives me nuts.
Here’s the setup: Within one window, I have a selectable Multi-Line Label (de facto an NSTextField) and an NSColorWell.
The Color Well allows the user to color geometric objects in the GUI; it has nothing to do with text whatsoever. Of course, clicking on the color well activates it, i.e. brings up the shared NSColorPanel and connects the color well to it.
The Text Field is completely independent from the colored objects in the GUI and presents data to the user. It is read-only, i.e. not editable. Since the data is organized in columns, I use tabs for text formatting and the setAttributedStringValue: method of NSTextField to display the data.
At first glimpse, everything works as you would expect in a such a trivial setup.
But here comes the rub: I want the user to be able to copy the data in the text field to process it elsewhere. Therefore, the NSTextField has to be selectable. And setting it to be selectable is where the problems start:
When the user clicks on the selectable text field to select the text, the window’s field editor takes over, and as a consequence, all the tab settings of the attributed text are lost and the text gets mingled. The usual way to prevent this is to set the allowsEditingTextAttributes property of the NSTextField to YES. If I do this, the tab formatting is preserved when the user selects the text. But now the NSColorPanel (if visible) unintentionally also switches to the text color (always black), and if the color well is active (connected to the NSColorPanel), it will remain active, thereby changing the color of all geometric GUI objects to black. Ouch!
I have found no way to set the selectable and allowsEditingTextAttributes properties of NSTextField to YES but still prevent it from communicating with the NSColorPanel.
The obvious alternative route would be to preserve the tab formatting for selected text even with allowsEditingTextAttributes set to NO (which would disconnect the color panel from the text field, as desired). But I’ve had no success with this approach either, although I do not really understand why:
My idea was to set the required tabs as the defaultParagraphStyle of the field editor of the text field. So, I set up a customized field editor:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
NSArray *myTabs = #[
[[NSTextTab alloc] initWithType:NSRightTabStopType location:100],
[[NSTextTab alloc] initWithType:NSRightTabStopType location:200],
[[NSTextTab alloc] initWithType:NSRightTabStopType location:300]
];
NSMutableParagraphStyle *myParagraphStyle = [[NSMutableParagraphStyle defaultParagraphStyle] mutableCopy];
[myParagraphStyle setTabStops:myTabs];
myFieldEditor = [NSTextView new]; // myFieldEditor is an instance variable
[myFieldEditor setDefaultParagraphStyle:myParagraphStyle];
[window setDelegate:self];
[window fieldEditor:YES forObject:myTextField];
}
And activate it for the text field in the windowWillReturnFieldEditor:toObject: delegate method:
- (id)windowWillReturnFieldEditor:(NSWindow *)sender toObject:(id)client
{
if (client == myTextField) return myFieldEditor;
return nil;
}
I even made sure that my custom field editor is indeed used by subclassing the NSTextFieldCell of my text field and logging the propagated field editor:
#implementation myTextFieldCell
- (NSText *)setUpFieldEditorAttributes:(NSText *)textObj
{
NSTextView *newTextObj = (NSTextView*)[super setUpFieldEditorAttributes:textObj];
NSLog(#"STYLE: %#", [newTextObj defaultParagraphStyle]);
return newTextObj;
}
#end
Now, when I select the text in the text field, I get the following log output:
2017-11-02 11:51:07.432 Demo[94807:303] STYLE: Alignment 4, LineSpacing 0, ParagraphSpacing 0, ParagraphSpacingBefore 0, HeadIndent 0, TailIndent 0, FirstLineHeadIndent 0, LineHeight 0/0, LineHeightMultiple 0, LineBreakMode 0, Tabs (
100R,
200R,
300R
), DefaultTabInterval 0, Blocks (null), Lists (null), BaseWritingDirection -1, HyphenationFactor 0, TighteningFactor 0.05, HeaderLevel 0
Which is exactly what is expected.
But still, the tab formatting disappears in the text field as soon as I select the text. I have no idea why this does not work.
So I’m stuck either way. If I set the allowsEditingTextAttributes property of NSTextField to YES, tab formatting is preserved when the text is selected, but my colored objects in the GUI unintentionally change to black. If I set the allowsEditingTextAttributes property to NO, the color panel behaves as it should, but the tab formatting is lost as soon as I select the text.
This is a very unfortunate case of Cocoa trying to be too smart and thereby making a completely trivial setup a huge issue.
Any ideas anyone?
OK, so I ended up with the suggestion that #Willeke (thanks!!) made in his comment to my question: To use NSTextView instead of NSTextField to implement my Multi-Line Label.
I will first sum up why it seems impossible to do what I wanted with NSTextField, and then the solution with NSTextView.
Why NSTextField doesn’t work
As described, my idea for a solution was to customize the field editor for the NSTextField, setting the tab stops I need, so that I need not set NSTextField’s allowsEditingTextAttributes property to YES (which would unintentionally couple the text field to the color panel). This, I hoped, would preserve the tab stops of my attributed string’s paragraph style when I select the text in the text field and thereby activate the field editor.
Extensive testing has shown that this does not work for several reasons:
As #Willeke pointed out, setting the usesFontPanel property of NSTextView to NO also breaks the connection of the text view to the color panel (as desired). However, this does not work for the NSTextView that is the field editor of the NSTextField, because in this context, this setting is always overwritten by the allowsEditingTextAttributes property of the NSTextField: If allowsEditingTextAttributes is YES, the font and color panels are coupled regardless of the value of usesFontPanel, if it is NO, the font and color panels are decoupled regardless of the value of usesFontPanel.
The idea to use the tab stops of a customized field editor instead of using the tab stops of my attributed string’s paragraph style (which would require allowsEditingTextAttributes to be YES) wouldn’t work, anyway, because the tab stops settings of the field editor are obviously always completely ignored by NSTextField, regardless of the value of the allowsEditingTextAttributes property. NSTextField always uses the evenly spaced default tab stops.
Judging from intense googling, the other variant – setting allowsEditingTextAttributes to YES but somehow modifying NSColorPanel to not connect to the NSTextField nevertheless – is impossible to implement without recurring to private methods of NSColorPanel.
How to implement the solution with NSTextView
While it seems overkill to instantiate a complete NSTextView embedded in a clip view and a scroll view just to get the functionality of a text field, in the end it’s the easiest (or even only possible) solution.
To make the scroll view disappear, you’ll have to basically uncheck everything in the NSScrollView’s Attribute inspector in IB, in particular Show Vertical Scroller. Set Draw Background and the border type to the kind of appearance you want; if you want to mimic a multi-line label (like I did), uncheck Draw Background and choose the invisible border type. In the Attribute inspector of the embedded NSTextView, also uncheck all attributes except Selectable, in particular Uses Font Panel.
Make sure the size of the NSTextView is large enough to hold the complete content string to avoid unintentional scrolling effects and fix the text position. If your content string ends with a line brake, you’ll need enough space for an empty line beneath it. If you did not uncheck Draw Background and this does not look the way you want, don’t draw the background of either the NSScrollView or the NSTextView, select the invisible border for NSScrollView and then put an NSBox of the desired size and appearance beneath them.
You can now set the attributed content string with:
[[myTextView textStorage] setAttributedString:myAttributedString];
Note that this does work although the editable property of NSTextView is set to NO since you’re modifying the NSTextStorage, not the NSTextView itself.
But unfortunately, we’re not finished yet.
When you’re using an NSTextField to display readonly data as you typically do in a Label, more often than not you would not want the text field to be part of your key view loop (that circles through your controls by pressing the Tab key). To achieve this, you can simply set the refusesFirstResponder property of NSTextField to YES. But NSTextView does not inherit from NSControl and therefore does not have this property. So in the end, we’ll have to subclass NSTextView to add the refusesFirstResponder property.
The implementation overwrites becomeFirstResponder and goes like this:
- (BOOL)becomeFirstResponder
{
if (!_refusesFirstResponder) return [super becomeFirstResponder];
NSEvent *event = [NSApp currentEvent];
if ([event type] == NSLeftMouseDown || [event type] == NSRightMouseDown) return [super becomeFirstResponder];
NSView *validKeyView = ([event modifierFlags] & NSShiftKeyMask)? [[[self previousValidKeyView] previousValidKeyView] previousValidKeyView] : [self nextValidKeyView];
[[NSOperationQueue mainQueue] addOperationWithBlock:^{[[self window] makeFirstResponder:validKeyView];}];
return NO;
}
If refusesFirstResponder is NO, we simply return super’s implementation.
If it is YES, we check if the NSTextView is about to become first responder because of a mouse click within it. If so, we also simply return super’s implementation, thereby allowing text selection with the mouse.
Other than that, we forward the first responder request to the next or previous key view (depending on whether the Shift key was pressed) and return NO, refusing to become first responder. Determining the previous key view is a bit tricky because the closest previous key view is the embedding NSClipView we don’t want or need, but have to use because Interface Builder does not offer a “pure” NSTextView. Then comes the embedding NSScrollView, and only then the previous key view we actually want.
Also, since we’re amidst a process which determines the first responder, we cannot simply invoke makeFirstResponder:, but have to postpone it to the next iteration of the run loop.
Now that we have implemented refusesFirstResponder, we’ll still have to mimic NSTextField’s behavior to dismiss any text selection when it loses first responder state. We can do this in an NSText delegate method. Assuming we don’t need other delegate functionality, we can make our subclass its own delegate and add this delegate method:
- (void)textDidEndEditing:(NSNotification*)notification
{
[[notification object] setSelectedRange:NSMakeRange(UINT64_MAX, 0)];
}
Finally, if we have to subclass, anyway, we might as well add a setAttributedString: convenience method.
So what we’ll end up with is this:
Header:
#import <Cocoa/Cocoa.h>
IB_DESIGNABLE
#interface MyTextFieldLikeTextView : NSTextView <NSTextViewDelegate>
#property IBInspectable BOOL refusesFirstResponder;
- (void)setAttributedString:(NSAttributedString*)attributedString;
#end
Implementation:
#import "MyTextFieldLikeTextView.h"
#implementation MyTextFieldLikeTextView
- (void)awakeFromNib
{
[self setDelegate:self];
}
- (BOOL)becomeFirstResponder
{
if (!_refusesFirstResponder) return [super becomeFirstResponder];
NSEvent *event = [NSApp currentEvent];
if ([event type] == NSLeftMouseDown || [event type] == NSRightMouseDown) return [super becomeFirstResponder];
NSView *validKeyView = ([event modifierFlags] & NSShiftKeyMask)? [[[self previousValidKeyView] previousValidKeyView] previousValidKeyView] : [self nextValidKeyView];
[[NSOperationQueue mainQueue] addOperationWithBlock:^{[[self window] makeFirstResponder:validKeyView];}];
return NO;
}
- (void)textDidEndEditing:(NSNotification*)notification
{
[[notification object] setSelectedRange:NSMakeRange(UINT64_MAX, 0)];
}
- (void)setAttributedString:(NSAttributedString*)attributedString
{
[[self textStorage] setAttributedString:attributedString];
}
#end
Still a lot of effort only because Cocoa tries to outsmart us and insists on connecting the NSColorPanel to each and every NSTextField that allows for attributed text …
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
I'm working with NSTextFields and enabling/disabling selection and editing, and I ran across some strange behavior in a sample app. I have a subclass of NSTextField called MyTextField; the only thing this subclass does is to deny first responder status whenever asked, as in:
#interface MyTextField : NSTextField
#end
#implementation MyTextField
- (BOOL)acceptsFirstResponder {
return NO;
}
#end
However, when I place an instance of this text field in a .xib, then launch the app, I can still click into the text field and start editing it. Is the text field ignoring the return value of -acceptsFirstResponder?
I've tried a couple things to work around/diagnose this:
The text field's class is properly set to MyTextField instead of NSTextField in the .xib
If I place an NSLog statement before the return, it gets printed to console as expected
If I return NO for -becomeFirstResponder, it exhibits the same behavior: I can still edit the field
If I call [myTextField setSelectable:NO] in the view controller, it works as I expect: I'm no longer able to click into the field
However, if I return NO from -isSelectable and -isEditable (without calling -setSelectable: explicitly), I can still select text in (and edit) the field
What's going on here?
I'm not sure about this, but I think this has to do with the fact that the first responder is not actually the text field, but the field editor, which is a special (non-visible) text view object. Have a look at the "Working with the field editor" section of the "Text Editing Programming Guide" in the Apple docs to see an explanation.
I have a custom view in a .xib file, which I use as the contentViewController for an MAAttachedWindow. The view has several NSTextFields in it.
When I open the MAAttachedWindow first time, everything is fine. Text shows up in all relevant text fields. Then, if I close the window (which sets it to nil) and then call it again (which reinitializes, using the same custom view as the contentViewController), the last firstResponder text field is now blank.
The strange thing is that if I click the "empty" text field, it shows the correct text. This can be edited, and behaves appropriately as long as this text field has focus. As soon as something else becomes firstResponder, the text vanishes again.
Updates:
Changing the color did not change the aforementioned behavior.
The text color does not change at any time during this process.
Placeholder text also is subject to the aforementioned behavior.
No errors are occurring at any time during this process.
This does not happen to NSSecureTextFields.
I first encountered this problem about 5 years ago with accessory view of a NSSavePanel.
The solution that I've found was to move the first responder to the panel itself, before it's closed. Here's my exact method:
- (void)windowDidEndSheet:(NSNotification *)notification
NSSavePanel *savePanel = [(XSDocument *)[self document] savePanel];
if (!savePanel)
return;
// this fixes a bug where on next opening one of accessory view's text field will be blank and behave strangely
[savePanel makeFirstResponder:savePanel];
}
Try changing color of textfield text to red color (or any other color) you may get what happens here.
I got it!
I simply needed to explicitly remove the viewController from its superview before closing (and subsequently deallocating) the MAAttachedWindow.
Try resigning all first responders before setting the window to nil.
I'm working with a NSOutlineView located on a HUD panel. I configured it so that it doesn't draw its background. Everything looks fine until I double click to edit a cell.
The field editor draws its background and focus ring which completely ruin the whole user experience.
This is what I'm doing in the subclass of NSTextFieldCell:
- (NSText *)setUpFieldEditorAttributes:(NSText *)textObj
{
NSText *text = [super setUpFieldEditorAttributes:textObj];
[text setDrawsBackground:YES];
[text setBackgroundColor:[NSColor darkGrayColor]];
return text;
}
If I use setDrawsBackground:NO it's completely ignored and I get a white background. My solution is far from being good because I can't touch the alpha component of the color (if I do that, again the field editor will use another color as a background), but at least I don't get a white background.
I'm wondering if there's an actual solution to this problem. Do I have to provide my own field editor? Is it worth it?
What I want is simply a field editor with no background and no focus ring, just the cursor blinking.
Thanks!
The problem is that the white background is drawn by NSTableView when it's sent -editColumn:row:withEvent:select:. It fills the cell's rect with +[NSColor textBackgroundColor].
If there's a public API for overriding the current setting for named colors from the developer colorspace, we could set it inside an override of -editColumn:row:withEvent:select: or the like. I do not recall such an API (pointers are appreciated). ALSO: I've only tested this code on Snow Leopard (even the Leopard SDK addendum below). Verify the code against the actual SDKs and runtime environments you intend to support.
NSTableView has a private accessor it uses for the fill color, but it's a read-only property. No setter, so we can't just change the value on a standard NSTableView. We must subclass it. (Since you want the same behavior in an outlineView and NSOutlineView is already a subclass of NSTableView, we're going to subclass NSOutlineView. But, aside from the superclass, the code is identical.)
#interface ASCOutlineView : NSOutlineView {
}
#end
#implementation ASCOutlineView
- _textBackgroundColor
{
return ([NSColor clearColor]);
}
#end
seems to be all one needs to prevent that glaring white block from ruining your HUD when editing table cells in Snow Leopard.
Apps compiled against the Leopard SDK need a little more support though. Leopard's tableViews may have hard-coded some rendering properties so we need to override a choice method.
NSTextFieldCells are actually wrappers for NSTextViews so they can be used inside controls. They normally share the same textView instance, which is managed by the window (or its subclass, panel, in this case). NSTableView alters the settings of the NSTextFieldCell to conform to system UI settings for editing data. Mostly. The NSTextFieldCell then propagates those settings to the NSTextView. At any point along this pipeline we can override a method or two to alter the values of those properties to match our own UI.
I use -[NSTextFieldCell setDrawsBackground:] because it requires little effort to get correct. It's also important to keep the internal state as consistent with the effect we're hoping to achieve in the event some other object might depend on that state.
#interface ASCTextFieldCell : NSTextFieldCell {
}
#end
#implementation ASCTextFieldCell
- (void)setDrawsBackground: (BOOL)flag
{
[super setDrawsBackground: NO];
}
#end
And preventing the focus ring from appearing while the cell's being edited is a simple matter of changing the setting of its focus ring type. Frustratingly, IB doesn't provide access to this property, so it must be done programmatically:
for(eachColumn in [hudOutlineView tableColumns])
{
columnCell = [[ASCTextFieldCell alloc] initTextCell: #""];
[eachColumn setDataCell: columnCell];
if([columnCell respondsToSelector: #selector(setFocusRingType:)] != NO)
[(NSTextFieldCell *)columnCell setFocusRingType: NSFocusRingTypeNone];
}
It looks like there is other background behind field editor, which is drawn as white.
Probably, NSCell, or background of row, whatever else.