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.

525 lines
19KB

  1. #ifndef BLOCKCOMPONENTS_H_INCLUDED
  2. #define BLOCKCOMPONENTS_H_INCLUDED
  3. #include "../JuceLibraryCode/JuceHeader.h"
  4. //==============================================================================
  5. /**
  6. Base class that renders a Block on the screen
  7. */
  8. class BlockComponent : public Component,
  9. public SettableTooltipClient,
  10. private TouchSurface::Listener,
  11. private ControlButton::Listener,
  12. private Timer
  13. {
  14. public:
  15. BlockComponent (Block::Ptr blockToUse)
  16. : block (blockToUse)
  17. {
  18. updateStatsAndTooltip();
  19. // Register BlockComponent as a listener to the touch surface
  20. if (auto touchSurface = block->getTouchSurface())
  21. touchSurface->addListener (this);
  22. // Register BlockComponent as a listener to any buttons
  23. for (auto button : block->getButtons())
  24. button->addListener (this);
  25. // If this is a Lightpad then set the grid program to be blank
  26. if (auto grid = block->getLEDGrid())
  27. grid->setProgram (new BitmapLEDProgram(*grid));
  28. // If this is a Lightpad then redraw it at 25Hz
  29. if (block->getType() == Block::lightPadBlock)
  30. startTimerHz (25);
  31. // Make sure the component can't go offscreen if it is draggable
  32. constrainer.setMinimumOnscreenAmounts (50, 50, 50, 50);
  33. }
  34. ~BlockComponent()
  35. {
  36. // Remove any listeners
  37. if (auto touchSurface = block->getTouchSurface())
  38. touchSurface->removeListener (this);
  39. for (auto button : block->getButtons())
  40. button->removeListener (this);
  41. }
  42. /** Called periodically to update the tooltip with inforamtion about the Block */
  43. void updateStatsAndTooltip()
  44. {
  45. // Get the battery level of this Block and inform any subclasses
  46. auto batteryLevel = block->getBatteryLevel();
  47. handleBatteryLevelUpdate (batteryLevel);
  48. // Update the tooltip
  49. setTooltip ("Name = " + block->getDeviceDescription() + "\n"
  50. + "UID = " + String (block->uid) + "\n"
  51. + "Serial number = " + block->serialNumber + "\n"
  52. + "Battery level = " + String ((int) (batteryLevel * 100)) + "%"
  53. + (block->isBatteryCharging() ? "++"
  54. : "--"));
  55. }
  56. /** Subclasses should override this to paint the Block object on the screen */
  57. virtual void paint (Graphics&) override = 0;
  58. /** Subclasses can override this to receive button down events from the Block */
  59. virtual void handleButtonPressed (ControlButton::ButtonFunction, uint32) {}
  60. /** Subclasses can override this to receive button up events from the Block */
  61. virtual void handleButtonReleased (ControlButton::ButtonFunction, uint32) {}
  62. /** Subclasses can override this to receive touch events from the Block */
  63. virtual void handleTouchChange (TouchSurface::Touch) {}
  64. /** Subclasses can override this to battery level updates from the Block */
  65. virtual void handleBatteryLevelUpdate (float) {}
  66. /** The Block object that this class represents */
  67. Block::Ptr block;
  68. //==============================================================================
  69. /** Returns an integer index corresponding to a physical position on the hardware
  70. for each type of Control Block. */
  71. static int controlButtonFunctionToIndex (ControlButton::ButtonFunction f)
  72. {
  73. using CB = ControlButton;
  74. static Array<ControlButton::ButtonFunction> map[] =
  75. {
  76. { CB::mode, CB::button0 },
  77. { CB::volume, CB::button1 },
  78. { CB::scale, CB::button2, CB::click },
  79. { CB::chord, CB::button3, CB::snap },
  80. { CB::arp, CB::button4, CB::back },
  81. { CB::sustain, CB::button5, CB::playOrPause },
  82. { CB::octave, CB::button6, CB::record },
  83. { CB::love, CB::button7, CB::learn },
  84. { CB::up },
  85. { CB::down }
  86. };
  87. for (int i = 0; i < numElementsInArray (map); ++i)
  88. if (map[i].contains (f))
  89. return i;
  90. return -1;
  91. }
  92. Point<float> getOffsetForPort (Block::ConnectionPort port)
  93. {
  94. using e = Block::ConnectionPort::DeviceEdge;
  95. switch (rotation)
  96. {
  97. case 0:
  98. {
  99. switch (port.edge)
  100. {
  101. case e::north:
  102. return { static_cast<float> (port.index), 0.0f };
  103. case e::east:
  104. return { static_cast<float> (block->getWidth()), static_cast<float> (port.index) };
  105. case e::south:
  106. return { static_cast<float> (port.index), static_cast<float> (block->getHeight()) };
  107. case e::west:
  108. return { 0.0f, static_cast<float> (port.index) };
  109. }
  110. }
  111. case 90:
  112. {
  113. switch (port.edge)
  114. {
  115. case e::north:
  116. return { 0.0f, static_cast<float> (port.index) };
  117. case e::east:
  118. return { static_cast<float> (-1.0f - port.index), static_cast<float> (block->getWidth()) };
  119. case e::south:
  120. return { static_cast<float> (0.0f - block->getHeight()), static_cast<float> (port.index) };
  121. case e::west:
  122. return { static_cast<float> (-1.0f - port.index), 0.0f };
  123. }
  124. }
  125. case 180:
  126. {
  127. switch (port.edge)
  128. {
  129. case e::north:
  130. return { static_cast<float> (-1.0f - port.index), 0.0f };
  131. case e::east:
  132. return { static_cast<float> (0.0f - block->getWidth()), static_cast<float> (-1.0f - port.index) };
  133. case e::south:
  134. return { static_cast<float> (-1.0f - port.index), static_cast<float> (0.0f - block->getHeight()) };
  135. case e::west:
  136. return { 0.0f, static_cast<float> (-1.0f - port.index) };
  137. }
  138. }
  139. case 270:
  140. {
  141. switch (port.edge)
  142. {
  143. case e::north:
  144. return { 0.0f, static_cast<float> (-1.0f - port.index) };
  145. case e::east:
  146. return { static_cast<float> (port.index), static_cast<float> (0 - block->getWidth()) };
  147. case e::south:
  148. return { static_cast<float> (block->getHeight()), static_cast<float> (-1.0f - port.index) };
  149. case e::west:
  150. return { static_cast<float> (port.index), 0.0f };
  151. }
  152. }
  153. }
  154. return Point<float>();
  155. }
  156. int rotation = 0;
  157. Point<float> topLeft = { 0.0f, 0.0f };
  158. private:
  159. /** Used to call repaint() periodically */
  160. void timerCallback() override { repaint(); }
  161. /** Overridden from TouchSurface::Listener */
  162. void touchChanged (TouchSurface&, const TouchSurface::Touch& t) override { handleTouchChange (t); }
  163. /** Overridden from ControlButton::Listener */
  164. void buttonPressed (ControlButton& b, Block::Timestamp t) override { handleButtonPressed (b.getType(), t); }
  165. /** Overridden from ControlButton::Listener */
  166. void buttonReleased (ControlButton& b, Block::Timestamp t) override { handleButtonReleased (b.getType(), t); }
  167. /** Overridden from MouseListener. Prepares the master Block component for dragging. */
  168. void mouseDown (const MouseEvent& e) override
  169. {
  170. if (block->isMasterBlock())
  171. componentDragger.startDraggingComponent (this, e);
  172. }
  173. /** Overridden from MouseListener. Drags the master Block component */
  174. void mouseDrag (const MouseEvent& e) override
  175. {
  176. if (block->isMasterBlock())
  177. {
  178. componentDragger.dragComponent (this, e, &constrainer);
  179. getParentComponent()->resized();
  180. }
  181. }
  182. ComponentDragger componentDragger;
  183. ComponentBoundsConstrainer constrainer;
  184. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BlockComponent)
  185. };
  186. //==============================================================================
  187. /**
  188. Class that renders a Lightpad on the screen
  189. */
  190. class LightpadComponent : public BlockComponent
  191. {
  192. public:
  193. LightpadComponent (Block::Ptr blockToUse)
  194. : BlockComponent (blockToUse)
  195. {
  196. }
  197. void paint (Graphics& g) override
  198. {
  199. auto r = getLocalBounds().toFloat();
  200. // clip the drawing area to only draw in the block area
  201. {
  202. Path clipArea;
  203. clipArea.addRoundedRectangle (r, r.getWidth() / 20.0f);
  204. g.reduceClipRegion (clipArea);
  205. }
  206. // Fill a black square for the Lightpad
  207. g.fillAll (Colours::black);
  208. // size ration between physical and on-screen blocks
  209. Point<float> ratio (r.getWidth() / block->getWidth(),
  210. r.getHeight() / block->getHeight());
  211. float maxCircleSize = block->getWidth() / 3.0f;
  212. // iterate over the list of current touches and draw them on the onscreen Block
  213. for (auto touch : touches)
  214. {
  215. float circleSize = touch.touch.z * maxCircleSize;
  216. Point<float> touchPosition (touch.touch.x,
  217. touch.touch.y);
  218. auto blob = Rectangle<float> (circleSize, circleSize)
  219. .withCentre (touchPosition) * ratio;
  220. ColourGradient cg (colourArray[touch.touch.index], blob.getCentreX(), blob.getCentreY(),
  221. Colours::transparentBlack, blob.getRight(), blob.getBottom(),
  222. true);
  223. g.setGradientFill (cg);
  224. g.fillEllipse (blob);
  225. }
  226. }
  227. void handleTouchChange (TouchSurface::Touch touch) override { touches.updateTouch (touch); }
  228. private:
  229. /** An Array of colours to use for touches */
  230. Array<Colour> colourArray = { Colours::red,
  231. Colours::blue,
  232. Colours::green,
  233. Colours::yellow,
  234. Colours::white,
  235. Colours::hotpink,
  236. Colours::mediumpurple };
  237. /** A list of current Touch events */
  238. TouchList<TouchSurface::Touch> touches;
  239. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LightpadComponent)
  240. };
  241. //==============================================================================
  242. /**
  243. Class that renders a Control Block on the screen
  244. */
  245. class ControlBlockComponent : public BlockComponent
  246. {
  247. public:
  248. ControlBlockComponent (Block::Ptr blockToUse)
  249. : BlockComponent (blockToUse),
  250. numLeds (block->getLEDRow()->getNumLEDs())
  251. {
  252. addAndMakeVisible (roundedRectangleButton);
  253. // Display the battery level on the LEDRow
  254. auto numLedsToTurnOn = static_cast<int> (numLeds * block->getBatteryLevel());
  255. // add LEDs
  256. for (int i = 0; i < numLeds; ++i)
  257. {
  258. auto ledComponent = new LEDComponent();
  259. ledComponent->setOnState (i < numLedsToTurnOn);
  260. addAndMakeVisible (leds.add (ledComponent));
  261. }
  262. previousNumLedsOn = numLedsToTurnOn;
  263. // add buttons
  264. for (int i = 0; i < 8; ++i)
  265. addAndMakeVisible (circleButtons[i]);
  266. }
  267. void resized() override
  268. {
  269. const auto r = getLocalBounds().reduced (10);
  270. const int rowHeight = r.getHeight() / 5;
  271. const int ledWidth = (r.getWidth() - 70) / numLeds;
  272. const int buttonWidth = (r.getWidth() - 40) / 5;
  273. auto row = r;
  274. auto ledRow = row.removeFromTop (rowHeight) .withSizeKeepingCentre (r.getWidth(), ledWidth);
  275. auto buttonRow1 = row.removeFromTop (rowHeight * 2).withSizeKeepingCentre (r.getWidth(), buttonWidth);
  276. auto buttonRow2 = row.removeFromTop (rowHeight * 2).withSizeKeepingCentre (r.getWidth(), buttonWidth);
  277. for (auto* led : leds)
  278. {
  279. led->setBounds (ledRow.removeFromLeft (ledWidth).reduced (2));
  280. ledRow.removeFromLeft (5);
  281. }
  282. for (int i = 0; i < 5; ++i)
  283. {
  284. circleButtons[i].setBounds (buttonRow1.removeFromLeft (buttonWidth).reduced (2));
  285. buttonRow1.removeFromLeft (10);
  286. }
  287. for (int i = 5; i < 8; ++i)
  288. {
  289. circleButtons[i].setBounds (buttonRow2.removeFromLeft (buttonWidth).reduced (2));
  290. buttonRow2.removeFromLeft (10);
  291. }
  292. roundedRectangleButton.setBounds (buttonRow2);
  293. }
  294. void paint (Graphics& g) override
  295. {
  296. auto r = getLocalBounds().toFloat();
  297. // Fill a black rectangle for the Control Block
  298. g.setColour (Colours::black);
  299. g.fillRoundedRectangle (r, r.getWidth() / 20.0f);
  300. }
  301. void handleButtonPressed (ControlButton::ButtonFunction function, uint32) override
  302. {
  303. displayButtonInteraction (controlButtonFunctionToIndex (function), true);
  304. }
  305. void handleButtonReleased (ControlButton::ButtonFunction function, uint32) override
  306. {
  307. displayButtonInteraction (controlButtonFunctionToIndex (function), false);
  308. }
  309. void handleBatteryLevelUpdate (float batteryLevel) override
  310. {
  311. // Update the number of LEDs that are on to represent the battery level
  312. int numLedsOn = static_cast<int> (numLeds * batteryLevel);
  313. if (numLedsOn != previousNumLedsOn)
  314. for (int i = 0; i < numLeds; ++i)
  315. leds.getUnchecked (i)->setOnState (i < numLedsOn);
  316. previousNumLedsOn = numLedsOn;
  317. repaint();
  318. }
  319. private:
  320. //==============================================================================
  321. /**
  322. Base class that renders a Control Block button
  323. */
  324. struct ControlBlockSubComponent : public Component,
  325. public TooltipClient
  326. {
  327. ControlBlockSubComponent (Colour componentColourToUse)
  328. : componentColour (componentColourToUse)
  329. {}
  330. /** Subclasses should override this to paint the button on the screen */
  331. virtual void paint (Graphics&) override = 0;
  332. /** Sets the colour of the button */
  333. void setColour (Colour c) { componentColour = c; }
  334. /** Sets the on state of the button */
  335. void setOnState (bool isOn)
  336. {
  337. onState = isOn;
  338. repaint();
  339. }
  340. /** Returns the Control Block tooltip */
  341. String getTooltip() override
  342. {
  343. for (Component* comp = this; comp != nullptr; comp = comp->getParentComponent())
  344. if (auto* sttc = dynamic_cast<SettableTooltipClient*> (comp))
  345. return sttc->getTooltip();
  346. return {};
  347. }
  348. //==============================================================================
  349. Colour componentColour;
  350. bool onState = false;
  351. //==============================================================================
  352. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ControlBlockSubComponent)
  353. };
  354. /**
  355. Class that renders a Control Block LED on the screen
  356. */
  357. struct LEDComponent : public ControlBlockSubComponent
  358. {
  359. LEDComponent() : ControlBlockSubComponent (Colours::green) {}
  360. void paint (Graphics& g) override
  361. {
  362. g.setColour (componentColour.withAlpha (onState ? 1.0f : 0.2f));
  363. g.fillEllipse (getLocalBounds().toFloat());
  364. }
  365. };
  366. /**
  367. Class that renders a Control Block single circular button on the screen
  368. */
  369. struct CircleButtonComponent : public ControlBlockSubComponent
  370. {
  371. CircleButtonComponent() : ControlBlockSubComponent (Colours::blue) {}
  372. void paint (Graphics& g) override
  373. {
  374. g.setColour (componentColour.withAlpha (onState ? 1.0f : 0.2f));
  375. g.fillEllipse (getLocalBounds().toFloat());
  376. }
  377. };
  378. /**
  379. Class that renders a Control Block rounded rectangular button containing two buttons
  380. on the screen
  381. */
  382. struct RoundedRectangleButtonComponent : public ControlBlockSubComponent
  383. {
  384. RoundedRectangleButtonComponent() : ControlBlockSubComponent (Colours::blue) {}
  385. void paint (Graphics& g) override
  386. {
  387. auto r = getLocalBounds().toFloat();
  388. g.setColour (componentColour.withAlpha (0.2f));
  389. g.fillRoundedRectangle (r.toFloat(), 20.0f);
  390. g.setColour (componentColour.withAlpha (1.0f));
  391. // is a button pressed?
  392. if (doubleButtonOnState[0] || doubleButtonOnState[1])
  393. {
  394. auto semiButtonWidth = r.getWidth() / 2.0f;
  395. auto semiButtonBounds = r.withWidth (semiButtonWidth)
  396. .withX (doubleButtonOnState[1] ? semiButtonWidth : 0)
  397. .reduced (5.0f, 2.0f);
  398. g.fillEllipse (semiButtonBounds);
  399. }
  400. }
  401. void setPressedState (bool isPressed, int button)
  402. {
  403. doubleButtonOnState[button] = isPressed;
  404. repaint();
  405. }
  406. private:
  407. bool doubleButtonOnState[2] = { false, false };
  408. };
  409. /** Displays a button press or release interaction for a button at a given index */
  410. void displayButtonInteraction (int buttonIndex, bool isPressed)
  411. {
  412. if (! isPositiveAndBelow (buttonIndex, 10))
  413. return;
  414. if (buttonIndex >= 8)
  415. roundedRectangleButton.setPressedState (isPressed, buttonIndex == 8);
  416. else
  417. circleButtons[buttonIndex].setOnState (isPressed);
  418. }
  419. //==============================================================================
  420. int numLeds;
  421. OwnedArray<LEDComponent> leds;
  422. CircleButtonComponent circleButtons[8];
  423. RoundedRectangleButtonComponent roundedRectangleButton;
  424. int previousNumLedsOn;
  425. //==============================================================================
  426. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ControlBlockComponent)
  427. };
  428. #endif // BLOCKCOMPONENTS_H_INCLUDED