Dialog window always behind the main plugin window


#1

I noticed a different behaviour between Windows and OS X, and I can’t get to do what I want on Mac… All I want is simply to have a VST/AU plugin open a dialog window on top of the main GUI window so it’s visible.

Works fine on Windows, but on Mac it appears (and remains) behind the plugin’s window, which is quite inconvenient…

I added of few lines of code to Juce’s DemoEditorComponent.cpp/.h in order to exhibit the behaviour difference between Mac and PC. Extra code is tagged by // EXTRA STUFF // comments. Adding this code and recompiling Juce’s VST (ou AU) plugin demo project will produce a plugin that behaves differently on Win/OSX.

Is it a bug in Juce? If not, what should I do to get on OSX the same behaviour as on Windows?

Here’s the code. To make the dialog pop up, click on an empty area of the plugin’s GUI.


DemoEditorComponent.cpp

/*
  ==============================================================================

   This file is part of the JUCE library - "Jules' Utility Class Extensions"
   Copyright 2004-7 by Raw Material Software ltd.

  ------------------------------------------------------------------------------

   JUCE can be redistributed and/or modified under the terms of the
   GNU General Public License, as published by the Free Software Foundation;
   either version 2 of the License, or (at your option) any later version.

   JUCE is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with JUCE; if not, visit www.gnu.org/licenses or write to the
   Free Software Foundation, Inc., 59 Temple Place, Suite 330,
   Boston, MA 02111-1307 USA

  ------------------------------------------------------------------------------

   If you'd like to release a closed-source product which uses JUCE, commercial
   licenses are also available: visit www.rawmaterialsoftware.com/juce for
   more information.

  ==============================================================================
*/

#include "../../../../juce.h"
#include "DemoEditorComponent.h"

// EXTRA STUFF //
class TestDlg:	public Component,
						public ButtonListener
{    
public:
    /// Constructors
    TestDlg( );
    ~TestDlg();

	void buttonClicked (Button* buttonThatWasClicked);
   
	TextButton* OkButton;
};

TestDlg::TestDlg( )
{
	setSize(500, 300);
	setAlwaysOnTop(true);	// useless...
	toFront(true);	// useless...
    addAndMakeVisible (OkButton = new TextButton (T("button")));
    OkButton->setButtonText (T("Ok"));
    OkButton->addButtonListener (this);
    OkButton->setBounds (200, 250, 80, 24);
}

TestDlg::~TestDlg()
{
    deleteAndZero (OkButton);
}

// listeners
void TestDlg::buttonClicked (Button* buttonThatWasClicked)
{
    Component *p = getParentComponent();
	p->exitModalState(1);
}

void DemoEditorComponent::mouseDown (const MouseEvent & e)
{
	TestDlg *pTestDlg;
	pTestDlg = new TestDlg();
	DialogWindow::showModalDialog ("Test", pTestDlg, this, Colours::white, true);
	delete pTestDlg;
}
// /EXTRA STUFF //

//==============================================================================
// quick-and-dirty function to format a timecode string
static const String timeToTimecodeString (const double seconds)
{
    const double absSecs = fabs (seconds);
    const tchar* const sign = (seconds < 0) ? T("-") : T("");

    const int hours = (int) (absSecs / (60.0 * 60.0));
    const int mins  = ((int) (absSecs / 60.0)) % 60;
    const int secs  = ((int) absSecs) % 60;

    return String::formatted (T("%s%02d:%02d:%02d:%03d"),
                              sign, hours, mins, secs,
                              roundDoubleToInt (absSecs * 1000) % 1000);
}

// quick-and-dirty function to format a bars/beats string
static const String ppqToBarsBeatsString (const double ppq,
                                          const double lastBarPPQ,
                                          const int numerator,
                                          const int denominator)
{
    if (numerator == 0 || denominator == 0)
        return T("1|1|0");

    const int ppqPerBar = (numerator * 4 / denominator);
    const double beats  = (fmod (ppq, ppqPerBar) / ppqPerBar) * numerator;

    const int bar       = ((int) ppq) / ppqPerBar + 1;
    const int beat      = ((int) beats) + 1;
    const int ticks     = ((int) (fmod (beats, 1.0) * 960.0));

    String s;
    s << bar << T('|') << beat << T('|') << ticks;
    return s;
}


//==============================================================================
DemoEditorComponent::DemoEditorComponent (DemoJuceFilter* const ownerFilter)
    : AudioProcessorEditor (ownerFilter)
{
    // create our gain slider..
    addAndMakeVisible (gainSlider = new Slider (T("gain")));
    gainSlider->addListener (this);
    gainSlider->setRange (0.0, 1.0, 0.01);
    gainSlider->setTooltip (T("changes the volume of the audio that runs through the plugin.."));

    // get the gain parameter from the filter and use it to set up our slider
    gainSlider->setValue (ownerFilter->getParameter (0), false);

    // create and add the midi keyboard component..
    addAndMakeVisible (midiKeyboard
        = new MidiKeyboardComponent (ownerFilter->keyboardState,
                                     MidiKeyboardComponent::horizontalKeyboard));

    // add a label that will display the current timecode and status..
    addAndMakeVisible (infoLabel = new Label (String::empty, String::empty));

    // add the triangular resizer component for the bottom-right of the UI
    addAndMakeVisible (resizer = new ResizableCornerComponent (this, &resizeLimits));
    resizeLimits.setSizeLimits (150, 150, 800, 300);

    // set our component's initial size to be the last one that was stored in the filter's settings
    setSize (ownerFilter->lastUIWidth,
             ownerFilter->lastUIHeight);

    // register ourselves with the filter - it will use its ChangeBroadcaster base
    // class to tell us when something has changed, and this will call our changeListenerCallback()
    // method.
    ownerFilter->addChangeListener (this);
}

DemoEditorComponent::~DemoEditorComponent()
{
    getFilter()->removeChangeListener (this);

    deleteAllChildren();
}

//==============================================================================
void DemoEditorComponent::paint (Graphics& g)
{
    // just clear the window
    g.fillAll (Colour::greyLevel (0.9f));
}

void DemoEditorComponent::resized()
{
    gainSlider->setBounds (10, 10, 200, 22);
    infoLabel->setBounds (10, 35, 450, 20);

    const int keyboardHeight = 70;
    midiKeyboard->setBounds (4, getHeight() - keyboardHeight - 4,
                             getWidth() - 8, keyboardHeight);

    resizer->setBounds (getWidth() - 16, getHeight() - 16, 16, 16);

    // if we've been resized, tell the filter so that it can store the new size
    // in its settings
    getFilter()->lastUIWidth = getWidth();
    getFilter()->lastUIHeight = getHeight();
}

//==============================================================================
void DemoEditorComponent::changeListenerCallback (void* source)
{
    // this is the filter telling us that it's changed, so we'll update our
    // display of the time, midi message, etc.
    updateParametersFromFilter();
}

void DemoEditorComponent::sliderValueChanged (Slider*)
{
    getFilter()->setParameterNotifyingHost (0, (float) gainSlider->getValue());
}

//==============================================================================
void DemoEditorComponent::updateParametersFromFilter()
{
    DemoJuceFilter* const filter = getFilter();

    // we use this lock to make sure the processBlock() method isn't writing to the
    // lastMidiMessage variable while we're trying to read it, but be extra-careful to
    // only hold the lock for a minimum amount of time..
    filter->getCallbackLock().enter();

    // take a local copy of the info we need while we've got the lock..
    const AudioPlayHead::CurrentPositionInfo positionInfo (filter->lastPosInfo);
    const float newGain = filter->getParameter (0);

    // ..release the lock ASAP
    filter->getCallbackLock().exit();


    // ..and after releasing the lock, we're free to do the time-consuming UI stuff..
    String infoText;
    infoText << String (positionInfo.bpm, 2) << T(" bpm, ")
             << positionInfo.timeSigNumerator << T("/") << positionInfo.timeSigDenominator
             << T("  -  ") << timeToTimecodeString (positionInfo.timeInSeconds)
             << T("  -  ") << ppqToBarsBeatsString (positionInfo.ppqPosition,
                                                    positionInfo.ppqPositionOfLastBarStart,
                                                    positionInfo.timeSigNumerator,
                                                    positionInfo.timeSigDenominator);

    if (positionInfo.isPlaying)
        infoText << T("  (playing)");

    infoLabel->setText (infoText, false);

    /* Update our slider.

       (note that it's important here to tell the slider not to send a change
       message, because that would cause it to call the filter with a parameter
       change message again, and the values would drift out.
    */
    gainSlider->setValue (newGain, false);

    setSize (filter->lastUIWidth,
             filter->lastUIHeight);
}

DemoEditorComponent.h

/*
  ==============================================================================

   This file is part of the JUCE library - "Jules' Utility Class Extensions"
   Copyright 2004-7 by Raw Material Software ltd.

  ------------------------------------------------------------------------------

   JUCE can be redistributed and/or modified under the terms of the
   GNU General Public License, as published by the Free Software Foundation;
   either version 2 of the License, or (at your option) any later version.

   JUCE is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with JUCE; if not, visit www.gnu.org/licenses or write to the
   Free Software Foundation, Inc., 59 Temple Place, Suite 330,
   Boston, MA 02111-1307 USA

  ------------------------------------------------------------------------------

   If you'd like to release a closed-source product which uses JUCE, commercial
   licenses are also available: visit www.rawmaterialsoftware.com/juce for
   more information.

  ==============================================================================
*/

#ifndef DEMOJUCEPLUGINEDITOR_H
#define DEMOJUCEPLUGINEDITOR_H

#include "DemoJuceFilter.h"


//==============================================================================
/**
    This is the Component that our filter will use as its UI.

    One or more of these is created by the DemoJuceFilter::createEditor() method,
    and they will be deleted at some later time by the wrapper code.

    To demonstrate the correct way of connecting a filter to its UI, this
    class is a ChangeListener, and our demo filter is a ChangeBroadcaster. The
    editor component registers with the filter when it's created and deregisters
    when it's destroyed. When the filter's parameters are changed, it broadcasts
    a message and this editor responds by updating its display.
*/
class DemoEditorComponent   : public AudioProcessorEditor,
                              public ChangeListener,
                              public SliderListener
{
public:
    /** Constructor.

        When created, this will register itself with the filter for changes. It's
        safe to assume that the filter won't be deleted before this object is.
    */
    DemoEditorComponent (DemoJuceFilter* const ownerFilter);

    /** Destructor. */
    ~DemoEditorComponent();

    //==============================================================================
    /** Our demo filter is a ChangeBroadcaster, and will call us back when one of
        its parameters changes.
    */
    void changeListenerCallback (void* source);

    void sliderValueChanged (Slider*);

// EXTRA STUFF //
	void DemoEditorComponent::mouseDown (const MouseEvent & e);
// /EXTRA STUFF //

    //==============================================================================
    /** Standard Juce paint callback. */
    void paint (Graphics& g);

    /** Standard Juce resize callback. */
    void resized();


private:
    //==============================================================================
    Slider* gainSlider;
    MidiKeyboardComponent* midiKeyboard;
    Label* infoLabel;
    ResizableCornerComponent* resizer;
    ComponentBoundsConstrainer resizeLimits;
    TooltipWindow tooltipWindow;

    void updateParametersFromFilter();

    // handy wrapper method to avoid having to cast the filter to a DemoJuceFilter
    // every time we need it..
    DemoJuceFilter* getFilter() const throw()       { return (DemoJuceFilter*) getAudioProcessor(); }
};


#endif

#2

Looks like you’re misunderstanding what’s going on there - your component will appear as a child of the actual dialog window, so calling setAlwaysOnTop or toFront on it would be pointless (especially in the constructor, at which point it’s not even on the screen).

Try creating a DialogWindow directly instead of calling showModalDialog, and then you can call toFront/setAlwaysOnTop on the window itself, which will work.


#3

[quote]Looks like you’re misunderstanding what’s going on there - your component will appear as a child of the actual dialog window, so calling setAlwaysOnTop or toFront on it would be pointless.
[/quote]

Yes, that’s what I thought. In the first place I didn’t use setAlwaysOnTop and toFront, then added them with very little hope it would make any difference, as I knew I was acting on the component inside the dialog window and strongly suspected I had to call them at the dialog window level in order for these functions to have the desired effect.

However, I couldn’t see how to do this using showModalDialog.

OK, so the reason why I couldn’t see how to do get my dialog to be on top using showModalDialog was that it wasn’t possible. This answers my question: I have to create the dialog myself. Thanks for the tip!