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.

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