UI Element Reuse

I’m curious to know how others deal with UI component reuses while using JUCE? I’ve built an interface that has groups of JUCE widgets that are repeated multiple times. I did this by creating Component classes and adding the individual JUCE widgets to these Component classes (where they get added and sized), then adding that Component class to the main PluginEditor class. Some of these Components have other Components nested within them. Everything is working nicely, I can add these Component classes to the Plugin Editor and all my nested JUCE widgets within them all show up. This seems like a good situation for finding a way to turn groups of JUCE items into objects, but it causes the issue of having to provide accessor functions to get to these widgets that reside internally in these Component classes, or make the widgets public, so I can just access them directly with the dot operator.

So I’m curious to hear how others deal with DRYing up repeating UI stuff.

You are doing it the right way. And, if it is just a Component that houses other Components, making them public is probably the best course of action.

3 Likes

One thing though, IMHO no component should access subcomponents of other components.
I group each component around a purpose, so it encapsulates all that is necessary. Surely I make exceptions, e.g. accessing the parent.
I have then functionality in a different class, not a component, that is usually owned by the JUCEApplication itself, of maybe the MainComponent, or the AudioProcessor in a plugin. A good example is the player, that usually has methods play, pause, setPosition and setOutputChannels…
I hand a reference to that player down the hierarchy to all the components, that need access, e.g. the timeline, the transport buttons, etc.

stuff like

getParent().subPanel.playButton.onClick = [&] { play(); };

Is extremely dangerous!

vs.

class TransportButtons : public Component
{
public:
    TransportButtons (Player& playerToUse) : player (playerToUse)
    {
        addAndMakeVisible (playButton);
        playButton.onClick = [player] { player.play(); };
    }
    
private:
    Player&    player;
    TextButton playButton { "Play", "Play" };
};

is safe, if the player is defined before the transportButtons member, since the player will always outlive the buttons.

2 Likes

Good points, @daniel.

But I interpreted his question to not be about sharing an instance of a component, but reusing code for a component, in multiple places. Like an Envelope editor, with knobs, or sliders, for ADSR.

1 Like

Sure, there was nothing wrong with your answer. What you proposed will compile and work.

I just wanted to stress, that it is important to limit the knowledge of implementation details of other classes, and exposing public members would give exactly that knowledge away, so you cannot refactor your class without adapting completely unrelated classes.

It might be overkill for small projects, but I think any time is a good time to start best practices :wink:

2 Likes

Thank you very much for the answers so far. What @cpr is saying is correct, I’m only trying to find a way DRY up my interface by creating objects out of repeated elements on my UI. If there is a better way, I would love to learn about it.

The only reason I need to have access to those inner widgets inside the Component classes is so they can interact with the PluginProcessor, which should live longer than the individual components (containing multiple widgets) themselves, since it is the thing that owns the PluginEditor, so I’m not so sure it would matter in this case if I provide access to them or not.

Sometimes I find myself in the need of reusing a set of widgets because a part of the UI repeats, but that set does not qualify as a stand-alone meaningful Component, often because it would become an entity far too complex.

Take for example a group of tone control knobs, I could make them easily reusable like this:

struct ToneKnobs
{
    Slider bass;
    Slider mid;
    Slider treble;
};

They don’t qualify as a stand-alone Component: embedding them in a “ToneControlPanel” would be overkill in the case I’m trying to explain: you may want to arrange them differently depending on the context where you’re using them, and nesting them in a panel also makes it difficult to arrange them w.r.t surrounding widgets (alignment, etc.).

Grouping them in a struct eases composition in more complex structs as well, I may end up doing something like:

struct ChannelStripWidgets
{
    Label name;
    Slider gain;
    ToneKnobs tone;
    Slider fader;
};

Let’s say I need 8 of those, I can have:

ChannelStripWidgets channels[8];

And keep the complexity of the code under control writing stuff like this, which every IDE can easily facilitate typing with auto-completion:

channels [0].label.setText ("whatever");
channels [3].tone.bass.getValue ();

In this case, the composition of each struct is exposed not because it is an implementation detail, but because it represents the structure of the UI, the “data model” that illustrates how the “primitive components” are conceptually organised without needing to wrap them inside other Components

3 Likes

Thank you, that is a great example.
The reason why I wouldn’t moan about it in a code review is, that it is strictly in one direction, always down the hierarchy.

To every universal statement there is a good exception, I gotta be more careful :wink:

2 Likes

:slight_smile: I did not mean to directly oppose your post, my intent was more like: “and then there’s this other choice in the spectrum of what can be done. Which route to choose depends on the specific case at hand”

1 Like

That’s absolutely how I understood it :slight_smile:

1 Like

Following on this; one way to DRY the initialization of these components, without polluting your class with a very localized function, is to use a lambda in the ctor. This is just a basic example.

MixerComponent::MixerComponent()
{
    auto setupChannelStrip = [this] (ChannelStripWidgets& csw, const String& name)
    {
        csw.name.setText(name);
        csw.name.setColour(...);
        csw.fader.setWhatever(...);
    };

    setupChannelStrip(channels[0], "Input 1");
    setupChannelStrip(channels[1], "Input 2");
    // etc

    // or something more 'data driven'
    for (auto [channelStripComponent, name] : std::vector<std::pair<ChannelStripWidgets*, String>> { {channels[0], "Input 1"},{channels[1], "Input 2"} })
        setupChannelStrip(*channelStripComponent, name);

    // or maybe the old fasion for loop
    for (const auto channelStripIndex { 0 }; channelStripIndex < 8; ++channelStripIndex)
        setupChannelStrip(channels[channelStripIndex ], String("Input ") + String(channelStripIndex 
 + 1));
}
2 Likes

Lots of cool answers here, but one of the things I really wanted to be able to do was provide a layout for these individual widgets within these component classes; I’m not sure if this is possible with some of the solutions provided yet or not. I’m using FlexBox for everything; I wanted to contain them, so that was sort of part of the reason why I went with a class.

I’m going to go ahead and just post the code here of what I done. Right now, every widget is still private, as I haven’t implemented anything more than just part of the UI with the stock JUCE widgets, but there is more to be done, as custom widgets, more controls, UI tweaks, and of course, the processBlock.

I don’t know, if that suits your workflow, but did you see my GUI WYSWYG editor?
You can create hierarchical layouts and define different flexbox in each node, just as you would in CSS.
You can also add your own components, connecting to parameters happens automatically.
And some visualisers like analyser, oscilloscope and level meters are also there out of the box.

More flexible layouts like TabBarComponent or even slideshows are on the wishlist, but is doable.

1 Like