setMacMainMenu with multiple windows


#1

Hi,

I’m trying to use setMacMainMenu with multiple DocumentWindows and I am running into all sorts of difficulty. For one, I’m not sure if I should be creating a single MenuBarModel, or one for each window. Secondly, in the latter case, it becomes very difficult to manage because calling setMacMainMenu(0) wipes out the menu for all windows. However, I don’t see how to make one MenuBarModel with all of its commands apply to each window.

In a semi-related issue, how does one ensure that a window responds to commands as soon as it is created. I’ve tried all sorts of things with grabKeyboardFocus, and the only thing that works for me is to start a timer that continually focuses the window until it is successful. Otherwise the user has to click inside the window first to get shortcuts to work. Simply calling grabKeyboardFocus in the constructor doesn’t work, of course, since the window is not visble yet, but nor does calling the method in something like repaint or resize.

Here’s the code (adapted from the hello world, using a single file and taking out MainComponent.cpp/h). You can use File->New to create a new window. See the HelloWorldWindow constructor and closeButtonPressed for the setMacMainMenu code.

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

juce::ApplicationCommandManager* acm = 0;

void makeNewWindow(void);

class MainComponent : public Component, public ButtonListener,
  public ApplicationCommandTarget, public MenuBarModel
{
  Label* helloWorldLabel;
  TextButton* quitButton;
  Path internalPath1;
  public:
  
  int MyIndex;
  
  //Hello world stuff...
  MainComponent() : helloWorldLabel (0), quitButton (0)
  {
    static int counter = 0;
    MyIndex = ++counter;
    
    addAndMakeVisible (helloWorldLabel = new Label (String::empty,
                                                    T("Hello World!")));
    helloWorldLabel->setFont (Font (40.0000f, Font::bold));
    helloWorldLabel->setJustificationType (Justification::centred);
    helloWorldLabel->setEditable (false, false, false);
    helloWorldLabel->setColour (Label::textColourId, Colours::black);
    helloWorldLabel->setColour (TextEditor::textColourId, Colours::black);
    helloWorldLabel->setColour (TextEditor::backgroundColourId, Colour (0x0));
    addAndMakeVisible (quitButton = new TextButton (String::empty));
    quitButton->setButtonText (T("Quit"));
    quitButton->addListener (this);

    addKeyListener(acm->getKeyMappings());

    setSize (600, 300);
  }

  ~MainComponent()
  {
    deleteAndZero (helloWorldLabel);
    deleteAndZero (quitButton);
  }

  void paint (Graphics& g)
  {
    g.fillAll (Colour (0xffc1d0ff));
    g.setColour (Colours::white);
    g.fillPath (internalPath1);
    g.setColour (Colour (0xff6f6f6f));
    g.strokePath (internalPath1, PathStrokeType (5.2000f));
  }

  void resized()
  {
    helloWorldLabel->setBounds (152, 80, 296, 48);
    quitButton->setBounds (getWidth() - 176, getHeight() - 60, 120, 32);
    internalPath1.clear();
    internalPath1.startNewSubPath (136.0f, 80.0f);
    internalPath1.quadraticTo (176.0f, 24.0f, 328.0f, 32.0f);
    internalPath1.quadraticTo (472.0f, 40.0f, 472.0f, 104.0f);
    internalPath1.quadraticTo (472.0f, 192.0f, 232.0f, 176.0f);
    internalPath1.lineTo (184.0f, 216.0f);
    internalPath1.lineTo (200.0f, 168.0f);
    internalPath1.quadraticTo (96.0f, 136.0f, 136.0f, 80.0f);
    internalPath1.closeSubPath();
  }

  void buttonClicked (Button* buttonThatWasClicked)
  {
    if (buttonThatWasClicked == quitButton)
      JUCEApplication::quit();
  }
  
  //Command stuff...
    
  juce::ApplicationCommandTarget* getNextCommandTarget(void)
  {
    return findFirstTargetParentComponent();
  }

  void getAllCommands(juce::Array<juce::CommandID>& Commands)
  {
    Commands.add(0x1000);
    Commands.add(0x2000);
    Commands.add(0x3000);
    Commands.add(0x4000);
  }

  juce::String getCommandNameFromID(const juce::CommandID ID)
  {
    std::cout << "getCommandNameFromID" << std::endl;
    if(ID == 0x1000) return "New";
    else if(ID == 0x2000) return "Open";
    else if(ID == 0x3000) return "Copy";
    else if(ID == 0x4000) return "Paste";
    else return "Unknown";
  }
  
  void getShortcuts(const juce::CommandID ID,
    juce::ApplicationCommandInfo& Info)
  {    
    std::cout << "getShortcuts: " << ID << std::endl;
  }

  void getCommandInfo(juce::CommandID ID,
    juce::ApplicationCommandInfo& Info)
  {
    if(ID == 0x1000) Info.shortName = "New";
    else if(ID == 0x2000) Info.shortName = "Open";
    else if(ID == 0x3000) Info.shortName = "Copy";
    else if(ID == 0x4000) Info.shortName = "Paste";
    if(ID == 0x1000)
      Info.addDefaultKeypress('n', juce::ModifierKeys::commandModifier);
    else if(ID == 0x2000)
      Info.addDefaultKeypress('o', juce::ModifierKeys::commandModifier);
    else if(ID == 0x3000)
      Info.addDefaultKeypress('c', juce::ModifierKeys::commandModifier);
    else if(ID == 0x4000)
      Info.addDefaultKeypress('v', juce::ModifierKeys::commandModifier);
    Info.setActive(true);
    Info.setTicked(false);
    std::cout << "getCommandInfo" << std::endl;
  }

  bool perform(const juce::ApplicationCommandTarget::InvocationInfo& Info)
  {
    if(Info.commandID == 0x1000)
      makeNewWindow();
    return true;
  }
  
  //Menu stuff...

  const juce::StringArray getMenuBarNames(void)
  {
    if(MyIndex == 1)
    {
      const char* TopLevelMenus[3] = {"FileA", "EditA", 0};
      return juce::StringArray(TopLevelMenus);
    }
    else
    {
      const char* TopLevelMenus[3] = {"FileB", "EditB", 0};
      return juce::StringArray(TopLevelMenus);
    }
  }
  
  const juce::PopupMenu getMenuForIndex(int Index, const juce::String& Name)
  {
    juce::PopupMenu Menu;

    if(Name == "File")
    {
      Menu.addCommandItem(acm, 0x1000);
      Menu.addCommandItem(acm, 0x2000);
    }
    else if(Name == "Edit")
    {
      Menu.addCommandItem(acm, 0x3000);
      Menu.addCommandItem(acm, 0x4000);
    }
    return Menu;
  }
  
  void menuItemSelected(int ID, int TopLevelMenuIndex) {}
};

struct HelloWorldWindow : public DocumentWindow
{
  HelloWorldWindow() : DocumentWindow (T("Menu Test"), Colours::lightgrey,
    DocumentWindow::allButtons, true)
  {
    MainComponent* const contentComponent = new MainComponent();
    acm->registerAllCommandsForTarget(contentComponent);
    acm->registerAllCommandsForTarget(JUCEApplication::getInstance());
    setContentComponent (contentComponent, true, true);
    setBounds(contentComponent->MyIndex * 50, contentComponent->MyIndex * 50, 
      getWidth(), getHeight());
    setVisible(true);
    
    MenuBarModel::setMacMainMenu(contentComponent);
    //setMenuBar(contentComponent);
    contentComponent->setApplicationCommandManagerToWatch(acm);
  }
  
  void closeButtonPressed()
  {
    /*
    //Scenario 1: After window is closed, click the main menu crashes the app.
    delete this;
    //*/
    
    //*
    //Scenario 2: After window closes, main menu disappers
    MenuBarModel::setMacMainMenu(0);
    delete this;
    //*/
    
    //*
    
    //*/
    
    //JUCEApplication::quit();
  }
};

void makeNewWindow(void)
{
  new HelloWorldWindow;
}

class JUCEHelloWorldApplication : public JUCEApplication
{
  public:
  void initialise (const String& commandLine)
  {
    acm = new juce::ApplicationCommandManager;
    new HelloWorldWindow;
  }
  
  JUCEHelloWorldApplication() {}
  void shutdown(){}
  const String getApplicationName() {return "Menu Test";}
  const String getApplicationVersion(){return "1.0";}
  bool moreThanOneInstanceAllowed(){return true;}
  void anotherInstanceStarted (const String& commandLine){}
};

START_JUCE_APPLICATION (JUCEHelloWorldApplication)

Thanks!
Andrew


#2

I’m trying to use setMacMainMenu with multiple DocumentWindows and I am running into all sorts of difficulty.

I’d suggest having one MenuBarModel for your application unless you have more than one type of DocumentWindow with radically different menus.

When you execute a command or populate your menus, start by getting the frontmost TopLevelWindow with juce::TopLevelWindow:: getActiveTopLevelWindow() and then dynamic_cast that to your DocumentWindow-derived class (don’t forget to check for NULL here!) and then use that window to either make the menu or run the command.

In a semi-related issue, how does one ensure that a window responds to commands as soon as it is created.

Good question, I’ve been wondering that myself just the last few days! Jules?


#3

Yes, good question… I’d need to do some investigating, as I don’t think I’ve seen the problem. Will try to look at it when I get a moment.


#4

[quote=“TomSwirly”]>
I’d suggest having one MenuBarModel for your application unless you have more than one type of DocumentWindow with radically different menus.
[/quote]

The logical way to do this would seem to have your JUCEApplication inheritor also be a MenuBarModel, allowing the application to handle events like closing the last window, or opening a new window when one isn’t open.

The problem with that is that MenuBarModel is an AsyncUpdater that will be called as the application is being constructed, and this will throw an exception because MessageManager::instance might not have been set up yet.

I can’t believe I’m the first person to try this out, so what’s the solution to handling MenuBar commands when there are no windows open?


#5

You could either create a custom MenuBarModel that responds to them, or use ApplicationCommandManager::setFirstCommandTarget to a non-UI object.


#6

I strongly recommend not doing that. :smiley:

There’s nothing per incorrect with it, your program would work, but Juce, for all its virtues, does rather encourage somewhat too large classes because of the multiple inheritance, so glueing together your JUCEApplication (which could be veryvery large unless you’re really careful to delegate all its functionality, as I do) and your MenuBarModel (which is almost certainly large as it’s probably a silly idea to delegate its functions elsewhere, and menu entries in a real-world application are numerous) is a Bad Idea because you’re pretty well certain to create a great huge whopping class that will get harder and harder to maintain as your program grows.


#7

In fact, I’d recommend not making your JUCEApplication class inherit from anything (apart from JUCEApplication, obviously!)


#8

Yes, good question… I’d need to do some investigating, as I don’t think I’ve seen the problem. Will try to look at it when I get a moment.[/quote]

What was the outcome on this?


#9

I’ve continued the grabKeyboardFocus issue on this thread with a workaround:
http://www.rawmaterialsoftware.com/viewtopic.php?f=4&t=9965