Mema
Memory Matrix — multi-channel audio matrix monitor and router
Loading...
Searching...
No Matches
TwoDFieldMultisliderComponent.cpp
Go to the documentation of this file.
1/* Copyright (c) 2024-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 <FixedFontTextEditor.h>
23
24
25namespace Mema
26{
27
28//#define PAINTINGHELPER
29
30
31//==============================================================================
32class TwoDFieldMultisliderKeyboardFocusTraverser : public juce::KeyboardFocusTraverser
33{
34public:
35 //==============================================================================
36 TwoDFieldMultisliderKeyboardFocusTraverser(const std::vector<juce::Component*>& focusElements)
37 : juce::KeyboardFocusTraverser()
38 {
39 m_focusElements = focusElements;
40 };
42
43 juce::Component* getDefaultComponent(juce::Component* parentComponent) override
44 {
45 if (m_focusElements.empty() || m_focusElements.front() == parentComponent)
46 return nullptr;
47 else
48 return m_focusElements.front();
49 };
50 juce::Component* getNextComponent(juce::Component* current) override
51 {
52 if (m_focusElements.empty())
53 return nullptr;
54
55 auto currentIter = std::find(m_focusElements.begin(), m_focusElements.end(), current);
56 if (currentIter == m_focusElements.end())
57 return nullptr;
58 else
59 {
60 if (currentIter == (m_focusElements.end() - 1))
61 return *(m_focusElements.begin());
62 else
63 return *(currentIter + 1);
64 }
65 };
66 juce::Component* getPreviousComponent(juce::Component* current) override
67 {
68 if (m_focusElements.empty())
69 return nullptr;
70
71 auto currentIter = std::find(m_focusElements.begin(), m_focusElements.end(), current);
72 if (currentIter == m_focusElements.end())
73 return nullptr;
74 else
75 {
76 if (currentIter == m_focusElements.begin())
77 return *(m_focusElements.end() - 1);
78 else
79 return *(currentIter - 1);
80 }
81 };
82 std::vector<juce::Component*> getAllComponents(juce::Component* /*parentComponent*/) override
83 {
84 return m_focusElements;
85 };
86
87private:
88 //==============================================================================
89 std::vector<juce::Component*> m_focusElements;
90};
91
92
93//==============================================================================
95 : JUCEAppBasics::TwoDFieldBase(), juce::Component()
96{
97 m_sharpnessEdit = std::make_unique<JUCEAppBasics::FixedFontTextEditor>("SharpnessEdit");
98 m_sharpnessEdit->setTooltip("Panning sharpness 0.0 ... 1.0");
99 m_sharpnessEdit->setText(juce::String(0.5), false);
100 m_sharpnessEdit->setInputFilter(new juce::TextEditor::LengthAndCharacterRestriction(3, "0123456789."), true);
101 m_sharpnessEdit->setJustification(juce::Justification::centred);
102 m_sharpnessEdit->onReturnKey = [=]() {
103 if (0 != m_currentlySelectedInput)
104 {
105 m_inputPositions[m_currentlySelectedInput].sharpness = jlimit(0.0f, 1.0f, m_sharpnessEdit->getText().getFloatValue());
107 onInputPositionChanged(m_currentlySelectedInput, m_inputPositions[m_currentlySelectedInput].value, m_inputPositions[m_currentlySelectedInput].sharpness, m_inputPositions[m_currentlySelectedInput].layer);
108 }
109 else
110 {
111 for (auto& inputPositionKV : m_inputPositions)
112 {
113 inputPositionKV.second.sharpness = jlimit(0.0f, 1.0f, m_sharpnessEdit->getText().getFloatValue());
115 onInputPositionChanged(inputPositionKV.first, inputPositionKV.second.value, inputPositionKV.second.sharpness, inputPositionKV.second.layer);
116 }
117 }
118 };
119 addAndMakeVisible(m_sharpnessEdit.get());
120 m_sharpnessLabel = std::make_unique<juce::Label>("SharpnessLabel");
121 m_sharpnessLabel->setText("Sharpness", juce::dontSendNotification);
122 m_sharpnessLabel->setJustificationType(juce::Justification::centredLeft);
123 addAndMakeVisible(m_sharpnessLabel.get());
124}
125
129
130std::unique_ptr<juce::ComponentTraverser> TwoDFieldMultisliderComponent::createKeyboardFocusTraverser()
131{
132 return std::make_unique<TwoDFieldMultisliderKeyboardFocusTraverser>(std::vector<juce::Component*>({ this, m_sharpnessEdit.get() }));
133 //juce::Component::createKeyboardFocusTraverser();
134}
135
137{
138 // (Our component is opaque, so we must completely fill the background with a solid colour)
139 g.fillAll(getLookAndFeel().findColour(juce::ResizableWindow::backgroundColourId));
140
141 // paint the level indications where applicable
142 if (!m_positionedChannelsArea.isEmpty())
143 paintCircularLevelIndication(g, m_positionedChannelsArea, m_channelLevelMaxPoints, m_clockwiseOrderedChannelTypes);
144 if (!m_positionedHeightChannelsArea.isEmpty())
145 paintCircularLevelIndication(g, m_positionedHeightChannelsArea, m_channelHeightLevelMaxPoints, m_clockwiseOrderedHeightChannelTypes);
146
147 // paint slider knobs
148 jassert(m_inputPositions.size() == m_inputPositionStackingOrder.size());
149 // reverse iteration, since what we want to be painted last(on top of everything) is at the beginning of the list!
150 for (auto const & inputNumber : std::vector<std::uint16_t>(m_inputPositionStackingOrder.rbegin(), m_inputPositionStackingOrder.rend()))
151 {
152 auto& inputPosition = m_inputPositions[inputNumber];
153 auto emptyRect = juce::Rectangle<float>();
154 auto& area = emptyRect;
155 if (ChannelLayer::Positioned == inputPosition.layer && !m_positionedChannelsArea.isEmpty())
156 area = m_positionedChannelsArea;
157 else if (ChannelLayer::PositionedHeight == inputPosition.layer && !m_positionedHeightChannelsArea.isEmpty())
158 area = m_positionedHeightChannelsArea;
159
160 if (!area.isEmpty())
161 paintSliderKnob(g, area, inputPosition.value.relXPos, inputPosition.value.relYPos, inputNumber, inputPosition.isOn, inputPosition.isSliding);
162 }
163}
164
165void TwoDFieldMultisliderComponent::paintCircularLevelIndication(juce::Graphics& g, const juce::Rectangle<float>& circleArea, const std::map<int, juce::Point<float>>& channelLevelMaxPoints, const juce::Array<juce::AudioChannelSet::ChannelType>& channelsToPaint)
166{
167#if defined DEBUG && defined PAINTINGHELPER
168 g.setColour(juce::Colours::blueviolet);
169 g.drawRect(circleArea);
170#endif
171
172 // fill circle background
173 g.setColour(getLookAndFeel().findColour(juce::ResizableWindow::backgroundColourId).darker());
174 g.fillEllipse(circleArea);
175
176#if defined DEBUG && defined PAINTINGHELPER
177 g.setColour(juce::Colours::red);
178 g.drawRect(circleArea);
179 g.setColour(juce::Colours::blue);
180 g.drawRect(getLocalBounds());
181#endif
182
183
184 const float meterWidth = 5.0f;
185 const float halfMeterWidth = 2.0f;
186
187
188 auto circleCenter = circleArea.getCentre();
189
190 // prepare max points
191 std::map<int, juce::Point<float>> centerToMaxVectors;
192 std::map<int, juce::Point<float>> meterWidthOffsetVectors;
193 for (int i = 0; i < channelsToPaint.size(); i++)
194 {
195 auto const& channelType = channelsToPaint[i];
196 auto iTy = int(channelType);
197 if (0 < channelLevelMaxPoints.count(iTy))
198 {
199 auto angleRad = juce::degreesToRadians(getAngleForChannelTypeInCurrentConfiguration(channelType));
200 centerToMaxVectors[iTy] = circleCenter - channelLevelMaxPoints.at(iTy);
201 meterWidthOffsetVectors[iTy] = { cosf(angleRad) * halfMeterWidth, sinf(angleRad) * halfMeterWidth };
202 }
203 }
204
205 // helper std::functions to avoid codeclones below
206 auto createAndPaintLevelPath = [=](std::map<int, juce::Point<float>>& centerToMaxPoints, std::map<int, juce::Point<float>>& meterWidthOffsetPoints, std::map<juce::AudioChannelSet::ChannelType, float>& levels, juce::Graphics& g, const juce::Colour& colour, bool stroke) {
207 juce::Path path;
208 auto pathStarted = false;
209 for (auto const& channelType : channelsToPaint)
210 {
211 auto iTy = int(channelType);
212 auto channelMaxPoint = circleCenter - (centerToMaxPoints[iTy] * levels[channelType]);
213
214 if (!pathStarted)
215 {
216 path.startNewSubPath(channelMaxPoint - meterWidthOffsetPoints[iTy]);
217 pathStarted = true;
218 }
219 else
220 path.lineTo(channelMaxPoint - meterWidthOffsetPoints[iTy]);
221
222 path.lineTo(channelMaxPoint + meterWidthOffsetPoints[iTy]);
223 }
224 path.closeSubPath();
225
226 g.setColour(colour);
227 if (stroke)
228 g.strokePath(path, juce::PathStrokeType(1));
229 else
230 g.fillPath(path);
231#if defined DEBUG && defined PAINTINGHELPER
232 g.setColour(juce::Colours::yellow);
233 g.drawRect(path.getBounds());
234#endif
235 };
236 auto paintLevelMeterLines = [=](std::map<int, juce::Point<float>>& centerToMaxPoints, std::map<int, juce::Point<float>>& meterWidthOffsetPoints, std::map<juce::AudioChannelSet::ChannelType, float>& levels, juce::Graphics& g, const juce::Colour& colour, bool isHoldLine) {
237 g.setColour(colour);
238 for (auto const& channelType : channelsToPaint)
239 {
240 auto iTy = int(channelType);
241 auto channelMaxPoint = circleCenter - (centerToMaxPoints[iTy] * levels[channelType]);
242
243 if (isHoldLine)
244 g.drawLine(juce::Line<float>(channelMaxPoint - meterWidthOffsetPoints[iTy], channelMaxPoint + meterWidthOffsetPoints[iTy]), 1.0f);
245 else
246 g.drawLine(juce::Line<float>(circleCenter, channelMaxPoint), meterWidth);
247 }
248 };
249
250 std::map<juce::AudioChannelSet::ChannelType, float> levels;
251 for (auto& vKV : m_inputToOutputVals)
252 {
253 levels.clear();
254 for (auto const& oVals : vKV.second)
255 {
256 auto& channel = oVals.first;
257 auto& state = oVals.second.first;
258 auto& level = oVals.second.second;
259 levels[channel] = state ? level : 0.0f;
260 }
261
262 juce::Colour colour;
263 if (m_currentlySelectedInput == vKV.first)
264 colour = getLookAndFeel().findColour(JUCEAppBasics::CustomLookAndFeel::ColourIds::MeteringRmsColourId).withAlpha(0.8f);
265 else
266 colour = getLookAndFeel().findColour(JUCEAppBasics::CustomLookAndFeel::ColourIds::MeteringRmsColourId).withAlpha(0.4f);
267
268 // paint hold values as path
269 createAndPaintLevelPath(centerToMaxVectors, meterWidthOffsetVectors, levels, g, colour, false);
270 // paint hold values as max line
271 paintLevelMeterLines(centerToMaxVectors, meterWidthOffsetVectors, levels, g, colour, false);
272 }
273
274 // draw a simple circle surrounding
275 g.setColour(getLookAndFeel().findColour(juce::TextButton::textColourOffId));
276 g.drawEllipse(circleArea, 1);
277
278 // draw dashed field dimension indication lines
279 float dparam[]{ 4.0f, 5.0f };
280 for (auto const& channelType : channelsToPaint)
281 if (0 < channelLevelMaxPoints.count(channelType))
282 g.drawDashedLine(juce::Line<float>(channelLevelMaxPoints.at(channelType), circleCenter), dparam, 2);
283
284 // draw channelType naming legend
285 g.setColour(getLookAndFeel().findColour(juce::TextButton::textColourOffId));
286 for (auto const& channelType : channelsToPaint)
287 {
288 if (0 >= channelLevelMaxPoints.count(channelType))
289 continue;
290
291 auto channelName = juce::AudioChannelSet::getAbbreviatedChannelTypeName(channelType);
292 auto textRect = juce::Rectangle<float>(juce::GlyphArrangement::getStringWidth(g.getCurrentFont(), channelName), g.getCurrentFont().getHeight());
293 auto angle = getAngleForChannelTypeInCurrentConfiguration(channelType);
294 auto textRectOffset = juce::Point<int>(-int(textRect.getWidth() / 2.0f), 0);
295 if (90.0f < angle)
296 angle += 180.0f;
297 else if (-90.0f > angle)
298 angle -= 180.0f;
299 else
300 textRectOffset.addXY(0, -int(g.getCurrentFont().getHeight()));
301 auto angleRad = juce::degreesToRadians(angle);
302
303 g.saveState();
304 g.setOrigin(channelLevelMaxPoints.at(channelType).toInt());
305 g.addTransform(juce::AffineTransform().translated(textRectOffset).rotated(angleRad));
306 g.drawText(channelName, textRect, Justification::centred, true);
307
308#if defined DEBUG && defined PAINTINGHELPER
309 g.setColour(juce::Colours::lightblue);
310 g.drawRect(textRect);
311 g.setColour(getLookAndFeel().findColour(juce::TextButton::textColourOffId));
312#endif
313
314 g.restoreState();
315 }
316}
317
318void TwoDFieldMultisliderComponent::paintSliderKnob(juce::Graphics& g, const juce::Rectangle<float>& sliderArea, const float& relXPos, const float& relYPos, const int& silderNumber, bool isSliderOn, bool isSliderSliding)
319{
320 juce::Path valueTrack;
321 auto minPoint = sliderArea.getCentre();
322 auto maxPoint = sliderArea.getCentre() + juce::Point<float>((sliderArea.getWidth() / 2) * relXPos, (sliderArea.getHeight() / 2) * relYPos * -1.0f);
323
324 if (isSliderOn)
325 {
326 if (isSliderSliding)
327 {
328 valueTrack.startNewSubPath(minPoint);
329 valueTrack.lineTo(maxPoint);
330 g.setColour(getLookAndFeel().findColour(JUCEAppBasics::CustomLookAndFeel::ColourIds::MeteringPeakColourId));
331 g.strokePath(valueTrack, { m_trackWidth, juce::PathStrokeType::curved, juce::PathStrokeType::rounded });
332 }
333
334 g.setColour(getLookAndFeel().findColour(juce::Slider::thumbColourId));
335 g.fillEllipse(juce::Rectangle<float>(static_cast<float>(m_thumbWidth), static_cast<float>(m_thumbWidth)).withCentre(maxPoint));
336
337 g.setColour(getLookAndFeel().findColour(juce::TextButton::textColourOnId));
338 g.drawText(juce::String(silderNumber), juce::Rectangle<float>(static_cast<float>(m_thumbWidth), static_cast<float>(m_thumbWidth)).withCentre(maxPoint), juce::Justification::centred);
339 }
340 else
341 {
342 if (isSliderSliding)
343 {
344 valueTrack.startNewSubPath(minPoint);
345 valueTrack.lineTo(maxPoint);
346 auto valueTrackOutline = valueTrack;
347 juce::PathStrokeType pt(m_trackWidth, juce::PathStrokeType::curved, juce::PathStrokeType::rounded);
348 pt.createStrokedPath(valueTrackOutline, valueTrack);
349 g.setColour(getLookAndFeel().findColour(JUCEAppBasics::CustomLookAndFeel::ColourIds::MeteringPeakColourId));
350 g.strokePath(valueTrackOutline, juce::PathStrokeType(1.0f));
351 }
352
353 g.setColour(getLookAndFeel().findColour(juce::ResizableWindow::ColourIds::backgroundColourId));
354 g.fillEllipse(juce::Rectangle<float>(static_cast<float>(m_thumbWidth - 1), static_cast<float>(m_thumbWidth - 1)).withCentre(maxPoint));
355
356 g.setColour(getLookAndFeel().findColour(juce::Slider::thumbColourId));
357 g.drawEllipse(juce::Rectangle<float>(static_cast<float>(m_thumbWidth), static_cast<float>(m_thumbWidth)).withCentre(maxPoint), 1.0f);
358
359 g.drawText(juce::String(silderNumber), juce::Rectangle<float>(static_cast<float> (m_thumbWidth), static_cast<float> (m_thumbWidth)).withCentre(maxPoint), juce::Justification::centred);
360 }
361}
362
364{
365 // process areas for level indication painting
366 auto coreTwoDFieldOnly = usesPositionedChannels() && !usesPositionedHeightChannels() && !usesDirectionlessChannels();
367 auto coreTwoDFieldWithMeterbridge = usesPositionedChannels() && !usesPositionedHeightChannels() && usesDirectionlessChannels();
368 auto bothTwoDFields = usesPositionedChannels() && usesPositionedHeightChannels() && !usesDirectionlessChannels();
369 auto bothTwoDFieldsWithMeterbridge = usesPositionedChannels() && usesPositionedHeightChannels() && usesDirectionlessChannels();
370
371 auto margin = 12.0f;
372 auto bounds = getLocalBounds().reduced(8).toFloat();
373 auto width = bounds.getWidth();
374 auto height = bounds.getHeight();
375
376 auto sharpnessBounds = bounds.toNearestInt();
377 sharpnessBounds = sharpnessBounds.removeFromBottom(40);
378 if (m_sharpnessEdit)
379 m_sharpnessEdit->setBounds(sharpnessBounds.removeFromBottom(20).removeFromLeft(40));
380 if (m_sharpnessLabel)
381 m_sharpnessLabel->setBounds(sharpnessBounds.removeFromLeft(75));
382
383 if (coreTwoDFieldOnly)
384 {
385 m_positionedChannelsArea = bounds.reduced(margin);
386 m_positionedHeightChannelsArea = {};
387 m_directionlessChannelsArea = {};
388 }
389 else if (coreTwoDFieldWithMeterbridge)
390 {
391 m_positionedChannelsArea = bounds;
392 m_directionlessChannelsArea = m_positionedChannelsArea.removeFromRight(float(m_directionLessChannelTypes.size() * m_ctrlsSize));
393 m_positionedChannelsArea.reduce(margin, margin);
394
395 m_positionedHeightChannelsArea = {};
396 }
397 else if (bothTwoDFields)
398 {
399 m_positionedHeightChannelsArea = bounds.reduced(margin);
400 m_positionedHeightChannelsArea.removeFromRight(width * (8.4f / 12.0f));
401 m_positionedHeightChannelsArea.removeFromBottom(height * (5.4f / 10.0f));
402
403 m_positionedChannelsArea = bounds.reduced(margin);
404 m_positionedChannelsArea.removeFromLeft(width * (3.4f / 12.0f));
405 m_positionedChannelsArea.removeFromTop(height * (1.4f / 10.0f));
406
407 m_directionlessChannelsArea = {};
408 }
409 else if (bothTwoDFieldsWithMeterbridge)
410 {
411 m_positionedHeightChannelsArea = bounds.reduced(margin);
412 m_positionedHeightChannelsArea.removeFromRight(width * (8.4f / 13.0f));
413 m_positionedHeightChannelsArea.removeFromBottom(height * (5.4f / 10.0f));
414
415 m_positionedChannelsArea = bounds;
416 m_directionlessChannelsArea = m_positionedChannelsArea.removeFromRight(float(m_directionLessChannelTypes.size() * m_ctrlsSize));
417 m_positionedChannelsArea.reduce(margin, margin);
418 m_positionedChannelsArea.removeFromLeft(width * (3.4f / 13.0f));
419 m_positionedChannelsArea.removeFromTop(height * (1.4f / 10.0f));
420 }
421
422 // Constrain each field area to a square so the drawn circle is not distorted
423 if (!m_positionedChannelsArea.isEmpty())
424 {
425 auto squareSize = juce::jmin(m_positionedChannelsArea.getWidth(), m_positionedChannelsArea.getHeight());
426 m_positionedChannelsArea = juce::Rectangle<float>(squareSize, squareSize).withCentre(m_positionedChannelsArea.getCentre());
427 }
428 if (!m_positionedHeightChannelsArea.isEmpty())
429 {
430 auto squareSize = juce::jmin(m_positionedHeightChannelsArea.getWidth(), m_positionedHeightChannelsArea.getHeight());
431 m_positionedHeightChannelsArea = juce::Rectangle<float>(squareSize, squareSize).withCentre(m_positionedHeightChannelsArea.getCentre());
432 }
433
434 for (auto const& channelType : m_clockwiseOrderedChannelTypes)
435 {
436 auto angleRad = juce::degreesToRadians(getAngleForChannelTypeInCurrentConfiguration(channelType));
437 auto radius = m_positionedChannelsArea.getWidth() / 2;
438 auto xLength = sinf(angleRad) * radius;
439 auto yLength = cosf(angleRad) * radius;
440 m_channelLevelMaxPoints[channelType] = m_positionedChannelsArea.getCentre() + juce::Point<float>(xLength, -yLength);
441 }
442
443 for (auto const& channelType : m_clockwiseOrderedHeightChannelTypes)
444 {
445 auto angleRad = juce::degreesToRadians(getAngleForChannelTypeInCurrentConfiguration(channelType));
446 auto radius = m_positionedHeightChannelsArea.getWidth() / 2;
447 auto xLength = sinf(angleRad) * radius;
448 auto yLength = cosf(angleRad) * radius;
449 m_channelHeightLevelMaxPoints[channelType] = m_positionedHeightChannelsArea.getCentre() + juce::Point<float>(xLength, -yLength);
450 }
451
452 if (!m_directionlessChannelsArea.isEmpty() && !m_directionslessChannelSliders.empty() && !m_directionslessChannelLabels.empty())
453 {
454 jassert(m_directionslessChannelSliders.size() == m_directionslessChannelLabels.size());
455 auto directionlessChannelsWidth = m_directionlessChannelsArea.getWidth() / m_directionslessChannelSliders.size();
456 auto areaToDivide = m_directionlessChannelsArea;
457 for (auto const& sliderKV : m_directionslessChannelSliders)
458 {
459 auto const& slider = sliderKV.second;
460 auto const& label = m_directionslessChannelLabels.at(sliderKV.first);
461 auto sliderBounds = areaToDivide.removeFromLeft(directionlessChannelsWidth).toNearestInt();
462 auto labelBounds = sliderBounds.removeFromBottom(m_thumbWidth);
463 if (slider)
464 slider->setBounds(sliderBounds);
465 if (label)
466 label->setBounds(labelBounds);
467 }
468 }
469}
470
472{
473 if (!m_directionslessChannelSliders.empty())
474 {
475 for (auto const& slider : m_directionslessChannelSliders)
476 if (slider.second)
477 slider.second->setColour(juce::Slider::ColourIds::trackColourId, getLookAndFeel().findColour(JUCEAppBasics::CustomLookAndFeel::ColourIds::MeteringRmsColourId));
478 }
479}
480
481void TwoDFieldMultisliderComponent::mouseDown(const juce::MouseEvent& e)
482{
483 // hit-test slider knobs
484 auto hadHit = false;
485 jassert(m_inputPositions.size() == m_inputPositionStackingOrder.size());
486 for (auto const& inputNumber : m_inputPositionStackingOrder)
487 {
488 auto& inputPosition = m_inputPositions[inputNumber];
489 auto emptyRect = juce::Rectangle<float>();
490 auto& area = emptyRect;
491 if (ChannelLayer::Positioned == inputPosition.layer && !m_positionedChannelsArea.isEmpty())
492 area = m_positionedChannelsArea;
493 else if (ChannelLayer::PositionedHeight == inputPosition.layer && !m_positionedHeightChannelsArea.isEmpty())
494 area = m_positionedHeightChannelsArea;
495 else if (ChannelLayer::Directionless == inputPosition.layer && !m_directionlessChannelsArea.isEmpty())
496 area = m_directionlessChannelsArea;
497
498 auto maxPoint = area.getCentre() - juce::Point<float>((area.getWidth() / 2) * -inputPosition.value.relXPos, (area.getHeight() / 2) * inputPosition.value.relYPos);
499 auto sliderKnob = juce::Rectangle<float>(static_cast<float>(m_thumbWidth), static_cast<float>(m_thumbWidth)).withCentre(maxPoint);
500 if (sliderKnob.contains(e.getMouseDownPosition().toFloat()) && false == hadHit)
501 {
502 inputPosition.isOn = true;
503
504 selectInput(inputNumber, true, juce::sendNotification);
505
506 hadHit = true;
507 }
508 else
509 {
510 selectInput(inputNumber, false, juce::sendNotification);
511 }
512 }
513
514 juce::Component::mouseDown(e);
515}
516
518{
519 juce::Component::mouseUp(e);
520}
521
523{
524 if (e.mouseWasDraggedSinceMouseDown())
525 {
526 // reset any sliding states slider knobs
527 jassert(m_inputPositions.size() == m_inputPositionStackingOrder.size());
528 for (auto const& inputNumber : m_inputPositionStackingOrder)
529 {
530 auto& inputPosition = m_inputPositions[inputNumber];
531 if (inputPosition.isSliding)
532 {
533 auto emptyRect = juce::Rectangle<float>();
534 auto& area = emptyRect;
535 if (ChannelLayer::Positioned == inputPosition.layer && !m_positionedChannelsArea.isEmpty())
536 area = m_positionedChannelsArea;
537 else if (ChannelLayer::PositionedHeight == inputPosition.layer && !m_positionedHeightChannelsArea.isEmpty())
538 area = m_positionedHeightChannelsArea;
539 else if (ChannelLayer::Directionless == inputPosition.layer && !m_directionlessChannelsArea.isEmpty())
540 area = m_directionlessChannelsArea;
541
542 auto mousePosition = e.getMouseDownPosition() + e.getOffsetFromDragStart();
543
544 juce::Path ellipsePath;
545 ellipsePath.addEllipse(area);
546 // if the mouse is within the resp. circle, do the regular calculation of knob pos
547 if (ellipsePath.contains(mousePosition.toFloat()))
548 {
549 auto positionInArea = area.getCentre() - area.getConstrainedPoint(mousePosition.toFloat());
550 auto relXPos = positionInArea.getX() / (0.5f * area.getWidth());
551 auto relYPos = positionInArea.getY() / (0.5f * area.getHeight());
552 setInputPosition(inputNumber, { -relXPos, relYPos }, inputPosition.sharpness, inputPosition.layer, juce::sendNotification);
553 }
554 else
555 {
556 juce::Path positionedChannelsEllipsePath, positionedHeightChannelsEllipsePath;
557 positionedChannelsEllipsePath.addEllipse(m_positionedChannelsArea);
558 positionedHeightChannelsEllipsePath.addEllipse(m_positionedHeightChannelsArea);
559 // check if the mouse has entered another circle area while dragging
560 if (positionedChannelsEllipsePath.contains(mousePosition.toFloat()))
561 {
562 auto positionInArea = m_positionedChannelsArea.getCentre() - m_positionedChannelsArea.getConstrainedPoint(mousePosition.toFloat());
563 auto relXPos = positionInArea.getX() / (0.5f * m_positionedChannelsArea.getWidth());
564 auto relYPos = positionInArea.getY() / (0.5f * m_positionedChannelsArea.getHeight());
565 setInputPosition(inputNumber, { -relXPos, relYPos }, inputPosition.sharpness, ChannelLayer::Positioned, juce::sendNotification);
566 }
567 else if (positionedHeightChannelsEllipsePath.contains(mousePosition.toFloat()))
568 {
569 auto positionInArea = m_positionedHeightChannelsArea.getCentre() - m_positionedHeightChannelsArea.getConstrainedPoint(mousePosition.toFloat());
570 auto relXPos = positionInArea.getX() / (0.5f * m_positionedHeightChannelsArea.getWidth());
571 auto relYPos = positionInArea.getY() / (0.5f * m_positionedHeightChannelsArea.getHeight());
572 setInputPosition(inputNumber, { -relXPos, relYPos }, inputPosition.sharpness, ChannelLayer::PositionedHeight, juce::sendNotification);
573 }
574 // finally do the clipping to original circle, if the dragging happens somewhere outside everything
575 else
576 {
577 juce::Point<float> constrainedPoint;
578 ellipsePath.getNearestPoint(mousePosition.toFloat(), constrainedPoint);
579 auto positionInArea = area.getCentre() - constrainedPoint;
580 auto relXPos = positionInArea.getX() / (0.5f * area.getWidth());
581 auto relYPos = positionInArea.getY() / (0.5f * area.getHeight());
582 inputPosition.value = { relXPos, relYPos };
583 setInputPosition(inputNumber, { -relXPos, relYPos }, inputPosition.sharpness, inputPosition.layer, juce::sendNotification);
584 }
585
586 }
587 }
588 }
589 }
590
591 juce::Component::mouseDrag(e);
592}
593
594void TwoDFieldMultisliderComponent::setInputPosition(std::uint16_t channel, const TwoDMultisliderValue& value, const float& sharpness, const ChannelLayer& layer, juce::NotificationType notification)
595{
596 jassert(m_inputPositions.size() == m_inputPositionStackingOrder.size());
597 m_inputPositions[channel].value = value;
598 m_inputPositions[channel].sharpness = sharpness;
599 m_inputPositions[channel].layer = layer;
600
601 repaint();
602
603 DBG(juce::String(__FUNCTION__) << " new pos: " << int(channel) << " " << value.relXPos << "," << value.relYPos << "(" << sharpness << "/" << layer << ")");
604
605 if (juce::dontSendNotification != notification && onInputPositionChanged)
606 onInputPositionChanged(channel, value, sharpness, layer);
607}
608
609void TwoDFieldMultisliderComponent::setInputPositionValue(std::uint16_t channel, const TwoDMultisliderValue& value, juce::NotificationType notification)
610{
611 jassert(m_inputPositions.size() == m_inputPositionStackingOrder.size());
612 m_inputPositions[channel].value = value;
613
614 repaint();
615
616 DBG(juce::String(__FUNCTION__) << " new pos: " << int(channel) << " " << value.relXPos << "," << value.relYPos << "(" << m_inputPositions[channel].sharpness << "/" << m_inputPositions[channel].layer << ")");
617
618 if (juce::dontSendNotification != notification && onInputPositionChanged)
619 onInputPositionChanged(channel, value, m_inputPositions[channel].sharpness, m_inputPositions[channel].layer);
620}
621
622void TwoDFieldMultisliderComponent::setInputPositionSharpness(std::uint16_t channel, const float& sharpness, juce::NotificationType notification)
623{
624 jassert(m_inputPositions.size() == m_inputPositionStackingOrder.size());
625 m_inputPositions[channel].sharpness = sharpness;
626
627 if (m_sharpnessEdit && 0 != m_currentlySelectedInput)
628 m_sharpnessEdit->setText(juce::String(sharpness), juce::dontSendNotification);
629
630 repaint();
631
632 DBG(juce::String(__FUNCTION__) << " new pos: " << int(channel) << " " << m_inputPositions[channel].value.relXPos << "," << m_inputPositions[channel].value.relYPos << "(" << sharpness << "/" << m_inputPositions[channel].layer << ")");
633
634 if (juce::dontSendNotification != notification && onInputPositionChanged)
635 onInputPositionChanged(channel, m_inputPositions[channel].value, sharpness, m_inputPositions[channel].layer);
636}
637
638void TwoDFieldMultisliderComponent::setInputPositionLayer(std::uint16_t channel, const ChannelLayer& layer, juce::NotificationType notification)
639{
640 jassert(m_inputPositions.size() == m_inputPositionStackingOrder.size());
641 m_inputPositions[channel].layer = layer;
642
643 repaint();
644
645 DBG(juce::String(__FUNCTION__) << " new pos: " << int(channel) << " " << m_inputPositions[channel].value.relXPos << "," << m_inputPositions[channel].value.relYPos << "(" << m_inputPositions[channel].sharpness << "/" << layer << ")");
646
647 if (juce::dontSendNotification != notification && onInputPositionChanged)
648 onInputPositionChanged(channel, m_inputPositions[channel].value, m_inputPositions[channel].sharpness, layer);
649}
650
652{
653 jassert(m_inputPositions.size() == m_inputPositionStackingOrder.size());
654 for (auto const& inputNumber : m_inputPositionStackingOrder)
655 {
656 auto& inputPosition = m_inputPositions[inputNumber];
658 onInputPositionChanged(inputNumber, inputPosition.value, inputPosition.sharpness, inputPosition.layer);
659 }
660}
661
662void TwoDFieldMultisliderComponent::selectInput(std::uint16_t channel, bool selectOn, juce::NotificationType notification)
663{
664 jassert(0 != m_inputPositions.count(channel));
665 if (0 == m_inputPositions.count(channel))
666 return;
667
668 if (selectOn)
669 {
670 auto posIter = std::find(m_inputPositionStackingOrder.begin(), m_inputPositionStackingOrder.end(), channel);
671 if (posIter != m_inputPositionStackingOrder.end() && posIter != m_inputPositionStackingOrder.begin())
672 {
673 m_inputPositionStackingOrder.erase(posIter);
674 m_inputPositionStackingOrder.insert(m_inputPositionStackingOrder.begin(), channel);
675 }
676 }
677
678 m_inputPositions[channel].isSliding = selectOn;
679 if (m_currentlySelectedInput == channel && !selectOn)
680 m_currentlySelectedInput = 0;
681 else if (m_currentlySelectedInput != channel && selectOn)
682 m_currentlySelectedInput = channel;
683
684 repaint();
685
686 if (!m_directionslessChannelSliders.empty() && !m_inputToOutputVals.empty())
687 {
688 for (auto const& slider : m_directionslessChannelSliders)
689 {
690 if (slider.second)
691 {
692 if (0 == m_currentlySelectedInput && slider.second)
693 {
694 configureDirectionlessSliderToRelativeCtrl(slider.first, *slider.second);
695 }
696 else if (selectOn && 0 < m_inputToOutputVals.count(channel))
697 {
698 auto output = slider.first;
699 jassert(0 < m_inputToOutputVals.at(channel).count(output));
700 if(0 < m_inputToOutputVals.at(channel).count(output))
701 {
702 slider.second->setTitle(juce::String(channel));
703 slider.second->setRange(0.0, 1.0, 0.01);
704 slider.second->displayValueConverter = [](double val) { return juce::String(juce::Decibels::gainToDecibels(val, static_cast<double>(ProcessorDataAnalyzer::getGlobalMindB())), 1) + " dB"; };
705 slider.second->setValue(m_inputToOutputVals.at(channel).at(output).second);
706 slider.second->setToggleState(m_inputToOutputVals.at(channel).at(output).first, juce::dontSendNotification);
707 }
708 }
709 }
710 }
711 }
712
713 if (m_sharpnessEdit)
714 {
715 if (0 != m_currentlySelectedInput)
716 {
717 m_sharpnessLabel->setText("In" + juce::String(m_currentlySelectedInput) + " sharpness", juce::dontSendNotification);
718 m_sharpnessEdit->setText(juce::String(m_inputPositions[m_currentlySelectedInput].sharpness), juce::dontSendNotification);
719 }
720 else
721 {
722 m_sharpnessLabel->setText("All sharpness", juce::dontSendNotification);
723 m_sharpnessEdit->setTextToShowWhenEmpty("0.5", getLookAndFeel().findColour(juce::TextButton::ColourIds::textColourOffId));
724 m_sharpnessEdit->setText("", juce::dontSendNotification);
725 }
726 }
727
728 if (juce::dontSendNotification != notification && onInputSelected && selectOn)
729 onInputSelected(channel);
730}
731
732void TwoDFieldMultisliderComponent::setIOCount(const std::pair<int, int>& ioCount)
733{
734 auto channelsToRemove = std::vector<std::uint16_t>();
735 auto channelsToAdd = std::vector<std::uint16_t>();
736 for (auto const& inputPositionKV : m_inputPositions)
737 {
738 if (inputPositionKV.first > ioCount.first)
739 channelsToRemove.push_back(inputPositionKV.first);
740 }
741 for (auto i = std::uint16_t(1); i <= ioCount.first; i++)
742 {
743 if (0 == m_inputPositions.count(i))
744 channelsToAdd.push_back(i);
745 }
746
747 for (auto const& channelToRemove : channelsToRemove)
748 {
749 m_inputPositions.erase(channelToRemove);
750 auto iterToRemove = std::find(m_inputPositionStackingOrder.begin(), m_inputPositionStackingOrder.end(), channelToRemove);
751 if (iterToRemove != m_inputPositionStackingOrder.end())
752 m_inputPositionStackingOrder.erase(iterToRemove);
753 }
754
755 auto angleRadDistributionSegment = channelsToAdd.empty() ? 1.0f : juce::degreesToRadians(360.0f / channelsToAdd.size());
756 auto defaultPosAngleRad = 0.0f;
757 for (auto const& channelToAdd : channelsToAdd)
758 {
759 m_inputPositionStackingOrder.push_back(channelToAdd);
760
761 auto defaultXPos = 0.5f * sinf(defaultPosAngleRad);
762 auto defaultYPos = 0.5f * cosf(defaultPosAngleRad);
763 m_inputPositions[channelToAdd] = { ChannelLayer::Positioned, { defaultXPos, defaultYPos }, 0.5f, false, false };
764 defaultPosAngleRad -= angleRadDistributionSegment;
765 }
766
767 m_currentOutputCount = ioCount.second;
768
769 repaint();
770}
771
773{
774 m_ctrlsSize = ctrlsSize;
775 m_thumbWidth = int(float(4 * ctrlsSize) / 7.0f);
776 m_trackWidth = float(8 * ctrlsSize) / 35.0f;
777
778 resized();
779 repaint();
780}
781
782void TwoDFieldMultisliderComponent::setInputToOutputStates(const std::map<std::uint16_t, std::map<std::uint16_t, bool>>& inputToOutputStates)
783{
784 for (auto const& iKV : inputToOutputStates)
785 {
786 for (auto const& oKV : iKV.second)
787 {
788 jassert(0 < iKV.first);
789 auto output = getChannelTypeForChannelNumberInCurrentConfiguration(oKV.first);
790 if (juce::AudioChannelSet::ChannelType::unknown != output)
791 m_inputToOutputVals[iKV.first][output].first = oKV.second;
792 if (m_directionLessChannelTypes.contains(output) && m_directionslessChannelSliders.at(output) && m_currentlySelectedInput == iKV.first)
793 m_directionslessChannelSliders.at(output)->setValue(oKV.second, juce::dontSendNotification);
794 }
795 }
796
797 repaint();
798}
799
800void TwoDFieldMultisliderComponent::setInputToOutputLevels(const std::map<std::uint16_t, std::map<std::uint16_t, float>>& inputToOutputLevels)
801{
802 for (auto const& iKV : inputToOutputLevels)
803 {
804 for (auto const& oKV : iKV.second)
805 {
806 jassert(0 < iKV.first);
807 auto output = getChannelTypeForChannelNumberInCurrentConfiguration(oKV.first);
808 if (juce::AudioChannelSet::ChannelType::unknown != output)
809 m_inputToOutputVals[iKV.first][output].second = oKV.second;
810 if (m_directionLessChannelTypes.contains(output) && m_directionslessChannelSliders.at(output) && m_currentlySelectedInput == iKV.first)
811 m_directionslessChannelSliders.at(output)->setValue(oKV.second, juce::dontSendNotification);
812 }
813 }
814
815 if (0 == m_currentlySelectedInput)
816 for (auto const& sliderKV : m_directionslessChannelSliders)
817 if (sliderKV.second && sliderKV.second->displayValueConverter) // hacky: use the presence of the valueconverter as indicator if we need to switch to relctrl or not
818 configureDirectionlessSliderToRelativeCtrl(sliderKV.first, *sliderKV.second);
819
820 repaint();
821}
822
823bool TwoDFieldMultisliderComponent::setChannelConfiguration(const juce::AudioChannelSet& channelLayout)
824{
825 auto wasUpdated = TwoDFieldBase::setChannelConfiguration(channelLayout);
826 if (wasUpdated)
827 rebuildDirectionslessChannelSliders();
828
829 return wasUpdated;
830}
831
832void TwoDFieldMultisliderComponent::rebuildDirectionslessChannelSliders()
833{
834 m_directionslessChannelSliders.clear();
835 for (auto const& channelType : m_directionLessChannelTypes)
836 {
837 m_directionslessChannelSliders[channelType] = std::make_unique<JUCEAppBasics::ToggleStateSlider>(juce::Slider::LinearVertical, juce::Slider::NoTextBox);
838 m_directionslessChannelSliders[channelType]->setColour(juce::Slider::ColourIds::trackColourId, getLookAndFeel().findColour(JUCEAppBasics::CustomLookAndFeel::ColourIds::MeteringRmsColourId));
839 m_directionslessChannelSliders[channelType]->setRange(0.0, 1.0, 0.01);
840 m_directionslessChannelSliders[channelType]->setToggleState(false, juce::dontSendNotification);
841 m_directionslessChannelSliders[channelType]->displayValueConverter = [](double val) { return juce::String(juce::Decibels::gainToDecibels(val, static_cast<double>(ProcessorDataAnalyzer::getGlobalMindB())), 1) + " dB"; };
842 m_directionslessChannelSliders[channelType]->onToggleStateChange = [this, channelType]() {
843 std::map<std::uint16_t, std::map<std::uint16_t, bool >> states;
844 if (0 != m_currentlySelectedInput)
845 {
846 m_inputToOutputVals[m_currentlySelectedInput][channelType].first = m_directionslessChannelSliders[channelType]->getToggleState();
847 states[m_currentlySelectedInput][std::uint16_t(getChannelNumberForChannelTypeInCurrentConfiguration(channelType))] = m_directionslessChannelSliders[channelType]->getToggleState();
848 }
849 else
850 {
851 for (auto& ioValKV : m_inputToOutputVals)
852 {
853 ioValKV.second[channelType].first = m_directionslessChannelSliders[channelType]->getToggleState();
854 auto outputChannel = getChannelNumberForChannelTypeInCurrentConfiguration(channelType);
855 if (outputChannel <= m_currentOutputCount)
856 states[ioValKV.first][std::uint16_t(outputChannel)] = m_directionslessChannelSliders[channelType]->getToggleState();
857 }
858 }
859
862 };
863 m_directionslessChannelSliders[channelType]->onValueChange = [this, channelType]() {
864 std::map<std::uint16_t, std::map<std::uint16_t, float >> values;
865 if (0 != m_currentlySelectedInput)
866 {
867 auto value = float(m_directionslessChannelSliders[channelType]->getValue());
868 m_inputToOutputVals[m_currentlySelectedInput][channelType].second = value;
869 values[m_currentlySelectedInput][std::uint16_t(getChannelNumberForChannelTypeInCurrentConfiguration(channelType))] = value;
870 }
871 else
872 {
873 auto latestValue = m_directionslessChannelSliders[channelType]->getValue();
874 auto latestDelta = latestValue - m_directionlessSliderRelRef[channelType];
875 for (auto& ioValKV : m_inputToOutputVals)
876 {
877 ioValKV.second[channelType].second = jlimit(0.0f, 1.0f, ioValKV.second[channelType].second + float(latestDelta));
878 auto outputChannel = getChannelNumberForChannelTypeInCurrentConfiguration(channelType);
879 if (outputChannel <= m_currentOutputCount)
880 values[ioValKV.first][std::uint16_t(outputChannel)] = ioValKV.second[channelType].second;
881 //DBG(juce::String(ioValKV.first) << ">" << juce::AudioChannelSet::getAbbreviatedChannelTypeName(channelType) << " val:" << ioValKV.second[channelType].second);
882 }
883 m_directionlessSliderRelRef[channelType] = latestValue;
884 }
885
888 };
889 addAndMakeVisible(m_directionslessChannelSliders[channelType].get());
890
891 m_directionslessChannelLabels[channelType] = std::make_unique<juce::Label>();
892 m_directionslessChannelLabels[channelType]->setText(juce::AudioChannelSet::getAbbreviatedChannelTypeName(channelType), juce::dontSendNotification);
893 m_directionslessChannelLabels[channelType]->setJustificationType(juce::Justification::centred);
894 addAndMakeVisible(m_directionslessChannelLabels[channelType].get());
895 }
896}
897
898void TwoDFieldMultisliderComponent::configureDirectionlessSliderToRelativeCtrl(const juce::AudioChannelSet::ChannelType& channelType, JUCEAppBasics::ToggleStateSlider& slider)
899{
900 auto anyInputToDirectionLessOff = false;
901 for (auto const& iKV : m_inputToOutputVals)
902 {
903 if (0 < iKV.second.count(channelType) && !iKV.second.at(channelType).first)
904 {
905 anyInputToDirectionLessOff = true;
906 break;
907 }
908 }
909
910 slider.setTitle("");
911 slider.setRange(0.0, 1.0, 0.01);
912 slider.setValue(0.5); // relative starting
913 slider.displayValueConverter = {};
914 slider.setToggleState(!anyInputToDirectionLessOff, juce::dontSendNotification);
915 m_directionlessSliderRelRef[channelType] = 0.5;
916}
917
918
919}
std::function< void(std::uint16_t channel, const TwoDMultisliderValue &value, const float &sharpness, std::optional< ChannelLayer > layer)> onInputPositionChanged
void setInputPositionValue(std::uint16_t channel, const TwoDMultisliderValue &value, juce::NotificationType notification=juce::dontSendNotification)
void mouseDown(const juce::MouseEvent &e) override
void setInputPositionLayer(std::uint16_t channel, const ChannelLayer &layer, juce::NotificationType notification=juce::dontSendNotification)
void setInputPositionSharpness(std::uint16_t channel, const float &sharpness, juce::NotificationType notification=juce::dontSendNotification)
void setInputToOutputStates(const std::map< std::uint16_t, std::map< std::uint16_t, bool > > &inputToOutputStates)
void selectInput(std::uint16_t channel, bool selectOn, juce::NotificationType notification=juce::dontSendNotification)
std::function< void(std::uint16_t channel)> onInputSelected
void setInputPosition(std::uint16_t channel, const TwoDMultisliderValue &value, const float &panningSharpness, const ChannelLayer &layer, juce::NotificationType notification=juce::dontSendNotification)
std::function< void(const std::map< std::uint16_t, std::map< std::uint16_t, float > > &)> onInputToOutputValuesChanged
std::function< void(const std::map< std::uint16_t, std::map< std::uint16_t, bool > > &)> onInputToOutputStatesChanged
void setIOCount(const std::pair< int, int > &ioCount)
void setInputToOutputLevels(const std::map< std::uint16_t, std::map< std::uint16_t, float > > &inputToOutputLevels)
std::unique_ptr< juce::ComponentTraverser > createKeyboardFocusTraverser() override
bool setChannelConfiguration(const juce::AudioChannelSet &channelLayout) override
std::vector< juce::Component * > getAllComponents(juce::Component *) override
juce::Component * getDefaultComponent(juce::Component *parentComponent) override
TwoDFieldMultisliderKeyboardFocusTraverser(const std::vector< juce::Component * > &focusElements)
juce::Component * getNextComponent(juce::Component *current) override
juce::Component * getPreviousComponent(juce::Component *current) override