commandStatusChanged() not reaching my ApplicationCommandTarget component


#1

I’m trying to change the menu bar commands from not active to active. I do this by changing ApplicationCommandInfo::setActive inside my component’s implementation of getCommandInfo.
As far as I understand, the way to “refresh” the command status is by calling commandStatusChanged(), yet it isn’t causing getCommandInfo() to get called, and in turn it means the menu item remains disabled.

The problem seems to be related to the ApplicationCommandManager not finding my component (although somehow it does find it every time when performing a command, no problems there).

My component does implement getNextCommandTarget() returning findFirstTargetParentComponent() and it is the only component that implements the ApplicationCommandTarget “protocol”.

Finally, I also tried to set my component as the first command target by calling getFirstCommandTarget(), but it asserts because of some check that the target and the next target are the same.

So at this point calling commandStatusChanged() only calls the App’s default implementation of getNextCommandTarget() which returns null, never reaching my component :-(.

Here’s some pseudo-code of how my app is set up:
MyApp : public JUCEApplication // by default returns nullptr for getNextCommandTarget()
MyMainWindow : public DocumentWindow, public MenuBarModel // sets up some special menu items in its constructor
MyMainComponent : public ApplicationCommandTarget // this is the window’s content component and primary app controller, in charge of handling all the commands.

Any help figuring this out will be greatly appreciated.

Cheers!


#2

Ok, solved!

In case someone runs into this, here is what I was missing:

setApplicationCommandManagerToWatch();

I have a custom menu bar model and the model wasn’t listening to command changes. Setting this in the constructor of my MenuBarModel fixed things.

Cheers!


#3

Yes was just about to post the same. Here is a simple example app if anybody else is struggling with this:

#include "../JuceLibraryCode/JuceHeader.h"

//==============================================================================
class CommandManagerApplication  : public JUCEApplication, public MenuBarModel
{
public:
    enum CommandIDs
    {
        funnyToggleCmd = 1
    };
    
    //==============================================================================
    CommandManagerApplication() : colour (Colours::yellow), pillEnabled (true) {}

    const String getApplicationName() override       { return ProjectInfo::projectName; }
    const String getApplicationVersion() override    { return ProjectInfo::versionString; }
    bool moreThanOneInstanceAllowed() override       { return true; }
    
    StringArray getMenuBarNames() override { return StringArray ("File"); }
    PopupMenu getMenuForIndex (int topLevelMenuIndex, const String& /*menuName*/) override
    {
        PopupMenu retval;
        
        retval.addCommandItem (&commandManager, funnyToggleCmd);
        retval.addCommandItem (&commandManager, StandardApplicationCommandIDs::quit);
        
        return retval;
    }
    
    void menuItemSelected (int /*menuItemID*/, int /*topLevelMenuIndex*/) override {}

    //==============================================================================
    void initialise (const String& commandLine) override
    {
        // This method is where you should put your application's initialisation code..

        mainWindow = new MainWindow (getApplicationName());
        
        commandManager.registerAllCommandsForTarget (this);
        setApplicationCommandManagerToWatch (&commandManager);
    }

    void shutdown() override
    {
        // Add your application's shutdown code here..

        mainWindow = nullptr; // (deletes our window)
    }

    //==============================================================================
    void getCommandInfo (CommandID cmdID, ApplicationCommandInfo& info) override
    {
        switch (cmdID)
        {
        case funnyToggleCmd:
            info.setInfo (TRANS("Take pill"), TRANS("Let this command transform your state of mind"),
                          "Transformative", 0);
            info.setActive (pillEnabled);
            break;
        default:
            JUCEApplication::getCommandInfo (cmdID, info);
        }
    }
    
    void getAllCommands (Array<CommandID>& cmds) override
    {
        JUCEApplication::getAllCommands (cmds);
        cmds.add (funnyToggleCmd);
    }
    
    bool perform (const InvocationInfo& info) override
    {
        switch (info.commandID)
        {
        case funnyToggleCmd:
            colour = colour.withRotatedHue (0.3f);
            if (mainWindow != nullptr)
                mainWindow->getTopLevelComponent()->repaint();
                
            return true;
        default:
            break;
        }
        
        return JUCEApplication::perform (info);
    }
    
    //==============================================================================
    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 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.
    }
    
    Colour getColour() const { return colour; }
    void setPillEnabledState (bool newState)
    {
        if (pillEnabled != newState)
        {
            pillEnabled = newState;
            commandManager.commandStatusChanged();
        }
    }

    //==============================================================================
    /*
        This class implements the desktop window that contains an instance of
        our MainContentComponent class.
    */
    class MainWindow    : public DocumentWindow
    {
    private:
        class MainContentComponent : public Component, private ToggleButton::Listener
        {
        public:
            MainContentComponent()
            : menuBar (dynamic_cast<CommandManagerApplication*> (JUCEApplication::getInstance())),
              toggle ("Enable/Disable me")
            
            {
                toggle.setClickingTogglesState (true);
                toggle.setToggleState (true, NotificationType::dontSendNotification);
                toggle.addListener (this);
                
                addAndMakeVisible (menuBar);
                addAndMakeVisible (toggle);
                
                setSize (640, 480);
            }
            
            void paint (Graphics& g) override
            {
                if (CommandManagerApplication* app = getApp())
                    g.fillAll (app->getColour());
            }
            
            void resized() override
            {
                Rectangle<int> r = getLocalBounds();
                
                menuBar.setBounds (r.removeFromTop (24));
                toggle.setBounds (r.withSizeKeepingCentre (180, 30));
            }
            
            void buttonClicked (Button*) override {}
            void buttonStateChanged (Button*) override
            {
                 if (CommandManagerApplication* app = getApp())
                     app->setPillEnabledState (toggle.getToggleState());
            }
            
        private:
            
            CommandManagerApplication* getApp() const
            {
                return dynamic_cast<CommandManagerApplication*> (JUCEApplication::getInstance());
            }
            
            MenuBarComponent menuBar;
            ToggleButton toggle;
        };
        
    public:
        MainWindow (String name)  : DocumentWindow (name,
                                                    Colours::lightgrey,
                                                    DocumentWindow::allButtons)
        {
            
            setUsingNativeTitleBar (true);
            setContentOwned (new MainContentComponent(), true);

            centreWithSize (getWidth(), getHeight());
            setVisible (true);
        }

        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.
        */

    private:
        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainWindow)
    };

private:
    ApplicationCommandManager commandManager;
    ScopedPointer<MainWindow> mainWindow;
    
    Colour colour;
    bool pillEnabled;
};

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