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.

329 lines
15KB

  1. /**
  2. @page example_blocks_monitor BlocksMonitor
  3. In order to compile and run this application you need to first download the @jucelink{JUCE framework}, which can be obtained from GitHub @jucegithub{here}.
  4. @section blocks_monitor_introduction Introduction
  5. BlocksMonitor is a simple JUCE application that shows currently connected Lightpad and Control %Block devices and visualises touches and button presses. It also displays some basic information about the Blocks.
  6. Generate a Projucer project from the PIP file located in the @s_file{JUCE/examples/BLOCKS/} folder, then navigate to the @s_file{BlocksMonitorDemo/Builds/} directory and open the code project in your IDE of choice. Run the application and connect your Blocks (if you do not know how to do this, see @ref connecting_blocks). Any devices that you have connected should now show up in the application window and this display will be updated as you add and remove Blocks. Lightpads are represented as a black square and will display the current touches as coloured circles, the size of which depend on the touch pressure, and Control Blocks are shown as rectangles containing the LED row and clickable buttons on the hardware. If you hover the mouse cursor over a %Block, a tooltip will appear displaying the name, UID, serial number and current battery level.
  7. @image html BlocksMonitor.png "The BlocksMonitor application with a Lightpad and 3 Control Blocks connected"
  8. @section blocks_monitor_topology Topology
  9. One of the fundamental concepts of the BLOCKS API is topology - a topology is a set of physically connected Blocks and the connections between them. Knowing when the topology has changed and accessing a data structure containing the current topology is the basis of any Blocks application.
  10. To access the current topology, @s_projcode{BlocksMonitorDemo} inherits from the TopologySource::Listener base class @s_item{[1]} and implements the TopologySource::Listener::topologyChanged() method @s_item{[2]}, a callback which is used to inform listeners when any physical devices have been added or removed.
  11. @code{.cpp}
  12. class BlocksMonitorDemo : public Component,
  13. public TopologySource::Listener, // [1]
  14. private Timer
  15. {
  16. public:
  17. //...
  18. void topologyChanged() override // [2]
  19. {
  20. //...
  21. @endcode
  22. In order to receive these callbacks, @s_projcode{BlocksMonitorDemo} contains an instance of the PhysicalTopologySource class @s_item{[3]} and registers itself as a listener to this object in its constructor @s_item{[4]}.
  23. @code{.cpp}
  24. BlocksMonitorDemo()
  25. {
  26. //...
  27. topologySource.addListener (this); // [4]
  28. //...
  29. }
  30. private:
  31. //...
  32. PhysicalTopologySource topologySource; // [3]
  33. OwnedArray<BlockComponent> blockComponents;
  34. BlockComponent* masterBlockComponent = nullptr;
  35. //...
  36. @endcode
  37. When the @s_projcode{topologyChanged()} method is called, this object can be used to access the updated topology through the PhysicalTopologySource::getCurrentTopology() method @s_item{[5]} which returns a BlockTopology struct containing an array of currently connected Block objects and an array of BlockDeviceConnection structs representing the connections between them.
  38. @code{.cpp}
  39. void topologyChanged() override
  40. {
  41. //...
  42. auto topology = topologySource.getCurrentTopology(); // [5]
  43. //...
  44. }
  45. @endcode
  46. @section blocks_monitor_block_object The Block Object
  47. The array of %Block objects contained in the %BlockTopology struct can be used to access individual %Block objects @s_item{[1]} and determine their type using the Block::getType() method @s_item{[2]}.
  48. @code{.cpp}
  49. for (auto& block : topology.blocks) // [1]
  50. {
  51. if (auto* blockComponent = createBlockComponent (block))
  52. {
  53. //...
  54. }
  55. }
  56. @endcode
  57. The application uses this information to construct an on-screen representation of the currently connected Blocks by creating either a @s_projcode{LightpadComponent} object @s_item{[3]} or a @s_projcode{ControlBlockComponent} object @s_item{[4]} for each %Block in the current topology.
  58. @code{.cpp}
  59. BlockComponent* createBlockComponent (Block::Ptr newBlock)
  60. {
  61. auto type = newBlock->getType(); // [2]
  62. if (type == Block::lightPadBlock)
  63. return new LightpadComponent (newBlock); // [3]
  64. if (type == Block::loopBlock || type == Block::liveBlock
  65. || type == Block::touchBlock || type == Block::developerControlBlock)
  66. return new ControlBlockComponent (newBlock); // [4]
  67. jassertfalse;
  68. return nullptr;
  69. }
  70. @endcode
  71. Both of these classes derive from @s_projcode{BlockComponent}, a relatively simple base class that contains some virtual functions for painting the %Block on screen and handling callbacks from the touch surface and/or buttons on the %Block.
  72. @code{.cpp}
  73. class BlockComponent : public Component,
  74. //...
  75. {
  76. public:
  77. //...
  78. virtual void paint (Graphics&) override = 0;
  79. virtual void handleButtonPressed (ControlButton::ButtonFunction, uint32) {}
  80. virtual void handleButtonReleased (ControlButton::ButtonFunction, uint32) {}
  81. virtual void handleTouchChange (TouchSurface::Touch) {}
  82. virtual void handleBatteryLevelUpdate (float) {}
  83. //...
  84. @endcode
  85. In its constructor, @s_projcode{BlockComponent} takes a pointer to the %Block object that it represents and adds itself as a listener to the touch surface (if it is a Lightpad) and buttons using the Block::getTouchSurface() and Block::getButtons() methods, respectively.
  86. @code{.cpp}
  87. BlockComponent (Block::Ptr blockToUse)
  88. : block (blockToUse)
  89. {
  90. //...
  91. if (auto touchSurface = block->getTouchSurface())
  92. touchSurface->addListener (this);
  93. for (auto button : block->getButtons())
  94. button->addListener (this);
  95. //...
  96. }
  97. @endcode
  98. It inherits from the TouchSurface::Listener and ControlButton::Listener classes and overrides the TouchSurface::Listener::touchChanged(), ControlButton::Listener::buttonPressed() and ControlButton::Listener::buttonReleased() methods to call its own virtual methods, which are implemented by the @s_projcode{LightpadComponent} and @s_projcode{ControlBlockComponent} classes to update the on-screen components.
  99. @code{.cpp}
  100. class BlockComponent : public Component,
  101. public SettableTooltipClient,
  102. private TouchSurface::Listener,
  103. private ControlButton::Listener,
  104. private Timer
  105. {
  106. private:
  107. //...
  108. void touchChanged (TouchSurface&, const TouchSurface::Touch& t) override { handleTouchChange (t); }
  109. void buttonPressed (ControlButton& b, Block::Timestamp t) override { handleButtonPressed (b.getType(), t); }
  110. void buttonReleased (ControlButton& b, Block::Timestamp t) override { handleButtonReleased (b.getType(), t); }
  111. //...
  112. @endcode
  113. To visualise touches on the Lightpad, @s_projcode{LightpadComponent} contains an instance of the TouchList class called @s_projcode{touches} and calls the TouchList::updateTouch() method whenever it receives a touch surface listener callback in the @s_projcode{LightpadComponent::handleTouchChange()} method.
  114. @code{.cpp}
  115. class LightpadComponent : public BlockComponent
  116. {
  117. public:
  118. //...
  119. void handleTouchChange (TouchSurface::Touch touch) override { touches.updateTouch (touch); }
  120. private:
  121. //...
  122. TouchList<TouchSurface::Touch> touches;
  123. //...
  124. @endcode
  125. The @s_projcode{LightpadBlock::paint()} method then iterates over the current TouchSurface::Touch objects in the %TouchList and visualises them on the component at 25Hz.
  126. @code{.cpp}
  127. void paint (Graphics& g) override
  128. {
  129. //...
  130. for (auto touch : touches)
  131. {
  132. //...
  133. }
  134. }
  135. @endcode
  136. The @s_projcode{ControlBlockComponent} class represents a generic Control %Block with 15 LEDs, 8 circular buttons and 1 rounded rectangular button. When a button is pressed on the physical Control %Block, the @s_projcode{BlockComponent::handleButtonPressed()} function is called and this class uses the ControlButton::ButtonFunction variable to determine which button was pressed and should be activated on the on-screen component.
  137. @code{.cpp}
  138. void handleButtonPressed (ControlButton::ButtonFunction function, uint32) override
  139. {
  140. displayButtonInteraction (controlButtonFunctionToIndex (function), true);
  141. }
  142. @endcode
  143. The same process is repeated for when the button is released.
  144. @code{.cpp}
  145. void handleButtonReleased (ControlButton::ButtonFunction function, uint32) override
  146. {
  147. displayButtonInteraction (controlButtonFunctionToIndex (function), false);
  148. }
  149. @endcode
  150. This class also overrides the @s_projcode{BlockComponent::handleBatteryLevelUpdate()} method to update which LEDs should be on based on the battery level, which is accessed in the @s_projcode{BlockComponent} base class using the Block::getBatteryLevel() and Block::isBatteryCharging() methods.
  151. @code{.cpp}
  152. void handleBatteryLevelUpdate (float batteryLevel) override
  153. {
  154. auto numLedsOn = static_cast<int> (numLeds * batteryLevel);
  155. if (numLedsOn != previousNumLedsOn)
  156. for (auto i = 0; i < numLeds; ++i)
  157. leds.getUnchecked (i)->setOnState (i < numLedsOn);
  158. previousNumLedsOn = numLedsOn;
  159. repaint();
  160. }
  161. @endcode
  162. These callback methods are a simple and powerful way to get user input from the Blocks and use this data to drive some process in your application. In this example, the input is simply mirrored on the screen but it could be used to control any number of things such as audio synthesis (see the @ref example_blocks_synth example) and graphics (see the @ref example_blocks_drawing example).
  163. @section blocks_monitor_connections Blocks Connections
  164. The %BlockTopology struct returned by the @s_projcode{%PhysicalTopologySource::getCurrentTopology()} method also contains an array of BlockDeviceConnection objects representing all the current DNA port connections between Blocks in the topology. A single %BlockDeviceConnection struct describes a physical connection between two ports on two Blocks and contains a Block::UID and Block::ConnectionPort object for each of the two devices.
  165. This information is used to calculate the position and rotation of each connected %Block and update the corresponding @s_projcode{topLeft} and @s_projcode{rotation} member variables of its @s_projcode{BlockComponent} so that the correct topology is displayed on the screen. The @s_projcode{topLeft} variable is a Point that describes the position of the top left of the @s_projcode{BlockComponent} in terms of logical device units relative to the top left of the master %Block at Point (0, 0).
  166. @code{.cpp}
  167. int rotation = 0;
  168. Point<float> topLeft = { 0.0f, 0.0f };
  169. @endcode
  170. Initially, all @s_projcode{BlockComponent} instances have the @s_projcode{topLeft} position (0, 0) and the @s_projcode{BlocksMonitorDemo::positionBlocks()} method iterates first over all of the Blocks connected to the master %Block and then any remaining Blocks and calculates the correct @s_projcode{topLeft} %Point and @s_projcode{rotation} for each using the array of %BlockDeviceConnection objects.
  171. @code{.cpp}
  172. void positionBlocks (BlockTopology topology)
  173. {
  174. //...
  175. Array<BlockDeviceConnection> masterBlockConnections;
  176. for (auto connection : topology.connections)
  177. if (connection.device1 == masterBlockComponent->block->uid || connection.device2 == masterBlockComponent->block->uid)
  178. masterBlockConnections.add (connection);
  179. while (maxDelta > 0.001f && --maxLoops)
  180. {
  181. //...
  182. for (auto connection : masterBlockConnections)
  183. {
  184. //...
  185. for (auto otherBlockComponent : blockComponents)
  186. {
  187. //...
  188. }
  189. }
  190. }
  191. Array<BlockComponent*> unpositionedBlocks;
  192. for (auto blockComponent : blockComponents)
  193. if (blockComponent != masterBlockComponent && ! blocksConnectedToMaster.contains (blockComponent))
  194. unpositionedBlocks.add (blockComponent);
  195. if (unpositionedBlocks.size() > 0)
  196. {
  197. //...
  198. while (maxDelta > 0.001f && --maxLoops)
  199. {
  200. //...
  201. for (auto blockComponent : unpositionedBlocks)
  202. {
  203. Array<BlockDeviceConnection> blockConnections;
  204. for (auto connection : topology.connections)
  205. if (connection.device1 == blockComponent->block->uid || connection.device2 == blockComponent->block->uid)
  206. blockConnections.add (connection);
  207. for (auto connection : blockConnections)
  208. {
  209. //...
  210. for (auto otherBlockComponent : blockComponents)
  211. {
  212. //...
  213. }
  214. }
  215. }
  216. }
  217. }
  218. }
  219. @endcode
  220. Then, in the @s_projcode{BlocksMonitorDemo::resized()} method these attributes are used to correctly position the components.
  221. @code{.cpp}
  222. void resized() override
  223. {
  224. //...
  225. if (numBlockComponents == 0)
  226. {
  227. //...
  228. return;
  229. }
  230. //...
  231. if (isInitialResized)
  232. {
  233. //...
  234. for (auto blockComponent : blockComponents)
  235. {
  236. auto topLeft = blockComponent->topLeft;
  237. auto rotation = blockComponent->rotation;
  238. //...
  239. }
  240. //...
  241. masterBlockComponent->centreWithSize (...);
  242. //...
  243. }
  244. else
  245. {
  246. masterBlockComponent->setSize (...);
  247. }
  248. for (auto blockComponent : blockComponents)
  249. {
  250. if (blockComponent == masterBlockComponent)
  251. continue;
  252. blockComponent->setBounds (...);
  253. if (blockComponent->rotation != 0)
  254. blockComponent->setTransform (AffineTransform::rotation (...));
  255. }
  256. }
  257. @endcode
  258. @section blocks_monitor_summary Summary
  259. This tutorial and the accompanying code has introduced the %BlockTopology and %Block objects, and demonstrated how to receive callbacks from connected Blocks when the touch surface or buttons are pressed, allowing you to use this input in your own applications.
  260. @section blocks_monitor_see_also See also
  261. - @ref example_blocks_drawing
  262. - @ref example_blocks_synth
  263. */