Mema
Memory Matrix — multi-channel audio matrix monitor and router
Loading...
Searching...
No Matches
PluginControlComponent.cpp
Go to the documentation of this file.
1/* Copyright (c) 2025, Christian Ahrens
2 *
3 * This file is part of Mema <https://github.com/ChristianAhrens/Mema>
4 *
5 * This tool is free software; you can redistribute it and/or modify it under
6 * the terms of the GNU Lesser General Public License version 3.0 as published
7 * by the Free Software Foundation.
8 *
9 * This tool is distributed in the hope that it will be useful, but WITHOUT
10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
12 * details.
13 *
14 * You should have received a copy of the GNU Lesser General Public License
15 * along with this tool; if not, write to the Free Software Foundation, Inc.,
16 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17 */
18
20
21#include <CustomLookAndFeel.h>
22#include <ToggleStateSlider.h>
24
25
26namespace Mema
27{
28
29
32{
33 m_parameterControlsGrid = std::make_unique<juce::Grid>();
34
35 auto noconfigui = juce::JUCEApplication::getInstance()->getCommandLineParameters().contains("--noconfigui");
36
37 m_enableButton = std::make_unique<juce::DrawableButton>("pluginEnable", juce::DrawableButton::ImageOnButtonBackground);
38 m_enableButton->setClickingTogglesState(true);
39 m_enableButton->setTooltip("Toggle plugin processing on/off");
40 m_enableButton->onStateChange = [=]() {
42 onPluginEnabledChanged(m_enableButton->getToggleState());
43 };
44 if (noconfigui)
45 addChildComponent(m_enableButton.get());
46 else
47 addAndMakeVisible(m_enableButton.get());
48
49 m_prePostButton = std::make_unique<juce::TextButton>("Post", "Toggle plugin pre/post matrix insertion");
50 m_prePostButton->setClickingTogglesState(true);
51 m_prePostButton->onStateChange = [=]() {
53 onPluginPrePostChanged(m_prePostButton->getToggleState());
54 };
55 if (noconfigui)
56 addChildComponent(m_prePostButton.get());
57 else
58 addAndMakeVisible(m_prePostButton.get());
59
60 m_pluginNameLabel = std::make_unique<juce::Label>("pluginName");
61 m_pluginNameLabel->setFont(m_pluginNameLabel->getFont().withHeight(25));
62 m_pluginNameLabel->setJustificationType(juce::Justification::centredLeft);
63 addAndMakeVisible(m_pluginNameLabel.get());
64}
65
69
70void PluginControlComponent::paint(juce::Graphics& g)
71{
72 // solid overall background
73 g.fillAll(getLookAndFeel().findColour(juce::ResizableWindow::backgroundColourId));
74}
75
77{
78 auto margin = 25;
79 auto bounds = getLocalBounds();
80 auto headerRow = bounds.removeFromTop(margin);
81
82 const bool enableVisible = m_enableButton && m_enableButton->isVisible();
83 const bool prePostVisible = m_prePostButton && m_prePostButton->isVisible();
84
85 if (enableVisible || prePostVisible)
86 {
87 // Fixed widths for the config buttons
88 const int enableBtnWidth = margin; // square icon button
89 const int prePostBtnWidth = 2 * margin; // "Post" text button
90
91 int totalBtnsWidth = 0;
92 if (enableVisible) totalBtnsWidth += enableBtnWidth;
93 if (prePostVisible) totalBtnsWidth += prePostBtnWidth;
94
95 // Give the label half the available width, but at least enough for the buttons' companion
96 const int labelWidth = juce::jmax(80, headerRow.getWidth() / 2);
97 const int groupWidth = totalBtnsWidth + labelWidth;
98
99 // Center the group horizontally in the header row
100 const int groupX = headerRow.getX() + juce::jmax(0, (headerRow.getWidth() - groupWidth) / 2);
101 auto groupBounds = juce::Rectangle<int>(groupX, headerRow.getY(),
102 juce::jmin(groupWidth, headerRow.getWidth()),
103 headerRow.getHeight());
104
105 if (enableVisible)
106 m_enableButton->setBounds(groupBounds.removeFromLeft(enableBtnWidth).reduced(2));
107 if (prePostVisible)
108 m_prePostButton->setBounds(groupBounds.removeFromLeft(prePostBtnWidth).reduced(2));
109 if (m_pluginNameLabel)
110 m_pluginNameLabel->setBounds(groupBounds);
111 }
112 else
113 {
114 if (m_pluginNameLabel)
115 m_pluginNameLabel->setBounds(headerRow);
116 }
117
118 bounds.removeFromLeft(margin);
119 bounds.removeFromRight(margin);
120 m_parameterBounds = bounds;
122}
123
125{
126 auto accentColour = getLookAndFeel().findColour(JUCEAppBasics::CustomLookAndFeel::ColourIds::MeteringRmsColourId);
127 auto textColour = getLookAndFeel().findColour(juce::TextButton::ColourIds::textColourOnId);
128
129 for (auto const& comboKV : m_parameterValueComboBoxes)
130 comboKV.second->setColour(juce::ComboBox::ColourIds::focusedOutlineColourId, accentColour);
131 for (auto const& sliderKV : m_parameterValueSliders)
132 sliderKV.second->setColour(juce::Slider::ColourIds::trackColourId, accentColour);
133 for (auto const& buttonKV : m_parameterValueButtons)
134 buttonKV.second->setColour(juce::TextButton::ColourIds::buttonOnColourId, accentColour);
135
136 if (m_enableButton)
137 {
138 auto drawable = juce::Drawable::createFromSVG(*juce::XmlDocument::parse(BinaryData::power_settings_24dp_svg).get());
139 drawable->replaceColour(juce::Colours::black, textColour);
140 m_enableButton->setImages(drawable.get(), nullptr, nullptr, nullptr, drawable.get());
141 }
142}
143
145{
146 setIOCount({ 0,0 }); // irrelevant
147
148 m_pluginName.clear();
149 m_pluginParameterInfos.clear();
150
151 if (m_enableButton)
152 m_enableButton->setToggleState(false, juce::dontSendNotification);
153 if (m_prePostButton)
154 m_prePostButton->setToggleState(false, juce::dontSendNotification);
155}
156
163
165{
166 return m_pluginName;
167}
168
169void PluginControlComponent::setPluginName(const std::string& pluginName)
170{
171 if (m_pluginName != pluginName)
172 {
173 m_pluginName = pluginName;
174 if (m_pluginNameLabel) m_pluginNameLabel->setText(m_pluginName, juce::dontSendNotification);
175 }
176}
177
178const std::vector<Mema::PluginParameterInfo>& PluginControlComponent::getParameterInfos()
179{
180 return m_pluginParameterInfos;
181}
182
183void PluginControlComponent::setParameterInfos(const std::vector<Mema::PluginParameterInfo>& parameterInfos)
184{
185 if (m_pluginParameterInfos != parameterInfos)
186 {
187 m_pluginParameterInfos = parameterInfos;
190 }
191}
192
194{
195 if (m_enableButton)
196 m_enableButton->setToggleState(enabled, juce::dontSendNotification);
197}
198
200{
201 if (m_prePostButton)
202 m_prePostButton->setToggleState(post, juce::dontSendNotification);
203}
204
205void PluginControlComponent::setParameterValue(std::uint16_t index, std::string id, float value)
206{
207 if (index < m_pluginParameterInfos.size())
208 {
209 m_pluginParameterInfos.at(index).currentValue = value;
210 m_pluginParameterInfos.at(index).id = id;
211
212 if (m_parameterValueComboBoxes.count(index) != 0)
213 m_parameterValueComboBoxes.at(index)->setSelectedId(int(value * (m_pluginParameterInfos.at(index).stepCount - 1) + 1), juce::dontSendNotification);
214 else if (m_parameterValueSliders.count(index) != 0)
215 m_parameterValueSliders.at(index)->setValue(float(value), juce::dontSendNotification);
216 else if (m_parameterValueButtons.count(index) != 0)
217 m_parameterValueButtons.at(index)->setToggleState(value > 0.5f, juce::dontSendNotification);
218 }
219}
220
222{
223 for (auto const& parameterInfo : m_pluginParameterInfos)
224 {
225 if (!parameterInfo.isRemoteControllable)
226 continue;
227
228 if (parameterInfo.type != ParameterControlType::Toggle)
229 {
230 if (m_parameterNameLabels.count(parameterInfo.index) != 0)
231 m_parameterNameLabels.at(parameterInfo.index)->setText(parameterInfo.name, juce::dontSendNotification);
232 else
233 {
234 m_parameterNameLabels[parameterInfo.index] = std::make_unique<juce::Label>(parameterInfo.id);
235 m_parameterNameLabels[parameterInfo.index]->setJustificationType(juce::Justification::centredBottom);
236 m_parameterNameLabels[parameterInfo.index]->setText(parameterInfo.name, juce::dontSendNotification);
237 addAndMakeVisible(m_parameterNameLabels[parameterInfo.index].get());
238 }
239 }
240
241 if (parameterInfo.type == ParameterControlType::Discrete)
242 {
243 if (m_parameterValueComboBoxes.count(parameterInfo.index) != 0)
244 m_parameterValueComboBoxes.at(parameterInfo.index)->setSelectedId(int(parameterInfo.currentValue * (parameterInfo.stepCount - 1) + 1), juce::dontSendNotification);
245 else
246 {
247 m_parameterValueComboBoxes[parameterInfo.index] = std::make_unique<juce::ComboBox>(parameterInfo.id);
248 jassert(parameterInfo.stepCount == parameterInfo.stepNames.size());
249 int i = 1;
250 for (auto const& stepName : parameterInfo.stepNames)
251 m_parameterValueComboBoxes[parameterInfo.index]->addItem(stepName, i++);
252 m_parameterValueComboBoxes[parameterInfo.index]->setSelectedId(int(parameterInfo.currentValue * (parameterInfo.stepCount - 1) + 1), juce::dontSendNotification);
253 m_parameterValueComboBoxes[parameterInfo.index]->onChange = [=]() {
254 auto pIdx = parameterInfo.index;
255 if (m_parameterValueComboBoxes.count(pIdx) > 0 && m_pluginParameterInfos.size() > pIdx)
256 {
257 auto sId = m_parameterValueComboBoxes[pIdx]->getSelectedId();
258 m_pluginParameterInfos[pIdx].currentValue = float((1.0f / (parameterInfo.stepCount - 1)) * (sId - 1));
259
261 onPluginParameterValueChanged(pIdx, m_pluginParameterInfos[pIdx].id.toStdString(), m_pluginParameterInfos[pIdx].currentValue);
262 }
263 };
264 addAndMakeVisible(m_parameterValueComboBoxes[parameterInfo.index].get());
265 }
266 }
267 else if (parameterInfo.type == ParameterControlType::Continuous)
268 {
269 if (m_parameterValueSliders.count(parameterInfo.index) != 0)
270 m_parameterValueSliders.at(parameterInfo.index)->setValue(parameterInfo.currentValue, juce::dontSendNotification);
271 else
272 {
273 m_parameterValueSliders[parameterInfo.index] = std::make_unique<JUCEAppBasics::ToggleStateSlider>(juce::Slider::Rotary, juce::Slider::NoTextBox);
274 m_parameterValueSliders[parameterInfo.index]->setSliderStyle(juce::Slider::SliderStyle::Rotary);
275 m_parameterValueSliders[parameterInfo.index]->setTogglalbe(false);
276 m_parameterValueSliders[parameterInfo.index]->setRange(parameterInfo.minValue, parameterInfo.maxValue);
277 m_parameterValueSliders[parameterInfo.index]->setValue(parameterInfo.currentValue, juce::dontSendNotification);
278 m_parameterValueSliders[parameterInfo.index]->onValueChange = [=]() {
279 auto pIdx = parameterInfo.index;
280 if (m_parameterValueSliders.count(pIdx) > 0 && m_pluginParameterInfos.size() > pIdx)
281 {
282 auto value = float(m_parameterValueSliders[pIdx]->getValue());
283 m_pluginParameterInfos[pIdx].currentValue = value;
284
286 onPluginParameterValueChanged(pIdx, m_pluginParameterInfos[pIdx].id.toStdString(), m_pluginParameterInfos[pIdx].currentValue);
287 }
288 };
289 addAndMakeVisible(m_parameterValueSliders[parameterInfo.index].get());
290 }
291 }
292 else if (parameterInfo.type == ParameterControlType::Toggle)
293 {
294 if (m_parameterValueButtons.count(parameterInfo.index) != 0)
295 m_parameterValueButtons.at(parameterInfo.index)->setToggleState(parameterInfo.currentValue > 0.5f, juce::dontSendNotification);
296 else
297 {
298 m_parameterValueButtons[parameterInfo.index] = std::make_unique<juce::TextButton>(parameterInfo.name, "Toggle to enable or disable parameter.");
299 m_parameterValueButtons[parameterInfo.index]->setClickingTogglesState(true);
300 m_parameterValueButtons[parameterInfo.index]->setToggleState(parameterInfo.currentValue > 0.5f, juce::dontSendNotification);
301 m_parameterValueButtons[parameterInfo.index]->onStateChange = [=]() {
302 auto pIdx = parameterInfo.index;
303 if (m_parameterValueButtons.count(pIdx) > 0 && m_pluginParameterInfos.size() > pIdx)
304 {
305 auto value = float(m_parameterValueButtons[pIdx]->getToggleState() ? 1.0f : 0.0f);
306 m_pluginParameterInfos[pIdx].currentValue = value;
307
309 onPluginParameterValueChanged(pIdx, m_pluginParameterInfos[pIdx].id.toStdString(), m_pluginParameterInfos[pIdx].currentValue);
310 }
311 };
312 addAndMakeVisible(m_parameterValueButtons[parameterInfo.index].get());
313 }
314 }
315 }
316
317 lookAndFeelChanged(); // trigger colour updates for all controls
318}
319
321{
322 auto bounds = m_parameterBounds;
323 if (bounds.isEmpty())
324 return;
325
326 auto itemCount = int(m_parameterValueComboBoxes.size() + m_parameterValueSliders.size() + m_parameterValueButtons.size());
327 if (itemCount == 0)
328 return;
329
330 const auto gridItemControlSize = 3 * int(getControlsSize());
331 const int labelHeight = 30;
332 const int controlMargin = 5;
333 const int rowPairHeight = labelHeight + gridItemControlSize;
334
335 const int availableWidth = bounds.getWidth();
336 const int availableHeight = bounds.getHeight();
337 const float availableAspect = float(availableWidth) / float(juce::jmax(1, availableHeight));
338
339 // Find itemsPerRow whose layout aspect ratio is closest to the available area's aspect ratio
340 const int maxItemsPerRow = juce::jmin(itemCount, juce::jmax(1, availableWidth / gridItemControlSize));
341 int itemsPerRow = 1;
342 float bestAspectDiff = std::numeric_limits<float>::max();
343 for (int n = 1; n <= maxItemsPerRow; ++n)
344 {
345 int rows = static_cast<int>(std::ceil(float(itemCount) / float(n)));
346 float layoutAspect = float(n * gridItemControlSize) / float(juce::jmax(1, rows * rowPairHeight));
347 float diff = std::abs(layoutAspect - availableAspect);
348 if (diff < bestAspectDiff)
349 {
350 bestAspectDiff = diff;
351 itemsPerRow = n;
352 }
353 }
354
355 const int numRowPairs = static_cast<int>(std::ceil(float(itemCount) / float(itemsPerRow)));
356 const int totalGridWidth = itemsPerRow * gridItemControlSize;
357 const int totalGridHeight = numRowPairs * rowPairHeight;
358
359 // Center the grid within the available bounds
360 auto gridBounds = juce::Rectangle<int>(
361 bounds.getX() + juce::jmax(0, (availableWidth - totalGridWidth) / 2),
362 bounds.getY() + juce::jmax(0, (availableHeight - totalGridHeight) / 2),
363 juce::jmin(totalGridWidth, availableWidth),
364 juce::jmin(totalGridHeight, availableHeight));
365
366 // Build the grid
367 m_parameterControlsGrid->templateRows.clear();
368 m_parameterControlsGrid->templateColumns.clear();
369 m_parameterControlsGrid->items.clear();
370
371 for (int i = 0; i < numRowPairs; ++i)
372 {
373 m_parameterControlsGrid->templateRows.add(juce::Grid::TrackInfo(juce::Grid::Px(labelHeight)));
374 m_parameterControlsGrid->templateRows.add(juce::Grid::TrackInfo(juce::Grid::Px(gridItemControlSize)));
375 }
376 for (int i = 0; i < itemsPerRow; ++i)
377 m_parameterControlsGrid->templateColumns.add(juce::Grid::TrackInfo(juce::Grid::Px(gridItemControlSize)));
378
379 int currentItem = 0;
380
381 // Iterate in display order (m_pluginParameterInfos arrives ordered from the server)
382 for (auto const& parameterInfo : m_pluginParameterInfos)
383 {
384 if (!parameterInfo.isRemoteControllable)
385 continue;
386
387 int row = (currentItem / itemsPerRow) * 2;
388 int col = currentItem % itemsPerRow;
389
390 if (parameterInfo.type == ParameterControlType::Toggle)
391 {
392 auto buttonIter = m_parameterValueButtons.find(parameterInfo.index);
393 if (buttonIter == m_parameterValueButtons.end() || !buttonIter->second)
394 continue;
395 auto* button = buttonIter->second.get();
396 auto buttonSize = float(gridItemControlSize - (controlMargin * 2));
397
398 m_parameterControlsGrid->items.add(juce::GridItem(*button)
399 .withArea(row + 2, col + 1)
400 .withMargin(juce::GridItem::Margin(controlMargin))
401 .withWidth(buttonSize)
402 .withHeight(buttonSize)
403 .withJustifySelf(juce::GridItem::JustifySelf::center)
404 .withAlignSelf(juce::GridItem::AlignSelf::center));
405 }
406 else if (parameterInfo.type == ParameterControlType::Discrete)
407 {
408 auto comboIter = m_parameterValueComboBoxes.find(parameterInfo.index);
409 auto labelIter = m_parameterNameLabels.find(parameterInfo.index);
410 if (comboIter == m_parameterValueComboBoxes.end() || !comboIter->second)
411 continue;
412 if (labelIter == m_parameterNameLabels.end() || !labelIter->second)
413 continue;
414
415 m_parameterControlsGrid->items.add(juce::GridItem(*labelIter->second)
416 .withArea(row + 1, col + 1)
417 .withMargin(juce::GridItem::Margin(2.0f)));
418 m_parameterControlsGrid->items.add(juce::GridItem(*comboIter->second)
419 .withArea(row + 2, col + 1)
420 .withMargin(juce::GridItem::Margin(controlMargin))
421 .withHeight(30)
422 .withJustifySelf(juce::GridItem::JustifySelf::center)
423 .withAlignSelf(juce::GridItem::AlignSelf::center));
424 }
425 else if (parameterInfo.type == ParameterControlType::Continuous)
426 {
427 auto sliderIter = m_parameterValueSliders.find(parameterInfo.index);
428 auto labelIter = m_parameterNameLabels.find(parameterInfo.index);
429 if (sliderIter == m_parameterValueSliders.end() || !sliderIter->second)
430 continue;
431 if (labelIter == m_parameterNameLabels.end() || !labelIter->second)
432 continue;
433 auto sliderSize = float(gridItemControlSize - (controlMargin * 2));
434
435 m_parameterControlsGrid->items.add(juce::GridItem(*labelIter->second)
436 .withArea(row + 1, col + 1)
437 .withMargin(juce::GridItem::Margin(2.0f)));
438 m_parameterControlsGrid->items.add(juce::GridItem(*sliderIter->second)
439 .withArea(row + 2, col + 1)
440 .withMargin(juce::GridItem::Margin(controlMargin))
441 .withWidth(sliderSize)
442 .withHeight(sliderSize)
443 .withJustifySelf(juce::GridItem::JustifySelf::center)
444 .withAlignSelf(juce::GridItem::AlignSelf::center));
445 }
446
447 currentItem++;
448 }
449
450 m_parameterControlsGrid->performLayout(gridBounds);
451}
452
453
454} // namespace Mema
Abstract base for all Mema.Re client control panels.
virtual void setControlsSize(const ControlsSize &ctrlsSize)
virtual void setIOCount(const std::pair< int, int > &ioCount)
ControlsSize
Size category for rendered control elements.
std::function< void(bool)> onPluginEnabledChanged
void setControlsSize(const ControlsSize &ctrlsSize) override
std::function< void(std::uint16_t, std::string, float)> onPluginParameterValueChanged
void setParameterInfos(const std::vector< Mema::PluginParameterInfo > &parameterInfos)
void setParameterValue(std::uint16_t index, std::string id, float value)
const std::vector< Mema::PluginParameterInfo > & getParameterInfos()
std::function< void(bool)> onPluginPrePostChanged
void setPluginName(const std::string &pluginName)