Plugin resizing and TopLevelWindow component problem (macOS)

I have a plugin, which contains child TopLevelWindow component. On macOS, there is strange behaviour - when plugin window is resized, TopLevelComponent changes its vertical position - like it would be bottom aligned ??!!??

On Windows, behaviour is correct … despite plugin resizing, TopLevelComponent stays on its original position as expected.

Any ideas?

I can provide videos and simple code if someone would like to look at it.

Tested with JUCE 4.3.1 and the latest JUCE 5 (Win10, macOS Mavericks and High Sierra) - VST2/VST3

Here are the screenshots, toplevelcomponent is blue, plugin window is white.

Before vertical resize:


After vertical resize:

Thanks!
Franci!

Another observation: when TopLevelComponent has been moved, setting its vertical position to the same value as before doesn’t work (setBounds). Component doesn’t move as it thinks it’s already in the proper position, so i guess this is a bug in JUCE??

I’m not seeing this, unless I’m misunderstanding the issue. Here’s the code snippet that I’ve been using to test:

/*******************************************************************************
 The block below describes the properties of this PIP. A PIP is a short snippet
 of code that can be read by the Projucer and used to generate a JUCE project.

 BEGIN_JUCE_PIP_METADATA

  name:             TopLevelTest

  dependencies:     juce_audio_basics, juce_audio_devices, juce_audio_formats, juce_audio_plugin_client, 
                    juce_audio_processors, juce_audio_utils, juce_core, juce_data_structures, juce_events, 
                    juce_graphics, juce_gui_basics, juce_gui_extra
  exporters:        vs2019, xcode_mac

  moduleFlags:      JUCE_STRICT_REFCOUNTEDPOINTER=1

  type:             AudioProcessor
  mainClass:        MyPlugin

 END_JUCE_PIP_METADATA

*******************************************************************************/

#pragma once


//==============================================================================
class MyPlugin  : public AudioProcessor
{
public:
    //==============================================================================
    MyPlugin()
        : AudioProcessor (BusesProperties().withInput  ("Input",  AudioChannelSet::stereo())
                                           .withOutput ("Output", AudioChannelSet::stereo()))
    {
    }

    ~MyPlugin()
    {
    }

    //==============================================================================
    void prepareToPlay (double, int) override
    {
        // Use this method as the place to do any pre-playback
        // initialisation that you need..
    }

    void releaseResources() override
    {
        // When playback stops, you can use this as an opportunity to free up any
        // spare memory, etc.
    }

    void processBlock (AudioBuffer<float>& buffer, MidiBuffer&) override
    {
        ScopedNoDenormals noDenormals;
        auto totalNumInputChannels  = getTotalNumInputChannels();
        auto totalNumOutputChannels = getTotalNumOutputChannels();

        // In case we have more outputs than inputs, this code clears any output
        // channels that didn't contain input data, (because these aren't
        // guaranteed to be empty - they may contain garbage).
        // This is here to avoid people getting screaming feedback
        // when they first compile a plugin, but obviously you don't need to keep
        // this code if your algorithm always overwrites all the output channels.
        for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i)
            buffer.clear (i, 0, buffer.getNumSamples());

        // This is the place where you'd normally do the guts of your plugin's
        // audio processing...
        // Make sure to reset the state if your inner loop is processing
        // the samples and the outer loop is handling the channels.
        // Alternatively, you can process the samples with the channels
        // interleaved by keeping the same state.
        for (int channel = 0; channel < totalNumInputChannels; ++channel)
        {
            auto* channelData = buffer.getWritePointer (channel);

            // ..do something to the data...
        }
    }

    //==============================================================================
    AudioProcessorEditor* createEditor() override          { return new Editor (this); }
    bool hasEditor() const override                        { return true;   }

    //==============================================================================
    const String getName() const override                  { return "TopLevelTest"; }
    bool acceptsMidi() const override                      { return false; }
    bool producesMidi() const override                     { return false; }
    double getTailLengthSeconds() const override           { return 0; }

    //==============================================================================
    int getNumPrograms() override                          { return 1; }
    int getCurrentProgram() override                       { return 0; }
    void setCurrentProgram (int) override                  {}
    const String getProgramName (int) override             { return {}; }
    void changeProgramName (int, const String&) override   {}

    //==============================================================================
    void getStateInformation (MemoryBlock& destData) override
    {
        // You should use this method to store your parameters in the memory block.
        // You could do that either as raw data, or use the XML or ValueTree classes
        // as intermediaries to make it easy to save and load complex data.
    }

    void setStateInformation (const void* data, int sizeInBytes) override
    {
        // You should use this method to restore your parameters from this memory block,
        // whose contents will have been created by the getStateInformation() call.
    }

    //==============================================================================
    bool isBusesLayoutSupported (const BusesLayout& layouts) const override
    {
        // This is the place where you check if the layout is supported.
        // In this template code we only support mono or stereo.
        if (layouts.getMainOutputChannelSet() != AudioChannelSet::mono()
            && layouts.getMainOutputChannelSet() != AudioChannelSet::stereo())
            return false;

        // This checks if the input layout matches the output layout
        if (layouts.getMainOutputChannelSet() != layouts.getMainInputChannelSet())
            return false;

        return true;
    }

private:
    //==============================================================================
    struct Editor  : public AudioProcessorEditor
    {
        Editor (AudioProcessor* p) : AudioProcessorEditor (p)
        {
            addAndMakeVisible (comp);
            
            setResizable (true, true);
            setSize (400, 300);
            
            Timer::callAfterDelay (1000, [this] { setSize (400, 500); });
        }
        
        void paint (Graphics& g) override
        {
            g.fillAll (getLookAndFeel().findColour (ResizableWindow::backgroundColourId));
        }
        
        void resized() override
        {
            comp.setBounds (0, 100, getWidth(), 200);
        }
        
        //==============================================================================
        struct TopLevel : public TopLevelWindow
        {
            TopLevel()  : TopLevelWindow ("top", false)  {}
            
            void paint (Graphics& g) override
            {
                g.fillAll (Colours::hotpink);
            }
        };
        
        TopLevel comp;
    };
    
    //==============================================================================
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MyPlugin)
};


Thx for answer!

My class, based on TopLevelWindow:

class MyTopLevelWindow : public TopLevelWindow
{
public:
    MyTopLevelWindow (String name) : TopLevelWindow (name, false) {}
    ~MyTopLevelWindow () {}
    void paint (Graphics& g) override { g.fillAll (Colours::blue); };
    int getDesktopWindowStyleFlags() const override { return 
    TopLevelWindow::getDesktopWindowStyleFlags() & ~ComponentPeer::windowHasDropShadow; }

private:
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MyTopLevelWindow)
};

The main difference in my code is that i am adding top level window to desktop as a child of main editor window.

childWindow->addToDesktop (childWindow->getDesktopWindowStyleFlags(), getWindowHandle());

I can show my files here, project is minimal, but maybe it would be easier to look in, if i send it to you over email (including VS and XCode projects) ??

I programmed a workaround, because misbehaviour can be predicted and compensated, but it would be great if this could be fixed on macOS.