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.

469 lines
18KB

  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. #pragma once
  20. #include "../JuceLibraryCode/JuceHeader.h"
  21. #include "BlockComponents.h"
  22. /**
  23. The main component where the Block components will be displayed
  24. */
  25. class MainComponent : public Component,
  26. public TopologySource::Listener,
  27. private Timer,
  28. private Button::Listener
  29. {
  30. public:
  31. MainComponent()
  32. {
  33. setSize (600, 600);
  34. noBlocksLabel.setText ("No BLOCKS connected...", dontSendNotification);
  35. noBlocksLabel.setJustificationType (Justification::centred);
  36. zoomOutButton.setButtonText ("+");
  37. zoomOutButton.addListener (this);
  38. zoomOutButton.setAlwaysOnTop (true);
  39. zoomInButton.setButtonText ("-");
  40. zoomInButton.addListener (this);
  41. zoomInButton.setAlwaysOnTop (true);
  42. // Register MainComponent as a listener to the PhysicalTopologySource object
  43. topologySource.addListener (this);
  44. startTimer (10000);
  45. addAndMakeVisible (noBlocksLabel);
  46. addAndMakeVisible (zoomOutButton);
  47. addAndMakeVisible (zoomInButton);
  48. #if JUCE_IOS
  49. connectButton.setButtonText ("Connect");
  50. connectButton.addListener (this);
  51. connectButton.setAlwaysOnTop (true);
  52. addAndMakeVisible (connectButton);
  53. #endif
  54. }
  55. void paint (Graphics& g) override
  56. {
  57. }
  58. void resized() override
  59. {
  60. #if JUCE_IOS
  61. connectButton.setBounds (getRight() - 100, 20, 80, 30);
  62. #endif
  63. noBlocksLabel.setVisible (false);
  64. const int numBlockComponents = blockComponents.size();
  65. // If there are no currently connected Blocks then display some text on the screen
  66. if (numBlockComponents == 0)
  67. {
  68. noBlocksLabel.setVisible (true);
  69. noBlocksLabel.setBounds (0, (getHeight() / 2) - 50, getWidth(), 100);
  70. return;
  71. }
  72. zoomOutButton.setBounds (10, getHeight() - 40, 40, 30);
  73. zoomInButton.setBounds (zoomOutButton.getRight(), zoomOutButton.getY(), 40, 30);
  74. if (isInitialResized)
  75. {
  76. // Work out the area needed in terms of Block units
  77. Rectangle<float> maxArea;
  78. for (auto blockComponent : blockComponents)
  79. {
  80. auto topLeft = blockComponent->topLeft;
  81. int rotation = blockComponent->rotation;
  82. int blockSize = 0;
  83. if (rotation == 180)
  84. blockSize = blockComponent->block->getWidth();
  85. else if (rotation == 90)
  86. blockSize = blockComponent->block->getHeight();
  87. if (topLeft.x - blockSize < maxArea.getX())
  88. maxArea.setX (topLeft.x - blockSize);
  89. blockSize = 0;
  90. if (rotation == 0)
  91. blockSize = blockComponent->block->getWidth();
  92. else if (rotation == 270)
  93. blockSize = blockComponent->block->getHeight();
  94. if (topLeft.x + blockSize > maxArea.getRight())
  95. maxArea.setWidth (topLeft.x + blockSize);
  96. blockSize = 0;
  97. if (rotation == 180)
  98. blockSize = blockComponent->block->getHeight();
  99. else if (rotation == 270)
  100. blockSize = blockComponent->block->getWidth();
  101. if (topLeft.y - blockSize < maxArea.getY())
  102. maxArea.setY (topLeft.y - blockSize);
  103. blockSize = 0;
  104. if (rotation == 0)
  105. blockSize = blockComponent->block->getHeight();
  106. else if (rotation == 90)
  107. blockSize = blockComponent->block->getWidth();
  108. if (topLeft.y + blockSize > maxArea.getBottom())
  109. maxArea.setHeight (topLeft.y + blockSize);
  110. }
  111. float totalWidth = std::abs (maxArea.getX()) + maxArea.getWidth();
  112. float totalHeight = std::abs (maxArea.getY()) + maxArea.getHeight();
  113. blockUnitInPixels = static_cast<int> (jmin ((getHeight() / totalHeight) - 50, (getWidth() / totalWidth) - 50));
  114. masterBlockComponent->centreWithSize (masterBlockComponent->block->getWidth() * blockUnitInPixels,
  115. masterBlockComponent->block->getHeight() * blockUnitInPixels);
  116. isInitialResized = false;
  117. }
  118. else
  119. {
  120. masterBlockComponent->setSize (masterBlockComponent->block->getWidth() * blockUnitInPixels, masterBlockComponent->block->getHeight() * blockUnitInPixels);
  121. }
  122. for (auto blockComponent : blockComponents)
  123. {
  124. if (blockComponent == masterBlockComponent)
  125. continue;
  126. blockComponent->setBounds (masterBlockComponent->getX() + static_cast<int> (blockComponent->topLeft.x * blockUnitInPixels),
  127. masterBlockComponent->getY() + static_cast<int> (blockComponent->topLeft.y * blockUnitInPixels),
  128. blockComponent->block->getWidth() * blockUnitInPixels,
  129. blockComponent->block->getHeight() * blockUnitInPixels);
  130. if (blockComponent->rotation != 0)
  131. blockComponent->setTransform (AffineTransform::rotation (blockComponent->rotation * (static_cast<float> (double_Pi) / 180.0f),
  132. static_cast<float> (blockComponent->getX()),
  133. static_cast<float> (blockComponent->getY())));
  134. }
  135. }
  136. /** Overridden from TopologySource::Listener, called when the topology changes */
  137. void topologyChanged() override
  138. {
  139. // Clear the array of Block components
  140. blockComponents.clear();
  141. masterBlockComponent = nullptr;
  142. // Get the current topology
  143. auto topology = topologySource.getCurrentTopology();
  144. // Create a BlockComponent object for each Block object and store a pointer to the master
  145. for (auto& block : topology.blocks)
  146. {
  147. if (auto* blockComponent = createBlockComponent (block))
  148. {
  149. addAndMakeVisible (blockComponents.add (blockComponent));
  150. if (blockComponent->block->isMasterBlock())
  151. masterBlockComponent = blockComponent;
  152. }
  153. }
  154. // Must have a master Block!
  155. if (topology.blocks.size() > 0)
  156. jassert (masterBlockComponent != nullptr);
  157. // Calculate the relative position and rotation for each Block
  158. positionBlocks (topology);
  159. // Update the display
  160. isInitialResized = true;
  161. resized();
  162. }
  163. private:
  164. /** Creates a BlockComponent object for a new Block and adds it to the content component */
  165. BlockComponent* createBlockComponent (Block::Ptr newBlock)
  166. {
  167. auto type = newBlock->getType();
  168. if (type == Block::lightPadBlock)
  169. return new LightpadComponent (newBlock);
  170. if (type == Block::loopBlock || type == Block::liveBlock)
  171. return new ControlBlockComponent (newBlock);
  172. // Should only be connecting a Lightpad or Control Block!
  173. jassertfalse;
  174. return nullptr;
  175. }
  176. /** Periodically updates the displayed BlockComponent tooltips */
  177. void timerCallback() override
  178. {
  179. for (auto c : blockComponents)
  180. c->updateStatsAndTooltip();
  181. }
  182. /** Zooms the display in or out */
  183. void buttonClicked (Button* button) override
  184. {
  185. #if JUCE_IOS
  186. if (button == &connectButton)
  187. {
  188. BluetoothMidiDevicePairingDialogue::open();
  189. return;
  190. }
  191. #endif
  192. if (button == &zoomOutButton || button == &zoomInButton)
  193. {
  194. blockUnitInPixels *= (button == &zoomOutButton ? 1.05f : 0.95f);
  195. resized();
  196. }
  197. }
  198. /** Calculates the position and rotation of each connected Block relative to the master Block */
  199. void positionBlocks (BlockTopology topology)
  200. {
  201. Array<BlockComponent*> blocksConnectedToMaster;
  202. float maxDelta = std::numeric_limits<float>::max();
  203. int maxLoops = 50;
  204. // Store all the connections to the master Block
  205. Array<BlockDeviceConnection> masterBlockConnections;
  206. for (auto connection : topology.connections)
  207. if (connection.device1 == masterBlockComponent->block->uid || connection.device2 == masterBlockComponent->block->uid)
  208. masterBlockConnections.add (connection);
  209. // Position all the Blocks that are connected to the master Block
  210. while (maxDelta > 0.001f && --maxLoops)
  211. {
  212. maxDelta = 0.0f;
  213. // Loop through each connection on the master Block
  214. for (auto connection : masterBlockConnections)
  215. {
  216. // Work out whether the master Block is device 1 or device 2 in the BlockDeviceConnection struct
  217. bool isDevice1 = true;
  218. if (masterBlockComponent->block->uid == connection.device2)
  219. isDevice1 = false;
  220. // Get the connected ports
  221. auto masterPort = isDevice1 ? connection.connectionPortOnDevice1 : connection.connectionPortOnDevice2;
  222. auto otherPort = isDevice1 ? connection.connectionPortOnDevice2 : connection.connectionPortOnDevice1;
  223. for (auto otherBlockComponent : blockComponents)
  224. {
  225. // Get the other block
  226. if (otherBlockComponent->block->uid == (isDevice1 ? connection.device2 : connection.device1))
  227. {
  228. blocksConnectedToMaster.addIfNotAlreadyThere (otherBlockComponent);
  229. // Get the rotation of the other Block relative to the master Block
  230. otherBlockComponent->rotation = getRotation (masterPort.edge, otherPort.edge);
  231. // Get the offsets for the connected ports
  232. auto masterBlockOffset = masterBlockComponent->getOffsetForPort (masterPort);
  233. auto otherBlockOffset = otherBlockComponent->topLeft + otherBlockComponent->getOffsetForPort (otherPort);
  234. // Work out the distance between the two connected ports
  235. auto delta = masterBlockOffset - otherBlockOffset;
  236. // Move the other block half the distance to the connection
  237. otherBlockComponent->topLeft += delta / 2.0f;
  238. // Work out whether we are close enough for the loop to end
  239. maxDelta = jmax (maxDelta, std::abs (delta.x), std::abs (delta.y));
  240. }
  241. }
  242. }
  243. }
  244. // Check if there are any Blocks that have not been positioned yet
  245. Array<BlockComponent*> unpositionedBlocks;
  246. for (auto blockComponent : blockComponents)
  247. if (blockComponent != masterBlockComponent && ! blocksConnectedToMaster.contains (blockComponent))
  248. unpositionedBlocks.add (blockComponent);
  249. if (unpositionedBlocks.size() > 0)
  250. {
  251. // Reset the loop conditions
  252. maxDelta = std::numeric_limits<float>::max();
  253. maxLoops = 50;
  254. // Position all the remaining Blocks
  255. while (maxDelta > 0.001f && --maxLoops)
  256. {
  257. maxDelta = 0.0f;
  258. // Loop through each unpositioned Block
  259. for (auto blockComponent : unpositionedBlocks)
  260. {
  261. // Store all the connections to this Block
  262. Array<BlockDeviceConnection> blockConnections;
  263. for (auto connection : topology.connections)
  264. if (connection.device1 == blockComponent->block->uid || connection.device2 == blockComponent->block->uid)
  265. blockConnections.add (connection);
  266. // Loop through each connection on this Block
  267. for (auto connection : blockConnections)
  268. {
  269. // Work out whether this Block is device 1 or device 2 in the BlockDeviceConnection struct
  270. bool isDevice1 = true;
  271. if (blockComponent->block->uid == connection.device2)
  272. isDevice1 = false;
  273. // Get the connected ports
  274. auto thisPort = isDevice1 ? connection.connectionPortOnDevice1 : connection.connectionPortOnDevice2;
  275. auto otherPort = isDevice1 ? connection.connectionPortOnDevice2 : connection.connectionPortOnDevice1;
  276. // Get the other Block
  277. for (auto otherBlockComponent : blockComponents)
  278. {
  279. if (otherBlockComponent->block->uid == (isDevice1 ? connection.device2 : connection.device1))
  280. {
  281. // Get the rotation
  282. int rotation = getRotation (otherPort.edge, thisPort.edge) + otherBlockComponent->rotation;
  283. if (rotation > 360)
  284. rotation -= 360;
  285. blockComponent->rotation = rotation;
  286. // Get the offsets for the connected ports
  287. auto otherBlockOffset = (otherBlockComponent->topLeft + otherBlockComponent->getOffsetForPort (otherPort));
  288. auto thisBlockOffset = (blockComponent->topLeft + blockComponent->getOffsetForPort (thisPort));
  289. // Work out the distance between the two connected ports
  290. auto delta = otherBlockOffset - thisBlockOffset;
  291. // Move this block half the distance to the connection
  292. blockComponent->topLeft += delta / 2.0f;
  293. // Work out whether we are close enough for the loop to end
  294. maxDelta = jmax (maxDelta, std::abs (delta.x), std::abs (delta.y));
  295. }
  296. }
  297. }
  298. }
  299. }
  300. }
  301. }
  302. /** Returns a rotation in degrees based on the connected edges of two blocks */
  303. int getRotation (Block::ConnectionPort::DeviceEdge staticEdge, Block::ConnectionPort::DeviceEdge rotatedEdge)
  304. {
  305. using edge = Block::ConnectionPort::DeviceEdge;
  306. switch (staticEdge)
  307. {
  308. case edge::north:
  309. {
  310. switch (rotatedEdge)
  311. {
  312. case edge::north:
  313. return 180;
  314. case edge::south:
  315. return 0;
  316. case edge::east:
  317. return 90;
  318. case edge::west:
  319. return 270;
  320. }
  321. }
  322. case edge::south:
  323. {
  324. switch (rotatedEdge)
  325. {
  326. case edge::north:
  327. return 0;
  328. case edge::south:
  329. return 180;
  330. case edge::east:
  331. return 270;
  332. case edge::west:
  333. return 90;
  334. }
  335. }
  336. case edge::east:
  337. {
  338. switch (rotatedEdge)
  339. {
  340. case edge::north:
  341. return 270;
  342. case edge::south:
  343. return 90;
  344. case edge::east:
  345. return 180;
  346. case edge::west:
  347. return 0;
  348. }
  349. }
  350. case edge::west:
  351. {
  352. switch (rotatedEdge)
  353. {
  354. case edge::north:
  355. return 90;
  356. case edge::south:
  357. return 270;
  358. case edge::east:
  359. return 0;
  360. case edge::west:
  361. return 180;
  362. }
  363. }
  364. }
  365. return 0;
  366. }
  367. //==============================================================================
  368. PhysicalTopologySource topologySource;
  369. OwnedArray<BlockComponent> blockComponents;
  370. BlockComponent* masterBlockComponent = nullptr;
  371. Label noBlocksLabel;
  372. TextButton zoomOutButton;
  373. TextButton zoomInButton;
  374. int blockUnitInPixels;
  375. bool isInitialResized;
  376. #if JUCE_IOS
  377. TextButton connectButton;
  378. #endif
  379. //==============================================================================
  380. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)
  381. };