#ifndef BLOCKCOMPONENTS_H_INCLUDED #define BLOCKCOMPONENTS_H_INCLUDED #include "../JuceLibraryCode/JuceHeader.h" //============================================================================== /** Base class that renders a Block on the screen */ class BlockComponent : public Component, public SettableTooltipClient, private TouchSurface::Listener, private ControlButton::Listener, private Timer { public: /** Constructor */ BlockComponent (Block::Ptr blockToUse) : block (blockToUse) { updateStatsAndTooltip(); // Register BlockComponent as a listener to the touch surface if (auto touchSurface = block->getTouchSurface()) touchSurface->addListener (this); // Register BlockComponent as a listener to any buttons for (auto button : block->getButtons()) button->addListener (this); // If this is a Lightpad then set the grid program to be blank if (auto grid = block->getLEDGrid()) grid->setProgram (new BitmapLEDProgram(*grid)); // If this is a Lightpad then redraw it at 25Hz if (block->getType() == Block::lightPadBlock) startTimerHz (25); } /** Destructor */ ~BlockComponent() { // Remove any listeners if (auto touchSurface = block->getTouchSurface()) touchSurface->removeListener (this); for (auto button : block->getButtons()) button->removeListener (this); } /** Called periodically to update the tooltip with inforamtion about the Block */ void updateStatsAndTooltip() { // Get the battery level of this Block and inform any subclasses const float batteryLevel = block->getBatteryLevel(); handleBatteryLevelUpdate (batteryLevel); // Format the tooltip string const String ttString = "Name = " + block->getDeviceDescription() + "\n" + "UID = " + String (block->uid) + "\n" + "Serial number = " + block->serialNumber + "\n" + "Battery level = " + String ((int) (batteryLevel * 100)) + "%" + (block->isBatteryCharging() ? "++" : "--"); // Update the tooltip string if it has changed if (ttString != getTooltip()) setTooltip (ttString); } /** Subclasses should override this to paint the Block object on the screen */ virtual void paint (Graphics&) override = 0; /** Subclasses can override this to receive button down events from the Block */ virtual void handleButtonPressed (ControlButton::ButtonFunction, uint32) {} /** Subclasses can override this to receive button up events from the Block */ virtual void handleButtonReleased (ControlButton::ButtonFunction, uint32) {} /** Subclasses can override this to receive touch events from the Block */ virtual void handleTouchChange (TouchSurface::Touch) {} /** Subclasses can override this to battery level updates from the Block */ virtual void handleBatteryLevelUpdate (float) {} /** The Block object that this class represents */ Block::Ptr block; //============================================================================== /** Returns an integer index corresponding to a physical position on the hardware for each type of Control Block. */ static int controlButtonFunctionToIndex (ControlButton::ButtonFunction f) { static std::initializer_list map[] = {{ControlButton::mode, ControlButton::button0}, {ControlButton::volume, ControlButton::button1}, {ControlButton::scale, ControlButton::button2, ControlButton::click}, {ControlButton::chord, ControlButton::button3, ControlButton::snap}, {ControlButton::arp,ControlButton:: button4, ControlButton::back}, {ControlButton::sustain, ControlButton::button5, ControlButton::playOrPause}, {ControlButton::octave, ControlButton::button6, ControlButton::record}, {ControlButton::love, ControlButton::button7, ControlButton::learn}, {ControlButton::up}, {ControlButton::down}}; for (size_t i = 0; i < (sizeof (map) / sizeof (map[0])); ++i) if (std::find (map[i].begin(), map[i].end(), f) != map[i].end()) return static_cast (i); return -1; } private: /** Used to call repaint() periodically */ void timerCallback() override { repaint(); } /** Overridden from TouchSurface::Listener */ void touchChanged (TouchSurface&, const TouchSurface::Touch& t) override { handleTouchChange (t); } /** Overridden from ControlButton::Listener */ void buttonPressed (ControlButton& b, Block::Timestamp t) override { handleButtonPressed (b.getType(), t); } /** Overridden from ControlButton::Listener */ void buttonReleased (ControlButton& b, Block::Timestamp t) override { handleButtonReleased (b.getType(), t); } //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BlockComponent) }; /** Class that renders a Lightpad on the screen */ class LightpadComponent : public BlockComponent { public: LightpadComponent (Block::Ptr blockToUse) : BlockComponent (blockToUse) { } void paint (Graphics& g) override { auto r = getLocalBounds().toFloat(); // clip the drawing area to only draw in the block area { Path clipArea; clipArea.addRoundedRectangle (r, r.getWidth() / 20.0f); g.reduceClipRegion (clipArea); } // Fill a black square for the Lightpad g.fillAll (Colours::black); // size ration between physical and on-screen blocks const Point ratio (r.getWidth() / block->getWidth(), r.getHeight() / block->getHeight()); const float maxCircleSize = block->getWidth() / 3.0f; // iterate over the list of current touches and draw them on the onscreen Block for (auto touch : touches) { const float circleSize = touch.touch.z * maxCircleSize; const Point touchPosition = Point (touch.touch.x, touch.touch.y); const Colour c = colourArray[touch.touch.index]; const Rectangle blob = (Rectangle (circleSize, circleSize).withCentre (touchPosition)) * ratio; const ColourGradient cg = ColourGradient (colourArray[touch.touch.index], blob.getCentreX(), blob.getCentreY(), Colours::transparentBlack, blob.getRight(), blob.getBottom(), true); g.setGradientFill (cg); g.fillEllipse (blob); } } void handleTouchChange (TouchSurface::Touch touch) override { touches.updateTouch (touch); } private: /** An Array of colours to use for touches */ Array colourArray = { Colours::red, Colours::blue, Colours::green, Colours::yellow, Colours::white, Colours::hotpink, Colours::mediumpurple }; /** A list of current Touch events */ TouchList touches; //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LightpadComponent) }; /** Class that renders a Control Block on the screen */ class ControlBlockComponent : public BlockComponent { public: ControlBlockComponent (Block::Ptr blockToUse) : BlockComponent (blockToUse), numLeds (block->getLEDRow()->getNumLEDs()) { addAndMakeVisible (roundedRectangleButton); // Display the battery level on the LEDRow int numLedsToTurnOn = static_cast (static_cast (numLeds) * block->getBatteryLevel()); // add LEDs LEDComponent* ledComponent; for (int i = 0; i < numLeds; ++i) { ledComponent = new LEDComponent(); ledComponent->setOnState (i < numLedsToTurnOn); addAndMakeVisible (leds.add (ledComponent)); } previousNumLedsOn = numLedsToTurnOn; // add buttons for (int i = 0; i < 8; ++i) addAndMakeVisible (circleButtons[i]); } void resized() override { const auto r = getLocalBounds().reduced (10); const int rowHeight = r.getHeight() / 5; const int ledWidth = (r.getWidth() - 70) / numLeds; const int buttonWidth = (r.getWidth() - 40) / 5; auto row = r; auto ledRow = row.removeFromTop (rowHeight) .withSizeKeepingCentre (r.getWidth(), ledWidth); auto buttonRow1 = row.removeFromTop (rowHeight * 2).withSizeKeepingCentre (r.getWidth(), buttonWidth); auto buttonRow2 = row.removeFromTop (rowHeight * 2).withSizeKeepingCentre (r.getWidth(), buttonWidth); for (int i = 0; i < numLeds; ++i) { leds.getUnchecked (i)->setBounds (ledRow.removeFromLeft (ledWidth).reduced (2)); ledRow.removeFromLeft (5); } for (int i = 0; i < 5; ++i) { circleButtons[i].setBounds (buttonRow1.removeFromLeft (buttonWidth).reduced (2)); buttonRow1.removeFromLeft (10); } for (int i = 5; i < 8; ++i) { circleButtons[i].setBounds (buttonRow2.removeFromLeft (buttonWidth).reduced (2)); buttonRow2.removeFromLeft (10); } roundedRectangleButton.setBounds (buttonRow2); } void paint (Graphics& g) override { const auto r = getLocalBounds().toFloat(); // Fill a black rectangle for the Control Block g.setColour (Colours::black); g.fillRoundedRectangle (r, r.getWidth() / 20.0f); } void handleButtonPressed (ControlButton::ButtonFunction function, uint32) override { displayButtonInteraction (controlButtonFunctionToIndex (function), true); } void handleButtonReleased (ControlButton::ButtonFunction function, uint32) override { displayButtonInteraction (controlButtonFunctionToIndex (function), false); } void handleBatteryLevelUpdate (float batteryLevel) override { // Update the number of LEDs that are on to represent the battery level int numLedsOn = static_cast (static_cast (numLeds) * batteryLevel); if (numLedsOn != previousNumLedsOn) for (int i = 0; i < numLeds; ++i) leds.getUnchecked (i)->setOnState (i < numLedsOn); previousNumLedsOn = numLedsOn; repaint(); } private: /** Base class that renders a Control Block button */ struct ControlBlockSubComponent : public Component, public TooltipClient { ControlBlockSubComponent (Colour componentColourToUse) : componentColour (componentColourToUse), onState (false) {} /** Subclasses should override this to paint the button on the screen */ virtual void paint (Graphics&) override = 0; /** Sets the colour of the button */ void setColour (Colour c) { componentColour = c; } /** Sets the on state of the button */ void setOnState (bool isOn) { onState = isOn; repaint(); } /** Returns the Control Block tooltip */ String getTooltip() override { for (Component* comp = this; comp != nullptr; comp = comp->getParentComponent()) if (SettableTooltipClient* sttc = dynamic_cast (comp)) return sttc->getTooltip(); return String(); } //============================================================================== Colour componentColour; bool onState; //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ControlBlockSubComponent) }; /** Class that renders a Control Block LED on the screen */ struct LEDComponent : public ControlBlockSubComponent { LEDComponent() : ControlBlockSubComponent (Colours::green) {} void paint (Graphics& g) override { g.setColour (componentColour.withAlpha (onState ? 1.0f : 0.2f)); g.fillEllipse (getLocalBounds().toFloat()); } }; /** Class that renders a Control Block single circular button on the screen */ struct CircleButtonComponent : public ControlBlockSubComponent { CircleButtonComponent() : ControlBlockSubComponent (Colours::blue) {} void paint (Graphics& g) override { g.setColour (componentColour.withAlpha (onState ? 1.0f : 0.2f)); g.fillEllipse (getLocalBounds().toFloat()); } }; /** Class that renders a Control Block rounded rectangular button containing two buttons on the screen */ struct RoundedRectangleButtonComponent : public ControlBlockSubComponent { RoundedRectangleButtonComponent() : ControlBlockSubComponent (Colours::blue) {} void paint (Graphics& g) override { const auto r = getLocalBounds().toFloat(); g.setColour (componentColour.withAlpha (0.2f)); g.fillRoundedRectangle (r.toFloat(), 20.0f); g.setColour (componentColour.withAlpha (1.0f)); // is a button pressed? if (doubleButtonOnState[0] || doubleButtonOnState[1]) { const float semiButtonWidth = r.getWidth() / 2.0f; const auto semiButtonBounds = r.withWidth (semiButtonWidth) .withX (doubleButtonOnState[1] ? semiButtonWidth : 0) .reduced (5.0f, 2.0f); g.fillEllipse (semiButtonBounds); } } void setPressedState (bool isPressed, int button) { doubleButtonOnState[button] = isPressed; repaint(); } private: bool doubleButtonOnState[2] = {false, false}; }; /** Displays a button press or release interaction for a button at a given index */ void displayButtonInteraction (int buttonIndex, bool isPressed) { if (! isPositiveAndBelow (buttonIndex, 10)) return; if (buttonIndex >= 8) roundedRectangleButton.setPressedState (isPressed, buttonIndex == 8); else circleButtons[buttonIndex].setOnState (isPressed); } //============================================================================== int numLeds; OwnedArray leds; CircleButtonComponent circleButtons[8]; RoundedRectangleButtonComponent roundedRectangleButton; int previousNumLedsOn; //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ControlBlockComponent) }; #endif // BLOCKCOMPONENTS_H_INCLUDED