| #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); |
|
|
| |
| 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]() |
| { |
| |
| 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); |
|
|
| |
| 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..."); |
|
|
| |
| 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); |
|
|
| |
| 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); |
|
|
| |
| 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(); |
| 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(); }); |
| }; |
|
|
| |
| 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"); |
| } |
|
|
| |
|
|
| void PluginBridgeEditor::paint(juce::Graphics& g) |
| { |
| g.fillAll(kBgDark); |
|
|
| |
| g.setColour(kBgTopBar); |
| g.fillRect(getLocalBounds().removeFromTop(kHeaderHeight)); |
|
|
| |
| g.setColour(juce::Colour(0xff333333)); |
| g.fillRect(0, kHeaderHeight - 1, getWidth(), 1); |
|
|
| |
| 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(); |
|
|
| |
| 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); |
|
|
| |
| channelDropdown.setBounds(area.removeFromTop(kChannelBarHeight).reduced(6, 2)); |
|
|
| |
| if (activePicker) |
| activePicker->setBounds(area); |
| else if (pluginEditor) |
| pluginEditor->setBounds(area); |
| else if (audioOnlyMode) |
| audioOnlyLabel.setBounds(area); |
| } |
|
|
| |
|
|
| void PluginBridgeEditor::timerCallback() |
| { |
| |
| 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); |
| } |
|
|
| |
| 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); |
| } |
|
|
| |
| 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; |
| } |
| } |
|
|
| |
|
|
| 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; |
| } |
|
|
| |
| 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)); |
| } |
|
|
| |
| 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()); |
| |
| }; |
|
|
| 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(); |
| }; |
|
|
| |
| if (pluginEditor) pluginEditor->setVisible(false); |
| if (audioOnlyMode) audioOnlyLabel.setVisible(false); |
|
|
| addAndMakeVisible(activePicker.get()); |
|
|
| |
| 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(); |
|
|
| |
| 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(); |
| } |
|
|
| |
|
|
| 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"); |
| } |
| } |
|
|
| |
|
|
| 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); |
| } |
|
|