How to customize the selected text colors of an NSTextField / NSTextView in an inactive state - cocoa

I'm using an NSTextField and customizing the fieldEditor using the setupFieldEditorAttributes: method. This allows me to set custom foreground and background colors for the selected text, which is important because my textField has a black background and white text. Generally, this works fine. However, my settings seem to be overridden when I deactivate the application and the window is no longer key. The fieldEditor NSTextView remains there, but drawing changes to a white text color and light gray selection color (the defaults). Does anyone have suggestions for how I can customize this drawing?

You can override [NSWindow willReturnFieldEditor:toObject:] and return there custom NSTextView with changed selection color.

Inspired by the answer to this question, the solution is to create an override of the NSLayoutManager that customizes the way in which the highlighting is performed based on the first responder state of the NSText view that owns it.
If the text view associated with this custom layout manager is the first responder, then it draws the selection using the color provided by macOS. If the text view is not the first responder, it uses the text view's background color as the selection color unless a custom color is provided via the setCustomInactiveColor method.
// ---------------------------------------------------------------------------
// IZLayoutManager CLASS
// ---------------------------------------------------------------------------
// Override NSLayoutManager to change how the currently selected text is
// highlighted when the owning NSTextView is not the first responder.
#interface IZLayoutManager : NSLayoutManager
{
}
-(instancetype)initWithOwningTextView:(NSTextView*)inOwningTextView;
#property (nullable, assign, nonatomic) NSTextView* owningTextView;
#property (nullable, strong, nonatomic) NSColor* customInactiveColor;
#end
#implementation IZLayoutManager
- (instancetype)initWithOwningTextView:(NSTextView*)inOwningTextView
{
self = [super init];
if (self) {
self.owningTextView = inOwningTextView;
}
return self;
}
- (void) dealloc
{
// my project is non-ARC; so we maually release any custom color
// we received; in non-ARC projects this is probably not necessary
if (self.customInactiveColor != NULL) {
[self.customInactiveColor release];
self.customInactiveColor = NULL;
}
[super dealloc];
}
// see extensive description of fillBackgroundRectArray in NSLayoutManager.h
// TL;DR: if you change the background color here, you must restore it before
// returning from this call
- (void) fillBackgroundRectArray:(const NSRect *)rectArray count:(NSUInteger)rectCount forCharacterRange:(NSRange)charRange color:(NSColor *)color
{
BOOL needToReestoreColor = NO;
if (self.owningTextView != NULL && [[self.owningTextView window] firstResponder] != self.owningTextView) {
if (self.customInactiveColor != NULL) {
[self.customInactiveColor setFill];
} else {
[[self.owningTextView backgroundColor] setFill];
}
needToReestoreColor = true;
}
[super fillBackgroundRectArray:rectArray count:rectCount forCharacterRange:charRange color:color];
if (needToReestoreColor) {
[color setFill];
}
}
#end
Then, after you've allocated the NSTextView, you need to do this:
NSTextView* myTextView = ... // get a reference to your text view
// allocate our custom layout manager
IZLayoutManager* layoutManager = [[[IZLayoutManager alloc] initWithOwningTextView:self] autorelease];
// if you want to use a color other than the background for
// the selected text, uncomment the following line and
// supply your desired color
// [layoutManager setCustomInactiveColor:[NSColor redColor]];
[[myTextView textContainer] replaceLayoutManager:layoutManager];

Related

NSTextFinder + programmatically changing the text in NSTextView

I have a NSTextView for which I want to use the find bar. The text is selectable, but not editable. I change the text in the text view programatically.
This setup can crash when NSTextFinder tries to select the next match after the text was changed. It seems NSTextFinder hold on to outdated ranges for incremental matches.
I tried several methods of changing the text:
[textView setString:#""];
or
NSTextStorage *newStorage = [[NSTextStorage alloc] initWithString:#""];
[textView.layoutManager replaceTextStorage:newStorage];
or
[textView.textStorage beginEditing];
[textView.textStorage setAttributedString:[[NSAttributedString alloc] initWithString:#""]];
[textView.textStorage endEditing];
Only replaceTextStorage: calls -[NSTextFinder noteClientStringWillChange]. None of the above invokes -[NSTextFinder cancelFindIndicator].
Even with NSTextFinder notified about the text change it can crash on Find Next (command-G).
I have also tried creating my own NSTextFinder instance as suggested in this post. Even though NSTextView does not implement the NSTextFinderClient protocol this works and fails just the same as without the NSTextFinder instance.
What is the correct way to use NSTextFinder with NSTextView?
I had the same problem with the text view in my app, and what makes it even more annoying is that all "solutions" you find on the internet are either incorrect or at least incomplete. So here is my contribution.
When you set textView.useFindBar = YES in a NSTextView, this text view creates a NSTextFinder internally, and forwards the search/replace commands to it. Unfortunately, NSTextView does not seem to handle correctly the changes you make programmatically to its associated NSTextStorage, which causes the crashes you mention.
If you want to change this behavior, creating your private NSTextFinder is not enough: you also need to avoid the use by the text view of its default text finder, otherwise conflicts will occur and the new text finder won't be of much use.
To do this, you have to subclass NSTextView:
#interface MyTextView : NSTextView
- (void) resetTextFinder; // A method to reset the view's text finder when you change the text storage
#end
And in your text view, you have to override the responder methods used for controlling the text finder:
#interface MyTextView () <NSTextFinderClient>
{
NSTextFinder* _textFinder; // define your own text finder
}
#property (readonly) NSTextFinder* textFinder;
#end
#implementation MyTextView
// Text finder command validation (could also be done in method validateUserInterfaceItem: if you prefer)
- (BOOL) validateMenuItem:(NSMenuItem *)menuItem
{
BOOL isValidItem = NO;
if (menuItem.action == #selector(performTextFinderAction:)) {
isValidItem = [self.textFinder validateAction:menuItem.tag];
}
// validate other menu items if needed
// ...
// and don't forget to call the superclass
else {
isValidItem = [super validateMenuItem:menuItem];
}
return isValidItem;
}
// Text Finder
- (NSTextFinder*) textFinder
{
// Create the text finder on demand
if (_textFinder == nil) {
_textFinder = [[NSTextFinder alloc] init];
_textFinder.client = self;
_textFinder.findBarContainer = [self enclosingScrollView];
_textFinder.incrementalSearchingEnabled = YES;
_textFinder.incrementalSearchingShouldDimContentView = YES;
}
return _textFinder;
}
- (void) resetTextFinder
{
if (_textFinder != nil) {
// Hide the text finder
[_textFinder cancelFindIndicator];
[_textFinder performAction:NSTextFinderActionHideFindInterface];
// Clear its client and container properties
_textFinder.client = nil;
_textFinder.findBarContainer = nil;
// And delete it
_textFinder = nil;
}
}
// This is where the commands are actually sent to the text finder
- (void) performTextFinderAction:(id<NSValidatedUserInterfaceItem>)sender
{
[self.textFinder performAction:sender.tag];
}
#end
In your text view, you still need to set properties usesFindBar and incrementalSearchingEnabled to YES.
And before changing the view's text storage (or text storage contents) you just need to call [myTextView resetTextFinder]; to recreate a brand new text finder for your new content the next time you will do a search.
If you want more information about NSTextFinder, the best doc I have seen is in the AppKit Release Notes for OS X 10.7
The solution I had come up with seems rather similar to the one offered by #jlj. In both solutions NSTextView is used as client of NSTextFinder.
It seems that the main difference is that I don't hide the find bar on text change. I also hold onto my NSTextFinder instance. To do so I need to call [textFinder noteClientStringWillChange].
Changing text:
NSTextView *textView = self.textView;
NSTextFinder *textFinder = self.textFinder;
[textFinder cancelFindIndicator];
[textFinder noteClientStringWillChange];
[textView setString:#"New text"];
The rest of the view controller code looks like this:
- (void)viewDidLoad
{
[super viewDidLoad];
NSTextFinder *textFinder = [[NSTextFinder alloc] init];
[textFinder setClient:(id < NSTextFinderClient >)textView];
[textFinder setFindBarContainer:[textView enclosingScrollView]];
[textView setUsesFindBar:YES];
[textView setIncrementalSearchingEnabled:YES];
self.textFinder = textFinder;
}
- (void)viewWillDisappear
{
NSTextFinder *textFinder = self.textFinder;
[textFinder cancelFindIndicator];
[super viewWillDisappear];
}
- (id)supplementalTargetForAction:(SEL)action sender:(id)sender
{
id target = [super supplementalTargetForAction:action sender:sender];
if (target != nil) {
return target;
}
if (action == #selector(performTextFinderAction:)) {
target = self.textView;
if (![target respondsToSelector:action]) {
target = [target supplementalTargetForAction:action sender:sender];
}
if ((target != self) && [target respondsToSelector:action]) {
return target;
}
}
return nil;
}

NSButton disabled title color always gray

I have a custom NSButton, but no matter what i do, the disabled color is always gray
I tried all solutions i came across
i'am setting the attributed string title with white foreground color (i looks like the color attribute is ignored for the disabled state)
i did set [[self cell] setImageDimsWhenDisabled:NO];
event when the documentations states
// When disabled, the image and text of an NSButtonCell are normally dimmed with gray.
// Radio buttons and switches use (imageDimsWhenDisabled == NO) so only their text is dimmed.
#property BOOL imageDimsWhenDisabled;
it doesn't work
My NSButton uses wantsUpdateLayer YES, so the draw methods are overwritten, but i don't understand, where the title is drawn
On OS X 10.9 I've managed to alter the color of the button's text when it's disabled by sub-classing the cell that draws the button.
Create a new NSButtonCell subclass in Xcode and override the following method:
- (NSRect)drawTitle:(NSAttributedString *)title
withFrame:(NSRect)frame
inView:(NSView *)controlView {
NSDictionary *attributes = [title attributesAtIndex:0 effectiveRange:nil];
NSColor *systemDisabled = [NSColor colorWithCatalogName:#"System"
colorName:#"disabledControlTextColor"];
NSColor *buttonTextColor = attributes[NSForegroundColorAttributeName];
if (systemDisabled == buttonTextColor) {
NSMutableDictionary *newAttrs = [attributes mutableCopy];
newAttrs[NSForegroundColorAttributeName] = [NSColor orangeColor];
title = [[NSAttributedString alloc] initWithString:title.string
attributes:newAttrs];
}
return [super drawTitle:title
withFrame:frame
inView:controlView];
}
Select the button in Xcode, then select its cell (maybe easiest to do this in the Interface Builder dock), now got to the Identity Inspector and set the cell's class to that of your subclass.
This is because of the default true value of
- (BOOL)_shouldDrawTextWithDisabledAppearance
Try to change this method instead of imageDimsWhenDisabled. If you are using Swift 4, I would do the following in the Bridging header:
#import <AppKit/AppKit.h>
#interface NSButtonCell (Private)
- (BOOL)_shouldDrawTextWithDisabledAppearance;
#end
And in the subclass of NSButtonCell:
override func _shouldDrawTextWithDisabledAppearance() -> Bool {
return false
}
And that's it: the grey should disappear

Getting duplicate header button cell in NSTableView when using NSPopUpButtonCell

I have a dynamic NSTableView which can add a number of columns depending on the data provided. For each column I have set the header cell to be a NSPopUpButtonCell. (Side-note: I've had to use a custom subclass class for NSTableHeaderView otherwise the menu doesn't pop-up). All works well, apart from a duplicate or extra header button cell on the top right. It mirrors perfectly the previous column selection as shown in screenshots. My question is how do I stop the NSTableView from recycling the previous popup header cell? (By the way I have tried the setCornerView method but that only effects the header area above the vertical scrollbar.)
I came across the same problem this week. I went with the quick fix,
[_tableView sizeLastColumnToFit];
(However, after discussion with OP this requires that you use a subclass of NSPopUpButtonCell in the header and also NSTableHeaderView. I attach my solution below)
You can to this by combining the approaches outlined here,
PopUpTableHeaderCell
DataTableHeaderView
Here is a simplified snippet,
// PopUpTableHeaderCell.h
#import <Cocoa/Cocoa.h>
/* Credit: http://www.cocoabuilder.com/archive/cocoa/133285-placing-controls-inside-table-header-view-solution.html#133285 */
#interface PopUpTableHeaderCell : NSPopUpButtonCell
#property (strong) NSTableHeaderCell *tableHeaderCell; // Just used for drawing the background
#end
// PopUpTableHeaderCell.m
#implementation PopUpTableHeaderCell
- (id)init {
if (self = [super init]){
// Init our table header cell and set a blank title, ready for drawing
_tableHeaderCell = [[NSTableHeaderCell alloc] init];
[_tableHeaderCell setTitle:#""];
// Set up the popup cell attributes
[self setControlSize:NSMiniControlSize];
[self setArrowPosition:NSPopUpNoArrow];
[self setBordered:NO];
[self setBezeled:NO];
[self setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
}
return self;
}
// We do all drawing ourselves to make our popup cell look like a header cell
- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView*)controlView{
[_tableHeaderCell drawWithFrame:cellFrame inView:controlView];
// Now draw the text and image over the top
[self drawInteriorWithFrame:cellFrame inView:controlView];
}
#end
Now for the NSTableViewHeader subclass.
//DataTableHeaderView.h
#import <Cocoa/Cocoa.h>
/* Credit: http://forums.macnn.com/79/developer-center/304072/problem-of-nspopupbuttoncell-within-nstableheaderview/ */
#interface DataTableHeaderView : NSTableHeaderView
#end
//DataTableHeaderView.m
#import "DataTableHeaderView.h"
/* Credit: http://forums.macnn.com/79/developer-center/304072/problem-of-nspopupbuttoncell-within-nstableheaderview/ */
#implementation DataTableHeaderView
- (id)initWithFrame:(NSRect)frame {
self = [super initWithFrame:frame];
if (self) {
// Initialization code here.
}
return self;
}
- (void)mouseDown:(NSEvent *)theEvent {
// Figure which column, if any, was clicked
NSPoint clickedPoint = [self convertPoint:theEvent.locationInWindow fromView:nil];
NSInteger columnIndex = [self columnAtPoint:clickedPoint];
if (columnIndex < 0) {
return [super mouseDown:theEvent];
}
NSRect columnRect = [self headerRectOfColumn:columnIndex];
// I want to preserve column resizing. If you do not, remove this
if (![self mouse:clickedPoint inRect:NSInsetRect(columnRect, 3, 0)]) {
return [super mouseDown:theEvent];
}
// Now, pop the cell's menu
[[[self.tableView.tableColumns objectAtIndex:columnIndex] headerCell] performClickWithFrame:columnRect inView:self];
[self setNeedsDisplay:YES];
}
- (BOOL)isOpaque {
return NO;
}
- (void)drawRect:(NSRect)dirtyRect {
[super drawRect:dirtyRect];
// Drawing code here.
}
#end
You can tie everything together in the AppDelegate -awakeFromNib or similar,
-(void) awakeFromNib {
/* NB the NSTableHeaderView class is changed to be an DataTableHeaderView in IB! */
NSUInteger numberOfColumnsWanted = 5;
for (NSUInteger i=0; i<numberOfColumnsWanted; i++) {
PopUpTableHeaderCell *headerCell;
headerCell = [[PopUpTableHeaderCell alloc] init];
[headerCell addItemWithTitle:#"item 1"];
[headerCell addItemWithTitle:#"item 2"];
[headerCell addItemWithTitle:#"item 3"];
NSTableColumn *column;
[column setHeaderCell:headerCell];
[column sizeToFit];
[_tableView addTableColumn:column];
}
/* If we don't do this we get a final (space filling) column with an unclickable (dummy) header */
[_tableView sizeLastColumnToFit];
}
Other than that I haven't figured out how to properly correct the drawing in that region.
It seems like it's the image of the last cell that is being duplicated. So I slightly more hack-ish approach would be to add a extra column to your table view with a blank name and which intentionally ignores the mouse clicks. Hopefully by setting the display properties of the last column you can make it look the way you want.
I couldn't find any NSTableView or NSTableViewDelegate method that allow control of this region, so may any other solution would be very complicated. I would be interested in a nice solution too, but I hope this gets you started!
I have this issue and i don't use NSPopUpButtonCell at all.
I just want to tell about other method how to hide this odd header. This methods will not remove an odd table column, i.e. if you have 2 'legal' columns and hide this extra 3rd column header, you will still be able to move separator between 2nd and 3rd column. But in this case you won't see redundant header even if you want to resize any column.
I still need solution how to completely remove the redundant column, and why this is happening. (and why Apple won't fix this bug?)
So... you can just calculate index of column which this header belongs to and according to this draw your header or don't. First, subclass NSTableHeaderCell and set it as a cell class for columns. Let assume your subclass named TableHeaderCell:
for column in self.tableView.tableColumns {
let col:NSTableColumn = column as! NSTableColumn
//you can operate with header cells even for view-based tableView's
//although the documentation says otherwise.
col.headerCell = TableHeaderCell(textCell: col.title)
//or what initialiser you will have
}
Then in TableHeaderCell's drawWithFrame method you should have:
override func drawWithFrame(cellFrame: NSRect, inView controlView: NSView) {
let headerView = controlView as! HashTableHeaderView
let columnIndex = headerView.columnAtPoint(cellFrame.origin)
if columnIndex == -1 {
return
}
//parent's drawWithFrame or your own draw logic:
super.drawWithFrame(cellFrame, inView: controlView)
}
After this you won't have redundant header drawn because it not belongs to any column and columnAtPoint method will return -1.

Change border "glow" color of NSTextField

I have an NSText field in MainMenu.xib and I have an action set to validate it for an email address. I want the NSTexFields border color (That blue glow) to be red when my action returns NO and green when the action returns YES. Here is the action:
-(BOOL) validEmail:(NSString*) emailString {
NSString *regExPattern = #"^[A-Z0-9._%+-]+#[A-Z0-9.-]+\\.[A-Z]{2,4}$";
NSRegularExpression *regEx = [[NSRegularExpression alloc] initWithPattern:regExPattern options:NSRegularExpressionCaseInsensitive error:nil];
NSUInteger regExMatches = [regEx numberOfMatchesInString:emailString options:0 range:NSMakeRange(0, [emailString length])];
NSLog(#"%ld", regExMatches);
if (regExMatches == 0) {
return NO;
} else
return YES;
}
I call this function and setting the text-color right now, but I would like to set the NSTextField's glow color instead.
- (void) controlTextDidChange:(NSNotification *)obj{
if ([obj object] == emailS) {
if ([self validEmail:[[obj object] stringValue]]) {
[[obj object] setTextColor:[NSColor colorWithSRGBRed:0 green:.59 blue:0 alpha:1.0]];
[reviewButton setEnabled:YES];
} else {
[[obj object] setTextColor:[NSColor colorWithSRGBRed:.59 green:0 blue:0 alpha:1.0]];
[reviewButton setEnabled:NO];
}
}
}
I am open to sub-classing NSTextField, but the cleanest way to do this would be greatly appreciated!
My solution for Swift 2 is
#IBOutlet weak var textField: NSTextField!
...
// === set border to red ===
// if text field focused right now
NSApplication.sharedApplication().mainWindow?.makeFirstResponder(self)
// disable following focusing
textField.focusRingType = .None
// enable layer
textField.wantsLayer = true
// change border color
textField.layer?.borderColor = NSColor.redColor().CGColor
// set border width
textField.layer?.borderWidth = 1
The way to go about this is to subclass NSTextFieldCell and draw your own focus ring. As far as I can tell there's no way to tell the system to draw the focus ring using your own color, so you'll have to call setFocusRingType:NSFocusRingTypeNone, check if the control has first responder status in your drawing method, and if so draw a focus ring using your own color.
If you decide to use this approach remember that the focus ring is a user defined style (blue or graphite), and there's no guarantee future versions of OSX won't allow the user to change the standard color to red or green. It's also likely the focus ring drawing style will change in future versions of OSX, at which point you'll have to update your drawing code in order for things to look right.

NSTextField in NSTableCellView

I have a view based NSTableView with a custom NSTableCellView. This custom NSTableCellView has several labels (NSTextField). The whole UI of the NSTableCellView is built in IB.
The NSTableCellView can be in a normal state and in a selected state. In the normal state all text labels should be black, in the selected state they should be white.
How can I manage this?
Override setBackgroundStyle: on the NSTableCellView to know when the background changes which is what affects what text color you should use in your cell.
For instance:
- (void)setBackgroundStyle:(NSBackgroundStyle)style
{
[super setBackgroundStyle:style];
// If the cell's text color is black, this sets it to white
[((NSCell *)self.descriptionField.cell) setBackgroundStyle:style];
// Otherwise you need to change the color manually
switch (style) {
case NSBackgroundStyleLight:
[self.descriptionField setTextColor:[NSColor colorWithCalibratedWhite:0.4 alpha:1.0]];
break;
case NSBackgroundStyleDark:
default:
[self.descriptionField setTextColor:[NSColor colorWithCalibratedWhite:1.0 alpha:1.0]];
break;
}
}
In source list table views the cell view's background style is set to Light, as is its textField's backgroundStyle, however the textField also draws a shadow under its text and haven't yet found exactly what is controlling that / determining that should it happen.
Probably the easiest way to accomplish this would be to subclass NSTextField and to override the drawRect: method in your subclass. There you can determine whether the NSTableCellView instance containing your NSTextField instances is currently selected by using this code (which I use with a NSOutlineView, but it should also work with NSTableView):
BOOL selected = NO;
id tableView = [[[self superview] superview] superview];
if ([tableView isKindOfClass:[NSTableView class]]) {
NSInteger row = [tableView selectedRow];
if (row != -1) {
id cellView = [tableView viewAtColumn:0 row:row makeIfNecessary:YES];
if ([cellView isEqualTo:[self superview]]) selected = YES;
}
}
Then draw the view like this:
if (selected) {
// set your color here
// draw [self stringValue] here in [self bounds]
} else {
// call [super drawRect]
}
This works no matter what style the table view has:
- (void)setBackgroundStyle:(NSBackgroundStyle)backgroundStyle {
[super setBackgroundStyle:backgroundStyle];
NSTableView *tableView = self.enclosingScrollView.documentView;
BOOL tableViewIsFirstResponder = [tableView isEqual:[self.window firstResponder]];
NSColor *color = nil;
if(backgroundStyle == NSBackgroundStyleLight) {
color = tableViewIsFirstResponder ? [NSColor lightGrayColor] : [NSColor darkGrayColor];
} else {
color = [NSColor whiteColor];
}
myTextField.textColor = color;
}
Swift 4
override var backgroundStyle: NSView.BackgroundStyle {
get {
return super.backgroundStyle
}
set {
self.yourCustomLabel.textColor = NSColor(calibratedWhite: 0.0, alpha: 1.0)//black
}
}

Resources