/* ============================================================================== 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" #include "FilterIOConfiguration.h" //============================================================================== static Array activePluginWindows; PluginWindow::PluginWindow (AudioProcessorEditor* pluginEditor, AudioProcessorGraph::Node* o, WindowFormatType t) : DocumentWindow (pluginEditor->getName(), LookAndFeel::getDefaultLookAndFeel().findColour (ResizableWindow::backgroundColourId), DocumentWindow::minimiseButton | DocumentWindow::closeButton), owner (o), type (t) { setSize (400, 300); setContentOwned (pluginEditor, true); setTopLeftPosition (owner->properties.getWithDefault (getLastXProp (type), Random::getSystemRandom().nextInt (500)), owner->properties.getWithDefault (getLastYProp (type), Random::getSystemRandom().nextInt (500))); owner->properties.set (getOpenProp (type), true); setVisible (true); activePluginWindows.add (this); } void PluginWindow::closeCurrentlyOpenWindowsFor (const uint32 nodeId) { for (int i = activePluginWindows.size(); --i >= 0;) if (activePluginWindows.getUnchecked(i)->owner->nodeId == nodeId) delete activePluginWindows.getUnchecked (i); } void PluginWindow::closeAllCurrentlyOpenWindows() { if (activePluginWindows.size() > 0) { for (int i = activePluginWindows.size(); --i >= 0;) delete activePluginWindows.getUnchecked (i); Component dummyModalComp; dummyModalComp.enterModalState (false); MessageManager::getInstance()->runDispatchLoopUntil (50); } } //============================================================================== struct ProcessorProgramPropertyComp : public PropertyComponent, private AudioProcessorListener { ProcessorProgramPropertyComp (const String& name, AudioProcessor& p) : PropertyComponent (name), owner (p) { owner.addListener (this); } ~ProcessorProgramPropertyComp() { owner.removeListener (this); } void refresh() override {} void audioProcessorChanged (AudioProcessor*) override {} void audioProcessorParameterChanged (AudioProcessor*, int, float) override {} AudioProcessor& owner; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ProcessorProgramPropertyComp) }; struct ProgramAudioProcessorEditor : public AudioProcessorEditor { ProgramAudioProcessorEditor (AudioProcessor* p) : AudioProcessorEditor (p) { jassert (p != nullptr); setOpaque (true); addAndMakeVisible (panel); Array programs; auto numPrograms = p->getNumPrograms(); int totalHeight = 0; for (int i = 0; i < numPrograms; ++i) { auto name = p->getProgramName (i).trim(); if (name.isEmpty()) name = "Unnamed"; auto pc = new ProcessorProgramPropertyComp (name, *p); programs.add (pc); totalHeight += pc->getPreferredHeight(); } panel.addProperties (programs); setSize (400, jlimit (25, 400, totalHeight)); } void paint (Graphics& g) override { g.fillAll (Colours::grey); } void resized() override { panel.setBounds (getLocalBounds()); } PropertyPanel panel; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ProgramAudioProcessorEditor) }; //============================================================================== PluginWindow* PluginWindow::getWindowFor (AudioProcessorGraph::Node* node, WindowFormatType type) { jassert (node != nullptr); for (auto* w : activePluginWindows) if (w->owner == node && w->type == type) return w; auto* processor = node->getProcessor(); AudioProcessorEditor* ui = nullptr; if (auto* pluginInstance = dynamic_cast (processor)) { auto description = pluginInstance->getPluginDescription(); if (description.pluginFormatName == "Internal") { getCommandManager().invokeDirectly (CommandIDs::showAudioSettings, false); return nullptr; } } if (type == Normal) { ui = processor->createEditorIfNeeded(); if (ui == nullptr) type = Generic; } if (ui == nullptr) { if (type == Generic || type == Parameters) ui = new GenericAudioProcessorEditor (processor); else if (type == Programs) ui = new ProgramAudioProcessorEditor (processor); else if (type == AudioIO) ui = new FilterIOConfigurationWindow (processor); } if (ui != nullptr) { if (auto* plugin = dynamic_cast (processor)) ui->setName (plugin->getName()); return new PluginWindow (ui, node, type); } return nullptr; } PluginWindow::~PluginWindow() { activePluginWindows.removeFirstMatchingValue (this); clearContentComponent(); } void PluginWindow::moved() { owner->properties.set (getLastXProp (type), getX()); owner->properties.set (getLastYProp (type), getY()); } void PluginWindow::closeButtonPressed() { owner->properties.set (getOpenProp (type), false); delete this; } //============================================================================== struct PinComponent : public Component, public SettableTooltipClient { PinComponent (FilterGraph& g, uint32 id, int i, bool isIn) : graph (g), pluginID (id), index (i), isInput (isIn) { if (auto node = graph.getNodeForId (pluginID)) { String tip; if (index == FilterGraph::midiChannelNumber) { tip = isInput ? "MIDI Input" : "MIDI Output"; } else { auto& processor = *node->getProcessor(); auto channel = processor.getOffsetInBusBufferForAbsoluteChannelIndex (isInput, index, 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 (index + 1); } setTooltip (tip); } setSize (16, 16); } void paint (Graphics& g) override { const float w = (float) getWidth(); const float 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 = (index == FilterGraph::midiChannelNumber ? Colours::red : Colours::green); g.setColour (colour.withRotatedHue (static_cast (busIdx) / 5.0f)); g.fillPath (p); } void mouseDown (const MouseEvent& e) override { getGraphPanel()->beginConnectorDrag (isInput ? 0 : pluginID, index, isInput ? pluginID : 0, index, e); } void mouseDrag (const MouseEvent& e) override { getGraphPanel()->dragConnector (e); } void mouseUp (const MouseEvent& e) override { getGraphPanel()->endDraggingConnector (e); } GraphEditorPanel* getGraphPanel() const noexcept { return findParentComponentOfClass(); } FilterGraph& graph; const uint32 pluginID; const int index; const bool isInput; int busIdx = 0; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PinComponent) }; //============================================================================== struct FilterComponent : public Component { FilterComponent (FilterGraph& g, uint32 id) : graph (g), pluginID (id) { shadow.setShadowProperties (DropShadow (Colours::black.withAlpha (0.5f), 3, { 0, 1 })); setComponentEffect (&shadow); setSize (150, 60); } ~FilterComponent() { deleteAllChildren(); } FilterComponent (const FilterComponent&) = delete; FilterComponent& operator= (const FilterComponent&) = delete; void mouseDown (const MouseEvent& e) override { originalPos = localPointToGlobal (Point()); toFront (true); if (e.mods.isPopupMenu()) { PopupMenu m; m.addItem (1, "Delete this filter"); m.addItem (2, "Disconnect all pins"); m.addSeparator(); m.addItem (3, "Show plugin UI"); m.addItem (4, "Show all programs"); m.addItem (5, "Show all parameters"); m.addSeparator(); m.addItem (6, "Configure Audio I/O"); m.addItem (7, "Test state save/load"); auto r = m.show(); if (r == 1) { graph.removeFilter (pluginID); return; } else if (r == 2) { graph.disconnectFilter (pluginID); } else { if (auto f = graph.getNodeForId (pluginID)) { auto* processor = f->getProcessor(); jassert (processor != nullptr); if (r == 7) { MemoryBlock state; processor->getStateInformation (state); processor->setStateInformation (state.getData(), (int) state.getSize()); } else { PluginWindow::WindowFormatType type = processor->hasEditor() ? PluginWindow::Normal : PluginWindow::Generic; switch (r) { case 4: type = PluginWindow::Programs; break; case 5: type = PluginWindow::Parameters; break; case 6: type = PluginWindow::AudioIO; break; default: break; }; if (auto* w = PluginWindow::getWindowFor (f, type)) w->toFront (true); } } } } } void mouseDrag (const MouseEvent& e) override { if (! e.mods.isPopupMenu()) { auto pos = originalPos + e.getOffsetFromDragStart(); if (getParentComponent() != nullptr) pos = getParentComponent()->getLocalPoint (nullptr, pos); graph.setNodePosition (pluginID, (pos.getX() + getWidth() / 2) / (double) getParentWidth(), (pos.getY() + getHeight() / 2) / (double) getParentHeight()); getGraphPanel()->updateComponents(); } } void mouseUp (const MouseEvent& e) override { if (e.mouseWasDraggedSinceMouseDown()) { graph.setChangedFlag (true); } else if (e.getNumberOfClicks() == 2) { if (auto f = graph.getNodeForId (pluginID)) if (auto* w = PluginWindow::getWindowFor (f, PluginWindow::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 { g.setColour (findColour (TextEditor::backgroundColourId)); const int x = 4; const int y = pinSize; const int w = getWidth() - x * 2; const int h = getHeight() - pinSize * 2; g.fillRect (x, y, w, h); g.setColour (findColour (TextEditor::textColourId)); g.setFont (font); g.drawFittedText (getName(), getLocalBounds().reduced (4, 2), Justification::centred, 2); } void resized() override { if (auto f = graph.getNodeForId (pluginID)) { if (auto* processor = f->getProcessor()) { for (auto* child : getChildren()) { if (auto* pin = dynamic_cast (child)) { const bool isInput = pin->isInput; int busIdx = 0; processor->getOffsetInBusBufferForAbsoluteChannelIndex (isInput, pin->index, busIdx); const int total = isInput ? numIns : numOuts; const int index = pin->index == FilterGraph::midiChannelNumber ? (total - 1) : pin->index; 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* child : getChildren()) if (auto* pin = dynamic_cast (child)) if (pin->index == index && isInput == pin->isInput) return getPosition().toFloat() + pin->getBounds().getCentre().toFloat(); return {}; } void update() { const AudioProcessorGraph::Node::Ptr f (graph.getNodeForId (pluginID)); if (f == nullptr) { delete this; return; } 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()); { Point p = graph.getNodePosition (pluginID); setCentreRelative ((float) p.x, (float) p.y); } if (numIns != numInputs || numOuts != numOutputs) { numInputs = numIns; numOutputs = numOuts; deleteAllChildren(); int i; for (i = 0; i < f->getProcessor()->getTotalNumInputChannels(); ++i) addAndMakeVisible (new PinComponent (graph, pluginID, i, true)); if (f->getProcessor()->acceptsMidi()) addAndMakeVisible (new PinComponent (graph, pluginID, FilterGraph::midiChannelNumber, true)); for (i = 0; i < f->getProcessor()->getTotalNumOutputChannels(); ++i) addAndMakeVisible (new PinComponent (graph, pluginID, i, false)); if (f->getProcessor()->producesMidi()) addAndMakeVisible (new PinComponent (graph, pluginID, FilterGraph::midiChannelNumber, false)); resized(); } } GraphEditorPanel* getGraphPanel() const noexcept { return findParentComponentOfClass(); } FilterGraph& graph; const uint32 pluginID; int numInputs = 0, numOutputs = 0; int pinSize = 16; Point originalPos; Font font { 13.0f, Font::bold }; int numIns = 0, numOuts = 0; DropShadowEffect shadow; }; //============================================================================== struct ConnectorComponent : public Component, public SettableTooltipClient { ConnectorComponent (FilterGraph& g) : graph (g) { setAlwaysOnTop (true); } void setInput (uint32 newSourceID, int newSourceChannel) { if (sourceFilterID != newSourceID || sourceFilterChannel != newSourceChannel) { sourceFilterID = newSourceID; sourceFilterChannel = newSourceChannel; update(); } } void setOutput (uint32 newDestID, int newDestChannel) { if (destFilterID != newDestID || destFilterChannel != newDestChannel) { destFilterID = newDestID; destFilterChannel = newDestChannel; 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* hostPanel = getGraphPanel()) { if (auto* src = hostPanel->getComponentForFilter (sourceFilterID)) p1 = src->getPinPos (sourceFilterChannel, false); if (auto* dest = hostPanel->getComponentForFilter (destFilterID)) p2 = dest->getPinPos (destFilterChannel, true); } } void paint (Graphics& g) override { if (sourceFilterChannel == FilterGraph::midiChannelNumber || destFilterChannel == FilterGraph::midiChannelNumber) { 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) { getGraphPanel()->dragConnector (e); } else if (e.mouseWasDraggedSinceMouseDown()) { dragging = true; graph.removeConnection (sourceFilterID, sourceFilterChannel, destFilterID, destFilterChannel); double distanceFromStart, distanceFromEnd; getDistancesFromEnds (e.position, distanceFromStart, distanceFromEnd); const bool isNearerSource = (distanceFromStart < distanceFromEnd); getGraphPanel()->beginConnectorDrag (isNearerSource ? 0 : sourceFilterID, sourceFilterChannel, isNearerSource ? destFilterID : 0, destFilterChannel, e); } } void mouseUp (const MouseEvent& e) override { if (dragging) getGraphPanel()->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 (float_Pi * 0.5f - (float) atan2 (p2.x - p1.x, p2.y - p1.y)) .translated ((p1 + p2) * 0.5f)); linePath.addPath (arrow); linePath.setUsingNonZeroWinding (true); } GraphEditorPanel* getGraphPanel() const noexcept { return findParentComponentOfClass(); } void getDistancesFromEnds (Point p, double& distanceFromStart, double& distanceFromEnd) const { Point p1, p2; getPoints (p1, p2); distanceFromStart = p1.getDistanceFrom (p); distanceFromEnd = p2.getDistanceFrom (p); } FilterGraph& graph; uint32 sourceFilterID = 0, destFilterID = 0; int sourceFilterChannel = 0, destFilterChannel = 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; deleteAllChildren(); } 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.addFilter (desc, position.toDouble() / Point ((double) getWidth(), (double) getHeight())); } FilterComponent* GraphEditorPanel::getComponentForFilter (const uint32 filterID) const { for (auto* child : getChildren()) if (auto* fc = dynamic_cast (child)) if (fc->pluginID == filterID) return fc; return nullptr; } ConnectorComponent* GraphEditorPanel::getComponentForConnection (const AudioProcessorGraph::Connection& conn) const { for (auto* child : getChildren()) { if (auto* c = dynamic_cast (child)) if (c->sourceFilterID == conn.sourceNodeId && c->destFilterID == conn.destNodeId && c->sourceFilterChannel == conn.sourceChannelIndex && c->destFilterChannel == conn.destChannelIndex) return c; } return nullptr; } PinComponent* GraphEditorPanel::findPinAt (Point pos) const { for (auto* child : getChildren()) if (auto* fc = dynamic_cast (child)) if (auto* pin = dynamic_cast (fc->getComponentAt (pos.toInt() - fc->getPosition()))) return pin; return nullptr; } void GraphEditorPanel::resized() { updateComponents(); } void GraphEditorPanel::changeListenerCallback (ChangeBroadcaster*) { updateComponents(); } void GraphEditorPanel::updateComponents() { auto children = getChildren(); for (auto child : children) if (auto* fc = dynamic_cast (static_cast (child))) fc->update(); for (int i = getNumChildComponents(); --i >= 0;) { auto* cc = dynamic_cast (getChildComponent (i)); if (cc != nullptr && cc != draggingConnector) { if (graph.getConnectionBetween (cc->sourceFilterID, cc->sourceFilterChannel, cc->destFilterID, cc->destFilterChannel) == nullptr) { delete cc; } else { cc->update(); } } } for (int i = graph.getNumFilters(); --i >= 0;) { auto f = graph.getNode (i); if (getComponentForFilter (f->nodeId) == 0) { auto* comp = new FilterComponent (graph, f->nodeId); addAndMakeVisible (comp); comp->update(); } } for (int i = graph.getNumConnections(); --i >= 0;) { auto* c = graph.getConnection (i); if (getComponentForConnection (*c) == 0) { auto* comp = new ConnectorComponent (graph); addAndMakeVisible (comp); comp->setInput (c->sourceNodeId, c->sourceChannelIndex); comp->setOutput (c->destNodeId, c->destChannelIndex); } } } void GraphEditorPanel::beginConnectorDrag (const uint32 sourceFilterID, const int sourceFilterChannel, const uint32 destFilterID, const int destFilterChannel, const MouseEvent& e) { draggingConnector = dynamic_cast (e.originalComponent); if (draggingConnector == nullptr) draggingConnector = new ConnectorComponent (graph); draggingConnector->setInput (sourceFilterID, sourceFilterChannel); draggingConnector->setOutput (destFilterID, destFilterChannel); 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 srcFilter = draggingConnector->sourceFilterID; auto srcChannel = draggingConnector->sourceFilterChannel; auto dstFilter = draggingConnector->destFilterID; auto dstChannel = draggingConnector->destFilterChannel; if (srcFilter == 0 && ! pin->isInput) { srcFilter = pin->pluginID; srcChannel = pin->index; } else if (dstFilter == 0 && pin->isInput) { dstFilter = pin->pluginID; dstChannel = pin->index; } if (graph.canConnect (srcFilter, srcChannel, dstFilter, dstChannel)) { pos = (pin->getParentComponent()->getPosition() + pin->getBounds().getCentre()).toFloat(); draggingConnector->setTooltip (pin->getTooltip()); } } if (draggingConnector->sourceFilterID == 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 srcFilter = draggingConnector->sourceFilterID; auto srcChannel = draggingConnector->sourceFilterChannel; auto dstFilter = draggingConnector->destFilterID; auto dstChannel = draggingConnector->destFilterChannel; draggingConnector = nullptr; if (auto* pin = findPinAt (e2.position)) { if (srcFilter == 0) { if (pin->isInput) return; srcFilter = pin->pluginID; srcChannel = pin->index; } else { if (! pin->isInput) return; dstFilter = pin->pluginID; dstChannel = pin->index; } graph.addConnection (srcFilter, srcChannel, dstFilter, dstChannel); } } //============================================================================== struct 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->getGraph()); 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()); deviceManager.removeChangeListener (graphPanel); deleteAllChildren(); graphPlayer.setProcessor (nullptr); graph = nullptr; } void GraphDocumentComponent::setDoublePrecision (bool doublePrecision) { graphPlayer.setDoublePrecisionProcessing (doublePrecision); }