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.

1348 lines
43KB

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