Download Source/Plugin/PluginBridgeEditor.mm from RAM2118/pluginbridge: direct link, hf CLI and curl.
- Browser
- Download file 19.3 kB
-
https://huggingface.co/RAM2118/pluginbridge/resolve/main/Source/Plugin/PluginBridgeEditor.mm
- Command line
-
hf download hf://RAM2118/pluginbridge/Source/Plugin/PluginBridgeEditor.mm
-
curl -L -o PluginBridgeEditor.mm https://huggingface.co/RAM2118/pluginbridge/resolve/main/Source/Plugin/PluginBridgeEditor.mm
19.3 kB
| 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); | |
| } | |