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) 2024, 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
22namespace Mema
23{
24
25//==============================================================================
26// ParameterRowComponent
27//==============================================================================
28
30 : paramIndex(paramIdx)
31{
32 toggleButton = std::make_unique<juce::ToggleButton>(info.name);
33 toggleButton->setToggleState(info.isRemoteControllable, juce::dontSendNotification);
34 addAndMakeVisible(toggleButton.get());
35
36 typeCombo = std::make_unique<juce::ComboBox>();
37 typeCombo->addItem("Continuous", static_cast<int>(ParameterControlType::Continuous) + 1);
38 typeCombo->addItem("Discrete", static_cast<int>(ParameterControlType::Discrete) + 1);
39 typeCombo->addItem("Toggle", static_cast<int>(ParameterControlType::Toggle) + 1);
40 typeCombo->setSelectedId(static_cast<int>(info.type) + 1, juce::dontSendNotification);
41 addAndMakeVisible(typeCombo.get());
42
43 stepsEdit = std::make_unique<JUCEAppBasics::FixedFontTextEditor>();
44 stepsEdit->setText(info.type == ParameterControlType::Toggle ? juce::String(2) : juce::String(info.stepCount), juce::dontSendNotification);
45 stepsEdit->setEnabled(info.type != ParameterControlType::Toggle);
46 stepsEdit->setInputRestrictions(3, "0123456789");
47 addAndMakeVisible(stepsEdit.get());
48
49 typeCombo->onChange = [this]() {
50 auto selectedType = static_cast<ParameterControlType>(typeCombo->getSelectedId() - 1);
51 stepsEdit->setEnabled(selectedType != ParameterControlType::Toggle);
52 if (selectedType == ParameterControlType::Toggle)
53 stepsEdit->setText(juce::String(2), juce::dontSendNotification);
54 };
55}
56
58{
59 auto bounds = getLocalBounds();
60 bounds.removeFromLeft(gripWidth);
61 toggleButton->setBounds(bounds.removeFromLeft(160));
62 typeCombo->setBounds(bounds.removeFromLeft(100));
63 stepsEdit->setBounds(bounds);
64}
65
66void ParameterRowComponent::paint(juce::Graphics& g)
67{
68 // Grip dots (2 columns × 3 rows)
69 const float dotSize = 2.0f;
70 const float colGap = 5.0f;
71 const float rowGap = 5.0f;
72 const float startX = (gripWidth - colGap - dotSize) * 0.5f;
73 const float startY = (getHeight() - 2.0f * rowGap - dotSize) * 0.5f;
74 g.setColour(findColour(juce::Label::textColourId).withAlpha(0.4f));
75 for (int r = 0; r < 3; ++r)
76 for (int c = 0; c < 2; ++c)
77 g.fillEllipse(startX + c * colGap, startY + r * rowGap, dotSize, dotSize);
78
79 // Insertion indicator drawn at the top or bottom edge
80 if (m_showInsertionLine)
81 {
82 g.setColour(findColour(juce::TextButton::ColourIds::textColourOnId));
83 g.fillRect(0, m_insertionLineAtTop ? 0 : getHeight() - 2, getWidth(), 2);
84 }
85}
86
87void ParameterRowComponent::mouseDown(const juce::MouseEvent& e)
88{
89 m_mouseDownInGrip = (e.x < gripWidth);
90}
91
92void ParameterRowComponent::mouseDrag(const juce::MouseEvent& e)
93{
94 if (m_mouseDownInGrip)
95 {
96 if (auto* container = juce::DragAndDropContainer::findParentDragContainerFor(this))
97 if (!container->isDragAndDropActive())
98 container->startDragging(juce::var(paramIndex), this);
99 }
100}
101
102void ParameterRowComponent::mouseUp(const juce::MouseEvent&)
103{
104 m_mouseDownInGrip = false;
105}
106
107bool ParameterRowComponent::isInterestedInDragSource(const SourceDetails& details)
108{
109 auto* source = dynamic_cast<ParameterRowComponent*>(details.sourceComponent.get());
110 return source != nullptr && source->paramIndex != paramIndex;
111}
112
113void ParameterRowComponent::itemDragEnter(const SourceDetails& details)
114{
115 m_showInsertionLine = true;
116 m_insertionLineAtTop = (details.localPosition.y < getHeight() / 2);
117 repaint();
118}
119
120void ParameterRowComponent::itemDragMove(const SourceDetails& details)
121{
122 bool atTop = (details.localPosition.y < getHeight() / 2);
123 if (atTop != m_insertionLineAtTop)
124 {
125 m_insertionLineAtTop = atTop;
126 repaint();
127 }
128}
129
130void ParameterRowComponent::itemDragExit(const SourceDetails&)
131{
132 m_showInsertionLine = false;
133 repaint();
134}
135
136void ParameterRowComponent::itemDropped(const SourceDetails& details)
137{
138 auto* source = dynamic_cast<ParameterRowComponent*>(details.sourceComponent.get());
139 if (source && onRowDropped)
140 onRowDropped(source->paramIndex, paramIndex, details.localPosition.y < getHeight() / 2);
141 m_showInsertionLine = false;
142 repaint();
143}
144
145//==============================================================================
146// ParameterListComponent
147//==============================================================================
148
149void ParameterListComponent::addRow(std::unique_ptr<ParameterRowComponent> row)
150{
151 row->onRowDropped = [this](int from, int to, bool before) {
152 reorderRow(from, to, before);
153 };
154 addAndMakeVisible(row.get());
155 m_rows.push_back(std::move(row));
156 layoutRows();
157}
158
160{
161 const int rowHeight = 28;
162 for (int i = 0; i < static_cast<int>(m_rows.size()); ++i)
163 m_rows[i]->setBounds(0, i * rowHeight, getWidth(), rowHeight);
164}
165
167{
168 std::vector<int> order;
169 order.reserve(m_rows.size());
170 for (auto const& row : m_rows)
171 order.push_back(row->paramIndex);
172 return order;
173}
174
176{
177 for (auto& row : m_rows)
178 if (row->paramIndex == paramIdx)
179 return row.get();
180 return nullptr;
181}
182
183void ParameterListComponent::reorderRow(int fromParamIndex, int toParamIndex, bool insertBefore)
184{
185 int fromIdx = -1, toIdx = -1;
186 for (int i = 0; i < static_cast<int>(m_rows.size()); ++i)
187 {
188 if (m_rows[i]->paramIndex == fromParamIndex) fromIdx = i;
189 if (m_rows[i]->paramIndex == toParamIndex) toIdx = i;
190 }
191
192 if (fromIdx < 0 || toIdx < 0)
193 return;
194
195 int insertIdx = insertBefore ? toIdx : toIdx + 1;
196
197 // Adjust insertion index for the removal of the source row
198 if (insertIdx > fromIdx)
199 insertIdx--;
200
201 if (insertIdx == fromIdx)
202 return;
203
204 auto movedRow = std::move(m_rows[fromIdx]);
205 m_rows.erase(m_rows.begin() + fromIdx);
206 m_rows.insert(m_rows.begin() + insertIdx, std::move(movedRow));
207
208 layoutRows();
209}
210
211//==============================================================================
213 : juce::Component()
214{
215 m_enableButton = std::make_unique<juce::DrawableButton>("Enable plug-in", juce::DrawableButton::ButtonStyle::ImageOnButtonBackground);
216 m_enableButton->setTooltip("Enable plug-in");
217 m_enableButton->setClickingTogglesState(true);
218 m_enableButton->onClick = [this] {
219 if (onPluginEnabledChange)
220 onPluginEnabledChange(m_enableButton->getToggleState());
221 };
222 addAndMakeVisible(m_enableButton.get());
223
224 m_spacing1 = std::make_unique<Spacing>();
225 addAndMakeVisible(m_spacing1.get());
226
227 m_postButton = std::make_unique<juce::TextButton>("Post", "Toggle plug-in pre/post");
228 m_postButton->setClickingTogglesState(true);
229 m_postButton->onClick = [this] {
230 if (onPluginPrePostChange)
231 onPluginPrePostChange(m_postButton->getToggleState());
232 };
233 addAndMakeVisible(m_postButton.get());
234
235 m_spacing2 = std::make_unique<Spacing>();
236 addAndMakeVisible(m_spacing2.get());
237
238 m_showEditorButton = std::make_unique<juce::TextButton>("None", "Show plug-in editor");
239 m_showEditorButton->onClick = [this] {
240 if (onShowPluginEditor)
241 onShowPluginEditor();
242 };
243 addAndMakeVisible(m_showEditorButton.get());
244
245 m_triggerSelectButton = std::make_unique<juce::DrawableButton>("Show plug-in selection menu", juce::DrawableButton::ButtonStyle::ImageOnButtonBackground);
246 m_triggerSelectButton->setTooltip("Show plug-in selection menu");
247 m_triggerSelectButton->onClick = [this] {
248 showPluginsList(juce::Desktop::getMousePosition());
249 };
250 addAndMakeVisible(m_triggerSelectButton.get());
251
252 m_spacing3 = std::make_unique<Spacing>();
253 addAndMakeVisible(m_spacing3.get());
254
255 m_parameterConfigButton = std::make_unique<juce::DrawableButton>("Configure plug-in parameters", juce::DrawableButton::ButtonStyle::ImageOnButtonBackground);
256 m_parameterConfigButton->setTooltip("Configure plug-in parameters");
257 m_parameterConfigButton->onClick = [this] {
258 showParameterConfig();
259 };
260 addAndMakeVisible(m_parameterConfigButton.get());
261
262 m_spacing4 = std::make_unique<Spacing>();
263 addAndMakeVisible(m_spacing4.get());
264
265 m_clearButton = std::make_unique<juce::DrawableButton>("Clear current plug-in", juce::DrawableButton::ButtonStyle::ImageOnButtonBackground);
266 m_clearButton->setTooltip("Clear current plug-in");
267 m_clearButton->onClick = [this] {
268 if (onClearPlugin)
269 onClearPlugin();
270 };
271 addAndMakeVisible(m_clearButton.get());
272
273 m_pluginSelectionComponent = std::make_unique<PluginListAndSelectComponent>();
274 m_pluginSelectionComponent->onPluginSelected = [=](const juce::PluginDescription& pluginDescription) {
275 if (onPluginSelected)
276 onPluginSelected(pluginDescription);
277 };
278}
279
281{
282
283}
284
285void PluginControlComponent::showPluginsList(juce::Point<int> showPosition)
286{
287 m_pluginSelectionComponent->setVisible(true);
288 m_pluginSelectionComponent->addToDesktop(juce::ComponentPeer::windowHasDropShadow);
289
290 auto const display = juce::Desktop::getInstance().getDisplays().getPrimaryDisplay();
291 if (nullptr != display && nullptr != m_pluginSelectionComponent)
292 {
293 if (display->totalArea.getHeight() < showPosition.getY() + m_pluginSelectionComponent->getHeight())
294 showPosition.setY(showPosition.getY() - m_pluginSelectionComponent->getHeight() - 30);
295 if (display->totalArea.getWidth() < showPosition.getX() + m_pluginSelectionComponent->getWidth())
296 showPosition.setX(showPosition.getX() - m_pluginSelectionComponent->getWidth() - 30);
297 }
298 m_pluginSelectionComponent->setTopLeftPosition(showPosition);
299}
300
302{
303 if (m_enableButton)
304 m_enableButton->setToggleState(enabled, juce::dontSendNotification);
305}
306
308{
309 if (m_postButton)
310 m_postButton->setToggleState(post, juce::dontSendNotification);
311}
312
313void PluginControlComponent::setSelectedPlugin(const juce::PluginDescription& pluginDescription)
314{
315 m_selectedPluginDescription = pluginDescription;
316
317 if (m_showEditorButton)
318 {
319 if (m_selectedPluginDescription.name.isEmpty())
320 m_showEditorButton->setButtonText("None");
321 else
322 m_showEditorButton->setButtonText(m_selectedPluginDescription.name);
323 }
324}
325
326void PluginControlComponent::setParameterInfos(const std::vector<Mema::PluginParameterInfo>& infos)
327{
328 if (infos.size() != m_parameterInfos.size())
329 {
330 m_parameterInfos.clear();
331 m_parameterDisplayOrder.clear();
332 }
333
334 auto key = 0;
335 for (auto const& info : infos)
336 {
337 if (0 < m_parameterInfos.count(key) || std::as_const(m_parameterInfos[key]) != info)
338 m_parameterInfos[key] = info;
339 key++;
340 }
341}
342
343const std::map<int, Mema::PluginParameterInfo>& PluginControlComponent::getParameterInfos()
344{
345 return m_parameterInfos;
346}
347
348void PluginControlComponent::setParameterDisplayOrder(const std::vector<int>& order)
349{
350 m_parameterDisplayOrder = order;
351}
352
354{
355 if (m_parameterDisplayOrder.empty())
356 for (auto& kv : m_parameterInfos)
357 m_parameterDisplayOrder.push_back(kv.first);
358 return m_parameterDisplayOrder;
359}
360
362{
363 if (m_selectedPluginDescription.name.isEmpty())
364 {
365 m_messageBox = std::make_unique<juce::AlertWindow>(
366 "Plug-in parameter setup not available",
367 "No plug-in selected.",
368 juce::MessageBoxIconType::WarningIcon);
369 m_messageBox->addButton("Ok", 1, juce::KeyPress(juce::KeyPress::returnKey));
370 m_messageBox->enterModalState(true, juce::ModalCallbackFunction::create([=](int returnValue) {
371 ignoreUnused(returnValue);
372 m_messageBox.reset();
373 }));
374 }
375 else if (m_parameterInfos.empty())
376 {
377 m_messageBox = std::make_unique<juce::AlertWindow>(
378 "Plug-in parameter setup not available",
379 "No parameters detected.",
380 juce::MessageBoxIconType::WarningIcon);
381 m_messageBox->addButton("Ok", 1, juce::KeyPress(juce::KeyPress::returnKey));
382 m_messageBox->enterModalState(true, juce::ModalCallbackFunction::create([=](int returnValue) {
383 ignoreUnused(returnValue);
384 m_messageBox.reset();
385 }));
386 }
387 else
388 {
389 m_messageBox = std::make_unique<juce::AlertWindow>(
390 "Plug-in parameter setup",
391 "Select which parameters should be remote-controllable and configure their control type.\nDrag the grip handle on the left of each row to reorder.",
392 juce::MessageBoxIconType::NoIcon);
393
394 // Initialise display order from natural key order if not yet set
395 if (m_parameterDisplayOrder.empty())
396 for (auto& kv : m_parameterInfos)
397 m_parameterDisplayOrder.push_back(kv.first);
398
399 // Save for cancel restore
400 auto savedDisplayOrder = m_parameterDisplayOrder;
401
402 const int rowHeight = 28;
403 const int totalWidth = ParameterRowComponent::gripWidth + 160 + 100 + 60; // grip + toggle + combo + steps
404 const int totalHeight = static_cast<int>(m_parameterInfos.size()) * rowHeight;
405
406 // Build the drag-reorderable list in current display order
407 m_messageBoxParameterListComponent = std::make_unique<ParameterListComponent>();
408 m_messageBoxParameterListComponent->setSize(totalWidth, totalHeight);
409 for (int paramIdx : m_parameterDisplayOrder)
410 m_messageBoxParameterListComponent->addRow(
411 std::make_unique<ParameterRowComponent>(paramIdx, m_parameterInfos.at(paramIdx)));
412
413 // Wrap in a scrollable viewport capped to avoid overflowing the screen
414 const int maxViewportHeight = 400;
415 const int viewportHeight = juce::jmin(totalHeight, maxViewportHeight);
416
417 m_messageBoxParameterTogglesViewport = std::make_unique<juce::Viewport>();
418 m_messageBoxParameterTogglesViewport->setViewedComponent(m_messageBoxParameterListComponent.get(), false);
419 m_messageBoxParameterTogglesViewport->setScrollBarsShown(totalHeight > maxViewportHeight, false);
420 m_messageBoxParameterTogglesViewport->setSize(
421 totalWidth + (totalHeight > maxViewportHeight ? m_messageBoxParameterTogglesViewport->getScrollBarThickness() : 0),
422 viewportHeight);
423
424 m_messageBox->addCustomComponent(m_messageBoxParameterTogglesViewport.get());
425 m_messageBox->addButton("Cancel", 0, juce::KeyPress(juce::KeyPress::escapeKey));
426 m_messageBox->addButton("Ok", 1, juce::KeyPress(juce::KeyPress::returnKey));
427
428 m_messageBox->enterModalState(true, juce::ModalCallbackFunction::create([=](int returnValue) {
429 if (returnValue == 1)
430 {
431 auto changeDetected = false;
432
433 for (auto& parameterKV : m_parameterInfos)
434 {
435 auto paramIndex = parameterKV.first;
436 auto& paramInfo = parameterKV.second;
437
438 auto* row = m_messageBoxParameterListComponent->getRowForParamIndex(paramIndex);
439 if (!row) continue;
440
441 // Check remote controllable toggle
442 auto newRemoteControllable = row->toggleButton->getToggleState();
443 if (newRemoteControllable != paramInfo.isRemoteControllable)
444 {
445 paramInfo.isRemoteControllable = newRemoteControllable;
446 changeDetected = true;
447 }
448
449 // Check control type
450 auto newType = static_cast<ParameterControlType>(row->typeCombo->getSelectedId() - 1);
451 if (newType != paramInfo.type)
452 {
453 paramInfo.type = newType;
454 if (newType == ParameterControlType::Toggle)
455 paramInfo.stepCount = 2;
456 changeDetected = true;
457 }
458
459 // Check step count - ignored for toggle type
460 if (newType != ParameterControlType::Toggle)
461 {
462 auto newStepCount = row->stepsEdit->getText().getIntValue();
463 newStepCount = juce::jmax(2, newStepCount); // Enforce minimum of 2 steps
464 if (newStepCount != paramInfo.stepCount)
465 {
466 paramInfo.stepCount = newStepCount;
467 changeDetected = true;
468 }
469 }
470 }
471
472 auto newDisplayOrder = m_messageBoxParameterListComponent->getDisplayOrder();
473 if (newDisplayOrder != savedDisplayOrder)
474 changeDetected = true;
475 m_parameterDisplayOrder = newDisplayOrder;
476
477 if (changeDetected)
478 {
481 }
482 }
483 else
484 {
485 // Cancel: restore the display order to what it was before the dialog opened
486 m_parameterDisplayOrder = savedDisplayOrder;
487 }
488
489 m_messageBoxParameterTogglesViewport.reset();
490 m_messageBoxParameterListComponent.reset();
491 m_messageBox.reset();
492 }));
493 }
494}
495
497{
498 auto bounds = getLocalBounds();
499 auto margin = 1;
500
501 if (m_enableButton)
502 m_enableButton->setBounds(bounds.removeFromLeft(bounds.getHeight()));
503 if (m_spacing1)
504 m_spacing1->setBounds(bounds.removeFromLeft(margin));
505 if (m_postButton)
506 m_postButton->setBounds(bounds.removeFromLeft(int(1.5f * bounds.getHeight())));
507 if (m_spacing2)
508 m_spacing2->setBounds(bounds.removeFromLeft(margin));
509 if (m_clearButton)
510 m_clearButton->setBounds(bounds.removeFromRight(bounds.getHeight()));
511 if (m_spacing3)
512 m_spacing3->setBounds(bounds.removeFromRight(margin));
513 if (m_parameterConfigButton)
514 m_parameterConfigButton->setBounds(bounds.removeFromRight(bounds.getHeight()));
515 if (m_spacing4)
516 m_spacing4->setBounds(bounds.removeFromRight(margin));
517 if (m_triggerSelectButton)
518 m_triggerSelectButton->setBounds(bounds.removeFromRight(bounds.getHeight()));
519 if (m_showEditorButton)
520 m_showEditorButton->setBounds(bounds);
521}
522
524{
525 // (Our component is opaque, so we must completely fill the background with a solid colour)
526 g.fillAll(getLookAndFeel().findColour(juce::LookAndFeel_V4::ColourScheme::defaultFill));
527}
528
530{
531 auto enableDrawable = juce::Drawable::createFromSVG(*juce::XmlDocument::parse(BinaryData::power_settings_24dp_svg).get());
532 enableDrawable->replaceColour(juce::Colours::black, getLookAndFeel().findColour(juce::TextButton::ColourIds::textColourOnId));
533 m_enableButton->setImages(enableDrawable.get());
534
535 auto triggerSelectDrawable = juce::Drawable::createFromSVG(*juce::XmlDocument::parse(BinaryData::stat_minus_1_24dp_svg).get());
536 triggerSelectDrawable->replaceColour(juce::Colours::black, getLookAndFeel().findColour(juce::TextButton::ColourIds::textColourOnId));
537 m_triggerSelectButton->setImages(triggerSelectDrawable.get());
538
539 auto parameterConfigButtonDrawable = juce::Drawable::createFromSVG(*juce::XmlDocument::parse(BinaryData::settings_24dp_svg).get());
540 parameterConfigButtonDrawable->replaceColour(juce::Colours::black, getLookAndFeel().findColour(juce::TextButton::ColourIds::textColourOnId));
541 m_parameterConfigButton->setImages(parameterConfigButtonDrawable.get());
542
543 auto clearButtonDrawable = juce::Drawable::createFromSVG(*juce::XmlDocument::parse(BinaryData::replay_24dp_svg).get());
544 clearButtonDrawable->replaceColour(juce::Colours::black, getLookAndFeel().findColour(juce::TextButton::ColourIds::textColourOnId));
545 m_clearButton->setImages(clearButtonDrawable.get());
546
547 juce::Component::lookAndFeelChanged();
548}
549
550
551}
ParameterRowComponent * getRowForParamIndex(int paramIdx)
std::vector< int > getDisplayOrder() const
void addRow(std::unique_ptr< ParameterRowComponent > row)
void reorderRow(int fromParamIndex, int toParamIndex, bool insertBefore)
std::function< void(int fromParamIndex, int toParamIndex, bool insertBefore)> onRowDropped
std::unique_ptr< JUCEAppBasics::FixedFontTextEditor > stepsEdit
void itemDragMove(const SourceDetails &details) override
void itemDropped(const SourceDetails &details) override
std::unique_ptr< juce::ComboBox > typeCombo
void paint(juce::Graphics &g) override
bool isInterestedInDragSource(const SourceDetails &details) override
void itemDragEnter(const SourceDetails &details) override
ParameterRowComponent(int paramIdx, const Mema::PluginParameterInfo &info)
void mouseDown(const juce::MouseEvent &e) override
void mouseDrag(const juce::MouseEvent &e) override
void itemDragExit(const SourceDetails &details) override
void mouseUp(const juce::MouseEvent &e) override
std::unique_ptr< juce::ToggleButton > toggleButton
void setSelectedPlugin(const juce::PluginDescription &pluginDescription)
void setParameterInfos(const std::vector< Mema::PluginParameterInfo > &parameterInfos)
std::function< void()> onPluginParametersStatusChanged
const std::vector< Mema::PluginParameterInfo > & getParameterInfos()
void setParameterDisplayOrder(const std::vector< int > &order)
void showPluginsList(juce::Point< int > showPosition)
const std::vector< int > & getParameterDisplayOrder()
Metadata describing a single plugin parameter exposed for remote control.
juce::String name
Human-readable parameter name.
bool isRemoteControllable
Whether this parameter is exposed for remote control.
int stepCount
Number of discrete steps (0 if continuous).
ParameterControlType type
Control widget type (slider, combo, toggle).