SystemTrayIconComponent support using NSStatusBar

Attached is an enhancement for the SystemTrayIconComponent that places the icon at the right hand side of the main menu where all those nice little icons live on the mac.
It would be great if this could be integrated into JUCE.

Cool, thanks - will have a look at that when I get a moment!

Attached is an updated version that uses the bounds of the invisible tray component to provide information about where the clicked nsstatusitem is positionend. This can be useful if you want to show a window which has an arrow pointing to the nsstatusitem for example.

Just searching for this actually…

This stuff’s checked in now if you want to let me know if it works…

I have tested it. It works fine. Thanks for integrating!

PS: If I could just hook up a JUCE component the Mac OS way (http://www.rawmaterialsoftware.com/viewtopic.php?f=2&t=11365). That would be awesome, my mini player is already finished and “only” needs to be hooked up to the status bar icon :wink:

Thanks, this is a very helpful. I have a problem though, it only seems to react to the left mouse button. When I click on the right mouse button, the handleStatusItemAction isn’t even executed. Any ideas?

The current implementation uses only an image for the NSStatusItem. This supports left click and command left click actions. If you really need a right click action the implementation would need to be implemented in a different way, using a custom view.

Well, if it has to play nice with other titlebar icons on MacOSX, left and right click actually should behave identical.

This seems to do the trick. There’s still one thing I haven’t figured out yes. When using a custom view, the NSStatusItem background highlighting doesn’t work anymore so I tried to simulate it. However, it can easily get out of sync since it’s not tied to a popup menu being visible or not. I guess there are some events that I could make it react to besides mouseDown, but I don’t know Cocoa well enough. Any thoughts on what I could use to detect when someone for instance clicks outside of the Juce popup menu or changes application focus. It must be possible since the default NSStatusItem highlight implementation seems to do it right.

/*
  ==============================================================================

   This file is part of the JUCE library - "Jules' Utility Class Extensions"
   Copyright 2004-11 by Raw Material Software Ltd.

  ------------------------------------------------------------------------------

   JUCE can be redistributed and/or modified under the terms of the GNU General
   Public License (Version 2), as published by the Free Software Foundation.
   A copy of the license is included in the JUCE distribution, or can be found
   online at www.gnu.org/licenses.

   JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
   A PARTICULAR PURPOSE.  See the GNU General Public License for more details.

  ------------------------------------------------------------------------------

   To release a closed-source product which uses JUCE, commercial licenses are
   available: visit www.rawmaterialsoftware.com/juce for more information.

  ==============================================================================
*/

namespace MouseCursorHelpers
{
    extern NSImage* createNSImage (const Image&);
}

class SystemTrayIconComponent::Pimpl
{
public:
    Pimpl (SystemTrayIconComponent& iconComp, const Image& im)
        : owner (iconComp), statusItem (nil),
          statusIcon (MouseCursorHelpers::createNSImage (im)),
          highlight (false)
    {
        static SystemTrayViewClass cls;
        view = [cls.createInstance() init];
        SystemTrayViewClass::setOwner (view, this);
        SystemTrayViewClass::setImage (view, statusIcon);
        
        setIconSize();

        statusItem = [[[NSStatusBar systemStatusBar] statusItemWithLength: NSSquareStatusItemLength] retain];
        [statusItem setView: view];
    }

    ~Pimpl()
    {
        [statusItem release];
        [view release];
        [statusIcon release];
    }

    void updateIcon (const Image& newImage)
    {
        [statusIcon release];
        statusIcon = MouseCursorHelpers::createNSImage (newImage);
        setIconSize();
        SystemTrayViewClass::setImage (view, statusIcon);
    }

    void handleStatusItemAction (NSEvent* e)
    {
        NSEventType type = [e type];

        const bool isLeft  = (type == NSLeftMouseDown  || type == NSLeftMouseUp);
        const bool isRight = (type == NSRightMouseDown || type == NSRightMouseUp);

        if (owner.isCurrentlyBlockedByAnotherModalComponent())
        {
            if (isLeft || isRight)
                if (Component* const current = Component::getCurrentlyModalComponent())
                    current->inputAttemptWhenModal();
        }
        else
        {
            ModifierKeys eventMods (ModifierKeys::getCurrentModifiersRealtime());

            if (([e modifierFlags] & NSCommandKeyMask) != 0)
                eventMods = eventMods.withFlags (ModifierKeys::commandModifier);

            NSRect r = [[e window] frame];
            r.origin.y = [[[NSScreen screens] objectAtIndex: 0] frame].size.height - r.origin.y - r.size.height;
            owner.setBounds (convertToRectInt (r));

            const Time now (Time::getCurrentTime());

            if (isLeft || isRight)  // Only mouse up is sent by the OS, so simulate a down/up
            {
                owner.mouseDown (MouseEvent (Desktop::getInstance().getMainMouseSource(),
                                             Point<int>(),
                                             eventMods.withFlags (isLeft ? ModifierKeys::leftButtonModifier
                                                                         : ModifierKeys::rightButtonModifier),
                                             &owner, &owner, now,
                                             Point<int>(), now, 1, false));

                owner.mouseUp (MouseEvent (Desktop::getInstance().getMainMouseSource(),
                                           Point<int>(), eventMods.withoutMouseButtons(),
                                           &owner, &owner, now,
                                           Point<int>(), now, 1, false));
            }
            else if (type == NSMouseMoved)
            {
                owner.mouseMove (MouseEvent (Desktop::getInstance().getMainMouseSource(),
                                             Point<int>(), eventMods,
                                             &owner, &owner, now,
                                             Point<int>(), now, 1, false));
            }
        }
    }

private:
    SystemTrayIconComponent& owner;
    NSStatusItem* statusItem;
    NSImage* statusIcon;
    NSControl* view;
    bool highlight;

    void setIconSize()
    {
        [statusIcon setSize: NSMakeSize (20.0f, 20.0f)];
    }
    
    struct SystemTrayViewClass : public ObjCClass <NSControl>
    {
        SystemTrayViewClass()  : ObjCClass <NSControl> ("JUCESystemTrayView_")
        {
            addIvar<SystemTrayIconComponent::Pimpl*> ("owner");
            addIvar<NSImage*> ("image");

            addMethod (@selector (mouseDown:), handleEventDown, "v@:@");
            addMethod (@selector (rightMouseDown:), handleEventDown, "v@:@");
            addMethod (@selector (drawRect:), drawRect, "v@:@");
            
            registerClass();
        }
        
        static void setOwner (id self, SystemTrayIconComponent::Pimpl* owner)
        {
            object_setInstanceVariable (self, "owner", owner);
        }
        
        static void setImage (id self, NSImage* image)
        {
            object_setInstanceVariable (self, "image", image);
        }
        
    private:
        static void handleEventDown (id self, SEL, NSEvent *e)
        {
            if (SystemTrayIconComponent::Pimpl* const owner = getIvar<SystemTrayIconComponent::Pimpl*> (self, "owner"))
            {
                owner->highlight = !owner->highlight;
                [self setNeedsDisplay: true];
                owner->handleStatusItemAction (e);
            }
        }
        
        static void drawRect (id self, SEL, NSRect /* rect */)
        {
            NSRect b = [self bounds];
            if (SystemTrayIconComponent::Pimpl* const owner = getIvar<SystemTrayIconComponent::Pimpl*> (self, "owner"))
            {
                [owner->statusItem drawStatusBarBackgroundInRect:b withHighlight:owner->highlight];
            }
            
            if (NSImage* const im = getIvar<NSImage*>(self, "image"))
            {

                NSSize s = [im size];
                NSRect rect = NSMakeRect(b.origin.x+((b.size.width-s.width)/2), b.origin.y+((b.size.height-s.height)/2), s.width, s.height);
                [im drawInRect:rect fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1];
            }
        }
    };
    
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl)
};


//==============================================================================
void SystemTrayIconComponent::setIconImage (const Image& newImage)
{
    if (newImage.isValid())
    {
        if (pimpl == nullptr)
            pimpl = new Pimpl (*this, newImage);
        else
            pimpl->updateIcon (newImage);
    }
    else
    {
        pimpl = nullptr;
    }
}

void SystemTrayIconComponent::setIconTooltip (const String& /* tooltip */)
{
    // xxx not yet implemented!
}

I may be wrong but whatever view / component is hooked up to the icon would need to be able to turn the highlighting off. If a Cocoa Panel would be shown when clicking the status bar item the panel should turn the highlighting off when it closes or looses focus using some window delegate methods:

[code]- (void)windowWillClose:(NSNotification *)notification

Right, the problem though is that due to the Juce abstraction, there’s nothing that ensures that a view is even shown. The standard highlighting behaviour of NSStatusItem seems to catch a sufficient set of conditions, even without something explicitly turning the highlighting off, so there must we some events that would be usable for this.

I added a new method SystemTrayIconComponent::setHighlighting(bool highlight) which in the Mac OS implementation forwards the call to the pimpl object. The pimpl object sets its highlight member accordingly and forces a repaint of the custom view.

Because the MouseEvent that comes with the mouseUp callback contains a pointer to the SystemTrayIconComponent in its originalComponent member you are able to give the Component that you want to show a pointer to the SystemTrayIconComponent. Using that pointer the Component can now turn off the highlighting in its destructor for example. This also gives you the ability to turn the highlighting off directly in the mouseUp callback if no component at all is shown.

The changed code is attached.

I also made sure that I am showing the Component modally in the mouseUp callback. For example:

[code]void SWTrayIconComponent::mouseUp (const MouseEvent& event)
{
SystemTrayIconComponent trayComponent = dynamic_cast<SystemTrayIconComponent>(event.originalComponent);
MiniPlayerPanel miniPlayer(m_coreView, trayComponent);
miniPlayer.setSize(150, 52);

juce::Rectangle<int> area = event.eventComponent->getScreenBounds();
CallOutBox calloutBox(miniPlayer, area, nullptr);
calloutBox.toFront(true);
calloutBox.enterModalState (true);
ModalComponentManager::getInstance()->runEventLoopForCurrentComponent();

}
[/code]
This gives me a consistent highlighting behavior. Toggling the shown component on and off by pressing the icon again also works well.

It seems if this change is accepted we would almost be there. As previously mentioned I am not able show a component without the menubar getting changed to the menubar of my app and I am also not able to always hide the shown component when the user clicks somewhere else.

Nice! Thanks, this is exactly what I was just sitting down for to implement and try out myself. Giving your changes a go now :slight_smile:

Cool, this works now, my problem now is when showing a Juce PopupMenu, it requires the application’s main window to be in the foreground. Otherwise, when just moving away from the statusbar component, the popup menu disappears before being able to make a selection.

I can’t just make the main windows visible and be in the foreground, since the whole purpose of that statusbar menu is to be able to use the app when the window is not visible :expressionless:

Really strange … when I move the mouse around without moving over the popup menu, it stays active. It’s when I actually move over the popup menu that it automatically disappears when the application isn’t in the foreground.

It seems your stuck at same place as me now. See the discussion here http://www.rawmaterialsoftware.com/viewtopic.php?f=2&t=11365. My last post in that thread describes what needs to be done if what is displayed would be a Cocoa window. How to do it with JUCE components is where I am stuck.

I am not sure how to go forward which is really unluck, the mini player is finished :slight_smile:

Yes, looks like I’m stuck in exactly the same spot as you. The system tray icon isn’t really important for my app, it would just be cool to have and I jumped on the opportunity when you ported the Juce component to MacOSX. I don’t really have time to dig into the bowels of Cocoa for this now as there are other planned features that are much more important. I just thought that you should know that I’m going to wait now to proceed to with this until it’s more mature. Sorry that I’ll not be able to help out more with this for now.

Same here. I need to continue work on Airplay support which is a must have. While the tray icon component would be very cool its just nice to have. I may revisit it once Airplay support is working correctly.

@Jules: Is it ok to integrate the changed implementation into JUCE? It is a step forward. The highlighting can be controlled now which enables clients to consistently turn it on and off and furthermore as gbevin correctly pointed out his view based implementation allows the right mouse button click to be handled in the way that is expected on Mac OS.