kopia lustrzana https://github.com/jameshball/osci-render
Redesign and remove MainComponent to put the file controls as a separate bar at the top
rodzic
517534782d
commit
f6d6868046
|
@ -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
|
||||
|
|
|
@ -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 |
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
};
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -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)
|
||||
};
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
};
|
||||
|
|
|
@ -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 };
|
||||
};
|
|
@ -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 };
|
||||
};
|
|
@ -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;
|
||||
};
|
|
@ -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
|
|
@ -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>
|
||||
|
|
Ładowanie…
Reference in New Issue