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.

1334 lines
43KB

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