From b5e63d3adfd6dc39b95f66c08b9d9425c951c249 Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 9 Nov 2025 05:28:02 +0000 Subject: [PATCH] Tabbed editor --- Source/UI/CustomPresetWindow.cpp | 287 +++++++++++++++++-------------- 1 file changed, 161 insertions(+), 126 deletions(-) diff --git a/Source/UI/CustomPresetWindow.cpp b/Source/UI/CustomPresetWindow.cpp index 53a4654..1c28628 100644 --- a/Source/UI/CustomPresetWindow.cpp +++ b/Source/UI/CustomPresetWindow.cpp @@ -367,6 +367,138 @@ private: float morphDisplayValue { 0.0f }; }; +class WaveBrowserComponent : public juce::Component +{ +public: + explicit WaveBrowserComponent(DummyWavetableSynthAudioProcessor& processorRef) + : audioProcessor(processorRef) + { + } + + void setOnWaveSelected(std::function handler) { onWaveSelected = std::move(handler); } + + void paint(juce::Graphics& g) override + { + g.fillAll(juce::Colours::black.withAlpha(0.35f)); + g.setColour(juce::Colours::grey); + g.drawRect(getLocalBounds()); + + auto grid = getLocalBounds().reduced(8); + const int cellW = juce::jmax(1, grid.getWidth() / kBrowserColumns); + const int cellH = juce::jmax(1, grid.getHeight() / kBrowserRows); + + browserCells.clear(); + browserCells.reserve(kBrowserColumns * kBrowserRows); + + const int waveCount = audioProcessor.getWaveTableCount(); + + for (int r = 0; r < kBrowserRows; ++r) + for (int c = 0; c < kBrowserColumns; ++c) + { + const int idx = r * kBrowserColumns + c; + auto cell = juce::Rectangle(grid.getX() + c * cellW, + grid.getY() + r * cellH, + cellW, cellH); + browserCells.push_back(cell); + + g.setColour(juce::Colours::darkgrey); + g.drawRect(cell); + + if (idx < waveCount) + { + if (const auto* tbl = audioProcessor.getPreviewTablePtr(idx)) + { + g.setColour(juce::Colours::lime); + juce::Path p; + p.startNewSubPath((float) cell.getX(), (float) cell.getCentreY()); + const int n = (int) tbl->size(); + for (int i = 0; i < n; i += 32) + { + const float x = juce::jmap((float) i, 0.0f, (float) n, + (float) cell.getX(), (float) cell.getRight()); + const float y = juce::jmap((*tbl)[(size_t) i], -1.0f, 1.0f, + (float) cell.getBottom(), (float) cell.getY()); + p.lineTo(x, y); + } + g.strokePath(p, juce::PathStrokeType(1.0f)); + } + } + else + { + g.setColour(juce::Colours::darkgrey); + g.drawLine((float) cell.getX(), (float) cell.getY(), (float) cell.getRight(), (float) cell.getBottom(), 0.5f); + g.drawLine((float) cell.getRight(), (float) cell.getY(), (float) cell.getX(), (float) cell.getBottom(), 0.5f); + } + } + } + + void mouseDown(const juce::MouseEvent& e) override + { + const int waveCount = audioProcessor.getWaveTableCount(); + for (size_t i = 0; i < browserCells.size(); ++i) + { + if (browserCells[i].contains(e.getPosition())) + { + if ((int) i >= waveCount) + return; + + if (onWaveSelected != nullptr) + onWaveSelected((int) i); + break; + } + } + } + + void refresh() { repaint(); } + +private: + DummyWavetableSynthAudioProcessor& audioProcessor; + std::vector> browserCells; + std::function onWaveSelected; +}; + +class EditorTabContent : public juce::Component +{ +public: + EditorTabContent(juce::Label& titleLabel, DrawWaveComponent& drawComponent) + : label(titleLabel), draw(drawComponent) + { + addAndMakeVisible(label); + addAndMakeVisible(draw); + } + + void resized() override + { + auto area = getLocalBounds().reduced(12); + auto labelArea = area.removeFromTop(20); + label.setBounds(labelArea); + area.removeFromTop(6); + draw.setBounds(area); + } + +private: + juce::Label& label; + DrawWaveComponent& draw; +}; + +class LibraryTabContent : public juce::Component +{ +public: + explicit LibraryTabContent(WaveBrowserComponent& browserComponent) + : browser(browserComponent) + { + addAndMakeVisible(browser); + } + + void resized() override + { + browser.setBounds(getLocalBounds().reduced(8)); + } + +private: + WaveBrowserComponent& browser; +}; + class ExampleUIPanel : public juce::Component, private juce::Timer { @@ -435,25 +567,31 @@ public: { slotIndices[(size_t) activeSlot] = slotIndex; updateSlotThumbnails(); + waveBrowser.refresh(); repaint(); } }; clearDraw.onClick = [this] { userDraw.clear(); }; presetButton.onClick = [this] { showPresetMenu(); }; - addAndMakeVisible(wavetableTabs); - wavetableTabs.addTab("Editor", juce::Colours::transparentBlack, &editorTab, false); - wavetableTabs.addTab("Library", juce::Colours::transparentBlack, &libraryTab, false); - wavetableTabs.setColour(juce::TabbedComponent::outlineColourId, juce::Colours::darkgrey); - - editorTab.addAndMakeVisible(lblDrawWave); - editorTab.addAndMakeVisible(userDraw); lblDrawWave.setText("DRAW WAVE", juce::dontSendNotification); lblDrawWave.setColour(juce::Label::textColourId, juce::Colours::white); lblDrawWave.setJustificationType(juce::Justification::left); + addAndMakeVisible(wavetableTabs); + wavetableTabs.addTab("Editor", juce::Colours::transparentBlack, &editorTabContent, false); + wavetableTabs.addTab("Library", juce::Colours::transparentBlack, &libraryTabContent, false); + wavetableTabs.setColour(juce::TabbedComponent::outlineColourId, juce::Colours::darkgrey); + + waveBrowser.setOnWaveSelected([this](int index) + { + slotIndices[(size_t) activeSlot] = index; + updateSlotThumbnails(); + repaint(); + }); + for (auto* box : { &slotABox, &slotBBox, &slotCBox }) - libraryTab.addAndMakeVisible(*box); + addAndMakeVisible(*box); slotABox.setName("A"); slotBBox.setName("B"); @@ -524,56 +662,6 @@ public: const int leftWidth = getWidth() / 2 - 20; const int rightX = leftWidth + 30; - auto browser = juce::Rectangle(10, blackTop + 8, leftWidth - 20, blackBottom - blackTop - 16); - g.setColour(juce::Colours::grey); - g.drawRect(browser); - - const int cellW = browser.getWidth() / kBrowserColumns; - const int cellH = browser.getHeight() / kBrowserRows; - browserCells.clear(); - browserCells.reserve(kBrowserColumns * kBrowserRows); - - const int waveCount = audioProcessor.getWaveTableCount(); - - for (int r = 0; r < kBrowserRows; ++r) - for (int c = 0; c < kBrowserColumns; ++c) - { - const int idx = r * kBrowserColumns + c; - auto cell = juce::Rectangle(browser.getX() + c * cellW, - browser.getY() + r * cellH, - cellW, cellH); - browserCells.push_back(cell); - - g.setColour(juce::Colours::darkgrey); - g.drawRect(cell); - - if (idx < waveCount) - { - if (const auto* tbl = audioProcessor.getPreviewTablePtr(idx)) - { - g.setColour(juce::Colours::lime); - juce::Path p; - p.startNewSubPath((float) cell.getX(), (float) cell.getCentreY()); - const int n = (int) tbl->size(); - for (int i = 0; i < n; i += 32) - { - const float x = juce::jmap((float) i, 0.0f, (float) n, - (float) cell.getX(), (float) cell.getRight()); - const float y = juce::jmap((*tbl)[(size_t) i], -1.0f, 1.0f, - (float) cell.getBottom(), (float) cell.getY()); - p.lineTo(x, y); - } - g.strokePath(p, juce::PathStrokeType(1.0f)); - } - } - else - { - g.setColour(juce::Colours::darkgrey); - g.drawLine((float) cell.getX(), (float) cell.getY(), (float) cell.getRight(), (float) cell.getBottom(), 0.5f); - g.drawLine((float) cell.getRight(), (float) cell.getY(), (float) cell.getX(), (float) cell.getBottom(), 0.5f); - } - } - g.setColour(juce::Colours::darkred); g.fillRect(juce::Rectangle(rightX, blackTop + 6, getWidth() - rightX - 30, 6)); g.setColour(juce::Colours::white); @@ -591,7 +679,12 @@ public: const int leftWidth = getWidth() / 2 - 20; const int rightX = leftWidth + 30; - morphSlider.setBounds(rightX, blackTop + 16, getWidth() - rightX - 30, 18); + const int boxH = 28; + slotABox.setBounds(rightX, blackTop + 6, 130, boxH); + slotBBox.setBounds(rightX + 140, blackTop + 6, 130, boxH); + slotCBox.setBounds(rightX + 280, blackTop + 6, 130, boxH); + + morphSlider.setBounds(rightX, blackTop + boxH + 10, getWidth() - rightX - 30, 18); morphLoopToggle.setBounds(rightX, morphSlider.getBottom() + 10, 120, 22); morphLoopMode.setBounds(morphLoopToggle.getRight() + 8, morphSlider.getBottom() + 6, 150, 26); @@ -601,49 +694,14 @@ public: auto tabBounds = juce::Rectangle(rightX, padTop, getWidth() - rightX - 16, blackBottom - padTop - 70); wavetableTabs.setBounds(tabBounds); - auto editorBounds = editorTab.getLocalBounds().reduced(12); - auto labelBounds = editorBounds.removeFromTop(20); - lblDrawWave.setBounds(labelBounds.getX(), labelBounds.getY(), 140, labelBounds.getHeight()); - editorBounds.removeFromTop(6); - userDraw.setBounds(editorBounds); - - auto libraryBounds = libraryTab.getLocalBounds().reduced(12); - const int slotSpacing = 12; - const int slotHeight = 32; - int slotWidth = (libraryBounds.getWidth() - slotSpacing * 2) / 3; - slotWidth = juce::jmax(90, slotWidth); - const bool fitsHorizontally = (slotWidth * 3 + slotSpacing * 2) <= libraryBounds.getWidth(); - - if (fitsHorizontally) - { - const int slotY = libraryBounds.getY(); - int slotX = libraryBounds.getX(); - slotABox.setBounds(slotX, slotY, slotWidth, slotHeight); - slotX += slotWidth + slotSpacing; - slotBBox.setBounds(slotX, slotY, slotWidth, slotHeight); - slotX += slotWidth + slotSpacing; - slotCBox.setBounds(slotX, slotY, slotWidth, slotHeight); - } - else - { - int slotY = libraryBounds.getY(); - const int slotX = libraryBounds.getX(); - slotWidth = libraryBounds.getWidth(); - for (auto* box : { &slotABox, &slotBBox, &slotCBox }) - { - box->setBounds(slotX, slotY, slotWidth, slotHeight); - slotY += slotHeight + slotSpacing; - } - } - - const int buttonRowY = wavetableTabs.getBottom() + 8; - addToBrowser.setBounds(wavetableTabs.getX() + 220, buttonRowY, 150, 28); + const int buttonRowY = tabBounds.getBottom() + 8; + addToBrowser.setBounds(tabBounds.getX() + 220, buttonRowY, 150, 28); clearDraw.setBounds(addToBrowser.getRight() + 12, buttonRowY, 150, 28); const int togglesY = bottom.getY() - 36; reverbOn.setBounds(getWidth() - 280, togglesY, 120, 24); osc2Mute .setBounds(getWidth() - 140, togglesY, 140, 24); - chorusOn.setBounds(wavetableTabs.getX(), buttonRowY, 90, 20); + chorusOn.setBounds(tabBounds.getX(), buttonRowY, 90, 20); const int masterW = 82; lblMaster.setBounds(getWidth() - masterW - 150, top.getY() + 4, 80, 16); @@ -688,11 +746,6 @@ public: place (10,1,lblRvWet, rvWet, "Rev Wet"); } - void mouseDown(const juce::MouseEvent& e) override - { - handleBrowserClick(e.getPosition()); - } - private: juce::Rectangle getTopPanelBounds() const { return { 0, 0, getWidth(), 80 }; } juce::Rectangle getBottomPanelBounds() const @@ -718,24 +771,6 @@ private: label.setColour(juce::Label::textColourId, juce::Colours::black); } - void handleBrowserClick(juce::Point pos) - { - const int waveCount = audioProcessor.getWaveTableCount(); - for (size_t i = 0; i < browserCells.size(); ++i) - { - if (browserCells[i].contains(pos)) - { - if ((int) i >= waveCount) - return; - - slotIndices[(size_t) activeSlot] = (int) i; - updateSlotThumbnails(); - repaint(); - break; - } - } - } - void setActiveSlot(int slot) { activeSlot = juce::jlimit(0, 2, slot); @@ -861,20 +896,20 @@ private: juce::ToggleButton chorusOn, reverbOn, osc2Mute; - juce::TabbedComponent wavetableTabs { juce::TabbedButtonBar::TabsAtTop }; - juce::Component editorTab, libraryTab; - WaveThumbnail slotABox, slotBBox, slotCBox; DrawWaveComponent userDraw; - juce::TextButton addToBrowser, clearDraw, presetButton; juce::Label lblDrawWave; + WaveBrowserComponent waveBrowser { audioProcessor }; + EditorTabContent editorTabContent { lblDrawWave, userDraw }; + LibraryTabContent libraryTabContent { waveBrowser }; + juce::TabbedComponent wavetableTabs { juce::TabbedButtonBar::TabsAtTop }; + + juce::TextButton addToBrowser, clearDraw, presetButton; juce::Label lblCutoff, lblAttack, lblDecay, lblSustain, lblRelease; juce::Label lblLfoRate, lblLfoDepth, lblFenvA, lblFenvD, lblFenvS, lblFenvR, lblFenvAmt; juce::Label lblChRate, lblChDepth, lblChDelay, lblChFb, lblChMix; juce::Label lblRvRoom, lblRvDamp, lblRvWidth, lblRvWet; - - std::vector> browserCells; std::array slotIndices { 0, 1, 2 }; int activeSlot { 0 }; juce::String selectedPresetLabel { "Presets" };