MidiKeyboardComponent - virtual functions

I would have imagined that MidiKeyboardComponent would have had more in the way of virtual functions? At the moment you can only customise the action for clicking on notes, but there appears to be no way of making your own response to the selection when dragging across the keys (short of processing the generated MIDI…)

Are there any plans to expand this?

Sure - if you think of any ways it could be tweaked to do what you need, just let me know.

ooh, here’s something it needs actually; i think it should not require a subclass to set the base octave for middle-c relating to the ‘c’ text; there should be a function that lets you set a value for this, taking exactly the same value as given to the MidiMessage::getMidiNoteName () function, allowing a global setting to be used in both places.

Good idea. I’ll pop that in there.

from overriding the getWhiteNoteText or whatever it’s called to achieve this myself, i found that i couldn’t do it in quite the same way as you had in the base; there’s no getKeyWidth() function :slight_smile:

And the main thing i’d like in there really is some kind of callback that’s called whenever a note is ‘selected’ by a drag event. At the moment I can only respond to the clicked notes, but dragging doesn’t call anything i can override.

Ok, Would this do the trick for you:

[code]/*

This file is part of the JUCE library - “Jules’ Utility Class Extensions”
Copyright 2004-6 by Raw Material Software ltd.


JUCE can be redistributed and/or modified under the terms of the
GNU General Public License, as published by the Free Software Foundation;
either version 2 of the License, or (at your option) any later version.

JUCE is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with JUCE; if not, visit Licenses - GNU Project - Free Software Foundation or write to the
Free Software Foundation, Inc., 59 Temple Place, Suite 330,
Boston, MA 02111-1307 USA


If you’d like to release a closed-source product which uses JUCE, commercial
licenses are also available: visit www.rawmaterialsoftware.com/juce for
more information.

==============================================================================
*/

BEGIN_JUCE_NAMESPACE

//==============================================================================
class MidiKeyboardUpDownButton : public Button
{
public:
MidiKeyboardUpDownButton (MidiKeyboardComponent* const owner_,
const int delta_)
: Button (String::empty),
owner (owner_),
delta (delta_)
{
setOpaque (true);
}

~MidiKeyboardUpDownButton()
{
}

void clicked()
{
    int note = owner->getLowestVisibleKey();

    if (delta < 0)
        note = (note - 1) / 12;
    else
        note = note / 12 + 1;

    owner->setLowestVisibleKey (note * 12);
}

void paintButton (Graphics& g,
                  bool isMouseOverButton,
                  bool isButtonDown)
{
    owner->drawUpDownButton (g, getWidth(), getHeight(),
                             isMouseOverButton, isButtonDown,
                             delta > 0);
}

private:
MidiKeyboardComponent* const owner;
const int delta;

MidiKeyboardUpDownButton (const MidiKeyboardUpDownButton&);
const MidiKeyboardUpDownButton& operator= (const MidiKeyboardUpDownButton&);

};

//==============================================================================
MidiKeyboardComponent::MidiKeyboardComponent (MidiKeyboardState& state_,
const Orientation orientation_)
: state (state_),
xOffset (0),
blackNoteLength (1),
keyWidth (16.0f),
orientation (orientation_),
midiChannel (1),
midiInChannelMask (0xffff),
velocity (1.0f),
noteUnderMouse (-1),
mouseDownNote (-1),
rangeStart (0),
rangeEnd (127),
firstKey (12 * 4),
canScroll (true),
mouseDragging (false),
keyPresses (4),
keyPressNotes (16),
keyMappingOctave (6),
octaveNumForMiddleC (3)
{
mouseOverKeyColour = Colours::yellow.withAlpha (0.5f);
keyDownColour = Colours::yellow.darker();
upDownButtonBackground = Colours::lightgrey;
upDownButtonArrow = Colours::black;

addChildComponent (scrollDown = new MidiKeyboardUpDownButton (this, -1));
addChildComponent (scrollUp   = new MidiKeyboardUpDownButton (this, 1));

// initialise with a default set of querty key-mappings..
const char* const keymap = "awsedftgyhujkolp;";

for (int i = String (keymap).length(); --i >= 0;)
    setKeyPressForNote (KeyPress (keymap[i], 0), i);

setOpaque (true);
setWantsKeyboardFocus (true);

state.addListener (this);

}

MidiKeyboardComponent::~MidiKeyboardComponent()
{
state.removeListener (this);
jassert (mouseDownNote < 0 && keysPressed.countNumberOfSetBits() == 0); // leaving stuck notes!

deleteAllChildren();

}

//==============================================================================
void MidiKeyboardComponent::setKeyWidth (const float widthInPixels)
{
keyWidth = widthInPixels;
resized();
}

void MidiKeyboardComponent::setOrientation (const Orientation newOrientation)
{
if (orientation != newOrientation)
{
orientation = newOrientation;
resized();
}
}

void MidiKeyboardComponent::setAvailableRange (const int lowestNote,
const 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 (rangeStart, rangeEnd, firstKey);
    resized();
}

}

void MidiKeyboardComponent::setLowestVisibleKey (int noteNumber)
{
noteNumber = jlimit (rangeStart, rangeEnd, noteNumber);

if (noteNumber != firstKey)
{
    firstKey = noteNumber;
    resized();
}

}

void MidiKeyboardComponent::setScrollButtonsVisible (const bool canScroll_)
{
if (canScroll != canScroll_)
{
canScroll = canScroll_;
resized();
}
}

void MidiKeyboardComponent::setColours (const Colour& mouseOverKeyColour_,
const Colour& keyDownColour_,
const Colour& upDownButtonBackground_,
const Colour& upDownButtonArrow_)
{
mouseOverKeyColour = mouseOverKeyColour_;
keyDownColour = keyDownColour_;
upDownButtonBackground = upDownButtonBackground_;
upDownButtonArrow = upDownButtonArrow_;
repaint();
}

//==============================================================================
void MidiKeyboardComponent::setMidiChannel (const int midiChannelNumber)
{
jassert (midiChannelNumber > 0 && midiChannelNumber <= 16);

if (midiChannel != midiChannelNumber)
{
    resetAnyKeysInUse();
    midiChannel = jlimit (1, 16, midiChannelNumber);
}

}

void MidiKeyboardComponent::setMidiChannelsToDisplay (const int midiChannelMask)
{
midiInChannelMask = midiChannelMask;
triggerAsyncUpdate();
}

void MidiKeyboardComponent::setVelocity (const float velocity_)
{
jassert (velocity > 0 && velocity <= 1.0f);

velocity = jlimit (0.0f, 1.0f, velocity_);

}

//==============================================================================
void MidiKeyboardComponent::getKeyPos (int midiNoteNumber, int& x, int& w) const
{
jassert (midiNoteNumber >= rangeStart && midiNoteNumber <= rangeEnd);
midiNoteNumber -= rangeStart;

const float blackNoteWidth = 0.7f;

const float notePos[] = { 0.0f, 1 - blackNoteWidth * 0.6f,
                          1.0f, 2 - blackNoteWidth * 0.4f,
                          2.0f, 3.0f, 4 - blackNoteWidth * 0.7f,
                          4.0f, 5 - blackNoteWidth * 0.5f,
                          5.0f, 6 - blackNoteWidth * 0.3f,
                          6.0f };

const float widths[] = { 1.0f, blackNoteWidth,
                         1.0f, blackNoteWidth,
                         1.0f, 1.0f, blackNoteWidth,
                         1.0f, blackNoteWidth,
                         1.0f, blackNoteWidth,
                         1.0f };

const int octave = midiNoteNumber / 12;
const int note = midiNoteNumber % 12;

x = roundFloatToInt (octave * 7.0f * keyWidth + notePos [note] * keyWidth) - xOffset;
w = roundFloatToInt (widths [note] * keyWidth);

}

const uint8 whiteNotes = { 0, 2, 4, 5, 7, 9, 11 };
const uint8 blackNotes = { 1, 3, 6, 8, 10 };

int MidiKeyboardComponent::xyToNote (int x, int y)
{
if (! reallyContains (x, y, false))
return -1;

if (orientation != horizontalKeyboard)
{
    swapVariables (x, y);

    if (orientation == verticalKeyboardFacingLeft)
        y = getWidth() - y;
    else
        x = getHeight() - x;
}

return remappedXYToNote (x + xOffset, y);

}

int MidiKeyboardComponent::remappedXYToNote (int x, int y) const
{
const int noteX = x % roundFloatToInt (keyWidth * 7.0f);
const int octaveStart = 12 * ((x - noteX) / roundFloatToInt (keyWidth * 7.0f));

if (y < blackNoteLength)
{
    for (int i = 0; i < 5; ++i)
    {
        const int note = rangeStart + octaveStart + blackNotes [i];

        if (note >= rangeStart && note <= rangeEnd)
        {
            int kx, kw;
            getKeyPos (note, kx, kw);
            kx += xOffset;

            if (x >= kx && x < kx + kw)
                return note;
        }
    }
}

int result = rangeStart + octaveStart + whiteNotes [noteX / roundFloatToInt (keyWidth)];

if (result < rangeStart || result > rangeEnd)
    result = -1;

return result;

}

//==============================================================================
void MidiKeyboardComponent::repaintNote (const int noteNum)
{
if (noteNum >= rangeStart && noteNum <= rangeEnd)
{
int x, w;
getKeyPos (noteNum, x, w);

    if (orientation == horizontalKeyboard)
        repaint (x, 0, w, getHeight());
    else if (orientation == verticalKeyboardFacingLeft)
        repaint (0, x, getWidth(), w);
    else if (orientation == verticalKeyboardFacingRight)
        repaint (0, getHeight() - x - w, getWidth(), w);
}

}

void MidiKeyboardComponent::paint (Graphics& g)
{
g.fillAll (Colours::white);

int x, w, octave;

for (octave = 0; octave < 128; octave += 12)
{
    for (int white = 0; white < 7; ++white)
    {
        const int noteNum = octave + whiteNotes [white];

        if (noteNum >= rangeStart && noteNum <= rangeEnd)
        {
            getKeyPos (noteNum, x, w);

            if (orientation == horizontalKeyboard)
                drawWhiteNote (noteNum, g, x, 0, w, getHeight(), state.isNoteOnForChannels (midiInChannelMask, noteNum), noteUnderMouse == noteNum);
            else if (orientation == verticalKeyboardFacingLeft)
                drawWhiteNote (noteNum, g, 0, x, getWidth(), w, state.isNoteOnForChannels (midiInChannelMask, noteNum), noteUnderMouse == noteNum);
            else if (orientation == verticalKeyboardFacingRight)
                drawWhiteNote (noteNum, g, 0, getHeight() - x - w, getWidth(), w, state.isNoteOnForChannels (midiInChannelMask, noteNum), noteUnderMouse == noteNum);
        }
    }
}

float x1 = 0.0f, y1 = 0.0f, x2 = 0.0f, y2 = 0.0f;

if (orientation == verticalKeyboardFacingLeft)
{
    x1 = getWidth() - 1.0f;
    x2 = getWidth() - 5.0f;
}
else if (orientation == verticalKeyboardFacingRight)
    x2 = 5.0f;
else
    y2 = 5.0f;

GradientBrush gb (Colours::black.withAlpha (0.3f), x1, y1,
                  Colours::transparentBlack, x2, y2, false);
g.setBrush (&gb);

getKeyPos (rangeEnd, x, w);
x += w;

if (orientation == verticalKeyboardFacingLeft)
    g.fillRect (getWidth() - 5, 0, 5, x);
else if (orientation == verticalKeyboardFacingRight)
    g.fillRect (0, 0, 5, x);
else
    g.fillRect (0, 0, x, 5);

g.setColour (Colours::black.withAlpha (0.2f));

if (orientation == verticalKeyboardFacingLeft)
    g.fillRect (0, 0, 1, x);
else if (orientation == verticalKeyboardFacingRight)
    g.fillRect (getWidth() - 1, 0, 1, x);
else
    g.fillRect (0, getHeight() - 1, x, 1);

for (octave = 0; octave < 128; octave += 12)
{
    for (int black = 0; black < 5; ++black)
    {
        const int noteNum = octave + blackNotes [black];

        if (noteNum >= rangeStart && noteNum <= rangeEnd)
        {
            getKeyPos (noteNum, x, w);

            if (orientation == horizontalKeyboard)
                drawBlackNote (noteNum, g, x, 0, w, blackNoteLength, state.isNoteOnForChannels (midiInChannelMask, noteNum), noteUnderMouse == noteNum);
            else if (orientation == verticalKeyboardFacingLeft)
                drawBlackNote (noteNum, g, getWidth() - blackNoteLength, x, blackNoteLength, w, state.isNoteOnForChannels (midiInChannelMask, noteNum), noteUnderMouse == noteNum);
            else if (orientation == verticalKeyboardFacingRight)
                drawBlackNote (noteNum, g, 0, getHeight() - x - w, blackNoteLength, w, state.isNoteOnForChannels (midiInChannelMask, noteNum), noteUnderMouse == noteNum);
        }
    }
}

}

void MidiKeyboardComponent::drawWhiteNote (int midiNoteNumber,
Graphics& g, int x, int y, int w, int h,
bool isDown, bool isOver)
{
Colour c (Colours::transparentWhite);

if (isDown)
    c = c.overlaidWith (keyDownColour);

if (isOver)
    c = c.overlaidWith (mouseOverKeyColour);

g.setColour (c);
g.fillRect (x, y, w, h);

const String text (getWhiteNoteText (midiNoteNumber));

if (! text.isEmpty())
{
    g.setColour (Colours::black);
    Font f (jmin (12.0f, keyWidth * 0.9f));
    f.setHorizontalScale (0.8f);
    g.setFont (f);
    Justification justification (Justification::centredBottom);

    if (orientation == verticalKeyboardFacingLeft)
        justification = Justification::centredLeft;
    else if (orientation == verticalKeyboardFacingRight)
        justification = Justification::centredRight;

    g.drawFittedText (text, x + 2, y + 2, w - 4, h - 4, justification, 1);
}

g.setColour (Colours::grey);

if (orientation == horizontalKeyboard)
    g.fillRect (x, y, 1, h);
else if (orientation == verticalKeyboardFacingLeft)
    g.fillRect (x, y, w, 1);
else if (orientation == verticalKeyboardFacingRight)
    g.fillRect (x, y + h - 1, w, 1);

if (midiNoteNumber == rangeEnd)
{
    if (orientation == horizontalKeyboard)
        g.fillRect (x + w, y, 1, h);
    else if (orientation == verticalKeyboardFacingLeft)
        g.fillRect (x, y + h, w, 1);
    else if (orientation == verticalKeyboardFacingRight)
        g.fillRect (x, y - 1, w, 1);
}

}

void MidiKeyboardComponent::drawBlackNote (int /midiNoteNumber/,
Graphics& g, int x, int y, int w, int h,
bool isDown, bool isOver)
{
Colour c (Colours::black);

if (isDown)
    c = c.overlaidWith (keyDownColour);

if (isOver)
    c = c.overlaidWith (mouseOverKeyColour);

g.setColour (c);
g.fillRect (x, y, w, h);

if (isDown)
{
    g.setColour (Colours::black);
    g.drawRect (x, y, w, h);
}
else
{
    const int xIndent = jmax (1, jmin (w, h) / 8);

    g.setColour (c.brighter());

    if (orientation == horizontalKeyboard)
        g.fillRect (x + xIndent, y, w - xIndent * 2, 7 * h / 8);
    else if (orientation == verticalKeyboardFacingLeft)
        g.fillRect (x + w / 8, y + xIndent, w - w / 8, h - xIndent * 2);
    else if (orientation == verticalKeyboardFacingRight)
        g.fillRect (x, y + xIndent, 7 * w / 8, h - xIndent * 2);
}

}

void MidiKeyboardComponent::setOctaveForMiddleC (const int octaveNumForMiddleC_) throw()
{
octaveNumForMiddleC = octaveNumForMiddleC_;
repaint();
}

const String MidiKeyboardComponent::getWhiteNoteText (const int midiNoteNumber)
{
if (keyWidth > 14.0f && midiNoteNumber % 12 == 0)
return MidiMessage::getMidiNoteName (midiNoteNumber, true, true, octaveNumForMiddleC);

return String::empty;

}

void MidiKeyboardComponent::drawUpDownButton (Graphics& g, int w, int h,
const bool isMouseOver,
const bool isButtonDown,
const bool movesOctavesUp)
{
g.fillAll (upDownButtonBackground);

float angle;

if (orientation == MidiKeyboardComponent::horizontalKeyboard)
    angle = movesOctavesUp ? 0.0f : 0.5f;
else if (orientation == MidiKeyboardComponent::verticalKeyboardFacingLeft)
    angle = movesOctavesUp ? 0.25f : 0.75f;
else
    angle = movesOctavesUp ? 0.75f : 0.25f;

Path path;
path.lineTo (0.0f, 1.0f);
path.lineTo (1.0f, 0.5f);
path.closeSubPath();

path.applyTransform (AffineTransform::identity
                        .rotated (float_Pi * 2.0f * angle, 0.5f, 0.5f));

g.setColour (upDownButtonArrow.withAlpha (isButtonDown ? 1.0f : (isMouseOver ? 0.6f : 0.4f)));

g.fillPath (path, path.getTransformToScaleToFit (1.0f, 1.0f,
                                                 w - 2.0f,
                                                 h - 2.0f,
                                                 true));

}

void MidiKeyboardComponent::resized()
{
int w = getWidth();
int h = getHeight();

if (orientation != horizontalKeyboard)
    swapVariables (w, h);

blackNoteLength = roundFloatToInt (h * 0.7f);

bool showScrollButtons = false;

if (canScroll)
{
    int kx2, kw2;
    getKeyPos (rangeEnd, kx2, kw2);

    kx2 += kw2;

    if (firstKey != rangeStart)
    {
        int kx1, kw1;
        getKeyPos (rangeStart, kx1, kw1);

        if (kx2 - kx1 <= w)
        {
            firstKey = rangeStart;
            repaint();
        }
    }

    showScrollButtons = (firstKey > rangeStart || kx2 > w + xOffset * 2);
}

scrollDown->setVisible (showScrollButtons);
scrollUp->setVisible (showScrollButtons);

xOffset = 0;

if (showScrollButtons)
{
    const int scrollButtonW = jmin (12, w / 2);

    if (orientation == horizontalKeyboard)
    {
        scrollDown->setBounds (0, 0, scrollButtonW, getHeight());
        scrollUp->setBounds (getWidth() - scrollButtonW, 0, scrollButtonW, getHeight());
    }
    else if (orientation == verticalKeyboardFacingLeft)
    {
        scrollDown->setBounds (0, 0, getWidth(), scrollButtonW);
        scrollUp->setBounds (0, getHeight() - scrollButtonW, getWidth(), scrollButtonW);
    }
    else if (orientation == verticalKeyboardFacingRight)
    {
        scrollDown->setBounds (0, getHeight() - scrollButtonW, getWidth(), scrollButtonW);
        scrollUp->setBounds (0, 0, getWidth(), scrollButtonW);
    }

    int endOfLastKey, kw;
    getKeyPos (rangeEnd, endOfLastKey, kw);
    endOfLastKey += kw;

    const int spaceAvailable = w - scrollButtonW * 2;
    const int lastStartKey = remappedXYToNote (endOfLastKey - spaceAvailable, 0) + 1;

    if (lastStartKey >= 0 && firstKey > lastStartKey)
        firstKey = jlimit (rangeStart, rangeEnd, lastStartKey);

    getKeyPos (firstKey, xOffset, kw);
    xOffset -= scrollButtonW;
}
else
{
    firstKey = rangeStart;
}

timerCallback();
repaint();

}

//==============================================================================
void MidiKeyboardComponent::handleNoteOn (MidiKeyboardState*, int /midiChannel/, int /midiNoteNumber/, float /velocity/)
{
triggerAsyncUpdate();
}

void MidiKeyboardComponent::handleNoteOff (MidiKeyboardState*, int /midiChannel/, int /midiNoteNumber/)
{
triggerAsyncUpdate();
}

void MidiKeyboardComponent::handleAsyncUpdate()
{
for (int i = rangeStart; i <= rangeEnd; ++i)
{
if (keysCurrentlyDrawnDown[i] != state.isNoteOnForChannels (midiInChannelMask, i))
{
keysCurrentlyDrawnDown.setBit (i, state.isNoteOnForChannels (midiInChannelMask, i));
repaintNote (i);
}
}
}

//==============================================================================
void MidiKeyboardComponent::resetAnyKeysInUse()
{
if (keysPressed.countNumberOfSetBits() > 0 || mouseDownNote > 0)
{
state.allNotesOff (midiChannel);
keysPressed.clear();
mouseDownNote = -1;
}
}

void MidiKeyboardComponent::updateNoteUnderMouse (int x, int y)
{
const int newNote = xyToNote (x, y);

if (noteUnderMouse != newNote)
{
    if (mouseDownNote >= 0)
    {
        state.noteOff (midiChannel, mouseDownNote);
        mouseDownNote = -1;
    }

    if (mouseDragging && newNote >= 0)
    {
        state.noteOn (midiChannel, newNote, velocity);
        mouseDownNote = newNote;
    }

    repaintNote (noteUnderMouse);
    noteUnderMouse = newNote;
    repaintNote (noteUnderMouse);
}
else if (mouseDownNote >= 0 && ! mouseDragging)
{
    state.noteOff (midiChannel, mouseDownNote);
    mouseDownNote = -1;
}

}

void MidiKeyboardComponent::mouseMove (const MouseEvent& e)
{
updateNoteUnderMouse (e.x, e.y);
stopTimer();
}

void MidiKeyboardComponent::mouseDrag (const MouseEvent& e)
{
const int newNote = xyToNote (e.x, e.y);

if (newNote >= 0)
    mouseDraggedToKey (newNote, e);

updateNoteUnderMouse (e.x, e.y);

}

bool MidiKeyboardComponent::mouseDownOnKey (int /midiNoteNumber/, const MouseEvent&)
{
return true;
}

void MidiKeyboardComponent::mouseDraggedToKey (int /midiNoteNumber/, const MouseEvent&)
{
}

void MidiKeyboardComponent::mouseDown (const MouseEvent& e)
{
const int newNote = xyToNote (e.x, e.y);
mouseDragging = false;

if (newNote >= 0 && mouseDownOnKey (newNote, e))
{
    repaintNote (noteUnderMouse);
    noteUnderMouse = -1;
    mouseDragging = true;

    updateNoteUnderMouse (e.x, e.y);
    startTimer (500);
}

}

void MidiKeyboardComponent::mouseUp (const MouseEvent& e)
{
mouseDragging = false;
updateNoteUnderMouse (e.x, e.y);

stopTimer();

}

void MidiKeyboardComponent::mouseEnter (const MouseEvent& e)
{
updateNoteUnderMouse (e.x, e.y);
}

void MidiKeyboardComponent::mouseExit (const MouseEvent& e)
{
updateNoteUnderMouse (e.x, e.y);
}

void MidiKeyboardComponent::mouseWheelMove (const MouseEvent&, float wheelIncrement)
{
setLowestVisibleKey (getLowestVisibleKey() + roundFloatToInt (wheelIncrement * 5.0f));
}

void MidiKeyboardComponent::timerCallback()
{
updateNoteUnderMouse (getMouseXRelative(),
getMouseYRelative());
}

//==============================================================================
void MidiKeyboardComponent::clearKeyMappings()
{
resetAnyKeysInUse();
keyPressNotes.clear();
keyPresses.clear();
}

void MidiKeyboardComponent::setKeyPressForNote (const KeyPress& key,
const int midiNoteOffsetFromC)
{
removeKeyPressForNote (midiNoteOffsetFromC);

keyPressNotes.add (midiNoteOffsetFromC);
keyPresses.add (new KeyPress (key));

}

void MidiKeyboardComponent::removeKeyPressForNote (const int midiNoteOffsetFromC)
{
for (int i = keyPressNotes.size(); --i >= 0;)
{
if (keyPressNotes.getUnchecked (i) == midiNoteOffsetFromC)
{
keyPressNotes.remove (i);
keyPresses.remove (i);
}
}
}

void MidiKeyboardComponent::setKeyPressBaseOctave (const int newOctaveNumber)
{
jassert (newOctaveNumber >= 0 && newOctaveNumber <= 10);

keyMappingOctave = newOctaveNumber;

}

void MidiKeyboardComponent::keyStateChanged()
{
bool keyPressUsed = false;

for (int i = keyPresses.size(); --i >= 0;)
{
    const KeyPress key (*keyPresses.getUnchecked (i));
    const int note = 12 * keyMappingOctave + keyPressNotes.getUnchecked (i);

    if (key.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);
            keyPressUsed = true;
        }
    }
}

if (! keyPressUsed)
    Component::keyStateChanged();

}

void MidiKeyboardComponent::focusLost (FocusChangeType)
{
resetAnyKeysInUse();
}

END_JUCE_NAMESPACE
[/code]

and

/*
  ==============================================================================

   This file is part of the JUCE library - "Jules' Utility Class Extensions"
   Copyright 2004-6 by Raw Material Software ltd.

  ------------------------------------------------------------------------------

   JUCE can be redistributed and/or modified under the terms of the
   GNU General Public License, as published by the Free Software Foundation;
   either version 2 of the License, or (at your option) any later version.

   JUCE is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with JUCE; if not, visit www.gnu.org/licenses or write to the
   Free Software Foundation, Inc., 59 Temple Place, Suite 330, 
   Boston, MA 02111-1307 USA

  ------------------------------------------------------------------------------

   If you'd like to release a closed-source product which uses JUCE, commercial
   licenses are also available: visit www.rawmaterialsoftware.com/juce for
   more information.

  ==============================================================================
*/










//==============================================================================
/**
    A component that displays a piano keyboard, whose notes can be clicked on.

    This component will mimic a physical midi keyboard, showing the current state of
    a MidiKeyboardState object. When the on-screen keys are clicked on, it will play these
    notes by calling the noteOn() and noteOff() methods of its MidiKeyboardState object.

    Another feature is that the computer keyboard can also be used to play notes. By
    default it maps the top two rows of a standard querty keyboard to the notes, but
    these can be remapped if needed. It will only respond to keypresses when it has
    the keyboard focus, so to disable this feature you can call setWantsKeyboardFocus (false).

    @see MidiKeyboardState
*/
class JUCE_API  MidiKeyboardComponent  : public Component,
                                         public MidiKeyboardStateListener,
                                         private Timer,
                                         private AsyncUpdater
{
public:
    //==============================================================================
    /** The direction of the keyboard.

        @see setOrientation
    */
    enum Orientation
    {
        horizontalKeyboard,
        verticalKeyboardFacingLeft,
        verticalKeyboardFacingRight,
    };

    /** Creates a MidiKeyboardComponent.

        @param state        the midi keyboard model that this component will represent
        @param orientation  whether the keyboard is horizonal or vertical
    */
    MidiKeyboardComponent (MidiKeyboardState& state,
                           const Orientation orientation);

    /** Destructor. */
    ~MidiKeyboardComponent();

    //==============================================================================
    /** Changes the velocity used in midi note-on messages that are triggered by clicking
        on the component.

        Values are 0 to 1.0, where 1.0 is the heaviest.

        @see setMidiChannel
    */
    void setVelocity (const float velocity);

    /** Changes the midi channel number that will be used for events triggered by clicking
        on the component.

        The channel must be between 1 and 16 (inclusive). This is the channel that will be
        passed on to the MidiKeyboardState::noteOn() method when the user clicks the component.

        Although this is the channel used for outgoing events, the component can display
        incoming events from more than one channel - see setMidiChannelsToDisplay()

        @see setVelocity
    */
    void setMidiChannel (const int midiChannelNumber);

    /** Returns the midi channel that the keyboard is using for midi messages.

        @see setMidiChannel
    */
    int getMidiChannel() const throw()                              { return midiChannel; }

    /** Sets a mask to indicate which incoming midi channels should be represented by
        key movements.

        The mask is a set of bits, where bit 0 = midi channel 1, bit 1 = midi channel 2, etc.

        If the MidiKeyboardState has a key down for any of the channels whose bits are set
        in this mask, the on-screen keys will also go down.

        By default, this mask is set to 0xffff (all channels displayed).

        @see setMidiChannel
    */
    void setMidiChannelsToDisplay (const int midiChannelMask);

    /** Returns the current set of midi channels represented by the component.

        This is the value that was set with setMidiChannelsToDisplay().
    */
    int getMidiChannelsToDisplay() const throw()                    { return midiInChannelMask; }

    //==============================================================================
    /** Changes the width used to draw the white keys. */
    void setKeyWidth (const float widthInPixels);

    /** Returns the width that was set by setKeyWidth(). */
    float getKeyWidth() const throw()                               { return keyWidth; }

    /** Changes the keyboard's current direction. */
    void setOrientation (const Orientation newOrientation);

    /** Returns the keyboard's current direction. */
    const Orientation getOrientation() const throw()                { return orientation; }

    /** Sets the range of midi notes that the keyboard will be limited to.

        By default the range is 0 to 127 (inclusive), but you can limit this if you
        only want a restricted set of the keys to be shown.

        Note that the values here are inclusive and must be between 0 and 127.
    */
    void setAvailableRange (const int lowestNote,
                            const int highestNote);

    /** Returns the first note in the available range.

        @see setAvailableRange
    */
    int getRangeStart() const throw()                               { return rangeStart; }

    /** Returns the last note in the available range.

        @see setAvailableRange
    */
    int getRangeEnd() const throw()                                 { return rangeEnd; }

    /** If the keyboard extends beyond the size of the component, this will scroll
        it to show the given key at the start.
    */
    void setLowestVisibleKey (int noteNumber);

    /** Returns the number of the first key shown in the component.

        @see setLowestVisibleKey
    */
    int getLowestVisibleKey() const throw()                         { return firstKey; }

    /** If set to true, then scroll buttons will appear at either end of the keyboard
        if there are too many notes to fit them all in the component at once.
    */
    void setScrollButtonsVisible (const bool canScroll);

    /** Sets the colours to use for highlighting keys.

        These colours are used by the default drawWhiteNote() and drawBlackNote()
        implementations.

        @param mouseOverKeyColour   the colour to overlay on keys that the mouse is over
        @param keyDownColour        the colour to use for keys that are down
        @param upDownButtonBackground   the colour for the background of the buttons at
                                        either end of the keyboard
        @param upDownButtonArrow    the colour for the arrows on the up/down buttons
    */
    void setColours (const Colour& mouseOverKeyColour,
                     const Colour& keyDownColour,
                     const Colour& upDownButtonBackground,
                     const Colour& upDownButtonArrow);

    //==============================================================================
    /** Deletes all key-mappings.

        @see setKeyPressForNote
    */
    void clearKeyMappings();

    /** Maps a key-press to a given note.

        @param key                  the key that should trigger the note
        @param midiNoteOffsetFromC  how many semitones above C the triggered note should
                                    be. The actual midi note that gets played will be
                                    this value + (12 * the current base octave). To change
                                    the base octave, see setKeyPressBaseOctave()
    */
    void setKeyPressForNote (const KeyPress& key,
                             const int midiNoteOffsetFromC);

    /** Removes any key-mappings for a given note.

        For a description of what the note number means, see setKeyPressForNote().
    */
    void removeKeyPressForNote (const int midiNoteOffsetFromC);

    /** Changes the base note above which key-press-triggered notes are played.

        The set of key-mappings that trigger notes can be moved up and down to cover
        the entire scale using this method.

        The value passed in is an octave number between 0 and 10 (inclusive), and
        indicates which C is the base note to which the key-mapped notes are
        relative.
    */
    void setKeyPressBaseOctave (const int newOctaveNumber);

    /** This sets the octave number which is shown as the octave number for middle C.

        This affects only the default implementation of getWhiteNoteText(), which
        passes this octave number to MidiMessage::getMidiNoteName() in order to
        get the note text. See MidiMessage::getMidiNoteName() for more info about
        the parameter.

        By default this value is set to 3.
    */
    void setOctaveForMiddleC (const int octaveNumForMiddleC) throw();

    //==============================================================================
    /** @internal */
    void paint (Graphics& g);
    /** @internal */
    void resized();
    /** @internal */
    void mouseMove (const MouseEvent& e);
    /** @internal */
    void mouseDrag (const MouseEvent& e);
    /** @internal */
    void mouseDown (const MouseEvent& e);
    /** @internal */
    void mouseUp (const MouseEvent& e);
    /** @internal */
    void mouseEnter (const MouseEvent& e);
    /** @internal */
    void mouseExit (const MouseEvent& e);
    /** @internal */
    void mouseWheelMove (const MouseEvent& e, float wheelIncrement);
    /** @internal */
    void timerCallback();
    /** @internal */
    void keyStateChanged();
    /** @internal */
    void focusLost (FocusChangeType cause);
    /** @internal */
    void handleNoteOn (MidiKeyboardState* source, int midiChannel, int midiNoteNumber, float velocity);
    /** @internal */
    void handleNoteOff (MidiKeyboardState* source, int midiChannel, int midiNoteNumber);
    /** @internal */
    void handleAsyncUpdate();

    //==============================================================================
    juce_UseDebuggingNewOperator

protected:
    friend class MidiKeyboardUpDownButton;

    //==============================================================================
    /** Draws a white note in the given rectangle.

        isOver indicates whether the mouse is over the key, isDown indicates whether the key is
        currently pressed down.

        When doing this, be sure to note the keyboard's orientation.
    */
    virtual void drawWhiteNote (int midiNoteNumber,
                                Graphics& g,
                                int x, int y, int w, int h,
                                bool isDown, bool isOver);

    /** Draws a black note in the given rectangle.

        isOver indicates whether the mouse is over the key, isDown indicates whether the key is
        currently pressed down.

        When doing this, be sure to note the keyboard's orientation.
    */
    virtual void drawBlackNote (int midiNoteNumber,
                                Graphics& g,
                                int x, int y, int w, int h,
                                bool isDown, bool isOver);

    /** Allows text to be drawn on the white notes.

        By default this is used to label the C in each octave, but could be used for other things.

        @see setOctaveForMiddleC
    */
    virtual const String getWhiteNoteText (const int midiNoteNumber);

    /** Draws the up and down buttons that change the base note. */
    virtual void drawUpDownButton (Graphics& g, int w, int h,
                                   const bool isMouseOver,
                                   const bool isButtonPressed,
                                   const bool movesOctavesUp);

    /** Callback when the mouse is clicked on a key.

        You could use this to do things like handle right-clicks on keys, etc.

        Return true if you want the click to trigger the note, or false if you
        want to handle it yourself and not have the note played.

        @see mouseDraggedToKey
    */
    virtual bool mouseDownOnKey (int midiNoteNumber, const MouseEvent& e);

    /** Callback when the mouse is dragged from one key onto another.

        @see mouseDownOnKey
    */
    virtual void mouseDraggedToKey (int midiNoteNumber, const MouseEvent& e);

private:
    //==============================================================================
    MidiKeyboardState& state;
    int xOffset, blackNoteLength;
    float keyWidth;
    Orientation orientation;

    int midiChannel, midiInChannelMask;
    float velocity;
    int noteUnderMouse, mouseDownNote;
    BitArray keysPressed, keysCurrentlyDrawnDown;

    int rangeStart, rangeEnd, firstKey;
    bool canScroll, mouseDragging;
    Colour mouseOverKeyColour, keyDownColour, upDownButtonBackground, upDownButtonArrow;
    Button* scrollDown;
    Button* scrollUp;

    OwnedArray <KeyPress> keyPresses;
    Array <int> keyPressNotes;
    int keyMappingOctave;
    int octaveNumForMiddleC;

    void getKeyPos (int midiNoteNumber, int& x, int& w) const;
    int xyToNote (int x, int y);
    int remappedXYToNote (int x, int y) const;
    void resetAnyKeysInUse();
    void updateNoteUnderMouse (int x, int y);
    void repaintNote (const int midiNoteNumber);

    MidiKeyboardComponent (const MidiKeyboardComponent&);
    const MidiKeyboardComponent& operator= (const MidiKeyboardComponent&);
};


top-of-cool in the world :smiley: thanks a lot for the update-ette :slight_smile:

oh, one more pretty handy one- it’d be ultra-uber-top if there were a way of latching notes, so clicking on one would have it stay down. There could be either just one or multiple notes that could stay down. In my MidiNoteMap plugin, i’m not using the keyboard to PLAY notes, i’m using it to SELECT them, and currently once you let go they disappear. :slight_smile:

ok - good idea, but remind me again about it: haven’t got time at the moment, and it’ll require a bit of hackery. Probably more a job for the MidiKeyboardState object than the UI component…

Hi.

Did you manage to create the “latching” key option for MidiKeyboardComponent?

Thanks

Phil

Don’t think I ever did that in the 4 years since the original post!

Yes sorry I did realise that it was a rather large bump after I posted it!

I have been working on MIOS Studio http://www.ucapps.de/mios_studio.html which has recently been rewritten from a Java based solution to use JUCE (among other things to get around the terrible java sysex support in newer macs) and one thing the old version had was this latching midi keyboard which can be very useful at times…

Do you think it would be much work to implement? I am happy to give it a go if you could give me some pointers!

Cheers

Phil

You’re welcome to have a go - it’d all be done in the MidiKeyboardComponent class, I guess. Would be interested to see what you come up with though!

I have found that by simply changing the following code in MidiKeyboardComponent::updateNoteUnderMouse I get “most” of the effect that I am looking for.

			if (!ModifierKeys::getCurrentModifiers().isRightButtonDown())
			{
				state.noteOff (midiChannel, mouseDownNote);
				mouseDownNote = -1;
			}

This allows you to right-drag the mouse over the keyboard and it latches until the keys are left-clicked or left-dragged over. I know all I am doing is suppressing the noteOff if the right button is down but it kind of works :slight_smile:

What this doesn’t do though is work with single right click’s of a note because of course the isRightButtonDown() isn’t true when the right button is clicked and released on the same key so the noteOff happens…

I am thinking that I will need to store the previous state of isRightButtonDown() and check for that in the keyStateChanged() function? What do you think?

Phil

A better way to do that would be to pass the MouseEvent object to the updateNoteUnderMouse() method, because the mouse event will contain a right-button flag when it’s a right mouse-up.

I did think about that but I noticed that not all functions that call updateNoteUnderMouse() have a MouseEvent object to pass.

void MidiKeyboardComponent::timerCallback()
{
	updateNoteUnderMouse (getMouseXYRelative());
}

As there is no specific “event” that is triggered, what exactly does this do???

Cheers

Phil

I think that’s there to stop notes getting stuck if the mouse-events stop. That’s not likely to happen in a normal app, but in a plugin, the host can do strange things with mouse-events.