Objective-c – How to draw custom window controls (close, minimize, and zoom buttons)

cocoamacosobjective-c

I've made an attempt to draw custom NSButtons, but it seems I'm reinventing the wheel here. Is there a way to just replace the default images used for the close, minimize and zoom buttons?

Several apps already do it:

  • OSX 10.8's Reminders app (they appear dark grey when the window is not key, vs most appear light grey)
  • Tweetbot (All buttons look totally custom)

More info:

I can generate the system defaults as such standardWindowButton:NSWindowCloseButton. But from there the setImage setter doesn't change the appearance of the buttons.

Best Solution

Edit: Since I wrote this, INAppStore has implemented a pretty nice way to do this with INWindowButton. If you're looking for a drag and drop solution check there, but the code below will still help you implement your own.


So I couldn't find a way to alter the standardWindowButtons. Here is a walkthrough of how I created my own buttons.

Note: There are 4 states the buttons can be in

  • Window inactive Window Inactive Controls
  • Window active - normal Window Active Normal Controls
  • Window active - hover Window Active Hover Controls
  • Window active - press Window Active Press Controls

On to the walkthrough!

Step 1: Hide the pre-existing buttons

NSButton *windowButton = [self standardWindowButton:NSWindowCloseButton];
[windowButton setHidden:YES];
windowButton = [self standardWindowButton:NSWindowMiniaturizeButton];
[windowButton setHidden:YES];
windowButton = [self standardWindowButton:NSWindowZoomButton];
[windowButton setHidden:YES];

Step 2: Setup the view in Interface Builder

You'll notice on hover the buttons all change to their hover state, so we need a container view to pick up the hover.

  • Create a container view to be 54px wide x 16px tall.
  • Create 3 Square style NSButtons, each 14px wide x 16px tall inside the container view.
  • Space out the buttons so there is are 6px gaps in-between.

Setup the buttons

  • In the attributes inspector, set the Image property for each button to the window-active-normal image.
  • Set the Alternate image property to the window-active-press image.
  • Turn Bordered off.
  • Set the Type to Momentary Change.
  • For each button set the identifier to close,minimize or zoom (Below you'll see how you can use this to make the NSButton subclass simpler)

Step 3: Subclass the container view & buttons

Container:

Create a new file, subclass NSView. Here we are going to use Notification Center to tell the buttons when they should switch to their hover state.

HMTrafficLightButtonsContainer.m

// Tells the view to pick up the hover event
- (void)viewDidMoveToWindow {
    [self addTrackingRect:[self bounds]
                    owner:self
                 userData:nil
             assumeInside:NO];
}

// When the mouse enters/exits we send out these notifications
- (void)mouseEntered:(NSEvent *)theEvent {
    [[NSNotificationCenter defaultCenter] postNotificationName:@"HMTrafficButtonMouseEnter" object:self];
}
- (void)mouseExited:(NSEvent *)theEvent {
    [[NSNotificationCenter defaultCenter] postNotificationName:@"HMTrafficButtonMouseExit" object:self];        
}

Buttons:

Create a new file, this time subclass NSButton. This one's a bit more to explain so I'll just post all the code.

HMTrafficLightButton.m

@implementation HMTrafficLightButton {
    NSImage *inactive;
    NSImage *active;
    NSImage *hover;
    NSImage *press;
    BOOL activeState;
    BOOL hoverState;
    BOOL pressedState;
}

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

- (id)initWithFrame:(NSRect)frameRect {
    self = [super initWithFrame:frameRect];
    if (self) {
        [self setup];
    }
    return self;
}

- (void)setup {
    // Setup images, we use the identifier to chose which image to load
    active = [NSImage imageNamed:[NSString stringWithFormat:@"window-button-%@-active",self.identifier]];
    hover = [NSImage imageNamed:[NSString stringWithFormat:@"window-button-%@-hover",self.identifier]];
    press = [NSImage imageNamed:[NSString stringWithFormat:@"window-button-%@-press",self.identifier]];
    inactive = [NSImage imageNamed:@"window-button-all-inactive"];

    // Checks to see if window is active or inactive when the `init` is called
    if ([self.window isMainWindow] && [[NSApplication sharedApplication] isActive]) {
        [self setActiveState];
    } else {
        [self setInactiveState];
    }

    // Watch for hover notifications from the container view
    // Also watches for notifications for when the window
    // becomes/resigns main
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(setActiveState)
                                                 name:NSWindowDidBecomeMainNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(setInactiveState)
                                                 name:NSWindowDidResignMainNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(hoverIn)
                                                 name:@"HMTrafficButtonMouseEnter"
                                               object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(hoverOut)
                                                 name:@"HMTrafficButtonMouseExit"
                                               object:nil];
}

- (void)mouseDown:(NSEvent *)theEvent {
    pressedState = YES;
    hoverState = NO;
    [super mouseDown:theEvent];
}

- (void)mouseUp:(NSEvent *)theEvent {
    pressedState = NO;
    hoverState = YES;
    [super mouseUp:theEvent];
}

- (void)setActiveState {
    activeState = YES;
    if (hoverState) {
        [self setImage:hover];
    } else {
        [self setImage:active];
    }
}

- (void)setInactiveState {
    activeState = NO;
    [self setImage:inactive];
}

- (void)hoverIn {
    hoverState = YES;
    [self setImage:hover];
}

- (void)hoverOut {
    hoverState = NO;
    if (activeState) {
        [self setImage:active];
    } else {
        [self setImage:inactive];
    }
}

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

@end

In IB set the Custom Class of the container view and all 3 buttons to their respective classes that we just created.

Step 4: Set the button actions

These methods, called from the view controller, are the same as the standardWindowButtons'. Link them to the buttons in IB.

- (IBAction)clickCloseButton:(id)sender {
    [self.view.window close];
}
- (IBAction)clickMinimizeButton:(id)sender {
    [self.view.window miniaturize:sender];
}
- (IBAction)clickZoomButton:(id)sender {
    [self.view.window zoom:sender];
}

Step 5: Add the view to the window

I have a separate xib and view controller setup specifically for the window controls. The view controller is called HMWindowControlsController

(HMWindowControlsController*) windowControlsController = [[HMWindowControlsController alloc] initWithNibName:@"WindowControls" bundle:nil];
NSView *windowControlsView = windowControlsController.view;
// Set the position of the window controls, the x is 7 px, the y will
// depend on your titlebar height.
windowControlsView.frame = NSMakeRect(7.0, 10.0, 54.0, 16.0);
// Add to target view
[targetView addSubview:windowControlsView];

Hope this helps. This is a pretty lengthy post, if you think I've made a mistake or left something out please let me know.