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.

700 lines
24KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE examples.
  4. Copyright (c) 2017 - ROLI Ltd.
  5. The code included in this file is provided under the terms of the ISC license
  6. http://www.isc.org/downloads/software-support-policy/isc-license. Permission
  7. To use, copy, modify, and/or distribute this software for any purpose with or
  8. without fee is hereby granted provided that the above copyright notice and
  9. this permission notice appear in all copies.
  10. THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES,
  11. WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR
  12. PURPOSE, ARE DISCLAIMED.
  13. ==============================================================================
  14. */
  15. /*******************************************************************************
  16. The block below describes the properties of this PIP. A PIP is a short snippet
  17. of code that can be read by the Projucer and used to generate a JUCE project.
  18. BEGIN_JUCE_PIP_METADATA
  19. name: BlocksDrawingDemo
  20. version: 1.0.0
  21. vendor: JUCE
  22. website: http://juce.com
  23. description: Blocks application to draw shapes.
  24. dependencies: juce_audio_basics, juce_audio_devices, juce_audio_formats,
  25. juce_audio_processors, juce_audio_utils, juce_blocks_basics,
  26. juce_core, juce_data_structures, juce_events, juce_graphics,
  27. juce_gui_basics, juce_gui_extra
  28. exporters: xcode_mac, vs2017, linux_make, xcode_iphone
  29. moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1
  30. type: Component
  31. mainClass: BlocksDrawingDemo
  32. useLocalCopy: 1
  33. END_JUCE_PIP_METADATA
  34. *******************************************************************************/
  35. #pragma once
  36. //==============================================================================
  37. /**
  38. Represents a single LED on a Lightpad
  39. */
  40. struct LEDComponent : public Component
  41. {
  42. LEDComponent() : ledColour (Colours::black) { setInterceptsMouseClicks (false, false); }
  43. void setColour (Colour newColour)
  44. {
  45. ledColour = newColour;
  46. repaint();
  47. }
  48. void paint (Graphics& g) override
  49. {
  50. g.setColour (ledColour);
  51. g.fillEllipse (getLocalBounds().toFloat());
  52. }
  53. Colour ledColour;
  54. };
  55. //==============================================================================
  56. /**
  57. A component that is used to represent a Lightpad on-screen
  58. */
  59. class DrawableLightpadComponent : public Component
  60. {
  61. public:
  62. DrawableLightpadComponent ()
  63. {
  64. for (auto x = 0; x < 15; ++x)
  65. for (auto y = 0; y < 15; ++y)
  66. addAndMakeVisible (leds.add (new LEDComponent()));
  67. }
  68. void paint (Graphics& g) override
  69. {
  70. auto r = getLocalBounds().toFloat();
  71. // Clip the drawing area to only draw in the block area
  72. {
  73. Path clipArea;
  74. clipArea.addRoundedRectangle (r, r.getWidth() / 20.0f);
  75. g.reduceClipRegion (clipArea);
  76. }
  77. // Fill a black square for the Lightpad
  78. g.fillAll (Colours::black);
  79. }
  80. void resized() override
  81. {
  82. auto r = getLocalBounds().reduced (10);
  83. auto circleWidth = r.getWidth() / 15;
  84. auto circleHeight = r.getHeight() / 15;
  85. for (auto x = 0; x < 15; ++x)
  86. for (auto y = 0; y < 15; ++y)
  87. leds.getUnchecked ((x * 15) + y)->setBounds (r.getX() + (x * circleWidth),
  88. r.getY() + (y * circleHeight),
  89. circleWidth, circleHeight);
  90. }
  91. void mouseDown (const MouseEvent& e) override
  92. {
  93. for (auto x = 0; x < 15; ++x)
  94. for (auto y = 0; y < 15; ++y)
  95. if (leds.getUnchecked ((x * 15) + y)->getBounds().contains (e.position.toInt()))
  96. listeners.call ([&] (Listener& l) { l.ledClicked (x, y, e.pressure); });
  97. }
  98. void mouseDrag (const MouseEvent& e) override
  99. {
  100. for (auto x = 0; x < 15; ++x)
  101. {
  102. for (auto y = 0; y < 15; ++y)
  103. {
  104. if (leds.getUnchecked ((x * 15) + y)->getBounds().contains (e.position.toInt()))
  105. {
  106. auto t = e.eventTime;
  107. if (lastLED == Point<int> (x, y) && t.toMilliseconds() - lastMouseEventTime.toMilliseconds() < 50)
  108. return;
  109. listeners.call ([&] (Listener& l) { l.ledClicked (x, y, e.pressure); });
  110. lastLED = { x, y };
  111. lastMouseEventTime = t;
  112. }
  113. }
  114. }
  115. }
  116. //==============================================================================
  117. /** Sets the colour of one of the LEDComponents */
  118. void setLEDColour (int x, int y, Colour c)
  119. {
  120. x = jmin (x, 14);
  121. y = jmin (y, 14);
  122. leds.getUnchecked ((x * 15) + y)->setColour (c);
  123. }
  124. //==============================================================================
  125. struct Listener
  126. {
  127. virtual ~Listener() {}
  128. /** Called when an LEDComponent has been clicked */
  129. virtual void ledClicked (int x, int y, float z) = 0;
  130. };
  131. void addListener (Listener* l) { listeners.add (l); }
  132. void removeListener (Listener* l) { listeners.remove (l); }
  133. private:
  134. OwnedArray<LEDComponent> leds;
  135. ListenerList<Listener> listeners;
  136. Time lastMouseEventTime;
  137. Point<int> lastLED;
  138. };
  139. //==============================================================================
  140. /**
  141. A struct that handles the setup and layout of the DrumPadGridProgram
  142. */
  143. struct ColourGrid
  144. {
  145. ColourGrid (int cols, int rows)
  146. : numColumns (cols),
  147. numRows (rows)
  148. {
  149. constructGridFillArray();
  150. }
  151. /** Creates a GridFill object for each pad in the grid and sets its colour
  152. and fill before adding it to an array of GridFill objects
  153. */
  154. void constructGridFillArray()
  155. {
  156. gridFillArray.clear();
  157. auto counter = 0;
  158. for (auto i = 0; i < numColumns; ++i)
  159. {
  160. for (auto j = 0; j < numRows; ++j)
  161. {
  162. DrumPadGridProgram::GridFill fill;
  163. Colour colourToUse = colourArray.getUnchecked (counter);
  164. fill.colour = colourToUse.withBrightness (colourToUse == currentColour ? 1.0f : 0.1f);
  165. if (colourToUse == Colours::black)
  166. fill.fillType = DrumPadGridProgram::GridFill::FillType::hollow;
  167. else
  168. fill.fillType = DrumPadGridProgram::GridFill::FillType::filled;
  169. gridFillArray.add (fill);
  170. if (++counter == colourArray.size())
  171. counter = 0;
  172. }
  173. }
  174. }
  175. /** Sets which colour should be active for a given touch co-ordinate. Returns
  176. true if the colour has changed
  177. */
  178. bool setActiveColourForTouch (int x, int y)
  179. {
  180. auto colourHasChanged = false;
  181. auto xindex = x / 5;
  182. auto yindex = y / 5;
  183. auto newColour = colourArray.getUnchecked ((yindex * 3) + xindex);
  184. if (currentColour != newColour)
  185. {
  186. currentColour = newColour;
  187. constructGridFillArray();
  188. colourHasChanged = true;
  189. }
  190. return colourHasChanged;
  191. }
  192. //==============================================================================
  193. int numColumns, numRows;
  194. Array<DrumPadGridProgram::GridFill> gridFillArray;
  195. Array<Colour> colourArray = { Colours::white, Colours::red, Colours::green,
  196. Colours::blue, Colours::hotpink, Colours::orange,
  197. Colours::magenta, Colours::cyan, Colours::black };
  198. Colour currentColour = Colours::hotpink;
  199. //==============================================================================
  200. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ColourGrid)
  201. };
  202. //==============================================================================
  203. /**
  204. The main component
  205. */
  206. class BlocksDrawingDemo : public Component,
  207. public TopologySource::Listener,
  208. private TouchSurface::Listener,
  209. private ControlButton::Listener,
  210. private DrawableLightpadComponent::Listener,
  211. private Timer
  212. {
  213. public:
  214. //==============================================================================
  215. BlocksDrawingDemo()
  216. {
  217. activeLeds.clear();
  218. // Register MainContentComponent as a listener to the PhysicalTopologySource object
  219. topologySource.addListener (this);
  220. infoLabel.setText ("Connect a Lightpad Block to draw.", dontSendNotification);
  221. infoLabel.setJustificationType (Justification::centred);
  222. addAndMakeVisible (infoLabel);
  223. addAndMakeVisible (lightpadComponent);
  224. lightpadComponent.setVisible (false);
  225. lightpadComponent.addListener (this);
  226. clearButton.setButtonText ("Clear");
  227. clearButton.onClick = [this] { clearLEDs(); };
  228. clearButton.setAlwaysOnTop (true);
  229. addAndMakeVisible (clearButton);
  230. brightnessSlider.setRange (0.0, 1.0);
  231. brightnessSlider.setValue (1.0);
  232. brightnessSlider.setAlwaysOnTop (true);
  233. brightnessSlider.setTextBoxStyle (Slider::TextEntryBoxPosition::NoTextBox, false, 0, 0);
  234. brightnessSlider.onValueChange = [this]
  235. {
  236. brightnessLED.setColour (layout.currentColour
  237. .withBrightness (layout.currentColour == Colours::black ? 0.0f
  238. : static_cast<float> (brightnessSlider.getValue())));
  239. };
  240. addAndMakeVisible (brightnessSlider);
  241. brightnessLED.setAlwaysOnTop (true);
  242. brightnessLED.setColour (layout.currentColour.withBrightness (static_cast<float> (brightnessSlider.getValue())));
  243. addAndMakeVisible (brightnessLED);
  244. #if JUCE_IOS
  245. connectButton.setButtonText ("Connect");
  246. connectButton.onClick = [] { BluetoothMidiDevicePairingDialogue::open(); };
  247. connectButton.setAlwaysOnTop (true);
  248. addAndMakeVisible (connectButton);
  249. #endif
  250. setSize (600, 600);
  251. topologyChanged();
  252. }
  253. ~BlocksDrawingDemo()
  254. {
  255. if (activeBlock != nullptr)
  256. detachActiveBlock();
  257. lightpadComponent.removeListener (this);
  258. topologySource.removeListener (this);
  259. }
  260. void resized() override
  261. {
  262. infoLabel.centreWithSize (getWidth(), 100);
  263. auto bounds = getLocalBounds().reduced (20);
  264. // top buttons
  265. auto topButtonArea = bounds.removeFromTop (getHeight() / 20);
  266. topButtonArea.removeFromLeft (20);
  267. clearButton.setBounds (topButtonArea.removeFromLeft (80));
  268. #if JUCE_IOS
  269. topButtonArea.removeFromRight (20);
  270. connectButton.setBounds (topButtonArea.removeFromRight (80));
  271. #endif
  272. bounds.removeFromTop (20);
  273. auto orientation = Desktop::getInstance().getCurrentOrientation();
  274. if (orientation == Desktop::DisplayOrientation::upright
  275. || orientation == Desktop::DisplayOrientation::upsideDown)
  276. {
  277. auto brightnessControlBounds = bounds.removeFromBottom (getHeight() / 10);
  278. brightnessSlider.setSliderStyle (Slider::SliderStyle::LinearHorizontal);
  279. brightnessLED.setBounds (brightnessControlBounds.removeFromLeft (getHeight() / 10));
  280. brightnessSlider.setBounds (brightnessControlBounds);
  281. }
  282. else
  283. {
  284. auto brightnessControlBounds = bounds.removeFromRight (getWidth() / 10);
  285. brightnessSlider.setSliderStyle (Slider::SliderStyle::LinearVertical);
  286. brightnessLED.setBounds (brightnessControlBounds.removeFromTop (getWidth() / 10));
  287. brightnessSlider.setBounds (brightnessControlBounds);
  288. }
  289. // lightpad component
  290. auto sideLength = jmin (bounds.getWidth() - 40, bounds.getHeight() - 40);
  291. lightpadComponent.centreWithSize (sideLength, sideLength);
  292. }
  293. /** Overridden from TopologySource::Listener. Called when the topology changes */
  294. void topologyChanged() override
  295. {
  296. lightpadComponent.setVisible (false);
  297. infoLabel.setVisible (true);
  298. // Reset the activeBlock object
  299. if (activeBlock != nullptr)
  300. detachActiveBlock();
  301. // Get the array of currently connected Block objects from the PhysicalTopologySource
  302. auto blocks = topologySource.getCurrentTopology().blocks;
  303. // Iterate over the array of Block objects
  304. for (auto b : blocks)
  305. {
  306. // Find the first Lightpad
  307. if (b->getType() == Block::Type::lightPadBlock)
  308. {
  309. activeBlock = b;
  310. // Register MainContentComponent as a listener to the touch surface
  311. if (auto surface = activeBlock->getTouchSurface())
  312. surface->addListener (this);
  313. // Register MainContentComponent as a listener to any buttons
  314. for (auto button : activeBlock->getButtons())
  315. button->addListener (this);
  316. // Get the LEDGrid object from the Lightpad and set its program to the program for the current mode
  317. if (auto grid = activeBlock->getLEDGrid())
  318. {
  319. // Work out scale factors to translate X and Y touches to LED indexes
  320. scaleX = (float) (grid->getNumColumns() - 1) / activeBlock->getWidth();
  321. scaleY = (float) (grid->getNumRows() - 1) / activeBlock->getHeight();
  322. setLEDProgram (*activeBlock);
  323. }
  324. // Make the on screen Lighpad component visible
  325. lightpadComponent.setVisible (true);
  326. infoLabel.setVisible (false);
  327. break;
  328. }
  329. }
  330. }
  331. private:
  332. //==============================================================================
  333. /** Overridden from TouchSurface::Listener. Called when a Touch is received on the Lightpad */
  334. void touchChanged (TouchSurface&, const TouchSurface::Touch& touch) override
  335. {
  336. // Translate X and Y touch events to LED indexes
  337. auto xLed = roundToInt (touch.x * scaleX);
  338. auto yLed = roundToInt (touch.y * scaleY);
  339. if (currentMode == colourPalette)
  340. {
  341. if (layout.setActiveColourForTouch (xLed, yLed))
  342. {
  343. if (auto* colourPaletteProgram = getPaletteProgram())
  344. {
  345. colourPaletteProgram->setGridFills (layout.numColumns, layout.numRows, layout.gridFillArray);
  346. brightnessLED.setColour (layout.currentColour
  347. .withBrightness (layout.currentColour == Colours::black ? 0.0f
  348. : static_cast<float> (brightnessSlider.getValue())));
  349. }
  350. }
  351. }
  352. else if (currentMode == canvas)
  353. {
  354. drawLED ((uint32) xLed, (uint32) yLed, touch.z, layout.currentColour);
  355. }
  356. }
  357. /** Overridden from ControlButton::Listener. Called when a button on the Lightpad is pressed */
  358. void buttonPressed (ControlButton&, Block::Timestamp) override {}
  359. /** Overridden from ControlButton::Listener. Called when a button on the Lightpad is released */
  360. void buttonReleased (ControlButton&, Block::Timestamp) override
  361. {
  362. if (currentMode == canvas)
  363. {
  364. // Wait 500ms to see if there is a second press
  365. if (! isTimerRunning())
  366. startTimer (500);
  367. else
  368. doublePress = true;
  369. }
  370. else if (currentMode == colourPalette)
  371. {
  372. // Switch to canvas mode and set the LEDGrid program
  373. currentMode = canvas;
  374. setLEDProgram (*activeBlock);
  375. }
  376. }
  377. void ledClicked (int x, int y, float z) override
  378. {
  379. drawLED ((uint32) x, (uint32) y,
  380. z == 0.0f ? static_cast<float> (brightnessSlider.getValue())
  381. : z * static_cast<float> (brightnessSlider.getValue()), layout.currentColour);
  382. }
  383. void timerCallback() override
  384. {
  385. if (doublePress)
  386. {
  387. clearLEDs();
  388. // Reset the doublePress flag
  389. doublePress = false;
  390. }
  391. else
  392. {
  393. // Switch to colour palette mode and set the LEDGrid program
  394. currentMode = colourPalette;
  395. setLEDProgram (*activeBlock);
  396. }
  397. stopTimer();
  398. }
  399. /** Removes TouchSurface and ControlButton listeners and sets activeBlock to nullptr */
  400. void detachActiveBlock()
  401. {
  402. if (auto surface = activeBlock->getTouchSurface())
  403. surface->removeListener (this);
  404. for (auto button : activeBlock->getButtons())
  405. button->removeListener (this);
  406. activeBlock = nullptr;
  407. }
  408. /** Sets the LEDGrid Program for the selected mode */
  409. void setLEDProgram (Block& block)
  410. {
  411. if (currentMode == canvas)
  412. {
  413. block.setProgram (new BitmapLEDProgram (block));
  414. // Redraw any previously drawn LEDs
  415. redrawLEDs();
  416. }
  417. else if (currentMode == colourPalette)
  418. {
  419. block.setProgram (new DrumPadGridProgram (block));
  420. // Setup the grid layout
  421. if (auto* program = getPaletteProgram())
  422. program->setGridFills (layout.numColumns, layout.numRows, layout.gridFillArray);
  423. }
  424. }
  425. void clearLEDs()
  426. {
  427. if (auto* canvasProgram = getCanvasProgram())
  428. {
  429. // Clear the LED grid
  430. for (uint32 x = 0; x < 15; ++x)
  431. {
  432. for (uint32 y = 0; y < 15; ++ y)
  433. {
  434. canvasProgram->setLED (x, y, Colours::black);
  435. lightpadComponent.setLEDColour ((int) x, (int) y, Colours::black);
  436. }
  437. }
  438. // Clear the ActiveLED array
  439. activeLeds.clear();
  440. }
  441. }
  442. /** Sets an LED on the Lightpad for a given touch co-ordinate and pressure */
  443. void drawLED (uint32 x0, uint32 y0, float z, Colour drawColour)
  444. {
  445. if (auto* canvasProgram = getCanvasProgram())
  446. {
  447. // Check if the activeLeds array already contains an ActiveLED object for this LED
  448. auto index = getLEDAt (x0, y0);
  449. // If the colour is black then just set the LED to black and return
  450. if (drawColour == Colours::black)
  451. {
  452. if (index >= 0)
  453. {
  454. canvasProgram->setLED (x0, y0, Colours::black);
  455. lightpadComponent.setLEDColour ((int) x0, (int) y0, Colours::black);
  456. activeLeds.remove (index);
  457. }
  458. return;
  459. }
  460. // If there is no ActiveLED obejct for this LED then create one,
  461. // add it to the array, set the LED on the Block and return
  462. if (index < 0)
  463. {
  464. ActiveLED led;
  465. led.x = x0;
  466. led.y = y0;
  467. led.colour = drawColour;
  468. led.brightness = z;
  469. activeLeds.add (led);
  470. canvasProgram->setLED (led.x, led.y, led.colour.withBrightness (led.brightness));
  471. lightpadComponent.setLEDColour ((int) led.x, (int) led.y, led.colour.withBrightness (led.brightness));
  472. return;
  473. }
  474. // Get the ActiveLED object for this LED
  475. auto currentLed = activeLeds.getReference (index);
  476. // If the LED colour is the same as the draw colour, add the brightnesses together.
  477. // If it is different, blend the colours
  478. if (currentLed.colour == drawColour)
  479. currentLed.brightness = jmin (currentLed.brightness + z, 1.0f);
  480. else
  481. currentLed.colour = currentLed.colour.interpolatedWith (drawColour, z);
  482. // Set the LED on the Block and change the ActiveLED object in the activeLeds array
  483. if (canvasProgram != nullptr)
  484. canvasProgram->setLED (currentLed.x, currentLed.y, currentLed.colour.withBrightness (currentLed.brightness));
  485. lightpadComponent.setLEDColour ((int) currentLed.x, (int) currentLed.y, currentLed.colour.withBrightness (currentLed.brightness));
  486. activeLeds.set (index, currentLed);
  487. }
  488. }
  489. /** Redraws the LEDs on the Lightpad from the activeLeds array */
  490. void redrawLEDs()
  491. {
  492. if (auto* canvasProgram = getCanvasProgram())
  493. {
  494. // Iterate over the activeLeds array and set the LEDs on the Block
  495. for (auto led : activeLeds)
  496. {
  497. canvasProgram->setLED (led.x, led.y, led.colour.withBrightness (led.brightness));
  498. lightpadComponent.setLEDColour ((int) led.x, (int) led.y, led.colour.withBrightness (led.brightness));
  499. }
  500. }
  501. }
  502. //==============================================================================
  503. BitmapLEDProgram* getCanvasProgram()
  504. {
  505. if (activeBlock != nullptr)
  506. return dynamic_cast<BitmapLEDProgram*> (activeBlock->getProgram());
  507. return nullptr;
  508. }
  509. DrumPadGridProgram* getPaletteProgram()
  510. {
  511. if (activeBlock != nullptr)
  512. return dynamic_cast<DrumPadGridProgram*> (activeBlock->getProgram());
  513. return nullptr;
  514. }
  515. //==============================================================================
  516. /**
  517. A struct that represents an active LED on the Lightpad.
  518. Has a position, colour and brightness.
  519. */
  520. struct ActiveLED
  521. {
  522. uint32 x, y;
  523. Colour colour;
  524. float brightness;
  525. /** Returns true if this LED occupies the given co-ordinates */
  526. bool occupies (uint32 xPos, uint32 yPos) const
  527. {
  528. return xPos == x && yPos == y;
  529. }
  530. };
  531. Array<ActiveLED> activeLeds;
  532. int getLEDAt (uint32 x, uint32 y) const
  533. {
  534. for (auto i = 0; i < activeLeds.size(); ++i)
  535. if (activeLeds.getReference (i).occupies (x, y))
  536. return i;
  537. return -1;
  538. }
  539. //==============================================================================
  540. enum DisplayMode
  541. {
  542. colourPalette = 0,
  543. canvas
  544. };
  545. DisplayMode currentMode = colourPalette;
  546. //==============================================================================
  547. ColourGrid layout { 3, 3 };
  548. PhysicalTopologySource topologySource;
  549. Block::Ptr activeBlock;
  550. float scaleX = 0.0f;
  551. float scaleY = 0.0f;
  552. bool doublePress = false;
  553. Label infoLabel;
  554. DrawableLightpadComponent lightpadComponent;
  555. TextButton clearButton;
  556. LEDComponent brightnessLED;
  557. Slider brightnessSlider;
  558. #if JUCE_IOS
  559. TextButton connectButton;
  560. #endif
  561. //==============================================================================
  562. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BlocksDrawingDemo)
  563. };