TextEditor inside a custom component in a PopupMenu

Hi,

We are trying to include a TextEditor in a custom component in a popup menu. On Windows it works as expected, but on Mac OS the text editor can't receive any keyboard input. The same happens for editable labels.

I attached a small test program showing the problem.

--
Roeland

 

Since file upload doesn't work, source code below:

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

class OurSwatch : public Component,
                  public TextEditor::Listener
{
public:
    OurSwatch() : showing(false)
    {
        t.setSize(200, 20);
        t.setText(text, dontSendNotification);
        t.setColour(TextEditor::backgroundColourId, Colours::yellow);

        t.addListener(this);
    }

    void paint(Graphics &g) override
    {
        g.fillAll(Colours::yellow);
        g.setColour(findColour(Label::textColourId));
        g.drawText(text, getLocalBounds(), juce::Justification::centred, false);
    }

    void mouseDown(const MouseEvent& event) override
    {
        if (showing) return;
        showing = true;

        PopupMenu m;
        m.addCustomItem(1, &t, 200, 40, false);

        m.showMenuAsync(PopupMenu::Options().withTargetComponent(this),
                        ModalCallbackFunction::forComponent (popupMenuFinishedCallback, this));
        repaint();
    }

    void textEditorTextChanged(TextEditor &t) override
    {
        text = t.getText();
        repaint();
    }

    static void popupMenuFinishedCallback(int result, OurSwatch* us)
    {
        us->showing = false;
    }

    String text;
    bool   showing;
    TextEditor t;
};

class OurWindow : public DocumentWindow
{
public:
    OurWindow()
    :
        DocumentWindow("test", Colours::black, DocumentWindow::closeButton)
    {}

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


//==============================================================================
class TestApplication  : public JUCEApplication
{
public:

    DocumentWindow *w;

    //==============================================================================
    TestApplication()
    :
        w(nullptr)
    {}

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

    //==============================================================================
    void initialise (const String&)
    {
        OurSwatch *sw = new OurSwatch();
        sw->setSize(200, 200);
        
        w = new OurWindow();
        w->setUsingNativeTitleBar(true);
        w->setTopLeftPosition(100,100);
        w->setContentOwned(sw, false);
        w->setVisible(true);
    }

    void shutdown()
    {
        delete w;
    }

    //==============================================================================
    void systemRequestedQuit()
    {
        quit();
    }

    void anotherInstanceStarted (const String& commandLine) {}
};

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

I'm interested in hearing about a fix for this as well, I've spent hours on trying to fix it myself without luck unfortunately..

On OS X it's working in my plugins, but in the standalone versions the popupmenu itself is stealing the keyboard focus and any attempt of giving the texteditor keyfocus is blocked.

If you don't really need a menu you can use a CallOutBox instead. The following code is roughly equivalent to popping up a menu.

    juce::CallOutBox::launchAsynchronously(popupContents, theButton->getScreenBounds(), NULL);

There are a few differences in behaviour with a PopupMenu:

  • The component will usually appear on the side of the component rather than below.
  • There is no way of launching an action when the call-out box is closed. launchAsynchronously always immediately returns.
  • popupContents is always deleted. Wrap it in another component if that's a problem.
  • If you want your software to run on Linux you'll get black borders around the CallOutBox component on any system which doesn't run a compositing window manager.

--
Roeland

That’s interesting. and this issue is still here.
The PopupMenu gets keyboard input but not my custom component (a slider with text box in my use case).

Bump! Any update on this?

The following works for me on the tip of the master branch, where the most important line is .withParentComponent (this) when showing the PopupMenu, without which the TextEditor won’t be able to grab focus when it’s opened and the KeyPress events won’t get through.
(You can still make the menu appear below the trigger Component by setting the parent to the top-level Component, if that’s what you’re aiming for in practise).

#include <JuceHeader.h>

class CustomItem : public juce::PopupMenu::CustomComponent
                 , private juce::Label::Listener
{
public:
    CustomItem (juce::String text, juce::TextEditor::Listener* _listener)
        : juce::PopupMenu::CustomComponent (false)
        , listener (_listener)
    {
        addAndMakeVisible (label);

        label.setEditable (true, false, false);
        label.setText (text, juce::dontSendNotification);
        label.addListener (this);
        
        label.setColour (juce::Label::backgroundColourId, findColour (juce::ResizableWindow::backgroundColourId));
    }
    
protected:
    void getIdealSize (int& idealWidth, int& idealHeight) override
    {
        idealWidth = 200;
        idealHeight = 40;
    }
    
    void resized() override
    {
        label.setBounds (getLocalBounds());
    }
    
private:
    void labelTextChanged (juce::Label*) override
    {
        
    }
    
    void editorShown (juce::Label*, juce::TextEditor& editor) override
    {
        editor.addListener (listener);
    }

    void editorHidden (juce::Label*, juce::TextEditor& editor) override
    {
        editor.removeListener (listener);
        juce::PopupMenu::CustomComponent::triggerMenuItem();
    }
    
    juce::Label label;
    juce::TextEditor::Listener* listener;
};

class OurSwatch : public Component
                , public juce::TextEditor::Listener
{
public:
    OurSwatch()
        : showing (false)
    {

    }

    void paint (juce::Graphics& g) override
    {
        g.fillAll (findColour (juce::ResizableWindow::backgroundColourId));
        g.setColour (findColour (juce::Label::textColourId));
        g.drawText (text, getLocalBounds(), juce::Justification::centred, false);
    }

    void mouseDown (const juce::MouseEvent& event) override
    {
        if (showing)
            return;
        
        showing = true;

        juce::PopupMenu m;
        m.addCustomItem (1, std::make_unique<CustomItem> (text, this), nullptr);

        auto callback = juce::ModalCallbackFunction::forComponent (popupMenuFinishedCallback, this);
        
        m.showMenuAsync (juce::PopupMenu::Options().withTargetComponent (this)
                                                   .withParentComponent (this),
                        callback);
        repaint();
    }

    void textEditorReturnKeyPressed (juce::TextEditor &t) override
    {
        text = t.getText();
        repaint();
    }

    static void popupMenuFinishedCallback (int result, OurSwatch* us)
    {
        us->showing = false;
    }

    juce::String text = "test";
    bool showing;
};

class OurWindow : public juce::DocumentWindow
{
public:
    OurWindow()
    : juce::DocumentWindow ("test", juce::Colours::black, juce::DocumentWindow::closeButton)
    {}

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


//==============================================================================
class TestApplication : public juce::JUCEApplication
{
public:
    std::unique_ptr<juce::DocumentWindow> w;

    //==============================================================================
    TestApplication()
        : w (nullptr)
    { }

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

    //==============================================================================
    void initialise (const juce::String&)
    {
        OurSwatch* sw = new OurSwatch();
        sw->setSize (200, 200);
        
        w = std::make_unique<OurWindow> ();
        w->setUsingNativeTitleBar (true);
        w->setTopLeftPosition (100,100);
        w->setContentOwned (sw, false);
        w->setVisible (true);
    }

    void shutdown()
    {
        w.reset();
    }

    //==============================================================================
    void systemRequestedQuit()
    {
        quit();
    }

    void anotherInstanceStarted (const juce::String& commandLine) {}
};

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