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.

548 lines
20KB

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