Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to draw custom window controls (close, minimize, and zoom buttons)

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.

like image 935
Alex Marchant Avatar asked Sep 18 '12 05:09

Alex Marchant


1 Answers

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.

like image 136
Alex Marchant Avatar answered Nov 09 '22 09:11

Alex Marchant