/* ============================================================================== This file is part of the JUCE 6 technical preview. Copyright (c) 2020 - Raw Material Software Limited You may use this code under the terms of the GPL v3 (see www.gnu.org/licenses). For this technical preview, this file is not subject to commercial licensing. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ #include #include "GraphEditorPanel.h" #include "../Plugins/InternalPlugins.h" #include "MainHostWindow.h" //============================================================================== #if JUCE_IOS class AUScanner { public: AUScanner (KnownPluginList& list) : knownPluginList (list), pool (5) { knownPluginList.clearBlacklistedFiles(); paths = formatToScan.getDefaultLocationsToSearch(); startScan(); } private: KnownPluginList& knownPluginList; AudioUnitPluginFormat formatToScan; std::unique_ptr scanner; FileSearchPath paths; ThreadPool pool; void startScan() { auto deadMansPedalFile = getAppProperties().getUserSettings() ->getFile().getSiblingFile ("RecentlyCrashedPluginsList"); scanner.reset (new PluginDirectoryScanner (knownPluginList, formatToScan, paths, true, deadMansPedalFile, true)); for (int i = 5; --i >= 0;) pool.addJob (new ScanJob (*this), true); } bool doNextScan() { String pluginBeingScanned; if (scanner->scanNextFile (true, pluginBeingScanned)) return true; return false; } struct ScanJob : public ThreadPoolJob { ScanJob (AUScanner& s) : ThreadPoolJob ("pluginscan"), scanner (s) {} JobStatus runJob() { while (scanner.doNextScan() && ! shouldExit()) {} return jobHasFinished; } AUScanner& scanner; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ScanJob) }; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AUScanner) }; #endif //============================================================================== 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 }; 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; PluginGraph& graph; AudioProcessorGraph::NodeAndChannel pin; const bool isInput; int busIdx = 0; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PinComponent) }; //============================================================================== struct GraphEditorPanel::PluginComponent : public Component, public Timer, private AudioProcessorParameter::Listener { PluginComponent (GraphEditorPanel& p, AudioProcessorGraph::NodeID id) : panel (p), graph (p.graph), pluginID (id) { shadow.setShadowProperties (DropShadow (Colours::black.withAlpha (0.5f), 3, { 0, 1 })); setComponentEffect (&shadow); if (auto f = graph.graph.getNodeForId (pluginID)) { if (auto* processor = f->getProcessor()) { if (auto* bypassParam = processor->getBypassParameter()) bypassParam->addListener (this); } } setSize (150, 60); } PluginComponent (const PluginComponent&) = delete; PluginComponent& operator= (const PluginComponent&) = delete; ~PluginComponent() override { if (auto f = graph.graph.getNodeForId (pluginID)) { if (auto* processor = f->getProcessor()) { if (auto* bypassParam = processor->getBypassParameter()) bypassParam->removeListener (this); } } } void mouseDown (const MouseEvent& e) override { originalPos = localPointToGlobal (Point()); toFront (true); if (isOnTouchDevice()) { startTimer (750); } else { if (e.mods.isPopupMenu()) showPopupMenu(); } } void mouseDrag (const MouseEvent& e) override { if (isOnTouchDevice() && e.getDistanceFromDragStart() > 5) stopTimer(); 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 (isOnTouchDevice()) { stopTimer(); callAfterDelay (250, []() { PopupMenu::dismissAllActiveMenus(); }); } 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); bool isBypassed = false; if (auto* f = graph.graph.getNodeForId (pluginID)) isBypassed = f->isBypassed(); auto boxColour = findColour (TextEditor::backgroundColourId); if (isBypassed) boxColour = boxColour.brighter(); g.setColour (boxColour); 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() { menu.reset (new PopupMenu); menu->addItem (1, "Delete this filter"); menu->addItem (2, "Disconnect all pins"); menu->addItem (3, "Toggle Bypass"); if (getProcessor()->hasEditor()) { menu->addSeparator(); menu->addItem (10, "Show plugin GUI"); menu->addItem (11, "Show all programs"); menu->addItem (12, "Show all parameters"); #if JUCE_WINDOWS && JUCE_WIN_PER_MONITOR_DPI_AWARE auto isTicked = false; if (auto* node = graph.graph.getNodeForId (pluginID)) isTicked = node->properties["DPIAware"]; menu->addItem (13, "Enable DPI awareness", true, isTicked); #endif menu->addItem (14, "Show debug log"); } menu->addSeparator(); menu->addItem (20, "Configure Audio I/O"); menu->addItem (21, "Test state save/load"); menu->showMenuAsync ({}, ModalCallbackFunction::create ([this] (int r) { switch (r) { case 1: graph.graph.removeNode (pluginID); break; case 2: graph.graph.disconnectNode (pluginID); break; case 3: { if (auto* node = graph.graph.getNodeForId (pluginID)) node->setBypassed (! node->isBypassed()); repaint(); break; } case 10: showWindow (PluginWindow::Type::normal); break; case 11: showWindow (PluginWindow::Type::programs); break; case 12: showWindow (PluginWindow::Type::generic) ; break; #if JUCE_WINDOWS && JUCE_WIN_PER_MONITOR_DPI_AWARE case 13: { if (auto* node = graph.graph.getNodeForId (pluginID)) node->properties.set ("DPIAware", ! node->properties ["DPIAware"]); break; } #endif case 14: showWindow (PluginWindow::Type::debug); 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); } void timerCallback() override { // this should only be called on touch devices jassert (isOnTouchDevice()); stopTimer(); showPopupMenu(); } void parameterValueChanged (int, float) override { repaint(); } void parameterGestureChanged (int, bool) override {} GraphEditorPanel& panel; PluginGraph& 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; std::unique_ptr menu; }; //============================================================================== struct GraphEditorPanel::ConnectorComponent : public Component, public SettableTooltipClient { explicit 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.getComponentForPlugin (connection.source.nodeID)) p1 = src->getPinPos (connection.source.channelIndex, false); if (auto* dest = panel.getComponentForPlugin (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 }; 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; PluginGraph& graph; AudioProcessorGraph::Connection connection { { {}, 0 }, { {}, 0 } }; Point lastInputPos, lastOutputPos; Path linePath, hitPath; bool dragging = false; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ConnectorComponent) }; //============================================================================== GraphEditorPanel::GraphEditorPanel (PluginGraph& 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 (isOnTouchDevice()) { originalTouchPos = e.position.toInt(); startTimer (750); } if (e.mods.isPopupMenu()) showPopupMenu (e.position.toInt()); } void GraphEditorPanel::mouseUp (const MouseEvent&) { if (isOnTouchDevice()) { stopTimer(); callAfterDelay (250, []() { PopupMenu::dismissAllActiveMenus(); }); } } void GraphEditorPanel::mouseDrag (const MouseEvent& e) { if (isOnTouchDevice() && e.getDistanceFromDragStart() > 5) stopTimer(); } void GraphEditorPanel::createNewPlugin (const PluginDescription& desc, Point position) { graph.addPlugin (desc, position.toDouble() / Point ((double) getWidth(), (double) getHeight())); } GraphEditorPanel::PluginComponent* GraphEditorPanel::getComponentForPlugin (AudioProcessorGraph::NodeID nodeID) const { for (auto* fc : nodes) if (fc->pluginID == nodeID) 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 (getComponentForPlugin (f->nodeID) == nullptr) { auto* comp = nodes.add (new PluginComponent (*this, f->nodeID)); addAndMakeVisible (comp); comp->update(); } } for (auto& c : graph.graph.getConnections()) { if (getComponentForConnection (c) == nullptr) { auto* comp = connectors.add (new ConnectorComponent (*this)); addAndMakeVisible (comp); comp->setInput (c.source); comp->setOutput (c.destination); } } } void GraphEditorPanel::showPopupMenu (Point mousePos) { menu.reset (new PopupMenu); if (auto* mainWindow = findParentComponentOfClass()) { mainWindow->addPluginsToMenu (*menu); menu->showMenuAsync ({}, ModalCallbackFunction::create ([this, mousePos] (int r) { if (r > 0) if (auto* mainWin = findParentComponentOfClass()) createNewPlugin (mainWin->getChosenType (r), mousePos); })); } } void GraphEditorPanel::beginConnectorDrag (AudioProcessorGraph::NodeAndChannel source, AudioProcessorGraph::NodeAndChannel dest, const MouseEvent& e) { auto* c = dynamic_cast (e.originalComponent); connectors.removeObject (c, false); draggingConnector.reset (c); if (draggingConnector == nullptr) draggingConnector.reset (new ConnectorComponent (*this)); draggingConnector->setInput (source); draggingConnector->setOutput (dest); addAndMakeVisible (draggingConnector.get()); 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 == AudioProcessorGraph::NodeID() && ! pin->isInput) { connection.source = pin->pin; } else if (connection.destination.nodeID == AudioProcessorGraph::NodeID() && 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 == AudioProcessorGraph::NodeID()) 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 == AudioProcessorGraph::NodeID()) { if (pin->isInput) return; connection.source = pin->pin; } else { if (! pin->isInput) return; connection.destination = pin->pin; } graph.graph.addConnection (connection); } } void GraphEditorPanel::timerCallback() { // this should only be called on touch devices jassert (isOnTouchDevice()); stopTimer(); showPopupMenu (originalTouchPos); } //============================================================================== 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) }; //============================================================================== class GraphDocumentComponent::TitleBarComponent : public Component, private Button::Listener { public: explicit TitleBarComponent (GraphDocumentComponent& graphDocumentComponent) : owner (graphDocumentComponent) { static const unsigned char burgerMenuPathData[] = { 110,109,0,0,128,64,0,0,32,65,108,0,0,224,65,0,0,32,65,98,254,212,232,65,0,0,32,65,0,0,240,65,252, 169,17,65,0,0,240,65,0,0,0,65,98,0,0,240,65,8,172,220,64,254,212,232,65,0,0,192,64,0,0,224,65,0,0, 192,64,108,0,0,128,64,0,0,192,64,98,16,88,57,64,0,0,192,64,0,0,0,64,8,172,220,64,0,0,0,64,0,0,0,65, 98,0,0,0,64,252,169,17,65,16,88,57,64,0,0,32,65,0,0,128,64,0,0,32,65,99,109,0,0,224,65,0,0,96,65,108, 0,0,128,64,0,0,96,65,98,16,88,57,64,0,0,96,65,0,0,0,64,4,86,110,65,0,0,0,64,0,0,128,65,98,0,0,0,64, 254,212,136,65,16,88,57,64,0,0,144,65,0,0,128,64,0,0,144,65,108,0,0,224,65,0,0,144,65,98,254,212,232, 65,0,0,144,65,0,0,240,65,254,212,136,65,0,0,240,65,0,0,128,65,98,0,0,240,65,4,86,110,65,254,212,232, 65,0,0,96,65,0,0,224,65,0,0,96,65,99,109,0,0,224,65,0,0,176,65,108,0,0,128,64,0,0,176,65,98,16,88,57, 64,0,0,176,65,0,0,0,64,2,43,183,65,0,0,0,64,0,0,192,65,98,0,0,0,64,254,212,200,65,16,88,57,64,0,0,208, 65,0,0,128,64,0,0,208,65,108,0,0,224,65,0,0,208,65,98,254,212,232,65,0,0,208,65,0,0,240,65,254,212, 200,65,0,0,240,65,0,0,192,65,98,0,0,240,65,2,43,183,65,254,212,232,65,0,0,176,65,0,0,224,65,0,0,176, 65,99,101,0,0 }; static const unsigned char pluginListPathData[] = { 110,109,193,202,222,64,80,50,21,64,108,0,0,48,65,0,0,0,0,108,160,154,112,65,80,50,21,64,108,0,0,48,65,80, 50,149,64,108,193,202,222,64,80,50,21,64,99,109,0,0,192,64,251,220,127,64,108,160,154,32,65,165,135,202, 64,108,160,154,32,65,250,220,47,65,108,0,0,192,64,102,144,10,65,108,0,0,192,64,251,220,127,64,99,109,0,0, 128,65,251,220,127,64,108,0,0,128,65,103,144,10,65,108,96,101,63,65,251,220,47,65,108,96,101,63,65,166,135, 202,64,108,0,0,128,65,251,220,127,64,99,109,96,101,79,65,148,76,69,65,108,0,0,136,65,0,0,32,65,108,80, 77,168,65,148,76,69,65,108,0,0,136,65,40,153,106,65,108,96,101,79,65,148,76,69,65,99,109,0,0,64,65,63,247, 95,65,108,80,77,128,65,233,161,130,65,108,80,77,128,65,125,238,167,65,108,0,0,64,65,51,72,149,65,108,0,0,64, 65,63,247,95,65,99,109,0,0,176,65,63,247,95,65,108,0,0,176,65,51,72,149,65,108,176,178,143,65,125,238,167,65, 108,176,178,143,65,233,161,130,65,108,0,0,176,65,63,247,95,65,99,109,12,86,118,63,148,76,69,65,108,0,0,160, 64,0,0,32,65,108,159,154,16,65,148,76,69,65,108,0,0,160,64,40,153,106,65,108,12,86,118,63,148,76,69,65,99, 109,0,0,0,0,63,247,95,65,108,62,53,129,64,233,161,130,65,108,62,53,129,64,125,238,167,65,108,0,0,0,0,51, 72,149,65,108,0,0,0,0,63,247,95,65,99,109,0,0,32,65,63,247,95,65,108,0,0,32,65,51,72,149,65,108,193,202,190, 64,125,238,167,65,108,193,202,190,64,233,161,130,65,108,0,0,32,65,63,247,95,65,99,101,0,0 }; { Path p; p.loadPathFromData (burgerMenuPathData, sizeof (burgerMenuPathData)); burgerButton.setShape (p, true, true, false); } { Path p; p.loadPathFromData (pluginListPathData, sizeof (pluginListPathData)); pluginButton.setShape (p, true, true, false); } burgerButton.addListener (this); addAndMakeVisible (burgerButton); pluginButton.addListener (this); addAndMakeVisible (pluginButton); titleLabel.setJustificationType (Justification::centredLeft); addAndMakeVisible (titleLabel); setOpaque (true); } private: void paint (Graphics& g) override { auto titleBarBackgroundColour = getLookAndFeel().findColour (ResizableWindow::backgroundColourId).darker(); g.setColour (titleBarBackgroundColour); g.fillRect (getLocalBounds()); } void resized() override { auto r = getLocalBounds(); burgerButton.setBounds (r.removeFromLeft (40).withSizeKeepingCentre (20, 20)); pluginButton.setBounds (r.removeFromRight (40).withSizeKeepingCentre (20, 20)); titleLabel.setFont (Font (static_cast (getHeight()) * 0.5f, Font::plain)); titleLabel.setBounds (r); } void buttonClicked (Button* b) override { owner.showSidePanel (b == &burgerButton); } GraphDocumentComponent& owner; Label titleLabel {"titleLabel", "Plugin Host"}; ShapeButton burgerButton {"burgerButton", Colours::lightgrey, Colours::lightgrey, Colours::white}; ShapeButton pluginButton {"pluginButton", Colours::lightgrey, Colours::lightgrey, Colours::white}; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TitleBarComponent) }; //============================================================================== struct GraphDocumentComponent::PluginListBoxModel : public ListBoxModel, public ChangeListener, public MouseListener { PluginListBoxModel (ListBox& lb, KnownPluginList& kpl) : owner (lb), knownPlugins (kpl) { knownPlugins.addChangeListener (this); owner.addMouseListener (this, true); #if JUCE_IOS scanner.reset (new AUScanner (knownPlugins)); #endif } int getNumRows() override { return knownPlugins.getNumTypes(); } void paintListBoxItem (int rowNumber, Graphics& g, int width, int height, bool rowIsSelected) override { g.fillAll (rowIsSelected ? Colour (0xff42A2C8) : Colour (0xff263238)); g.setColour (rowIsSelected ? Colours::black : Colours::white); if (rowNumber < knownPlugins.getNumTypes()) g.drawFittedText (knownPlugins.getTypes()[rowNumber].name, { 0, 0, width, height - 2 }, Justification::centred, 1); g.setColour (Colours::black.withAlpha (0.4f)); g.drawRect (0, height - 1, width, 1); } var getDragSourceDescription (const SparseSet& selectedRows) override { if (! isOverSelectedRow) return var(); return String ("PLUGIN: " + String (selectedRows[0])); } void changeListenerCallback (ChangeBroadcaster*) override { owner.updateContent(); } void mouseDown (const MouseEvent& e) override { isOverSelectedRow = owner.getRowPosition (owner.getSelectedRow(), true) .contains (e.getEventRelativeTo (&owner).getMouseDownPosition()); } ListBox& owner; KnownPluginList& knownPlugins; bool isOverSelectedRow = false; #if JUCE_IOS std::unique_ptr scanner; #endif JUCE_DECLARE_NON_COPYABLE (PluginListBoxModel) }; //============================================================================== GraphDocumentComponent::GraphDocumentComponent (AudioPluginFormatManager& fm, AudioDeviceManager& dm, KnownPluginList& kpl) : graph (new PluginGraph (fm)), deviceManager (dm), pluginList (kpl), graphPlayer (getAppProperties().getUserSettings()->getBoolValue ("doublePrecisionProcessing", false)) { init(); deviceManager.addChangeListener (graphPanel.get()); deviceManager.addAudioCallback (&graphPlayer); deviceManager.addMidiInputDeviceCallback ({}, &graphPlayer.getMidiMessageCollector()); deviceManager.addChangeListener (this); } void GraphDocumentComponent::init() { updateMidiOutput(); graphPanel.reset (new GraphEditorPanel (*graph)); addAndMakeVisible (graphPanel.get()); graphPlayer.setProcessor (&graph->graph); keyState.addListener (&graphPlayer.getMidiMessageCollector()); keyboardComp.reset (new MidiKeyboardComponent (keyState, MidiKeyboardComponent::horizontalKeyboard)); addAndMakeVisible (keyboardComp.get()); statusBar.reset (new TooltipBar()); addAndMakeVisible (statusBar.get()); graphPanel->updateComponents(); if (isOnTouchDevice()) { if (isOnTouchDevice()) { titleBarComponent.reset (new TitleBarComponent (*this)); addAndMakeVisible (titleBarComponent.get()); } pluginListBoxModel.reset (new PluginListBoxModel (pluginListBox, pluginList)); pluginListBox.setModel (pluginListBoxModel.get()); pluginListBox.setRowHeight (40); pluginListSidePanel.setContent (&pluginListBox, false); mobileSettingsSidePanel.setContent (new AudioDeviceSelectorComponent (deviceManager, 0, 2, 0, 2, true, true, true, false)); if (isOnTouchDevice()) { addAndMakeVisible (pluginListSidePanel); addAndMakeVisible (mobileSettingsSidePanel); } } } GraphDocumentComponent::~GraphDocumentComponent() { if (midiOutput != nullptr) midiOutput->stopBackgroundThread(); releaseGraph(); keyState.removeListener (&graphPlayer.getMidiMessageCollector()); } void GraphDocumentComponent::resized() { auto r = getLocalBounds(); const int titleBarHeight = 40; const int keysHeight = 60; const int statusHeight = 20; if (isOnTouchDevice()) titleBarComponent->setBounds (r.removeFromTop(titleBarHeight)); keyboardComp->setBounds (r.removeFromBottom (keysHeight)); statusBar->setBounds (r.removeFromBottom (statusHeight)); graphPanel->setBounds (r); checkAvailableWidth(); } void GraphDocumentComponent::createNewPlugin (const PluginDescription& desc, Point pos) { graphPanel->createNewPlugin (desc, pos); } void GraphDocumentComponent::unfocusKeyboardComponent() { keyboardComp->unfocusAllComponents(); } void GraphDocumentComponent::releaseGraph() { deviceManager.removeAudioCallback (&graphPlayer); deviceManager.removeMidiInputDeviceCallback ({}, &graphPlayer.getMidiMessageCollector()); if (graphPanel != nullptr) { deviceManager.removeChangeListener (graphPanel.get()); graphPanel = nullptr; } keyboardComp = nullptr; statusBar = nullptr; graphPlayer.setProcessor (nullptr); graph = nullptr; } bool GraphDocumentComponent::isInterestedInDragSource (const SourceDetails& details) { return ((dynamic_cast (details.sourceComponent.get()) != nullptr) && details.description.toString().startsWith ("PLUGIN")); } void GraphDocumentComponent::itemDropped (const SourceDetails& details) { // don't allow items to be dropped behind the sidebar if (pluginListSidePanel.getBounds().contains (details.localPosition)) return; auto pluginTypeIndex = details.description.toString() .fromFirstOccurrenceOf ("PLUGIN: ", false, false) .getIntValue(); // must be a valid index! jassert (isPositiveAndBelow (pluginTypeIndex, pluginList.getNumTypes())); createNewPlugin (pluginList.getTypes()[pluginTypeIndex], details.localPosition); } void GraphDocumentComponent::showSidePanel (bool showSettingsPanel) { if (showSettingsPanel) mobileSettingsSidePanel.showOrHide (true); else pluginListSidePanel.showOrHide (true); checkAvailableWidth(); lastOpenedSidePanel = showSettingsPanel ? &mobileSettingsSidePanel : &pluginListSidePanel; } void GraphDocumentComponent::hideLastSidePanel() { if (lastOpenedSidePanel != nullptr) lastOpenedSidePanel->showOrHide (false); if (mobileSettingsSidePanel.isPanelShowing()) lastOpenedSidePanel = &mobileSettingsSidePanel; else if (pluginListSidePanel.isPanelShowing()) lastOpenedSidePanel = &pluginListSidePanel; else lastOpenedSidePanel = nullptr; } void GraphDocumentComponent::checkAvailableWidth() { if (mobileSettingsSidePanel.isPanelShowing() && pluginListSidePanel.isPanelShowing()) { if (getWidth() - (mobileSettingsSidePanel.getWidth() + pluginListSidePanel.getWidth()) < 150) hideLastSidePanel(); } } void GraphDocumentComponent::setDoublePrecision (bool doublePrecision) { graphPlayer.setDoublePrecisionProcessing (doublePrecision); } bool GraphDocumentComponent::closeAnyOpenPluginWindows() { return graphPanel->graph.closeAnyOpenPluginWindows(); } void GraphDocumentComponent::changeListenerCallback (ChangeBroadcaster*) { updateMidiOutput(); } void GraphDocumentComponent::updateMidiOutput() { auto* defaultMidiOutput = deviceManager.getDefaultMidiOutput(); if (midiOutput != defaultMidiOutput) { midiOutput = defaultMidiOutput; if (midiOutput != nullptr) midiOutput->startBackgroundThread(); graphPlayer.setMidiOutput (midiOutput); } }