Creating a macOS menu bar app?

SystemTrayIconComponent seems set up to only show a simple menu. On macOS an NSMenu.

Has anyone managed to make a more complex menu bar app using JUCE…? i.e. when the menu bar icon is clicked a non-menu JUCE component is shown underneath? Like the Dropbox app, for example.

Before I embark on something highly customised does anyone have any existing insight?

Jamie

1 Like

I think I managed to do what you’re referring to. I simply changed the MainWindow class in the Main.cpp so that it doesn’t inherit DocumentWindow. It then it doesn’t have a title bar and is not draggable. Here is the part in my app that does that:

        //==============================================================================
    /*
     This loads the main component without a containing window, so it doesn't have a title bar and can't be dragged not resized.
     */
    
#if JUCE_WINDOWS || JUCE_LINUX || JUCE_MAC
    class MainWindow
    {
    public:
        MainWindow (String name)
        {
            taskbarIcon.reset (new TaskbarComponent(*this));
            mMainComponent.reset (new MainComponent ());
            mMainComponent->setSize(mainComponentW, mainComponentH);
            mMainComponent->setVisible(isMainComponentVisible);
            
            int taskbarIconX = taskbarIcon->getX();
            int taskbarIconW = taskbarIcon->getWidth();
            int taskbarIconH = taskbarIcon->getHeight();
            
            mMainComponent->addToDesktop(ComponentPeer::windowHasDropShadow);
            mMainComponent->setTopLeftPosition(taskbarIconX - mainComponentW / 2, 50);
           
        }
        
        //        void closeButtonPressed() override
        //        {
        //            // This is called when the user tries to close this window. Here, we'll just
        //            // ask the app to quit when this happens, but you can change this to do
        //            // whatever you need.
        //            JUCEApplication::getInstance()->systemRequestedQuit();
        //        }
        
        /* Note: Be careful if you override any DocumentWindow methods - the base
         class uses a lot of them, so by overriding you might break its functionality.
         It's best to do all your work in your content component instead, but if
         you really have to override any DocumentWindow methods, make sure your
         subclass also calls the superclass's method.
         */
        ~MainWindow(){
            mMainComponent = nullptr;
        }
        
         void toggleVisibilty(){
            
            isMainComponentVisible = !isMainComponentVisible;
            
             if (mMainComponent != nullptr){
                mMainComponent->setVisible(isMainComponentVisible);
            }
        }
        
        void updatePositionFromTrayIcon(int iconCenterX, int iconH){
//            int taskbarIconX = taskbarIcon->getX();
//            int taskbarIconW = taskbarIcon->getWidth();
//            int taskbarIconH = taskbarIcon->getHeight();
//
            if (mMainComponent != nullptr){
                mMainComponent->setTopLeftPosition(iconCenterX - mainComponentW / 2, iconH);
            }
            
            
        }
        
        // Just add a simple icon to the Window system tray area or Mac menu bar..
        struct TaskbarComponent  : public SystemTrayIconComponent, private Timer {
            
            TaskbarComponent( MainWindow& window) : mainWindow(window) {
                setIconImage (ImageFileFormat::loadFrom(BinaryData::mic_png, BinaryData::mic_pngSize),
                              ImageFileFormat::loadFrom(BinaryData::mic_png, BinaryData::mic_pngSize));
                setIconTooltip (JUCEApplication::getInstance()->getApplicationName());
            }
            
    
            void mouseDown (const MouseEvent& e) override {
                // On OSX, there can be problems launching a menu when we're not the foreground
                // process, so just in case, we'll first make our process active, and then use a
                // timer to wait a moment before opening our menu, which gives the OS some time to
                // get its act together and bring our windows to the front.
                
                
                int iconCenterX = Desktop::getMousePosition().x;
                int iconH = getParentMonitorArea().getY() + 5;

                mainWindow.updatePositionFromTrayIcon(iconCenterX, iconH);
                
                Process::makeForegroundProcess();
                mainWindow.toggleVisibilty();
                
            
                //startTimer (50);
            }
            
            
            // This is invoked when the menu is clicked or dismissed
            static void menuInvocationCallback (int chosenItemID, TaskbarComponent*){
                if (chosenItemID == 1)
                    JUCEApplication::getInstance()->systemRequestedQuit();
            }
            
            void timerCallback() override {
                stopTimer();
                
                PopupMenu m;
                m.addItem (1, "Quit");
                
                // It's always better to open menus asynchronously when possible.
                m.showMenuAsync (PopupMenu::Options(),
                                 ModalCallbackFunction::forComponent (menuInvocationCallback, this));
            }
            
             MainWindow& mainWindow;
            
        };
        
        
        
    private:
        int mainComponentW = 400;
        int mainComponentH = 600;
        bool isMainComponentVisible = false;
        std::unique_ptr<MainComponent> mMainComponent;
        std::unique_ptr<Component> taskbarIcon;
        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainWindow)
    };
    
    
#endif
2 Likes

Is there any way to get the screen position and size of the SystemTrayIconComponent?
I have been using the code above posted by @Bulien, but this uses mouse click positions to place the UI around the SystemTrayIconComponent instead of using an accurate position of the icon. In addition, the UI pops up on the wrong monitor when I have multiple monitors on MacOS, with one monitor placed above another.

I would love to mimic the behavior of the Roli Connect app on MacOS. It always consistently places the UI in the same position below the menu bar icon (maybe this app doesn’t use JUCE?). I am sure the behavior I am looking for is achievable by modifying the JUCE source code and accessing native MacOS code, but would love to know if there is an existing, cross-platform solution for this.

Thanks!

2 Likes

Has anyone seen success with this? I am looking to do something similar.