DocumentWindow z-order Question

Hi JUCE developers,

I need to create a DAW-like application on Windows & MacOS that meets the following requirements:
(1) Always display the plug-in window before the main window.
(2) But, the main window should be operable even while the plug-in window is displayed.
(3) When another application (e.g. web browser, text editor, etc.) comes to the front, Both the main window and the plug-in window should go to the back.

I have an implementation that satisfies (1) and (2), but I don’t know how to implement it to satisfy (3) at the same time.
Even when another application is active, the plugin window is displayed at the front.
The current implementation is as follows. I have also created a sample project, which is attached.
PluginWindowSample.zip (9.9 KB)

<MainComponent.h>

#pragma once

#include <JuceHeader.h>

class PluginEditorWindow;

class MainComponent  : public juce::Component
{
public:
    MainComponent();
    ~MainComponent() override;

    void paint (juce::Graphics&) override;
    void resized() override;

    void SetEditorVisible(bool should_be_visible);

private:

    juce::TextButton editor_button_;
    std::unique_ptr<PluginEditorWindow> plugin_editor_window_;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)
};

<MainComponent.cpp>

#include "MainComponent.h"
#include "PluginWindow.h"

MainComponent::MainComponent()
    :editor_button_("Editor", "Open/Close Editor")
{
    editor_button_.setClickingTogglesState(true);
    editor_button_.setColour(juce::TextButton::buttonOnColourId, juce::Colours::blue);
    editor_button_.onClick = [this] 
    {
        SetEditorVisible(editor_button_.getToggleState());
    };

    addAndMakeVisible(editor_button_);
    setSize (800, 400);
}

MainComponent::~MainComponent()
{
}

void MainComponent::paint (juce::Graphics& g)
{
    g.fillAll (getLookAndFeel().findColour (juce::ResizableWindow::backgroundColourId));
    g.setColour(findColour(juce::Label::textColourId));
    g.drawText("This is MainWindow.", getLocalBounds(), juce::Justification::centredTop);
}

void MainComponent::resized()
{
    editor_button_.setCentrePosition(getLocalBounds().getCentre());
    editor_button_.centreWithSize(100, 50);
}

void MainComponent::SetEditorVisible(bool should_be_visible)
{
    if (should_be_visible)
    {
        if(plugin_editor_window_ == nullptr)
            plugin_editor_window_.reset(new PluginEditorWindow(*this));
    }
    else
    {
        plugin_editor_window_ = nullptr;
    }

    if (editor_button_.getToggleState() != should_be_visible)
        editor_button_.setToggleState(should_be_visible, juce::dontSendNotification);
}

<PluginWindow.h>


#pragma once
#include "JuceHeader.h"
#include "MainComponent.h"

class EditorContent : public juce::Component
{
public:
    EditorContent() 
    {
        setSize(600, 400);
    }

    ~EditorContent() {}

    void paint(juce::Graphics& g) override
    {
        g.setColour(findColour(juce::Label::textColourId));
        g.drawText("This is EditorWindow.", getLocalBounds(), juce::Justification::centred);
    }
};

class PluginEditorWindow :public juce::DocumentWindow
{
public:
    PluginEditorWindow(MainComponent& main_component)
        : DocumentWindow("PluginWindow", juce::LookAndFeel::getDefaultLookAndFeel().findColour(juce::DocumentWindow::backgroundColourId),
            DocumentWindow::minimiseButton | DocumentWindow::closeButton),
        main_component_(main_component)

    {
        setContentOwned(new EditorContent(), true);
        setVisible(true);
        setAlwaysOnTop(true);
    }

    ~PluginEditorWindow() override
    {
        clearContentComponent();
    }

    void closeButtonPressed() override
    {
        main_component_.SetEditorVisible(false);
    }

private:
    float getDesktopScaleFactor() const override { return 1.0f; }

    MainComponent& main_component_;
};

Does anyone know how to implement something that satisfies all three of the above conditions?

Thanks.

I had the exact same problem a few months ago with a DAW application I’m developing. I remember finding an informative post on the subject here on the forum, but now I can’t find it again. If I recall correctly the conclusion was that, when setting setAlwaysOnTop the window will stay on top no matter what - even if the application / main window goes out of focus. So you need to listen for changes and turn setAlwaysOnTop on and off accordingly.
The way I ended up doing it, was by using the fact that TopLevelWindow::getActiveTopLevelWindow() returns nullptr if none of the application’s windows are active, which makes it a good definition of when the app is focused.
I then override DocumentWindow::activeWindowStatusChanged in my MainWindow class to get a callback, when the main window goes in or out focus and hide any open PluginWindows from the desktop if the app goes out of focus.

It works great for me, but I have only checked on Mac so far. Might be different on windows and linux - would be great to know if you try it out. This is how the callback looks in MainWindow:

void MainWindow::activeWindowStatusChanged()
{
    int n = getNumTopLevelWindows();
    bool isAppActive = getActiveTopLevelWindow() != nullptr;
    for (int i = 0; i < n; i++)
    {
        if (auto w = dynamic_cast<PluginWindow*> (getTopLevelWindow (i)))
        {
            w->setAlwaysOnTop (isAppActive);
            if (isAppActive && ! w->isOnDesktop())
                w->addToDesktop();
            
            if (! isAppActive)
                w->removeFromDesktop();
        }
    }
}

Hope this helps!

1 Like

@chrhaase
Thanks for your code suggestion!

I have tried it on Windows and MacOS.
It worked correctly when the focus was shifted from MainWindow to other apps, but when the focus was shifted from PluginWindow to other apps, PluginWindow was still displayed at the forefront.
The behavior was the same on both my Windows and MacOS.

MainWindow::activeWindowStatusChanged() was not called when the focus was shifted from PluginWindow to another app.
I guess, the plugin I’m displaying is the one that requires Focus, so maybe chrhaase’s code won’t work.

Does anyone have a better way to do this?

Oh you’re right. My app has the same behaviour… activeWindowStatusChanged only gets called for the window whose status changed. So we need the same override in PluginWindow. It works here when I simply copy the activeWindowStatusChanged from MainWindow to PluginWindow

Just to add… getNumTopLevelWindows, getActiveTopLevelWindow and getTopLevelWindow are all static, so I just put the whole thing in a free function that I can call from both MainWindow::activeWindowStatusChanged and PluginWindow::activeWindowStatusChanged .

void windowStatusChanged()
{
    int n = TopLevelWindow::getNumTopLevelWindows();
    bool isAppActive = TopLevelWindow::getActiveTopLevelWindow() != nullptr;
    for (int i = 0; i < n; i++)
    {
        if (auto w = dynamic_cast<PluginWindow*> (TopLevelWindow::getTopLevelWindow (i)))
        {
            w->setAlwaysOnTop (isAppActive);
            if (isAppActive && ! w->isOnDesktop())
                w->addToDesktop();
            
            if (! isAppActive)
                w->removeFromDesktop();
        }
    }
}

@chrhaase
Thank you again!
I can confirm that the code you provided improved it.

However, I hate to say it, but I am having problems with the following two points.

(1) If you call removeFromDesktop() and addToDesktop(), the Editor of the Plugin displayed inside the PluginWindow will not be drawn correctly. (I am trying to display a VST3 plugin that I am developing)
(2) When a program is extended to display multiple PluginWindows, the z-order of the PluginWindow is fixed and switching between PluginWindows is not possible.

Sorry for the afterthought, but do you know of a way to solve these problems as well?

Thank you for the thorough testing! I want this to work as well, I just haven’t gotten to it yet.
(1) I can’t reproduce this behaviour. I have tried opening different VST3 plugins and they all get drawn correctly, also when removed and added to desktop again. Could this be something specifically with your plugin?
(2) I think this is the same here. I can shift between plugin windows, but sometimes one the windows will be hidden even though app is still in focus. I think this happens when you click the first opened window.

I put some DBG statements in my windowStatusChanged and turns out there a lot of calls to this every time I click a plugin window. I think what might be happening is that addToDesktop and removeToDesktop often will result in a call to activeWindowStatusChanged.

Thank you for checking.
(1) It seems to be a problem specific to my plugin. I’ll look into the plugin side.
(2) I also confirmed that activeWindowStatusChanged() is called many times.

JUCE may not be able to fully meet this requirement.
I don’t think JUCE’s functionality around GUI will be dramatically improved, so I will continue to consider it, including considering other cross-platform frameworks just for the GUI part.
Thank you very much for thinking with me.

I found this post while searching through the past Forum and it solved this problem for me!

I inherited the juce::Timer class from PluginWindow and implemented timerCallback() as follows.

void timerCallback() override {
    if (Process::isForegroundProcess()) {
        if (!isAlwaysOnTop()) {
            setAlwaysOnTop(true);
            setVisible(true);
        }
    } else {
        if (isAlwaysOnTop()) {
            setAlwaysOnTop(false);
            setVisible(false);
        }
    }
}
1 Like

Great, thanks for sharing!