[OSX] Resizing issue with addToDesktop() and attachToNativeWindow

Hi,

on OSX we encounter a strange resizing issue with a component attached to a native window. We use this method to host a plugin UI in another surrounding JUCE plugin UI. There is a AudioProcessorEditor component and a child component is attached to the top-level component using addToDesktop(0, getTopLevelComponent()->getWindowHandle()). Full code can be seen below.

When the top-level component is resized (using the +/- buttons), the attached component should follow but the attached component’s y-position is incorrect afterwards. This is due to the fact that JUCE internally needs to invert the coordinate system at juce_mac_NSViewComponentPeer.mm, NSViewComponentPeer::setBounds(), l.285. The transform relies on the dimensions of the superview but this has not yet been set at this point. Hence, the resulting coordinates refer to the old parent window size.

I tried to find a fix like inverting the order of resize callbacks that are triggered in JUCE but did not succeed. My second try was to manually force a complete resize of the window hierachy with the Resize button as a workaround. But the resizes bail out as the sizes in JUCE are the same.

So my question would be if you see a potential easy fix in JUCE or if there is a good workaround to make this work in my code? Or maybe I am wrong with my understanding how this should work?

Thanks,
Sebastian

Example Code

class NativeComponent : public Component
{
public:
    
    void paint (Graphics& g)
    {
        g.fillAll (Colours::yellow);
        
        g.setColour (Colours::black);
        g.setFont (15.0f);
        g.drawFittedText ("Native window!", getLocalBounds(), Justification::centred, 1);
    }
    
};


class CompPeerTestAudioProcessorEditor  : public AudioProcessorEditor, public Button::Listener
{
public:
    CompPeerTestAudioProcessorEditor (CompPeerTestAudioProcessor& p) : AudioProcessorEditor (&p), processor (p)
    {
        setSize (400, 300);
        
        addAndMakeVisible(btnPlus);
        addAndMakeVisible(btnMinus);
        addAndMakeVisible(btnResize);
        
        btnPlus.setButtonText("+");
        btnMinus.setButtonText("-");
        btnResize.setButtonText("Trigger Resize");
        
        btnPlus.addListener(this);
        btnMinus.addListener(this);
        btnResize.addListener(this);
        
        addAndMakeVisible(natcomp);
    }

    ~CompPeerTestAudioProcessorEditor() {}

    //==============================================================================
    void paint (Graphics& g) override
    {
        g.fillAll (Colours::green);
        
        g.setColour (Colours::white);
        g.setFont (15.0f);
        g.drawFittedText ("Hello World!", getLocalBounds(), Justification::centred, 1);
        g.drawLine(0, 100, getWidth(), 100);
        g.drawLine(0, getHeight() - 100, getWidth(), getHeight() - 100);
    }
    
    void resized() override
    {
        btnPlus.setBounds(0, 0, 20, 20);
        btnMinus.setBounds(25, 0, 20, 20);
        btnResize.setBounds(50, 0, 200, 20);
        
        natcomp.setBounds(50, 100, getWidth() - 100, getHeight() - 200);
    }

    void parentHierarchyChanged() override
    {
        natcomp.removeFromDesktop();
        natcomp.addToDesktop(0, getTopLevelComponent()->getWindowHandle());
    }

private:
    
    void buttonClicked(Button* button) override
    {
        if (button == &btnPlus)
        {
            setSize(getWidth(), getHeight() + 200);
        }
        else if (button == &btnMinus)
        {
            setSize(getWidth(), getHeight() - 200);
        }
        else if (button == &btnResize)
        {
            // Force complete resize event chain by changing the size
            setSize(getWidth(), getHeight() - 1);
            setSize(getWidth(), getHeight() + 1);
        }
    }
    
    CompPeerTestAudioProcessor& processor;

    TextButton btnPlus, btnMinus, btnResize;
    NativeComponent natcomp;
    
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CompPeerTestAudioProcessorEditor)
};

Sorry for bumping this issue up a bit.

Could someone from the JUCE team at least recommend a way to force a setBounds() on all components? Or any way to fire the event chain such that all component sizes are recalculated?

That would already be suffcient for my use case. My main problem is that setBounds() does not have any effect because the component size is unchanged although the NSView did not yet pick up the correct size. So calling setBounds() again would already solve the problem.

1 Like

Just ran into the same issue. I want to show a component that’s always center-top in front of a component attached to an OpenGLContext. I had to do two things to have my overlaid component positioned correctly:

  • call setBounds() in an async callback fired via the message manager. apparently not all data is up-to-date the moment componentMovedOrResized() is called.
  • call setBounds(newbounds.expanded(1)) and then setBounds(newbounds) to circumvent the if (wasMoved || wasResized) in Component::setBounds(...)

yes, it’s a hack and it looks dorky because there’s a delay before the overlay jumpt to the correct position. The glorious pile of spaghetti workaround code now looks like this:

SafePointer<UpdateNotification> safeThis(this);

MessageManager::getInstance()->callAsync([safeThis]()
{
    if(UpdateNotification* self = safeThis.getComponent())
    {
        if(const auto parent = self->parent.getComponent())
        {
            const auto boundsInParent = self->getBounds().withCentre(parent->getLocalBounds().getCentre()).withY(0);
            const auto boundOnScreen = parent->localAreaToGlobal(boundsInParent);
            const auto boundsInRealParent = parent->getTopLevelComponent()->getLocalArea(nullptr, boundOnScreen);
            DBG(boundsInRealParent.toString());
            self->setBounds(boundsInRealParent.expanded(1));
            self->setBounds(boundsInRealParent);
        }
    }
});

Hope this helps someone.

If anybody has an idea how to do this properly, please chime in. Or maybe there’s a better way to do this. In my particular case I want to show a component in front of a component a OpenGLContext is attached to.

Thanks for your patience, and for providing some sample code to reproduce the issue. I’ve merged a fix which should allow the resizing and positioning behaviour of nested NSViews to behave as expected. In particular, your demo code works correctly with this patch applied. Hopefully this change should mean that hacks and workarounds (like slightly resizing an embedded component using an AsyncUpdater) should be unnecessary.

Here on the JUCE team we’ve all tried out this change and checked it for regressions. We think it looks good, but given that this is quite a significant and low-level change, it’s possible that there are still a few subtle bugs lurking. If you encounter any new issues with this change, please let us know!

Amazing news, thanks for tackling that bug! :slight_smile: I tested the fix in our app/plugins and it immediatley resolved a couple of very annoying issues we had to workaround for a long time.

1 Like