iain
iain

Reputation: 5683

Changing color of NSWindow title text

I know this won't be a popular question and some people don't like apps that have a non-standard look, but it is useful for my application.

Is it possible to change the color of the NSWindow's titlebar text, in a "standard" non-private API way?

I know it's possible if I use private API (as mentioned in this answer) but I presume it is possible to do it without private API as Pixelmator has done it and not been rejected from MAS. I know it's also possible to do it by making a borderless window and drawing everything myself, but I don't think that's how Pixelmator is doing it, because they still get all the additional bits that comes with the standard NSWindow titlebar; draggable icons, rename the window, the dropdown menu for document revisions and the fullscreen button.

Basically, I've made a black window using setBackgroundColor: but the text still comes up as black, which doesn't work on a black background.

So does anyone know a way to do this, or how Pixelmator is doing it?

Upvotes: 12

Views: 5639

Answers (4)

Guilherme Rambo
Guilherme Rambo

Reputation: 2046

This is the way I do It:

#import <objc/runtime.h>

@interface SOWindow : NSWindow
@end

@interface SOWindowFrameOverrides : NSView
@end

@implementation SOWindow

+ (void)load
{
    SEL selector = NSSelectorFromString(@"_currentTitleColor");
    SEL originalSelector = NSSelectorFromString(@"_original_currentTitleColor");
    Class frameClass = NSClassFromString(@"NSThemeFrame");

    Method m = class_getInstanceMethod(frameClass, selector);
    Method m2 = class_getInstanceMethod([SOWindowFrameOverrides class], selector);
    class_addMethod(frameClass, originalSelector, method_getImplementation(m), method_getTypeEncoding(m));
    method_exchangeImplementations(m, m2);
}

@end

@implementation SOWindowFrameOverrides

- (NSColor *)_currentTitleColor
{
    if ([self.window isKindOfClass:[SOWindow class]]) {
        return [NSColor redColor];
    } else {
        return [self _original_currentTitleColor];
    }
}

- (NSColor *)_original_currentTitleColor
{
    // will be filled in at runtime
    return nil;
}

@end

While the view hierarchy has changed a lot from Mavericks to Yosemite, _currentTitleColor has been in the API for a long time and will probably not change in the near future. Even tough method swizzling is a hacky way of doing It, I find It more elegant than traversing the view hierarchy, but that's just me.

If you want to customize the title even further, you can override _titleTextField on NSThemeFrame to return a customized text field (I have used It to change the backgroundStyle on my F3X project).

Upvotes: 1

Marc T.
Marc T.

Reputation: 5320

Since OS X 10.10 it should be enough to change the appearance of the window to NSAppearanceNameVibrantDark

  window.appearance = NSAppearance(named:NSAppearanceNameVibrantDark)

Thought it's worth to mention since most of the answers you can find are out of date.

Upvotes: 11

Stefan Arentz
Stefan Arentz

Reputation: 34935

Here is a solution in Swift. It is late and I'm tired so this is probably not optimal but it works.

First, here is a function to find a view within an hierarchy, with the option to skip a specific view. (Which is useful if we are going search through window.contentView.superview.subviews and we want to ignore your own views in the contentView)

func findViewInSubview(subviews: [NSView], #ignoreView: NSView, test: (NSView) -> Bool) -> NSView? {
    for v in subviews {
        if test(v) {
            return v
        } else if v != ignoreView {
            if let found = findViewInSubview(v.subviews as [NSView], ignoreView: ignoreView, test) {
                return found
            }
        }
    }
    return nil
}

And here is how you would use it, for example from an NSViewController subclass. Note that you need to do it when the window has become visible, so you can't do it in viewDidLoad.

override func viewDidAppear() {
    if let windowContentView = view.window?.contentView as? NSView {
        if let windowContentSuperView = windowContentView.superview {
            let titleView = findViewInSubview(windowContentSuperView.subviews as [NSView], ignoreView: windowContentView) { (view) -> Bool in
                // We find the title by looking for an NSTextField. You may
                // want to make this test more strict and for example also
                // check for the title string value to be sure.
                return view is NSTextField
            }
            if let titleView = titleView as? NSTextField {
                titleView.attributedStringValue = NSAttributedString(string: "Hello", attributes: [NSForegroundColorAttributeName: NSColor.redColor()])
            }
        }
    }
}

Do note that you are playing with fire. Internals like this are unspecified for a reason.

Upvotes: 3

Charlie Monroe
Charlie Monroe

Reputation: 1250

You can solve this either as mentioned in the comments to your question using some private API - you, however, won't be able to submit that app to the AppStore.

Other solution is to get [[myWindow contentView] superview] - which gets you the aforementioned NSThemeView instance. All you need to do then is to search the subviews of the view's (actually called a frame view) subviews for any instances of NSTextField and modify those. Note that the hierarchy of these private views may change with each release of OS X, potentially broking your code.

The probably best solution is to subclass (only subclass, no other customization) NSWindow and implement the -title and -setTitle: methods - for each window, you'd set the actual title to @"" (by calling [super setTitle:@""]) and then place your own, programmatically created, NSTextField instance into the frame view (i.e. [[[self contentView] superview] addSubview:myTextField], where myTextField is the text field. You need to figure out the exact placement, etc. of the field, but that's the easiest part.

Upvotes: 0

Related Questions