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.

994 lines
38KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE examples.
  4. Copyright (c) 2017 - ROLI Ltd.
  5. The code included in this file is provided under the terms of the ISC license
  6. http://www.isc.org/downloads/software-support-policy/isc-license. Permission
  7. To use, copy, modify, and/or distribute this software for any purpose with or
  8. without fee is hereby granted provided that the above copyright notice and
  9. this permission notice appear in all copies.
  10. THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES,
  11. WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR
  12. PURPOSE, ARE DISCLAIMED.
  13. ==============================================================================
  14. */
  15. /*******************************************************************************
  16. The block below describes the properties of this PIP. A PIP is a short snippet
  17. of code that can be read by the Projucer and used to generate a JUCE project.
  18. BEGIN_JUCE_PIP_METADATA
  19. name: BlocksMonitorDemo
  20. version: 1.0.0
  21. vendor: JUCE
  22. website: http://juce.com
  23. description: Application to monitor Blocks devices.
  24. dependencies: juce_audio_basics, juce_audio_devices, juce_audio_formats,
  25. juce_audio_processors, juce_audio_utils, juce_blocks_basics,
  26. juce_core, juce_data_structures, juce_events, juce_graphics,
  27. juce_gui_basics, juce_gui_extra
  28. exporters: xcode_mac, vs2017, linux_make, xcode_iphone
  29. moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1
  30. type: Component
  31. mainClass: BlocksMonitorDemo
  32. useLocalCopy: 1
  33. END_JUCE_PIP_METADATA
  34. *******************************************************************************/
  35. #pragma once
  36. //==============================================================================
  37. /**
  38. Base class that renders a Block on the screen
  39. */
  40. class BlockComponent : public Component,
  41. public SettableTooltipClient,
  42. private TouchSurface::Listener,
  43. private ControlButton::Listener,
  44. private Timer
  45. {
  46. public:
  47. BlockComponent (Block::Ptr blockToUse)
  48. : block (blockToUse)
  49. {
  50. updateStatsAndTooltip();
  51. // Register BlockComponent as a listener to the touch surface
  52. if (auto touchSurface = block->getTouchSurface())
  53. touchSurface->addListener (this);
  54. // Register BlockComponent as a listener to any buttons
  55. for (auto button : block->getButtons())
  56. button->addListener (this);
  57. // If this is a Lightpad then set the grid program to be blank
  58. if (block->getLEDGrid() != nullptr)
  59. block->setProgram (new BitmapLEDProgram (*block));
  60. // If this is a Lightpad then redraw it at 25Hz
  61. if (block->getType() == Block::lightPadBlock)
  62. startTimerHz (25);
  63. // Make sure the component can't go offscreen if it is draggable
  64. constrainer.setMinimumOnscreenAmounts (50, 50, 50, 50);
  65. }
  66. ~BlockComponent()
  67. {
  68. // Remove any listeners
  69. if (auto touchSurface = block->getTouchSurface())
  70. touchSurface->removeListener (this);
  71. for (auto button : block->getButtons())
  72. button->removeListener (this);
  73. }
  74. /** Called periodically to update the tooltip with inforamtion about the Block */
  75. void updateStatsAndTooltip()
  76. {
  77. // Get the battery level of this Block and inform any subclasses
  78. auto batteryLevel = block->getBatteryLevel();
  79. handleBatteryLevelUpdate (batteryLevel);
  80. // Update the tooltip
  81. setTooltip ("Name = " + block->getDeviceDescription() + "\n"
  82. + "UID = " + String (block->uid) + "\n"
  83. + "Serial number = " + block->serialNumber + "\n"
  84. + "Battery level = " + String ((int) (batteryLevel * 100)) + "%"
  85. + (block->isBatteryCharging() ? "++"
  86. : "--"));
  87. }
  88. /** Subclasses should override this to paint the Block object on the screen */
  89. void paint (Graphics&) override = 0;
  90. /** Subclasses can override this to receive button down events from the Block */
  91. virtual void handleButtonPressed (ControlButton::ButtonFunction, uint32) {}
  92. /** Subclasses can override this to receive button up events from the Block */
  93. virtual void handleButtonReleased (ControlButton::ButtonFunction, uint32) {}
  94. /** Subclasses can override this to receive touch events from the Block */
  95. virtual void handleTouchChange (TouchSurface::Touch) {}
  96. /** Subclasses can override this to battery level updates from the Block */
  97. virtual void handleBatteryLevelUpdate (float) {}
  98. /** The Block object that this class represents */
  99. Block::Ptr block;
  100. //==============================================================================
  101. /** Returns an integer index corresponding to a physical position on the hardware
  102. for each type of Control Block. */
  103. static int controlButtonFunctionToIndex (ControlButton::ButtonFunction f)
  104. {
  105. using CB = ControlButton;
  106. static Array<ControlButton::ButtonFunction> map[] =
  107. {
  108. { CB::mode, CB::button0, CB::velocitySensitivity },
  109. { CB::volume, CB::button1, CB::glideSensitivity },
  110. { CB::scale, CB::button2, CB::slideSensitivity, CB::click },
  111. { CB::chord, CB::button3, CB::pressSensitivity, CB::snap },
  112. { CB::arp, CB::button4, CB::liftSensitivity, CB::back },
  113. { CB::sustain, CB::button5, CB::fixedVelocity, CB::playOrPause },
  114. { CB::octave, CB::button6, CB::glideLock, CB::record },
  115. { CB::love, CB::button7, CB::pianoMode, CB::learn },
  116. { CB::up },
  117. { CB::down }
  118. };
  119. for (auto i = 0; i < numElementsInArray (map); ++i)
  120. if (map[i].contains (f))
  121. return i;
  122. return -1;
  123. }
  124. Point<float> getOffsetForPort (Block::ConnectionPort port)
  125. {
  126. using e = Block::ConnectionPort::DeviceEdge;
  127. switch (rotation)
  128. {
  129. case 0:
  130. {
  131. switch (port.edge)
  132. {
  133. case e::north:
  134. return { static_cast<float> (port.index), 0.0f };
  135. case e::east:
  136. return { static_cast<float> (block->getWidth()), static_cast<float> (port.index) };
  137. case e::south:
  138. return { static_cast<float> (port.index), static_cast<float> (block->getHeight()) };
  139. case e::west:
  140. return { 0.0f, static_cast<float> (port.index) };
  141. }
  142. }
  143. case 90:
  144. {
  145. switch (port.edge)
  146. {
  147. case e::north:
  148. return { 0.0f, static_cast<float> (port.index) };
  149. case e::east:
  150. return { static_cast<float> (-1.0f - port.index), static_cast<float> (block->getWidth()) };
  151. case e::south:
  152. return { static_cast<float> (0.0f - block->getHeight()), static_cast<float> (port.index) };
  153. case e::west:
  154. return { static_cast<float> (-1.0f - port.index), 0.0f };
  155. }
  156. }
  157. case 180:
  158. {
  159. switch (port.edge)
  160. {
  161. case e::north:
  162. return { static_cast<float> (-1.0f - port.index), 0.0f };
  163. case e::east:
  164. return { static_cast<float> (0.0f - block->getWidth()), static_cast<float> (-1.0f - port.index) };
  165. case e::south:
  166. return { static_cast<float> (-1.0f - port.index), static_cast<float> (0.0f - block->getHeight()) };
  167. case e::west:
  168. return { 0.0f, static_cast<float> (-1.0f - port.index) };
  169. }
  170. }
  171. case 270:
  172. {
  173. switch (port.edge)
  174. {
  175. case e::north:
  176. return { 0.0f, static_cast<float> (-1.0f - port.index) };
  177. case e::east:
  178. return { static_cast<float> (port.index), static_cast<float> (0 - block->getWidth()) };
  179. case e::south:
  180. return { static_cast<float> (block->getHeight()), static_cast<float> (-1.0f - port.index) };
  181. case e::west:
  182. return { static_cast<float> (port.index), 0.0f };
  183. }
  184. }
  185. }
  186. return {};
  187. }
  188. int rotation = 0;
  189. Point<float> topLeft = { 0.0f, 0.0f };
  190. private:
  191. /** Used to call repaint() periodically */
  192. void timerCallback() override { repaint(); }
  193. /** Overridden from TouchSurface::Listener */
  194. void touchChanged (TouchSurface&, const TouchSurface::Touch& t) override { handleTouchChange (t); }
  195. /** Overridden from ControlButton::Listener */
  196. void buttonPressed (ControlButton& b, Block::Timestamp t) override { handleButtonPressed (b.getType(), t); }
  197. /** Overridden from ControlButton::Listener */
  198. void buttonReleased (ControlButton& b, Block::Timestamp t) override { handleButtonReleased (b.getType(), t); }
  199. /** Overridden from MouseListener. Prepares the master Block component for dragging. */
  200. void mouseDown (const MouseEvent& e) override
  201. {
  202. if (block->isMasterBlock())
  203. componentDragger.startDraggingComponent (this, e);
  204. }
  205. /** Overridden from MouseListener. Drags the master Block component */
  206. void mouseDrag (const MouseEvent& e) override
  207. {
  208. if (block->isMasterBlock())
  209. {
  210. componentDragger.dragComponent (this, e, &constrainer);
  211. getParentComponent()->resized();
  212. }
  213. }
  214. ComponentDragger componentDragger;
  215. ComponentBoundsConstrainer constrainer;
  216. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BlockComponent)
  217. };
  218. //==============================================================================
  219. /**
  220. Class that renders a Lightpad on the screen
  221. */
  222. class LightpadComponent : public BlockComponent
  223. {
  224. public:
  225. LightpadComponent (Block::Ptr blockToUse)
  226. : BlockComponent (blockToUse)
  227. {}
  228. void paint (Graphics& g) override
  229. {
  230. auto r = getLocalBounds().toFloat();
  231. // clip the drawing area to only draw in the block area
  232. {
  233. Path clipArea;
  234. clipArea.addRoundedRectangle (r, r.getWidth() / 20.0f);
  235. g.reduceClipRegion (clipArea);
  236. }
  237. // Fill a black square for the Lightpad
  238. g.fillAll (Colours::black);
  239. // size ration between physical and on-screen blocks
  240. Point<float> ratio (r.getWidth() / block->getWidth(),
  241. r.getHeight() / block->getHeight());
  242. auto maxCircleSize = block->getWidth() / 3.0f;
  243. // iterate over the list of current touches and draw them on the onscreen Block
  244. for (auto touch : touches)
  245. {
  246. auto circleSize = touch.touch.z * maxCircleSize;
  247. Point<float> touchPosition (touch.touch.x,
  248. touch.touch.y);
  249. auto blob = Rectangle<float> (circleSize, circleSize)
  250. .withCentre (touchPosition) * ratio;
  251. ColourGradient cg (colourArray[touch.touch.index], blob.getCentreX(), blob.getCentreY(),
  252. Colours::transparentBlack, blob.getRight(), blob.getBottom(),
  253. true);
  254. g.setGradientFill (cg);
  255. g.fillEllipse (blob);
  256. }
  257. }
  258. void handleTouchChange (TouchSurface::Touch touch) override { touches.updateTouch (touch); }
  259. private:
  260. /** An Array of colours to use for touches */
  261. Array<Colour> colourArray = { Colours::red,
  262. Colours::blue,
  263. Colours::green,
  264. Colours::yellow,
  265. Colours::white,
  266. Colours::hotpink,
  267. Colours::mediumpurple };
  268. /** A list of current Touch events */
  269. TouchList<TouchSurface::Touch> touches;
  270. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LightpadComponent)
  271. };
  272. //==============================================================================
  273. /**
  274. Class that renders a Control Block on the screen
  275. */
  276. class ControlBlockComponent : public BlockComponent
  277. {
  278. public:
  279. ControlBlockComponent (Block::Ptr blockToUse)
  280. : BlockComponent (blockToUse),
  281. numLeds (block->getLEDRow()->getNumLEDs())
  282. {
  283. addAndMakeVisible (roundedRectangleButton);
  284. // Display the battery level on the LEDRow
  285. auto numLedsToTurnOn = static_cast<int> (numLeds * block->getBatteryLevel());
  286. // add LEDs
  287. for (auto i = 0; i < numLeds; ++i)
  288. {
  289. auto ledComponent = new LEDComponent();
  290. ledComponent->setOnState (i < numLedsToTurnOn);
  291. addAndMakeVisible (leds.add (ledComponent));
  292. }
  293. previousNumLedsOn = numLedsToTurnOn;
  294. // add buttons
  295. for (auto i = 0; i < 8; ++i)
  296. addAndMakeVisible (circleButtons[i]);
  297. }
  298. void resized() override
  299. {
  300. auto r = getLocalBounds().reduced (10);
  301. auto rowHeight = r.getHeight() / 5;
  302. auto ledWidth = (r.getWidth() - 70) / numLeds;
  303. auto buttonWidth = (r.getWidth() - 40) / 5;
  304. auto row = r;
  305. auto ledRow = row.removeFromTop (rowHeight) .withSizeKeepingCentre (r.getWidth(), ledWidth);
  306. auto buttonRow1 = row.removeFromTop (rowHeight * 2).withSizeKeepingCentre (r.getWidth(), buttonWidth);
  307. auto buttonRow2 = row.removeFromTop (rowHeight * 2).withSizeKeepingCentre (r.getWidth(), buttonWidth);
  308. for (auto* led : leds)
  309. {
  310. led->setBounds (ledRow.removeFromLeft (ledWidth).reduced (2));
  311. ledRow.removeFromLeft (5);
  312. }
  313. for (auto i = 0; i < 5; ++i)
  314. {
  315. circleButtons[i].setBounds (buttonRow1.removeFromLeft (buttonWidth).reduced (2));
  316. buttonRow1.removeFromLeft (10);
  317. }
  318. for (auto i = 5; i < 8; ++i)
  319. {
  320. circleButtons[i].setBounds (buttonRow2.removeFromLeft (buttonWidth).reduced (2));
  321. buttonRow2.removeFromLeft (10);
  322. }
  323. roundedRectangleButton.setBounds (buttonRow2);
  324. }
  325. void paint (Graphics& g) override
  326. {
  327. auto r = getLocalBounds().toFloat();
  328. // Fill a black rectangle for the Control Block
  329. g.setColour (Colours::black);
  330. g.fillRoundedRectangle (r, r.getWidth() / 20.0f);
  331. }
  332. void handleButtonPressed (ControlButton::ButtonFunction function, uint32) override
  333. {
  334. displayButtonInteraction (controlButtonFunctionToIndex (function), true);
  335. }
  336. void handleButtonReleased (ControlButton::ButtonFunction function, uint32) override
  337. {
  338. displayButtonInteraction (controlButtonFunctionToIndex (function), false);
  339. }
  340. void handleBatteryLevelUpdate (float batteryLevel) override
  341. {
  342. // Update the number of LEDs that are on to represent the battery level
  343. auto numLedsOn = static_cast<int> (numLeds * batteryLevel);
  344. if (numLedsOn != previousNumLedsOn)
  345. for (auto i = 0; i < numLeds; ++i)
  346. leds.getUnchecked (i)->setOnState (i < numLedsOn);
  347. previousNumLedsOn = numLedsOn;
  348. repaint();
  349. }
  350. private:
  351. //==============================================================================
  352. /**
  353. Base class that renders a Control Block button
  354. */
  355. struct ControlBlockSubComponent : public Component,
  356. public TooltipClient
  357. {
  358. ControlBlockSubComponent (Colour componentColourToUse)
  359. : componentColour (componentColourToUse)
  360. {}
  361. /** Subclasses should override this to paint the button on the screen */
  362. void paint (Graphics&) override = 0;
  363. /** Sets the colour of the button */
  364. void setColour (Colour c) { componentColour = c; }
  365. /** Sets the on state of the button */
  366. void setOnState (bool isOn)
  367. {
  368. onState = isOn;
  369. repaint();
  370. }
  371. /** Returns the Control Block tooltip */
  372. String getTooltip() override
  373. {
  374. for (Component* comp = this; comp != nullptr; comp = comp->getParentComponent())
  375. if (auto* sttc = dynamic_cast<SettableTooltipClient*> (comp))
  376. return sttc->getTooltip();
  377. return {};
  378. }
  379. //==============================================================================
  380. Colour componentColour;
  381. bool onState = false;
  382. //==============================================================================
  383. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ControlBlockSubComponent)
  384. };
  385. /**
  386. Class that renders a Control Block LED on the screen
  387. */
  388. struct LEDComponent : public ControlBlockSubComponent
  389. {
  390. LEDComponent() : ControlBlockSubComponent (Colours::green) {}
  391. void paint (Graphics& g) override
  392. {
  393. g.setColour (componentColour.withAlpha (onState ? 1.0f : 0.2f));
  394. g.fillEllipse (getLocalBounds().toFloat());
  395. }
  396. };
  397. /**
  398. Class that renders a Control Block single circular button on the screen
  399. */
  400. struct CircleButtonComponent : public ControlBlockSubComponent
  401. {
  402. CircleButtonComponent() : ControlBlockSubComponent (Colours::blue) {}
  403. void paint (Graphics& g) override
  404. {
  405. g.setColour (componentColour.withAlpha (onState ? 1.0f : 0.2f));
  406. g.fillEllipse (getLocalBounds().toFloat());
  407. }
  408. };
  409. /**
  410. Class that renders a Control Block rounded rectangular button containing two buttons
  411. on the screen
  412. */
  413. struct RoundedRectangleButtonComponent : public ControlBlockSubComponent
  414. {
  415. RoundedRectangleButtonComponent() : ControlBlockSubComponent (Colours::blue) {}
  416. void paint (Graphics& g) override
  417. {
  418. auto r = getLocalBounds().toFloat();
  419. g.setColour (componentColour.withAlpha (0.2f));
  420. g.fillRoundedRectangle (r.toFloat(), 20.0f);
  421. g.setColour (componentColour.withAlpha (1.0f));
  422. // is a button pressed?
  423. if (doubleButtonOnState[0] || doubleButtonOnState[1])
  424. {
  425. auto semiButtonWidth = r.getWidth() / 2.0f;
  426. auto semiButtonBounds = r.withWidth (semiButtonWidth)
  427. .withX (doubleButtonOnState[1] ? semiButtonWidth : 0)
  428. .reduced (5.0f, 2.0f);
  429. g.fillEllipse (semiButtonBounds);
  430. }
  431. }
  432. void setPressedState (bool isPressed, int button)
  433. {
  434. doubleButtonOnState[button] = isPressed;
  435. repaint();
  436. }
  437. private:
  438. bool doubleButtonOnState[2] = { false, false };
  439. };
  440. /** Displays a button press or release interaction for a button at a given index */
  441. void displayButtonInteraction (int buttonIndex, bool isPressed)
  442. {
  443. if (! isPositiveAndBelow (buttonIndex, 10))
  444. return;
  445. if (buttonIndex >= 8)
  446. roundedRectangleButton.setPressedState (isPressed, buttonIndex == 8);
  447. else
  448. circleButtons[buttonIndex].setOnState (isPressed);
  449. }
  450. //==============================================================================
  451. int numLeds;
  452. OwnedArray<LEDComponent> leds;
  453. CircleButtonComponent circleButtons[8];
  454. RoundedRectangleButtonComponent roundedRectangleButton;
  455. int previousNumLedsOn;
  456. //==============================================================================
  457. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ControlBlockComponent)
  458. };
  459. //==============================================================================
  460. /**
  461. The main component where the Block components will be displayed
  462. */
  463. class BlocksMonitorDemo : public Component,
  464. public TopologySource::Listener,
  465. private Timer
  466. {
  467. public:
  468. BlocksMonitorDemo()
  469. {
  470. noBlocksLabel.setText ("No BLOCKS connected...", dontSendNotification);
  471. noBlocksLabel.setJustificationType (Justification::centred);
  472. zoomOutButton.setButtonText ("+");
  473. zoomOutButton.onClick = [this] { blockUnitInPixels = (int) (blockUnitInPixels * 1.05f); resized(); };
  474. zoomOutButton.setAlwaysOnTop (true);
  475. zoomInButton.setButtonText ("-");
  476. zoomInButton.onClick = [this] { blockUnitInPixels = (int) (blockUnitInPixels * 0.95f); resized(); };
  477. zoomInButton.setAlwaysOnTop (true);
  478. // Register BlocksMonitorDemo as a listener to the PhysicalTopologySource object
  479. topologySource.addListener (this);
  480. startTimer (10000);
  481. addAndMakeVisible (noBlocksLabel);
  482. addAndMakeVisible (zoomOutButton);
  483. addAndMakeVisible (zoomInButton);
  484. #if JUCE_IOS
  485. connectButton.setButtonText ("Connect");
  486. connectButton.onClick = [] { BluetoothMidiDevicePairingDialogue::open(); };
  487. connectButton.setAlwaysOnTop (true);
  488. addAndMakeVisible (connectButton);
  489. #endif
  490. setSize (600, 600);
  491. topologyChanged();
  492. }
  493. ~BlocksMonitorDemo()
  494. {
  495. topologySource.removeListener (this);
  496. }
  497. void paint (Graphics&) override {}
  498. void resized() override
  499. {
  500. #if JUCE_IOS
  501. connectButton.setBounds (getRight() - 100, 20, 80, 30);
  502. #endif
  503. noBlocksLabel.setVisible (false);
  504. auto numBlockComponents = blockComponents.size();
  505. // If there are no currently connected Blocks then display some text on the screen
  506. if (numBlockComponents == 0)
  507. {
  508. noBlocksLabel.setVisible (true);
  509. noBlocksLabel.setBounds (0, (getHeight() / 2) - 50, getWidth(), 100);
  510. return;
  511. }
  512. zoomOutButton.setBounds (10, getHeight() - 40, 40, 30);
  513. zoomInButton.setBounds (zoomOutButton.getRight(), zoomOutButton.getY(), 40, 30);
  514. if (isInitialResized)
  515. {
  516. // Work out the area needed in terms of Block units
  517. Rectangle<float> maxArea;
  518. for (auto blockComponent : blockComponents)
  519. {
  520. auto topLeft = blockComponent->topLeft;
  521. auto rotation = blockComponent->rotation;
  522. auto blockSize = 0;
  523. if (rotation == 180)
  524. blockSize = blockComponent->block->getWidth();
  525. else if (rotation == 90)
  526. blockSize = blockComponent->block->getHeight();
  527. if (topLeft.x - blockSize < maxArea.getX())
  528. maxArea.setX (topLeft.x - blockSize);
  529. blockSize = 0;
  530. if (rotation == 0)
  531. blockSize = blockComponent->block->getWidth();
  532. else if (rotation == 270)
  533. blockSize = blockComponent->block->getHeight();
  534. if (topLeft.x + blockSize > maxArea.getRight())
  535. maxArea.setWidth (topLeft.x + blockSize);
  536. blockSize = 0;
  537. if (rotation == 180)
  538. blockSize = blockComponent->block->getHeight();
  539. else if (rotation == 270)
  540. blockSize = blockComponent->block->getWidth();
  541. if (topLeft.y - blockSize < maxArea.getY())
  542. maxArea.setY (topLeft.y - blockSize);
  543. blockSize = 0;
  544. if (rotation == 0)
  545. blockSize = blockComponent->block->getHeight();
  546. else if (rotation == 90)
  547. blockSize = blockComponent->block->getWidth();
  548. if (topLeft.y + blockSize > maxArea.getBottom())
  549. maxArea.setHeight (topLeft.y + blockSize);
  550. }
  551. auto totalWidth = std::abs (maxArea.getX()) + maxArea.getWidth();
  552. auto totalHeight = std::abs (maxArea.getY()) + maxArea.getHeight();
  553. blockUnitInPixels = static_cast<int> (jmin ((getHeight() / totalHeight) - 50, (getWidth() / totalWidth) - 50));
  554. masterBlockComponent->centreWithSize (masterBlockComponent->block->getWidth() * blockUnitInPixels,
  555. masterBlockComponent->block->getHeight() * blockUnitInPixels);
  556. isInitialResized = false;
  557. }
  558. else
  559. {
  560. masterBlockComponent->setSize (masterBlockComponent->block->getWidth() * blockUnitInPixels, masterBlockComponent->block->getHeight() * blockUnitInPixels);
  561. }
  562. for (auto blockComponent : blockComponents)
  563. {
  564. if (blockComponent == masterBlockComponent)
  565. continue;
  566. blockComponent->setBounds (masterBlockComponent->getX() + static_cast<int> (blockComponent->topLeft.x * blockUnitInPixels),
  567. masterBlockComponent->getY() + static_cast<int> (blockComponent->topLeft.y * blockUnitInPixels),
  568. blockComponent->block->getWidth() * blockUnitInPixels,
  569. blockComponent->block->getHeight() * blockUnitInPixels);
  570. if (blockComponent->rotation != 0)
  571. blockComponent->setTransform (AffineTransform::rotation (static_cast<float> (degreesToRadians (blockComponent->rotation)),
  572. static_cast<float> (blockComponent->getX()),
  573. static_cast<float> (blockComponent->getY())));
  574. }
  575. }
  576. /** Overridden from TopologySource::Listener, called when the topology changes */
  577. void topologyChanged() override
  578. {
  579. // Clear the array of Block components
  580. blockComponents.clear();
  581. masterBlockComponent = nullptr;
  582. // Get the current topology
  583. auto topology = topologySource.getCurrentTopology();
  584. // Create a BlockComponent object for each Block object and store a pointer to the master
  585. for (auto& block : topology.blocks)
  586. {
  587. if (auto* blockComponent = createBlockComponent (block))
  588. {
  589. addAndMakeVisible (blockComponents.add (blockComponent));
  590. if (blockComponent->block->isMasterBlock())
  591. masterBlockComponent = blockComponent;
  592. }
  593. }
  594. // Must have a master Block!
  595. if (topology.blocks.size() > 0)
  596. jassert (masterBlockComponent != nullptr);
  597. // Calculate the relative position and rotation for each Block
  598. positionBlocks (topology);
  599. // Update the display
  600. isInitialResized = true;
  601. resized();
  602. }
  603. private:
  604. /** Creates a BlockComponent object for a new Block and adds it to the content component */
  605. BlockComponent* createBlockComponent (Block::Ptr newBlock)
  606. {
  607. auto type = newBlock->getType();
  608. if (type == Block::lightPadBlock)
  609. return new LightpadComponent (newBlock);
  610. if (type == Block::loopBlock || type == Block::liveBlock
  611. || type == Block::touchBlock || type == Block::developerControlBlock)
  612. return new ControlBlockComponent (newBlock);
  613. // Should only be connecting a Lightpad or Control Block!
  614. jassertfalse;
  615. return nullptr;
  616. }
  617. /** Periodically updates the displayed BlockComponent tooltips */
  618. void timerCallback() override
  619. {
  620. for (auto c : blockComponents)
  621. c->updateStatsAndTooltip();
  622. }
  623. /** Calculates the position and rotation of each connected Block relative to the master Block */
  624. void positionBlocks (BlockTopology topology)
  625. {
  626. Array<BlockComponent*> blocksConnectedToMaster;
  627. auto maxDelta = std::numeric_limits<float>::max();
  628. auto maxLoops = 50;
  629. // Store all the connections to the master Block
  630. Array<BlockDeviceConnection> masterBlockConnections;
  631. for (auto connection : topology.connections)
  632. if (connection.device1 == masterBlockComponent->block->uid || connection.device2 == masterBlockComponent->block->uid)
  633. masterBlockConnections.add (connection);
  634. // Position all the Blocks that are connected to the master Block
  635. while (maxDelta > 0.001f && --maxLoops)
  636. {
  637. maxDelta = 0.0f;
  638. // Loop through each connection on the master Block
  639. for (auto connection : masterBlockConnections)
  640. {
  641. // Work out whether the master Block is device 1 or device 2 in the BlockDeviceConnection struct
  642. bool isDevice1 = true;
  643. if (masterBlockComponent->block->uid == connection.device2)
  644. isDevice1 = false;
  645. // Get the connected ports
  646. auto masterPort = isDevice1 ? connection.connectionPortOnDevice1 : connection.connectionPortOnDevice2;
  647. auto otherPort = isDevice1 ? connection.connectionPortOnDevice2 : connection.connectionPortOnDevice1;
  648. for (auto otherBlockComponent : blockComponents)
  649. {
  650. // Get the other block
  651. if (otherBlockComponent->block->uid == (isDevice1 ? connection.device2 : connection.device1))
  652. {
  653. blocksConnectedToMaster.addIfNotAlreadyThere (otherBlockComponent);
  654. // Get the rotation of the other Block relative to the master Block
  655. otherBlockComponent->rotation = getRotation (masterPort.edge, otherPort.edge);
  656. // Get the offsets for the connected ports
  657. auto masterBlockOffset = masterBlockComponent->getOffsetForPort (masterPort);
  658. auto otherBlockOffset = otherBlockComponent->topLeft + otherBlockComponent->getOffsetForPort (otherPort);
  659. // Work out the distance between the two connected ports
  660. auto delta = masterBlockOffset - otherBlockOffset;
  661. // Move the other block half the distance to the connection
  662. otherBlockComponent->topLeft += delta / 2.0f;
  663. // Work out whether we are close enough for the loop to end
  664. maxDelta = jmax (maxDelta, std::abs (delta.x), std::abs (delta.y));
  665. }
  666. }
  667. }
  668. }
  669. // Check if there are any Blocks that have not been positioned yet
  670. Array<BlockComponent*> unpositionedBlocks;
  671. for (auto blockComponent : blockComponents)
  672. if (blockComponent != masterBlockComponent && ! blocksConnectedToMaster.contains (blockComponent))
  673. unpositionedBlocks.add (blockComponent);
  674. if (unpositionedBlocks.size() > 0)
  675. {
  676. // Reset the loop conditions
  677. maxDelta = std::numeric_limits<float>::max();
  678. maxLoops = 50;
  679. // Position all the remaining Blocks
  680. while (maxDelta > 0.001f && --maxLoops)
  681. {
  682. maxDelta = 0.0f;
  683. // Loop through each unpositioned Block
  684. for (auto blockComponent : unpositionedBlocks)
  685. {
  686. // Store all the connections to this Block
  687. Array<BlockDeviceConnection> blockConnections;
  688. for (auto connection : topology.connections)
  689. if (connection.device1 == blockComponent->block->uid || connection.device2 == blockComponent->block->uid)
  690. blockConnections.add (connection);
  691. // Loop through each connection on this Block
  692. for (auto connection : blockConnections)
  693. {
  694. // Work out whether this Block is device 1 or device 2 in the BlockDeviceConnection struct
  695. auto isDevice1 = true;
  696. if (blockComponent->block->uid == connection.device2)
  697. isDevice1 = false;
  698. // Get the connected ports
  699. auto thisPort = isDevice1 ? connection.connectionPortOnDevice1 : connection.connectionPortOnDevice2;
  700. auto otherPort = isDevice1 ? connection.connectionPortOnDevice2 : connection.connectionPortOnDevice1;
  701. // Get the other Block
  702. for (auto otherBlockComponent : blockComponents)
  703. {
  704. if (otherBlockComponent->block->uid == (isDevice1 ? connection.device2 : connection.device1))
  705. {
  706. // Get the rotation
  707. auto rotation = getRotation (otherPort.edge, thisPort.edge) + otherBlockComponent->rotation;
  708. if (rotation > 360)
  709. rotation -= 360;
  710. blockComponent->rotation = rotation;
  711. // Get the offsets for the connected ports
  712. auto otherBlockOffset = (otherBlockComponent->topLeft + otherBlockComponent->getOffsetForPort (otherPort));
  713. auto thisBlockOffset = (blockComponent->topLeft + blockComponent->getOffsetForPort (thisPort));
  714. // Work out the distance between the two connected ports
  715. auto delta = otherBlockOffset - thisBlockOffset;
  716. // Move this block half the distance to the connection
  717. blockComponent->topLeft += delta / 2.0f;
  718. // Work out whether we are close enough for the loop to end
  719. maxDelta = jmax (maxDelta, std::abs (delta.x), std::abs (delta.y));
  720. }
  721. }
  722. }
  723. }
  724. }
  725. }
  726. }
  727. /** Returns a rotation in degrees based on the connected edges of two blocks */
  728. int getRotation (Block::ConnectionPort::DeviceEdge staticEdge, Block::ConnectionPort::DeviceEdge rotatedEdge)
  729. {
  730. using edge = Block::ConnectionPort::DeviceEdge;
  731. switch (staticEdge)
  732. {
  733. case edge::north:
  734. {
  735. switch (rotatedEdge)
  736. {
  737. case edge::north:
  738. return 180;
  739. case edge::south:
  740. return 0;
  741. case edge::east:
  742. return 90;
  743. case edge::west:
  744. return 270;
  745. }
  746. }
  747. case edge::south:
  748. {
  749. switch (rotatedEdge)
  750. {
  751. case edge::north:
  752. return 0;
  753. case edge::south:
  754. return 180;
  755. case edge::east:
  756. return 270;
  757. case edge::west:
  758. return 90;
  759. }
  760. }
  761. case edge::east:
  762. {
  763. switch (rotatedEdge)
  764. {
  765. case edge::north:
  766. return 270;
  767. case edge::south:
  768. return 90;
  769. case edge::east:
  770. return 180;
  771. case edge::west:
  772. return 0;
  773. }
  774. }
  775. case edge::west:
  776. {
  777. switch (rotatedEdge)
  778. {
  779. case edge::north:
  780. return 90;
  781. case edge::south:
  782. return 270;
  783. case edge::east:
  784. return 0;
  785. case edge::west:
  786. return 180;
  787. }
  788. }
  789. }
  790. return 0;
  791. }
  792. //==============================================================================
  793. PhysicalTopologySource topologySource;
  794. OwnedArray<BlockComponent> blockComponents;
  795. BlockComponent* masterBlockComponent = nullptr;
  796. Label noBlocksLabel;
  797. TextButton zoomOutButton;
  798. TextButton zoomInButton;
  799. int blockUnitInPixels;
  800. bool isInitialResized;
  801. #if JUCE_IOS
  802. TextButton connectButton;
  803. #endif
  804. //==============================================================================
  805. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BlocksMonitorDemo)
  806. };