#include "PluginBridgeEditor.h" static const juce::Colour kBgDark (0xff0d0d1a); static const juce::Colour kBgPanel (0xff1a1a2e); static const juce::Colour kAccent (0xff6c63ff); static const juce::Colour kTextPrimary (0xffe8e8f0); static const juce::Colour kTextSecondary(0xff8888aa); static const juce::Colour kGreen (0xff4cef50); static const juce::Colour kRed (0xffef5350); PluginBridgeEditor::PluginBridgeEditor(PluginBridgeProcessor& p) : AudioProcessorEditor(&p), processorRef(p) { setSize(kDefaultWidth, kDefaultHeight); setResizable(true, true); // Title titleLabel.setText("PluginBridge", juce::dontSendNotification); titleLabel.setFont(juce::FontOptions(14.0f)); titleLabel.setJustificationType(juce::Justification::centredLeft); titleLabel.setColour(juce::Label::textColourId, kTextPrimary); addAndMakeVisible(titleLabel); // MCP status mcpStatusLabel.setFont(juce::FontOptions(11.0f)); mcpStatusLabel.setJustificationType(juce::Justification::centredRight); addAndMakeVisible(mcpStatusLabel); // Plugin name pluginStatusLabel.setFont(juce::FontOptions(11.0f)); pluginStatusLabel.setJustificationType(juce::Justification::centredLeft); pluginStatusLabel.setColour(juce::Label::textColourId, kTextSecondary); addAndMakeVisible(pluginStatusLabel); // Select button selectButton.onClick = [this]() { showPluginMenu(); }; selectButton.setColour(juce::TextButton::buttonColourId, kAccent); selectButton.setColour(juce::TextButton::textColourOffId, kTextPrimary); addAndMakeVisible(selectButton); // Callbacks from processor processorRef.onPluginLoaded = [this]() { juce::MessageManager::callAsync([this]() { embedPluginEditor(); }); }; processorRef.onPluginUnloaded = [this]() { juce::MessageManager::callAsync([this]() { removePluginEditor(); }); }; processorRef.onLoadFailed = [this](const juce::String& error) { juce::MessageManager::callAsync([this, error]() { pluginStatusLabel.setText(error, juce::dontSendNotification); pluginStatusLabel.setColour(juce::Label::textColourId, kRed); }); }; // If plugin already loaded (e.g. restoring state), embed it if (processorRef.isPluginLoaded()) embedPluginEditor(); startTimerHz(4); } PluginBridgeEditor::~PluginBridgeEditor() { stopTimer(); processorRef.onPluginLoaded = nullptr; processorRef.onPluginUnloaded = nullptr; processorRef.onLoadFailed = nullptr; removePluginEditor(); } void PluginBridgeEditor::paint(juce::Graphics& g) { g.fillAll(kBgDark); // Top bar background auto topBar = getLocalBounds().removeFromTop(kTopBarHeight); g.setColour(kBgPanel); g.fillRect(topBar); // Separator line g.setColour(kAccent.withAlpha(0.3f)); g.drawHorizontalLine(kTopBarHeight - 1, 0.0f, (float)getWidth()); } void PluginBridgeEditor::resized() { auto area = getLocalBounds(); auto topBar = area.removeFromTop(kTopBarHeight).reduced(6, 4); // Top bar layout titleLabel.setBounds(topBar.removeFromLeft(90)); selectButton.setBounds(topBar.removeFromLeft(110).reduced(0, 2)); topBar.removeFromLeft(6); pluginStatusLabel.setBounds(topBar.removeFromLeft(topBar.getWidth() - 70)); mcpStatusLabel.setBounds(topBar); // Plugin editor fills the rest if (hostedEditor) hostedEditor->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()); pluginStatusLabel.setText(juce::String(processorRef.getLoadedPluginParamCount()) + " params", juce::dontSendNotification); pluginStatusLabel.setColour(juce::Label::textColourId, kGreen); } else if (pluginStatusLabel.getColour(juce::Label::textColourId) != kRed) { selectButton.setButtonText("Select Plugin"); pluginStatusLabel.setText("No plugin loaded", juce::dontSendNotification); pluginStatusLabel.setColour(juce::Label::textColourId, kTextSecondary); } } void PluginBridgeEditor::embedPluginEditor() { removePluginEditor(); auto* editor = processorRef.createHostedPluginEditor(); if (!editor) return; hostedEditor.reset(editor); addAndMakeVisible(hostedEditor.get()); // Resize PluginBridge editor to fit the hosted plugin's GUI + top bar auto editorWidth = std::max(hostedEditor->getWidth(), kDefaultWidth); auto editorHeight = hostedEditor->getHeight() + kTopBarHeight; setSize(editorWidth, editorHeight); } void PluginBridgeEditor::removePluginEditor() { if (hostedEditor) { removeChildComponent(hostedEditor.get()); hostedEditor.reset(); setSize(kDefaultWidth, kDefaultHeight); } } void PluginBridgeEditor::showPluginMenu() { auto files = processorRef.getInstalledPluginFiles(); juce::PopupMenu menu; if (files.isEmpty()) { menu.addItem(-1, "(No plugins found)", false); menu.showMenuAsync(juce::PopupMenu::Options().withTargetComponent(&selectButton), [](int) {}); return; } juce::PopupMenu vst3Menu; juce::PopupMenu auMenu; int itemId = 1; for (int i = 0; i < files.size(); ++i) { const auto& file = files[i]; auto name = file.getFileNameWithoutExtension(); auto path = file.getFullPathName(); bool blocked = processorRef.isBlocked(path); if (file.getFileExtension() == ".vst3") vst3Menu.addItem(itemId, blocked ? name + " (not compatible)" : name, !blocked); else auMenu.addItem(itemId, blocked ? name + " (not compatible)" : name, !blocked); itemId++; } if (vst3Menu.getNumItems() > 0) menu.addSubMenu("VST3", vst3Menu); if (auMenu.getNumItems() > 0) menu.addSubMenu("Audio Units", auMenu); menu.addSeparator(); menu.addItem(9999, "Clear blocklist"); menu.showMenuAsync(juce::PopupMenu::Options().withTargetComponent(&selectButton), [this, files](int result) { if (result == 9999) { processorRef.getBlocklist().clear(); return; } if (result > 0 && result <= files.size()) { pluginStatusLabel.setText("Loading...", juce::dontSendNotification); pluginStatusLabel.setColour(juce::Label::textColourId, kTextSecondary); processorRef.loadPlugin(files[result - 1].getFullPathName()); } }); }