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