How can I set [NSTextView selectedTextAttributes] on a background window?
Asked Answered
B

3

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

Belenbelesprit answered 18/4, 2013 at 1:52 Comment(6)
Have you tried subclassing NSWindow and overriding resignKeyWindow?Swadeshi
@Swadeshi what should I do in that method? I just tried setting selectedTextAttirbutes but it doesn't have any effect.Belenbelesprit
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.Swadeshi
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.Belenbelesprit
Did you try setting the attributes on the window's field editor instead (also a NSTextView)? Reference here.Islek
@Islek 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.Belenbelesprit
C
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.

Compost answered 3/1, 2016 at 15:49 Comment(3)
Just curious, why call setFill? I would expect passing the new colour to fillBackgroundRectArray would handle everything properly?Nonchalant
@Nonchalant I updated my answer to provide that information. It's too long to be in comment.Compost
In Mac 10.14 this no longer works had to check for PFColor.unemphasizedSelectedContentBackgroundColor()Schmo
K
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
Klong answered 11/5, 2013 at 2:33 Comment(2)
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.Belenbelesprit
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.Perusal
C
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
}
Ctesiphon answered 20/11, 2019 at 9:35 Comment(1)
This works and is the best answer IMO.Rhodolite

© 2022 - 2024 — McMap. All rights reserved.