Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

activeinactive

like image 996
Abhi Beckert Avatar asked Apr 18 '13 01:04

Abhi Beckert


3 Answers

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.

like image 63
eonil Avatar answered Nov 12 '22 14:11

eonil


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
like image 5
Berry Blue Avatar answered Nov 12 '22 15:11

Berry Blue


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
}
like image 1
vauxhall Avatar answered Nov 12 '22 15:11

vauxhall