Prevent NSTableRowView from changing highlight when not in focus? - cocoa

I've got an NSSplitView with a table view in the left pane. I've noticed that when I have an item selected in the left pane and I change focus to the right pane, the left pane loses focus and the highlighted row's highlight color turns to gray and the text turns black.
I have overridden the highlight color by overriding drawSelectionInRect in NSTableRowView. By doing this, the highlight color remains the same custom color, but the text turns dark which looks wrong.
Can I either let the highlight color change when the table view is out of focus, or prevent the text from turning dark when it's out of focus?

It appears that OS X calls setBackgroundStyle on the row view and its subviews when you click on the other view in the split view. Interestingly it seems to only happen to NSOutlineView.
Since I have code in place already to handle changing my images to different images when rows are selected (for improved contrast), and I am explicitly setting the background style when I need to using outlineViewSelectionDidChange and outlineViewSelectionIsChanging I overrode setBackgroundStyle to be a no-op and I made my own custom setter which changed the _backgroundStyle ivar.
It looks like this:
#implementation TextFieldWithHighlighting
#synthesize backgroundStyle = _backgroundStyle;
- (NSBackgroundStyle) backgroundStyle {
return _backgroundStyle;
}
#synthesize secretBackgroundStyle = _secretBackgroundStyle;
- (NSBackgroundStyle) secretBackgroundStyle {
return _backgroundStyle;
}
- (void)setBackgroundStyle:(NSBackgroundStyle)backgroundStyle {
}
- (void) setSecretBackgroundStyle:(NSBackgroundStyle)secretBackgroundStyle {
_backgroundStyle = secretBackgroundStyle;
self.needsDisplay = YES;
}
- (void)drawRect:(NSRect)dirtyRect {
if(_backgroundStyle == NSBackgroundStyleDark) {
if(self.originalTextColor == nil) {
self.originalTextColor = self.textColor;
}
self.textColor = [NSColor whiteColor];
} else {
if(self.originalTextColor) {
self.textColor = self.originalTextColor;
} else {
self.textColor = [NSColor colorWithCalibratedRed:0x40/255.0 green:0x40/255.0 blue:0x41/255.0 alpha:0xFF/255.0];
}
}
[super drawRect:dirtyRect];
}
#end
It only works for me because I am explicitly handling rows changing their selection and AFAIK I am not relying anywhere on the OS changing it for me. If I could do it again I'd just push for using the system highlight color in which case I get this functionality for free.

Just as a small contribution to your answer. This works as well with the weird contrast generated with the drawSelectionInRect implementation. Here's a Swift solution that also works implemented in an NSTableCellView:
override var backgroundStyle: NSBackgroundStyle {
get {
return self.backgroundStyle
}
set {
}
}

Related

Weird rectangle (NSBannerView) in left top of NSTableView

I'm working on an macOS app and I'm experiencing a weird issue. In the left top corner of my NSTableView (above the header), a grey rectangle is shown:
(I've added an NSBox behind the NSTableView to make it more clear in the screenshot)
With the Debug View Hierarchy, I've seen it's an NSBannerView which is added to the Scroll View wrapping the Table View.
My UI is built with Interface Builder (a storyboard). I've checked and unchecked lots of checkboxes in the Interface Builder but can't find what it is. Google and Stack Overflow also don't give any clues. Googling for "NSBannerView" even only yields some macOS header files.
How to get rid of the rectangle?
Environment details
macOS Mojave 10.14 Beta (18A365a)
Xcode 9.4.1 (9F2000)
Subclass NSTableRowView and overwrite layout, where you hide the view
- (void)layout {
[super layout];
for (NSView * v in self.subviews) {
if ([v.className isEqual:#"NSBannerView"]) {
v.hidden = YES;
}
}
}
Making this hiding thing into didAddSubview might be a better solution like the example below. Because layout is called each time you select the row.
override func didAddSubview(_ subview: NSView) {
super.didAddSubview(subview)
if subview.className == "NSBannerView" {
subview.isHidden = true
}
}
I use currently use this to correct a ugly looking NSBannerView:
func tableView(_ tableView: NSTableView, didAdd rowView: NSTableRowView, forRow row: Int) {
if rowView.isGroupRowStyle {
for view in rowView.subviews {
if let effect = view.subviews.first as? NSVisualEffectView {
effect.material = .contentBackground // Or something else, the default material for group headers is .headerView
}
}
}
}

Reposition custom UIBarbuttonItems in a UIToolbar

So I have a UIToolbar which has a custom height (which I set by overriding UIToolbar's -sizeThatFits. I want to position my UIBarButtonItems higher in the tool bar. I do so by overriding -layoutSuviews as such:
-(void)layoutSubviews
{
[super layoutSubviews];
for(UIBarButtonItem* item in self.items)
{
UIView *view;
#try {
view = [item valueForKey:#"view"];
}
#catch (NSException *exception) { NSLog(#"%#", exception.reason); }
view.frame = CGRectMake(view.frame.origin.x, view.frame.origin.y, view.frame.size.width, view.frame.size.height-33);
}
}
}
This works fine for bar buttons made with -initWithBarButtonSystemItem:, however it has no effect on bar buttons that I've made with -initWithCustomView: or -initWithTitle:.
Should I be using something different in [item valueForKey:#"view"], or is there a different way?
Never figured out a sublime answer to this question. I solved my needs by adding switches, etc to custom bar button item.

getting a NSTextField to grow with the text in auto layout?

I'm trying to get my NSTextField to have its height grow (much like in iChat or Adium) once the user types enough text to overflow the width of the control (as asked on this post)
I've implimented the accepted answer yet I can't seem to get it to work. I have uploaded my attempt at http://scottyob.com/pub/autoGrowingExample.zip
Ideally, when the text grows, the containing window should grow with it, but I'm trying baby steps here.
Solved it! (Inspired by https://github.com/jerrykrinock/CategoriesObjC/blob/master/NS(Attributed)String%2BGeometrics/NS(Attributed)String%2BGeometrics.m )
Reading the Apple Documentation is usually helpful. Apple has engineered all this text layout stuff to be powerful enough to handle all sorts of complicated edge cases which is sometimes extremely helpful, and sometimes not.
Firstly, I set the text field to wrap lines on word break, so we actually get multiple lines. (Your example code even had an if statement so it did nothing at all when wrapping was turned off).
The trick to this one was to note that when text is being edited, it’s printed by a ‘field editor’ – a heavy weight NSTextView object, owned by an NSWindow, that’s reused by whatever NSTextField is currently the ‘first responder’ (selected). The NSTextView has a single NSTextContainer (rectangle where text goes), which has a NSLayoutManager to layout the text. We can ask the layout manager how much space it wants to use up, to get the new height of our text field.
The other trick was to override the NSText delegate method - (void)textDidChange:(NSNotification *)notification to invalidate the intrinsic content size when the text is changed (so it doesn’t just wait to update when you commit changed by pressing return).
The reason I didn’t use cellSizeForBounds as you originally suggested was I couldn’t solve your problem – even when invalidating the intrinsic content size of the cell, cellSizeForBounds: continued to return the old size.
Find the example project on GitHub.
#interface TSTTextGrowth()
{
BOOL _hasLastIntrinsicSize;
BOOL _isEditing;
NSSize _lastIntrinsicSize;
}
#end
#implementation TSTTextGrowth
- (void)textDidBeginEditing:(NSNotification *)notification
{
[super textDidBeginEditing:notification];
_isEditing = YES;
}
- (void)textDidEndEditing:(NSNotification *)notification
{
[super textDidEndEditing:notification];
_isEditing = NO;
}
- (void)textDidChange:(NSNotification *)notification
{
[super textDidChange:notification];
[self invalidateIntrinsicContentSize];
}
-(NSSize)intrinsicContentSize
{
NSSize intrinsicSize = _lastIntrinsicSize;
// Only update the size if we’re editing the text, or if we’ve not set it yet
// If we try and update it while another text field is selected, it may shrink back down to only the size of one line (for some reason?)
if(_isEditing || !_hasLastIntrinsicSize)
{
intrinsicSize = [super intrinsicContentSize];
// If we’re being edited, get the shared NSTextView field editor, so we can get more info
NSText *fieldEditor = [self.window fieldEditor:NO forObject:self];
if([fieldEditor isKindOfClass:[NSTextView class]])
{
NSTextView *textView = (NSTextView *)fieldEditor;
NSRect usedRect = [textView.textContainer.layoutManager usedRectForTextContainer:textView.textContainer];
usedRect.size.height += 5.0; // magic number! (the field editor TextView is offset within the NSTextField. It’s easy to get the space above (it’s origin), but it’s difficult to get the default spacing for the bottom, as we may be changing the height
intrinsicSize.height = usedRect.size.height;
}
_lastIntrinsicSize = intrinsicSize;
_hasLastIntrinsicSize = YES;
}
return intrinsicSize;
}
#end
As a last note, I’ve never actually used auto layout myself – the demos look amazing, but whenever I actually try it myself, I can’t get it to work quite right and it makes things more complicated. However, in this case, I think it actually did save a bunch of work – without it, -intrinsicContentSize wouldn’t exist, and you’d possibly have to set the frame yourself, calculating the new origin as well as the new size (not too difficult, but just more code).
The solution by DouglasHeriot only works for fixed width text fields. In my app, I have text fields that I want to grow both horizontally and vertically. Therefore I modified the solution as follows:
AutosizingTextField.h
#interface AutosizingTextField : NSTextField {
BOOL isEditing;
}
#end
AutosizingTextField.m
#implementation AutosizingTextField
- (void)textDidBeginEditing:(NSNotification *)notification
{
[super textDidBeginEditing:notification];
isEditing = YES;
}
- (void)textDidEndEditing:(NSNotification *)notification
{
[super textDidEndEditing:notification];
isEditing = NO;
}
- (void)textDidChange:(NSNotification *)notification
{
[super textDidChange:notification];
[self invalidateIntrinsicContentSize];
}
-(NSSize)intrinsicContentSize
{
if(isEditing)
{
NSText *fieldEditor = [self.window fieldEditor:NO forObject:self];
if(fieldEditor)
{
NSTextFieldCell *cellCopy = [self.cell copy];
cellCopy.stringValue = fieldEditor.string;
return [cellCopy cellSize];
}
}
return [self.cell cellSize];
}
#end
There's a minor issue remaining: When typing spaces, the text jumps a bit to the left. However, that's not a problem in my app, because the text fields shouldn't contain spaces in most cases.
The solution of DouglasHeriot works great for me.
Here is the same code on Swift 4
class GrowingTextField: NSTextField {
var editing = false
var lastIntrinsicSize = NSSize.zero
var hasLastIntrinsicSize = false
override func textDidBeginEditing(_ notification: Notification) {
super.textDidBeginEditing(notification)
editing = true
}
override func textDidEndEditing(_ notification: Notification) {
super.textDidEndEditing(notification)
editing = false
}
override func textDidChange(_ notification: Notification) {
super.textDidChange(notification)
invalidateIntrinsicContentSize()
}
override var intrinsicContentSize: NSSize {
get {
var intrinsicSize = lastIntrinsicSize
if editing || !hasLastIntrinsicSize {
intrinsicSize = super.intrinsicContentSize
// If we’re being edited, get the shared NSTextView field editor, so we can get more info
if let textView = self.window?.fieldEditor(false, for: self) as? NSTextView, let textContainer = textView.textContainer, var usedRect = textView.textContainer?.layoutManager?.usedRect(for: textContainer) {
usedRect.size.height += 5.0 // magic number! (the field editor TextView is offset within the NSTextField. It’s easy to get the space above (it’s origin), but it’s difficult to get the default spacing for the bottom, as we may be changing the height
intrinsicSize.height = usedRect.size.height
}
lastIntrinsicSize = intrinsicSize
hasLastIntrinsicSize = true
}
return intrinsicSize
}
}
}
This solution also works when setting the string value of the text field and when it's resized by AutoLayout. It just uses the attributed text property to calculate the intrinsic content size whenever it's needed.
class AutoGrowingTextField: NSTextField {
var maximumHeight: CGFloat = 100
override var intrinsicContentSize: NSSize {
let height = attributedStringValue.boundingRect(
with: NSSize(width: bounds.width - 8, height: maximumHeight),
options: [NSString.DrawingOptions.usesLineFragmentOrigin]
).height + 5
return NSSize(width: NSView.noIntrinsicMetric, height: height)
}
override func textDidChange(_ notification: Notification) {
super.textDidChange(notification)
invalidateIntrinsicContentSize()
}
override func layout() {
super.layout()
invalidateIntrinsicContentSize()
}
}
And if you want to limit the size of the TextField (e.g.):
if (intrinsicSize.height > 100)
{
intrinsicSize = _lastIntrinsicSize;
}
else
{
_lastIntrinsicSize = intrinsicSize;
_hasLastIntrinsicSize = YES;
}
(Diff)
One thing I’m having trouble with is getting the NSTextField embedded in an NSScrollView and having it work properly (especially within an NSStackView). Going to look at whether it wouldn’t be easier with NSTextView instead.
I came up with an alternative solution that works well for me:
- (NSSize)intrinsicContentSize {
return [self sizeThatFits:NSMakeSize(self.frame.size.width, CGFLOAT_MAX)];
}
- (void)textDidChange:(NSNotification *)notification {
[super textDidChange:notification];
[self invalidateIntrinsicContentSize];
}
Basically we're just constraining the width to the given width of the element and based on that calculate the fitting height.

NSColorPanel blocking mouse up events

I am using a NSColorWell which is set to continuously update. I need to know when the user is done editing the control (mouse up) from the color picker in the color panel.
I installed an event monitor and am successfully receiving mouse down and mouse moved messages, however NSColorPanel appears to block mouse up.
The bottom line is that I want to add the final selected color to my undo stack without all the intermediate colors generated while the user is choosing their selection.
Is there a way of creating a custom NSColorPanel and replacing the shared panel with the thought of overriding its mouseUp and sending a message?
In my research this issue has been broached on a few occasions, however I have not read a successful resolution.
Regards,
- George Lawrence Storm, Keencoyote Invention Services
I discovered that if we observe color keypath of NSColorPanel we get called one extra time on mouse up events. This allows us to ignore action messages from NSColorWell when the left mouse button is down and to get the final color from keypath observer.
In this application delegate example code colorChanged: is a NSColorWell action method.
void* const ColorPanelColorContext = (void*)1001;
#interface AppDelegate()
#property (weak) NSColorWell *updatingColorWell;
#end
#implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)notification {
NSColorPanel *colorPanel = [NSColorPanel sharedColorPanel];
[colorPanel addObserver:self forKeyPath:#"color"
options:0 context:ColorPanelColorContext];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary *)change context:(void *)context {
if (context == ColorPanelColorContext) {
if (![self isLeftMouseButtonDown]) {
if (self.updatingColorWell) {
NSColorWell *colorWell = self.updatingColorWell;
[colorWell sendAction:[colorWell action] to:[colorWell target]];
}
self.updatingColorWell = nil;
}
}
}
- (IBAction)colorChanged:(id)sender {
if ([self isLeftMouseButtonDown]) {
self.updatingColorWell = sender;
} else {
NSColorWell *colorWell = sender;
[self updateFinalColor:[colorWell color]];
self.updatingColorWell = nil;
}
}
- (void)updateFinalColor:(NSColor*)color {
// Do something with the final color...
}
- (BOOL)isLeftMouseButtonDown {
return ([NSEvent pressedMouseButtons] & 1) == 1;
}
#end
In the Interface Builder, select your color well, and then uncheck the Continuous checkbox in the Attributes Inspector. Additionally, add the following line of code somewhere appropriate like in the applicationDidFinishLaunching: method or awakeFromNib method:
[[NSColorPanel sharedColorPanel] setContinuous:NO];
In other words, both the shared color panel and your color well need to have continuous set to NO in order for this to work properly.
The proper way to do what you want is to use NSUndoManager's -begin/endUndoGrouping. So you'd do something like this
[undoManager beginUndoGrouping];
// ... whatever code you need to show the color picker
// ...then when the color has been chosen
[undoManager endUndoGrouping];
The purpose of undo groups is exactly what you're trying to accomplish - to make all changes into a single undo.

Text color of extra label in view-based NSTableView

In a view-based NSTableView, your custom row and cell views (subclasses of NSTableRowView and NSTableCellView) get their backgroundStyle property set, so you know if the background is light or predominantly dark (for the selected, highlighted row).
This even gets passed to immediate subviews.
Now, the default text label of the table cell view reacts correctly to this, so on a dark background, the text is drawn in a suitable light color.
However, an NSTextField added to provide extra text (with a custom text color set in Interface Builder) does not automatically adhere to this convention.
Is there a simple way in the API to get the text field to play nice, or do I have to subclass it?
Instead of overriding drawRect, you could also do this:
- (void)setBackgroundStyle:(NSBackgroundStyle)backgroundStyle {
NSColor *textColor = (backgroundStyle == NSBackgroundStyleDark) ? [NSColor windowBackgroundColor] : [NSColor controlShadowColor];
self.detailTextField.textColor = textColor;
[super setBackgroundStyle:backgroundStyle];
}
See also here: http://gentlebytes.com/blog/2011/08/30/view-based-table-views-in-lion-part-1-of-2/
Just subclass NSTableCellView then implement drawRect:
- (void)drawRect:(NSRect)dirtyRect
{
// Drawing code here.
if (self.backgroundStyle == NSBackgroundStyleDark) {
[yourTextFieldIVar setTextColor:[NSColor whiteColor]];
} else if(self.backgroundStyle == NSBackgroundStyleLight) {
[yourTextFieldIVar setTextColor:[NSColor blackColor]];
}
}

Resources