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'd like to change the color of all of my NSToolbarItem's labels.
I need to set it to white because it suits better to the background color of my NSWindow, but it defaults to black and I haven't found a way to change it neither in Interface Builder nor directly by code (NSToolbarItem implements setLabel, but it just sets the text string).
If possible, I'd like to avoid:
Replacing the whole NSToolbar by a custom NSView. Would feel like reinventing the wheel to me.
Having to create custom NSViews inside NSToolbarItem. It would imply having to leave blank all of its labels and adding the white-colored label inside the custom view.
In case anyone is interested, I solved it by:
Using custom views inside NSToolbarItems containing both a button and a label.
Displaying icon only instead of icon + label in NSToolbar in order to hide the default label.
I had to deal with another problem related to a bug with Interface Builder: the custom view was not showing at all. I was able to fix it thanks to this answer.
You can change it using NSMutableAttributeString.
For example:
-(void) awakeFromNib{
NSMutableAttributedString *title = [[NSMutableAttributedString alloc] initWithString:self.label];
NSRange titleRange = NSMakeRange(0, title.length);
[title addAttribute:NSForegroundColorAttributeName value:[NSColor redColor] range:titleRange];
[self setLabel:title]; }
You have to subclass NSBarButtonItem and override the drawRect method for that. Otherwise it will use [NSColor controlTextColor] or [NSColor disabledControlTextColor].
As the title suggest, I've enabled right clicks for my tableview with customized rows. Anywhere there is an NSTextField, it blocks the right click.
is there a userInteractionEnabled equivalent for cocoa like on the iphone?
I though I probably needed to subclass everything in my NSTableCellView subclass, but I just needed to override -(NSView*)hitTest:(NSPoint)aPoint method in my NSTableCellView subclass to return self.
I'm able to right click on text fields within my custom view based table view cell. Here is how I configure it:
NSTextField *tf = [[NSTextField alloc] initWithFrame:NSZeroRect];
self.textField = tf;
self.imageView.autoresizingMask=NSViewWidthSizable;
self.textField.editable=NO;
self.textField.selectable=NO;
self.textField.drawsBackground=NO;
self.textField.bordered=NO;
self.textField.bezeled=NO;
self.textField.target=self;
self.textField.action=#selector(textDidEndEditing:);
[self.textField.cell setLineBreakMode:NSLineBreakByTruncatingMiddle];
Also, make sure you are setting the -menu property of NSTableView and not the cell view to enable to menu. (I don't know if that will make a difference to your issue but it is how I do right clicking in a table view.)
Trying to get an NSTableView in IB to be selectable but not editable. But de-selecting "Editable" for a column also removes the selecting capability.
Can someone tell me where I should insert this code to make it work (not working in app delegate or window controller) :
NSTextFieldCell *aCell = [tableColumn dataCell];
[aCell setEditable: NO];
[aCell setSelectable: YES];
BTW that table is updated by dictionary bindings, and the dictionary controller is set to not editable.
Set the columns to Editable, but the individual cell behaviour to Selectable.
I'd try implementing tableView:shouldEditTableColumn:row:in your NSTableViews delegate and return NO. See here.
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.