13

As of OS X 10.10 most of NSStatusItem has been deprecated in favour of the button property, which consists of an NSStatusBarButton. It should work like a normal button but unfortunately the cell and setCell methods in NSStatusButton have also been deprecated. As a result of this I'm struggling to find a way to keep the button highlighted after it's clicked (Normally the button is highlighted on mouse down, and unhighlighted on mouse up. I want to keep it highlighted after mouse up).

Calling [NSStatusButton setHighlighted:] in its action doesn't work because it seems to unhighlight itself once the mouse is up. On the other hand, using a delay to call it on the next loop i.e. [self performSelector: withDelay:] causes the highlight to flash in a rather unsightly way. It works, but doesn't look nice.

Setting the button type to NSToggleButton removes the highlight entirely and instead highlights the template image which was odd.

Those were the only methods I could think of. Is there anyway to override this NSButtonCell mouseUp behaviour?

Luke
  • 4,321
  • 1
  • 34
  • 56
  • Where did you read that `NSStatusItem` is deprecated? – PnotNP Oct 08 '14 at 23:48
  • @NulledPointer https://developer.apple.com/library/prerelease/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSStatusItem_Class/index.html#//apple_ref/doc/uid/TP40004118 – Luke Oct 10 '14 at 07:38
  • 4
    NSStatusItem is not deprecated, several of its methods have been deprecated in 10.10. – ctpenrose Jan 06 '15 at 22:42

9 Answers9

12

Here is one more option. Don't set NSStatusItem's action property. Instead add a local event monitor:

[NSEvent addLocalMonitorForEventsMatchingMask:(NSLeftMouseDown | NSRightMouseDown)
                                      handler:^NSEvent *(NSEvent *event) {
                                          if (event.window == self.statusItem.button.window) {
                                              [self itemClicked];
                                              return nil;
                                          }
                                          return event;
                                      }];

Then in -itemClicked highlight the button using highlight: method:

- (void)itemClicked {
    [self.statusItem.button highlight:YES];
    // Do other stuff 
}

To unhighlight just call button's highlight:NO where you need.

Anton Simakov
  • 263
  • 3
  • 12
7

Swift 3 version of Manfred Urban's answer. Works on El Capitan.

extension NSStatusBarButton {

   public override func mouseDown(_ event: NSEvent) {

        if (event.modifierFlags.contains(NSControlKeyMask)) {
            self.rightMouseDown(event)
            return
        }

        self.highlight(true)

        (self.target as? TrivialTargetClass)?.togglePopover()
    }
}

Don't forget to set the buttons highlight property to false again if appropriate.

Community
  • 1
  • 1
erikvdplas
  • 471
  • 4
  • 9
  • Works on Sierra too. – rsfinn Apr 21 '17 at 22:46
  • I was actually expecting this not to work, since [extensions are not meant to be used for overriding behavior](https://stackoverflow.com/questions/38213286/overriding-methods-in-swift-extensions). But, the override does work on Mojave. – Ryan H Jan 12 '19 at 19:24
4

I added a subview to the status item, and inside that view I added event handlers for mouseDown etc. which called [[statusItem button] highlight:true]. As it turns out setHighlighted: doesn't do the same thing as highlight:.

NSArray *array = [NSArray arrayWithObjects:[statusItem button], [self statusItemView], nil];
[[[statusItem button] superview] setSubviews:array];
//Highlight like so:
[[statusItem button] highlight:true];

EDIT: As of El Capitan this method no longer works, and neither does statusItem.button.highlight = true either

Luke
  • 4,321
  • 1
  • 34
  • 56
4

Struggling with this issue myself, I discovered that overwriting mouseDown: in a category on NSStatusBarButton works:

#import "MUTargetClass.h"

@implementation NSStatusBarButton (Additions)

- (void)mouseDown:(NSEvent *)theEvent
{
    // Relay CTRL+Click to perform a right click action
    if(theEvent.modifierFlags & NSControlKeyMask)
    {
        [self rightMouseDown:theEvent];
        return;
    }

    // Handle highlighting
    [self setHighlighted:YES];

    // Perform action on target
    [(MUTargetClass *)self.target actionSelector:self];
}

@end

MUTargetClass could then for example implement:

#import "NSStatusBarButton+Additions.h"

@implementation MUTargetClass

[…]
    self.statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength];
    NSStatusBarButton *button = [self.statusItem button];
    [button setTarget:self];
[…]

- (void)actionSelector:(id)sender
{
    // Whatever behavior a click on the button should invoke
}

[…]
    // Reset button's highlighting status when done
    [[self.statusItem button] setHighlighted:NO];
[…]

@end

Note that the functionality of CTRL+clicking the button is getting lost in the mouseDown: -override. As shown above, it can be restored by relaying the event to rightMouseDown:.

A simpler way of having the action called would be something along the lines of [self.target performSelector:self.action] in the category and [self.statusItem setAction:@selector(actionSelector:)] in the target class, however this may cause a leak in ARC projects.

Edit: This works on El Capitan, too.

Community
  • 1
  • 1
Manfred Urban
  • 428
  • 5
  • 12
3

TL;DR: Any NSButton instance (with NSImage that has template=YES) inside NSStatusItem property visually looks exactly like NSStatusBarButton. You can control their highlight property as you want.

If you want to control highlight of your NSStatusItem manually and at the same time get all appearance benefits from using NSStatusBarButton you can use slightly different approach. You can create your own NSButton instance which has its own property highlight that is completely under your control. Then you have to create NSImage instance and set its template property to YES. Then you have to add this button to [NSStatusItem view] (yes, which is softly deprecated) or even as subview to system created [NSStatusItem button]. After this you have to draw background of NSStatusItem manually with [NSStatusItem drawStatusBarBackgroundInRect:withHighlight:] (which is also deprecated, oh).

With this approach, you can combine full control of look and feel of your NSStatusItem and get automatic styling of button's image.

Valentin Shergin
  • 6,630
  • 2
  • 45
  • 50
  • 2
    I spent all day trying to work around the newer API but thanks to your comment I tried out the deprecated `drawStatusBarBackgroundInRect:withHighlight:` API and it works much better in my opinion. I'm quite sure Apple still uses the deprecated API for the their menu bar icons too since the highlight stays on. Very strange that Apple would only flash the highlight in the new `NSStatusBarButton`. – iMaddin Jun 23 '15 at 21:17
2

Anton's solution is perfect. Here it is in Swift 3:

NSEvent.addLocalMonitorForEvents(matching: .leftMouseDown) { [weak self] event in
    if event.window == self?.statusItem.button?.window {
        // Your action:
        self?.togglePopover(self?.statusItem.button)
        return nil
    }

    return event
}

I added an observer for NSApplicationWillResignActive to close the popover and set the button's isHighlighted to false.

Sam Soffes
  • 14,138
  • 9
  • 68
  • 78
1

One idea is swizzling isHighlighted of NSStatusBarButtonCell and return an arbitrary value. https://gist.github.com/questbeat/3244601c8dc779484076

But the problem is whether Apple's review team allows this approach or not...

questbeat
  • 11
  • 2
0

Luke’s answer is great. I’m just sharing my implementation based on that.

NSButton *button = [[NSButton alloc] initWithFrame:self.statusItem.button.frame];
button.alphaValue = 0;
NSArray *array = @[self.statusItem.button, button];
self.statusItem.button.superview.subviews = array;

[button sendActionOn:(NSLeftMouseDownMask | NSRightMouseDownMask)];
button.target = self;
button.action = @selector(statusItemClicked);
Xhacker Liu
  • 1,503
  • 1
  • 15
  • 25
0

If you schedule the highlighting of the button in for a subsequent execution on the main thread everything seems to work out. This does work on El Capitan as well.

    if self.appPopover.shown {
        self.appPopover.performClose(sender)
    } else {
        if let button = statusItem.button {
            // set the button's highlighted property to true
            dispatch_async(dispatch_get_main_queue(), {
                button.highlighted = true
            })

            appPopover.showRelativeToRect(button.bounds, ofView: button, preferredEdge: NSRectEdge.MinY)
        }
    }

This does work, but you might notice a little flicker due to the button state being changed from ON to OFF and then ON again. The OFF happens because of the popover display. So, to fix that, just move the appPopover.showRelativeToRect() call inside the dispatch_async() block, right after setting the button highlighted.

DarezGhost
  • 59
  • 11