8

The default value for [NSTextView selectedTextAttributes] is unusable in my app, because i allow the user to select colors (syntax highlighting) that are almost exactly the same as the background color.

I have written some math to determine a suitable color and can use this to set it:

textView.selectedTextAttributes = @{
  NSBackgroundColorAttributeName: [NSColor yellowColor],
  NSForegroundColorAttributeName: [NSColor redColor]
  };

But when the window is in the background, it still uses the system default light grey.

I've attached screenshots of the above code with active vs inactive window. — how can I change the selected text background colour of the inactive window?

active inactive

Abhi Beckert
  • 32,787
  • 12
  • 83
  • 110
  • Have you tried subclassing NSWindow and overriding `resignKeyWindow`? – CodaFi Apr 18 '13 at 02:31
  • @CodaFi what should I do in that method? I just tried setting selectedTextAttirbutes but it doesn't have any effect. – Abhi Beckert Apr 18 '13 at 04:02
  • 1
    Hm... Check NSWindow.h. There's a boat load of functions you can use to grab onto whenever the window resigns/gains key status. You can assign the attributes from there. – CodaFi Apr 18 '13 at 04:03
  • If I change selectedTextAttributes.NSForegroundColorAttributeName in resignKeyWindow (and everywhere else I tried) it works, but changing NSBackgroundColorAttributeName has no effect - it must get the color from somewhere else. – Abhi Beckert Apr 18 '13 at 04:09
  • 1
    Did you try setting the attributes on the window's **field editor** instead (also a `NSTextView`)? [Reference here](https://developer.apple.com/library/mac/#documentation/Cocoa/Reference/ApplicationKit/Classes/NSWindow_Class/Reference/Reference.html). – Pascal May 09 '13 at 23:49
  • @Pascal I hadn't tested it, no. But I just did and it seems to have no effect. Reading the docs it looks like the `fieldEditor` is only used by "simple" controls such as `NSTextField`. `NSTextView` probably doesn't use it. – Abhi Beckert May 11 '13 at 00:59

3 Answers3

11

You can override the colour by overriding drawing method of NSLayoutManager.

final class LayoutManager1: NSLayoutManager {
    override func fillBackgroundRectArray(rectArray: UnsafePointer<NSRect>, count rectCount: Int, forCharacterRange charRange: NSRange, color: NSColor) {
        let color1 = color == NSColor.secondarySelectedControlColor() ? NSColor.redColor() : color
        color1.setFill()
        super.fillBackgroundRectArray(rectArray, count: rectCount, forCharacterRange: charRange, color: color1)
        color.setFill()
    }
}

And replace NSTextView's layout manager to it.

textView.textContainer!.replaceLayoutManager(layoutManager1)

Here's full working example.


As @Kyle asks for reason of setFill, I add some update.

From Apple manual:

... the charRange and color parameters are passed in merely for informational purposes; the color is already set in the graphics state. If for any reason you modify it, you must restore it before returning from this method. ...

Which means passing-in other color into super call has no effect, and you just need to NSColor.setFill to make it work with super call. Also, the manual requires to set it back to original one.

eonil
  • 83,476
  • 81
  • 317
  • 516
  • Just curious, why call `setFill`? I would expect passing the new colour to `fillBackgroundRectArray` would handle everything properly? – Kyle Apr 07 '16 at 12:07
  • 1
    @Kyle I updated my answer to provide that information. It's too long to be in comment. – eonil Apr 15 '16 at 21:17
  • In Mac 10.14 this no longer works had to check for PFColor.unemphasizedSelectedContentBackgroundColor() – Krzysztof May 11 '22 at 12:42
6

It's not when the window is in the background it's when the NSTextView is not selected. I don't think you can change that behavior. enter image description here

You could create an attributed string and add the NSBackgroundColorAttributeName attribute to the range of the selected text when it loses focus. The attributed string stays the same color even when the focus is lost.

NSMutableAttributedString *string = [[NSMutableAttributedString alloc] initWithString:@"hello world"];
[string addAttribute:NSForegroundColorAttributeName value:[NSColor redColor] range:NSMakeRange(1, 7)];
[string addAttribute:NSBackgroundColorAttributeName value:[NSColor yellowColor] range:NSMakeRange(1, 7)];
[self.myTextView insertText:string];

enter image description here

EDIT by Abhi Beckert: this is how I implemented this answer (note I also had to disable the built in selected text attributes, or else they override the ones I'm setting):

@implementation MyTextView

- (id)initWithCoder:(NSCoder *)aDecoder
{
  if (!(self = [super initWithCoder:aDecoder]))
    return nil;

  // disable built in selected text attributes
  self.selectedTextAttributes = @{};

  return self;
}

- (id)initWithFrame:(NSRect)frameRect textContainer:(NSTextContainer *)container
{
  if (!(self = [super initWithFrame:frameRect textContainer:container]))
    return nil;

  // disable built in selected text attributes
  self.selectedTextAttributes = @{};

  return self;
}

- (void)setSelectedRanges:(NSArray *)ranges affinity:(NSSelectionAffinity)affinity stillSelecting:(BOOL)stillSelectingFlag
{
  // remove from old ranges
  for (NSValue *value in self.selectedRanges) {
    if (value.rangeValue.length == 0)
      continue;

    [self.textStorage removeAttribute:NSBackgroundColorAttributeName range:value.rangeValue];
  }

  // apply to new ranges
  for (NSValue *value in ranges) {
    if (value.rangeValue.length == 0)
      continue;

    [self.textStorage addAttribute:NSBackgroundColorAttributeName value:[NSColor yellowColor] range:value.rangeValue];
  }

  [super setSelectedRanges:ranges affinity:affinity stillSelecting:stillSelectingFlag];
}

@end
Abhi Beckert
  • 32,787
  • 12
  • 83
  • 110
Berry Blue
  • 15,330
  • 18
  • 62
  • 113
  • Thank you! I had my subclass override setSelectedRange: to manually apply attributes to the text storage, and then set the selectedTextAttribtues to an empty dictionary, and it's working. I'll edit your answer in a second to have the code I used. – Abhi Beckert May 11 '13 at 03:46
  • Beware that setting attributes to the text storage means you save the attributes as part of a colored text, as you do in rich text editors. I'd recommend using NSLayoutManager's temporary attributes instead, which are targeted for transient styling like this. – ctietze Dec 13 '17 at 08:12
1

You can specify that your NSTextView should be treated as first responder by overriding layoutManagerOwnsFirstResponder(in:) from NSLayoutManager and the selection will use your defined attributes.

In Swift 5.1 would be:

override func layoutManagerOwnsFirstResponder(in window: NSWindow) -> Bool {
    true
}
vicegax
  • 4,709
  • 28
  • 37