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.

522 lines
19KB

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