From 517534782deff2027b5ac2d1f00084a5bda1156b Mon Sep 17 00:00:00 2001 From: James H Ball Date: Mon, 18 Aug 2025 22:14:54 +0100 Subject: [PATCH] Add first pass at example files dialog --- Source/MainComponent.cpp | 8 ++ Source/MainComponent.h | 1 + Source/SettingsComponent.cpp | 34 ++++- Source/SettingsComponent.h | 6 + Source/components/EffectTypeItemComponent.cpp | 123 ------------------ Source/components/EffectTypeItemComponent.h | 38 ------ .../components/ExampleFilesGridComponent.cpp | 108 +++++++++++++++ Source/components/ExampleFilesGridComponent.h | 48 +++++++ osci-render.jucer | 11 +- 9 files changed, 208 insertions(+), 169 deletions(-) delete mode 100644 Source/components/EffectTypeItemComponent.cpp delete mode 100644 Source/components/EffectTypeItemComponent.h create mode 100644 Source/components/ExampleFilesGridComponent.cpp create mode 100644 Source/components/ExampleFilesGridComponent.h diff --git a/Source/MainComponent.cpp b/Source/MainComponent.cpp index 6890e4eb..8eda35d7 100644 --- a/Source/MainComponent.cpp +++ b/Source/MainComponent.cpp @@ -12,6 +12,12 @@ MainComponent::MainComponent(OscirenderAudioProcessor& p, OscirenderAudioProcess 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) { @@ -198,6 +204,8 @@ void MainComponent::resized() { 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) { diff --git a/Source/MainComponent.h b/Source/MainComponent.h index fb0e7587..22a7c491 100644 --- a/Source/MainComponent.h +++ b/Source/MainComponent.h @@ -38,6 +38,7 @@ private: juce::TextEditor fileName; juce::ComboBox fileType; juce::TextButton createFile{"Create File"}; + juce::TextButton showExamplesButton{"Examples"}; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MainComponent) }; diff --git a/Source/SettingsComponent.cpp b/Source/SettingsComponent.cpp index 87cf4937..e31bb8c8 100644 --- a/Source/SettingsComponent.cpp +++ b/Source/SettingsComponent.cpp @@ -11,6 +11,15 @@ SettingsComponent::SettingsComponent(OscirenderAudioProcessor& p, OscirenderAudi addAndMakeVisible(midi); addChildComponent(txt); addChildComponent(frame); + addChildComponent(examples); + + examples.onClosed = [this]() { + showExamples(false); + }; + examples.onExampleOpened = [this](const juce::String& fileName, bool shouldOpenEditor) { + pluginEditor.addCodeEditor(audioProcessor.getCurrentFileIndex()); + pluginEditor.fileUpdated(fileName, shouldOpenEditor); + }; double midiLayoutPreferredSize = std::any_cast(audioProcessor.getProperty("midiLayoutPreferredSize", pluginEditor.CLOSED_PREF_SIZE)); double mainLayoutPreferredSize = std::any_cast(audioProcessor.getProperty("mainLayoutPreferredSize", -0.5)); @@ -63,10 +72,22 @@ void SettingsComponent::resized() { dummyBounds.removeFromBottom(pluginEditor.RESIZER_BAR_SIZE); } - perspective.setBounds(dummyBounds.removeFromBottom(120)); - dummyBounds.removeFromBottom(pluginEditor.RESIZER_BAR_SIZE); - - effects.setBounds(dummyBounds); + if (examplesVisible) { + // Hide other panels while examples are visible + perspective.setVisible(false); + effects.setVisible(false); + txt.setVisible(false); + frame.setVisible(false); + examples.setVisible(true); + examples.setBounds(dummyBounds); + } else { + examples.setVisible(false); + perspective.setVisible(true); + effects.setVisible(true); + perspective.setBounds(dummyBounds.removeFromBottom(120)); + dummyBounds.removeFromBottom(pluginEditor.RESIZER_BAR_SIZE); + effects.setBounds(dummyBounds); + } if (isVisible() && getWidth() > 0 && getHeight() > 0) { audioProcessor.setProperty("midiLayoutPreferredSize", midiLayout.getItemCurrentRelativeSize(2)); @@ -128,6 +149,11 @@ void SettingsComponent::mouseMove(const juce::MouseEvent& event) { setMouseCursor(juce::MouseCursor::NormalCursor); } +void SettingsComponent::showExamples(bool shouldShow) { + examplesVisible = shouldShow; + resized(); +} + void SettingsComponent::mouseDown(const juce::MouseEvent& event) { for (int i = 0; i < 1; i++) { if (toggleComponents[i]->getBounds().removeFromTop(pluginEditor.CLOSED_PREF_SIZE).contains(event.getPosition())) { diff --git a/Source/SettingsComponent.h b/Source/SettingsComponent.h index 911e56fd..2136cb24 100644 --- a/Source/SettingsComponent.h +++ b/Source/SettingsComponent.h @@ -10,6 +10,7 @@ #include "PerspectiveComponent.h" #include "PluginProcessor.h" #include "TxtComponent.h" +#include "components/ExampleFilesGridComponent.h" class OscirenderAudioProcessorEditor; class SettingsComponent : public juce::Component { @@ -21,6 +22,8 @@ public: void update(); void mouseMove(const juce::MouseEvent& event) override; void mouseDown(const juce::MouseEvent& event) override; + // Show or hide the example files grid panel on the right-hand side + void showExamples(bool shouldShow); private: OscirenderAudioProcessor& audioProcessor; @@ -32,6 +35,9 @@ private: FrameSettingsComponent frame{audioProcessor, pluginEditor}; EffectsComponent effects{audioProcessor, pluginEditor}; MidiComponent midi{audioProcessor, pluginEditor}; + ExampleFilesGridComponent examples{audioProcessor}; + + bool examplesVisible = false; juce::StretchableLayoutManager midiLayout; juce::StretchableLayoutResizerBar midiResizerBar{&midiLayout, 1, false}; diff --git a/Source/components/EffectTypeItemComponent.cpp b/Source/components/EffectTypeItemComponent.cpp deleted file mode 100644 index e15c9701..00000000 --- a/Source/components/EffectTypeItemComponent.cpp +++ /dev/null @@ -1,123 +0,0 @@ -#include "EffectTypeItemComponent.h" - -EffectTypeItemComponent::EffectTypeItemComponent(const juce::String& name, const juce::String& icon, const juce::String& id) - : effectName(name), effectId(id) -{ - juce::String iconSvg = icon; - if (icon.isEmpty()) { - // Default icon if none is provided - iconSvg = juce::String::createStringFromData(BinaryData::rotate_svg, BinaryData::rotate_svgSize); - } - iconButton = std::make_unique( - "effectIcon", - iconSvg, - juce::Colours::white.withAlpha(0.7f) - ); - - // Make the icon non-interactive since this is just a visual element - iconButton->setInterceptsMouseClicks(false, false); - addAndMakeVisible(*iconButton); -} - -EffectTypeItemComponent::~EffectTypeItemComponent() = default; - -void EffectTypeItemComponent::paint(juce::Graphics& g) -{ - auto bounds = getLocalBounds().toFloat().reduced(10); - - // Get animation progress from inherited HoverAnimationMixin (disabled => no hover) - auto animationProgress = isEnabled() ? getAnimationProgress() : 0.0f; - - // Apply upward shift based on animation progress - auto yOffset = -animationProgress * HOVER_LIFT_AMOUNT; - bounds = bounds.translated(0, yOffset); - - // Draw drop shadow - if (animationProgress > 0.01f) { - juce::DropShadow shadow; - shadow.colour = juce::Colours::lime.withAlpha(animationProgress * 0.2f); - shadow.radius = 15 * animationProgress; - shadow.offset = juce::Point(0, 4); - - if (shadow.radius > 0) { - juce::Path shadowPath; - shadowPath.addRoundedRectangle(bounds.toFloat(), CORNER_RADIUS); - shadow.drawForPath(g, shadowPath); - } - } - - // Draw background with rounded corners - interpolate between normal and hover colors - juce::Colour normalBgColour = Colours::veryDark; - juce::Colour hoverBgColour = normalBgColour.brighter(0.05f); - juce::Colour bgColour = normalBgColour.interpolatedWith(hoverBgColour, animationProgress); - - g.setColour(bgColour); - g.fillRoundedRectangle(bounds.toFloat(), CORNER_RADIUS); - - // Draw colored outline - juce::Colour outlineColour = juce::Colour::fromRGB(160, 160, 160); - g.setColour(outlineColour.withAlpha(0.9f)); - g.drawRoundedRectangle(bounds.toFloat(), CORNER_RADIUS, 1.0f); - - // Create text area - now accounting for icon space on the left - auto textArea = bounds.reduced(8, 4); - textArea.removeFromLeft(28); // Remove space for icon (24px + 4px gap) - - g.setColour(juce::Colours::white); - g.setFont(juce::FontOptions(16.0f, juce::Font::plain)); - g.drawText(effectName, textArea, juce::Justification::centred, true); - - // If disabled, draw a dark transparent overlay over the rounded rect to simplify visuals - if (! isEnabled()) { - g.setColour(juce::Colours::black.withAlpha(0.35f)); - g.fillRoundedRectangle(bounds.toFloat(), CORNER_RADIUS); - } -} - -void EffectTypeItemComponent::resized() -{ - auto bounds = getLocalBounds().reduced(10); - - // Reserve space for the icon on the left - auto iconArea = bounds.removeFromLeft(60); // 24px for icon - iconArea = iconArea.withSizeKeepingCentre(40, 40); // Make icon 20x20px - - iconButton->setBounds(iconArea); - - // Get animation progress and calculate Y offset - auto animationProgress = isEnabled() ? getAnimationProgress() : 0.0f; - auto yOffset = -animationProgress * HOVER_LIFT_AMOUNT; - - iconButton->setTransform(juce::AffineTransform::translation(0, yOffset)); -} - -void EffectTypeItemComponent::mouseDown(const juce::MouseEvent& event) -{ - if (! isEnabled()) return; - // Extend base behavior to keep hover press animation - HoverAnimationMixin::mouseDown(event); - // Ensure any hover preview is cleared before permanently selecting/enabling the effect - if (onHoverEnd) onHoverEnd(); - if (onEffectSelected) { - onEffectSelected(effectId); - } -} - -void EffectTypeItemComponent::mouseMove(const juce::MouseEvent& event) { - setMouseCursor(isEnabled() ? juce::MouseCursor::PointingHandCursor : juce::MouseCursor::NormalCursor); - juce::Desktop::getInstance().getMainMouseSource().forceMouseCursorUpdate(); -} - -void EffectTypeItemComponent::mouseEnter(const juce::MouseEvent& event) -{ - HoverAnimationMixin::mouseEnter(event); - if (isEnabled() && onHoverStart) - onHoverStart(effectId); -} - -void EffectTypeItemComponent::mouseExit(const juce::MouseEvent& event) -{ - HoverAnimationMixin::mouseExit(event); - if (onHoverEnd) - onHoverEnd(); -} diff --git a/Source/components/EffectTypeItemComponent.h b/Source/components/EffectTypeItemComponent.h deleted file mode 100644 index 4bfab69e..00000000 --- a/Source/components/EffectTypeItemComponent.h +++ /dev/null @@ -1,38 +0,0 @@ -#pragma once -#include -#include "../LookAndFeel.h" -#include "HoverAnimationMixin.h" -#include "SvgButton.h" - -class EffectTypeItemComponent : public HoverAnimationMixin -{ -public: - EffectTypeItemComponent(const juce::String& name, const juce::String& icon, const juce::String& id); - ~EffectTypeItemComponent() override; - - void paint(juce::Graphics& g) override; - void resized() override; - void mouseDown(const juce::MouseEvent& event) override; - void mouseMove(const juce::MouseEvent& event) override; - void mouseEnter(const juce::MouseEvent& event) override; - void mouseExit(const juce::MouseEvent& event) override; - - const juce::String& getEffectId() const { return effectId; } - const juce::String& getEffectName() const { return effectName; } - - std::function onEffectSelected; - std::function onHoverStart; - std::function onHoverEnd; - -private: - juce::String effectName; - juce::String effectId; - - // Icon for the effect - std::unique_ptr iconButton; - - static constexpr int CORNER_RADIUS = 8; - static constexpr float HOVER_LIFT_AMOUNT = 2.0f; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(EffectTypeItemComponent) -}; diff --git a/Source/components/ExampleFilesGridComponent.cpp b/Source/components/ExampleFilesGridComponent.cpp new file mode 100644 index 00000000..577c0923 --- /dev/null +++ b/Source/components/ExampleFilesGridComponent.cpp @@ -0,0 +1,108 @@ +#include "ExampleFilesGridComponent.h" +#include "GridItemComponent.h" +#include "../JuceLibraryCode/BinaryData.h" + +ExampleFilesGridComponent::ExampleFilesGridComponent(OscirenderAudioProcessor& processor) + : audioProcessor(processor) +{ + // Top bar + addAndMakeVisible(title); + addAndMakeVisible(closeButton); + styleHeading(title); + closeButton.onClick = [this]() { if (onClosed) onClosed(); }; + + // Add categories to component + auto addCat = [this](CategoryViews& cat) { + styleHeading(cat.heading); + addAndMakeVisible(cat.heading); + addAndMakeVisible(cat.grid); + }; + addCat(audioCat); + addCat(textCat); + addCat(imagesCat); + addCat(luaCat); + addCat(modelsCat); + addCat(svgsCat); + + populate(); +} + +void ExampleFilesGridComponent::styleHeading(juce::Label& l) +{ + l.setInterceptsMouseClicks(false, false); + l.setJustificationType(juce::Justification::left); + l.setFont(juce::FontOptions(16.0f, juce::Font::bold)); +} + +void ExampleFilesGridComponent::paint(juce::Graphics& g) +{ + // transparent background +} + +void ExampleFilesGridComponent::resized() +{ + auto bounds = getLocalBounds(); + auto top = bounds.removeFromTop(30); + title.setBounds(top.removeFromLeft(200).reduced(4)); + closeButton.setBounds(top.removeFromRight(80).reduced(4)); + + auto layCat = [&](CategoryViews& cat) { + auto header = bounds.removeFromTop(24); + cat.heading.setBounds(header.reduced(2)); + auto h = cat.grid.calculateRequiredHeight(bounds.getWidth()); + cat.grid.setBounds(bounds.removeFromTop(h)); + bounds.removeFromTop(8); // gap + }; + + layCat(audioCat); + layCat(textCat); + layCat(imagesCat); + layCat(luaCat); + layCat(modelsCat); + layCat(svgsCat); +} + +void ExampleFilesGridComponent::addExample(CategoryViews& cat, const juce::String& fileName, const char* data, int size) +{ + // Use placeholder icon for now + auto* item = new GridItemComponent(fileName, juce::String::createStringFromData(BinaryData::random_svg, BinaryData::random_svgSize), fileName); + item->onItemSelected = [this, fileName, data, size](const juce::String&) { + juce::SpinLock::ScopedLockType parsersLock(audioProcessor.parsersLock); + audioProcessor.addFile(fileName, data, size); + // 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); + }; + cat.grid.addItem(item); +} + +void ExampleFilesGridComponent::populate() +{ + // Audio examples + addExample(audioCat, "sosci.flac", BinaryData::sosci_flac, BinaryData::sosci_flacSize); + + // Text examples + addExample(textCat, "helloworld.txt", BinaryData::helloworld_txt, BinaryData::helloworld_txtSize); + addExample(textCat, "greek.txt", BinaryData::greek_txt, BinaryData::greek_txtSize); + + // Image examples (will open as images via frame settings path) + addExample(imagesCat, "empty.jpg", BinaryData::empty_jpg, BinaryData::empty_jpgSize); + addExample(imagesCat, "no_reflection.jpg", BinaryData::no_reflection_jpg, BinaryData::no_reflection_jpgSize); + addExample(imagesCat, "noise.jpg", BinaryData::noise_jpg, BinaryData::noise_jpgSize); + addExample(imagesCat, "real.png", BinaryData::real_png, BinaryData::real_pngSize); + addExample(imagesCat, "real_reflection.png", BinaryData::real_reflection_png, BinaryData::real_reflection_pngSize); + addExample(imagesCat, "vector_display.png", BinaryData::vector_display_png, BinaryData::vector_display_pngSize); + addExample(imagesCat, "vector_display_reflection.png", BinaryData::vector_display_reflection_png, BinaryData::vector_display_reflection_pngSize); + + // Lua examples + addExample(luaCat, "demo.lua", BinaryData::demo_lua, BinaryData::demo_luaSize); + + // 3D model examples + addExample(modelsCat, "cube.obj", BinaryData::cube_obj, BinaryData::cube_objSize); + + // SVG examples (just a subset for brevity, can add more) + addExample(svgsCat, "demo.svg", BinaryData::demo_svg, BinaryData::demo_svgSize); + addExample(svgsCat, "lua.svg", BinaryData::lua_svg, BinaryData::lua_svgSize); + addExample(svgsCat, "trace.svg", BinaryData::trace_svg, BinaryData::trace_svgSize); + addExample(svgsCat, "wobble.svg", BinaryData::wobble_svg, BinaryData::wobble_svgSize); +} diff --git a/Source/components/ExampleFilesGridComponent.h b/Source/components/ExampleFilesGridComponent.h new file mode 100644 index 00000000..e9421bc0 --- /dev/null +++ b/Source/components/ExampleFilesGridComponent.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include "../PluginProcessor.h" +#include "GridComponent.h" + +// A grid-based browser for example files grouped by category +class ExampleFilesGridComponent : public juce::Component +{ +public: + ExampleFilesGridComponent(OscirenderAudioProcessor& processor); + ~ExampleFilesGridComponent() override = default; + + void paint(juce::Graphics& g) override; + void resized() override; + + // Called when the user closes the view + std::function onClosed; + // Called after the file has been added to the processor; consumer may open editors, etc. + std::function onExampleOpened; + +private: + OscirenderAudioProcessor& audioProcessor; + + // Top bar + juce::Label title { {}, "Examples" }; + juce::TextButton closeButton { "Close" }; + + // Categories + struct CategoryViews { + juce::Label heading; + GridComponent grid; + }; + + CategoryViews audioCat { juce::Label({}, "Audio"), GridComponent{} }; + CategoryViews textCat { juce::Label({}, "Text"), GridComponent{} }; + CategoryViews imagesCat { juce::Label({}, "Images"), GridComponent{} }; + CategoryViews luaCat { juce::Label({}, "Lua"), GridComponent{} }; + CategoryViews modelsCat { juce::Label({}, "3D models"), GridComponent{} }; + CategoryViews svgsCat { juce::Label({}, "SVGs"), GridComponent{} }; + + // Helpers + void addExample(CategoryViews& cat, const juce::String& fileName, const char* data, int size); + void populate(); + void styleHeading(juce::Label& l); + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ExampleFilesGridComponent) +}; diff --git a/osci-render.jucer b/osci-render.jucer index 11df1687..e520c992 100644 --- a/osci-render.jucer +++ b/osci-render.jucer @@ -9,6 +9,9 @@ pluginAUMainType="'aumf'"> + + + @@ -191,12 +194,12 @@ file="Source/components/EffectTypeGridComponent.cpp"/> - - + +