/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2022 - Raw Material Software Limited 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 7 End-User License Agreement and JUCE Privacy Policy. End User License Agreement: www.juce.com/juce-7-licence Privacy Policy: www.juce.com/juce-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 { constexpr uint8 whiteNotes[] = { 0, 2, 4, 5, 7, 9, 11 }; constexpr uint8 blackNotes[] = { 1, 3, 6, 8, 10 }; //============================================================================== struct KeyboardComponentBase::UpDownButton : public Button { UpDownButton (KeyboardComponentBase& c, int d) : Button ({}), owner (c), delta (d) { } void clicked() override { auto note = owner.getLowestVisibleKey(); note = delta < 0 ? (note - 1) / 12 : note / 12 + 1; owner.setLowestVisibleKey (note * 12); } using Button::clicked; void paintButton (Graphics& g, bool shouldDrawButtonAsHighlighted, bool shouldDrawButtonAsDown) override { owner.drawUpDownButton (g, getWidth(), getHeight(), shouldDrawButtonAsHighlighted, shouldDrawButtonAsDown, delta > 0); } private: KeyboardComponentBase& owner; int delta; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UpDownButton) }; //============================================================================== KeyboardComponentBase::KeyboardComponentBase (Orientation o) : orientation (o) { scrollDown = std::make_unique (*this, -1); scrollUp = std::make_unique (*this, 1); addChildComponent (*scrollDown); addChildComponent (*scrollUp); colourChanged(); } //============================================================================== void KeyboardComponentBase::setKeyWidth (float widthInPixels) { jassert (widthInPixels > 0); if (keyWidth != widthInPixels) // Prevent infinite recursion if the width is being computed in a 'resized()' callback { keyWidth = widthInPixels; resized(); } } void KeyboardComponentBase::setScrollButtonWidth (int widthInPixels) { jassert (widthInPixels > 0); if (scrollButtonWidth != widthInPixels) { scrollButtonWidth = widthInPixels; resized(); } } void KeyboardComponentBase::setOrientation (Orientation newOrientation) { if (orientation != newOrientation) { orientation = newOrientation; resized(); } } void KeyboardComponentBase::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 KeyboardComponentBase::setLowestVisibleKey (int noteNumber) { setLowestVisibleKeyFloat ((float) noteNumber); } void KeyboardComponentBase::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(); } } float KeyboardComponentBase::getWhiteNoteLength() const noexcept { return (orientation == horizontalKeyboard) ? (float) getHeight() : (float) getWidth(); } void KeyboardComponentBase::setBlackNoteLengthProportion (float ratio) noexcept { jassert (ratio >= 0.0f && ratio <= 1.0f); if (blackNoteLengthRatio != ratio) { blackNoteLengthRatio = ratio; resized(); } } float KeyboardComponentBase::getBlackNoteLength() const noexcept { auto whiteNoteLength = orientation == horizontalKeyboard ? getHeight() : getWidth(); return (float) whiteNoteLength * blackNoteLengthRatio; } void KeyboardComponentBase::setBlackNoteWidthProportion (float ratio) noexcept { jassert (ratio >= 0.0f && ratio <= 1.0f); if (blackNoteWidthRatio != ratio) { blackNoteWidthRatio = ratio; resized(); } } void KeyboardComponentBase::setScrollButtonsVisible (bool newCanScroll) { if (canScroll != newCanScroll) { canScroll = newCanScroll; resized(); } } //============================================================================== Range KeyboardComponentBase::getKeyPos (int midiNoteNumber) const { return getKeyPosition (midiNoteNumber, keyWidth) - xOffset - getKeyPosition (rangeStart, keyWidth).getStart(); } float KeyboardComponentBase::getKeyStartPosition (int midiNoteNumber) const { return getKeyPos (midiNoteNumber).getStart(); } float KeyboardComponentBase::getTotalKeyboardWidth() const noexcept { return getKeyPos (rangeEnd).getEnd(); } KeyboardComponentBase::NoteAndVelocity KeyboardComponentBase::getNoteAndVelocityAtPosition (Point pos, bool children) { if (! reallyContains (pos, children)) return { -1, 0.0f }; auto p = pos; if (orientation != horizontalKeyboard) { p = { p.y, p.x }; if (orientation == verticalKeyboardFacingLeft) p = { p.x, (float) getWidth() - p.y }; else p = { (float) getHeight() - p.x, p.y }; } return remappedXYToNote (p + Point (xOffset, 0)); } KeyboardComponentBase::NoteAndVelocity KeyboardComponentBase::remappedXYToNote (Point pos) 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 (rangeStart <= note && note <= rangeEnd) { if (getKeyPos (note).contains (pos.x - xOffset)) { return { note, jmax (0.0f, pos.y / blackNoteLength) }; } } } } } 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(); return { note, jmax (0.0f, pos.y / (float) whiteNoteLength) }; } } } } return { -1, 0 }; } Rectangle KeyboardComponentBase::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 { (float) getWidth() - blackNoteLength, x, blackNoteLength, w }; case verticalKeyboardFacingRight: return { 0, (float) 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, (float) getHeight() - x - w, (float) getWidth(), w }; default: jassertfalse; break; } } return {}; } //============================================================================== void KeyboardComponentBase::setOctaveForMiddleC (int octaveNum) { octaveNumForMiddleC = octaveNum; repaint(); } //============================================================================== void KeyboardComponentBase::drawUpDownButton (Graphics& g, int w, int h, bool mouseOver, bool buttonDown, bool movesOctavesUp) { g.fillAll (findColour (upDownButtonBackgroundColourId)); float angle = 0; switch (getOrientation()) { 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::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, (float) w - 2.0f, (float) h - 2.0f, true)); } Range KeyboardComponentBase::getKeyPosition (int midiNoteNumber, float targetKeyWidth) const { auto ratio = getBlackNoteWidthProportion(); static const float notePos[] = { 0.0f, 1 - ratio * 0.6f, 1.0f, 2 - ratio * 0.4f, 2.0f, 3.0f, 4 - ratio * 0.7f, 4.0f, 5 - ratio * 0.5f, 5.0f, 6 - ratio * 0.3f, 6.0f }; auto octave = midiNoteNumber / 12; auto note = midiNoteNumber % 12; auto start = (float) octave * 7.0f * targetKeyWidth + notePos[note] * targetKeyWidth; auto width = MidiMessage::isMidiNoteBlack (note) ? blackNoteWidthRatio * targetKeyWidth : targetKeyWidth; return { start, start + width }; } //============================================================================== void KeyboardComponentBase::paint (Graphics& g) { drawKeyboardBackground (g, getLocalBounds().toFloat()); for (int octaveBase = 0; octaveBase < 128; octaveBase += 12) { for (auto noteNum : whiteNotes) { const auto key = octaveBase + noteNum; if (rangeStart <= key && key <= rangeEnd) drawWhiteKey (key, g, getRectangleForKey (key)); } for (auto noteNum : blackNotes) { const auto key = octaveBase + noteNum; if (rangeStart <= key && key <= rangeEnd) drawBlackKey (key, g, getRectangleForKey (key)); } } } void KeyboardComponentBase::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 <= (float) 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(); auto spaceAvailable = w; auto lastStartKey = remappedXYToNote ({ endOfLastKey - (float) spaceAvailable, 0 }).note + 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() > (float) w); repaint(); } } //============================================================================== void KeyboardComponentBase::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); } } // namespace juce