Message loop hanging when using SystemTrayIconComponent::showDropdownMenu

Hi :slight_smile:

I want to use a system tray icon with a native macOS menu, which is done by using the showDropdownMenu() function.

My issue is that when calling JUCEApplication::getInstance()->systemRequestedQuit(); from one of the menu items of the system tray icon, the message dispatch loop hangs until the program is clicked again (either the tray icon or the edit: clicking anywhere in the screen actually closes the program). Note that the same function called from a non-native system tray icon menu, or from the macOS menu on the top left, does not have the same issue.

In the debugger I can see the call to shutdownNSApp(); at juce_MessageManager_mac.mm:352 which seem to be fine. But then the program is stuck in [NSApp run]; of runDispatchLoop() (same file at line 335).

Just FYI, there is no example anywhere on how to use SystemTrayIconComponent::showDropdownMenu so I found my way - which might be wrong and might be the cause of this issue. If not, then it must be a bug somewhere in the JUCE code.

Here’s a test project:
test-project-with-issue.zip (3.1 KB)

Thank you!
Simon

PS: this is a continuation of a previous post, though the topic is slightly different and the subject name does not match anymore - so I feel it deserves its own topic.


PPS: Just for easy reference, here is the Main.cpp from the upload above:

#include <JuceHeader.h>


class TrayIconComponent : public juce::SystemTrayIconComponent,
                          private juce::Timer
{
  public:
    TrayIconComponent() {
        juce::Image icon(juce::Image::ARGB, 32, 32, true);

        juce::Graphics g(icon);
        g.fillAll(juce::Colours::transparentBlack);
        g.setColour(juce::Colours::white);
        g.setFont(24.0f); // Adjust the font size as needed

        juce::String text = "T";
        g.drawText(text, 0, 0, 32, 32, juce::Justification::centredLeft, true);

        setIconImage(icon, icon);
    }
    
    /// @brief This function handles the mouse click event on the system tray icon
    /// @param event
    void mouseDown(const juce::MouseEvent &event) override
    {
        juce::Process::makeForegroundProcess();
        startTimer(50); // like the JUCE demo, we use a timer to give a bit of time before showing the menu
    }


    
  private:
    void timerCallback() override {
        stopTimer();
        juce::PopupMenu m;
        m.addItem("Quit", [] () { juce::JUCEApplication::getInstance()->systemRequestedQuit(); } );
        juce::MessageManager::callAsync([this, m]() { showDropdownMenu(m); });
     }
    
    
    class MyMenuBarModel final : public juce::MenuBarModel
    {
     public:
       MyMenuBarModel() {
           juce::PopupMenu m;
           m.addItem("Quit", [] () { juce::JUCEApplication::getInstance()->systemRequestedQuit(); } );
           juce::MenuBarModel::setMacMainMenu(this, &m);
       }
       ~MyMenuBarModel() override {
           juce::MenuBarModel::setMacMainMenu (nullptr);
       }
       
       juce::StringArray getMenuBarNames() override { return {""}; }
       juce::PopupMenu getMenuForIndex (int, const juce::String&) override { return juce::PopupMenu(); }
       void menuItemSelected (int, int) override { }
       
     private:
       JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MyMenuBarModel)
    };
    
    MyMenuBarModel model;
    
    
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TrayIconComponent)
    
};



class MainWindow : public juce::DocumentWindow
{
  public:
    MainWindow (juce::String name)  : DocumentWindow (name, juce::Colours::lightgrey, DocumentWindow::allButtons) {
        setUsingNativeTitleBar (true);
        centreWithSize (400, 400);
        setResizable (true, true);
        setVisible (true);
    }

    void closeButtonPressed() override {
        juce::JUCEApplication::getInstance()->systemRequestedQuit();
    }

  private:
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainWindow)
};



class NewProjectApplication  : public juce::JUCEApplication
{
public:
    NewProjectApplication() {}

    const juce::String getApplicationName() override       { return ProjectInfo::projectName; }
    const juce::String getApplicationVersion() override    { return ProjectInfo::versionString; }
    bool moreThanOneInstanceAllowed() override             { return false; }

    //==============================================================================
    void initialise (const juce::String& commandLine) override
    {
        mainWindow.reset (new MainWindow (getApplicationName()));
        trayIconComponent.reset(new TrayIconComponent());
    }

    void shutdown() override
    {
        trayIconComponent = nullptr;
        mainWindow = nullptr;
    }

    //==============================================================================
    void systemRequestedQuit() override
    {
        // This is called when the app is being asked to quit: you can ignore this
        // request and let the app carry on running, or call quit() to allow the app to close.
        quit();
    }

    void anotherInstanceStarted (const juce::String& commandLine) override
    {
        // When another instance of the app is launched while this one is running,
        // this method is invoked, and the commandLine parameter tells you what
        // the other instance's command-line arguments were.
    }
     
    private:
        std::unique_ptr<MainWindow> mainWindow;
        std::unique_ptr<TrayIconComponent> trayIconComponent;
};


//==============================================================================
// This macro generates the main() routine that launches the app.
START_JUCE_APPLICATION (NewProjectApplication)

Has anyone a thought about this? :slight_smile:

could you try calling JUCEApplicationBase::quit(); instead of systemRequestedQuit() when you want to quit the application.

Thanks Anthony for the reply. Unfortunately that does not change anything (I think it makes sense since I could see systemRequestedQuit()calling quit()in the debugger). I also tried with and without DEBUG mode and the issue is the same.

The big issue is exiting the application, but there is also that strange thing where one out of 2 times clicking on the icon does not call SystemTrayIconComponent::mouseDown(), so the menu does not show. I’m not sure if this second issue is related to the exiting somehow.

Just FYI, using the default menu with m.showMenuAsync() does not have any of those problems.


EDIT: actually doing some more testing, the two issues are definitely related.
It seems that the issue is the program is hanging after the trayicon menu is closed. Then it won’t be possible to do anything with the program (for example opening the menu again, or exiting), until the user has clicked somewhere in the screen.

  1. Example 1 - reopening the menu

    • (1) click the icon to open the menu
    • (2) click somewhere else to close the menu
    • then (3) click one more time somewhere else
      • → then you can open the menu again, it works every time.
  2. Example 2 - exiting

    • (1) click the icon to open the menu
    • (2) click “Exit” in the menu, the menu is closed and the program hangs
    • then (3) click anywhere on the screen
      • → the program stops hanging and it exits correctly

EDIT 2: Could it be related to how the menu is created? My Cocoa/Swift skills are pretty much inexistent, but it seems JUCE is using a deprecated function to create the menu:

_
juce_SystemTrayIcon_mac.cpp:59

    void showMenu (const PopupMenu& menu)
    {
        if (NSMenu* m = createNSMenu (menu, "MenuBarItem", -2, -3, true))
        {
            setHighlighted (true);
            stopTimer();

            // There's currently no good alternative to this.
            [statusItem.get() popUpStatusItemMenu: m];

            startTimer (1);
        }
    }

Any particular reason we are using popUpStatusItemMenu and not button as suggested by the doc?

After taking a look at this I think there are clearly a few improvements we could make to this but for the time being could you try removing the timer and calling showDropdownMenu (m) directly from mouseDown()? I don’t completely understand why yet but for some reason that seems to cause the odd every other click issue.

Up until JUCE 8 we supported deployment on macOS 10.9+, according to the docs button was only available from 10.10+.

Thank you for the replies Anthony.

Very interesting find! It seems it’s a “which thread calls” kind of problem. Moving the content of the timer callback into mouseDown() was not enough, I also had to remove the Async so that the showDropdownMenu(m) call was done directly in the mouseDown() thread.

So what I did was removing all references to the Timer and replace mouseDown() with this:

    /// @brief This function handles the mouse click event on the system tray icon
    /// @param event
    void mouseDown(const juce::MouseEvent &event) override
    {
        juce::Process::makeForegroundProcess();
        
        juce::PopupMenu m;
        m.addItem("Quit", [] () { juce::JUCEApplication::getInstance()->systemRequestedQuit(); } );
        showDropdownMenu(m);
    }

This solved the issue :smiley:
Hopefully that has not created new issues, I read multiple times that it’s a good idea to show menus async (i.e. from the message thread).