/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2017 - ROLI Ltd. JUCE is an open source library subject to commercial or open-source licensing. By using JUCE, you agree to the terms of both the JUCE 5 End-User License Agreement and JUCE 5 Privacy Policy (both updated and effective as of the 27th April 2017). End User License Agreement: www.juce.com/juce-5-licence Privacy Policy: www.juce.com/juce-5-privacy-policy Or: You may also use this code under the terms of the GPL v3 (see www.gnu.org/licenses). JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ #include "../JuceLibraryCode/JuceHeader.h" #include "GraphEditorPanel.h" #include "InternalFilters.h" #include "MainHostWindow.h" //============================================================================== struct GraphEditorPanel::PinComponent : public Component, public SettableTooltipClient { PinComponent (GraphEditorPanel& p, AudioProcessorGraph::NodeAndChannel pinToUse, bool isIn) : panel (p), graph (p.graph), pin (pinToUse), isInput (isIn) { if (auto node = graph.graph.getNodeForId (pin.nodeID)) { String tip; if (pin.isMIDI()) { tip = isInput ? "MIDI Input" : "MIDI Output"; } else { auto& processor = *node->getProcessor(); auto channel = processor.getOffsetInBusBufferForAbsoluteChannelIndex (isInput, pin.channelIndex, busIdx); if (auto* bus = processor.getBus (isInput, busIdx)) tip = bus->getName() + ": " + AudioChannelSet::getAbbreviatedChannelTypeName (bus->getCurrentLayout().getTypeOfChannel (channel)); else tip = (isInput ? "Main Input: " : "Main Output: ") + String (pin.channelIndex + 1); } setTooltip (tip); } setSize (16, 16); } void paint (Graphics& g) override { auto w = (float) getWidth(); auto h = (float) getHeight(); Path p; p.addEllipse (w * 0.25f, h * 0.25f, w * 0.5f, h * 0.5f); p.addRectangle (w * 0.4f, isInput ? (0.5f * h) : 0.0f, w * 0.2f, h * 0.5f); auto colour = (pin.isMIDI() ? Colours::red : Colours::green); g.setColour (colour.withRotatedHue (busIdx / 5.0f)); g.fillPath (p); } void mouseDown (const MouseEvent& e) override { AudioProcessorGraph::NodeAndChannel dummy { 0, 0 }; panel.beginConnectorDrag (isInput ? dummy : pin, isInput ? pin : dummy, e); } void mouseDrag (const MouseEvent& e) override { panel.dragConnector (e); } void mouseUp (const MouseEvent& e) override { panel.endDraggingConnector (e); } GraphEditorPanel& panel; FilterGraph& graph; AudioProcessorGraph::NodeAndChannel pin; const bool isInput; int busIdx = 0; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PinComponent) }; //============================================================================== struct GraphEditorPanel::FilterComponent : public Component { FilterComponent (GraphEditorPanel& p, uint32 id) : panel (p), graph (p.graph), pluginID (id) { shadow.setShadowProperties (DropShadow (Colours::black.withAlpha (0.5f), 3, { 0, 1 })); setComponentEffect (&shadow); setSize (150, 60); } FilterComponent (const FilterComponent&) = delete; FilterComponent& operator= (const FilterComponent&) = delete; void mouseDown (const MouseEvent& e) override { originalPos = localPointToGlobal (Point()); toFront (true); if (e.mods.isPopupMenu()) showPopupMenu(); } void mouseDrag (const MouseEvent& e) override { if (! e.mods.isPopupMenu()) { auto pos = originalPos + e.getOffsetFromDragStart(); if (getParentComponent() != nullptr) pos = getParentComponent()->getLocalPoint (nullptr, pos); pos += getLocalBounds().getCentre(); graph.setNodePosition (pluginID, { pos.x / (double) getParentWidth(), pos.y / (double) getParentHeight() }); panel.updateComponents(); } } void mouseUp (const MouseEvent& e) override { if (e.mouseWasDraggedSinceMouseDown()) { graph.setChangedFlag (true); } else if (e.getNumberOfClicks() == 2) { if (auto f = graph.graph.getNodeForId (pluginID)) if (auto* w = graph.getOrCreateWindowFor (f, PluginWindow::Type::normal)) w->toFront (true); } } bool hitTest (int x, int y) override { for (auto* child : getChildren()) if (child->getBounds().contains (x, y)) return true; return x >= 3 && x < getWidth() - 6 && y >= pinSize && y < getHeight() - pinSize; } void paint (Graphics& g) override { auto boxArea = getLocalBounds().reduced (4, pinSize); g.setColour (findColour (TextEditor::backgroundColourId)); g.fillRect (boxArea.toFloat()); g.setColour (findColour (TextEditor::textColourId)); g.setFont (font); g.drawFittedText (getName(), boxArea, Justification::centred, 2); } void resized() override { if (auto f = graph.graph.getNodeForId (pluginID)) { if (auto* processor = f->getProcessor()) { for (auto* pin : pins) { const bool isInput = pin->isInput; auto channelIndex = pin->pin.channelIndex; int busIdx = 0; processor->getOffsetInBusBufferForAbsoluteChannelIndex (isInput, channelIndex, busIdx); const int total = isInput ? numIns : numOuts; const int index = pin->pin.isMIDI() ? (total - 1) : channelIndex; auto totalSpaces = static_cast (total) + (static_cast (jmax (0, processor->getBusCount (isInput) - 1)) * 0.5f); auto indexPos = static_cast (index) + (static_cast (busIdx) * 0.5f); pin->setBounds (proportionOfWidth ((1.0f + indexPos) / (totalSpaces + 1.0f)) - pinSize / 2, pin->isInput ? 0 : (getHeight() - pinSize), pinSize, pinSize); } } } } Point getPinPos (int index, bool isInput) const { for (auto* pin : pins) if (pin->pin.channelIndex == index && isInput == pin->isInput) return getPosition().toFloat() + pin->getBounds().getCentre().toFloat(); return {}; } void update() { const AudioProcessorGraph::Node::Ptr f (graph.graph.getNodeForId (pluginID)); jassert (f != nullptr); numIns = f->getProcessor()->getTotalNumInputChannels(); if (f->getProcessor()->acceptsMidi()) ++numIns; numOuts = f->getProcessor()->getTotalNumOutputChannels(); if (f->getProcessor()->producesMidi()) ++numOuts; int w = 100; int h = 60; w = jmax (w, (jmax (numIns, numOuts) + 1) * 20); const int textWidth = font.getStringWidth (f->getProcessor()->getName()); w = jmax (w, 16 + jmin (textWidth, 300)); if (textWidth > 300) h = 100; setSize (w, h); setName (f->getProcessor()->getName()); { auto p = graph.getNodePosition (pluginID); setCentreRelative ((float) p.x, (float) p.y); } if (numIns != numInputs || numOuts != numOutputs) { numInputs = numIns; numOutputs = numOuts; pins.clear(); for (int i = 0; i < f->getProcessor()->getTotalNumInputChannels(); ++i) addAndMakeVisible (pins.add (new PinComponent (panel, { pluginID, i }, true))); if (f->getProcessor()->acceptsMidi()) addAndMakeVisible (pins.add (new PinComponent (panel, { pluginID, AudioProcessorGraph::midiChannelIndex }, true))); for (int i = 0; i < f->getProcessor()->getTotalNumOutputChannels(); ++i) addAndMakeVisible (pins.add (new PinComponent (panel, { pluginID, i }, false))); if (f->getProcessor()->producesMidi()) addAndMakeVisible (pins.add (new PinComponent (panel, { pluginID, AudioProcessorGraph::midiChannelIndex }, false))); resized(); } } AudioProcessor* getProcessor() const { if (auto node = graph.graph.getNodeForId (pluginID)) return node->getProcessor(); return {}; } void showPopupMenu() { PopupMenu m; m.addItem (1, "Delete this filter"); m.addItem (2, "Disconnect all pins"); m.addSeparator(); m.addItem (10, "Show plugin GUI"); m.addItem (11, "Show all programs"); m.addItem (12, "Show all parameters"); m.addSeparator(); m.addItem (20, "Configure Audio I/O"); m.addItem (21, "Test state save/load"); switch (m.show()) { case 1: graph.graph.removeNode (pluginID); break; case 2: graph.graph.disconnectNode (pluginID); break; case 10: showWindow (PluginWindow::Type::normal); break; case 11: showWindow (PluginWindow::Type::programs); break; case 12: showWindow (PluginWindow::Type::generic); break; case 20: showWindow (PluginWindow::Type::audioIO); break; case 21: testStateSaveLoad(); break; default: break; } } void testStateSaveLoad() { if (auto* processor = getProcessor()) { MemoryBlock state; processor->getStateInformation (state); processor->setStateInformation (state.getData(), (int) state.getSize()); } } void showWindow (PluginWindow::Type type) { if (auto node = graph.graph.getNodeForId (pluginID)) if (auto* w = graph.getOrCreateWindowFor (node, type)) w->toFront (true); } GraphEditorPanel& panel; FilterGraph& graph; const AudioProcessorGraph::NodeID pluginID; OwnedArray pins; int numInputs = 0, numOutputs = 0; int pinSize = 16; Point originalPos; Font font { 13.0f, Font::bold }; int numIns = 0, numOuts = 0; DropShadowEffect shadow; }; //============================================================================== struct GraphEditorPanel::ConnectorComponent : public Component, public SettableTooltipClient { ConnectorComponent (GraphEditorPanel& p) : panel (p), graph (p.graph) { setAlwaysOnTop (true); } void setInput (AudioProcessorGraph::NodeAndChannel newSource) { if (connection.source != newSource) { connection.source = newSource; update(); } } void setOutput (AudioProcessorGraph::NodeAndChannel newDest) { if (connection.destination != newDest) { connection.destination = newDest; update(); } } void dragStart (Point pos) { lastInputPos = pos; resizeToFit(); } void dragEnd (Point pos) { lastOutputPos = pos; resizeToFit(); } void update() { Point p1, p2; getPoints (p1, p2); if (lastInputPos != p1 || lastOutputPos != p2) resizeToFit(); } void resizeToFit() { Point p1, p2; getPoints (p1, p2); auto newBounds = Rectangle (p1, p2).expanded (4.0f).getSmallestIntegerContainer(); if (newBounds != getBounds()) setBounds (newBounds); else resized(); repaint(); } void getPoints (Point& p1, Point& p2) const { p1 = lastInputPos; p2 = lastOutputPos; if (auto* src = panel.getComponentForFilter (connection.source.nodeID)) p1 = src->getPinPos (connection.source.channelIndex, false); if (auto* dest = panel.getComponentForFilter (connection.destination.nodeID)) p2 = dest->getPinPos (connection.destination.channelIndex, true); } void paint (Graphics& g) override { if (connection.source.isMIDI() || connection.destination.isMIDI()) g.setColour (Colours::red); else g.setColour (Colours::green); g.fillPath (linePath); } bool hitTest (int x, int y) override { auto pos = Point (x, y).toFloat(); if (hitPath.contains (pos)) { double distanceFromStart, distanceFromEnd; getDistancesFromEnds (pos, distanceFromStart, distanceFromEnd); // avoid clicking the connector when over a pin return distanceFromStart > 7.0 && distanceFromEnd > 7.0; } return false; } void mouseDown (const MouseEvent&) override { dragging = false; } void mouseDrag (const MouseEvent& e) override { if (dragging) { panel.dragConnector (e); } else if (e.mouseWasDraggedSinceMouseDown()) { dragging = true; graph.graph.removeConnection (connection); double distanceFromStart, distanceFromEnd; getDistancesFromEnds (getPosition().toFloat() + e.position, distanceFromStart, distanceFromEnd); const bool isNearerSource = (distanceFromStart < distanceFromEnd); AudioProcessorGraph::NodeAndChannel dummy { 0, 0 }; panel.beginConnectorDrag (isNearerSource ? dummy : connection.source, isNearerSource ? connection.destination : dummy, e); } } void mouseUp (const MouseEvent& e) override { if (dragging) panel.endDraggingConnector (e); } void resized() override { Point p1, p2; getPoints (p1, p2); lastInputPos = p1; lastOutputPos = p2; p1 -= getPosition().toFloat(); p2 -= getPosition().toFloat(); linePath.clear(); linePath.startNewSubPath (p1); linePath.cubicTo (p1.x, p1.y + (p2.y - p1.y) * 0.33f, p2.x, p1.y + (p2.y - p1.y) * 0.66f, p2.x, p2.y); PathStrokeType wideStroke (8.0f); wideStroke.createStrokedPath (hitPath, linePath); PathStrokeType stroke (2.5f); stroke.createStrokedPath (linePath, linePath); auto arrowW = 5.0f; auto arrowL = 4.0f; Path arrow; arrow.addTriangle (-arrowL, arrowW, -arrowL, -arrowW, arrowL, 0.0f); arrow.applyTransform (AffineTransform() .rotated (MathConstants::halfPi - (float) atan2 (p2.x - p1.x, p2.y - p1.y)) .translated ((p1 + p2) * 0.5f)); linePath.addPath (arrow); linePath.setUsingNonZeroWinding (true); } void getDistancesFromEnds (Point p, double& distanceFromStart, double& distanceFromEnd) const { Point p1, p2; getPoints (p1, p2); distanceFromStart = p1.getDistanceFrom (p); distanceFromEnd = p2.getDistanceFrom (p); } GraphEditorPanel& panel; FilterGraph& graph; AudioProcessorGraph::Connection connection { { 0, 0 }, { 0, 0 } }; Point lastInputPos, lastOutputPos; Path linePath, hitPath; bool dragging = false; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ConnectorComponent) }; //============================================================================== GraphEditorPanel::GraphEditorPanel (FilterGraph& g) : graph (g) { graph.addChangeListener (this); setOpaque (true); } GraphEditorPanel::~GraphEditorPanel() { graph.removeChangeListener (this); draggingConnector = nullptr; nodes.clear(); connectors.clear(); } void GraphEditorPanel::paint (Graphics& g) { g.fillAll (getLookAndFeel().findColour (ResizableWindow::backgroundColourId)); } void GraphEditorPanel::mouseDown (const MouseEvent& e) { if (e.mods.isPopupMenu()) { PopupMenu m; if (auto* mainWindow = findParentComponentOfClass()) { mainWindow->addPluginsToMenu (m); auto r = m.show(); if (auto* desc = mainWindow->getChosenType (r)) createNewPlugin (*desc, e.position.toInt()); } } } void GraphEditorPanel::createNewPlugin (const PluginDescription& desc, Point position) { graph.addPlugin (desc, position.toDouble() / Point ((double) getWidth(), (double) getHeight())); } GraphEditorPanel::FilterComponent* GraphEditorPanel::getComponentForFilter (const uint32 filterID) const { for (auto* fc : nodes) if (fc->pluginID == filterID) return fc; return nullptr; } GraphEditorPanel::ConnectorComponent* GraphEditorPanel::getComponentForConnection (const AudioProcessorGraph::Connection& conn) const { for (auto* cc : connectors) if (cc->connection == conn) return cc; return nullptr; } GraphEditorPanel::PinComponent* GraphEditorPanel::findPinAt (Point pos) const { for (auto* fc : nodes) { // NB: A Visual Studio optimiser error means we have to put this Component* in a local // variable before trying to cast it, or it gets mysteriously optimised away.. auto* comp = fc->getComponentAt (pos.toInt() - fc->getPosition()); if (auto* pin = dynamic_cast (comp)) return pin; } return nullptr; } void GraphEditorPanel::resized() { updateComponents(); } void GraphEditorPanel::changeListenerCallback (ChangeBroadcaster*) { updateComponents(); } void GraphEditorPanel::updateComponents() { for (int i = nodes.size(); --i >= 0;) if (graph.graph.getNodeForId (nodes.getUnchecked(i)->pluginID) == nullptr) nodes.remove (i); for (int i = connectors.size(); --i >= 0;) if (! graph.graph.isConnected (connectors.getUnchecked(i)->connection)) connectors.remove (i); for (auto* fc : nodes) fc->update(); for (auto* cc : connectors) cc->update(); for (auto* f : graph.graph.getNodes()) { if (getComponentForFilter (f->nodeID) == 0) { auto* comp = nodes.add (new FilterComponent (*this, f->nodeID)); addAndMakeVisible (comp); comp->update(); } } for (auto& c : graph.graph.getConnections()) { if (getComponentForConnection (c) == 0) { auto* comp = connectors.add (new ConnectorComponent (*this)); addAndMakeVisible (comp); comp->setInput (c.source); comp->setOutput (c.destination); } } } void GraphEditorPanel::beginConnectorDrag (AudioProcessorGraph::NodeAndChannel source, AudioProcessorGraph::NodeAndChannel dest, const MouseEvent& e) { auto* c = dynamic_cast (e.originalComponent); connectors.removeObject (c, false); draggingConnector = c; if (draggingConnector == nullptr) draggingConnector = new ConnectorComponent (*this); draggingConnector->setInput (source); draggingConnector->setOutput (dest); addAndMakeVisible (draggingConnector); draggingConnector->toFront (false); dragConnector (e); } void GraphEditorPanel::dragConnector (const MouseEvent& e) { auto e2 = e.getEventRelativeTo (this); if (draggingConnector != nullptr) { draggingConnector->setTooltip ({}); auto pos = e2.position; if (auto* pin = findPinAt (pos)) { auto connection = draggingConnector->connection; if (connection.source.nodeID == 0 && ! pin->isInput) { connection.source = pin->pin; } else if (connection.destination.nodeID == 0 && pin->isInput) { connection.destination = pin->pin; } if (graph.graph.canConnect (connection)) { pos = (pin->getParentComponent()->getPosition() + pin->getBounds().getCentre()).toFloat(); draggingConnector->setTooltip (pin->getTooltip()); } } if (draggingConnector->connection.source.nodeID == 0) draggingConnector->dragStart (pos); else draggingConnector->dragEnd (pos); } } void GraphEditorPanel::endDraggingConnector (const MouseEvent& e) { if (draggingConnector == nullptr) return; draggingConnector->setTooltip ({}); auto e2 = e.getEventRelativeTo (this); auto connection = draggingConnector->connection; draggingConnector = nullptr; if (auto* pin = findPinAt (e2.position)) { if (connection.source.nodeID == 0) { if (pin->isInput) return; connection.source = pin->pin; } else { if (! pin->isInput) return; connection.destination = pin->pin; } graph.graph.addConnection (connection); } } //============================================================================== struct GraphDocumentComponent::TooltipBar : public Component, private Timer { TooltipBar() { startTimer (100); } void paint (Graphics& g) override { g.setFont (Font (getHeight() * 0.7f, Font::bold)); g.setColour (Colours::black); g.drawFittedText (tip, 10, 0, getWidth() - 12, getHeight(), Justification::centredLeft, 1); } void timerCallback() override { String newTip; if (auto* underMouse = Desktop::getInstance().getMainMouseSource().getComponentUnderMouse()) if (auto* ttc = dynamic_cast (underMouse)) if (! (underMouse->isMouseButtonDown() || underMouse->isCurrentlyBlockedByAnotherModalComponent())) newTip = ttc->getTooltip(); if (newTip != tip) { tip = newTip; repaint(); } } String tip; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TooltipBar) }; //============================================================================== GraphDocumentComponent::GraphDocumentComponent (AudioPluginFormatManager& fm, AudioDeviceManager& dm) : graph (new FilterGraph (fm)), deviceManager (dm), graphPlayer (getAppProperties().getUserSettings()->getBoolValue ("doublePrecisionProcessing", false)) { addAndMakeVisible (graphPanel = new GraphEditorPanel (*graph)); deviceManager.addChangeListener (graphPanel); graphPlayer.setProcessor (&graph->graph); keyState.addListener (&graphPlayer.getMidiMessageCollector()); addAndMakeVisible (keyboardComp = new MidiKeyboardComponent (keyState, MidiKeyboardComponent::horizontalKeyboard)); addAndMakeVisible (statusBar = new TooltipBar()); deviceManager.addAudioCallback (&graphPlayer); deviceManager.addMidiInputCallback (String(), &graphPlayer.getMidiMessageCollector()); graphPanel->updateComponents(); } GraphDocumentComponent::~GraphDocumentComponent() { releaseGraph(); keyState.removeListener (&graphPlayer.getMidiMessageCollector()); } void GraphDocumentComponent::resized() { const int keysHeight = 60; const int statusHeight = 20; graphPanel->setBounds (0, 0, getWidth(), getHeight() - keysHeight); statusBar->setBounds (0, getHeight() - keysHeight - statusHeight, getWidth(), statusHeight); keyboardComp->setBounds (0, getHeight() - keysHeight, getWidth(), keysHeight); } void GraphDocumentComponent::createNewPlugin (const PluginDescription& desc, Point pos) { graphPanel->createNewPlugin (desc, pos); } void GraphDocumentComponent::unfocusKeyboardComponent() { keyboardComp->unfocusAllComponents(); } void GraphDocumentComponent::releaseGraph() { deviceManager.removeAudioCallback (&graphPlayer); deviceManager.removeMidiInputCallback (String(), &graphPlayer.getMidiMessageCollector()); if (graphPanel != nullptr) { deviceManager.removeChangeListener (graphPanel); graphPanel = nullptr; } keyboardComp = nullptr; statusBar = nullptr; graphPlayer.setProcessor (nullptr); graph = nullptr; } void GraphDocumentComponent::setDoublePrecision (bool doublePrecision) { graphPlayer.setDoublePrecisionProcessing (doublePrecision); } bool GraphDocumentComponent::closeAnyOpenPluginWindows() { return graphPanel->graph.closeAnyOpenPluginWindows(); }