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.

1347 lines
43KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE 6 technical preview.
  4. Copyright (c) 2017 - ROLI Ltd.
  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. ConnectorComponent (GraphEditorPanel& p) : panel (p), graph (p.graph)
  403. {
  404. setAlwaysOnTop (true);
  405. }
  406. void setInput (AudioProcessorGraph::NodeAndChannel newSource)
  407. {
  408. if (connection.source != newSource)
  409. {
  410. connection.source = newSource;
  411. update();
  412. }
  413. }
  414. void setOutput (AudioProcessorGraph::NodeAndChannel newDest)
  415. {
  416. if (connection.destination != newDest)
  417. {
  418. connection.destination = newDest;
  419. update();
  420. }
  421. }
  422. void dragStart (Point<float> pos)
  423. {
  424. lastInputPos = pos;
  425. resizeToFit();
  426. }
  427. void dragEnd (Point<float> pos)
  428. {
  429. lastOutputPos = pos;
  430. resizeToFit();
  431. }
  432. void update()
  433. {
  434. Point<float> p1, p2;
  435. getPoints (p1, p2);
  436. if (lastInputPos != p1 || lastOutputPos != p2)
  437. resizeToFit();
  438. }
  439. void resizeToFit()
  440. {
  441. Point<float> p1, p2;
  442. getPoints (p1, p2);
  443. auto newBounds = Rectangle<float> (p1, p2).expanded (4.0f).getSmallestIntegerContainer();
  444. if (newBounds != getBounds())
  445. setBounds (newBounds);
  446. else
  447. resized();
  448. repaint();
  449. }
  450. void getPoints (Point<float>& p1, Point<float>& p2) const
  451. {
  452. p1 = lastInputPos;
  453. p2 = lastOutputPos;
  454. if (auto* src = panel.getComponentForPlugin (connection.source.nodeID))
  455. p1 = src->getPinPos (connection.source.channelIndex, false);
  456. if (auto* dest = panel.getComponentForPlugin (connection.destination.nodeID))
  457. p2 = dest->getPinPos (connection.destination.channelIndex, true);
  458. }
  459. void paint (Graphics& g) override
  460. {
  461. if (connection.source.isMIDI() || connection.destination.isMIDI())
  462. g.setColour (Colours::red);
  463. else
  464. g.setColour (Colours::green);
  465. g.fillPath (linePath);
  466. }
  467. bool hitTest (int x, int y) override
  468. {
  469. auto pos = Point<int> (x, y).toFloat();
  470. if (hitPath.contains (pos))
  471. {
  472. double distanceFromStart, distanceFromEnd;
  473. getDistancesFromEnds (pos, distanceFromStart, distanceFromEnd);
  474. // avoid clicking the connector when over a pin
  475. return distanceFromStart > 7.0 && distanceFromEnd > 7.0;
  476. }
  477. return false;
  478. }
  479. void mouseDown (const MouseEvent&) override
  480. {
  481. dragging = false;
  482. }
  483. void mouseDrag (const MouseEvent& e) override
  484. {
  485. if (dragging)
  486. {
  487. panel.dragConnector (e);
  488. }
  489. else if (e.mouseWasDraggedSinceMouseDown())
  490. {
  491. dragging = true;
  492. graph.graph.removeConnection (connection);
  493. double distanceFromStart, distanceFromEnd;
  494. getDistancesFromEnds (getPosition().toFloat() + e.position, distanceFromStart, distanceFromEnd);
  495. const bool isNearerSource = (distanceFromStart < distanceFromEnd);
  496. AudioProcessorGraph::NodeAndChannel dummy { {}, 0 };
  497. panel.beginConnectorDrag (isNearerSource ? dummy : connection.source,
  498. isNearerSource ? connection.destination : dummy,
  499. e);
  500. }
  501. }
  502. void mouseUp (const MouseEvent& e) override
  503. {
  504. if (dragging)
  505. panel.endDraggingConnector (e);
  506. }
  507. void resized() override
  508. {
  509. Point<float> p1, p2;
  510. getPoints (p1, p2);
  511. lastInputPos = p1;
  512. lastOutputPos = p2;
  513. p1 -= getPosition().toFloat();
  514. p2 -= getPosition().toFloat();
  515. linePath.clear();
  516. linePath.startNewSubPath (p1);
  517. linePath.cubicTo (p1.x, p1.y + (p2.y - p1.y) * 0.33f,
  518. p2.x, p1.y + (p2.y - p1.y) * 0.66f,
  519. p2.x, p2.y);
  520. PathStrokeType wideStroke (8.0f);
  521. wideStroke.createStrokedPath (hitPath, linePath);
  522. PathStrokeType stroke (2.5f);
  523. stroke.createStrokedPath (linePath, linePath);
  524. auto arrowW = 5.0f;
  525. auto arrowL = 4.0f;
  526. Path arrow;
  527. arrow.addTriangle (-arrowL, arrowW,
  528. -arrowL, -arrowW,
  529. arrowL, 0.0f);
  530. arrow.applyTransform (AffineTransform()
  531. .rotated (MathConstants<float>::halfPi - (float) atan2 (p2.x - p1.x, p2.y - p1.y))
  532. .translated ((p1 + p2) * 0.5f));
  533. linePath.addPath (arrow);
  534. linePath.setUsingNonZeroWinding (true);
  535. }
  536. void getDistancesFromEnds (Point<float> p, double& distanceFromStart, double& distanceFromEnd) const
  537. {
  538. Point<float> p1, p2;
  539. getPoints (p1, p2);
  540. distanceFromStart = p1.getDistanceFrom (p);
  541. distanceFromEnd = p2.getDistanceFrom (p);
  542. }
  543. GraphEditorPanel& panel;
  544. PluginGraph& graph;
  545. AudioProcessorGraph::Connection connection { { {}, 0 }, { {}, 0 } };
  546. Point<float> lastInputPos, lastOutputPos;
  547. Path linePath, hitPath;
  548. bool dragging = false;
  549. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ConnectorComponent)
  550. };
  551. //==============================================================================
  552. GraphEditorPanel::GraphEditorPanel (PluginGraph& g) : graph (g)
  553. {
  554. graph.addChangeListener (this);
  555. setOpaque (true);
  556. }
  557. GraphEditorPanel::~GraphEditorPanel()
  558. {
  559. graph.removeChangeListener (this);
  560. draggingConnector = nullptr;
  561. nodes.clear();
  562. connectors.clear();
  563. }
  564. void GraphEditorPanel::paint (Graphics& g)
  565. {
  566. g.fillAll (getLookAndFeel().findColour (ResizableWindow::backgroundColourId));
  567. }
  568. void GraphEditorPanel::mouseDown (const MouseEvent& e)
  569. {
  570. if (isOnTouchDevice())
  571. {
  572. originalTouchPos = e.position.toInt();
  573. startTimer (750);
  574. }
  575. if (e.mods.isPopupMenu())
  576. showPopupMenu (e.position.toInt());
  577. }
  578. void GraphEditorPanel::mouseUp (const MouseEvent&)
  579. {
  580. if (isOnTouchDevice())
  581. {
  582. stopTimer();
  583. callAfterDelay (250, []() { PopupMenu::dismissAllActiveMenus(); });
  584. }
  585. }
  586. void GraphEditorPanel::mouseDrag (const MouseEvent& e)
  587. {
  588. if (isOnTouchDevice() && e.getDistanceFromDragStart() > 5)
  589. stopTimer();
  590. }
  591. void GraphEditorPanel::createNewPlugin (const PluginDescription& desc, Point<int> position)
  592. {
  593. graph.addPlugin (desc, position.toDouble() / Point<double> ((double) getWidth(), (double) getHeight()));
  594. }
  595. GraphEditorPanel::PluginComponent* GraphEditorPanel::getComponentForPlugin (AudioProcessorGraph::NodeID nodeID) const
  596. {
  597. for (auto* fc : nodes)
  598. if (fc->pluginID == nodeID)
  599. return fc;
  600. return nullptr;
  601. }
  602. GraphEditorPanel::ConnectorComponent* GraphEditorPanel::getComponentForConnection (const AudioProcessorGraph::Connection& conn) const
  603. {
  604. for (auto* cc : connectors)
  605. if (cc->connection == conn)
  606. return cc;
  607. return nullptr;
  608. }
  609. GraphEditorPanel::PinComponent* GraphEditorPanel::findPinAt (Point<float> pos) const
  610. {
  611. for (auto* fc : nodes)
  612. {
  613. // NB: A Visual Studio optimiser error means we have to put this Component* in a local
  614. // variable before trying to cast it, or it gets mysteriously optimised away..
  615. auto* comp = fc->getComponentAt (pos.toInt() - fc->getPosition());
  616. if (auto* pin = dynamic_cast<PinComponent*> (comp))
  617. return pin;
  618. }
  619. return nullptr;
  620. }
  621. void GraphEditorPanel::resized()
  622. {
  623. updateComponents();
  624. }
  625. void GraphEditorPanel::changeListenerCallback (ChangeBroadcaster*)
  626. {
  627. updateComponents();
  628. }
  629. void GraphEditorPanel::updateComponents()
  630. {
  631. for (int i = nodes.size(); --i >= 0;)
  632. if (graph.graph.getNodeForId (nodes.getUnchecked(i)->pluginID) == nullptr)
  633. nodes.remove (i);
  634. for (int i = connectors.size(); --i >= 0;)
  635. if (! graph.graph.isConnected (connectors.getUnchecked(i)->connection))
  636. connectors.remove (i);
  637. for (auto* fc : nodes)
  638. fc->update();
  639. for (auto* cc : connectors)
  640. cc->update();
  641. for (auto* f : graph.graph.getNodes())
  642. {
  643. if (getComponentForPlugin (f->nodeID) == nullptr)
  644. {
  645. auto* comp = nodes.add (new PluginComponent (*this, f->nodeID));
  646. addAndMakeVisible (comp);
  647. comp->update();
  648. }
  649. }
  650. for (auto& c : graph.graph.getConnections())
  651. {
  652. if (getComponentForConnection (c) == nullptr)
  653. {
  654. auto* comp = connectors.add (new ConnectorComponent (*this));
  655. addAndMakeVisible (comp);
  656. comp->setInput (c.source);
  657. comp->setOutput (c.destination);
  658. }
  659. }
  660. }
  661. void GraphEditorPanel::showPopupMenu (Point<int> mousePos)
  662. {
  663. menu.reset (new PopupMenu);
  664. if (auto* mainWindow = findParentComponentOfClass<MainHostWindow>())
  665. {
  666. mainWindow->addPluginsToMenu (*menu);
  667. menu->showMenuAsync ({},
  668. ModalCallbackFunction::create ([this, mousePos] (int r)
  669. {
  670. if (r > 0)
  671. if (auto* mainWin = findParentComponentOfClass<MainHostWindow>())
  672. createNewPlugin (mainWin->getChosenType (r), mousePos);
  673. }));
  674. }
  675. }
  676. void GraphEditorPanel::beginConnectorDrag (AudioProcessorGraph::NodeAndChannel source,
  677. AudioProcessorGraph::NodeAndChannel dest,
  678. const MouseEvent& e)
  679. {
  680. auto* c = dynamic_cast<ConnectorComponent*> (e.originalComponent);
  681. connectors.removeObject (c, false);
  682. draggingConnector.reset (c);
  683. if (draggingConnector == nullptr)
  684. draggingConnector.reset (new ConnectorComponent (*this));
  685. draggingConnector->setInput (source);
  686. draggingConnector->setOutput (dest);
  687. addAndMakeVisible (draggingConnector.get());
  688. draggingConnector->toFront (false);
  689. dragConnector (e);
  690. }
  691. void GraphEditorPanel::dragConnector (const MouseEvent& e)
  692. {
  693. auto e2 = e.getEventRelativeTo (this);
  694. if (draggingConnector != nullptr)
  695. {
  696. draggingConnector->setTooltip ({});
  697. auto pos = e2.position;
  698. if (auto* pin = findPinAt (pos))
  699. {
  700. auto connection = draggingConnector->connection;
  701. if (connection.source.nodeID == AudioProcessorGraph::NodeID() && ! pin->isInput)
  702. {
  703. connection.source = pin->pin;
  704. }
  705. else if (connection.destination.nodeID == AudioProcessorGraph::NodeID() && pin->isInput)
  706. {
  707. connection.destination = pin->pin;
  708. }
  709. if (graph.graph.canConnect (connection))
  710. {
  711. pos = (pin->getParentComponent()->getPosition() + pin->getBounds().getCentre()).toFloat();
  712. draggingConnector->setTooltip (pin->getTooltip());
  713. }
  714. }
  715. if (draggingConnector->connection.source.nodeID == AudioProcessorGraph::NodeID())
  716. draggingConnector->dragStart (pos);
  717. else
  718. draggingConnector->dragEnd (pos);
  719. }
  720. }
  721. void GraphEditorPanel::endDraggingConnector (const MouseEvent& e)
  722. {
  723. if (draggingConnector == nullptr)
  724. return;
  725. draggingConnector->setTooltip ({});
  726. auto e2 = e.getEventRelativeTo (this);
  727. auto connection = draggingConnector->connection;
  728. draggingConnector = nullptr;
  729. if (auto* pin = findPinAt (e2.position))
  730. {
  731. if (connection.source.nodeID == AudioProcessorGraph::NodeID())
  732. {
  733. if (pin->isInput)
  734. return;
  735. connection.source = pin->pin;
  736. }
  737. else
  738. {
  739. if (! pin->isInput)
  740. return;
  741. connection.destination = pin->pin;
  742. }
  743. graph.graph.addConnection (connection);
  744. }
  745. }
  746. void GraphEditorPanel::timerCallback()
  747. {
  748. // this should only be called on touch devices
  749. jassert (isOnTouchDevice());
  750. stopTimer();
  751. showPopupMenu (originalTouchPos);
  752. }
  753. //==============================================================================
  754. struct GraphDocumentComponent::TooltipBar : public Component,
  755. private Timer
  756. {
  757. TooltipBar()
  758. {
  759. startTimer (100);
  760. }
  761. void paint (Graphics& g) override
  762. {
  763. g.setFont (Font (getHeight() * 0.7f, Font::bold));
  764. g.setColour (Colours::black);
  765. g.drawFittedText (tip, 10, 0, getWidth() - 12, getHeight(), Justification::centredLeft, 1);
  766. }
  767. void timerCallback() override
  768. {
  769. String newTip;
  770. if (auto* underMouse = Desktop::getInstance().getMainMouseSource().getComponentUnderMouse())
  771. if (auto* ttc = dynamic_cast<TooltipClient*> (underMouse))
  772. if (! (underMouse->isMouseButtonDown() || underMouse->isCurrentlyBlockedByAnotherModalComponent()))
  773. newTip = ttc->getTooltip();
  774. if (newTip != tip)
  775. {
  776. tip = newTip;
  777. repaint();
  778. }
  779. }
  780. String tip;
  781. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TooltipBar)
  782. };
  783. //==============================================================================
  784. class GraphDocumentComponent::TitleBarComponent : public Component,
  785. private Button::Listener
  786. {
  787. public:
  788. TitleBarComponent (GraphDocumentComponent& graphDocumentComponent)
  789. : owner (graphDocumentComponent)
  790. {
  791. static const unsigned char burgerMenuPathData[]
  792. = { 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,
  793. 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,
  794. 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,
  795. 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,
  796. 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,
  797. 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,
  798. 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,
  799. 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,
  800. 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,
  801. 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,
  802. 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,
  803. 65,99,101,0,0 };
  804. static const unsigned char pluginListPathData[]
  805. = { 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,
  806. 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,
  807. 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,
  808. 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,
  809. 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,
  810. 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,
  811. 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,
  812. 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,
  813. 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,
  814. 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,
  815. 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,
  816. 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,
  817. 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 };
  818. {
  819. Path p;
  820. p.loadPathFromData (burgerMenuPathData, sizeof (burgerMenuPathData));
  821. burgerButton.setShape (p, true, true, false);
  822. }
  823. {
  824. Path p;
  825. p.loadPathFromData (pluginListPathData, sizeof (pluginListPathData));
  826. pluginButton.setShape (p, true, true, false);
  827. }
  828. burgerButton.addListener (this);
  829. addAndMakeVisible (burgerButton);
  830. pluginButton.addListener (this);
  831. addAndMakeVisible (pluginButton);
  832. titleLabel.setJustificationType (Justification::centredLeft);
  833. addAndMakeVisible (titleLabel);
  834. setOpaque (true);
  835. }
  836. private:
  837. void paint (Graphics& g) override
  838. {
  839. auto titleBarBackgroundColour = getLookAndFeel().findColour (ResizableWindow::backgroundColourId).darker();
  840. g.setColour (titleBarBackgroundColour);
  841. g.fillRect (getLocalBounds());
  842. }
  843. void resized() override
  844. {
  845. auto r = getLocalBounds();
  846. burgerButton.setBounds (r.removeFromLeft (40).withSizeKeepingCentre (20, 20));
  847. pluginButton.setBounds (r.removeFromRight (40).withSizeKeepingCentre (20, 20));
  848. titleLabel.setFont (Font (static_cast<float> (getHeight()) * 0.5f, Font::plain));
  849. titleLabel.setBounds (r);
  850. }
  851. void buttonClicked (Button* b) override
  852. {
  853. owner.showSidePanel (b == &burgerButton);
  854. }
  855. GraphDocumentComponent& owner;
  856. Label titleLabel {"titleLabel", "Plugin Host"};
  857. ShapeButton burgerButton {"burgerButton", Colours::lightgrey, Colours::lightgrey, Colours::white};
  858. ShapeButton pluginButton {"pluginButton", Colours::lightgrey, Colours::lightgrey, Colours::white};
  859. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TitleBarComponent)
  860. };
  861. //==============================================================================
  862. struct GraphDocumentComponent::PluginListBoxModel : public ListBoxModel,
  863. public ChangeListener,
  864. public MouseListener
  865. {
  866. PluginListBoxModel (ListBox& lb, KnownPluginList& kpl)
  867. : owner (lb),
  868. knownPlugins (kpl)
  869. {
  870. knownPlugins.addChangeListener (this);
  871. owner.addMouseListener (this, true);
  872. #if JUCE_IOS
  873. scanner.reset (new AUScanner (knownPlugins));
  874. #endif
  875. }
  876. int getNumRows() override
  877. {
  878. return knownPlugins.getNumTypes();
  879. }
  880. void paintListBoxItem (int rowNumber, Graphics& g,
  881. int width, int height, bool rowIsSelected) override
  882. {
  883. g.fillAll (rowIsSelected ? Colour (0xff42A2C8)
  884. : Colour (0xff263238));
  885. g.setColour (rowIsSelected ? Colours::black : Colours::white);
  886. if (rowNumber < knownPlugins.getNumTypes())
  887. g.drawFittedText (knownPlugins.getTypes()[rowNumber].name, { 0, 0, width, height - 2 }, Justification::centred, 1);
  888. g.setColour (Colours::black.withAlpha (0.4f));
  889. g.drawRect (0, height - 1, width, 1);
  890. }
  891. var getDragSourceDescription (const SparseSet<int>& selectedRows) override
  892. {
  893. if (! isOverSelectedRow)
  894. return var();
  895. return String ("PLUGIN: " + String (selectedRows[0]));
  896. }
  897. void changeListenerCallback (ChangeBroadcaster*) override
  898. {
  899. owner.updateContent();
  900. }
  901. void mouseDown (const MouseEvent& e) override
  902. {
  903. isOverSelectedRow = owner.getRowPosition (owner.getSelectedRow(), true)
  904. .contains (e.getEventRelativeTo (&owner).getMouseDownPosition());
  905. }
  906. ListBox& owner;
  907. KnownPluginList& knownPlugins;
  908. bool isOverSelectedRow = false;
  909. #if JUCE_IOS
  910. std::unique_ptr<AUScanner> scanner;
  911. #endif
  912. JUCE_DECLARE_NON_COPYABLE (PluginListBoxModel)
  913. };
  914. //==============================================================================
  915. GraphDocumentComponent::GraphDocumentComponent (AudioPluginFormatManager& fm,
  916. AudioDeviceManager& dm,
  917. KnownPluginList& kpl)
  918. : graph (new PluginGraph (fm)),
  919. deviceManager (dm),
  920. pluginList (kpl),
  921. graphPlayer (getAppProperties().getUserSettings()->getBoolValue ("doublePrecisionProcessing", false))
  922. {
  923. init();
  924. deviceManager.addChangeListener (graphPanel.get());
  925. deviceManager.addAudioCallback (&graphPlayer);
  926. deviceManager.addMidiInputDeviceCallback ({}, &graphPlayer.getMidiMessageCollector());
  927. deviceManager.addChangeListener (this);
  928. }
  929. void GraphDocumentComponent::init()
  930. {
  931. updateMidiOutput();
  932. graphPanel.reset (new GraphEditorPanel (*graph));
  933. addAndMakeVisible (graphPanel.get());
  934. graphPlayer.setProcessor (&graph->graph);
  935. keyState.addListener (&graphPlayer.getMidiMessageCollector());
  936. keyboardComp.reset (new MidiKeyboardComponent (keyState, MidiKeyboardComponent::horizontalKeyboard));
  937. addAndMakeVisible (keyboardComp.get());
  938. statusBar.reset (new TooltipBar());
  939. addAndMakeVisible (statusBar.get());
  940. graphPanel->updateComponents();
  941. if (isOnTouchDevice())
  942. {
  943. if (isOnTouchDevice())
  944. {
  945. titleBarComponent.reset (new TitleBarComponent (*this));
  946. addAndMakeVisible (titleBarComponent.get());
  947. }
  948. pluginListBoxModel.reset (new PluginListBoxModel (pluginListBox, pluginList));
  949. pluginListBox.setModel (pluginListBoxModel.get());
  950. pluginListBox.setRowHeight (40);
  951. pluginListSidePanel.setContent (&pluginListBox, false);
  952. mobileSettingsSidePanel.setContent (new AudioDeviceSelectorComponent (deviceManager,
  953. 0, 2, 0, 2,
  954. true, true, true, false));
  955. if (isOnTouchDevice())
  956. {
  957. addAndMakeVisible (pluginListSidePanel);
  958. addAndMakeVisible (mobileSettingsSidePanel);
  959. }
  960. }
  961. }
  962. GraphDocumentComponent::~GraphDocumentComponent()
  963. {
  964. if (midiOutput != nullptr)
  965. midiOutput->stopBackgroundThread();
  966. releaseGraph();
  967. keyState.removeListener (&graphPlayer.getMidiMessageCollector());
  968. }
  969. void GraphDocumentComponent::resized()
  970. {
  971. auto r = getLocalBounds();
  972. const int titleBarHeight = 40;
  973. const int keysHeight = 60;
  974. const int statusHeight = 20;
  975. if (isOnTouchDevice())
  976. titleBarComponent->setBounds (r.removeFromTop(titleBarHeight));
  977. keyboardComp->setBounds (r.removeFromBottom (keysHeight));
  978. statusBar->setBounds (r.removeFromBottom (statusHeight));
  979. graphPanel->setBounds (r);
  980. checkAvailableWidth();
  981. }
  982. void GraphDocumentComponent::createNewPlugin (const PluginDescription& desc, Point<int> pos)
  983. {
  984. graphPanel->createNewPlugin (desc, pos);
  985. }
  986. void GraphDocumentComponent::unfocusKeyboardComponent()
  987. {
  988. keyboardComp->unfocusAllComponents();
  989. }
  990. void GraphDocumentComponent::releaseGraph()
  991. {
  992. deviceManager.removeAudioCallback (&graphPlayer);
  993. deviceManager.removeMidiInputDeviceCallback ({}, &graphPlayer.getMidiMessageCollector());
  994. if (graphPanel != nullptr)
  995. {
  996. deviceManager.removeChangeListener (graphPanel.get());
  997. graphPanel = nullptr;
  998. }
  999. keyboardComp = nullptr;
  1000. statusBar = nullptr;
  1001. graphPlayer.setProcessor (nullptr);
  1002. graph = nullptr;
  1003. }
  1004. bool GraphDocumentComponent::isInterestedInDragSource (const SourceDetails& details)
  1005. {
  1006. return ((dynamic_cast<ListBox*> (details.sourceComponent.get()) != nullptr)
  1007. && details.description.toString().startsWith ("PLUGIN"));
  1008. }
  1009. void GraphDocumentComponent::itemDropped (const SourceDetails& details)
  1010. {
  1011. // don't allow items to be dropped behind the sidebar
  1012. if (pluginListSidePanel.getBounds().contains (details.localPosition))
  1013. return;
  1014. auto pluginTypeIndex = details.description.toString()
  1015. .fromFirstOccurrenceOf ("PLUGIN: ", false, false)
  1016. .getIntValue();
  1017. // must be a valid index!
  1018. jassert (isPositiveAndBelow (pluginTypeIndex, pluginList.getNumTypes()));
  1019. createNewPlugin (pluginList.getTypes()[pluginTypeIndex], details.localPosition);
  1020. }
  1021. void GraphDocumentComponent::showSidePanel (bool showSettingsPanel)
  1022. {
  1023. if (showSettingsPanel)
  1024. mobileSettingsSidePanel.showOrHide (true);
  1025. else
  1026. pluginListSidePanel.showOrHide (true);
  1027. checkAvailableWidth();
  1028. lastOpenedSidePanel = showSettingsPanel ? &mobileSettingsSidePanel
  1029. : &pluginListSidePanel;
  1030. }
  1031. void GraphDocumentComponent::hideLastSidePanel()
  1032. {
  1033. if (lastOpenedSidePanel != nullptr)
  1034. lastOpenedSidePanel->showOrHide (false);
  1035. if (mobileSettingsSidePanel.isPanelShowing()) lastOpenedSidePanel = &mobileSettingsSidePanel;
  1036. else if (pluginListSidePanel.isPanelShowing()) lastOpenedSidePanel = &pluginListSidePanel;
  1037. else lastOpenedSidePanel = nullptr;
  1038. }
  1039. void GraphDocumentComponent::checkAvailableWidth()
  1040. {
  1041. if (mobileSettingsSidePanel.isPanelShowing() && pluginListSidePanel.isPanelShowing())
  1042. {
  1043. if (getWidth() - (mobileSettingsSidePanel.getWidth() + pluginListSidePanel.getWidth()) < 150)
  1044. hideLastSidePanel();
  1045. }
  1046. }
  1047. void GraphDocumentComponent::setDoublePrecision (bool doublePrecision)
  1048. {
  1049. graphPlayer.setDoublePrecisionProcessing (doublePrecision);
  1050. }
  1051. bool GraphDocumentComponent::closeAnyOpenPluginWindows()
  1052. {
  1053. return graphPanel->graph.closeAnyOpenPluginWindows();
  1054. }
  1055. void GraphDocumentComponent::changeListenerCallback (ChangeBroadcaster*)
  1056. {
  1057. updateMidiOutput();
  1058. }
  1059. void GraphDocumentComponent::updateMidiOutput()
  1060. {
  1061. auto* defaultMidiOutput = deviceManager.getDefaultMidiOutput();
  1062. if (midiOutput != defaultMidiOutput)
  1063. {
  1064. midiOutput = defaultMidiOutput;
  1065. if (midiOutput != nullptr)
  1066. midiOutput->startBackgroundThread();
  1067. graphPlayer.setMidiOutput (midiOutput);
  1068. }
  1069. }