Skip to content

Commit af81d2d

Browse files
tucktuckg00seclaude
andcommitted
feat: add built-in sample browser side panel
Adds a togglable file browser docked to the left of the editor. Replaces the header LOAD/APPEND buttons with a single FILES toggle that opens the panel. The browser supports drive/bookmark navigation, back/forward/up/refresh history, a path editor with error flash, a resizable splitter between locations and files, drag-and-drop of audio files onto the waveform, and double-click load (append when a sample is already loaded). Browser visibility and bookmarks persist in user settings. Window width grows by 260px when the browser is open. Also: header sample/status text is centered, warning messages use the theme's color5 instead of hard-coded orange, the Wayland empty-state hint auto-hides after ~5s, and waveform file drops now filter non-audio extensions. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent e8265a3 commit af81d2d

10 files changed

Lines changed: 1121 additions & 73 deletions

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
66

77
## [Unreleased]
88

9+
### Added
10+
- Built-in sample browser side panel: toggle with the new FILES button in the header to navigate your file system, bookmark folders, browse with back/forward/up/refresh, and drag or double-click audio files into the waveform. Browser visibility and bookmarks are saved with your user settings.
11+
12+
### Changed
13+
- Replaced the LOAD and APPEND buttons in the header with a single FILES toggle that opens the new browser panel
14+
- Sample name and status text in the header is now centered, and warning text follows the active theme's warning color instead of a hard-coded orange
15+
16+
### Fixed
17+
- Wayland drag-and-drop fallback hint on the empty waveform now auto-hides after a few seconds instead of staying on screen indefinitely
18+
- Dropping non-audio files onto the waveform no longer attempts to load them
19+
920
## [0.14.0] - 2026-05-01
1021

1122
### Added

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ target_sources(Intersect PRIVATE
146146
src/params/ParamLayout.cpp
147147
src/ui/IntersectLookAndFeel.cpp
148148
src/ui/HeaderBar.cpp
149+
src/ui/SampleBrowserPanel.cpp
149150
src/ui/SampleLane.cpp
150151
src/ui/SignalChainBar.cpp
151152
src/ui/WaveformView.cpp

src/PluginEditor.cpp

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ static constexpr float kScrollbarH = 10.0f;
1111
static constexpr float kActionH = 22.0f;
1212
static constexpr float kWaveformMinH = 180.0f;
1313
static constexpr float kCollapsedSignalChainH = 114.0f;
14+
static constexpr int kSampleBrowserW = 260;
1415

1516
static juce::File getSettingsDir()
1617
{
@@ -93,6 +94,7 @@ FadeOverlayState resolveSelectedFadeOverlayState (const IntersectProcessor::UiSl
9394
IntersectEditor::IntersectEditor (IntersectProcessor& p)
9495
: AudioProcessorEditor (p),
9596
processor (p),
97+
sampleBrowser(),
9698
headerBar (p),
9799
sampleLane (p, waveformView),
98100
signalChainBar (p),
@@ -103,6 +105,7 @@ IntersectEditor::IntersectEditor (IntersectProcessor& p)
103105
{
104106
setLookAndFeel (&lnf);
105107

108+
addChildComponent (sampleBrowser);
106109
addAndMakeVisible (headerBar);
107110
addAndMakeVisible (sampleLane);
108111
addAndMakeVisible (signalChainBar);
@@ -116,11 +119,18 @@ IntersectEditor::IntersectEditor (IntersectProcessor& p)
116119
sliceLane.onInteraction = [this] { deleteTarget = DeleteTarget::slice; };
117120
waveformView.onInteraction = [this] { deleteTarget = DeleteTarget::slice; };
118121
actionPanel.onDeleteRequested = [this] { performContextualDelete(); };
122+
headerBar.onBrowserToggle = [this] { setSampleBrowserVisible (! sampleBrowserVisible); };
123+
sampleBrowser.onFilesChosen = [this] (const std::vector<juce::File>& files) { loadBrowserFiles (files); };
124+
sampleBrowser.onBookmarksChanged = [this]
125+
{
126+
float scale = processor.apvts.getRawParameterValue (ParamIds::uiScale)->load();
127+
saveUserSettings (scale, getTheme().name);
128+
};
119129

120130
signalChainBar.onHeightChanged = [this]
121131
{
122132
float delta = signalChainBar.getDesiredHeight() - kCollapsedSignalChainH;
123-
setSize (kBaseW, kBaseH + (int) delta);
133+
setSize (kBaseW + (sampleBrowserVisible ? kSampleBrowserW : 0), kBaseH + (int) delta);
124134
};
125135

126136
// Write default theme files if they don't exist
@@ -138,7 +148,7 @@ IntersectEditor::IntersectEditor (IntersectProcessor& p)
138148
}
139149

140150
setWantsKeyboardFocus (true);
141-
setSize (kBaseW, kBaseH);
151+
setSize (kBaseW + (sampleBrowserVisible ? kSampleBrowserW : 0), kBaseH);
142152
lastUiSnapshotVersion = processor.getUiSliceSnapshotVersion();
143153
lastGlobalFadeCrossfade = processor.apvts.getRawParameterValue (ParamIds::defaultCrossfade)->load();
144154
lastGlobalFadeLoopMode = juce::roundToInt (processor.apvts.getRawParameterValue (ParamIds::defaultLoop)->load());
@@ -159,6 +169,12 @@ void IntersectEditor::paint (juce::Graphics& g)
159169

160170
void IntersectEditor::resized()
161171
{
172+
auto bounds = getLocalBounds();
173+
if (sampleBrowserVisible)
174+
sampleBrowser.setBounds (bounds.removeFromLeft (kSampleBrowserW));
175+
else
176+
sampleBrowser.setBounds ({});
177+
162178
juce::FlexBox shell;
163179
shell.flexDirection = juce::FlexBox::Direction::column;
164180
shell.flexWrap = juce::FlexBox::Wrap::noWrap;
@@ -192,7 +208,7 @@ void IntersectEditor::resized()
192208
.withMaxHeight (signalChainH)
193209
.withHeight (signalChainH));
194210

195-
shell.performLayout (getLocalBounds().toFloat());
211+
shell.performLayout (bounds.toFloat());
196212
}
197213

198214
bool IntersectEditor::keyPressed (const juce::KeyPress& key)
@@ -337,6 +353,41 @@ void IntersectEditor::performContextualDelete()
337353
actionPanel.deleteSelectedSliceDirect();
338354
}
339355

356+
void IntersectEditor::setSampleBrowserVisible (bool shouldBeVisible)
357+
{
358+
if (sampleBrowserVisible == shouldBeVisible)
359+
return;
360+
361+
sampleBrowserVisible = shouldBeVisible;
362+
sampleBrowser.setVisible (sampleBrowserVisible);
363+
364+
const float delta = signalChainBar.getDesiredHeight() - kCollapsedSignalChainH;
365+
setSize (kBaseW + (sampleBrowserVisible ? kSampleBrowserW : 0), kBaseH + (int) delta);
366+
367+
float scale = processor.apvts.getRawParameterValue (ParamIds::uiScale)->load();
368+
saveUserSettings (scale, getTheme().name);
369+
}
370+
371+
void IntersectEditor::loadBrowserFiles (const std::vector<juce::File>& files)
372+
{
373+
std::vector<juce::File> validFiles;
374+
validFiles.reserve (files.size());
375+
for (const auto& file : files)
376+
if (file.existsAsFile())
377+
validFiles.push_back (file);
378+
379+
if (validFiles.empty())
380+
return;
381+
382+
const bool doAppend = processor.sampleData.isLoaded();
383+
processor.loadFilesAsync (validFiles, doAppend);
384+
if (! doAppend)
385+
{
386+
processor.zoom.store (1.0f);
387+
processor.scroll.store (0.0f);
388+
}
389+
}
390+
340391
void IntersectEditor::timerCallback()
341392
{
342393
// Apply deferred non-RT parameter restores from undo/redo.
@@ -553,6 +604,14 @@ void IntersectEditor::saveUserSettings (float scale, const juce::String& themeNa
553604
content << "nrpnChannel: " << processor.midiEditState.channel.load (std::memory_order_relaxed) << "\n";
554605
content << "nrpnBlockCc: " << (processor.midiEditState.consumeMidiEditCc.load (std::memory_order_relaxed) ? "true" : "false") << "\n";
555606
content << "middleC: " << middleCOctave << "\n";
607+
content << "sampleBrowserVisible: " << (sampleBrowserVisible ? "true" : "false") << "\n";
608+
const auto browserBookmarks = sampleBrowser.getBookmarks();
609+
if (! browserBookmarks.isEmpty())
610+
{
611+
content << "sampleBrowserBookmarks:\n";
612+
for (const auto& path : browserBookmarks)
613+
content << " - " << path << "\n";
614+
}
556615
const auto stemFolder = processor.getStemModelFolder();
557616
if (stemFolder != juce::File())
558617
content << "stemModelFolder: " << stemFolder.getFullPathName() << "\n";
@@ -564,14 +623,32 @@ void IntersectEditor::loadUserSettings()
564623
{
565624
savedScale = -1.0f;
566625
juce::String themeName = "dark";
626+
juce::StringArray browserBookmarks;
567627

568628
auto file = getUserSettingsFile();
569629
if (file.existsAsFile())
570630
{
571631
auto content = file.loadFileAsString();
632+
bool readingBrowserBookmarks = false;
572633
for (auto line : juce::StringArray::fromLines (content))
573634
{
635+
const auto rawLine = line;
574636
line = line.trim();
637+
638+
if (readingBrowserBookmarks)
639+
{
640+
if (line.startsWith ("-"))
641+
{
642+
auto path = line.fromFirstOccurrenceOf ("-", false, false).trim();
643+
if (path.isNotEmpty())
644+
browserBookmarks.addIfNotAlreadyThere (path);
645+
continue;
646+
}
647+
648+
if (! rawLine.startsWithChar (' ') && ! rawLine.startsWithChar ('\t'))
649+
readingBrowserBookmarks = false;
650+
}
651+
575652
if (line.startsWith ("uiScale:"))
576653
{
577654
float val = line.fromFirstOccurrenceOf (":", false, false).trim().getFloatValue();
@@ -603,6 +680,15 @@ void IntersectEditor::loadUserSettings()
603680
if (val == 3 || val == 4 || val == 5)
604681
middleCOctave = val;
605682
}
683+
else if (line.startsWith ("sampleBrowserVisible:"))
684+
{
685+
auto val = line.fromFirstOccurrenceOf (":", false, false).trim();
686+
sampleBrowserVisible = val == "true";
687+
}
688+
else if (line.startsWith ("sampleBrowserBookmarks:"))
689+
{
690+
readingBrowserBookmarks = true;
691+
}
606692
else if (line.startsWith ("stemModelFolder:"))
607693
{
608694
processor.setStemModelFolder (juce::File (line.fromFirstOccurrenceOf (":", false, false).trim()));
@@ -623,6 +709,8 @@ void IntersectEditor::loadUserSettings()
623709

624710
signalChainBar.middleCOctave = middleCOctave;
625711
processor.middleCOctave.store (middleCOctave, std::memory_order_relaxed);
712+
sampleBrowser.setBookmarks (browserBookmarks);
713+
sampleBrowser.setVisible (sampleBrowserVisible);
626714

627715
// Apply theme
628716
applyTheme (themeName);

src/PluginEditor.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
#include "ui/IntersectLookAndFeel.h"
55
#include "ui/HeaderBar.h"
66
#include "ui/SampleLane.h"
7+
#include "ui/SampleBrowserPanel.h"
78
#include "ui/SignalChainBar.h"
89
#include "ui/SliceLane.h"
910
#include "ui/WaveformView.h"
1011
#include "ui/ScrollZoomBar.h"
1112
#include "ui/ActionPanel.h"
1213

1314
class IntersectEditor : public juce::AudioProcessorEditor,
15+
public juce::DragAndDropContainer,
1416
private juce::Timer
1517
{
1618
public:
@@ -38,6 +40,8 @@ class IntersectEditor : public juce::AudioProcessorEditor,
3840
void timerCallback() override;
3941
void ensureDefaultThemes();
4042
void loadUserSettings();
43+
void setSampleBrowserVisible (bool shouldBeVisible);
44+
void loadBrowserFiles (const std::vector<juce::File>& files);
4145

4246
IntersectProcessor& processor;
4347
int middleCOctave = 4;
@@ -52,10 +56,12 @@ class IntersectEditor : public juce::AudioProcessorEditor,
5256
bool lastWaveformAnimating = false;
5357
bool lastPreviewActive = false;
5458
float savedScale = -1.0f;
59+
bool sampleBrowserVisible = false;
5560
uint32_t lastUiSnapshotVersion = 0;
5661
DeleteTarget deleteTarget = DeleteTarget::slice;
5762

5863
IntersectLookAndFeel lnf;
64+
SampleBrowserPanel sampleBrowser;
5965
HeaderBar headerBar;
6066
SampleLane sampleLane;
6167
SignalChainBar signalChainBar;

0 commit comments

Comments
 (0)