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 for (auto const& channelType : m_clockwiseOrderedChannelTypes)
423 {
424 auto angleRad = juce::degreesToRadians(getAngleForChannelTypeInCurrentConfiguration(channelType));
425 auto xLength = sinf(angleRad) * (m_positionedChannelsArea.getHeight() / 2);
426 auto yLength = cosf(angleRad) * (m_positionedChannelsArea.getWidth() / 2);
427 m_channelLevelMaxPoints[channelType] = m_positionedChannelsArea.getCentre() + juce::Point<float>(xLength, -yLength);
428 }
429
430 for (auto const& channelType : m_clockwiseOrderedHeightChannelTypes)
431 {
432 auto angleRad = juce::degreesToRadians(getAngleForChannelTypeInCurrentConfiguration(channelType));
433 auto xLength = sinf(angleRad) * (m_positionedHeightChannelsArea.getHeight() / 2);
434 auto yLength = cosf(angleRad) * (m_positionedHeightChannelsArea.getWidth() / 2);
435 m_channelHeightLevelMaxPoints[channelType] = m_positionedHeightChannelsArea.getCentre() + juce::Point<float>(xLength, -yLength);
436 }
437
438 if (!m_directionlessChannelsArea.isEmpty() && !m_directionslessChannelSliders.empty() && !m_directionslessChannelLabels.empty())
439 {
440 jassert(m_directionslessChannelSliders.size() == m_directionslessChannelLabels.size());
441 auto directionlessChannelsWidth = m_directionlessChannelsArea.getWidth() / m_directionslessChannelSliders.size();
442 auto areaToDivide = m_directionlessChannelsArea;
443 for (auto const& sliderKV : m_directionslessChannelSliders)
444 {
445 auto const& slider = sliderKV.second;
446 auto const& label = m_directionslessChannelLabels.at(sliderKV.first);
447 auto sliderBounds = areaToDivide.removeFromLeft(directionlessChannelsWidth).toNearestInt();
448 auto labelBounds = sliderBounds.removeFromBottom(m_thumbWidth);
449 if (slider)
450 slider->setBounds(sliderBounds);
451 if (label)
452 label->setBounds(labelBounds);
453 }
454 }
455}
456
458{
459 if (!m_directionslessChannelSliders.empty())
460 {
461 for (auto const& slider : m_directionslessChannelSliders)
462 if (slider.second)
463 slider.second->setColour(juce::Slider::ColourIds::trackColourId, getLookAndFeel().findColour(JUCEAppBasics::CustomLookAndFeel::ColourIds::MeteringRmsColourId));
464 }
465}
466
467void TwoDFieldMultisliderComponent::mouseDown(const juce::MouseEvent& e)
468{
469 // hit-test slider knobs
470 auto hadHit = false;
471 jassert(m_inputPositions.size() == m_inputPositionStackingOrder.size());
472 for (auto const& inputNumber : m_inputPositionStackingOrder)
473 {
474 auto& inputPosition = m_inputPositions[inputNumber];
475 auto emptyRect = juce::Rectangle<float>();
476 auto& area = emptyRect;
477 if (ChannelLayer::Positioned == inputPosition.layer && !m_positionedChannelsArea.isEmpty())
478 area = m_positionedChannelsArea;
479 else if (ChannelLayer::PositionedHeight == inputPosition.layer && !m_positionedHeightChannelsArea.isEmpty())
480 area = m_positionedHeightChannelsArea;
481 else if (ChannelLayer::Directionless == inputPosition.layer && !m_directionlessChannelsArea.isEmpty())
482 area = m_directionlessChannelsArea;
483
484 auto maxPoint = area.getCentre() - juce::Point<float>((area.getWidth() / 2) * -inputPosition.value.relXPos, (area.getHeight() / 2) * inputPosition.value.relYPos);
485 auto sliderKnob = juce::Rectangle<float>(static_cast<float>(m_thumbWidth), static_cast<float>(m_thumbWidth)).withCentre(maxPoint);
486 if (sliderKnob.contains(e.getMouseDownPosition().toFloat()) && false == hadHit)
487 {
488 inputPosition.isOn = true;
489
490 selectInput(inputNumber, true, juce::sendNotification);
491
492 hadHit = true;
493 }
494 else
495 {
496 selectInput(inputNumber, false, juce::sendNotification);
497 }
498 }
499
500 juce::Component::mouseDown(e);
501}
502
504{
505 juce::Component::mouseUp(e);
506}
507
509{
510 if (e.mouseWasDraggedSinceMouseDown())
511 {
512 // reset any sliding states slider knobs
513 jassert(m_inputPositions.size() == m_inputPositionStackingOrder.size());
514 for (auto const& inputNumber : m_inputPositionStackingOrder)
515 {
516 auto& inputPosition = m_inputPositions[inputNumber];
517 if (inputPosition.isSliding)
518 {
519 auto emptyRect = juce::Rectangle<float>();
520 auto& area = emptyRect;
521 if (ChannelLayer::Positioned == inputPosition.layer && !m_positionedChannelsArea.isEmpty())
522 area = m_positionedChannelsArea;
523 else if (ChannelLayer::PositionedHeight == inputPosition.layer && !m_positionedHeightChannelsArea.isEmpty())
524 area = m_positionedHeightChannelsArea;
525 else if (ChannelLayer::Directionless == inputPosition.layer && !m_directionlessChannelsArea.isEmpty())
526 area = m_directionlessChannelsArea;
527
528 auto mousePosition = e.getMouseDownPosition() + e.getOffsetFromDragStart();
529
530 juce::Path ellipsePath;
531 ellipsePath.addEllipse(area);
532 // if the mouse is within the resp. circle, do the regular calculation of knob pos
533 if (ellipsePath.contains(mousePosition.toFloat()))
534 {
535 auto positionInArea = area.getCentre() - area.getConstrainedPoint(mousePosition.toFloat());
536 auto relXPos = positionInArea.getX() / (0.5f * area.getWidth());
537 auto relYPos = positionInArea.getY() / (0.5f * area.getHeight());
538 setInputPosition(inputNumber, { -relXPos, relYPos }, inputPosition.sharpness, inputPosition.layer, juce::sendNotification);
539 }
540 else
541 {
542 juce::Path positionedChannelsEllipsePath, positionedHeightChannelsEllipsePath;
543 positionedChannelsEllipsePath.addEllipse(m_positionedChannelsArea);
544 positionedHeightChannelsEllipsePath.addEllipse(m_positionedHeightChannelsArea);
545 // check if the mouse has entered another circle area while dragging
546 if (positionedChannelsEllipsePath.contains(mousePosition.toFloat()))
547 {
548 auto positionInArea = m_positionedChannelsArea.getCentre() - m_positionedChannelsArea.getConstrainedPoint(mousePosition.toFloat());
549 auto relXPos = positionInArea.getX() / (0.5f * m_positionedChannelsArea.getWidth());
550 auto relYPos = positionInArea.getY() / (0.5f * m_positionedChannelsArea.getHeight());
551 setInputPosition(inputNumber, { -relXPos, relYPos }, inputPosition.sharpness, ChannelLayer::Positioned, juce::sendNotification);
552 }
553 else if (positionedHeightChannelsEllipsePath.contains(mousePosition.toFloat()))
554 {
555 auto positionInArea = m_positionedHeightChannelsArea.getCentre() - m_positionedHeightChannelsArea.getConstrainedPoint(mousePosition.toFloat());
556 auto relXPos = positionInArea.getX() / (0.5f * m_positionedHeightChannelsArea.getWidth());
557 auto relYPos = positionInArea.getY() / (0.5f * m_positionedHeightChannelsArea.getHeight());
558 setInputPosition(inputNumber, { -relXPos, relYPos }, inputPosition.sharpness, ChannelLayer::PositionedHeight, juce::sendNotification);
559 }
560 // finally do the clipping to original circle, if the dragging happens somewhere outside everything
561 else
562 {
563 juce::Point<float> constrainedPoint;
564 ellipsePath.getNearestPoint(mousePosition.toFloat(), constrainedPoint);
565 auto positionInArea = area.getCentre() - constrainedPoint;
566 auto relXPos = positionInArea.getX() / (0.5f * area.getWidth());
567 auto relYPos = positionInArea.getY() / (0.5f * area.getHeight());
568 inputPosition.value = { relXPos, relYPos };
569 setInputPosition(inputNumber, { -relXPos, relYPos }, inputPosition.sharpness, inputPosition.layer, juce::sendNotification);
570 }
571
572 }
573 }
574 }
575 }
576
577 juce::Component::mouseDrag(e);
578}
579
580void TwoDFieldMultisliderComponent::setInputPosition(std::uint16_t channel, const TwoDMultisliderValue& value, const float& sharpness, const ChannelLayer& layer, juce::NotificationType notification)
581{
582 jassert(m_inputPositions.size() == m_inputPositionStackingOrder.size());
583 m_inputPositions[channel].value = value;
584 m_inputPositions[channel].sharpness = sharpness;
585 m_inputPositions[channel].layer = layer;
586
587 repaint();
588
589 DBG(juce::String(__FUNCTION__) << " new pos: " << int(channel) << " " << value.relXPos << "," << value.relYPos << "(" << sharpness << "/" << layer << ")");
590
591 if (juce::dontSendNotification != notification && onInputPositionChanged)
592 onInputPositionChanged(channel, value, sharpness, layer);
593}
594
595void TwoDFieldMultisliderComponent::setInputPositionValue(std::uint16_t channel, const TwoDMultisliderValue& value, juce::NotificationType notification)
596{
597 jassert(m_inputPositions.size() == m_inputPositionStackingOrder.size());
598 m_inputPositions[channel].value = value;
599
600 repaint();
601
602 DBG(juce::String(__FUNCTION__) << " new pos: " << int(channel) << " " << value.relXPos << "," << value.relYPos << "(" << m_inputPositions[channel].sharpness << "/" << m_inputPositions[channel].layer << ")");
603
604 if (juce::dontSendNotification != notification && onInputPositionChanged)
605 onInputPositionChanged(channel, value, m_inputPositions[channel].sharpness, m_inputPositions[channel].layer);
606}
607
608void TwoDFieldMultisliderComponent::setInputPositionSharpness(std::uint16_t channel, const float& sharpness, juce::NotificationType notification)
609{
610 jassert(m_inputPositions.size() == m_inputPositionStackingOrder.size());
611 m_inputPositions[channel].sharpness = sharpness;
612
613 if (m_sharpnessEdit && 0 != m_currentlySelectedInput)
614 m_sharpnessEdit->setText(juce::String(sharpness), juce::dontSendNotification);
615
616 repaint();
617
618 DBG(juce::String(__FUNCTION__) << " new pos: " << int(channel) << " " << m_inputPositions[channel].value.relXPos << "," << m_inputPositions[channel].value.relYPos << "(" << sharpness << "/" << m_inputPositions[channel].layer << ")");
619
620 if (juce::dontSendNotification != notification && onInputPositionChanged)
621 onInputPositionChanged(channel, m_inputPositions[channel].value, sharpness, m_inputPositions[channel].layer);
622}
623
624void TwoDFieldMultisliderComponent::setInputPositionLayer(std::uint16_t channel, const ChannelLayer& layer, juce::NotificationType notification)
625{
626 jassert(m_inputPositions.size() == m_inputPositionStackingOrder.size());
627 m_inputPositions[channel].layer = layer;
628
629 repaint();
630
631 DBG(juce::String(__FUNCTION__) << " new pos: " << int(channel) << " " << m_inputPositions[channel].value.relXPos << "," << m_inputPositions[channel].value.relYPos << "(" << m_inputPositions[channel].sharpness << "/" << layer << ")");
632
633 if (juce::dontSendNotification != notification && onInputPositionChanged)
634 onInputPositionChanged(channel, m_inputPositions[channel].value, m_inputPositions[channel].sharpness, layer);
635}
636
638{
639 jassert(m_inputPositions.size() == m_inputPositionStackingOrder.size());
640 for (auto const& inputNumber : m_inputPositionStackingOrder)
641 {
642 auto& inputPosition = m_inputPositions[inputNumber];
644 onInputPositionChanged(inputNumber, inputPosition.value, inputPosition.sharpness, inputPosition.layer);
645 }
646}
647
648void TwoDFieldMultisliderComponent::selectInput(std::uint16_t channel, bool selectOn, juce::NotificationType notification)
649{
650 jassert(0 != m_inputPositions.count(channel));
651 if (0 == m_inputPositions.count(channel))
652 return;
653
654 if (selectOn)
655 {
656 auto posIter = std::find(m_inputPositionStackingOrder.begin(), m_inputPositionStackingOrder.end(), channel);
657 if (posIter != m_inputPositionStackingOrder.end() && posIter != m_inputPositionStackingOrder.begin())
658 {
659 m_inputPositionStackingOrder.erase(posIter);
660 m_inputPositionStackingOrder.insert(m_inputPositionStackingOrder.begin(), channel);
661 }
662 }
663
664 m_inputPositions[channel].isSliding = selectOn;
665 if (m_currentlySelectedInput == channel && !selectOn)
666 m_currentlySelectedInput = 0;
667 else if (m_currentlySelectedInput != channel && selectOn)
668 m_currentlySelectedInput = channel;
669
670 repaint();
671
672 if (!m_directionslessChannelSliders.empty() && !m_inputToOutputVals.empty())
673 {
674 for (auto const& slider : m_directionslessChannelSliders)
675 {
676 if (slider.second)
677 {
678 if (0 == m_currentlySelectedInput && slider.second)
679 {
680 configureDirectionlessSliderToRelativeCtrl(slider.first, *slider.second);
681 }
682 else if (selectOn && 0 < m_inputToOutputVals.count(channel))
683 {
684 auto output = slider.first;
685 jassert(0 < m_inputToOutputVals.at(channel).count(output));
686 if(0 < m_inputToOutputVals.at(channel).count(output))
687 {
688 slider.second->setTitle(juce::String(channel));
689 slider.second->setRange(0.0, 1.0, 0.01);
690 slider.second->displayValueConverter = [](double val) { return juce::String(juce::Decibels::gainToDecibels(val, static_cast<double>(ProcessorDataAnalyzer::getGlobalMindB())), 1) + " dB"; };
691 slider.second->setValue(m_inputToOutputVals.at(channel).at(output).second);
692 slider.second->setToggleState(m_inputToOutputVals.at(channel).at(output).first, juce::dontSendNotification);
693 }
694 }
695 }
696 }
697 }
698
699 if (m_sharpnessEdit)
700 {
701 if (0 != m_currentlySelectedInput)
702 {
703 m_sharpnessLabel->setText("In" + juce::String(m_currentlySelectedInput) + " sharpness", juce::dontSendNotification);
704 m_sharpnessEdit->setText(juce::String(m_inputPositions[m_currentlySelectedInput].sharpness), juce::dontSendNotification);
705 }
706 else
707 {
708 m_sharpnessLabel->setText("All sharpness", juce::dontSendNotification);
709 m_sharpnessEdit->setTextToShowWhenEmpty("0.5", getLookAndFeel().findColour(juce::TextButton::ColourIds::textColourOffId));
710 m_sharpnessEdit->setText("", juce::dontSendNotification);
711 }
712 }
713
714 if (juce::dontSendNotification != notification && onInputSelected && selectOn)
715 onInputSelected(channel);
716}
717
718void TwoDFieldMultisliderComponent::setIOCount(const std::pair<int, int>& ioCount)
719{
720 auto channelsToRemove = std::vector<std::uint16_t>();
721 auto channelsToAdd = std::vector<std::uint16_t>();
722 for (auto const& inputPositionKV : m_inputPositions)
723 {
724 if (inputPositionKV.first > ioCount.first)
725 channelsToRemove.push_back(inputPositionKV.first);
726 }
727 for (auto i = std::uint16_t(1); i <= ioCount.first; i++)
728 {
729 if (0 == m_inputPositions.count(i))
730 channelsToAdd.push_back(i);
731 }
732
733 for (auto const& channelToRemove : channelsToRemove)
734 {
735 m_inputPositions.erase(channelToRemove);
736 auto iterToRemove = std::find(m_inputPositionStackingOrder.begin(), m_inputPositionStackingOrder.end(), channelToRemove);
737 if (iterToRemove != m_inputPositionStackingOrder.end())
738 m_inputPositionStackingOrder.erase(iterToRemove);
739 }
740
741 auto angleRadDistributionSegment = channelsToAdd.empty() ? 1.0f : juce::degreesToRadians(360.0f / channelsToAdd.size());
742 auto defaultPosAngleRad = 0.0f;
743 for (auto const& channelToAdd : channelsToAdd)
744 {
745 m_inputPositionStackingOrder.push_back(channelToAdd);
746
747 auto defaultXPos = 0.5f * sinf(defaultPosAngleRad);
748 auto defaultYPos = 0.5f * cosf(defaultPosAngleRad);
749 m_inputPositions[channelToAdd] = { ChannelLayer::Positioned, { defaultXPos, defaultYPos }, 0.5f, false, false };
750 defaultPosAngleRad -= angleRadDistributionSegment;
751 }
752
753 m_currentOutputCount = ioCount.second;
754
755 repaint();
756}
757
759{
760 m_ctrlsSize = ctrlsSize;
761 m_thumbWidth = int(float(4 * ctrlsSize) / 7.0f);
762 m_trackWidth = float(8 * ctrlsSize) / 35.0f;
763
764 resized();
765 repaint();
766}
767
768void TwoDFieldMultisliderComponent::setInputToOutputStates(const std::map<std::uint16_t, std::map<std::uint16_t, bool>>& inputToOutputStates)
769{
770 for (auto const& iKV : inputToOutputStates)
771 {
772 for (auto const& oKV : iKV.second)
773 {
774 jassert(0 < iKV.first);
775 auto output = getChannelTypeForChannelNumberInCurrentConfiguration(oKV.first);
776 if (juce::AudioChannelSet::ChannelType::unknown != output)
777 m_inputToOutputVals[iKV.first][output].first = oKV.second;
778 if (m_directionLessChannelTypes.contains(output) && m_directionslessChannelSliders.at(output) && m_currentlySelectedInput == iKV.first)
779 m_directionslessChannelSliders.at(output)->setValue(oKV.second, juce::dontSendNotification);
780 }
781 }
782
783 repaint();
784}
785
786void TwoDFieldMultisliderComponent::setInputToOutputLevels(const std::map<std::uint16_t, std::map<std::uint16_t, float>>& inputToOutputLevels)
787{
788 for (auto const& iKV : inputToOutputLevels)
789 {
790 for (auto const& oKV : iKV.second)
791 {
792 jassert(0 < iKV.first);
793 auto output = getChannelTypeForChannelNumberInCurrentConfiguration(oKV.first);
794 if (juce::AudioChannelSet::ChannelType::unknown != output)
795 m_inputToOutputVals[iKV.first][output].second = oKV.second;
796 if (m_directionLessChannelTypes.contains(output) && m_directionslessChannelSliders.at(output) && m_currentlySelectedInput == iKV.first)
797 m_directionslessChannelSliders.at(output)->setValue(oKV.second, juce::dontSendNotification);
798 }
799 }
800
801 if (0 == m_currentlySelectedInput)
802 for (auto const& sliderKV : m_directionslessChannelSliders)
803 if (sliderKV.second && sliderKV.second->displayValueConverter) // hacky: use the presence of the valueconverter as indicator if we need to switch to relctrl or not
804 configureDirectionlessSliderToRelativeCtrl(sliderKV.first, *sliderKV.second);
805
806 repaint();
807}
808
809bool TwoDFieldMultisliderComponent::setChannelConfiguration(const juce::AudioChannelSet& channelLayout)
810{
811 auto wasUpdated = TwoDFieldBase::setChannelConfiguration(channelLayout);
812 if (wasUpdated)
813 rebuildDirectionslessChannelSliders();
814
815 return wasUpdated;
816}
817
818void TwoDFieldMultisliderComponent::rebuildDirectionslessChannelSliders()
819{
820 m_directionslessChannelSliders.clear();
821 for (auto const& channelType : m_directionLessChannelTypes)
822 {
823 m_directionslessChannelSliders[channelType] = std::make_unique<JUCEAppBasics::ToggleStateSlider>(juce::Slider::LinearVertical, juce::Slider::NoTextBox);
824 m_directionslessChannelSliders[channelType]->setColour(juce::Slider::ColourIds::trackColourId, getLookAndFeel().findColour(JUCEAppBasics::CustomLookAndFeel::ColourIds::MeteringRmsColourId));
825 m_directionslessChannelSliders[channelType]->setRange(0.0, 1.0, 0.01);
826 m_directionslessChannelSliders[channelType]->setToggleState(false, juce::dontSendNotification);
827 m_directionslessChannelSliders[channelType]->displayValueConverter = [](double val) { return juce::String(juce::Decibels::gainToDecibels(val, static_cast<double>(ProcessorDataAnalyzer::getGlobalMindB())), 1) + " dB"; };
828 m_directionslessChannelSliders[channelType]->onToggleStateChange = [this, channelType]() {
829 std::map<std::uint16_t, std::map<std::uint16_t, bool >> states;
830 if (0 != m_currentlySelectedInput)
831 {
832 m_inputToOutputVals[m_currentlySelectedInput][channelType].first = m_directionslessChannelSliders[channelType]->getToggleState();
833 states[m_currentlySelectedInput][std::uint16_t(getChannelNumberForChannelTypeInCurrentConfiguration(channelType))] = m_directionslessChannelSliders[channelType]->getToggleState();
834 }
835 else
836 {
837 for (auto& ioValKV : m_inputToOutputVals)
838 {
839 ioValKV.second[channelType].first = m_directionslessChannelSliders[channelType]->getToggleState();
840 auto outputChannel = getChannelNumberForChannelTypeInCurrentConfiguration(channelType);
841 if (outputChannel <= m_currentOutputCount)
842 states[ioValKV.first][std::uint16_t(outputChannel)] = m_directionslessChannelSliders[channelType]->getToggleState();
843 }
844 }
845
848 };
849 m_directionslessChannelSliders[channelType]->onValueChange = [this, channelType]() {
850 std::map<std::uint16_t, std::map<std::uint16_t, float >> values;
851 if (0 != m_currentlySelectedInput)
852 {
853 auto value = float(m_directionslessChannelSliders[channelType]->getValue());
854 m_inputToOutputVals[m_currentlySelectedInput][channelType].second = value;
855 values[m_currentlySelectedInput][std::uint16_t(getChannelNumberForChannelTypeInCurrentConfiguration(channelType))] = value;
856 }
857 else
858 {
859 auto latestValue = m_directionslessChannelSliders[channelType]->getValue();
860 auto latestDelta = latestValue - m_directionlessSliderRelRef[channelType];
861 for (auto& ioValKV : m_inputToOutputVals)
862 {
863 ioValKV.second[channelType].second = jlimit(0.0f, 1.0f, ioValKV.second[channelType].second + float(latestDelta));
864 auto outputChannel = getChannelNumberForChannelTypeInCurrentConfiguration(channelType);
865 if (outputChannel <= m_currentOutputCount)
866 values[ioValKV.first][std::uint16_t(outputChannel)] = ioValKV.second[channelType].second;
867 //DBG(juce::String(ioValKV.first) << ">" << juce::AudioChannelSet::getAbbreviatedChannelTypeName(channelType) << " val:" << ioValKV.second[channelType].second);
868 }
869 m_directionlessSliderRelRef[channelType] = latestValue;
870 }
871
874 };
875 addAndMakeVisible(m_directionslessChannelSliders[channelType].get());
876
877 m_directionslessChannelLabels[channelType] = std::make_unique<juce::Label>();
878 m_directionslessChannelLabels[channelType]->setText(juce::AudioChannelSet::getAbbreviatedChannelTypeName(channelType), juce::dontSendNotification);
879 m_directionslessChannelLabels[channelType]->setJustificationType(juce::Justification::centred);
880 addAndMakeVisible(m_directionslessChannelLabels[channelType].get());
881 }
882}
883
884void TwoDFieldMultisliderComponent::configureDirectionlessSliderToRelativeCtrl(const juce::AudioChannelSet::ChannelType& channelType, JUCEAppBasics::ToggleStateSlider& slider)
885{
886 auto anyInputToDirectionLessOff = false;
887 for (auto const& iKV : m_inputToOutputVals)
888 {
889 if (0 < iKV.second.count(channelType) && !iKV.second.at(channelType).first)
890 {
891 anyInputToDirectionLessOff = true;
892 break;
893 }
894 }
895
896 slider.setTitle("");
897 slider.setRange(0.0, 1.0, 0.01);
898 slider.setValue(0.5); // relative starting
899 slider.displayValueConverter = {};
900 slider.setToggleState(!anyInputToDirectionLessOff, juce::dontSendNotification);
901 m_directionlessSliderRelRef[channelType] = 0.5;
902}
903
904
905}
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
Definition Mema.cpp:27