How would you go about nested components updating data to the top level component

Hi there, in the editor I have a nested component (several levels):

A Sequence containg bars
Some Bars containing Steps
Some Steps containing info on note value velocity and such

This is for the ui. So I Instantiate my Sequnce and then the sequnce instatiates some bars and each bar instatiates some steps. All fine and dandy and it doesnt look great by now but it all works.

Now I change a value in one of the steps (or the amount of steps in a bar or the amount of bars in a seqeunce). The dynamic creation of the objects and stuff and repainting is no problem.

Ho would you go to get thisinformaton up to the Editor so it can communicate it to the processor (maybe Valuetreestate or such but that is not the question)

I could run a timer in my top level component and cycle through all subcomponents and collect data, which I belive is not the right way

I could on the change of a subcomponent call a function on the parent and let it know about the change and the parent will call its parent all the way up. This is I believe a better way.

I could attach all values to a ValueTreestate right away in the subcomponents but as I am having for example a lot of steps in a bar how would I go about the naming ? Give an ID to the step or bar and include it in the name?

I believe its definitely not method one, which would of course perfectly work, my preferred is option two and then create all the ValueTree members in the top level component at once.

I have no code to show yet as I did not start implementing any of this but I have for example the step component (see below)

Any advise ?

thanks in advanced

.h

	class SqStep : public juce::Component,
		juce::Slider::Listener
	{
	public:
		SqStep();
		~SqStep() override;

		void paint(juce::Graphics&) override;
		void resized() override;
		void sliderValueChanged(juce::Slider* slider) override;

		struct StepData
		{
			int stPitch = 36;
			int stOctave = 1;
			juce::uint8 stVelocity =100;
			float stDuration = 0.75f;
		};

	private:
		juce::Slider pitchSlider;
		juce::Slider octaveSlider;
		juce::Slider velocitySlider;
		juce::Slider durationSlider;

		StepData stepData;
		JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SqStep)
	};```



cpp
SqStep::SqStep()
{		
	pitchSlider.setRange(juce::Range<double>(1, 12),1.0);
	pitchSlider.setValue(12);
	pitchSlider.setSliderStyle(juce::Slider::SliderStyle::LinearBarVertical);
	pitchSlider.setTextBoxStyle(juce::Slider::TextEntryBoxPosition::NoTextBox, false, 0, 0);
	pitchSlider.setColour(juce::Slider::ColourIds::trackColourId, juce::Colours::green);
	addAndMakeVisible(pitchSlider);
	pitchSlider.addListener(this);
	
	octaveSlider.setRange(juce::Range<double>(0, 6), 1.0);
	octaveSlider.setValue(3);
	octaveSlider.setSliderStyle(juce::Slider::SliderStyle::LinearBarVertical);
	octaveSlider.setTextBoxStyle(juce::Slider::TextEntryBoxPosition::NoTextBox,false,0,0);
	octaveSlider.setColour(juce::Slider::ColourIds::trackColourId, juce::Colours::darkgreen);
	addAndMakeVisible(octaveSlider);
	octaveSlider.addListener(this);

	durationSlider.setRange(juce::Range<double>(0, 1), 0.05f);
	durationSlider.setValue(0.75f);
	durationSlider.setSliderStyle(juce::Slider::SliderStyle::LinearBarVertical);
	durationSlider.setTextBoxStyle(juce::Slider::TextEntryBoxPosition::NoTextBox, false, 0, 0);
	durationSlider.setColour(juce::Slider::ColourIds::trackColourId, juce::Colours::darkorange);
	addAndMakeVisible(durationSlider);
	durationSlider.addListener(this);

	velocitySlider.setRange(juce::Range<double>(0, 127), 1.0);
	velocitySlider.setValue(100);
	velocitySlider.setSliderStyle(juce::Slider::SliderStyle::LinearBarVertical);
	velocitySlider.setTextBoxStyle(juce::Slider::TextEntryBoxPosition::NoTextBox, false, 0, 0);
	velocitySlider.setColour(juce::Slider::ColourIds::trackColourId, juce::Colours::blueviolet);
	addAndMakeVisible(velocitySlider);
	velocitySlider.addListener(this);
}

SqStep::~SqStep()
{
}

void SqStep::paint(juce::Graphics & g)
{
	g.fillAll(getLookAndFeel().findColour(juce::ResizableWindow::backgroundColourId));   // clear the background

	g.setColour(juce::Colours::white);
	g.drawRect(getLocalBounds(), 1);   // draw an outline around the component
}

void SqStep::resized()
{
	auto myBounds = getLocalBounds();
	myBounds.reduce(3, 3);
	auto totalHeight = myBounds.getHeight();
	auto sliderHeight = totalHeight * 0.3f;
	auto rotaryHeight = totalHeight * 0.15f;
	auto verticalSliderHeight = totalHeight * 0.1f;
	auto dividerHeight = totalHeight * 0.05f;
			
	octaveSlider.setBounds(myBounds.removeFromTop(rotaryHeight));
	myBounds.removeFromTop(dividerHeight);
	pitchSlider.setBounds(myBounds.removeFromTop(sliderHeight));
	myBounds.removeFromTop(dividerHeight);
	velocitySlider.setBounds(myBounds.removeFromTop(sliderHeight));
	myBounds.removeFromTop(dividerHeight);
	durationSlider.setBounds(myBounds.removeFromTop(verticalSliderHeight));
}

void SqStep::sliderValueChanged(juce::Slider * slider)
{
	if (slider == &pitchSlider)
	{
		auto pitch = slider->getValue();
		stepData.stPitch = (int) (stepData.stOctave * pitch);
	}

	if (slider == &octaveSlider)
	{
		auto octave = slider->getValue();
		stepData.stOctave = octave;
	}

	if (slider == &velocitySlider)
	{
		auto velocity = slider->getValue();
		stepData.stVelocity = velocity;
	}

	if (slider == &durationSlider)
	{
		auto duration = slider->getValue();
		stepData.stDuration = duration;
		
	}
}

You’re thinking in the correct direction. The first issue is that you have coupled your data with your UI components. So, you are stuck thinking about how to have your UI components communicate with each other. But, if you create a data model, and treat the UI as views into that data, you should improve your access to the underlying data, and be able to do what you want. ValueTrees might be the solution, as they offer some great stuff out of the box, including callbacks for when a value changes. So, consumers of the data can sign up for those callbacks, and do work (update UI, etc) based on those changes. You point out many of the considerations one has to think about, including are there ValueTree entries per sequencer event, or is that a bad design? I would say start refactoring to remove the data ownership from your UI, and see where you get. Asking more specific questions along the way. :slight_smile:

3 Likes

You can just pass a reference to your top level editor into the constructor of each child object, and then the children can call the top editor directly. No need to pass it up a chain of parents.

EDIT (added):

class SqStep : public juce::Component,
		juce::Slider::Listener
	{
public:
		SqStep(MainComponent& main);
		~SqStep() override;
private:
        MainComponent& mainComponent;

Then in your constructor:

SqStep::SqStep(MainComponent& main)
: mainComponent (main)
{
...

Then your SqStep components can just communicate directly with the MainComponent, using the reference, i.e.

mainComponent.updateMyData();

whatever you do, do not use getTopLevelComponent() for that. it sounds like it is exactly what you need, but the top level component might be a different one than the pluginEditor. it can for example be the one that contains both the pluginEditor as well as the top bar with the minimize- and close buttons etc. or it can be the host itself, if it is made with JUCE as well. I once had a bug, where I tried to position something with the topLevelComponent and it got positioned wrong in standalone because of that. So what you should actually do is: Always have a reference to the pluginEditor at the places, where you need it. I personally put it into my custom Utils object, which I find to be a recommendable practice, because then you always have the most important things around without having to care so much about it.

Hi many thanks,

I would have used getParent but as stephenk suggested I might just pass a reference.

Also @cpr2323 I have not coupled my data with ui, I am trying to come up with a clean and good way to get the changes a user makes in the ui collected in one place and then change it. Still I like the thoughts about the data model and I will now first put though in the data model itself and do some refactoring and see where it gets me.

Many thanks guys!

This might be helpful: The tracktion engine examples contain some UI code that deals with a data model with in the case of tracktion contains settings for E.G. zooming in/out. It also shows how to handle callbacks asynchronously which is helpful for optimising for responsive UI.

Would highly recommend reading some of Martin Fowlers work, espectially GUI Architectures.

In general, you don’t want your UI elements talking to each other directly as that causes coupling and makes it increasingly difficult to make changes and move things around as your project grows. Instead you want some sort of Model that stores the state of your software that they can then all read/write to. The model then effectively becomes a mediator.

2 Likes

Wow @ImJimmy,

I even did not know these books/articles existed - well maybe that the problem with being a hobbyist :wink:

Many thanks this is really great added value!

1 Like

And since it’s about design in general ; think about DOD vs OOP (a trending topic few years ago).

Note that juce::ValueTree is OOP friendly (easy to use with Observer/Listener pattern).

My apologies, I assumed the data was bound to the component because of the stepData member variable.

For the past handful of years I have been using ValueTrees for my data model, and just passing two root VT’s into everything else, which then put/get what they need as branches from the root. I actually use a system where I create classes that hide the ValueTrees, but still offering getters/setters/callbacks.

Once you get a system in place, it makes over all development so much easier.

Allo good,

basically the more I think about it I am asking myself what whould I need these for anyway? If I go for an event driven model I can update the data model right from the event when it occurs. But I am not that far yet, need to learn a lot - basically thats why I am doing this anyway :wink:

Thank you!

1 Like