Current advised way to do plugin editor contents scaling?

Hi,

I received a request from a user who would like my plugin editor to be rescalable (on Windows and Mac).

Currently, the plugin size nicely follows the settings of the screen it’s on, in the sense that its size relative to the standard OS windows (such as Explorer or Finder) remains constant. Which is the normal expected behavior: you set the OS scaling up so that you can get enough screen estate with windows that are still nicely readable, and my plugin seems to currently follow that (even if you need to tweak some host settings a bit in some cases).

However, I can also understand that some people (like the requesting user) may also want to keep a 4K screen set to 100% scaling in order to get as much on the screen as possible (even if everything becomes pretty small then) and then want the plugin they’re working with to be larger while they’re tweaking parameters.
In order to do this, it would be needed to allow the user to rescale the plugin editor window the way he/she wants.

If doing it dynamically by dragging a corner/sizer is too error-prone, changing a preference with a few fixed possibilities in the plugin’s settings would be fine too (although that would be a bit more limiting, as it would affect all instances of the plugin, but hey, it could be a start…)

Currently, my plugin is bitmap-based (and I’m not going to change that now) so an actual rescaling of the pixels is the only option (yes, I realize it may look a bit blurry or pixelated then…)

I have now read several topics on the forum dating back to a few years ago (about fixed desktop scaling factor, an extra parent component with an affine transform, AudioProcessorEditor::setScaleFactor, etc…), but I think I got lost in all the possibilities and not sure what could actually still work today and what not…

So: what is the current advised way (for the latest 6.1.2 master release) to make it possible for a user to rescale the plugin editor window and its contents (including tooltips and combo/listboxes)?

So, it looks like I can get it to work in 2 ways (only tested on Windows 10 for now with the VST2 version, but with 3 combinations of screen resolutions + scaling settings):

  1. in the editor constructor, I do:
setResizable(true, false);
  1. in the lister callback code for a specific button, I do:
setScaleFactor(scale); // scale can be only 1, 1.5 or 2
  1. nothing in my resized() method, as I don’t want to reorganize my GUI, but only scale it

If I do it this way, each editor can have its own scale. For this case I would probably need to store the scale in the processor (or even in its state) somehow, so it’s set back the same way the next time an editor is opened.

Then I also tried using the following in the button callback:

Desktop::getInstance().setGlobalScaleFactor(scale); // scale can only be 1, 1.5 or 2

and that scales all editors (as expected from the name I guess). For this case, I would probably make more sense to store the (global) scale in a plugin preference.

I did see some issue where tooltips now appear in strange places when the scale is not 1 (and in Cubase the text and the drop shadow of the tooltip sometimes showed in 2 different places).
But I guess this might be because our tooltip window currently hasn’t got its parent set to the editor component, so it’s not restricted to the plugin GUI window and can extend on to the desktop area outside the editor, and that case might be a bit buggy perhaps?
I’ll need to see if changing that helps. I suppose it should be sufficient to simply do the following in my Editor member variable declarations for that:

TooltipWindow m_TooltipWindow{ this };

@ed95 or @reuk If you have moment at some point: is what I described above the proposed way to do things? Any comments / advice?
It might be good to have an example plugin in the JUCE repository that shows how to do this (I looked around, but only AudioPluginDemo seems to have an editor, and while it does allow for resizing, it doesn’t allow for scaling the GUI at the moment).

Edit:
Actually, I just noticed that the docs for AudioProcessorEditor::setScaleFactor say:
“Can be called by a host to tell the editor that it should use a non-unity GUI scale.”
So perhaps it’s not a good idea for the plugin to call this method itself… But then how to do it correctly?

As I understand it, neither of these approaches are ideal.

  • setScaleFactor will be called by the host when the platform’s scale factor changes (e.g. the plugin editor window is dragged between monitors with different DPI values). The plugin should generally not call setScaleFactor on itself, because this may cause the editor to display at the wrong size on hi-DPI displays.
  • Desktop::setGlobalScaleFactor will set the scale factor for all JUCE components equally (including things like dialogs, tooltips etc.) which is probably not the behaviour you’re looking for.

I think the “correct” way to achieve this is to move the contents of the editor into a new Component which is a child of your AudioProcessorEditor. Then, you can call Component::setTransform on this child component, passing a transform created with AffineTransform::scale.

1 Like

Thanks for the reply.

OK, so setScaleFactor doesn’t seem like a good idea then indeed.

As for the Desktop::setGlobalScaleFactor method: I wouldn’t really mind to have the tooltips show up at the same scale as the rest of my GUI, and for dialogs I use the native dialogs IIRC. So that still sounds OK for me. The only thing is of course that you can’t have different scale factors per editor…

I’ll see how it goes on macOS with the setGlobalScaleFactor and if other issues things pop up with that, otherwise I’ll also check out the extra in-between component approach you suggest.

I also just realized that in the case of a per-plugin scale factor + storing that factor in the state of the plugin might actually not be a good idea, as that could lead to huge plugin windows when someone moves a DAW project from a studio 5K monitor to simple laptop… These kinds of things are never simple, are they? :wink:

I’m in the process of implementing exactly this feature and we’ve decided for having the scale as a preference that is:

  • local to the computer (not stored in the state of the plug-in that’s saved/recalled by the DAW)
  • shared by all instances of the plug-in (no possibility to set a custom scale per-instance)

As for how it’s implemented, we’ve gone the route suggested by Reuk with a parent Component that sets a scale AffineTransform on the Component that actually contains the UI

1 Like

@yfede Seems like that is indeed the way to go.

I assume that alone is not sufficient, right? Isn’t there also something we need to do to signal the host to enlarge the plugin window somehow? I mean: if we set that transform’s scale factor to 2, then the content may get scaled, but that doesn’t make the window we received from the host twice as large, or does it?

So, if I get it right, instead of this structure:

AudioProcessorEditor
  - bunch of sliders
  - bunch of knobs
  - bunch of labels

we get this:

AudioProcessorEditor
  ScalingComponent
    - bunch of sliders
    - bunch of knobs
    - bunch of labels

with the “scaling component” then set to fill the entire AudioProcessorEditor component, and with a scaling-only AffineTransform applied to it.

The annoying thing is that since my GUI is made with the Projucer GUI editor, all the child widgets are added directly to my AudioProcessorEditor component (I can’t simply change that to add them to an extra scaling component).
So, I’ll probably need to turn my current AudioProcessorEditor component into a normal Component (that doesn’t make a difference for the GUI editor), and then move the various AudioProcessorEditor-specific calls to a new AudioProcessorEditor class, and then propagate these calls to my (by then) normal Component that contains all the widgets.

Yes, your reasoning about the required component hierarchy is correct.
For other reasons, I already had a similar hierarchy in place so I didn’t have to actually add the intermediate Component. What I have is:

EditorComponent // subclass of juce::AudioProcessorEditor
    EditorContent content; // subclass of juce::Component
        - bunch Components for controlling parameters

content is the Component to which the scale is applied with setTransform().

To keep the EditorComponent size in sync with the size of content, which changes whenever the scale changes, I have this function in EditorComponent:

void EditorComponent::fitAroundContent ()
{
    /* content should never move from 0, 0 */
    jassert (content.getPosition () == juce::Point <int> (0, 0));

    /* This convoluted way of obtaining the contentBounds returns
     the bounds occupied by the content in this EditorComponent, taking into
     account the scale transform applied to the content */
    const auto contentBounds = getLocalArea (content, content.getLocalBounds ());
    
    setSize (contentBounds.getRight (), contentBounds.getBottom ());
}

I call it in the constructor of EditorComponent after I have created content and set its scale, so that EditorComponent starts off with the correct size.

Then EditorComponent registers itself as a ComponentListener for content and
componentMovedOrResized() takes care of keeping the EditorComponent size in sync:

void EditorComponent::componentMovedOrResized (Component& component, bool /*wasMoved*/, bool /*wasResized*/)
{
    jassert (&component == &content);
    fitAroundContent ();
}
3 Likes

Thanks @yfede ! That all seems to make sense indeed.

So, if I understand it correctly, the actual resizing of the host’s plugin window happens when you call setSize() in the fitAroundContent() method? (this somehow makes the host notice there is a new size and it then resizes the plugin window)

I suppose you also call setResizable(true, false) in the constructor of the editor, right? (since you set the scale as a preference, that second argument for the resizing corner probably stays false)

When I did my experiments mentioned above, I saw some strange things with AAX plugins not resizing at all (unless you re-open the editor window) + also some strange tooltip placements. Does this method work for AAX plugins too (I actually ship VST2, AU and AAX)? And no tooltip placement strangeness?

Last question: is this with the latest JUCE master (tag 6.1.2)?

FWIW, I don’t even make AudioProcessorEditors for each plugin, I just have a base class that owns and displays a component of some kind inside an editor, like:

template < class ContentComponentType >
class PluginEditor : public juce::AudioProcessorEditor
{
...
};

And then you can do that same thing for your processor, to avoid boilerplate… What i usually do is first define the “headless” processor, and then:

template < class ProcessorType, class MainComponentType >
class ProcessorWithEditor : ProcessorType
{
    bool hasEditor() final { return true; }

    juce::AudioProcessorEditor* createEditor() final 
    {
        return new PluginEditor< MainComponentType >;
    }
};
1 Like

Exactly!

I’m not calling setResizable() at all, which I suppose it’s equivalent to calling setResizable (false, false).
The doc for the first argument reads (emphasis mine):

allowHostToResize: whether the editor’s parent window can be resized by the host. Even if this is false, you can still resize your window yourself by calling setBounds (for example, when a user clicks on a button in your editor to drop out a panel) which will bypass any resizable/constraints checks.

Apart from the scale, the size of my UI is fixed, so I don’t want it to be resizable by the host (in my understanding, one such resizing could be dictated by the host for example if the user drags one of the borders of the plug-in window provided by the DAW)

Yes, I have just tested (being a little nervous because I didn’t test AAX before) and I’m happy to report that it works as expected in AAX too.

I’m wondering if the reason why it doesn’t resize for you in AAX could be that first true argument passed to setResizable(): if the plug-in expects the host to be the one in command for what it concerns its size, it could very well be that the plug-in doesn’t trigger a resize of the host window “from inside”, to avoid conflicting with the size imposed by the host “from outside”.

I only have a custom drawn tooltip in the whole UI and I couldn’t see any placement issue with it

Last question: is this with the latest JUCE master (tag 6.1.2)?

I’m currently on JUCE tag 6.1.1 but wanting to update ASAP to 6.1.2

1 Like

What’s the difference/advantage of using a transform instead of just calling setSize and handling the layout in the resized function? Just curious :wink:

That all sounds great!
Thanks also for pointing out that allowHostToResize note: reading this again I think you might be right that setting it to false could be the reason for some of the issues I saw, but the method you outlined here definitely seems the way to go. I’ve bookmarked this thread and will try this next, after I deal with some other (unrelated) issues I seem to have now in some hosts since upgrading to 6.1.2 from 5.4.1.
Thanks for your feedback and sharing @yfede !

In my case, my GUI is currently fully bitmap-based + created with the JUCE GUI editor (which creates code that uses fixed absolute or “relatively-anchored” positions), so it’s not that easy to just change the layout coordinates (at least if I want to go back in the other direction again later on to edit some more things in the GUI editor).

So it’s easier to use a fixed layout and then scale the component: everything stays in position relative to each other as the transform changes the positions and sizes of you subcomponents, but of course the bitmaps get do scaled too up when you scale up. Perhaps there’s a way around that by re-creating the bitmaps at say scale 2x and have them auto-scale down (for sizes < 200%) or up (for sizes > 200%) as needed, which should still look reasonable I suppose. But for now, the bitmaps will just have to look a bit blurry or pixelated.

Thats actually how I am doing it. I am not using a transform, but I do all the scaling inside the “resized” function.
The main reason is: Every now and then I have seen bug reports on the forum, where using a transform led to weird side effects. Like, if your have two monitors and try to create a popup window on the left monitor it does not work. Or similar.

Might be I am a bit too cautious?
Doing it inside “resized” has the disadvantage, that the setBound method only allows integer positions. Meaning, that you can have slight position wiggles when scaling (which you would not have with the transform method).
On the other hand, it seemed safer to me :smiley: