pluginbridge / Source /Plugin /PluginBridgeEditor.mm
Sitar118
feat: channel name dropdown in Editor (Sprint 6 Part 5)
6b15202
Raw
History Blame Contribute Delete
19.3 kB
#include "PluginBridgeEditor.h"
#include "PBLog.h"
static const juce::Colour kBgDark (0xff1e1e1e);
static const juce::Colour kBgPanel (0xff252525);
static const juce::Colour kBgTopBar (0xff1a1a1a);
static const juce::Colour kAccent (0xff5b9bd5);
static const juce::Colour kTextPrimary (0xffe0e0e0);
static const juce::Colour kTextSecondary(0xff888888);
static const juce::Colour kGreen (0xff5ea65e);
static const juce::Colour kRed (0xff994444);
static const juce::Colour kOrange (0xffb07a20);
PluginBridgeEditor::PluginBridgeEditor(PluginBridgeProcessor& p)
: AudioProcessorEditor(&p), processorRef(p)
{
PBLog::startSession("Editor");
PBLOG("Editor", "Constructor β€” editor opened"
+ (p.isPluginLoaded() ? " (plugin already loaded: " + p.getLoadedPluginName() + ")" : ""));
setSize(kDefaultWidth, kDefaultHeight);
setResizable(true, true);
// ── Top bar ───────────────────────────────────────────────────────────────
titleLabel.setText("PluginBridge", juce::dontSendNotification);
titleLabel.setFont(juce::FontOptions(12.0f));
titleLabel.setJustificationType(juce::Justification::centredLeft);
titleLabel.setColour(juce::Label::textColourId, juce::Colour(0xff666666));
addAndMakeVisible(titleLabel);
mcpStatusLabel.setFont(juce::FontOptions(10.0f));
mcpStatusLabel.setJustificationType(juce::Justification::centredRight);
addAndMakeVisible(mcpStatusLabel);
pluginStatusLabel.setFont(juce::FontOptions(10.0f));
pluginStatusLabel.setJustificationType(juce::Justification::centredLeft);
pluginStatusLabel.setColour(juce::Label::textColourId, kTextSecondary);
addAndMakeVisible(pluginStatusLabel);
selectButton.onClick = [this]()
{
// Toggle picker: if already open, close it
if (activePicker)
hidePicker();
else
showPluginMenu();
};
selectButton.setColour(juce::TextButton::buttonColourId, juce::Colour(0xff2d2d2d));
selectButton.setColour(juce::TextButton::textColourOffId, kTextPrimary);
selectButton.setColour(juce::ComboBox::outlineColourId, juce::Colour(0xff404040));
addAndMakeVisible(selectButton);
// ── Channel name dropdown ─────────────────────────────────────────────────
channelDropdown.addSectionHeading("── Vocals ──");
channelDropdown.addItem("Lead Vocal", 1);
channelDropdown.addItem("Backing Vocal", 2);
channelDropdown.addItem("Adlibs", 3);
channelDropdown.addSeparator();
channelDropdown.addSectionHeading("── Drums ──");
channelDropdown.addItem("Kick", 4);
channelDropdown.addItem("Snare", 5);
channelDropdown.addItem("Hi-Hat", 6);
channelDropdown.addItem("Overheads", 7);
channelDropdown.addItem("Percussion", 8);
channelDropdown.addSeparator();
channelDropdown.addSectionHeading("── Bass ──");
channelDropdown.addItem("Bass / 808", 9);
channelDropdown.addItem("Bass Guitar", 10);
channelDropdown.addItem("Sub Bass", 11);
channelDropdown.addItem("Synth Bass", 12);
channelDropdown.addSeparator();
channelDropdown.addSectionHeading("── Keys & Guitar ──");
channelDropdown.addItem("Piano / Keys", 13);
channelDropdown.addItem("Acoustic Guitar",14);
channelDropdown.addItem("Electric Guitar",15);
channelDropdown.addSeparator();
channelDropdown.addSectionHeading("── Synths ──");
channelDropdown.addItem("Synth Lead", 16);
channelDropdown.addItem("Synth Pad / Pluck", 17);
channelDropdown.addSeparator();
channelDropdown.addSectionHeading("── Strings & Brass ──");
channelDropdown.addItem("Strings", 18);
channelDropdown.addItem("Brass / Woodwind", 19);
channelDropdown.addItem("Orchestral", 20);
channelDropdown.addSeparator();
channelDropdown.addSectionHeading("── FX ──");
channelDropdown.addItem("Reverb / Delay", 21);
channelDropdown.addItem("Foley / Ambient",22);
channelDropdown.addSeparator();
channelDropdown.addSectionHeading("── Buses ──");
channelDropdown.addItem("Vocal Bus", 23);
channelDropdown.addItem("Drum Bus", 24);
channelDropdown.addItem("Guitar Bus", 25);
channelDropdown.addItem("Synth Bus", 26);
channelDropdown.addItem("Music Bus", 27);
channelDropdown.addItem("Instrument Bus", 28);
channelDropdown.addItem("Master Bus / Mix Bus", 29);
channelDropdown.addSeparator();
channelDropdown.addSectionHeading("── Custom ──");
channelDropdown.addItem("Custom...", 30);
channelDropdown.setColour(juce::ComboBox::backgroundColourId, juce::Colour(0xff2d2d2d));
channelDropdown.setColour(juce::ComboBox::textColourId, kTextPrimary);
channelDropdown.setColour(juce::ComboBox::outlineColourId, juce::Colour(0xff404040));
channelDropdown.setColour(juce::ComboBox::arrowColourId, kTextSecondary);
channelDropdown.setTextWhenNothingSelected("Select channel...");
// Restore saved channel name
channelDropdown.setText(processorRef.getChannelName(), juce::dontSendNotification);
channelDropdown.onChange = [this]()
{
auto text = channelDropdown.getText();
if (text == "Custom...")
{
auto* aw = new juce::AlertWindow("Channel Name",
"Enter a name for this channel:",
juce::MessageBoxIconType::NoIcon);
aw->addTextEditor("name", processorRef.getChannelName());
aw->addButton("OK", 1);
aw->addButton("Cancel", 0);
aw->enterModalState(true,
juce::ModalCallbackFunction::create([this, aw](int result)
{
if (result == 1)
{
auto name = aw->getTextEditorContents("name");
if (name.isNotEmpty())
{
channelDropdown.setText(name, juce::dontSendNotification);
processorRef.setChannelName(name);
}
}
delete aw;
}), true);
}
else if (text.isNotEmpty())
{
processorRef.setChannelName(text);
}
};
addAndMakeVisible(channelDropdown);
// ── Audio-only label ──────────────────────────────────────────────────────
audioOnlyLabel.setFont(juce::FontOptions(11.0f));
audioOnlyLabel.setJustificationType(juce::Justification::centred);
audioOnlyLabel.setColour(juce::Label::textColourId, kOrange);
audioOnlyLabel.setColour(juce::Label::backgroundColourId, kBgDark);
audioOnlyLabel.setVisible(false);
addChildComponent(audioOnlyLabel);
// ── Processor callbacks ───────────────────────────────────────────────────
processorRef.onPluginWillLoad = [this]()
{
PBLOG("Editor", "onPluginWillLoad β€” detaching old editor NOW (before plugin destroyed)");
detachPluginEditor();
hideAudioOnlyUI();
};
processorRef.onPluginLoaded = [this]()
{
juce::MessageManager::callAsync([this]()
{
PBLOG("Editor", "onPluginLoaded β€” "
+ juce::String(processorRef.getInProcessPlugin() ? "attaching GUI editor" : "showing audio-only UI"));
showingError = false;
hidePicker(); // close picker before showing plugin
if (processorRef.getInProcessPlugin())
attachPluginEditor();
else
showAudioOnlyUI();
});
};
processorRef.onPluginUnloaded = [this]()
{
juce::MessageManager::callAsync([this]()
{
PBLOG("Editor", "onPluginUnloaded β€” detaching and resizing");
detachPluginEditor();
hideAudioOnlyUI();
setSize(kDefaultWidth, kDefaultHeight);
});
};
processorRef.onLoadFailed = [this](const juce::String& error)
{
juce::MessageManager::callAsync([this, error]()
{
PBLOG("Editor", "onLoadFailed: " + error);
pluginStatusLabel.setText(error, juce::dontSendNotification);
bool isScanning = error.startsWith("Scanning");
pluginStatusLabel.setColour(juce::Label::textColourId,
isScanning ? kOrange : kRed);
showingError = !isScanning;
});
};
processorRef.onScanProgress = [this](const juce::String&, bool)
{
juce::MessageManager::callAsync([this]() { repaint(); });
};
// Restore GUI if plugin already loaded (editor re-opened)
if (processorRef.isPluginLoaded())
{
if (processorRef.getInProcessPlugin())
attachPluginEditor();
else
showAudioOnlyUI();
}
startTimerHz(4);
}
PluginBridgeEditor::~PluginBridgeEditor()
{
PBLOG("Editor", "Destructor β€” detaching and clearing callbacks");
stopTimer();
processorRef.onPluginWillLoad = nullptr;
processorRef.onPluginLoaded = nullptr;
processorRef.onPluginUnloaded = nullptr;
processorRef.onLoadFailed = nullptr;
processorRef.onScanProgress = nullptr;
activePicker.reset();
detachPluginEditor();
PBLOG("Editor", "Destructor β€” done");
}
// ── Paint / Resize ────────────────────────────────────────────────────────────
void PluginBridgeEditor::paint(juce::Graphics& g)
{
g.fillAll(kBgDark);
// Header (top bar + channel bar)
g.setColour(kBgTopBar);
g.fillRect(getLocalBounds().removeFromTop(kHeaderHeight));
// Separator line under header
g.setColour(juce::Colour(0xff333333));
g.fillRect(0, kHeaderHeight - 1, getWidth(), 1);
// Empty state hint (only when no picker, no plugin, no error)
if (!activePicker && !pluginEditor && !audioOnlyMode
&& !processorRef.isPluginLoaded() && !showingError)
{
g.setColour(juce::Colour(0xff444444));
g.setFont(juce::FontOptions(12.0f));
g.drawText("Click Select Plugin to browse your library",
getLocalBounds().withTrimmedTop(kHeaderHeight),
juce::Justification::centred);
}
}
void PluginBridgeEditor::resized()
{
auto area = getLocalBounds();
// Row 1: title + select button + status + MCP port
auto topBar = area.removeFromTop(kTopBarHeight).reduced(6, 4);
titleLabel.setBounds(topBar.removeFromLeft(80));
selectButton.setBounds(topBar.removeFromLeft(110).reduced(0, 2));
topBar.removeFromLeft(6);
pluginStatusLabel.setBounds(topBar.removeFromLeft(topBar.getWidth() - 64));
mcpStatusLabel.setBounds(topBar);
// Row 2: channel name dropdown (full width)
channelDropdown.setBounds(area.removeFromTop(kChannelBarHeight).reduced(6, 2));
// Content area: picker > plugin editor > audio-only (priority order)
if (activePicker)
activePicker->setBounds(area);
else if (pluginEditor)
pluginEditor->setBounds(area);
else if (audioOnlyMode)
audioOnlyLabel.setBounds(area);
}
// ── Timer ─────────────────────────────────────────────────────────────────────
void PluginBridgeEditor::timerCallback()
{
// MCP status
if (processorRef.isMcpServerRunning())
{
mcpStatusLabel.setText("MCP:" + juce::String(processorRef.getMcpServerPort()),
juce::dontSendNotification);
mcpStatusLabel.setColour(juce::Label::textColourId, kGreen);
}
else
{
mcpStatusLabel.setText("MCP:off", juce::dontSendNotification);
mcpStatusLabel.setColour(juce::Label::textColourId, kRed);
}
// Plugin status
if (processorRef.isPluginLoaded())
{
selectButton.setButtonText(processorRef.getLoadedPluginName());
if (!showingError)
{
auto suffix = audioOnlyMode ? " [audio only]" : "";
pluginStatusLabel.setText(
juce::String(processorRef.getLoadedPluginParamCount()) + " params" + suffix,
juce::dontSendNotification);
pluginStatusLabel.setColour(juce::Label::textColourId,
audioOnlyMode ? kOrange : kGreen);
}
}
else if (!showingError)
{
selectButton.setButtonText(activePicker ? "Close Browser" : "Select Plugin");
pluginStatusLabel.setText("", juce::dontSendNotification);
}
// Helper crash
if (processorRef.getConnectionState() == HelperConnection::State::Crashed)
{
detachPluginEditor();
hideAudioOnlyUI();
pluginStatusLabel.setText("Plugin crashed β€” select again to retry",
juce::dontSendNotification);
pluginStatusLabel.setColour(juce::Label::textColourId, kRed);
showingError = true;
}
}
// ── Plugin Picker (inline browser) ───────────────────────────────────────────
void PluginBridgeEditor::showPluginMenu()
{
auto files = processorRef.getInstalledPluginFiles();
auto& safetyDB = processorRef.getSafetyDB();
if (files.isEmpty())
{
juce::PopupMenu m;
m.addItem(-1, "(No plugins found)", false);
m.showMenuAsync(juce::PopupMenu::Options().withTargetComponent(&selectButton), [](int){});
return;
}
// Build entry list
std::vector<PluginPickerComponent::Entry> entries;
entries.reserve((size_t)files.size());
for (auto& f : files)
{
PluginPickerComponent::Entry e;
e.file = f;
e.name = f.getFileNameWithoutExtension();
e.manufacturer = processorRef.getPluginManufacturer(f);
e.blocked = processorRef.isBlocked(f.getFullPathName());
e.safety = safetyDB.getSafety(f.getFullPathName());
entries.push_back(std::move(e));
}
// Create picker and wire callbacks
activePicker = std::make_unique<PluginPickerComponent>(std::move(entries));
activePicker->onPluginSelected = [this](const juce::File& file)
{
PBLOG("Editor", "Picker: selected \"" + file.getFileNameWithoutExtension() + "\"");
showingError = false;
pluginStatusLabel.setText("Loading...", juce::dontSendNotification);
pluginStatusLabel.setColour(juce::Label::textColourId, kTextSecondary);
processorRef.loadPlugin(file.getFullPathName());
// picker will be removed by onPluginLoaded callback
};
activePicker->onClose = [this]() { hidePicker(); };
activePicker->onClearBlocklist = [this]()
{
PBLOG("Editor", "Picker: clearing blocklist");
processorRef.getBlocklist().clear();
};
activePicker->onClearSafetyCache = [this]()
{
PBLOG("Editor", "Picker: clearing safety cache β€” re-scanning all plugins");
auto& db = processorRef.getSafetyDB();
for (auto& f : processorRef.getInstalledPluginFiles())
db.setSafety(f.getFullPathName(), PluginSafetyDB::Safety::Unknown);
processorRef.startBackgroundScan();
};
// Hide plugin editor / audio-only during browse (they'll come back on load)
if (pluginEditor) pluginEditor->setVisible(false);
if (audioOnlyMode) audioOnlyLabel.setVisible(false);
addAndMakeVisible(activePicker.get());
// Resize editor to show the picker
int newW = std::max(getWidth(), PluginPickerComponent::kWidth);
int newH = kHeaderHeight + PluginPickerComponent::kHeight;
setSize(newW, newH);
PBLOG("Editor", "showPluginMenu: picker opened");
}
void PluginBridgeEditor::hidePicker()
{
if (!activePicker) return;
PBLOG("Editor", "hidePicker: closing browser");
removeChildComponent(activePicker.get());
activePicker.reset();
// Restore whatever was visible before
if (pluginEditor)
{
pluginEditor->setVisible(true);
setSize(std::max(pluginEditor->getWidth(), kDefaultWidth),
pluginEditor->getHeight() + kHeaderHeight);
}
else if (audioOnlyMode)
{
audioOnlyLabel.setVisible(true);
setSize(kDefaultWidth, kHeaderHeight + kAudioOnlyHeight);
}
else
{
setSize(kDefaultWidth, kDefaultHeight);
}
repaint();
}
// ── In-process plugin editor ──────────────────────────────────────────────────
void PluginBridgeEditor::attachPluginEditor()
{
PBLOG("Editor", "attachPluginEditor: start");
detachPluginEditor();
hideAudioOnlyUI();
auto* plugin = processorRef.getInProcessPlugin();
if (!plugin || !plugin->hasEditor())
{
PBLOG("Editor", "attachPluginEditor: no plugin or no editor β€” abort");
return;
}
PBLOG("Editor", "attachPluginEditor: calling createEditor() on \"" + plugin->getName() + "\"");
pluginEditor.reset(plugin->createEditor());
if (!pluginEditor)
{
PBLOG("Editor", "attachPluginEditor: createEditor() returned null");
return;
}
addAndMakeVisible(pluginEditor.get());
int w = std::max(pluginEditor->getWidth(), kDefaultWidth);
int h = pluginEditor->getHeight() + kHeaderHeight;
PBLOG("Editor", "attachPluginEditor: SUCCESS β€” size " + juce::String(w) + "x" + juce::String(h));
setSize(w, h);
}
void PluginBridgeEditor::detachPluginEditor()
{
if (pluginEditor)
{
PBLOG("Editor", "detachPluginEditor: removing editor component");
removeChildComponent(pluginEditor.get());
pluginEditor.reset();
PBLOG("Editor", "detachPluginEditor: done");
}
}
// ── Audio-only fallback UI ────────────────────────────────────────────────────
void PluginBridgeEditor::showAudioOnlyUI()
{
detachPluginEditor();
audioOnlyMode = true;
auto name = processorRef.getLoadedPluginName();
audioOnlyLabel.setText(
name + "\n\nGUI unavailable β€” plugin failed safety scan.\n"
"Audio processing active. Use MCP or automation for parameters.",
juce::dontSendNotification);
audioOnlyLabel.setVisible(true);
setSize(kDefaultWidth, kHeaderHeight + kAudioOnlyHeight);
}
void PluginBridgeEditor::hideAudioOnlyUI()
{
audioOnlyMode = false;
audioOnlyLabel.setVisible(false);
}