The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1402 lines
45KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2022 - Raw Material Software Limited
  5. JUCE is an open source library subject to commercial or open-source
  6. licensing.
  7. By using JUCE, you agree to the terms of both the JUCE 7 End-User License
  8. Agreement and JUCE Privacy Policy.
  9. End User License Agreement: www.juce.com/juce-7-licence
  10. Privacy Policy: www.juce.com/juce-privacy-policy
  11. Or: You may also use this code under the terms of the GPL v3 (see
  12. www.gnu.org/licenses).
  13. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  14. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  15. DISCLAIMED.
  16. ==============================================================================
  17. */
  18. #include <JuceHeader.h>
  19. #include "GraphEditorPanel.h"
  20. #include "../Plugins/InternalPlugins.h"
  21. #include "MainHostWindow.h"
  22. //==============================================================================
  23. #if JUCE_IOS
  24. class AUScanner
  25. {
  26. public:
  27. explicit AUScanner (KnownPluginList& list)
  28. : knownPluginList (list)
  29. {
  30. knownPluginList.clearBlacklistedFiles();
  31. paths = formatToScan.getDefaultLocationsToSearch();
  32. startScan();
  33. }
  34. private:
  35. KnownPluginList& knownPluginList;
  36. AudioUnitPluginFormat formatToScan;
  37. std::unique_ptr<PluginDirectoryScanner> scanner;
  38. FileSearchPath paths;
  39. static constexpr auto numJobs = 5;
  40. ThreadPool pool { numJobs };
  41. void startScan()
  42. {
  43. auto deadMansPedalFile = getAppProperties().getUserSettings()
  44. ->getFile().getSiblingFile ("RecentlyCrashedPluginsList");
  45. scanner.reset (new PluginDirectoryScanner (knownPluginList, formatToScan, paths,
  46. true, deadMansPedalFile, true));
  47. for (int i = numJobs; --i >= 0;)
  48. pool.addJob (new ScanJob (*this), true);
  49. }
  50. bool doNextScan()
  51. {
  52. String pluginBeingScanned;
  53. return scanner->scanNextFile (true, pluginBeingScanned);
  54. }
  55. struct ScanJob : public ThreadPoolJob
  56. {
  57. ScanJob (AUScanner& s) : ThreadPoolJob ("pluginscan"), scanner (s) {}
  58. JobStatus runJob()
  59. {
  60. while (scanner.doNextScan() && ! shouldExit())
  61. {}
  62. return jobHasFinished;
  63. }
  64. AUScanner& scanner;
  65. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ScanJob)
  66. };
  67. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AUScanner)
  68. };
  69. #endif
  70. //==============================================================================
  71. struct GraphEditorPanel::PinComponent : public Component,
  72. public SettableTooltipClient
  73. {
  74. PinComponent (GraphEditorPanel& p, AudioProcessorGraph::NodeAndChannel pinToUse, bool isIn)
  75. : panel (p), graph (p.graph), pin (pinToUse), isInput (isIn)
  76. {
  77. if (auto node = graph.graph.getNodeForId (pin.nodeID))
  78. {
  79. String tip;
  80. if (pin.isMIDI())
  81. {
  82. tip = isInput ? "MIDI Input"
  83. : "MIDI Output";
  84. }
  85. else
  86. {
  87. auto& processor = *node->getProcessor();
  88. auto channel = processor.getOffsetInBusBufferForAbsoluteChannelIndex (isInput, pin.channelIndex, busIdx);
  89. if (auto* bus = processor.getBus (isInput, busIdx))
  90. tip = bus->getName() + ": " + AudioChannelSet::getAbbreviatedChannelTypeName (bus->getCurrentLayout().getTypeOfChannel (channel));
  91. else
  92. tip = (isInput ? "Main Input: "
  93. : "Main Output: ") + String (pin.channelIndex + 1);
  94. }
  95. setTooltip (tip);
  96. }
  97. setSize (16, 16);
  98. }
  99. void paint (Graphics& g) override
  100. {
  101. auto w = (float) getWidth();
  102. auto h = (float) getHeight();
  103. Path p;
  104. p.addEllipse (w * 0.25f, h * 0.25f, w * 0.5f, h * 0.5f);
  105. p.addRectangle (w * 0.4f, isInput ? (0.5f * h) : 0.0f, w * 0.2f, h * 0.5f);
  106. auto colour = (pin.isMIDI() ? Colours::red : Colours::green);
  107. g.setColour (colour.withRotatedHue ((float) busIdx / 5.0f));
  108. g.fillPath (p);
  109. }
  110. void mouseDown (const MouseEvent& e) override
  111. {
  112. AudioProcessorGraph::NodeAndChannel dummy { {}, 0 };
  113. panel.beginConnectorDrag (isInput ? dummy : pin,
  114. isInput ? pin : dummy,
  115. e);
  116. }
  117. void mouseDrag (const MouseEvent& e) override
  118. {
  119. panel.dragConnector (e);
  120. }
  121. void mouseUp (const MouseEvent& e) override
  122. {
  123. panel.endDraggingConnector (e);
  124. }
  125. GraphEditorPanel& panel;
  126. PluginGraph& graph;
  127. AudioProcessorGraph::NodeAndChannel pin;
  128. const bool isInput;
  129. int busIdx = 0;
  130. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PinComponent)
  131. };
  132. //==============================================================================
  133. struct GraphEditorPanel::PluginComponent : public Component,
  134. public Timer,
  135. private AudioProcessorParameter::Listener,
  136. private AsyncUpdater
  137. {
  138. PluginComponent (GraphEditorPanel& p, AudioProcessorGraph::NodeID id) : panel (p), graph (p.graph), pluginID (id)
  139. {
  140. shadow.setShadowProperties (DropShadow (Colours::black.withAlpha (0.5f), 3, { 0, 1 }));
  141. setComponentEffect (&shadow);
  142. if (auto f = graph.graph.getNodeForId (pluginID))
  143. {
  144. if (auto* processor = f->getProcessor())
  145. {
  146. if (auto* bypassParam = processor->getBypassParameter())
  147. bypassParam->addListener (this);
  148. }
  149. }
  150. setSize (150, 60);
  151. }
  152. PluginComponent (const PluginComponent&) = delete;
  153. PluginComponent& operator= (const PluginComponent&) = delete;
  154. ~PluginComponent() override
  155. {
  156. if (auto f = graph.graph.getNodeForId (pluginID))
  157. {
  158. if (auto* processor = f->getProcessor())
  159. {
  160. if (auto* bypassParam = processor->getBypassParameter())
  161. bypassParam->removeListener (this);
  162. }
  163. }
  164. }
  165. void mouseDown (const MouseEvent& e) override
  166. {
  167. originalPos = localPointToGlobal (Point<int>());
  168. toFront (true);
  169. if (isOnTouchDevice())
  170. {
  171. startTimer (750);
  172. }
  173. else
  174. {
  175. if (e.mods.isPopupMenu())
  176. showPopupMenu();
  177. }
  178. }
  179. void mouseDrag (const MouseEvent& e) override
  180. {
  181. if (isOnTouchDevice() && e.getDistanceFromDragStart() > 5)
  182. stopTimer();
  183. if (! e.mods.isPopupMenu())
  184. {
  185. auto pos = originalPos + e.getOffsetFromDragStart();
  186. if (getParentComponent() != nullptr)
  187. pos = getParentComponent()->getLocalPoint (nullptr, pos);
  188. pos += getLocalBounds().getCentre();
  189. graph.setNodePosition (pluginID,
  190. { pos.x / (double) getParentWidth(),
  191. pos.y / (double) getParentHeight() });
  192. panel.updateComponents();
  193. }
  194. }
  195. void mouseUp (const MouseEvent& e) override
  196. {
  197. if (isOnTouchDevice())
  198. {
  199. stopTimer();
  200. callAfterDelay (250, []() { PopupMenu::dismissAllActiveMenus(); });
  201. }
  202. if (e.mouseWasDraggedSinceMouseDown())
  203. {
  204. graph.setChangedFlag (true);
  205. }
  206. else if (e.getNumberOfClicks() == 2)
  207. {
  208. if (auto f = graph.graph.getNodeForId (pluginID))
  209. if (auto* w = graph.getOrCreateWindowFor (f, PluginWindow::Type::normal))
  210. w->toFront (true);
  211. }
  212. }
  213. bool hitTest (int x, int y) override
  214. {
  215. for (auto* child : getChildren())
  216. if (child->getBounds().contains (x, y))
  217. return true;
  218. return x >= 3 && x < getWidth() - 6 && y >= pinSize && y < getHeight() - pinSize;
  219. }
  220. void paint (Graphics& g) override
  221. {
  222. auto boxArea = getLocalBounds().reduced (4, pinSize);
  223. bool isBypassed = false;
  224. if (auto* f = graph.graph.getNodeForId (pluginID))
  225. isBypassed = f->isBypassed();
  226. auto boxColour = findColour (TextEditor::backgroundColourId);
  227. if (isBypassed)
  228. boxColour = boxColour.brighter();
  229. g.setColour (boxColour);
  230. g.fillRect (boxArea.toFloat());
  231. g.setColour (findColour (TextEditor::textColourId));
  232. g.setFont (font);
  233. g.drawFittedText (getName(), boxArea, Justification::centred, 2);
  234. }
  235. void resized() override
  236. {
  237. if (auto f = graph.graph.getNodeForId (pluginID))
  238. {
  239. if (auto* processor = f->getProcessor())
  240. {
  241. for (auto* pin : pins)
  242. {
  243. const bool isInput = pin->isInput;
  244. auto channelIndex = pin->pin.channelIndex;
  245. int busIdx = 0;
  246. processor->getOffsetInBusBufferForAbsoluteChannelIndex (isInput, channelIndex, busIdx);
  247. const int total = isInput ? numIns : numOuts;
  248. const int index = pin->pin.isMIDI() ? (total - 1) : channelIndex;
  249. auto totalSpaces = static_cast<float> (total) + (static_cast<float> (jmax (0, processor->getBusCount (isInput) - 1)) * 0.5f);
  250. auto indexPos = static_cast<float> (index) + (static_cast<float> (busIdx) * 0.5f);
  251. pin->setBounds (proportionOfWidth ((1.0f + indexPos) / (totalSpaces + 1.0f)) - pinSize / 2,
  252. pin->isInput ? 0 : (getHeight() - pinSize),
  253. pinSize, pinSize);
  254. }
  255. }
  256. }
  257. }
  258. Point<float> getPinPos (int index, bool isInput) const
  259. {
  260. for (auto* pin : pins)
  261. if (pin->pin.channelIndex == index && isInput == pin->isInput)
  262. return getPosition().toFloat() + pin->getBounds().getCentre().toFloat();
  263. return {};
  264. }
  265. void update()
  266. {
  267. const AudioProcessorGraph::Node::Ptr f (graph.graph.getNodeForId (pluginID));
  268. jassert (f != nullptr);
  269. auto& processor = *f->getProcessor();
  270. numIns = processor.getTotalNumInputChannels();
  271. if (processor.acceptsMidi())
  272. ++numIns;
  273. numOuts = processor.getTotalNumOutputChannels();
  274. if (processor.producesMidi())
  275. ++numOuts;
  276. int w = 100;
  277. int h = 60;
  278. w = jmax (w, (jmax (numIns, numOuts) + 1) * 20);
  279. const int textWidth = font.getStringWidth (processor.getName());
  280. w = jmax (w, 16 + jmin (textWidth, 300));
  281. if (textWidth > 300)
  282. h = 100;
  283. setSize (w, h);
  284. setName (processor.getName() + formatSuffix);
  285. {
  286. auto p = graph.getNodePosition (pluginID);
  287. setCentreRelative ((float) p.x, (float) p.y);
  288. }
  289. if (numIns != numInputs || numOuts != numOutputs)
  290. {
  291. numInputs = numIns;
  292. numOutputs = numOuts;
  293. pins.clear();
  294. for (int i = 0; i < processor.getTotalNumInputChannels(); ++i)
  295. addAndMakeVisible (pins.add (new PinComponent (panel, { pluginID, i }, true)));
  296. if (processor.acceptsMidi())
  297. addAndMakeVisible (pins.add (new PinComponent (panel, { pluginID, AudioProcessorGraph::midiChannelIndex }, true)));
  298. for (int i = 0; i < processor.getTotalNumOutputChannels(); ++i)
  299. addAndMakeVisible (pins.add (new PinComponent (panel, { pluginID, i }, false)));
  300. if (processor.producesMidi())
  301. addAndMakeVisible (pins.add (new PinComponent (panel, { pluginID, AudioProcessorGraph::midiChannelIndex }, false)));
  302. resized();
  303. }
  304. }
  305. AudioProcessor* getProcessor() const
  306. {
  307. if (auto node = graph.graph.getNodeForId (pluginID))
  308. return node->getProcessor();
  309. return {};
  310. }
  311. bool isNodeUsingARA() const
  312. {
  313. if (auto node = graph.graph.getNodeForId (pluginID))
  314. return node->properties["useARA"];
  315. return false;
  316. }
  317. void showPopupMenu()
  318. {
  319. menu.reset (new PopupMenu);
  320. menu->addItem ("Delete this filter", [this] { graph.graph.removeNode (pluginID); });
  321. menu->addItem ("Disconnect all pins", [this] { graph.graph.disconnectNode (pluginID); });
  322. menu->addItem ("Toggle Bypass", [this]
  323. {
  324. if (auto* node = graph.graph.getNodeForId (pluginID))
  325. node->setBypassed (! node->isBypassed());
  326. repaint();
  327. });
  328. menu->addSeparator();
  329. if (getProcessor()->hasEditor())
  330. menu->addItem ("Show plugin GUI", [this] { showWindow (PluginWindow::Type::normal); });
  331. menu->addItem ("Show all programs", [this] { showWindow (PluginWindow::Type::programs); });
  332. menu->addItem ("Show all parameters", [this] { showWindow (PluginWindow::Type::generic); });
  333. menu->addItem ("Show debug log", [this] { showWindow (PluginWindow::Type::debug); });
  334. #if JUCE_PLUGINHOST_ARA && (JUCE_MAC || JUCE_WINDOWS || JUCE_LINUX)
  335. if (auto* instance = dynamic_cast<AudioPluginInstance*> (getProcessor()))
  336. if (instance->getPluginDescription().hasARAExtension && isNodeUsingARA())
  337. menu->addItem ("Show ARA host controls", [this] { showWindow (PluginWindow::Type::araHost); });
  338. #endif
  339. if (autoScaleOptionAvailable)
  340. addPluginAutoScaleOptionsSubMenu (dynamic_cast<AudioPluginInstance*> (getProcessor()), *menu);
  341. menu->addSeparator();
  342. menu->addItem ("Configure Audio I/O", [this] { showWindow (PluginWindow::Type::audioIO); });
  343. menu->addItem ("Test state save/load", [this] { testStateSaveLoad(); });
  344. #if ! JUCE_IOS && ! JUCE_ANDROID
  345. menu->addSeparator();
  346. menu->addItem ("Save plugin state", [this] { savePluginState(); });
  347. menu->addItem ("Load plugin state", [this] { loadPluginState(); });
  348. #endif
  349. menu->showMenuAsync ({});
  350. }
  351. void testStateSaveLoad()
  352. {
  353. if (auto* processor = getProcessor())
  354. {
  355. MemoryBlock state;
  356. processor->getStateInformation (state);
  357. processor->setStateInformation (state.getData(), (int) state.getSize());
  358. }
  359. }
  360. void showWindow (PluginWindow::Type type)
  361. {
  362. if (auto node = graph.graph.getNodeForId (pluginID))
  363. if (auto* w = graph.getOrCreateWindowFor (node, type))
  364. w->toFront (true);
  365. }
  366. void timerCallback() override
  367. {
  368. // this should only be called on touch devices
  369. jassert (isOnTouchDevice());
  370. stopTimer();
  371. showPopupMenu();
  372. }
  373. void parameterValueChanged (int, float) override
  374. {
  375. // Parameter changes might come from the audio thread or elsewhere, but
  376. // we can only call repaint from the message thread.
  377. triggerAsyncUpdate();
  378. }
  379. void parameterGestureChanged (int, bool) override {}
  380. void handleAsyncUpdate() override { repaint(); }
  381. void savePluginState()
  382. {
  383. fileChooser = std::make_unique<FileChooser> ("Save plugin state");
  384. const auto onChosen = [ref = SafePointer<PluginComponent> (this)] (const FileChooser& chooser)
  385. {
  386. if (ref == nullptr)
  387. return;
  388. const auto result = chooser.getResult();
  389. if (result == File())
  390. return;
  391. if (auto* node = ref->graph.graph.getNodeForId (ref->pluginID))
  392. {
  393. MemoryBlock block;
  394. node->getProcessor()->getStateInformation (block);
  395. result.replaceWithData (block.getData(), block.getSize());
  396. }
  397. };
  398. fileChooser->launchAsync (FileBrowserComponent::saveMode | FileBrowserComponent::warnAboutOverwriting, onChosen);
  399. }
  400. void loadPluginState()
  401. {
  402. fileChooser = std::make_unique<FileChooser> ("Load plugin state");
  403. const auto onChosen = [ref = SafePointer<PluginComponent> (this)] (const FileChooser& chooser)
  404. {
  405. if (ref == nullptr)
  406. return;
  407. const auto result = chooser.getResult();
  408. if (result == File())
  409. return;
  410. if (auto* node = ref->graph.graph.getNodeForId (ref->pluginID))
  411. {
  412. if (auto stream = result.createInputStream())
  413. {
  414. MemoryBlock block;
  415. stream->readIntoMemoryBlock (block);
  416. node->getProcessor()->setStateInformation (block.getData(), (int) block.getSize());
  417. }
  418. }
  419. };
  420. fileChooser->launchAsync (FileBrowserComponent::openMode | FileBrowserComponent::canSelectFiles, onChosen);
  421. }
  422. GraphEditorPanel& panel;
  423. PluginGraph& graph;
  424. const AudioProcessorGraph::NodeID pluginID;
  425. OwnedArray<PinComponent> pins;
  426. int numInputs = 0, numOutputs = 0;
  427. int pinSize = 16;
  428. Point<int> originalPos;
  429. Font font { 13.0f, Font::bold };
  430. int numIns = 0, numOuts = 0;
  431. DropShadowEffect shadow;
  432. std::unique_ptr<PopupMenu> menu;
  433. std::unique_ptr<FileChooser> fileChooser;
  434. const String formatSuffix = getFormatSuffix (getProcessor());
  435. };
  436. //==============================================================================
  437. struct GraphEditorPanel::ConnectorComponent : public Component,
  438. public SettableTooltipClient
  439. {
  440. explicit ConnectorComponent (GraphEditorPanel& p)
  441. : panel (p), graph (p.graph)
  442. {
  443. setAlwaysOnTop (true);
  444. }
  445. void setInput (AudioProcessorGraph::NodeAndChannel newSource)
  446. {
  447. if (connection.source != newSource)
  448. {
  449. connection.source = newSource;
  450. update();
  451. }
  452. }
  453. void setOutput (AudioProcessorGraph::NodeAndChannel newDest)
  454. {
  455. if (connection.destination != newDest)
  456. {
  457. connection.destination = newDest;
  458. update();
  459. }
  460. }
  461. void dragStart (Point<float> pos)
  462. {
  463. lastInputPos = pos;
  464. resizeToFit();
  465. }
  466. void dragEnd (Point<float> pos)
  467. {
  468. lastOutputPos = pos;
  469. resizeToFit();
  470. }
  471. void update()
  472. {
  473. Point<float> p1, p2;
  474. getPoints (p1, p2);
  475. if (lastInputPos != p1 || lastOutputPos != p2)
  476. resizeToFit();
  477. }
  478. void resizeToFit()
  479. {
  480. Point<float> p1, p2;
  481. getPoints (p1, p2);
  482. auto newBounds = Rectangle<float> (p1, p2).expanded (4.0f).getSmallestIntegerContainer();
  483. if (newBounds != getBounds())
  484. setBounds (newBounds);
  485. else
  486. resized();
  487. repaint();
  488. }
  489. void getPoints (Point<float>& p1, Point<float>& p2) const
  490. {
  491. p1 = lastInputPos;
  492. p2 = lastOutputPos;
  493. if (auto* src = panel.getComponentForPlugin (connection.source.nodeID))
  494. p1 = src->getPinPos (connection.source.channelIndex, false);
  495. if (auto* dest = panel.getComponentForPlugin (connection.destination.nodeID))
  496. p2 = dest->getPinPos (connection.destination.channelIndex, true);
  497. }
  498. void paint (Graphics& g) override
  499. {
  500. if (connection.source.isMIDI() || connection.destination.isMIDI())
  501. g.setColour (Colours::red);
  502. else
  503. g.setColour (Colours::green);
  504. g.fillPath (linePath);
  505. }
  506. bool hitTest (int x, int y) override
  507. {
  508. auto pos = Point<int> (x, y).toFloat();
  509. if (hitPath.contains (pos))
  510. {
  511. double distanceFromStart, distanceFromEnd;
  512. getDistancesFromEnds (pos, distanceFromStart, distanceFromEnd);
  513. // avoid clicking the connector when over a pin
  514. return distanceFromStart > 7.0 && distanceFromEnd > 7.0;
  515. }
  516. return false;
  517. }
  518. void mouseDown (const MouseEvent&) override
  519. {
  520. dragging = false;
  521. }
  522. void mouseDrag (const MouseEvent& e) override
  523. {
  524. if (dragging)
  525. {
  526. panel.dragConnector (e);
  527. }
  528. else if (e.mouseWasDraggedSinceMouseDown())
  529. {
  530. dragging = true;
  531. graph.graph.removeConnection (connection);
  532. double distanceFromStart, distanceFromEnd;
  533. getDistancesFromEnds (getPosition().toFloat() + e.position, distanceFromStart, distanceFromEnd);
  534. const bool isNearerSource = (distanceFromStart < distanceFromEnd);
  535. AudioProcessorGraph::NodeAndChannel dummy { {}, 0 };
  536. panel.beginConnectorDrag (isNearerSource ? dummy : connection.source,
  537. isNearerSource ? connection.destination : dummy,
  538. e);
  539. }
  540. }
  541. void mouseUp (const MouseEvent& e) override
  542. {
  543. if (dragging)
  544. panel.endDraggingConnector (e);
  545. }
  546. void resized() override
  547. {
  548. Point<float> p1, p2;
  549. getPoints (p1, p2);
  550. lastInputPos = p1;
  551. lastOutputPos = p2;
  552. p1 -= getPosition().toFloat();
  553. p2 -= getPosition().toFloat();
  554. linePath.clear();
  555. linePath.startNewSubPath (p1);
  556. linePath.cubicTo (p1.x, p1.y + (p2.y - p1.y) * 0.33f,
  557. p2.x, p1.y + (p2.y - p1.y) * 0.66f,
  558. p2.x, p2.y);
  559. PathStrokeType wideStroke (8.0f);
  560. wideStroke.createStrokedPath (hitPath, linePath);
  561. PathStrokeType stroke (2.5f);
  562. stroke.createStrokedPath (linePath, linePath);
  563. auto arrowW = 5.0f;
  564. auto arrowL = 4.0f;
  565. Path arrow;
  566. arrow.addTriangle (-arrowL, arrowW,
  567. -arrowL, -arrowW,
  568. arrowL, 0.0f);
  569. arrow.applyTransform (AffineTransform()
  570. .rotated (MathConstants<float>::halfPi - (float) atan2 (p2.x - p1.x, p2.y - p1.y))
  571. .translated ((p1 + p2) * 0.5f));
  572. linePath.addPath (arrow);
  573. linePath.setUsingNonZeroWinding (true);
  574. }
  575. void getDistancesFromEnds (Point<float> p, double& distanceFromStart, double& distanceFromEnd) const
  576. {
  577. Point<float> p1, p2;
  578. getPoints (p1, p2);
  579. distanceFromStart = p1.getDistanceFrom (p);
  580. distanceFromEnd = p2.getDistanceFrom (p);
  581. }
  582. GraphEditorPanel& panel;
  583. PluginGraph& graph;
  584. AudioProcessorGraph::Connection connection { { {}, 0 }, { {}, 0 } };
  585. Point<float> lastInputPos, lastOutputPos;
  586. Path linePath, hitPath;
  587. bool dragging = false;
  588. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ConnectorComponent)
  589. };
  590. //==============================================================================
  591. GraphEditorPanel::GraphEditorPanel (PluginGraph& g) : graph (g)
  592. {
  593. graph.addChangeListener (this);
  594. setOpaque (true);
  595. }
  596. GraphEditorPanel::~GraphEditorPanel()
  597. {
  598. graph.removeChangeListener (this);
  599. draggingConnector = nullptr;
  600. nodes.clear();
  601. connectors.clear();
  602. }
  603. void GraphEditorPanel::paint (Graphics& g)
  604. {
  605. g.fillAll (getLookAndFeel().findColour (ResizableWindow::backgroundColourId));
  606. }
  607. void GraphEditorPanel::mouseDown (const MouseEvent& e)
  608. {
  609. if (isOnTouchDevice())
  610. {
  611. originalTouchPos = e.position.toInt();
  612. startTimer (750);
  613. }
  614. if (e.mods.isPopupMenu())
  615. showPopupMenu (e.position.toInt());
  616. }
  617. void GraphEditorPanel::mouseUp (const MouseEvent&)
  618. {
  619. if (isOnTouchDevice())
  620. {
  621. stopTimer();
  622. callAfterDelay (250, []() { PopupMenu::dismissAllActiveMenus(); });
  623. }
  624. }
  625. void GraphEditorPanel::mouseDrag (const MouseEvent& e)
  626. {
  627. if (isOnTouchDevice() && e.getDistanceFromDragStart() > 5)
  628. stopTimer();
  629. }
  630. void GraphEditorPanel::createNewPlugin (const PluginDescriptionAndPreference& desc, Point<int> position)
  631. {
  632. graph.addPlugin (desc, position.toDouble() / Point<double> ((double) getWidth(), (double) getHeight()));
  633. }
  634. GraphEditorPanel::PluginComponent* GraphEditorPanel::getComponentForPlugin (AudioProcessorGraph::NodeID nodeID) const
  635. {
  636. for (auto* fc : nodes)
  637. if (fc->pluginID == nodeID)
  638. return fc;
  639. return nullptr;
  640. }
  641. GraphEditorPanel::ConnectorComponent* GraphEditorPanel::getComponentForConnection (const AudioProcessorGraph::Connection& conn) const
  642. {
  643. for (auto* cc : connectors)
  644. if (cc->connection == conn)
  645. return cc;
  646. return nullptr;
  647. }
  648. GraphEditorPanel::PinComponent* GraphEditorPanel::findPinAt (Point<float> pos) const
  649. {
  650. for (auto* fc : nodes)
  651. {
  652. // NB: A Visual Studio optimiser error means we have to put this Component* in a local
  653. // variable before trying to cast it, or it gets mysteriously optimised away..
  654. auto* comp = fc->getComponentAt (pos.toInt() - fc->getPosition());
  655. if (auto* pin = dynamic_cast<PinComponent*> (comp))
  656. return pin;
  657. }
  658. return nullptr;
  659. }
  660. void GraphEditorPanel::resized()
  661. {
  662. updateComponents();
  663. }
  664. void GraphEditorPanel::changeListenerCallback (ChangeBroadcaster*)
  665. {
  666. updateComponents();
  667. }
  668. void GraphEditorPanel::updateComponents()
  669. {
  670. for (int i = nodes.size(); --i >= 0;)
  671. if (graph.graph.getNodeForId (nodes.getUnchecked(i)->pluginID) == nullptr)
  672. nodes.remove (i);
  673. for (int i = connectors.size(); --i >= 0;)
  674. if (! graph.graph.isConnected (connectors.getUnchecked(i)->connection))
  675. connectors.remove (i);
  676. for (auto* fc : nodes)
  677. fc->update();
  678. for (auto* cc : connectors)
  679. cc->update();
  680. for (auto* f : graph.graph.getNodes())
  681. {
  682. if (getComponentForPlugin (f->nodeID) == nullptr)
  683. {
  684. auto* comp = nodes.add (new PluginComponent (*this, f->nodeID));
  685. addAndMakeVisible (comp);
  686. comp->update();
  687. }
  688. }
  689. for (auto& c : graph.graph.getConnections())
  690. {
  691. if (getComponentForConnection (c) == nullptr)
  692. {
  693. auto* comp = connectors.add (new ConnectorComponent (*this));
  694. addAndMakeVisible (comp);
  695. comp->setInput (c.source);
  696. comp->setOutput (c.destination);
  697. }
  698. }
  699. }
  700. void GraphEditorPanel::showPopupMenu (Point<int> mousePos)
  701. {
  702. menu.reset (new PopupMenu);
  703. if (auto* mainWindow = findParentComponentOfClass<MainHostWindow>())
  704. {
  705. mainWindow->addPluginsToMenu (*menu);
  706. menu->showMenuAsync ({},
  707. ModalCallbackFunction::create ([this, mousePos] (int r)
  708. {
  709. if (r > 0)
  710. if (auto* mainWin = findParentComponentOfClass<MainHostWindow>())
  711. createNewPlugin (mainWin->getChosenType (r), mousePos);
  712. }));
  713. }
  714. }
  715. void GraphEditorPanel::beginConnectorDrag (AudioProcessorGraph::NodeAndChannel source,
  716. AudioProcessorGraph::NodeAndChannel dest,
  717. const MouseEvent& e)
  718. {
  719. auto* c = dynamic_cast<ConnectorComponent*> (e.originalComponent);
  720. connectors.removeObject (c, false);
  721. draggingConnector.reset (c);
  722. if (draggingConnector == nullptr)
  723. draggingConnector.reset (new ConnectorComponent (*this));
  724. draggingConnector->setInput (source);
  725. draggingConnector->setOutput (dest);
  726. addAndMakeVisible (draggingConnector.get());
  727. draggingConnector->toFront (false);
  728. dragConnector (e);
  729. }
  730. void GraphEditorPanel::dragConnector (const MouseEvent& e)
  731. {
  732. auto e2 = e.getEventRelativeTo (this);
  733. if (draggingConnector != nullptr)
  734. {
  735. draggingConnector->setTooltip ({});
  736. auto pos = e2.position;
  737. if (auto* pin = findPinAt (pos))
  738. {
  739. auto connection = draggingConnector->connection;
  740. if (connection.source.nodeID == AudioProcessorGraph::NodeID() && ! pin->isInput)
  741. {
  742. connection.source = pin->pin;
  743. }
  744. else if (connection.destination.nodeID == AudioProcessorGraph::NodeID() && pin->isInput)
  745. {
  746. connection.destination = pin->pin;
  747. }
  748. if (graph.graph.canConnect (connection))
  749. {
  750. pos = (pin->getParentComponent()->getPosition() + pin->getBounds().getCentre()).toFloat();
  751. draggingConnector->setTooltip (pin->getTooltip());
  752. }
  753. }
  754. if (draggingConnector->connection.source.nodeID == AudioProcessorGraph::NodeID())
  755. draggingConnector->dragStart (pos);
  756. else
  757. draggingConnector->dragEnd (pos);
  758. }
  759. }
  760. void GraphEditorPanel::endDraggingConnector (const MouseEvent& e)
  761. {
  762. if (draggingConnector == nullptr)
  763. return;
  764. draggingConnector->setTooltip ({});
  765. auto e2 = e.getEventRelativeTo (this);
  766. auto connection = draggingConnector->connection;
  767. draggingConnector = nullptr;
  768. if (auto* pin = findPinAt (e2.position))
  769. {
  770. if (connection.source.nodeID == AudioProcessorGraph::NodeID())
  771. {
  772. if (pin->isInput)
  773. return;
  774. connection.source = pin->pin;
  775. }
  776. else
  777. {
  778. if (! pin->isInput)
  779. return;
  780. connection.destination = pin->pin;
  781. }
  782. graph.graph.addConnection (connection);
  783. }
  784. }
  785. void GraphEditorPanel::timerCallback()
  786. {
  787. // this should only be called on touch devices
  788. jassert (isOnTouchDevice());
  789. stopTimer();
  790. showPopupMenu (originalTouchPos);
  791. }
  792. //==============================================================================
  793. struct GraphDocumentComponent::TooltipBar : public Component,
  794. private Timer
  795. {
  796. TooltipBar()
  797. {
  798. startTimer (100);
  799. }
  800. void paint (Graphics& g) override
  801. {
  802. g.setFont (Font ((float) getHeight() * 0.7f, Font::bold));
  803. g.setColour (Colours::black);
  804. g.drawFittedText (tip, 10, 0, getWidth() - 12, getHeight(), Justification::centredLeft, 1);
  805. }
  806. void timerCallback() override
  807. {
  808. String newTip;
  809. if (auto* underMouse = Desktop::getInstance().getMainMouseSource().getComponentUnderMouse())
  810. if (auto* ttc = dynamic_cast<TooltipClient*> (underMouse))
  811. if (! (underMouse->isMouseButtonDown() || underMouse->isCurrentlyBlockedByAnotherModalComponent()))
  812. newTip = ttc->getTooltip();
  813. if (newTip != tip)
  814. {
  815. tip = newTip;
  816. repaint();
  817. }
  818. }
  819. String tip;
  820. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TooltipBar)
  821. };
  822. //==============================================================================
  823. class GraphDocumentComponent::TitleBarComponent : public Component,
  824. private Button::Listener
  825. {
  826. public:
  827. explicit TitleBarComponent (GraphDocumentComponent& graphDocumentComponent)
  828. : owner (graphDocumentComponent)
  829. {
  830. static const unsigned char burgerMenuPathData[]
  831. = { 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,
  832. 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,
  833. 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,
  834. 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,
  835. 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,
  836. 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,
  837. 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,
  838. 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,
  839. 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,
  840. 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,
  841. 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,
  842. 65,99,101,0,0 };
  843. static const unsigned char pluginListPathData[]
  844. = { 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,
  845. 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,
  846. 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,
  847. 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,
  848. 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,
  849. 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,
  850. 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,
  851. 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,
  852. 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,
  853. 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,
  854. 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,
  855. 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,
  856. 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 };
  857. {
  858. Path p;
  859. p.loadPathFromData (burgerMenuPathData, sizeof (burgerMenuPathData));
  860. burgerButton.setShape (p, true, true, false);
  861. }
  862. {
  863. Path p;
  864. p.loadPathFromData (pluginListPathData, sizeof (pluginListPathData));
  865. pluginButton.setShape (p, true, true, false);
  866. }
  867. burgerButton.addListener (this);
  868. addAndMakeVisible (burgerButton);
  869. pluginButton.addListener (this);
  870. addAndMakeVisible (pluginButton);
  871. titleLabel.setJustificationType (Justification::centredLeft);
  872. addAndMakeVisible (titleLabel);
  873. setOpaque (true);
  874. }
  875. private:
  876. void paint (Graphics& g) override
  877. {
  878. auto titleBarBackgroundColour = getLookAndFeel().findColour (ResizableWindow::backgroundColourId).darker();
  879. g.setColour (titleBarBackgroundColour);
  880. g.fillRect (getLocalBounds());
  881. }
  882. void resized() override
  883. {
  884. auto r = getLocalBounds();
  885. burgerButton.setBounds (r.removeFromLeft (40).withSizeKeepingCentre (20, 20));
  886. pluginButton.setBounds (r.removeFromRight (40).withSizeKeepingCentre (20, 20));
  887. titleLabel.setFont (Font (static_cast<float> (getHeight()) * 0.5f, Font::plain));
  888. titleLabel.setBounds (r);
  889. }
  890. void buttonClicked (Button* b) override
  891. {
  892. owner.showSidePanel (b == &burgerButton);
  893. }
  894. GraphDocumentComponent& owner;
  895. Label titleLabel {"titleLabel", "Plugin Host"};
  896. ShapeButton burgerButton {"burgerButton", Colours::lightgrey, Colours::lightgrey, Colours::white};
  897. ShapeButton pluginButton {"pluginButton", Colours::lightgrey, Colours::lightgrey, Colours::white};
  898. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TitleBarComponent)
  899. };
  900. //==============================================================================
  901. struct GraphDocumentComponent::PluginListBoxModel : public ListBoxModel,
  902. public ChangeListener,
  903. public MouseListener
  904. {
  905. PluginListBoxModel (ListBox& lb, KnownPluginList& kpl)
  906. : owner (lb),
  907. knownPlugins (kpl)
  908. {
  909. knownPlugins.addChangeListener (this);
  910. owner.addMouseListener (this, true);
  911. #if JUCE_IOS
  912. scanner.reset (new AUScanner (knownPlugins));
  913. #endif
  914. }
  915. int getNumRows() override
  916. {
  917. return knownPlugins.getNumTypes();
  918. }
  919. void paintListBoxItem (int rowNumber, Graphics& g,
  920. int width, int height, bool rowIsSelected) override
  921. {
  922. g.fillAll (rowIsSelected ? Colour (0xff42A2C8)
  923. : Colour (0xff263238));
  924. g.setColour (rowIsSelected ? Colours::black : Colours::white);
  925. if (rowNumber < knownPlugins.getNumTypes())
  926. g.drawFittedText (knownPlugins.getTypes()[rowNumber].name, { 0, 0, width, height - 2 }, Justification::centred, 1);
  927. g.setColour (Colours::black.withAlpha (0.4f));
  928. g.drawRect (0, height - 1, width, 1);
  929. }
  930. var getDragSourceDescription (const SparseSet<int>& selectedRows) override
  931. {
  932. if (! isOverSelectedRow)
  933. return var();
  934. return String ("PLUGIN: " + String (selectedRows[0]));
  935. }
  936. void changeListenerCallback (ChangeBroadcaster*) override
  937. {
  938. owner.updateContent();
  939. }
  940. void mouseDown (const MouseEvent& e) override
  941. {
  942. isOverSelectedRow = owner.getRowPosition (owner.getSelectedRow(), true)
  943. .contains (e.getEventRelativeTo (&owner).getMouseDownPosition());
  944. }
  945. ListBox& owner;
  946. KnownPluginList& knownPlugins;
  947. bool isOverSelectedRow = false;
  948. #if JUCE_IOS
  949. std::unique_ptr<AUScanner> scanner;
  950. #endif
  951. JUCE_DECLARE_NON_COPYABLE (PluginListBoxModel)
  952. };
  953. //==============================================================================
  954. GraphDocumentComponent::GraphDocumentComponent (AudioPluginFormatManager& fm,
  955. AudioDeviceManager& dm,
  956. KnownPluginList& kpl)
  957. : graph (new PluginGraph (fm, kpl)),
  958. deviceManager (dm),
  959. pluginList (kpl),
  960. graphPlayer (getAppProperties().getUserSettings()->getBoolValue ("doublePrecisionProcessing", false))
  961. {
  962. init();
  963. deviceManager.addChangeListener (graphPanel.get());
  964. deviceManager.addAudioCallback (&graphPlayer);
  965. deviceManager.addMidiInputDeviceCallback ({}, &graphPlayer.getMidiMessageCollector());
  966. deviceManager.addChangeListener (this);
  967. }
  968. void GraphDocumentComponent::init()
  969. {
  970. updateMidiOutput();
  971. graphPanel.reset (new GraphEditorPanel (*graph));
  972. addAndMakeVisible (graphPanel.get());
  973. graphPlayer.setProcessor (&graph->graph);
  974. keyState.addListener (&graphPlayer.getMidiMessageCollector());
  975. keyboardComp.reset (new MidiKeyboardComponent (keyState, MidiKeyboardComponent::horizontalKeyboard));
  976. addAndMakeVisible (keyboardComp.get());
  977. statusBar.reset (new TooltipBar());
  978. addAndMakeVisible (statusBar.get());
  979. graphPanel->updateComponents();
  980. if (isOnTouchDevice())
  981. {
  982. titleBarComponent.reset (new TitleBarComponent (*this));
  983. addAndMakeVisible (titleBarComponent.get());
  984. pluginListBoxModel.reset (new PluginListBoxModel (pluginListBox, pluginList));
  985. pluginListBox.setModel (pluginListBoxModel.get());
  986. pluginListBox.setRowHeight (40);
  987. pluginListSidePanel.setContent (&pluginListBox, false);
  988. mobileSettingsSidePanel.setContent (new AudioDeviceSelectorComponent (deviceManager,
  989. 0, 2, 0, 2,
  990. true, true, true, false));
  991. addAndMakeVisible (pluginListSidePanel);
  992. addAndMakeVisible (mobileSettingsSidePanel);
  993. }
  994. }
  995. GraphDocumentComponent::~GraphDocumentComponent()
  996. {
  997. if (midiOutput != nullptr)
  998. midiOutput->stopBackgroundThread();
  999. releaseGraph();
  1000. keyState.removeListener (&graphPlayer.getMidiMessageCollector());
  1001. }
  1002. void GraphDocumentComponent::resized()
  1003. {
  1004. auto r = [this]
  1005. {
  1006. auto bounds = getLocalBounds();
  1007. if (auto* display = Desktop::getInstance().getDisplays().getDisplayForRect (getScreenBounds()))
  1008. return display->safeAreaInsets.subtractedFrom (bounds);
  1009. return bounds;
  1010. }();
  1011. const int titleBarHeight = 40;
  1012. const int keysHeight = 60;
  1013. const int statusHeight = 20;
  1014. if (isOnTouchDevice())
  1015. titleBarComponent->setBounds (r.removeFromTop(titleBarHeight));
  1016. keyboardComp->setBounds (r.removeFromBottom (keysHeight));
  1017. statusBar->setBounds (r.removeFromBottom (statusHeight));
  1018. graphPanel->setBounds (r);
  1019. checkAvailableWidth();
  1020. }
  1021. void GraphDocumentComponent::createNewPlugin (const PluginDescriptionAndPreference& desc, Point<int> pos)
  1022. {
  1023. graphPanel->createNewPlugin (desc, pos);
  1024. }
  1025. void GraphDocumentComponent::releaseGraph()
  1026. {
  1027. deviceManager.removeAudioCallback (&graphPlayer);
  1028. deviceManager.removeMidiInputDeviceCallback ({}, &graphPlayer.getMidiMessageCollector());
  1029. if (graphPanel != nullptr)
  1030. {
  1031. deviceManager.removeChangeListener (graphPanel.get());
  1032. graphPanel = nullptr;
  1033. }
  1034. keyboardComp = nullptr;
  1035. statusBar = nullptr;
  1036. graphPlayer.setProcessor (nullptr);
  1037. graph = nullptr;
  1038. }
  1039. bool GraphDocumentComponent::isInterestedInDragSource (const SourceDetails& details)
  1040. {
  1041. return ((dynamic_cast<ListBox*> (details.sourceComponent.get()) != nullptr)
  1042. && details.description.toString().startsWith ("PLUGIN"));
  1043. }
  1044. void GraphDocumentComponent::itemDropped (const SourceDetails& details)
  1045. {
  1046. // don't allow items to be dropped behind the sidebar
  1047. if (pluginListSidePanel.getBounds().contains (details.localPosition))
  1048. return;
  1049. auto pluginTypeIndex = details.description.toString()
  1050. .fromFirstOccurrenceOf ("PLUGIN: ", false, false)
  1051. .getIntValue();
  1052. // must be a valid index!
  1053. jassert (isPositiveAndBelow (pluginTypeIndex, pluginList.getNumTypes()));
  1054. createNewPlugin (PluginDescriptionAndPreference { pluginList.getTypes()[pluginTypeIndex] },
  1055. details.localPosition);
  1056. }
  1057. void GraphDocumentComponent::showSidePanel (bool showSettingsPanel)
  1058. {
  1059. if (showSettingsPanel)
  1060. mobileSettingsSidePanel.showOrHide (true);
  1061. else
  1062. pluginListSidePanel.showOrHide (true);
  1063. checkAvailableWidth();
  1064. lastOpenedSidePanel = showSettingsPanel ? &mobileSettingsSidePanel
  1065. : &pluginListSidePanel;
  1066. }
  1067. void GraphDocumentComponent::hideLastSidePanel()
  1068. {
  1069. if (lastOpenedSidePanel != nullptr)
  1070. lastOpenedSidePanel->showOrHide (false);
  1071. if (mobileSettingsSidePanel.isPanelShowing()) lastOpenedSidePanel = &mobileSettingsSidePanel;
  1072. else if (pluginListSidePanel.isPanelShowing()) lastOpenedSidePanel = &pluginListSidePanel;
  1073. else lastOpenedSidePanel = nullptr;
  1074. }
  1075. void GraphDocumentComponent::checkAvailableWidth()
  1076. {
  1077. if (mobileSettingsSidePanel.isPanelShowing() && pluginListSidePanel.isPanelShowing())
  1078. {
  1079. if (getWidth() - (mobileSettingsSidePanel.getWidth() + pluginListSidePanel.getWidth()) < 150)
  1080. hideLastSidePanel();
  1081. }
  1082. }
  1083. void GraphDocumentComponent::setDoublePrecision (bool doublePrecision)
  1084. {
  1085. graphPlayer.setDoublePrecisionProcessing (doublePrecision);
  1086. }
  1087. bool GraphDocumentComponent::closeAnyOpenPluginWindows()
  1088. {
  1089. return graphPanel->graph.closeAnyOpenPluginWindows();
  1090. }
  1091. void GraphDocumentComponent::changeListenerCallback (ChangeBroadcaster*)
  1092. {
  1093. updateMidiOutput();
  1094. }
  1095. void GraphDocumentComponent::updateMidiOutput()
  1096. {
  1097. auto* defaultMidiOutput = deviceManager.getDefaultMidiOutput();
  1098. if (midiOutput != defaultMidiOutput)
  1099. {
  1100. midiOutput = defaultMidiOutput;
  1101. if (midiOutput != nullptr)
  1102. midiOutput->startBackgroundThread();
  1103. graphPlayer.setMidiOutput (midiOutput);
  1104. }
  1105. }