#pragma once #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: 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); // Make sure the component can't go offscreen if it is draggable constrainer.setMinimumOnscreenAmounts (50, 50, 50, 50); } ~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 auto batteryLevel = block->getBatteryLevel(); handleBatteryLevelUpdate (batteryLevel); // Update the tooltip setTooltip ("Name = " + block->getDeviceDescription() + "\n" + "UID = " + String (block->uid) + "\n" + "Serial number = " + block->serialNumber + "\n" + "Battery level = " + String ((int) (batteryLevel * 100)) + "%" + (block->isBatteryCharging() ? "++" : "--")); } /** 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) { using CB = ControlButton; static Array map[] = { { CB::mode, CB::button0 }, { CB::volume, CB::button1 }, { CB::scale, CB::button2, CB::click }, { CB::chord, CB::button3, CB::snap }, { CB::arp, CB::button4, CB::back }, { CB::sustain, CB::button5, CB::playOrPause }, { CB::octave, CB::button6, CB::record }, { CB::love, CB::button7, CB::learn }, { CB::up }, { CB::down } }; for (int i = 0; i < numElementsInArray (map); ++i) if (map[i].contains (f)) return i; return -1; } Point getOffsetForPort (Block::ConnectionPort port) { using e = Block::ConnectionPort::DeviceEdge; switch (rotation) { case 0: { switch (port.edge) { case e::north: return { static_cast (port.index), 0.0f }; case e::east: return { static_cast (block->getWidth()), static_cast (port.index) }; case e::south: return { static_cast (port.index), static_cast (block->getHeight()) }; case e::west: return { 0.0f, static_cast (port.index) }; } } case 90: { switch (port.edge) { case e::north: return { 0.0f, static_cast (port.index) }; case e::east: return { static_cast (-1.0f - port.index), static_cast (block->getWidth()) }; case e::south: return { static_cast (0.0f - block->getHeight()), static_cast (port.index) }; case e::west: return { static_cast (-1.0f - port.index), 0.0f }; } } case 180: { switch (port.edge) { case e::north: return { static_cast (-1.0f - port.index), 0.0f }; case e::east: return { static_cast (0.0f - block->getWidth()), static_cast (-1.0f - port.index) }; case e::south: return { static_cast (-1.0f - port.index), static_cast (0.0f - block->getHeight()) }; case e::west: return { 0.0f, static_cast (-1.0f - port.index) }; } } case 270: { switch (port.edge) { case e::north: return { 0.0f, static_cast (-1.0f - port.index) }; case e::east: return { static_cast (port.index), static_cast (0 - block->getWidth()) }; case e::south: return { static_cast (block->getHeight()), static_cast (-1.0f - port.index) }; case e::west: return { static_cast (port.index), 0.0f }; } } } return Point(); } int rotation = 0; Point topLeft = { 0.0f, 0.0f }; 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); } /** Overridden from MouseListener. Prepares the master Block component for dragging. */ void mouseDown (const MouseEvent& e) override { if (block->isMasterBlock()) componentDragger.startDraggingComponent (this, e); } /** Overridden from MouseListener. Drags the master Block component */ void mouseDrag (const MouseEvent& e) override { if (block->isMasterBlock()) { componentDragger.dragComponent (this, e, &constrainer); getParentComponent()->resized(); } } ComponentDragger componentDragger; ComponentBoundsConstrainer constrainer; 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 Point ratio (r.getWidth() / block->getWidth(), r.getHeight() / block->getHeight()); float maxCircleSize = block->getWidth() / 3.0f; // iterate over the list of current touches and draw them on the onscreen Block for (auto touch : touches) { float circleSize = touch.touch.z * maxCircleSize; Point touchPosition (touch.touch.x, touch.touch.y); auto blob = Rectangle (circleSize, circleSize) .withCentre (touchPosition) * ratio; ColourGradient cg (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 auto numLedsToTurnOn = static_cast (numLeds * block->getBatteryLevel()); // add LEDs for (int i = 0; i < numLeds; ++i) { auto 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 (auto* led : leds) { led->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 { 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 (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) {} /** 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 (auto* sttc = dynamic_cast (comp)) return sttc->getTooltip(); return {}; } //============================================================================== Colour componentColour; bool onState = false; //============================================================================== 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 { 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]) { auto semiButtonWidth = r.getWidth() / 2.0f; 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) };