From f6d686804616a3b3f0c9ea046fec6d6d088e3c94 Mon Sep 17 00:00:00 2001 From: James H Ball Date: Fri, 22 Aug 2025 11:37:33 +0100 Subject: [PATCH] Redesign and remove MainComponent to put the file controls as a separate bar at the top --- .gitmodules | 4 + Resources/svg/plus.svg | 1 + Source/CommonPluginEditor.h | 8 + Source/EffectsComponent.cpp | 16 +- Source/EffectsComponent.h | 4 +- Source/MainComponent.cpp | 259 ------------------ Source/MainComponent.h | 44 --- Source/SettingsComponent.cpp | 94 ++++++- Source/SettingsComponent.h | 13 +- .../components/ExampleFilesGridComponent.cpp | 109 +++++++- Source/components/ExampleFilesGridComponent.h | 22 +- Source/components/FileControlsComponent.cpp | 136 +++++++++ Source/components/FileControlsComponent.h | 34 +++ Source/components/GridComponent.cpp | 103 +++++-- Source/components/GridComponent.h | 18 +- Source/components/ScrollFadeMixin.h | 196 ------------- Source/components/ScrollFadeOverlay.h | 97 +++++++ Source/components/ScrollFadeViewport.h | 91 ++++++ Source/components/VListBox.cpp | 18 +- modules/melatonin_inspector | 1 + osci-render.jucer | 18 +- 21 files changed, 699 insertions(+), 587 deletions(-) create mode 100644 Resources/svg/plus.svg delete mode 100644 Source/MainComponent.cpp delete mode 100644 Source/MainComponent.h create mode 100644 Source/components/FileControlsComponent.cpp create mode 100644 Source/components/FileControlsComponent.h delete mode 100644 Source/components/ScrollFadeMixin.h create mode 100644 Source/components/ScrollFadeOverlay.h create mode 100644 Source/components/ScrollFadeViewport.h create mode 160000 modules/melatonin_inspector diff --git a/.gitmodules b/.gitmodules index 5a5ceefe..635e41f4 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,7 @@ [submodule "Source/lua/lua"] path = Source/lua/lua url = ../../lua/lua.git +[submodule "modules/melatonin_inspector"] + path = modules/melatonin_inspector + url = https://github.com/sudara/melatonin_inspector.git + branch = main diff --git a/Resources/svg/plus.svg b/Resources/svg/plus.svg new file mode 100644 index 00000000..bb280a85 --- /dev/null +++ b/Resources/svg/plus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Source/CommonPluginEditor.h b/Source/CommonPluginEditor.h index d973ffde..0c7d61f0 100644 --- a/Source/CommonPluginEditor.h +++ b/Source/CommonPluginEditor.h @@ -10,6 +10,10 @@ #include "components/VolumeComponent.h" #include "components/DownloaderComponent.h" +#if DEBUG + #include "melatonin_inspector/melatonin_inspector.h" +#endif + class CommonPluginEditor : public juce::AudioProcessorEditor { public: CommonPluginEditor(CommonAudioProcessor&, juce::String appName, juce::String projectFileType, int width, int height); @@ -78,6 +82,10 @@ public: juce::OpenGLContext openGlContext; #endif +#if DEBUG + melatonin::Inspector inspector { *this, false }; +#endif + bool keyPressed(const juce::KeyPress& key) override; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(CommonPluginEditor) diff --git a/Source/EffectsComponent.cpp b/Source/EffectsComponent.cpp index 0ae8fe45..8c59ae1d 100644 --- a/Source/EffectsComponent.cpp +++ b/Source/EffectsComponent.cpp @@ -95,9 +95,6 @@ EffectsComponent::EffectsComponent(OscirenderAudioProcessor& p, OscirenderAudioP spacer->setSize(1, LIST_SPACER); // top padding listBox.setHeaderComponent(std::move(spacer)); } - // Setup scroll fade mixin - initScrollFade(*this); - attachToListBox(listBox); // Wire "+ Add new effect" button below the list addEffectButton.onClick = [this]() { if (itemData.onAddNewEffectRequested) itemData.onAddNewEffectRequested(); @@ -112,6 +109,7 @@ EffectsComponent::EffectsComponent(OscirenderAudioProcessor& p, OscirenderAudioP } else { grid.setVisible(false); listBox.setVisible(true); + listBox.updateContent(); } } @@ -132,17 +130,18 @@ void EffectsComponent::resized() { area.removeFromTop(6); if (showingGrid) { grid.setBounds(area); + grid.setVisible(true); addEffectButton.setVisible(false); - // Hide fade when grid is shown - setScrollFadeVisible(false); + listBox.setVisible(false); } else { // Reserve space at bottom for the add button auto addBtnHeight = 44; auto listArea = area; auto buttonArea = listArea.removeFromBottom(addBtnHeight); listBox.setBounds(listArea); - // Layout bottom fade overlay; visible if list is scrollable - layoutScrollFade(listArea.withTrimmedTop(LIST_SPACER), true, 48); + listBox.setVisible(true); + grid.setVisible(false); + listBox.updateContent(); addEffectButton.setVisible(true); addEffectButton.setBounds(buttonArea.reduced(0, 4)); } @@ -151,7 +150,4 @@ void EffectsComponent::resized() { void EffectsComponent::changeListenerCallback(juce::ChangeBroadcaster* source) { itemData.resetData(); listBox.updateContent(); - // Re-layout scroll fades after content changes - if (! showingGrid) - layoutScrollFade(listBox.getBounds().withTrimmedTop(LIST_SPACER), true, 48); } diff --git a/Source/EffectsComponent.h b/Source/EffectsComponent.h index 18d7cd10..6551bd4f 100644 --- a/Source/EffectsComponent.h +++ b/Source/EffectsComponent.h @@ -6,11 +6,11 @@ #include "PluginProcessor.h" #include "components/DraggableListBox.h" #include "components/EffectsListComponent.h" -#include "components/ScrollFadeMixin.h" +#include "components/ScrollFadeViewport.h" #include "components/EffectTypeGridComponent.h" class OscirenderAudioProcessorEditor; -class EffectsComponent : public juce::GroupComponent, public juce::ChangeListener, private ScrollFadeMixin { +class EffectsComponent : public juce::GroupComponent, public juce::ChangeListener { public: EffectsComponent(OscirenderAudioProcessor&, OscirenderAudioProcessorEditor&); ~EffectsComponent() override; diff --git a/Source/MainComponent.cpp b/Source/MainComponent.cpp deleted file mode 100644 index 8eda35d7..00000000 --- a/Source/MainComponent.cpp +++ /dev/null @@ -1,259 +0,0 @@ -#include "MainComponent.h" - -#include "PluginEditor.h" -#include "parser/FileParser.h" -#include "parser/FrameProducer.h" - -MainComponent::MainComponent(OscirenderAudioProcessor& p, OscirenderAudioProcessorEditor& editor) : audioProcessor(p), pluginEditor(editor) { - setText("Main Settings"); - - addAndMakeVisible(editor.volume); - - addAndMakeVisible(fileButton); - fileButton.setButtonText("Choose File(s)"); - - // Show Examples panel - addAndMakeVisible(showExamplesButton); - showExamplesButton.onClick = [this] { - pluginEditor.settings.showExamples(true); - }; - - fileButton.onClick = [this] { - juce::String fileFormats; - for (auto& ext : audioProcessor.FILE_EXTENSIONS) { - fileFormats += "*." + ext + ";"; - } - chooser = std::make_unique("Open", audioProcessor.getLastOpenedDirectory(), fileFormats); - auto flags = juce::FileBrowserComponent::openMode | juce::FileBrowserComponent::canSelectMultipleItems | - juce::FileBrowserComponent::canSelectFiles; - - chooser->launchAsync(flags, [this](const juce::FileChooser& chooser) { - juce::SpinLock::ScopedLockType parsersLock(audioProcessor.parsersLock); - bool fileAdded = false; - for (auto& file : chooser.getResults()) { - if (file != juce::File()) { - audioProcessor.setLastOpenedDirectory(file.getParentDirectory()); - audioProcessor.addFile(file); - pluginEditor.addCodeEditor(audioProcessor.getCurrentFileIndex()); - fileAdded = true; - } - } - - if (fileAdded) { - pluginEditor.fileUpdated(audioProcessor.getCurrentFileName()); - } - }); - }; - - addAndMakeVisible(closeFileButton); - - closeFileButton.onClick = [this] { - juce::SpinLock::ScopedLockType lock(audioProcessor.parsersLock); - int index = audioProcessor.getCurrentFileIndex(); - if (index == -1) { - return; - } - audioProcessor.removeFile(audioProcessor.getCurrentFileIndex()); - }; - - closeFileButton.setTooltip("Close the currently open file."); - - addAndMakeVisible(inputEnabled); - inputEnabled.onClick = [this] { - audioProcessor.inputEnabled->setBoolValueNotifyingHost(!audioProcessor.inputEnabled->getBoolValue()); - }; - - addAndMakeVisible(fileLabel); - fileLabel.setJustificationType(juce::Justification::centred); - updateFileLabel(); - - addAndMakeVisible(leftArrow); - leftArrow.onClick = [this] { - juce::SpinLock::ScopedLockType parserLock(audioProcessor.parsersLock); - juce::SpinLock::ScopedLockType effectsLock(audioProcessor.effectsLock); - - int index = audioProcessor.getCurrentFileIndex(); - - if (index > 0) { - audioProcessor.changeCurrentFile(index - 1); - pluginEditor.fileUpdated(audioProcessor.getCurrentFileName()); - } - }; - leftArrow.setTooltip("Change to previous file (k)."); - - addAndMakeVisible(rightArrow); - rightArrow.onClick = [this] { - juce::SpinLock::ScopedLockType parserLock(audioProcessor.parsersLock); - juce::SpinLock::ScopedLockType effectsLock(audioProcessor.effectsLock); - - int index = audioProcessor.getCurrentFileIndex(); - - if (index < audioProcessor.numFiles() - 1) { - audioProcessor.changeCurrentFile(index + 1); - pluginEditor.fileUpdated(audioProcessor.getCurrentFileName()); - } - }; - rightArrow.setTooltip("Change to next file (j)."); - - addAndMakeVisible(fileName); - fileType.addItem(".lua", 1); - fileType.addItem(".svg", 2); - fileType.addItem(".obj", 3); - fileType.addItem(".txt", 4); - fileType.setSelectedId(1); - addAndMakeVisible(fileType); - addAndMakeVisible(createFile); - - createFile.onClick = [this] { - juce::SpinLock::ScopedLockType parsersLock(audioProcessor.parsersLock); - auto fileNameText = fileName.getText(); - auto fileTypeText = fileType.getText(); - auto fileName = fileNameText + fileTypeText; - if (fileTypeText == ".lua") { - audioProcessor.addFile(fileNameText + fileTypeText, BinaryData::demo_lua, BinaryData::demo_luaSize); - } else if (fileTypeText == ".svg") { - audioProcessor.addFile(fileNameText + fileTypeText, BinaryData::demo_svg, BinaryData::demo_svgSize); - } else if (fileTypeText == ".obj") { - audioProcessor.addFile(fileNameText + fileTypeText, BinaryData::cube_obj, BinaryData::cube_objSize); - } else if (fileTypeText == ".txt") { - audioProcessor.addFile(fileNameText + fileTypeText, BinaryData::helloworld_txt, BinaryData::helloworld_txtSize); - } else { - return; - } - - pluginEditor.addCodeEditor(audioProcessor.getCurrentFileIndex()); - pluginEditor.fileUpdated(fileName, fileTypeText == ".lua" || fileTypeText == ".txt"); - }; - - fileName.setFont(juce::Font(16.0f, juce::Font::plain)); - fileName.setText("filename"); - - fileName.onReturnKey = [this] { - createFile.triggerClick(); - }; - - osci::BooleanParameter* visualiserFullScreen = audioProcessor.visualiserParameters.visualiserFullScreen; - pluginEditor.visualiser.setFullScreen(visualiserFullScreen->getBoolValue()); - - addAndMakeVisible(pluginEditor.visualiser); - pluginEditor.visualiser.setFullScreenCallback([this, visualiserFullScreen](FullScreenMode mode) { - if (mode == FullScreenMode::TOGGLE) { - visualiserFullScreen->setBoolValueNotifyingHost(!visualiserFullScreen->getBoolValue()); - } else if (mode == FullScreenMode::FULL_SCREEN) { - visualiserFullScreen->setBoolValueNotifyingHost(true); - } else if (mode == FullScreenMode::MAIN_COMPONENT) { - visualiserFullScreen->setBoolValueNotifyingHost(false); - } - - pluginEditor.visualiser.setFullScreen(visualiserFullScreen->getBoolValue()); - - pluginEditor.resized(); - pluginEditor.repaint(); - resized(); - repaint(); - }); - - visualiserFullScreen->addListener(this); -} - -MainComponent::~MainComponent() { - audioProcessor.visualiserParameters.visualiserFullScreen->removeListener(this); -} - -// syphonLock must be held when calling this function -void MainComponent::updateFileLabel() { - showLeftArrow = audioProcessor.getCurrentFileIndex() > 0; - showRightArrow = audioProcessor.getCurrentFileIndex() < audioProcessor.numFiles() - 1; - - { -#if (JUCE_MAC || JUCE_WINDOWS) && OSCI_PREMIUM - if (audioProcessor.syphonInputActive) { - fileLabel.setText(pluginEditor.getSyphonSourceName(), juce::dontSendNotification); - } else -#endif - if (audioProcessor.objectServerRendering) { - fileLabel.setText("Rendering from Blender", juce::dontSendNotification); - } else if (audioProcessor.getCurrentFileIndex() == -1) { - fileLabel.setText("No file open", juce::dontSendNotification); - } else { - fileLabel.setText(audioProcessor.getCurrentFileName(), juce::dontSendNotification); - } - } - - resized(); -} - -void MainComponent::parameterValueChanged(int parameterIndex, float newValue) { - juce::MessageManager::callAsync([this] { - pluginEditor.resized(); - pluginEditor.repaint(); - resized(); - repaint(); - }); -} - -void MainComponent::parameterGestureChanged(int parameterIndex, bool gestureIsStarting) {} - -void MainComponent::resized() { - juce::Rectangle bounds = getLocalBounds().withTrimmedTop(20).reduced(20); - auto buttonWidth = 120; - auto buttonHeight = 30; - auto padding = 10; - auto rowPadding = 10; - - auto row = bounds.removeFromTop(buttonHeight); - fileButton.setBounds(row.removeFromLeft(buttonWidth)); - row.removeFromLeft(rowPadding); - showExamplesButton.setBounds(row.removeFromLeft(buttonWidth)); - row.removeFromLeft(rowPadding); - inputEnabled.setBounds(row.removeFromLeft(20)); - row.removeFromLeft(rowPadding); - if (audioProcessor.getCurrentFileIndex() != -1) { - closeFileButton.setBounds(row.removeFromRight(20)); - row.removeFromRight(rowPadding); - } else { - closeFileButton.setBounds(juce::Rectangle()); - } - - auto arrowLeftBounds = row.removeFromLeft(15); - if (showLeftArrow) { - leftArrow.setBounds(arrowLeftBounds); - } else { - leftArrow.setBounds(0, 0, 0, 0); - } - row.removeFromLeft(rowPadding); - - auto arrowRightBounds = row.removeFromRight(15); - if (showRightArrow) { - rightArrow.setBounds(arrowRightBounds); - } else { - rightArrow.setBounds(0, 0, 0, 0); - } - row.removeFromRight(rowPadding); - - fileLabel.setBounds(row); - - bounds.removeFromTop(padding); - row = bounds.removeFromTop(buttonHeight); - fileName.setBounds(row.removeFromLeft(buttonWidth)); - row.removeFromLeft(rowPadding); - fileType.setBounds(row.removeFromLeft(buttonWidth / 2)); - row.removeFromLeft(rowPadding); - createFile.setBounds(row.removeFromLeft(buttonWidth)); - - bounds.removeFromTop(padding); - bounds.expand(15, 0); - - auto volumeArea = bounds.removeFromLeft(30); - pluginEditor.volume.setBounds(volumeArea.withSizeKeepingCentre(volumeArea.getWidth(), juce::jmin(volumeArea.getHeight(), 300))); - - if (!audioProcessor.visualiserParameters.visualiserFullScreen->getBoolValue()) { - auto minDim = juce::jmin(bounds.getWidth(), bounds.getHeight()); - juce::Point localTopLeft = {bounds.getX(), bounds.getY()}; - juce::Point topLeft = pluginEditor.getLocalPoint(this, localTopLeft); - auto shiftedBounds = bounds; - shiftedBounds.setX(topLeft.getX()); - shiftedBounds.setY(topLeft.getY()); - pluginEditor.visualiser.setBounds(shiftedBounds.withSizeKeepingCentre(minDim, minDim + 25).reduced(10)); - } -} diff --git a/Source/MainComponent.h b/Source/MainComponent.h deleted file mode 100644 index 22a7c491..00000000 --- a/Source/MainComponent.h +++ /dev/null @@ -1,44 +0,0 @@ -#pragma once - -#include -#include "PluginProcessor.h" -#include "parser/FileParser.h" -#include "parser/FrameProducer.h" -#include "visualiser/VisualiserComponent.h" -#include "UGen/ugen_JuceEnvelopeComponent.h" -#include "components/SvgButton.h" - -class OscirenderAudioProcessorEditor; -class MainComponent : public juce::GroupComponent, public juce::AudioProcessorParameter::Listener { -public: - MainComponent(OscirenderAudioProcessor&, OscirenderAudioProcessorEditor&); - ~MainComponent() override; - - void resized() override; - void updateFileLabel(); - void parameterValueChanged(int parameterIndex, float newValue) override; - void parameterGestureChanged(int parameterIndex, bool gestureIsStarting) override; - -private: - OscirenderAudioProcessor& audioProcessor; - OscirenderAudioProcessorEditor& pluginEditor; - - bool isBinaryFile(juce::String name); - - std::unique_ptr chooser; - juce::TextButton fileButton; - SvgButton closeFileButton{"closeFile", juce::String(BinaryData::delete_svg), juce::Colours::red}; - SvgButton inputEnabled{"inputEnabled", juce::String(BinaryData::microphone_svg), juce::Colours::white, juce::Colours::red, audioProcessor.inputEnabled}; - juce::Label fileLabel; - SvgButton leftArrow{"leftArrow", juce::String(BinaryData::left_arrow_svg), juce::Colours::white}; - SvgButton rightArrow{"rightArrow", juce::String(BinaryData::right_arrow_svg), juce::Colours::white}; - bool showLeftArrow = false; - bool showRightArrow = false; - - juce::TextEditor fileName; - juce::ComboBox fileType; - juce::TextButton createFile{"Create File"}; - juce::TextButton showExamplesButton{"Examples"}; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MainComponent) -}; diff --git a/Source/SettingsComponent.cpp b/Source/SettingsComponent.cpp index e31bb8c8..58baacdf 100644 --- a/Source/SettingsComponent.cpp +++ b/Source/SettingsComponent.cpp @@ -4,7 +4,7 @@ SettingsComponent::SettingsComponent(OscirenderAudioProcessor& p, OscirenderAudioProcessorEditor& editor) : audioProcessor(p), pluginEditor(editor) { addAndMakeVisible(effects); - addAndMakeVisible(main); + addAndMakeVisible(fileControls); addAndMakeVisible(perspective); addAndMakeVisible(midiResizerBar); addAndMakeVisible(mainResizerBar); @@ -31,9 +31,51 @@ SettingsComponent::SettingsComponent(OscirenderAudioProcessor& p, OscirenderAudi mainLayout.setItemLayout(0, -0.1, -0.9, mainLayoutPreferredSize); mainLayout.setItemLayout(1, pluginEditor.RESIZER_BAR_SIZE, pluginEditor.RESIZER_BAR_SIZE, pluginEditor.RESIZER_BAR_SIZE); mainLayout.setItemLayout(2, -0.1, -0.9, -(1.0 + mainLayoutPreferredSize)); + + addAndMakeVisible(editor.volume); + + osci::BooleanParameter* visualiserFullScreen = audioProcessor.visualiserParameters.visualiserFullScreen; + pluginEditor.visualiser.setFullScreen(visualiserFullScreen->getBoolValue()); + + addAndMakeVisible(pluginEditor.visualiser); + pluginEditor.visualiser.setFullScreenCallback([this, visualiserFullScreen](FullScreenMode mode) { + if (mode == FullScreenMode::TOGGLE) { + visualiserFullScreen->setBoolValueNotifyingHost(!visualiserFullScreen->getBoolValue()); + } else if (mode == FullScreenMode::FULL_SCREEN) { + visualiserFullScreen->setBoolValueNotifyingHost(true); + } else if (mode == FullScreenMode::MAIN_COMPONENT) { + visualiserFullScreen->setBoolValueNotifyingHost(false); + } + + pluginEditor.visualiser.setFullScreen(visualiserFullScreen->getBoolValue()); + + pluginEditor.resized(); + pluginEditor.repaint(); + resized(); + repaint(); + }); + + visualiserFullScreen->addListener(this); } +SettingsComponent::~SettingsComponent() { + audioProcessor.visualiserParameters.visualiserFullScreen->removeListener(this); +} + +void SettingsComponent::parameterValueChanged(int parameterIndex, float newValue) { + juce::MessageManager::callAsync([this] { + pluginEditor.resized(); + pluginEditor.repaint(); + resized(); + repaint(); + }); +} + +void SettingsComponent::parameterGestureChanged(int parameterIndex, bool gestureIsStarting) {} + void SettingsComponent::resized() { + auto padding = 7; + auto area = getLocalBounds(); area.removeFromLeft(5); area.removeFromRight(5); @@ -55,21 +97,41 @@ void SettingsComponent::resized() { mainLayout.layOutComponents(columns, 3, dummy.getX(), dummy.getY(), dummy.getWidth(), dummy.getHeight(), false, true); auto bounds = dummy2.getBounds(); - main.setBounds(bounds); + auto row = bounds.removeFromTop(30); + fileControls.setBounds(row.removeFromLeft(bounds.getWidth())); + bounds.removeFromTop(padding); - juce::Component* effectSettings = nullptr; + volumeVisualiserBounds = bounds; + bounds.reduce(5, 5); - if (txt.isVisible()) { - effectSettings = &txt; - } else if (frame.isVisible()) { - effectSettings = &frame; + auto volumeArea = bounds.removeFromLeft(30); + pluginEditor.volume.setBounds(volumeArea.withSizeKeepingCentre(volumeArea.getWidth(), juce::jmin(volumeArea.getHeight(), 300))); + + if (!audioProcessor.visualiserParameters.visualiserFullScreen->getBoolValue()) { + auto minDim = juce::jmin(bounds.getWidth(), bounds.getHeight()); + juce::Point localTopLeft = {bounds.getX(), bounds.getY()}; + juce::Point topLeft = pluginEditor.getLocalPoint(this, localTopLeft); + auto shiftedBounds = bounds; + shiftedBounds.setX(topLeft.getX()); + shiftedBounds.setY(topLeft.getY()); + pluginEditor.visualiser.setBounds(shiftedBounds); } + juce::Component* effectSettings = nullptr; auto dummyBounds = dummy.getBounds(); - if (effectSettings != nullptr) { - effectSettings->setBounds(dummyBounds.removeFromBottom(160)); - dummyBounds.removeFromBottom(pluginEditor.RESIZER_BAR_SIZE); + // Only reserve space for effect settings panel when not showing the Open Files panel + if (!examplesVisible) { + if (txt.isVisible()) { + effectSettings = &txt; + } else if (frame.isVisible()) { + effectSettings = &frame; + } + + if (effectSettings != nullptr) { + effectSettings->setBounds(dummyBounds.removeFromBottom(160)); + dummyBounds.removeFromBottom(pluginEditor.RESIZER_BAR_SIZE); + } } if (examplesVisible) { @@ -97,6 +159,11 @@ void SettingsComponent::resized() { repaint(); } +void SettingsComponent::paint(juce::Graphics& g) { + g.setColour(juce::Colours::black); + g.fillRoundedRectangle(volumeVisualiserBounds.toFloat(), OscirenderLookAndFeel::RECT_RADIUS); +} + // syphonLock must be held when calling this function void SettingsComponent::fileUpdated(juce::String fileName) { juce::String extension = fileName.fromLastOccurrenceOf(".", true, false).toLowerCase(); @@ -130,7 +197,7 @@ void SettingsComponent::fileUpdated(juce::String fileName) { frame.setImage(isImage); frame.resized(); } - main.updateFileLabel(); + fileControls.updateFileLabel(); resized(); } @@ -152,6 +219,11 @@ void SettingsComponent::mouseMove(const juce::MouseEvent& event) { void SettingsComponent::showExamples(bool shouldShow) { examplesVisible = shouldShow; resized(); + if (examplesVisible) { + // Force layout so the ExampleFilesGridComponent sizes its viewport/content right away + examples.resized(); + examples.repaint(); + } } void SettingsComponent::mouseDown(const juce::MouseEvent& event) { diff --git a/Source/SettingsComponent.h b/Source/SettingsComponent.h index 2136cb24..1944dc5b 100644 --- a/Source/SettingsComponent.h +++ b/Source/SettingsComponent.h @@ -2,22 +2,27 @@ #include +#include "LookAndFeel.h" #include "EffectsComponent.h" #include "FrameSettingsComponent.h" #include "LuaComponent.h" -#include "MainComponent.h" #include "MidiComponent.h" #include "PerspectiveComponent.h" #include "PluginProcessor.h" #include "TxtComponent.h" #include "components/ExampleFilesGridComponent.h" +#include "components/FileControlsComponent.h" class OscirenderAudioProcessorEditor; -class SettingsComponent : public juce::Component { +class SettingsComponent : public juce::Component, public juce::AudioProcessorParameter::Listener { public: SettingsComponent(OscirenderAudioProcessor&, OscirenderAudioProcessorEditor&); + ~SettingsComponent() override; void resized() override; + void paint(juce::Graphics& g) override; + void parameterValueChanged(int parameterIndex, float newValue) override; + void parameterGestureChanged(int parameterIndex, bool gestureIsStarting) override; void fileUpdated(juce::String fileName); void update(); void mouseMove(const juce::MouseEvent& event) override; @@ -29,7 +34,7 @@ private: OscirenderAudioProcessor& audioProcessor; OscirenderAudioProcessorEditor& pluginEditor; - MainComponent main{audioProcessor, pluginEditor}; + FileControlsComponent fileControls{audioProcessor, pluginEditor}; PerspectiveComponent perspective{audioProcessor, pluginEditor}; TxtComponent txt{audioProcessor, pluginEditor}; FrameSettingsComponent frame{audioProcessor, pluginEditor}; @@ -48,5 +53,7 @@ private: juce::StretchableLayoutManager* toggleLayouts[1] = {&midiLayout}; double prefSizes[1] = {300}; + juce::Rectangle volumeVisualiserBounds; + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SettingsComponent) }; diff --git a/Source/components/ExampleFilesGridComponent.cpp b/Source/components/ExampleFilesGridComponent.cpp index 577c0923..1295bf14 100644 --- a/Source/components/ExampleFilesGridComponent.cpp +++ b/Source/components/ExampleFilesGridComponent.cpp @@ -5,17 +5,30 @@ ExampleFilesGridComponent::ExampleFilesGridComponent(OscirenderAudioProcessor& processor) : audioProcessor(processor) { - // Top bar - addAndMakeVisible(title); + // Group styling + addAndMakeVisible(group); + group.setText("Open Files"); + + // Outer viewport for all content + addAndMakeVisible(viewport); + viewport.setViewedComponent(&content, false); + viewport.setScrollBarsShown(true, false); + + // Close button in header addAndMakeVisible(closeButton); - styleHeading(title); closeButton.onClick = [this]() { if (onClosed) onClosed(); }; - // Add categories to component + // Choose files button in header + addAndMakeVisible(chooseFilesButton); + chooseFilesButton.onClick = [this]() { openFileChooser(); }; + + // Add categories to content; configure grids (no internal viewport, no centering) auto addCat = [this](CategoryViews& cat) { styleHeading(cat.heading); - addAndMakeVisible(cat.heading); - addAndMakeVisible(cat.grid); + content.addAndMakeVisible(cat.heading); + content.addAndMakeVisible(cat.grid); + cat.grid.setUseViewport(false); + cat.grid.setUseCenteringPlaceholders(false); }; addCat(audioCat); addCat(textCat); @@ -36,22 +49,56 @@ void ExampleFilesGridComponent::styleHeading(juce::Label& l) void ExampleFilesGridComponent::paint(juce::Graphics& g) { - // transparent background + // transparent background; group draws its own background } void ExampleFilesGridComponent::resized() { + // Fill entire area without margin auto bounds = getLocalBounds(); - auto top = bounds.removeFromTop(30); - title.setBounds(top.removeFromLeft(200).reduced(4)); - closeButton.setBounds(top.removeFromRight(80).reduced(4)); + + // Layout group to fill + group.setBounds(bounds); + + // Compute header height based on group font + padding. GroupComponent typically draws a label at top ~20px. + const int headerH = 32; // reserve space for group heading text strip (avoid overlap) + + // Position header controls within group header area + { + auto headerBounds = group.getBounds().removeFromTop(headerH); + auto closeSize = 18; + auto closeArea = headerBounds.removeFromRight(closeSize + 8).withSizeKeepingCentre(closeSize, closeSize); + closeButton.setBounds(closeArea); + + auto buttonW = 140; + auto buttonH = 24; + auto chooseArea = headerBounds.removeFromLeft(buttonW).withSizeKeepingCentre(buttonW, buttonH); + chooseFilesButton.setBounds(chooseArea); + } + + // Inside group, leave room for the group text by padding the viewport area + auto inner = group.getLocalBounds(); + inner.removeFromTop(headerH); // ensure viewport starts below header + // Translate to this component's coordinate space + inner = inner.translated(group.getX(), group.getY()); + + // Layout outer viewport inside group + viewport.setBounds(inner); + viewport.setFadeVisible(true); + // Ensure overlay and layout are up-to-date when shown + viewport.resized(); + + // Lay out content height based on all categories + auto contentArea = viewport.getLocalBounds(); + int y = 0; auto layCat = [&](CategoryViews& cat) { - auto header = bounds.removeFromTop(24); + auto header = juce::Rectangle(contentArea.getX(), contentArea.getY() + y, contentArea.getWidth(), 24); cat.heading.setBounds(header.reduced(2)); - auto h = cat.grid.calculateRequiredHeight(bounds.getWidth()); - cat.grid.setBounds(bounds.removeFromTop(h)); - bounds.removeFromTop(8); // gap + y += 24; + const int gridHeight = cat.grid.calculateRequiredHeight(contentArea.getWidth()); + cat.grid.setBounds(contentArea.getX(), contentArea.getY() + y, contentArea.getWidth(), gridHeight); + y += gridHeight + 8; // gap }; layCat(audioCat); @@ -60,6 +107,8 @@ void ExampleFilesGridComponent::resized() layCat(luaCat); layCat(modelsCat); layCat(svgsCat); + + content.setSize(contentArea.getWidth(), y); } void ExampleFilesGridComponent::addExample(CategoryViews& cat, const juce::String& fileName, const char* data, int size) @@ -72,6 +121,8 @@ void ExampleFilesGridComponent::addExample(CategoryViews& cat, const juce::Strin // Signal to UI layer that a new example was added so it can open editors, etc. const bool openEditor = fileName.endsWithIgnoreCase(".lua") || fileName.endsWithIgnoreCase(".txt"); if (onExampleOpened) onExampleOpened(fileName, openEditor); + // Auto-close after selection + if (onClosed) onClosed(); }; cat.grid.addItem(item); } @@ -106,3 +157,33 @@ void ExampleFilesGridComponent::populate() addExample(svgsCat, "trace.svg", BinaryData::trace_svg, BinaryData::trace_svgSize); addExample(svgsCat, "wobble.svg", BinaryData::wobble_svg, BinaryData::wobble_svgSize); } + +void ExampleFilesGridComponent::openFileChooser() +{ + juce::String fileFormats; + for (auto& ext : audioProcessor.FILE_EXTENSIONS) { + fileFormats += "*." + ext + ";"; + } + chooser = std::make_unique("Open", audioProcessor.getLastOpenedDirectory(), fileFormats); + auto flags = juce::FileBrowserComponent::openMode | juce::FileBrowserComponent::canSelectMultipleItems | + juce::FileBrowserComponent::canSelectFiles; + + chooser->launchAsync(flags, [this](const juce::FileChooser& chooserRef) { + juce::SpinLock::ScopedLockType parsersLock(audioProcessor.parsersLock); + bool anyAdded = false; + juce::String lastName; + for (auto& file : chooserRef.getResults()) { + if (file != juce::File()) { + audioProcessor.setLastOpenedDirectory(file.getParentDirectory()); + audioProcessor.addFile(file); + anyAdded = true; + lastName = file.getFileName(); + } + } + + if (anyAdded) { + if (onExampleOpened) onExampleOpened(audioProcessor.getCurrentFileName(), shouldOpenEditorFor(lastName)); + if (onClosed) onClosed(); + } + }); +} diff --git a/Source/components/ExampleFilesGridComponent.h b/Source/components/ExampleFilesGridComponent.h index e9421bc0..309bde7b 100644 --- a/Source/components/ExampleFilesGridComponent.h +++ b/Source/components/ExampleFilesGridComponent.h @@ -3,8 +3,10 @@ #include #include "../PluginProcessor.h" #include "GridComponent.h" +#include "SvgButton.h" +#include "ScrollFadeViewport.h" -// A grid-based browser for example files grouped by category +// A grid-based browser for opening files: includes examples by category and a generic file chooser class ExampleFilesGridComponent : public juce::Component { public: @@ -22,9 +24,19 @@ public: private: OscirenderAudioProcessor& audioProcessor; - // Top bar - juce::Label title { {}, "Examples" }; - juce::TextButton closeButton { "Close" }; + // Outer chrome and scrolling + juce::GroupComponent group { {}, "Open Files" }; + ScrollFadeViewport viewport; // Outer scroll container for entire examples panel + juce::Component content; // Holds all headings + category grids + + // Close icon overlayed in the group header + SvgButton closeButton { "closeExamples", + juce::String::createStringFromData(BinaryData::close_svg, BinaryData::close_svgSize), + juce::Colours::white, juce::Colours::white }; + + // Choose files button (moved from MainComponent) + juce::TextButton chooseFilesButton { "Choose File(s)" }; + std::unique_ptr chooser; // Categories struct CategoryViews { @@ -43,6 +55,8 @@ private: void addExample(CategoryViews& cat, const juce::String& fileName, const char* data, int size); void populate(); void styleHeading(juce::Label& l); + void openFileChooser(); + static bool shouldOpenEditorFor(const juce::String& fileName) { return fileName.endsWithIgnoreCase(".lua") || fileName.endsWithIgnoreCase(".txt"); } JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ExampleFilesGridComponent) }; diff --git a/Source/components/FileControlsComponent.cpp b/Source/components/FileControlsComponent.cpp new file mode 100644 index 00000000..98f39d91 --- /dev/null +++ b/Source/components/FileControlsComponent.cpp @@ -0,0 +1,136 @@ +#include "FileControlsComponent.h" +#include "../PluginEditor.h" + +FileControlsComponent::FileControlsComponent(OscirenderAudioProcessor& p, OscirenderAudioProcessorEditor& editor) + : audioProcessor(p), pluginEditor(editor) +{ + // Open Files panel button + addAndMakeVisible(openPanelButton); + openPanelButton.setTooltip("Open files and examples"); + openPanelButton.onClick = [this] { + pluginEditor.settings.showExamples(true); + }; + + // File navigation + addAndMakeVisible(leftArrow); + leftArrow.setTooltip("Change to previous file (k)."); + leftArrow.onClick = [this] { + juce::SpinLock::ScopedLockType parserLock(audioProcessor.parsersLock); + juce::SpinLock::ScopedLockType effectsLock(audioProcessor.effectsLock); + int index = audioProcessor.getCurrentFileIndex(); + if (index > 0) { + audioProcessor.changeCurrentFile(index - 1); + pluginEditor.fileUpdated(audioProcessor.getCurrentFileName()); + } + }; + + addAndMakeVisible(rightArrow); + rightArrow.setTooltip("Change to next file (j)."); + rightArrow.onClick = [this] { + juce::SpinLock::ScopedLockType parserLock(audioProcessor.parsersLock); + juce::SpinLock::ScopedLockType effectsLock(audioProcessor.effectsLock); + int index = audioProcessor.getCurrentFileIndex(); + if (index < audioProcessor.numFiles() - 1) { + audioProcessor.changeCurrentFile(index + 1); + pluginEditor.fileUpdated(audioProcessor.getCurrentFileName()); + } + }; + + // Close current file + addAndMakeVisible(closeFileButton); + closeFileButton.setTooltip("Close the currently open file."); + closeFileButton.onClick = [this] { + juce::SpinLock::ScopedLockType lock(audioProcessor.parsersLock); + int index = audioProcessor.getCurrentFileIndex(); + if (index == -1) return; + audioProcessor.removeFile(audioProcessor.getCurrentFileIndex()); + updateFileLabel(); + }; + + // microphone icon + addAndMakeVisible(inputEnabled); + inputEnabled.onClick = [this] { + audioProcessor.inputEnabled->setBoolValueNotifyingHost(!audioProcessor.inputEnabled->getBoolValue()); + updateFileLabel(); + }; + + // Current file label + addAndMakeVisible(fileLabel); + fileLabel.setJustificationType(juce::Justification::centred); + updateFileLabel(); +} + +void FileControlsComponent::paint(juce::Graphics& g) +{ + // Rounded veryDark background + auto b = getLocalBounds().toFloat(); + auto bg = Colours::veryDark; + g.setColour(bg); + g.fillRoundedRectangle(b, OscirenderLookAndFeel::RECT_RADIUS); +} + +void FileControlsComponent::resized() +{ + auto bounds = getLocalBounds().reduced(8, 2); + const int h = bounds.getHeight(); + const int icon = juce::jmin(h, 22); + const int gap = 8; + + // Layout: [Mic] [<] [Label expands] [>] [Close] [Open] + inputEnabled.setBounds(bounds.removeFromLeft(icon)); + bounds.removeFromLeft(gap); + + if (leftArrow.isVisible()) { + auto leftArea = bounds.removeFromLeft(icon); + leftArrow.setBounds(leftArea.withSizeKeepingCentre(icon, icon)); + bounds.removeFromLeft(gap); + } + + if (openPanelButton.isVisible()) { + openPanelButton.setBounds(bounds.removeFromRight(icon).withSizeKeepingCentre(icon, icon)); + bounds.removeFromRight(gap); + } + + if (closeFileButton.isVisible()) { + auto closeArea = bounds.removeFromRight(icon); + closeFileButton.setBounds(closeArea.withSizeKeepingCentre(icon, icon)); + bounds.removeFromRight(gap); + } + + if (rightArrow.isVisible()) { + auto rightArea = bounds.removeFromRight(icon); + rightArrow.setBounds(rightArea.withSizeKeepingCentre(icon, icon)); + bounds.removeFromRight(gap); + } + + fileLabel.setBounds(bounds); +} + +void FileControlsComponent::updateFileLabel() +{ + bool fileOpen = audioProcessor.getCurrentFileIndex() != -1 && !audioProcessor.objectServerRendering && !audioProcessor.inputEnabled->getBoolValue(); + bool showLeftArrow = audioProcessor.getCurrentFileIndex() > 0 && fileOpen; + bool showRightArrow = audioProcessor.getCurrentFileIndex() < audioProcessor.numFiles() - 1 && fileOpen; + + openPanelButton.setVisible(fileOpen); + closeFileButton.setVisible(fileOpen); + leftArrow.setVisible(showLeftArrow); + rightArrow.setVisible(showRightArrow); + +#if (JUCE_MAC || JUCE_WINDOWS) && OSCI_PREMIUM + if (audioProcessor.syphonInputActive) { + fileLabel.setText(pluginEditor.getSyphonSourceName(), juce::dontSendNotification); + } else +#endif + if (audioProcessor.objectServerRendering) { + fileLabel.setText("Rendering from Blender", juce::dontSendNotification); + } else if (audioProcessor.inputEnabled->getBoolValue()) { + fileLabel.setText("Using external audio", juce::dontSendNotification); + } else if (audioProcessor.getCurrentFileIndex() == -1) { + fileLabel.setText("No file open", juce::dontSendNotification); + } else { + fileLabel.setText(audioProcessor.getCurrentFileName(), juce::dontSendNotification); + } + + resized(); +} diff --git a/Source/components/FileControlsComponent.h b/Source/components/FileControlsComponent.h new file mode 100644 index 00000000..d1b6a0ab --- /dev/null +++ b/Source/components/FileControlsComponent.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include "../PluginProcessor.h" +#include "SvgButton.h" +#include "../LookAndFeel.h" + +class OscirenderAudioProcessorEditor; + +// Compact toolbar grouping: Open panel, left/right file nav, current file label, and close button +class FileControlsComponent : public juce::Component { +public: + FileControlsComponent(OscirenderAudioProcessor& p, OscirenderAudioProcessorEditor& editor); + + void paint(juce::Graphics& g) override; + void resized() override; + + // Called to refresh label and arrow visibility when current file changes + void updateFileLabel(); + +private: + OscirenderAudioProcessor& audioProcessor; + OscirenderAudioProcessorEditor& pluginEditor; + + // Controls + SvgButton inputEnabled{"inputEnabled", juce::String(BinaryData::microphone_svg), juce::Colours::white, juce::Colours::red, audioProcessor.inputEnabled}; + SvgButton leftArrow { "leftArrow", juce::String(BinaryData::left_arrow_svg), juce::Colours::white }; + SvgButton rightArrow { "rightArrow", juce::String(BinaryData::right_arrow_svg), juce::Colours::white }; + SvgButton closeFileButton{ "closeFile", juce::String(BinaryData::delete_svg), juce::Colours::red }; + SvgButton openPanelButton { "openFiles", juce::String(BinaryData::plus_svg), juce::Colours::white, juce::Colours::white }; + juce::Label fileLabel; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(FileControlsComponent) +}; diff --git a/Source/components/GridComponent.cpp b/Source/components/GridComponent.cpp index e07db256..e09a7072 100644 --- a/Source/components/GridComponent.cpp +++ b/Source/components/GridComponent.cpp @@ -2,13 +2,10 @@ GridComponent::GridComponent() { - // Setup scrollable viewport and content + // Default: use internal viewport addAndMakeVisible(viewport); viewport.setViewedComponent(&content, false); viewport.setScrollBarsShown(true, false); // vertical only - // Setup reusable bottom fade - initScrollFade(*this); - attachToViewport(viewport); } GridComponent::~GridComponent() = default; @@ -33,15 +30,31 @@ void GridComponent::paint(juce::Graphics& g) void GridComponent::resized() { auto bounds = getLocalBounds(); - viewport.setBounds(bounds); - auto contentArea = viewport.getLocalBounds(); - // Lock content width to viewport width to avoid horizontal scrolling - content.setSize(contentArea.getWidth(), content.getHeight()); + juce::Rectangle contentArea; + if (useInternalViewport) + { + viewport.setBounds(bounds); + viewport.setFadeVisible(true); + contentArea = viewport.getLocalBounds(); + // Lock content width to viewport width to avoid horizontal scrolling + content.setSize(contentArea.getWidth(), content.getHeight()); + } + else + { + // No internal viewport: lay out content directly within our bounds + viewport.setBounds(0, 0, 0, 0); + viewport.setFadeVisible(false); + contentArea = bounds; + content.setBounds(contentArea); + content.setSize(contentArea.getWidth(), contentArea.getHeight()); + } // Create FlexBox for responsive grid layout within content flexBox = juce::FlexBox(); flexBox.flexWrap = juce::FlexBox::Wrap::wrap; - flexBox.justifyContent = juce::FlexBox::JustifyContent::spaceBetween; + flexBox.justifyContent = useCenteringPlaceholders + ? juce::FlexBox::JustifyContent::spaceBetween + : juce::FlexBox::JustifyContent::flexStart; flexBox.alignContent = juce::FlexBox::AlignContent::flexStart; flexBox.flexDirection = juce::FlexBox::Direction::row; @@ -81,34 +94,47 @@ void GridComponent::resized() for (int c = 0; c < itemsPerRow; ++c) addItemFlex(items.getUnchecked(index++)); - // Add last row centered with balanced placeholders + // Add last row; optionally centered with placeholders or left-aligned if (remainder > 0) { - const int missing = itemsPerRow - remainder; - const int leftPad = missing / 2; - const int rightPad = missing - leftPad; + if (useCenteringPlaceholders) + { + const int missing = itemsPerRow - remainder; + const int leftPad = missing / 2; + const int rightPad = missing - leftPad; - for (int i = 0; i < leftPad; ++i) addPlaceholder(); - for (int i = 0; i < remainder; ++i) addItemFlex(items.getUnchecked(index++)); - for (int i = 0; i < rightPad; ++i) addPlaceholder(); + for (int i = 0; i < leftPad; ++i) addPlaceholder(); + for (int i = 0; i < remainder; ++i) addItemFlex(items.getUnchecked(index++)); + for (int i = 0; i < rightPad; ++i) addPlaceholder(); + } + else + { + for (int i = 0; i < remainder; ++i) addItemFlex(items.getUnchecked(index++)); + } } // Compute required content height const int requiredHeight = calculateRequiredHeight(viewW); - // If content is shorter than viewport, make content at least as tall as viewport + // If content is shorter than container, fill height; otherwise, set to required height int yOffset = 0; - if (requiredHeight < viewH) { - content.setSize(viewW, viewH); - yOffset = (viewH - requiredHeight) / 2; - } else { - content.setSize(viewW, requiredHeight); + if (useInternalViewport) + { + const int viewH = contentArea.getHeight(); + if (requiredHeight < viewH) { + content.setSize(viewW, viewH); + yOffset = (viewH - requiredHeight) / 2; + } else { + content.setSize(viewW, requiredHeight); + } + // Layout items within content at the computed offset + flexBox.performLayout(juce::Rectangle(0.0f, (float) yOffset, (float) viewW, (float) requiredHeight)); + } + else + { + content.setSize(viewW, requiredHeight); + flexBox.performLayout(juce::Rectangle(0.0f, 0.0f, (float) viewW, (float) requiredHeight)); } - // Layout items within content at the computed offset - flexBox.performLayout(juce::Rectangle(0.0f, (float) yOffset, (float) viewW, (float) requiredHeight)); - - // Layout bottom scroll fade over the viewport area - layoutScrollFadeIfNeeded(); } int GridComponent::calculateRequiredHeight(int availableWidth) const @@ -125,7 +151,26 @@ int GridComponent::calculateRequiredHeight(int availableWidth) const return numRows * 80; // ITEM_HEIGHT } -void GridComponent::layoutScrollFadeIfNeeded() + +void GridComponent::setUseViewport(bool shouldUseViewport) { - layoutScrollFade(viewport.getBounds(), true, 48); + if (useInternalViewport == shouldUseViewport) + return; + + useInternalViewport = shouldUseViewport; + + if (useInternalViewport) + { + // Reattach content to viewport and attach fade listeners + if (viewport.getViewedComponent() != &content) + viewport.setViewedComponent(&content, false); + } + else + { + // Hide viewport and lay out items directly + viewport.setViewedComponent(nullptr, false); + if (content.getParentComponent() != this) + addAndMakeVisible(content); + } + resized(); } diff --git a/Source/components/GridComponent.h b/Source/components/GridComponent.h index a9dbbbcf..bcb49781 100644 --- a/Source/components/GridComponent.h +++ b/Source/components/GridComponent.h @@ -1,10 +1,10 @@ #pragma once #include -#include "ScrollFadeMixin.h" +#include "ScrollFadeViewport.h" #include "GridItemComponent.h" // Generic grid component that owns and lays out GridItemComponent children -class GridComponent : public juce::Component, private ScrollFadeMixin +class GridComponent : public juce::Component { public: GridComponent(); @@ -18,8 +18,17 @@ public: juce::OwnedArray& getItems() { return items; } int calculateRequiredHeight(int availableWidth) const; + // Configuration: when true (default), pad the final row with placeholders so it's centered. + // When false, rows are left-aligned with no placeholders. + void setUseCenteringPlaceholders(bool shouldCenter) { useCenteringPlaceholders = shouldCenter; resized(); } + + // Configuration: when true (default), GridComponent uses its own internal Viewport. + // When false, the grid lays out directly without an internal scroll container (for embedding + // inside a parent Viewport). + void setUseViewport(bool shouldUseViewport); + private: - juce::Viewport viewport; // scroll container + ScrollFadeViewport viewport; // scroll container with fades juce::Component content; // holds the grid items juce::OwnedArray items; juce::FlexBox flexBox; @@ -27,7 +36,8 @@ private: static constexpr int ITEM_HEIGHT = 80; static constexpr int MIN_ITEM_WIDTH = 180; - void layoutScrollFadeIfNeeded(); + bool useCenteringPlaceholders { true }; + bool useInternalViewport { true }; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(GridComponent) }; diff --git a/Source/components/ScrollFadeMixin.h b/Source/components/ScrollFadeMixin.h deleted file mode 100644 index 4204c832..00000000 --- a/Source/components/ScrollFadeMixin.h +++ /dev/null @@ -1,196 +0,0 @@ -#pragma once - -#include -#include "VListBox.h" -#include "../LookAndFeel.h" - -// Overlay component that can render top and/or bottom scroll fades with adaptive strength. -class ScrollFadeOverlay : public juce::Component { -public: - ScrollFadeOverlay() { - setInterceptsMouseClicks(false, false); - setOpaque(false); - } - - void setFadeHeight(int hTopAndBottom) { - fadeHeightTop = fadeHeightBottom = juce::jmax(4, hTopAndBottom); - } - - void setFadeHeights(int topH, int bottomH) { - fadeHeightTop = juce::jmax(4, topH); - fadeHeightBottom = juce::jmax(4, bottomH); - } - - void setSidesEnabled(bool top, bool bottom) { - enableTop = top; - enableBottom = bottom; - } - - // Position the overlay to fully cover the scrollable viewport area (owner coordinates) - void layoutOver(const juce::Rectangle& listBounds) { - setBounds(listBounds); - } - - // Toggle per-side visibility/strength based on viewport scroll and enable flag. - void updateVisibilityFromViewport(juce::Viewport* vp, bool enabled) { - showTop = showBottom = false; - strengthTop = strengthBottom = 0.0f; - - if (enabled && vp != nullptr && vp->getVerticalScrollBar().isVisible()) { - auto& sb = vp->getVerticalScrollBar(); - const double start = sb.getCurrentRangeStart(); - const double size = sb.getCurrentRangeSize(); - const double max = sb.getMaximumRangeLimit(); - - // Top fade strength scales with how far from top we are - const bool atTop = start <= 0.5; - const double topDist = start; // pixels scrolled down - strengthTop = (float) juce::jlimit(0.0, 1.0, topDist / (double) juce::jmax(1, fadeHeightTop)); - showTop = enableTop && !atTop && strengthTop > 0.01f; - - // Bottom fade strength scales with how far from bottom we are - const double remaining = (max - (start + size)); - const bool atBottom = remaining <= 0.5; - strengthBottom = (float) juce::jlimit(0.0, 1.0, remaining / (double) juce::jmax(1, fadeHeightBottom)); - showBottom = enableBottom && !atBottom && strengthBottom > 0.01f; - } - - const bool anyVisible = (showTop || showBottom); - setVisible(anyVisible); - if (anyVisible) - repaint(); - } - - void paint(juce::Graphics& g) override { - auto area = getLocalBounds(); - const auto bg = findColour(groupComponentBackgroundColourId); - - if (showTop && fadeHeightTop > 0) { - const int h = juce::jmin(fadeHeightTop, area.getHeight()); - auto topRect = area.removeFromTop(h); - juce::ColourGradient gradTop(bg.withAlpha(strengthTop), - (float) topRect.getX(), (float) topRect.getY(), - bg.withAlpha(0.0f), - (float) topRect.getX(), (float) topRect.getBottom(), - false); - g.setGradientFill(gradTop); - g.fillRect(topRect); - } - - // Reset area for bottom drawing - area = getLocalBounds(); - if (showBottom && fadeHeightBottom > 0) { - const int h = juce::jmin(fadeHeightBottom, area.getHeight()); - auto bottomRect = area.removeFromBottom(h); - juce::ColourGradient gradBottom(bg.withAlpha(strengthBottom), - (float) bottomRect.getX(), (float) bottomRect.getBottom(), - bg.withAlpha(0.0f), - (float) bottomRect.getX(), (float) bottomRect.getY(), - false); - g.setGradientFill(gradBottom); - g.fillRect(bottomRect); - } - } - -private: - int fadeHeightTop { 48 }; - int fadeHeightBottom { 48 }; - bool enableTop { true }; - bool enableBottom { true }; - bool showTop { false }; - bool showBottom { false }; - float strengthTop { 0.0f }; - float strengthBottom { 0.0f }; -}; - -// Mixin to attach a bottom fade overlay for any scrollable area (ListBox or Viewport). -class ScrollFadeMixin { -public: - virtual ~ScrollFadeMixin() { - detachScrollListeners(); - } - -protected: - void initScrollFade(juce::Component& owner) { - if (! scrollFade) - scrollFade = std::make_unique(); - if (scrollFade->getParentComponent() != &owner) - owner.addAndMakeVisible(*scrollFade); - - scrollListener.owner = this; - } - - void attachToListBox(VListBox& list) { - detachScrollListeners(); - scrollViewport = list.getViewport(); - attachScrollListeners(); - } - - void attachToViewport(juce::Viewport& vp) { - detachScrollListeners(); - scrollViewport = &vp; - attachScrollListeners(); - } - - // Call from owner's resized(). listBounds must be in the owner's coordinate space. - void layoutScrollFade(const juce::Rectangle& listBounds, bool enabled = true, int fadeHeight = 48) { - if (! scrollFade) - return; - lastListBounds = listBounds; - lastEnabled = enabled; - lastFadeHeight = fadeHeight; - scrollFade->setFadeHeight(fadeHeight); - scrollFade->layoutOver(listBounds); - scrollFade->toFront(false); - scrollFade->updateVisibilityFromViewport(getViewport(), enabled); - } - - // Explicitly hide/show (e.g., when switching views) - void setScrollFadeVisible(bool shouldBeVisible) { - lastEnabled = shouldBeVisible; - if (scrollFade) - scrollFade->setVisible(shouldBeVisible); - } - - // Allow configuring which sides to render (default is both true) - void setScrollFadeSides(bool enableTop, bool enableBottom) { - if (scrollFade) { - scrollFade->setSidesEnabled(enableTop, enableBottom); - // Recompute since sides changed - scrollFade->updateVisibilityFromViewport(getViewport(), lastEnabled); - } - } - -protected: - std::unique_ptr scrollFade; - juce::Component::SafePointer scrollViewport; - juce::Viewport* getViewport() const noexcept { return static_cast(scrollViewport.getComponent()); } - -private: - // Listen to vertical scrollbar to update fade visibility while scrolling - struct VScrollListener : juce::ScrollBar::Listener { - ScrollFadeMixin* owner { nullptr }; - void scrollBarMoved(juce::ScrollBar*, double) override { - if (owner && owner->scrollFade) { - // Recompute visibility using last-known enabled state - owner->scrollFade->updateVisibilityFromViewport(owner->getViewport(), owner->lastEnabled); - } - } - } scrollListener; - - void attachScrollListeners() { - if (auto* vp = getViewport()) { - vp->getVerticalScrollBar().addListener(&scrollListener); - } - } - - void detachScrollListeners() { - if (auto* vp = getViewport()) { - vp->getVerticalScrollBar().removeListener(&scrollListener); - } - } - - juce::Rectangle lastListBounds; - bool lastEnabled { true }; - int lastFadeHeight { 48 }; -}; diff --git a/Source/components/ScrollFadeOverlay.h b/Source/components/ScrollFadeOverlay.h new file mode 100644 index 00000000..53f24384 --- /dev/null +++ b/Source/components/ScrollFadeOverlay.h @@ -0,0 +1,97 @@ +#pragma once + +#include +#include "../LookAndFeel.h" + +// Standalone overlay component for drawing gradient fades at the top/bottom of a scroll area. +class ScrollFadeOverlay : public juce::Component { +public: + ScrollFadeOverlay() { + setInterceptsMouseClicks(false, false); + setOpaque(false); + } + + void setFadeHeight(int hTopAndBottom) { + fadeHeightTop = fadeHeightBottom = juce::jmax(4, hTopAndBottom); + } + + void setFadeHeights(int topH, int bottomH) { + fadeHeightTop = juce::jmax(4, topH); + fadeHeightBottom = juce::jmax(4, bottomH); + } + + void setSidesEnabled(bool top, bool bottom) { + enableTop = top; + enableBottom = bottom; + } + + void layoutOver(const juce::Rectangle& listBounds) { setBounds(listBounds); } + + void updateVisibilityFromViewport(juce::Viewport* vp, bool enabled) { + showTop = showBottom = false; + strengthTop = strengthBottom = 0.0f; + + if (enabled && vp != nullptr && vp->getVerticalScrollBar().isVisible()) { + auto& sb = vp->getVerticalScrollBar(); + const double start = sb.getCurrentRangeStart(); + const double size = sb.getCurrentRangeSize(); + const double max = sb.getMaximumRangeLimit(); + + const bool atTop = start <= 0.5; + const double topDist = start; + strengthTop = (float) juce::jlimit(0.0, 1.0, topDist / (double) juce::jmax(1, fadeHeightTop)); + showTop = enableTop && !atTop && strengthTop > 0.01f; + + const double remaining = (max - (start + size)); + const bool atBottom = remaining <= 0.5; + strengthBottom = (float) juce::jlimit(0.0, 1.0, remaining / (double) juce::jmax(1, fadeHeightBottom)); + showBottom = enableBottom && !atBottom && strengthBottom > 0.01f; + } + + const bool anyVisible = (showTop || showBottom); + setVisible(anyVisible); + if (anyVisible) repaint(); + } + + void paint(juce::Graphics& g) override { + auto area = getLocalBounds(); + const auto bg = (getParentComponent() != nullptr) + ? getParentComponent()->findColour(groupComponentBackgroundColourId) + : findColour(groupComponentBackgroundColourId); + + if (showTop && fadeHeightTop > 0) { + const int h = juce::jmin(fadeHeightTop, area.getHeight()); + auto topRect = area.removeFromTop(h); + juce::ColourGradient gradTop(bg.withAlpha(strengthTop), + (float) topRect.getX(), (float) topRect.getY(), + bg.withAlpha(0.0f), + (float) topRect.getX(), (float) topRect.getBottom(), + false); + g.setGradientFill(gradTop); + g.fillRect(topRect); + } + + area = getLocalBounds(); + if (showBottom && fadeHeightBottom > 0) { + const int h = juce::jmin(fadeHeightBottom, area.getHeight()); + auto bottomRect = area.removeFromBottom(h); + juce::ColourGradient gradBottom(bg.withAlpha(strengthBottom), + (float) bottomRect.getX(), (float) bottomRect.getBottom(), + bg.withAlpha(0.0f), + (float) bottomRect.getX(), (float) bottomRect.getY(), + false); + g.setGradientFill(gradBottom); + g.fillRect(bottomRect); + } + } + +private: + int fadeHeightTop { 48 }; + int fadeHeightBottom { 48 }; + bool enableTop { true }; + bool enableBottom { true }; + bool showTop { false }; + bool showBottom { false }; + float strengthTop { 0.0f }; + float strengthBottom { 0.0f }; +}; diff --git a/Source/components/ScrollFadeViewport.h b/Source/components/ScrollFadeViewport.h new file mode 100644 index 00000000..38e18d7e --- /dev/null +++ b/Source/components/ScrollFadeViewport.h @@ -0,0 +1,91 @@ +#pragma once + +#include +#include "ScrollFadeOverlay.h" + +// Viewport with built-in scroll fade overlay handling. +// Automatically updates fade visibility based on scrollbar position. +class ScrollFadeViewport : public juce::Viewport { +public: + ScrollFadeViewport() { + // Ensure vertical scrolling is active by default + setScrollBarsShown(true, false); + + addAndMakeVisible(overlay); + overlay.setInterceptsMouseClicks(false, false); + overlay.setAlwaysOnTop(true); + overlay.setSidesEnabled(enableTop, enableBottom); + overlay.setFadeHeight(fadeHeight); + + vScrollListener.owner = this; + getVerticalScrollBar().addListener(&vScrollListener); + + // Initialise overlay visibility based on current scrollbar state + updateOverlay(); + } + void setViewedComponent(juce::Component* newViewedComponent, bool deleteComponentWhenNoLongerNeeded) { + juce::Viewport::setViewedComponent(newViewedComponent, deleteComponentWhenNoLongerNeeded); + layoutOverlay(); + } + + ~ScrollFadeViewport() override { + getVerticalScrollBar().removeListener(&vScrollListener); + } + + void resized() override { + juce::Viewport::resized(); + layoutOverlay(); + } + + void visibleAreaChanged(const juce::Rectangle&) override { + // Called when scroll position changes or content size affects visible area + updateOverlay(); + } + + void childBoundsChanged(juce::Component* child) override { + juce::Viewport::childBoundsChanged(child); + // Content resized; ensure overlay covers and visibility is recomputed + layoutOverlay(); + } + + // Configure which sides to render + void setSidesEnabled(bool top, bool bottom) { + enableTop = top; enableBottom = bottom; + overlay.setSidesEnabled(top, bottom); + updateOverlay(); + } + + void setFadeHeight(int height) { + fadeHeight = juce::jmax(4, height); + overlay.setFadeHeight(fadeHeight); + layoutOverlay(); + } + + void setFadeVisible(bool shouldBeVisible) { fadesEnabled = shouldBeVisible; updateOverlay(); } + +private: + ScrollFadeOverlay overlay; + bool enableTop { true }; + bool enableBottom { true }; + bool fadesEnabled { true }; + int fadeHeight { 48 }; + + void layoutOverlay() { + // Cover the viewport's content display area. + overlay.layoutOver(getLocalBounds()); + overlay.toFront(false); + updateOverlay(); + } + + void updateOverlay() { + overlay.updateVisibilityFromViewport(this, fadesEnabled); + overlay.repaint(); + } + + struct VSBListener : juce::ScrollBar::Listener { + ScrollFadeViewport* owner { nullptr }; + void scrollBarMoved(juce::ScrollBar*, double) override { + if (owner) owner->updateOverlay(); + } + } vScrollListener; +}; diff --git a/Source/components/VListBox.cpp b/Source/components/VListBox.cpp index 2c5294bd..0e467984 100644 --- a/Source/components/VListBox.cpp +++ b/Source/components/VListBox.cpp @@ -24,6 +24,7 @@ */ #include "VListBox.h" +#include "ScrollFadeViewport.h" class VListBox::RowComponent : public juce::Component, public TooltipClient { @@ -157,17 +158,22 @@ public: }; //============================================================================== -class VListBox::ListViewport : public juce::Viewport +class VListBox::ListViewport : public ScrollFadeViewport { public: ListViewport (VListBox& lb) : owner (lb) { setWantsKeyboardFocus (false); - auto content = new juce::Component(); - setViewedComponent (content); + auto content = new juce::Component(); + setViewedComponent(content, false); content->setWantsKeyboardFocus (false); + // Enable scroll fades for list views by default + setFadeVisible(true); + setSidesEnabled(true, true); + setFadeHeight(48); + updateAllRows(); } @@ -212,8 +218,10 @@ public: return -1; } - void visibleAreaChanged (const juce::Rectangle&) override + void visibleAreaChanged (const juce::Rectangle& newVisibleArea) override { + // Ensure scroll-fade overlay updates + ScrollFadeViewport::visibleAreaChanged(newVisibleArea); updateVisibleArea (true); if (auto* m = owner.getModel()) @@ -346,7 +354,7 @@ public: } } - return juce::Viewport::keyPressed (key); + return juce::Viewport::keyPressed (key); } private: diff --git a/modules/melatonin_inspector b/modules/melatonin_inspector new file mode 160000 index 00000000..9e91e4e3 --- /dev/null +++ b/modules/melatonin_inspector @@ -0,0 +1 @@ +Subproject commit 9e91e4e3d6cc41688c8d2108ef7ed33c1a90dcc9 diff --git a/osci-render.jucer b/osci-render.jucer index e520c992..984520f7 100644 --- a/osci-render.jucer +++ b/osci-render.jucer @@ -75,6 +75,7 @@ + @@ -200,6 +201,10 @@ resource="0" file="Source/components/ExampleFilesGridComponent.cpp"/> + + @@ -716,9 +721,6 @@ - - @@ -779,6 +781,7 @@ + + + userNotes="D86A3M3H2L"> + userNotes="Developer ID Application: James Ball (D86A3M3H2L)"/> + userNotes="Developer ID Application: James Ball (D86A3M3H2L)"/> @@ -860,6 +864,7 @@ + @@ -892,6 +897,7 @@ +