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.

1326 lines
43KB

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