Redesign and remove MainComponent to put the file controls as a separate bar at the top

pull/319/head
James H Ball 2025-08-22 11:37:33 +01:00
rodzic 517534782d
commit f6d6868046
21 zmienionych plików z 699 dodań i 587 usunięć

4
.gitmodules vendored
Wyświetl plik

@ -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

Wyświetl plik

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z" /></svg>

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 120 B

Wyświetl plik

@ -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)

Wyświetl plik

@ -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);
}

Wyświetl plik

@ -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;

Wyświetl plik

@ -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<juce::FileChooser>("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<int> 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<int>());
}
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<int> localTopLeft = {bounds.getX(), bounds.getY()};
juce::Point<int> 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));
}
}

Wyświetl plik

@ -1,44 +0,0 @@
#pragma once
#include <JuceHeader.h>
#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<juce::FileChooser> 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)
};

Wyświetl plik

@ -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<int> localTopLeft = {bounds.getX(), bounds.getY()};
juce::Point<int> 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) {

Wyświetl plik

@ -2,22 +2,27 @@
#include <JuceHeader.h>
#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<int> volumeVisualiserBounds;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SettingsComponent)
};

Wyświetl plik

@ -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<int>(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<juce::FileChooser>("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();
}
});
}

Wyświetl plik

@ -3,8 +3,10 @@
#include <JuceHeader.h>
#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<juce::FileChooser> 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)
};

Wyświetl plik

@ -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();
}

Wyświetl plik

@ -0,0 +1,34 @@
#pragma once
#include <JuceHeader.h>
#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)
};

Wyświetl plik

@ -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<int> 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<float>(0.0f, (float) yOffset, (float) viewW, (float) requiredHeight));
}
else
{
content.setSize(viewW, requiredHeight);
flexBox.performLayout(juce::Rectangle<float>(0.0f, 0.0f, (float) viewW, (float) requiredHeight));
}
// Layout items within content at the computed offset
flexBox.performLayout(juce::Rectangle<float>(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();
}

Wyświetl plik

@ -1,10 +1,10 @@
#pragma once
#include <JuceHeader.h>
#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<GridItemComponent>& 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<GridItemComponent> 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)
};

Wyświetl plik

@ -1,196 +0,0 @@
#pragma once
#include <JuceHeader.h>
#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<int>& 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<ScrollFadeOverlay>();
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<int>& 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<ScrollFadeOverlay> scrollFade;
juce::Component::SafePointer<juce::Viewport> scrollViewport;
juce::Viewport* getViewport() const noexcept { return static_cast<juce::Viewport*>(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<int> lastListBounds;
bool lastEnabled { true };
int lastFadeHeight { 48 };
};

Wyświetl plik

@ -0,0 +1,97 @@
#pragma once
#include <JuceHeader.h>
#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<int>& 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 };
};

Wyświetl plik

@ -0,0 +1,91 @@
#pragma once
#include <JuceHeader.h>
#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<int>&) 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;
};

Wyświetl plik

@ -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<int>&) override
void visibleAreaChanged (const juce::Rectangle<int>& 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:

@ -0,0 +1 @@
Subproject commit 9e91e4e3d6cc41688c8d2108ef7ed33c1a90dcc9

Wyświetl plik

@ -75,6 +75,7 @@
<FILE id="f2D5tv" name="pause.svg" compile="0" resource="1" file="Resources/svg/pause.svg"/>
<FILE id="D2AI1b" name="pencil.svg" compile="0" resource="1" file="Resources/svg/pencil.svg"/>
<FILE id="sfWuFd" name="play.svg" compile="0" resource="1" file="Resources/svg/play.svg"/>
<FILE id="FJG3Ht" name="plus.svg" compile="0" resource="1" file="Resources/svg/plus.svg"/>
<FILE id="PFc2q2" name="random.svg" compile="0" resource="1" file="Resources/svg/random.svg"/>
<FILE id="CE6di2" name="range.svg" compile="0" resource="1" file="Resources/svg/range.svg"/>
<FILE id="n79IAy" name="record.svg" compile="0" resource="1" file="Resources/svg/record.svg"/>
@ -200,6 +201,10 @@
resource="0" file="Source/components/ExampleFilesGridComponent.cpp"/>
<FILE id="K9EITe" name="ExampleFilesGridComponent.h" compile="0" resource="0"
file="Source/components/ExampleFilesGridComponent.h"/>
<FILE id="oGIcdI" name="FileControlsComponent.cpp" compile="1" resource="0"
file="Source/components/FileControlsComponent.cpp"/>
<FILE id="uo7flZ" name="FileControlsComponent.h" compile="0" resource="0"
file="Source/components/FileControlsComponent.h"/>
<FILE id="sqD2Zy" name="GridComponent.cpp" compile="1" resource="0"
file="Source/components/GridComponent.cpp"/>
<FILE id="QRwdXD" name="GridComponent.h" compile="0" resource="0" file="Source/components/GridComponent.h"/>
@ -716,9 +721,6 @@
<FILE id="X26RjJ" name="LuaComponent.cpp" compile="1" resource="0"
file="Source/LuaComponent.cpp"/>
<FILE id="g5xRHT" name="LuaComponent.h" compile="0" resource="0" file="Source/LuaComponent.h"/>
<FILE id="GKBQ8j" name="MainComponent.cpp" compile="1" resource="0"
file="Source/MainComponent.cpp"/>
<FILE id="RU8fGr" name="MainComponent.h" compile="0" resource="0" file="Source/MainComponent.h"/>
<FILE id="cFVaxu" name="MathUtil.h" compile="0" resource="0" file="Source/MathUtil.h"/>
<FILE id="eB92KJ" name="MidiComponent.cpp" compile="1" resource="0"
file="Source/MidiComponent.cpp"/>
@ -779,6 +781,7 @@
<MODULEPATH id="juce_sharedtexture" path="modules"/>
<MODULEPATH id="osci_render_core" path="modules"/>
<MODULEPATH id="chowdsp_gui" path="modules/chowdsp_utils/modules/gui"/>
<MODULEPATH id="melatonin_inspector" path="modules"/>
</MODULEPATHS>
</LINUX_MAKE>
<VS2022 targetFolder="Builds/osci-render/VisualStudio2022" smallIcon="pSc1mq"
@ -816,6 +819,7 @@
<MODULEPATH id="juce_sharedtexture" path="modules"/>
<MODULEPATH id="osci_render_core" path="modules"/>
<MODULEPATH id="chowdsp_gui" path="modules/chowdsp_utils/modules/gui"/>
<MODULEPATH id="melatonin_inspector" path="modules"/>
</MODULEPATHS>
</VS2022>
<XCODE_MAC targetFolder="Builds/osci-render/MacOSX" extraLinkerFlags="-Wl,-weak_reference_mismatches,weak"
@ -824,14 +828,14 @@
microphonePermissionNeeded="1" frameworkSearchPaths="/Library/Frameworks"
extraCustomFrameworks="/Library/Frameworks/Syphon.framework"
hardenedRuntime="1" hardenedRuntimeOptions="com.apple.security.cs.disable-library-validation,com.apple.security.device.audio-input"
iosDevelopmentTeamID="D86A3M3H2L">
userNotes="D86A3M3H2L">
<CONFIGURATIONS>
<CONFIGURATION isDebug="1" name="Debug" targetName="osci-render" customXcodeFlags="LD_RUNPATH_SEARCH_PATHS = '/Library/Frameworks',OTHER_CODE_SIGN_FLAGS = --timestamp --force --deep"
codeSigningIdentity="Developer ID Application: James Ball (D86A3M3H2L)"/>
userNotes="Developer ID Application: James Ball (D86A3M3H2L)"/>
<CONFIGURATION name="Release" targetName="osci-render" customXcodeFlags="LD_RUNPATH_SEARCH_PATHS = '/Library/Frameworks',CODE_SIGN_INJECT_BASE_ENTITLEMENTS=NO,OTHER_CODE_SIGN_FLAGS = --timestamp --force --deep"
codeSigningIdentity="Developer ID Application: James Ball (D86A3M3H2L)"/>
<CONFIGURATION name="Release (Development)" targetName="osci-render" customXcodeFlags="LD_RUNPATH_SEARCH_PATHS = '/Library/Frameworks',OTHER_CODE_SIGN_FLAGS = --timestamp --force --deep"
codeSigningIdentity="Developer ID Application: James Ball (D86A3M3H2L)"/>
userNotes="Developer ID Application: James Ball (D86A3M3H2L)"/>
</CONFIGURATIONS>
<MODULEPATHS>
<MODULEPATH id="juce_audio_basics" path="../../../JUCE/modules"/>
@ -860,6 +864,7 @@
<MODULEPATH id="juce_sharedtexture" path="modules"/>
<MODULEPATH id="osci_render_core" path="modules"/>
<MODULEPATH id="chowdsp_gui" path="modules/chowdsp_utils/modules/gui"/>
<MODULEPATH id="melatonin_inspector" path="modules"/>
</MODULEPATHS>
</XCODE_MAC>
</EXPORTFORMATS>
@ -892,6 +897,7 @@
<MODULE id="juce_gui_extra" showAllCode="1" useLocalCopy="0" useGlobalPath="1"/>
<MODULE id="juce_opengl" showAllCode="1" useLocalCopy="0" useGlobalPath="1"/>
<MODULE id="juce_sharedtexture" showAllCode="1" useLocalCopy="0" useGlobalPath="0"/>
<MODULE id="melatonin_inspector" showAllCode="1" useLocalCopy="0" useGlobalPath="0"/>
<MODULE id="osci_render_core" showAllCode="1" useLocalCopy="0" useGlobalPath="0"/>
</MODULES>
</JUCERPROJECT>