919 lines
28 KiB
C++
919 lines
28 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.
|
|
|
|
==============================================================================
|
|
*/
|
|
|
|
namespace juce
|
|
{
|
|
|
|
static const uint8 whiteNotes[] = { 0, 2, 4, 5, 7, 9, 11 };
|
|
static const uint8 blackNotes[] = { 1, 3, 6, 8, 10 };
|
|
|
|
|
|
struct MidiKeyboardComponent::UpDownButton : public Button
|
|
{
|
|
UpDownButton (MidiKeyboardComponent& c, int d)
|
|
: Button ({}), owner (c), delta (d)
|
|
{
|
|
}
|
|
|
|
void clicked() override
|
|
{
|
|
auto note = owner.getLowestVisibleKey();
|
|
|
|
if (delta < 0)
|
|
note = (note - 1) / 12;
|
|
else
|
|
note = note / 12 + 1;
|
|
|
|
owner.setLowestVisibleKey (note * 12);
|
|
}
|
|
|
|
void paintButton (Graphics& g, bool shouldDrawButtonAsHighlighted, bool shouldDrawButtonAsDown) override
|
|
{
|
|
owner.drawUpDownButton (g, getWidth(), getHeight(),
|
|
shouldDrawButtonAsHighlighted, shouldDrawButtonAsDown,
|
|
delta > 0);
|
|
}
|
|
|
|
private:
|
|
MidiKeyboardComponent& owner;
|
|
const int delta;
|
|
|
|
JUCE_DECLARE_NON_COPYABLE (UpDownButton)
|
|
};
|
|
|
|
//==============================================================================
|
|
MidiKeyboardComponent::MidiKeyboardComponent (MidiKeyboardState& s, Orientation o)
|
|
: state (s), orientation (o)
|
|
{
|
|
scrollDown.reset (new UpDownButton (*this, -1));
|
|
scrollUp .reset (new UpDownButton (*this, 1));
|
|
|
|
addChildComponent (scrollDown.get());
|
|
addChildComponent (scrollUp.get());
|
|
|
|
// initialise with a default set of qwerty key-mappings..
|
|
int note = 0;
|
|
|
|
for (char c : "awsedftgyhujkolp;")
|
|
setKeyPressForNote (KeyPress (c, 0, 0), note++);
|
|
|
|
mouseOverNotes.insertMultiple (0, -1, 32);
|
|
mouseDownNotes.insertMultiple (0, -1, 32);
|
|
|
|
colourChanged();
|
|
setWantsKeyboardFocus (true);
|
|
|
|
state.addListener (this);
|
|
|
|
startTimerHz (20);
|
|
}
|
|
|
|
MidiKeyboardComponent::~MidiKeyboardComponent()
|
|
{
|
|
state.removeListener (this);
|
|
}
|
|
|
|
//==============================================================================
|
|
void MidiKeyboardComponent::setKeyWidth (float widthInPixels)
|
|
{
|
|
jassert (widthInPixels > 0);
|
|
|
|
if (keyWidth != widthInPixels) // Prevent infinite recursion if the width is being computed in a 'resized()' call-back
|
|
{
|
|
keyWidth = widthInPixels;
|
|
resized();
|
|
}
|
|
}
|
|
|
|
void MidiKeyboardComponent::setScrollButtonWidth (int widthInPixels)
|
|
{
|
|
jassert (widthInPixels > 0);
|
|
|
|
if (scrollButtonWidth != widthInPixels)
|
|
{
|
|
scrollButtonWidth = widthInPixels;
|
|
resized();
|
|
}
|
|
}
|
|
|
|
void MidiKeyboardComponent::setOrientation (Orientation newOrientation)
|
|
{
|
|
if (orientation != newOrientation)
|
|
{
|
|
orientation = newOrientation;
|
|
resized();
|
|
}
|
|
}
|
|
|
|
void MidiKeyboardComponent::setAvailableRange (int lowestNote, int highestNote)
|
|
{
|
|
jassert (lowestNote >= 0 && lowestNote <= 127);
|
|
jassert (highestNote >= 0 && highestNote <= 127);
|
|
jassert (lowestNote <= highestNote);
|
|
|
|
if (rangeStart != lowestNote || rangeEnd != highestNote)
|
|
{
|
|
rangeStart = jlimit (0, 127, lowestNote);
|
|
rangeEnd = jlimit (0, 127, highestNote);
|
|
firstKey = jlimit ((float) rangeStart, (float) rangeEnd, firstKey);
|
|
resized();
|
|
}
|
|
}
|
|
|
|
void MidiKeyboardComponent::setLowestVisibleKey (int noteNumber)
|
|
{
|
|
setLowestVisibleKeyFloat ((float) noteNumber);
|
|
}
|
|
|
|
void MidiKeyboardComponent::setLowestVisibleKeyFloat (float noteNumber)
|
|
{
|
|
noteNumber = jlimit ((float) rangeStart, (float) rangeEnd, noteNumber);
|
|
|
|
if (noteNumber != firstKey)
|
|
{
|
|
bool hasMoved = (((int) firstKey) != (int) noteNumber);
|
|
firstKey = noteNumber;
|
|
|
|
if (hasMoved)
|
|
sendChangeMessage();
|
|
|
|
resized();
|
|
}
|
|
}
|
|
|
|
void MidiKeyboardComponent::setScrollButtonsVisible (bool newCanScroll)
|
|
{
|
|
if (canScroll != newCanScroll)
|
|
{
|
|
canScroll = newCanScroll;
|
|
resized();
|
|
}
|
|
}
|
|
|
|
void MidiKeyboardComponent::colourChanged()
|
|
{
|
|
setOpaque (findColour (whiteNoteColourId).isOpaque());
|
|
repaint();
|
|
}
|
|
|
|
//==============================================================================
|
|
void MidiKeyboardComponent::setMidiChannel (int midiChannelNumber)
|
|
{
|
|
jassert (midiChannelNumber > 0 && midiChannelNumber <= 16);
|
|
|
|
if (midiChannel != midiChannelNumber)
|
|
{
|
|
resetAnyKeysInUse();
|
|
midiChannel = jlimit (1, 16, midiChannelNumber);
|
|
}
|
|
}
|
|
|
|
void MidiKeyboardComponent::setMidiChannelsToDisplay (int midiChannelMask)
|
|
{
|
|
midiInChannelMask = midiChannelMask;
|
|
shouldCheckState = true;
|
|
}
|
|
|
|
void MidiKeyboardComponent::setVelocity (float v, bool useMousePosition)
|
|
{
|
|
velocity = jlimit (0.0f, 1.0f, v);
|
|
useMousePositionForVelocity = useMousePosition;
|
|
}
|
|
|
|
//==============================================================================
|
|
Range<float> MidiKeyboardComponent::getKeyPosition (int midiNoteNumber, float targetKeyWidth) const
|
|
{
|
|
jassert (midiNoteNumber >= 0 && midiNoteNumber < 128);
|
|
|
|
static const float notePos[] = { 0.0f, 1 - blackNoteWidthRatio * 0.6f,
|
|
1.0f, 2 - blackNoteWidthRatio * 0.4f,
|
|
2.0f,
|
|
3.0f, 4 - blackNoteWidthRatio * 0.7f,
|
|
4.0f, 5 - blackNoteWidthRatio * 0.5f,
|
|
5.0f, 6 - blackNoteWidthRatio * 0.3f,
|
|
6.0f };
|
|
|
|
auto octave = midiNoteNumber / 12;
|
|
auto note = midiNoteNumber % 12;
|
|
|
|
auto start = octave * 7.0f * targetKeyWidth + notePos[note] * targetKeyWidth;
|
|
auto width = MidiMessage::isMidiNoteBlack (note) ? blackNoteWidthRatio * targetKeyWidth : targetKeyWidth;
|
|
|
|
return { start, start + width };
|
|
}
|
|
|
|
Range<float> MidiKeyboardComponent::getKeyPos (int midiNoteNumber) const
|
|
{
|
|
return getKeyPosition (midiNoteNumber, keyWidth)
|
|
- xOffset
|
|
- getKeyPosition (rangeStart, keyWidth).getStart();
|
|
}
|
|
|
|
Rectangle<float> MidiKeyboardComponent::getRectangleForKey (int note) const
|
|
{
|
|
jassert (note >= rangeStart && note <= rangeEnd);
|
|
|
|
auto pos = getKeyPos (note);
|
|
auto x = pos.getStart();
|
|
auto w = pos.getLength();
|
|
|
|
if (MidiMessage::isMidiNoteBlack (note))
|
|
{
|
|
auto blackNoteLength = getBlackNoteLength();
|
|
|
|
switch (orientation)
|
|
{
|
|
case horizontalKeyboard: return { x, 0, w, blackNoteLength };
|
|
case verticalKeyboardFacingLeft: return { getWidth() - blackNoteLength, x, blackNoteLength, w };
|
|
case verticalKeyboardFacingRight: return { 0, getHeight() - x - w, blackNoteLength, w };
|
|
default: jassertfalse; break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
switch (orientation)
|
|
{
|
|
case horizontalKeyboard: return { x, 0, w, (float) getHeight() };
|
|
case verticalKeyboardFacingLeft: return { 0, x, (float) getWidth(), w };
|
|
case verticalKeyboardFacingRight: return { 0, getHeight() - x - w, (float) getWidth(), w };
|
|
default: jassertfalse; break;
|
|
}
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
float MidiKeyboardComponent::getKeyStartPosition (int midiNoteNumber) const
|
|
{
|
|
return getKeyPos (midiNoteNumber).getStart();
|
|
}
|
|
|
|
float MidiKeyboardComponent::getTotalKeyboardWidth() const noexcept
|
|
{
|
|
return getKeyPos (rangeEnd).getEnd();
|
|
}
|
|
|
|
int MidiKeyboardComponent::getNoteAtPosition (Point<float> p)
|
|
{
|
|
float v;
|
|
return xyToNote (p, v);
|
|
}
|
|
|
|
int MidiKeyboardComponent::xyToNote (Point<float> pos, float& mousePositionVelocity)
|
|
{
|
|
if (! reallyContains (pos.toInt(), false))
|
|
return -1;
|
|
|
|
auto p = pos;
|
|
|
|
if (orientation != horizontalKeyboard)
|
|
{
|
|
p = { p.y, p.x };
|
|
|
|
if (orientation == verticalKeyboardFacingLeft)
|
|
p = { p.x, getWidth() - p.y };
|
|
else
|
|
p = { getHeight() - p.x, p.y };
|
|
}
|
|
|
|
return remappedXYToNote (p + Point<float> (xOffset, 0), mousePositionVelocity);
|
|
}
|
|
|
|
int MidiKeyboardComponent::remappedXYToNote (Point<float> pos, float& mousePositionVelocity) const
|
|
{
|
|
auto blackNoteLength = getBlackNoteLength();
|
|
|
|
if (pos.getY() < blackNoteLength)
|
|
{
|
|
for (int octaveStart = 12 * (rangeStart / 12); octaveStart <= rangeEnd; octaveStart += 12)
|
|
{
|
|
for (int i = 0; i < 5; ++i)
|
|
{
|
|
auto note = octaveStart + blackNotes[i];
|
|
|
|
if (note >= rangeStart && note <= rangeEnd)
|
|
{
|
|
if (getKeyPos (note).contains (pos.x - xOffset))
|
|
{
|
|
mousePositionVelocity = jmax (0.0f, pos.y / blackNoteLength);
|
|
return note;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int octaveStart = 12 * (rangeStart / 12); octaveStart <= rangeEnd; octaveStart += 12)
|
|
{
|
|
for (int i = 0; i < 7; ++i)
|
|
{
|
|
auto note = octaveStart + whiteNotes[i];
|
|
|
|
if (note >= rangeStart && note <= rangeEnd)
|
|
{
|
|
if (getKeyPos (note).contains (pos.x - xOffset))
|
|
{
|
|
auto whiteNoteLength = (orientation == horizontalKeyboard) ? getHeight() : getWidth();
|
|
mousePositionVelocity = jmax (0.0f, pos.y / (float) whiteNoteLength);
|
|
return note;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
mousePositionVelocity = 0;
|
|
return -1;
|
|
}
|
|
|
|
//==============================================================================
|
|
void MidiKeyboardComponent::repaintNote (int noteNum)
|
|
{
|
|
if (noteNum >= rangeStart && noteNum <= rangeEnd)
|
|
repaint (getRectangleForKey (noteNum).getSmallestIntegerContainer());
|
|
}
|
|
|
|
void MidiKeyboardComponent::paint (Graphics& g)
|
|
{
|
|
g.fillAll (findColour (whiteNoteColourId));
|
|
|
|
auto lineColour = findColour (keySeparatorLineColourId);
|
|
auto textColour = findColour (textLabelColourId);
|
|
|
|
for (int octave = 0; octave < 128; octave += 12)
|
|
{
|
|
for (int white = 0; white < 7; ++white)
|
|
{
|
|
auto noteNum = octave + whiteNotes[white];
|
|
|
|
if (noteNum >= rangeStart && noteNum <= rangeEnd)
|
|
drawWhiteNote (noteNum, g, getRectangleForKey (noteNum),
|
|
state.isNoteOnForChannels (midiInChannelMask, noteNum),
|
|
mouseOverNotes.contains (noteNum), lineColour, textColour);
|
|
}
|
|
}
|
|
|
|
float x1 = 0.0f, y1 = 0.0f, x2 = 0.0f, y2 = 0.0f;
|
|
auto width = getWidth();
|
|
auto height = getHeight();
|
|
|
|
if (orientation == verticalKeyboardFacingLeft)
|
|
{
|
|
x1 = width - 1.0f;
|
|
x2 = width - 5.0f;
|
|
}
|
|
else if (orientation == verticalKeyboardFacingRight)
|
|
x2 = 5.0f;
|
|
else
|
|
y2 = 5.0f;
|
|
|
|
auto x = getKeyPos (rangeEnd).getEnd();
|
|
auto shadowCol = findColour (shadowColourId);
|
|
|
|
if (! shadowCol.isTransparent())
|
|
{
|
|
g.setGradientFill (ColourGradient (shadowCol, x1, y1, shadowCol.withAlpha (0.0f), x2, y2, false));
|
|
|
|
switch (orientation)
|
|
{
|
|
case horizontalKeyboard: g.fillRect (0.0f, 0.0f, x, 5.0f); break;
|
|
case verticalKeyboardFacingLeft: g.fillRect (width - 5.0f, 0.0f, 5.0f, x); break;
|
|
case verticalKeyboardFacingRight: g.fillRect (0.0f, 0.0f, 5.0f, x); break;
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
if (! lineColour.isTransparent())
|
|
{
|
|
g.setColour (lineColour);
|
|
|
|
switch (orientation)
|
|
{
|
|
case horizontalKeyboard: g.fillRect (0.0f, height - 1.0f, x, 1.0f); break;
|
|
case verticalKeyboardFacingLeft: g.fillRect (0.0f, 0.0f, 1.0f, x); break;
|
|
case verticalKeyboardFacingRight: g.fillRect (width - 1.0f, 0.0f, 1.0f, x); break;
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
auto blackNoteColour = findColour (blackNoteColourId);
|
|
|
|
for (int octave = 0; octave < 128; octave += 12)
|
|
{
|
|
for (int black = 0; black < 5; ++black)
|
|
{
|
|
auto noteNum = octave + blackNotes[black];
|
|
|
|
if (noteNum >= rangeStart && noteNum <= rangeEnd)
|
|
drawBlackNote (noteNum, g, getRectangleForKey (noteNum),
|
|
state.isNoteOnForChannels (midiInChannelMask, noteNum),
|
|
mouseOverNotes.contains (noteNum), blackNoteColour);
|
|
}
|
|
}
|
|
}
|
|
|
|
void MidiKeyboardComponent::drawWhiteNote (int midiNoteNumber, Graphics& g, Rectangle<float> area,
|
|
bool isDown, bool isOver, Colour lineColour, Colour textColour)
|
|
{
|
|
auto c = Colours::transparentWhite;
|
|
|
|
if (isDown) c = findColour (keyDownOverlayColourId);
|
|
if (isOver) c = c.overlaidWith (findColour (mouseOverKeyOverlayColourId));
|
|
|
|
g.setColour (c);
|
|
g.fillRect (area);
|
|
|
|
auto text = getWhiteNoteText (midiNoteNumber);
|
|
|
|
if (text.isNotEmpty())
|
|
{
|
|
auto fontHeight = jmin (12.0f, keyWidth * 0.9f);
|
|
|
|
g.setColour (textColour);
|
|
g.setFont (Font (fontHeight).withHorizontalScale (0.8f));
|
|
|
|
switch (orientation)
|
|
{
|
|
case horizontalKeyboard: g.drawText (text, area.withTrimmedLeft (1.0f).withTrimmedBottom (2.0f), Justification::centredBottom, false); break;
|
|
case verticalKeyboardFacingLeft: g.drawText (text, area.reduced (2.0f), Justification::centredLeft, false); break;
|
|
case verticalKeyboardFacingRight: g.drawText (text, area.reduced (2.0f), Justification::centredRight, false); break;
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
if (! lineColour.isTransparent())
|
|
{
|
|
g.setColour (lineColour);
|
|
|
|
switch (orientation)
|
|
{
|
|
case horizontalKeyboard: g.fillRect (area.withWidth (1.0f)); break;
|
|
case verticalKeyboardFacingLeft: g.fillRect (area.withHeight (1.0f)); break;
|
|
case verticalKeyboardFacingRight: g.fillRect (area.removeFromBottom (1.0f)); break;
|
|
default: break;
|
|
}
|
|
|
|
if (midiNoteNumber == rangeEnd)
|
|
{
|
|
switch (orientation)
|
|
{
|
|
case horizontalKeyboard: g.fillRect (area.expanded (1.0f, 0).removeFromRight (1.0f)); break;
|
|
case verticalKeyboardFacingLeft: g.fillRect (area.expanded (0, 1.0f).removeFromBottom (1.0f)); break;
|
|
case verticalKeyboardFacingRight: g.fillRect (area.expanded (0, 1.0f).removeFromTop (1.0f)); break;
|
|
default: break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void MidiKeyboardComponent::drawBlackNote (int /*midiNoteNumber*/, Graphics& g, Rectangle<float> area,
|
|
bool isDown, bool isOver, Colour noteFillColour)
|
|
{
|
|
auto c = noteFillColour;
|
|
|
|
if (isDown) c = c.overlaidWith (findColour (keyDownOverlayColourId));
|
|
if (isOver) c = c.overlaidWith (findColour (mouseOverKeyOverlayColourId));
|
|
|
|
g.setColour (c);
|
|
g.fillRect (area);
|
|
|
|
if (isDown)
|
|
{
|
|
g.setColour (noteFillColour);
|
|
g.drawRect (area);
|
|
}
|
|
else
|
|
{
|
|
g.setColour (c.brighter());
|
|
auto sideIndent = 1.0f / 8.0f;
|
|
auto topIndent = 7.0f / 8.0f;
|
|
auto w = area.getWidth();
|
|
auto h = area.getHeight();
|
|
|
|
switch (orientation)
|
|
{
|
|
case horizontalKeyboard: g.fillRect (area.reduced (w * sideIndent, 0).removeFromTop (h * topIndent)); break;
|
|
case verticalKeyboardFacingLeft: g.fillRect (area.reduced (0, h * sideIndent).removeFromRight (w * topIndent)); break;
|
|
case verticalKeyboardFacingRight: g.fillRect (area.reduced (0, h * sideIndent).removeFromLeft (w * topIndent)); break;
|
|
default: break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void MidiKeyboardComponent::setOctaveForMiddleC (int octaveNum)
|
|
{
|
|
octaveNumForMiddleC = octaveNum;
|
|
repaint();
|
|
}
|
|
|
|
String MidiKeyboardComponent::getWhiteNoteText (int midiNoteNumber)
|
|
{
|
|
if (midiNoteNumber % 12 == 0)
|
|
return MidiMessage::getMidiNoteName (midiNoteNumber, true, true, octaveNumForMiddleC);
|
|
|
|
return {};
|
|
}
|
|
|
|
void MidiKeyboardComponent::drawUpDownButton (Graphics& g, int w, int h,
|
|
bool mouseOver,
|
|
bool buttonDown,
|
|
bool movesOctavesUp)
|
|
{
|
|
g.fillAll (findColour (upDownButtonBackgroundColourId));
|
|
|
|
float angle = 0;
|
|
|
|
switch (orientation)
|
|
{
|
|
case horizontalKeyboard: angle = movesOctavesUp ? 0.0f : 0.5f; break;
|
|
case verticalKeyboardFacingLeft: angle = movesOctavesUp ? 0.25f : 0.75f; break;
|
|
case verticalKeyboardFacingRight: angle = movesOctavesUp ? 0.75f : 0.25f; break;
|
|
default: jassertfalse; break;
|
|
}
|
|
|
|
Path path;
|
|
path.addTriangle (0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.5f);
|
|
path.applyTransform (AffineTransform::rotation (MathConstants<float>::twoPi * angle, 0.5f, 0.5f));
|
|
|
|
g.setColour (findColour (upDownButtonArrowColourId)
|
|
.withAlpha (buttonDown ? 1.0f : (mouseOver ? 0.6f : 0.4f)));
|
|
|
|
g.fillPath (path, path.getTransformToScaleToFit (1.0f, 1.0f, w - 2.0f, h - 2.0f, true));
|
|
}
|
|
|
|
void MidiKeyboardComponent::setBlackNoteLengthProportion (float ratio) noexcept
|
|
{
|
|
jassert (ratio >= 0.0f && ratio <= 1.0f);
|
|
|
|
if (blackNoteLengthRatio != ratio)
|
|
{
|
|
blackNoteLengthRatio = ratio;
|
|
resized();
|
|
}
|
|
}
|
|
|
|
float MidiKeyboardComponent::getBlackNoteLength() const noexcept
|
|
{
|
|
auto whiteNoteLength = orientation == horizontalKeyboard ? getHeight() : getWidth();
|
|
return whiteNoteLength * blackNoteLengthRatio;
|
|
}
|
|
|
|
void MidiKeyboardComponent::setBlackNoteWidthProportion (float ratio) noexcept
|
|
{
|
|
jassert (ratio >= 0.0f && ratio <= 1.0f);
|
|
|
|
if (blackNoteWidthRatio != ratio)
|
|
{
|
|
blackNoteWidthRatio = ratio;
|
|
resized();
|
|
}
|
|
}
|
|
|
|
void MidiKeyboardComponent::resized()
|
|
{
|
|
auto w = getWidth();
|
|
auto h = getHeight();
|
|
|
|
if (w > 0 && h > 0)
|
|
{
|
|
if (orientation != horizontalKeyboard)
|
|
std::swap (w, h);
|
|
|
|
auto kx2 = getKeyPos (rangeEnd).getEnd();
|
|
|
|
if ((int) firstKey != rangeStart)
|
|
{
|
|
auto kx1 = getKeyPos (rangeStart).getStart();
|
|
|
|
if (kx2 - kx1 <= w)
|
|
{
|
|
firstKey = (float) rangeStart;
|
|
sendChangeMessage();
|
|
repaint();
|
|
}
|
|
}
|
|
|
|
scrollDown->setVisible (canScroll && firstKey > (float) rangeStart);
|
|
|
|
xOffset = 0;
|
|
|
|
if (canScroll)
|
|
{
|
|
auto scrollButtonW = jmin (scrollButtonWidth, w / 2);
|
|
auto r = getLocalBounds();
|
|
|
|
if (orientation == horizontalKeyboard)
|
|
{
|
|
scrollDown->setBounds (r.removeFromLeft (scrollButtonW));
|
|
scrollUp ->setBounds (r.removeFromRight (scrollButtonW));
|
|
}
|
|
else if (orientation == verticalKeyboardFacingLeft)
|
|
{
|
|
scrollDown->setBounds (r.removeFromTop (scrollButtonW));
|
|
scrollUp ->setBounds (r.removeFromBottom (scrollButtonW));
|
|
}
|
|
else
|
|
{
|
|
scrollDown->setBounds (r.removeFromBottom (scrollButtonW));
|
|
scrollUp ->setBounds (r.removeFromTop (scrollButtonW));
|
|
}
|
|
|
|
auto endOfLastKey = getKeyPos (rangeEnd).getEnd();
|
|
|
|
float mousePositionVelocity;
|
|
auto spaceAvailable = w;
|
|
auto lastStartKey = remappedXYToNote ({ endOfLastKey - spaceAvailable, 0 }, mousePositionVelocity) + 1;
|
|
|
|
if (lastStartKey >= 0 && ((int) firstKey) > lastStartKey)
|
|
{
|
|
firstKey = (float) jlimit (rangeStart, rangeEnd, lastStartKey);
|
|
sendChangeMessage();
|
|
}
|
|
|
|
xOffset = getKeyPos ((int) firstKey).getStart();
|
|
}
|
|
else
|
|
{
|
|
firstKey = (float) rangeStart;
|
|
}
|
|
|
|
scrollUp->setVisible (canScroll && getKeyPos (rangeEnd).getStart() > w);
|
|
repaint();
|
|
}
|
|
}
|
|
|
|
//==============================================================================
|
|
void MidiKeyboardComponent::handleNoteOn (MidiKeyboardState*, int /*midiChannel*/, int /*midiNoteNumber*/, float /*velocity*/)
|
|
{
|
|
shouldCheckState = true; // (probably being called from the audio thread, so avoid blocking in here)
|
|
}
|
|
|
|
void MidiKeyboardComponent::handleNoteOff (MidiKeyboardState*, int /*midiChannel*/, int /*midiNoteNumber*/, float /*velocity*/)
|
|
{
|
|
shouldCheckState = true; // (probably being called from the audio thread, so avoid blocking in here)
|
|
}
|
|
|
|
//==============================================================================
|
|
void MidiKeyboardComponent::resetAnyKeysInUse()
|
|
{
|
|
if (! keysPressed.isZero())
|
|
{
|
|
for (int i = 128; --i >= 0;)
|
|
if (keysPressed[i])
|
|
state.noteOff (midiChannel, i, 0.0f);
|
|
|
|
keysPressed.clear();
|
|
}
|
|
|
|
for (int i = mouseDownNotes.size(); --i >= 0;)
|
|
{
|
|
auto noteDown = mouseDownNotes.getUnchecked(i);
|
|
|
|
if (noteDown >= 0)
|
|
{
|
|
state.noteOff (midiChannel, noteDown, 0.0f);
|
|
mouseDownNotes.set (i, -1);
|
|
}
|
|
|
|
mouseOverNotes.set (i, -1);
|
|
}
|
|
}
|
|
|
|
void MidiKeyboardComponent::updateNoteUnderMouse (const MouseEvent& e, bool isDown)
|
|
{
|
|
updateNoteUnderMouse (e.getEventRelativeTo (this).position, isDown, e.source.getIndex());
|
|
}
|
|
|
|
void MidiKeyboardComponent::updateNoteUnderMouse (Point<float> pos, bool isDown, int fingerNum)
|
|
{
|
|
float mousePositionVelocity = 0.0f;
|
|
auto newNote = xyToNote (pos, mousePositionVelocity);
|
|
auto oldNote = mouseOverNotes.getUnchecked (fingerNum);
|
|
auto oldNoteDown = mouseDownNotes.getUnchecked (fingerNum);
|
|
auto eventVelocity = useMousePositionForVelocity ? mousePositionVelocity * velocity : velocity;
|
|
|
|
if (oldNote != newNote)
|
|
{
|
|
repaintNote (oldNote);
|
|
repaintNote (newNote);
|
|
mouseOverNotes.set (fingerNum, newNote);
|
|
}
|
|
|
|
if (isDown)
|
|
{
|
|
if (newNote != oldNoteDown)
|
|
{
|
|
if (oldNoteDown >= 0)
|
|
{
|
|
mouseDownNotes.set (fingerNum, -1);
|
|
|
|
if (! mouseDownNotes.contains (oldNoteDown))
|
|
state.noteOff (midiChannel, oldNoteDown, eventVelocity);
|
|
}
|
|
|
|
if (newNote >= 0 && ! mouseDownNotes.contains (newNote))
|
|
{
|
|
state.noteOn (midiChannel, newNote, eventVelocity);
|
|
mouseDownNotes.set (fingerNum, newNote);
|
|
}
|
|
}
|
|
}
|
|
else if (oldNoteDown >= 0)
|
|
{
|
|
mouseDownNotes.set (fingerNum, -1);
|
|
|
|
if (! mouseDownNotes.contains (oldNoteDown))
|
|
state.noteOff (midiChannel, oldNoteDown, eventVelocity);
|
|
}
|
|
}
|
|
|
|
void MidiKeyboardComponent::mouseMove (const MouseEvent& e)
|
|
{
|
|
updateNoteUnderMouse (e, false);
|
|
shouldCheckMousePos = false;
|
|
}
|
|
|
|
void MidiKeyboardComponent::mouseDrag (const MouseEvent& e)
|
|
{
|
|
float mousePositionVelocity;
|
|
auto newNote = xyToNote (e.position, mousePositionVelocity);
|
|
|
|
if (newNote >= 0)
|
|
mouseDraggedToKey (newNote, e);
|
|
|
|
updateNoteUnderMouse (e, true);
|
|
}
|
|
|
|
bool MidiKeyboardComponent::mouseDownOnKey (int, const MouseEvent&) { return true; }
|
|
void MidiKeyboardComponent::mouseDraggedToKey (int, const MouseEvent&) {}
|
|
void MidiKeyboardComponent::mouseUpOnKey (int, const MouseEvent&) {}
|
|
|
|
void MidiKeyboardComponent::mouseDown (const MouseEvent& e)
|
|
{
|
|
float mousePositionVelocity;
|
|
auto newNote = xyToNote (e.position, mousePositionVelocity);
|
|
|
|
if (newNote >= 0 && mouseDownOnKey (newNote, e))
|
|
{
|
|
updateNoteUnderMouse (e, true);
|
|
shouldCheckMousePos = true;
|
|
}
|
|
}
|
|
|
|
void MidiKeyboardComponent::mouseUp (const MouseEvent& e)
|
|
{
|
|
updateNoteUnderMouse (e, false);
|
|
shouldCheckMousePos = false;
|
|
|
|
float mousePositionVelocity;
|
|
auto note = xyToNote (e.position, mousePositionVelocity);
|
|
|
|
if (note >= 0)
|
|
mouseUpOnKey (note, e);
|
|
}
|
|
|
|
void MidiKeyboardComponent::mouseEnter (const MouseEvent& e)
|
|
{
|
|
updateNoteUnderMouse (e, false);
|
|
}
|
|
|
|
void MidiKeyboardComponent::mouseExit (const MouseEvent& e)
|
|
{
|
|
updateNoteUnderMouse (e, false);
|
|
}
|
|
|
|
void MidiKeyboardComponent::mouseWheelMove (const MouseEvent&, const MouseWheelDetails& wheel)
|
|
{
|
|
auto amount = (orientation == horizontalKeyboard && wheel.deltaX != 0)
|
|
? wheel.deltaX : (orientation == verticalKeyboardFacingLeft ? wheel.deltaY
|
|
: -wheel.deltaY);
|
|
|
|
setLowestVisibleKeyFloat (firstKey - amount * keyWidth);
|
|
}
|
|
|
|
void MidiKeyboardComponent::timerCallback()
|
|
{
|
|
if (shouldCheckState)
|
|
{
|
|
shouldCheckState = false;
|
|
|
|
for (int i = rangeStart; i <= rangeEnd; ++i)
|
|
{
|
|
bool isOn = state.isNoteOnForChannels (midiInChannelMask, i);
|
|
|
|
if (keysCurrentlyDrawnDown[i] != isOn)
|
|
{
|
|
keysCurrentlyDrawnDown.setBit (i, isOn);
|
|
repaintNote (i);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (shouldCheckMousePos)
|
|
{
|
|
for (auto& ms : Desktop::getInstance().getMouseSources())
|
|
if (ms.getComponentUnderMouse() == this || isParentOf (ms.getComponentUnderMouse()))
|
|
updateNoteUnderMouse (getLocalPoint (nullptr, ms.getScreenPosition()), ms.isDragging(), ms.getIndex());
|
|
}
|
|
}
|
|
|
|
//==============================================================================
|
|
void MidiKeyboardComponent::clearKeyMappings()
|
|
{
|
|
resetAnyKeysInUse();
|
|
keyPressNotes.clear();
|
|
keyPresses.clear();
|
|
}
|
|
|
|
void MidiKeyboardComponent::setKeyPressForNote (const KeyPress& key, int midiNoteOffsetFromC)
|
|
{
|
|
removeKeyPressForNote (midiNoteOffsetFromC);
|
|
|
|
keyPressNotes.add (midiNoteOffsetFromC);
|
|
keyPresses.add (key);
|
|
}
|
|
|
|
void MidiKeyboardComponent::removeKeyPressForNote (int midiNoteOffsetFromC)
|
|
{
|
|
for (int i = keyPressNotes.size(); --i >= 0;)
|
|
{
|
|
if (keyPressNotes.getUnchecked (i) == midiNoteOffsetFromC)
|
|
{
|
|
keyPressNotes.remove (i);
|
|
keyPresses.remove (i);
|
|
}
|
|
}
|
|
}
|
|
|
|
void MidiKeyboardComponent::setKeyPressBaseOctave (int newOctaveNumber)
|
|
{
|
|
jassert (newOctaveNumber >= 0 && newOctaveNumber <= 10);
|
|
|
|
keyMappingOctave = newOctaveNumber;
|
|
}
|
|
|
|
bool MidiKeyboardComponent::keyStateChanged (bool /*isKeyDown*/)
|
|
{
|
|
bool keyPressUsed = false;
|
|
|
|
for (int i = keyPresses.size(); --i >= 0;)
|
|
{
|
|
auto note = 12 * keyMappingOctave + keyPressNotes.getUnchecked (i);
|
|
|
|
if (keyPresses.getReference(i).isCurrentlyDown())
|
|
{
|
|
if (! keysPressed[note])
|
|
{
|
|
keysPressed.setBit (note);
|
|
state.noteOn (midiChannel, note, velocity);
|
|
keyPressUsed = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (keysPressed[note])
|
|
{
|
|
keysPressed.clearBit (note);
|
|
state.noteOff (midiChannel, note, 0.0f);
|
|
keyPressUsed = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return keyPressUsed;
|
|
}
|
|
|
|
bool MidiKeyboardComponent::keyPressed (const KeyPress& key)
|
|
{
|
|
return keyPresses.contains (key);
|
|
}
|
|
|
|
void MidiKeyboardComponent::focusLost (FocusChangeType)
|
|
{
|
|
resetAnyKeysInUse();
|
|
}
|
|
|
|
} // namespace juce
|