1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-10 23:44:24 +00:00
JUCE/examples/BLOCKS/BlocksMonitor/Source/BlockComponents.h

547 lines
20 KiB
C++

/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2017 - ROLI Ltd.
JUCE is an open source library subject to commercial or open-source
licensing.
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
27th April 2017).
End User License Agreement: www.juce.com/juce-5-licence
Privacy Policy: www.juce.com/juce-5-privacy-policy
Or: You may also use this code under the terms of the GPL v3 (see
www.gnu.org/licenses).
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
#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())
block->setProgram (new BitmapLEDProgram (*block));
// 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<ControlButton::ButtonFunction> 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<float> getOffsetForPort (Block::ConnectionPort port)
{
using e = Block::ConnectionPort::DeviceEdge;
switch (rotation)
{
case 0:
{
switch (port.edge)
{
case e::north:
return { static_cast<float> (port.index), 0.0f };
case e::east:
return { static_cast<float> (block->getWidth()), static_cast<float> (port.index) };
case e::south:
return { static_cast<float> (port.index), static_cast<float> (block->getHeight()) };
case e::west:
return { 0.0f, static_cast<float> (port.index) };
}
}
case 90:
{
switch (port.edge)
{
case e::north:
return { 0.0f, static_cast<float> (port.index) };
case e::east:
return { static_cast<float> (-1.0f - port.index), static_cast<float> (block->getWidth()) };
case e::south:
return { static_cast<float> (0.0f - block->getHeight()), static_cast<float> (port.index) };
case e::west:
return { static_cast<float> (-1.0f - port.index), 0.0f };
}
}
case 180:
{
switch (port.edge)
{
case e::north:
return { static_cast<float> (-1.0f - port.index), 0.0f };
case e::east:
return { static_cast<float> (0.0f - block->getWidth()), static_cast<float> (-1.0f - port.index) };
case e::south:
return { static_cast<float> (-1.0f - port.index), static_cast<float> (0.0f - block->getHeight()) };
case e::west:
return { 0.0f, static_cast<float> (-1.0f - port.index) };
}
}
case 270:
{
switch (port.edge)
{
case e::north:
return { 0.0f, static_cast<float> (-1.0f - port.index) };
case e::east:
return { static_cast<float> (port.index), static_cast<float> (0 - block->getWidth()) };
case e::south:
return { static_cast<float> (block->getHeight()), static_cast<float> (-1.0f - port.index) };
case e::west:
return { static_cast<float> (port.index), 0.0f };
}
}
}
return Point<float>();
}
int rotation = 0;
Point<float> 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<float> 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<float> touchPosition (touch.touch.x,
touch.touch.y);
auto blob = Rectangle<float> (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<Colour> colourArray = { Colours::red,
Colours::blue,
Colours::green,
Colours::yellow,
Colours::white,
Colours::hotpink,
Colours::mediumpurple };
/** A list of current Touch events */
TouchList<TouchSurface::Touch> 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<int> (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<int> (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<SettableTooltipClient*> (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<LEDComponent> leds;
CircleButtonComponent circleButtons[8];
RoundedRectangleButtonComponent roundedRectangleButton;
int previousNumLedsOn;
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ControlBlockComponent)
};