1 /*
2   ==============================================================================
3 
4    This file is part of the JUCE library.
5    Copyright (c) 2020 - Raw Material Software Limited
6 
7    JUCE is an open source library subject to commercial or open-source
8    licensing.
9 
10    By using JUCE, you agree to the terms of both the JUCE 6 End-User License
11    Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
12 
13    End User License Agreement: www.juce.com/juce-6-licence
14    Privacy Policy: www.juce.com/juce-privacy-policy
15 
16    Or: You may also use this code under the terms of the GPL v3 (see
17    www.gnu.org/licenses).
18 
19    JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
20    EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
21    DISCLAIMED.
22 
23   ==============================================================================
24 */
25 
26 namespace juce
27 {
28 
29 static const uint8 whiteNotes[] = { 0, 2, 4, 5, 7, 9, 11 };
30 static const uint8 blackNotes[] = { 1, 3, 6, 8, 10 };
31 
32 
33 struct MidiKeyboardComponent::UpDownButton  : public Button
34 {
UpDownButtonjuce::MidiKeyboardComponent::UpDownButton35     UpDownButton (MidiKeyboardComponent& c, int d)
36         : Button ({}), owner (c), delta (d)
37     {
38     }
39 
clickedjuce::MidiKeyboardComponent::UpDownButton40     void clicked() override
41     {
42         auto note = owner.getLowestVisibleKey();
43 
44         if (delta < 0)
45             note = (note - 1) / 12;
46         else
47             note = note / 12 + 1;
48 
49         owner.setLowestVisibleKey (note * 12);
50     }
51 
52     using Button::clicked;
53 
paintButtonjuce::MidiKeyboardComponent::UpDownButton54     void paintButton (Graphics& g, bool shouldDrawButtonAsHighlighted, bool shouldDrawButtonAsDown) override
55     {
56         owner.drawUpDownButton (g, getWidth(), getHeight(),
57                                 shouldDrawButtonAsHighlighted, shouldDrawButtonAsDown,
58                                 delta > 0);
59     }
60 
61 private:
62     MidiKeyboardComponent& owner;
63     const int delta;
64 
65     JUCE_DECLARE_NON_COPYABLE (UpDownButton)
66 };
67 
68 //==============================================================================
MidiKeyboardComponent(MidiKeyboardState & s,Orientation o)69 MidiKeyboardComponent::MidiKeyboardComponent (MidiKeyboardState& s, Orientation o)
70     : state (s), orientation (o)
71 {
72     scrollDown.reset (new UpDownButton (*this, -1));
73     scrollUp  .reset (new UpDownButton (*this, 1));
74 
75     addChildComponent (scrollDown.get());
76     addChildComponent (scrollUp.get());
77 
78     // initialise with a default set of qwerty key-mappings..
79     int note = 0;
80 
81     for (char c : "awsedftgyhujkolp;")
82         setKeyPressForNote (KeyPress (c, 0, 0), note++);
83 
84     mouseOverNotes.insertMultiple (0, -1, 32);
85     mouseDownNotes.insertMultiple (0, -1, 32);
86 
87     colourChanged();
88     setWantsKeyboardFocus (true);
89 
90     state.addListener (this);
91 
92     startTimerHz (20);
93 }
94 
~MidiKeyboardComponent()95 MidiKeyboardComponent::~MidiKeyboardComponent()
96 {
97     state.removeListener (this);
98 }
99 
100 //==============================================================================
setKeyWidth(float widthInPixels)101 void MidiKeyboardComponent::setKeyWidth (float widthInPixels)
102 {
103     jassert (widthInPixels > 0);
104 
105     if (keyWidth != widthInPixels) // Prevent infinite recursion if the width is being computed in a 'resized()' call-back
106     {
107         keyWidth = widthInPixels;
108         resized();
109     }
110 }
111 
setScrollButtonWidth(int widthInPixels)112 void MidiKeyboardComponent::setScrollButtonWidth (int widthInPixels)
113 {
114     jassert (widthInPixels > 0);
115 
116     if (scrollButtonWidth != widthInPixels)
117     {
118         scrollButtonWidth = widthInPixels;
119         resized();
120     }
121 }
122 
setOrientation(Orientation newOrientation)123 void MidiKeyboardComponent::setOrientation (Orientation newOrientation)
124 {
125     if (orientation != newOrientation)
126     {
127         orientation = newOrientation;
128         resized();
129     }
130 }
131 
setAvailableRange(int lowestNote,int highestNote)132 void MidiKeyboardComponent::setAvailableRange (int lowestNote, int highestNote)
133 {
134     jassert (lowestNote >= 0 && lowestNote <= 127);
135     jassert (highestNote >= 0 && highestNote <= 127);
136     jassert (lowestNote <= highestNote);
137 
138     if (rangeStart != lowestNote || rangeEnd != highestNote)
139     {
140         rangeStart = jlimit (0, 127, lowestNote);
141         rangeEnd = jlimit (0, 127, highestNote);
142         firstKey = jlimit ((float) rangeStart, (float) rangeEnd, firstKey);
143         resized();
144     }
145 }
146 
setLowestVisibleKey(int noteNumber)147 void MidiKeyboardComponent::setLowestVisibleKey (int noteNumber)
148 {
149     setLowestVisibleKeyFloat ((float) noteNumber);
150 }
151 
setLowestVisibleKeyFloat(float noteNumber)152 void MidiKeyboardComponent::setLowestVisibleKeyFloat (float noteNumber)
153 {
154     noteNumber = jlimit ((float) rangeStart, (float) rangeEnd, noteNumber);
155 
156     if (noteNumber != firstKey)
157     {
158         bool hasMoved = (((int) firstKey) != (int) noteNumber);
159         firstKey = noteNumber;
160 
161         if (hasMoved)
162             sendChangeMessage();
163 
164         resized();
165     }
166 }
167 
setScrollButtonsVisible(bool newCanScroll)168 void MidiKeyboardComponent::setScrollButtonsVisible (bool newCanScroll)
169 {
170     if (canScroll != newCanScroll)
171     {
172         canScroll = newCanScroll;
173         resized();
174     }
175 }
176 
colourChanged()177 void MidiKeyboardComponent::colourChanged()
178 {
179     setOpaque (findColour (whiteNoteColourId).isOpaque());
180     repaint();
181 }
182 
183 //==============================================================================
setMidiChannel(int midiChannelNumber)184 void MidiKeyboardComponent::setMidiChannel (int midiChannelNumber)
185 {
186     jassert (midiChannelNumber > 0 && midiChannelNumber <= 16);
187 
188     if (midiChannel != midiChannelNumber)
189     {
190         resetAnyKeysInUse();
191         midiChannel = jlimit (1, 16, midiChannelNumber);
192     }
193 }
194 
setMidiChannelsToDisplay(int midiChannelMask)195 void MidiKeyboardComponent::setMidiChannelsToDisplay (int midiChannelMask)
196 {
197     midiInChannelMask = midiChannelMask;
198     shouldCheckState = true;
199 }
200 
setVelocity(float v,bool useMousePosition)201 void MidiKeyboardComponent::setVelocity (float v, bool useMousePosition)
202 {
203     velocity = jlimit (0.0f, 1.0f, v);
204     useMousePositionForVelocity = useMousePosition;
205 }
206 
207 //==============================================================================
getKeyPosition(int midiNoteNumber,float targetKeyWidth) const208 Range<float> MidiKeyboardComponent::getKeyPosition (int midiNoteNumber, float targetKeyWidth) const
209 {
210     jassert (midiNoteNumber >= 0 && midiNoteNumber < 128);
211 
212     static const float notePos[] = { 0.0f, 1 - blackNoteWidthRatio * 0.6f,
213                                      1.0f, 2 - blackNoteWidthRatio * 0.4f,
214                                      2.0f,
215                                      3.0f, 4 - blackNoteWidthRatio * 0.7f,
216                                      4.0f, 5 - blackNoteWidthRatio * 0.5f,
217                                      5.0f, 6 - blackNoteWidthRatio * 0.3f,
218                                      6.0f };
219 
220     auto octave = midiNoteNumber / 12;
221     auto note   = midiNoteNumber % 12;
222 
223     auto start = (float) octave * 7.0f * targetKeyWidth + notePos[note] * targetKeyWidth;
224     auto width = MidiMessage::isMidiNoteBlack (note) ? blackNoteWidthRatio * targetKeyWidth : targetKeyWidth;
225 
226     return { start, start + width };
227 }
228 
getKeyPos(int midiNoteNumber) const229 Range<float> MidiKeyboardComponent::getKeyPos (int midiNoteNumber) const
230 {
231     return getKeyPosition (midiNoteNumber, keyWidth)
232              - xOffset
233              - getKeyPosition (rangeStart, keyWidth).getStart();
234 }
235 
getRectangleForKey(int note) const236 Rectangle<float> MidiKeyboardComponent::getRectangleForKey (int note) const
237 {
238     jassert (note >= rangeStart && note <= rangeEnd);
239 
240     auto pos = getKeyPos (note);
241     auto x = pos.getStart();
242     auto w = pos.getLength();
243 
244     if (MidiMessage::isMidiNoteBlack (note))
245     {
246         auto blackNoteLength = getBlackNoteLength();
247 
248         switch (orientation)
249         {
250             case horizontalKeyboard:            return { x, 0, w, blackNoteLength };
251             case verticalKeyboardFacingLeft:    return { (float) getWidth() - blackNoteLength, x, blackNoteLength, w };
252             case verticalKeyboardFacingRight:   return { 0, (float) getHeight() - x - w, blackNoteLength, w };
253             default:                            jassertfalse; break;
254         }
255     }
256     else
257     {
258         switch (orientation)
259         {
260             case horizontalKeyboard:            return { x, 0, w, (float) getHeight() };
261             case verticalKeyboardFacingLeft:    return { 0, x, (float) getWidth(), w };
262             case verticalKeyboardFacingRight:   return { 0, (float) getHeight() - x - w, (float) getWidth(), w };
263             default:                            jassertfalse; break;
264         }
265     }
266 
267     return {};
268 }
269 
getKeyStartPosition(int midiNoteNumber) const270 float MidiKeyboardComponent::getKeyStartPosition (int midiNoteNumber) const
271 {
272     return getKeyPos (midiNoteNumber).getStart();
273 }
274 
getTotalKeyboardWidth() const275 float MidiKeyboardComponent::getTotalKeyboardWidth() const noexcept
276 {
277     return getKeyPos (rangeEnd).getEnd();
278 }
279 
getNoteAtPosition(Point<float> p)280 int MidiKeyboardComponent::getNoteAtPosition (Point<float> p)
281 {
282     float v;
283     return xyToNote (p, v);
284 }
285 
xyToNote(Point<float> pos,float & mousePositionVelocity)286 int MidiKeyboardComponent::xyToNote (Point<float> pos, float& mousePositionVelocity)
287 {
288     if (! reallyContains (pos.toInt(), false))
289         return -1;
290 
291     auto p = pos;
292 
293     if (orientation != horizontalKeyboard)
294     {
295         p = { p.y, p.x };
296 
297         if (orientation == verticalKeyboardFacingLeft)
298             p = { p.x, (float) getWidth() - p.y };
299         else
300             p = { (float) getHeight() - p.x, p.y };
301     }
302 
303     return remappedXYToNote (p + Point<float> (xOffset, 0), mousePositionVelocity);
304 }
305 
remappedXYToNote(Point<float> pos,float & mousePositionVelocity) const306 int MidiKeyboardComponent::remappedXYToNote (Point<float> pos, float& mousePositionVelocity) const
307 {
308     auto blackNoteLength = getBlackNoteLength();
309 
310     if (pos.getY() < blackNoteLength)
311     {
312         for (int octaveStart = 12 * (rangeStart / 12); octaveStart <= rangeEnd; octaveStart += 12)
313         {
314             for (int i = 0; i < 5; ++i)
315             {
316                 auto note = octaveStart + blackNotes[i];
317 
318                 if (note >= rangeStart && note <= rangeEnd)
319                 {
320                     if (getKeyPos (note).contains (pos.x - xOffset))
321                     {
322                         mousePositionVelocity = jmax (0.0f, pos.y / blackNoteLength);
323                         return note;
324                     }
325                 }
326             }
327         }
328     }
329 
330     for (int octaveStart = 12 * (rangeStart / 12); octaveStart <= rangeEnd; octaveStart += 12)
331     {
332         for (int i = 0; i < 7; ++i)
333         {
334             auto note = octaveStart + whiteNotes[i];
335 
336             if (note >= rangeStart && note <= rangeEnd)
337             {
338                 if (getKeyPos (note).contains (pos.x - xOffset))
339                 {
340                     auto whiteNoteLength = (orientation == horizontalKeyboard) ? getHeight() : getWidth();
341                     mousePositionVelocity = jmax (0.0f, pos.y / (float) whiteNoteLength);
342                     return note;
343                 }
344             }
345         }
346     }
347 
348     mousePositionVelocity = 0;
349     return -1;
350 }
351 
352 //==============================================================================
repaintNote(int noteNum)353 void MidiKeyboardComponent::repaintNote (int noteNum)
354 {
355     if (noteNum >= rangeStart && noteNum <= rangeEnd)
356         repaint (getRectangleForKey (noteNum).getSmallestIntegerContainer());
357 }
358 
paint(Graphics & g)359 void MidiKeyboardComponent::paint (Graphics& g)
360 {
361     g.fillAll (findColour (whiteNoteColourId));
362 
363     auto lineColour = findColour (keySeparatorLineColourId);
364     auto textColour = findColour (textLabelColourId);
365 
366     for (int octave = 0; octave < 128; octave += 12)
367     {
368         for (int white = 0; white < 7; ++white)
369         {
370             auto noteNum = octave + whiteNotes[white];
371 
372             if (noteNum >= rangeStart && noteNum <= rangeEnd)
373                 drawWhiteNote (noteNum, g, getRectangleForKey (noteNum),
374                                state.isNoteOnForChannels (midiInChannelMask, noteNum),
375                                mouseOverNotes.contains (noteNum), lineColour, textColour);
376         }
377     }
378 
379     float x1 = 0.0f, y1 = 0.0f, x2 = 0.0f, y2 = 0.0f;
380     auto width = getWidth();
381     auto height = getHeight();
382 
383     if (orientation == verticalKeyboardFacingLeft)
384     {
385         x1 = (float) width - 1.0f;
386         x2 = (float) width - 5.0f;
387     }
388     else if (orientation == verticalKeyboardFacingRight)
389         x2 = 5.0f;
390     else
391         y2 = 5.0f;
392 
393     auto x = getKeyPos (rangeEnd).getEnd();
394     auto shadowCol = findColour (shadowColourId);
395 
396     if (! shadowCol.isTransparent())
397     {
398         g.setGradientFill (ColourGradient (shadowCol, x1, y1, shadowCol.withAlpha (0.0f), x2, y2, false));
399 
400         switch (orientation)
401         {
402             case horizontalKeyboard:            g.fillRect (0.0f, 0.0f, x, 5.0f); break;
403             case verticalKeyboardFacingLeft:    g.fillRect ((float) width - 5.0f, 0.0f, 5.0f, x); break;
404             case verticalKeyboardFacingRight:   g.fillRect (0.0f, 0.0f, 5.0f, x); break;
405             default: break;
406         }
407     }
408 
409     if (! lineColour.isTransparent())
410     {
411         g.setColour (lineColour);
412 
413         switch (orientation)
414         {
415             case horizontalKeyboard:            g.fillRect (0.0f, (float) height - 1.0f, x, 1.0f); break;
416             case verticalKeyboardFacingLeft:    g.fillRect (0.0f, 0.0f, 1.0f, x); break;
417             case verticalKeyboardFacingRight:   g.fillRect ((float) width - 1.0f, 0.0f, 1.0f, x); break;
418             default: break;
419         }
420     }
421 
422     auto blackNoteColour = findColour (blackNoteColourId);
423 
424     for (int octave = 0; octave < 128; octave += 12)
425     {
426         for (int black = 0; black < 5; ++black)
427         {
428             auto noteNum = octave + blackNotes[black];
429 
430             if (noteNum >= rangeStart && noteNum <= rangeEnd)
431                 drawBlackNote (noteNum, g, getRectangleForKey (noteNum),
432                                state.isNoteOnForChannels (midiInChannelMask, noteNum),
433                                mouseOverNotes.contains (noteNum), blackNoteColour);
434         }
435     }
436 }
437 
drawWhiteNote(int midiNoteNumber,Graphics & g,Rectangle<float> area,bool isDown,bool isOver,Colour lineColour,Colour textColour)438 void MidiKeyboardComponent::drawWhiteNote (int midiNoteNumber, Graphics& g, Rectangle<float> area,
439                                            bool isDown, bool isOver, Colour lineColour, Colour textColour)
440 {
441     auto c = Colours::transparentWhite;
442 
443     if (isDown)  c = findColour (keyDownOverlayColourId);
444     if (isOver)  c = c.overlaidWith (findColour (mouseOverKeyOverlayColourId));
445 
446     g.setColour (c);
447     g.fillRect (area);
448 
449     auto text = getWhiteNoteText (midiNoteNumber);
450 
451     if (text.isNotEmpty())
452     {
453         auto fontHeight = jmin (12.0f, keyWidth * 0.9f);
454 
455         g.setColour (textColour);
456         g.setFont (Font (fontHeight).withHorizontalScale (0.8f));
457 
458         switch (orientation)
459         {
460             case horizontalKeyboard:            g.drawText (text, area.withTrimmedLeft (1.0f).withTrimmedBottom (2.0f), Justification::centredBottom, false); break;
461             case verticalKeyboardFacingLeft:    g.drawText (text, area.reduced (2.0f), Justification::centredLeft,   false); break;
462             case verticalKeyboardFacingRight:   g.drawText (text, area.reduced (2.0f), Justification::centredRight,  false); break;
463             default: break;
464         }
465     }
466 
467     if (! lineColour.isTransparent())
468     {
469         g.setColour (lineColour);
470 
471         switch (orientation)
472         {
473             case horizontalKeyboard:            g.fillRect (area.withWidth (1.0f)); break;
474             case verticalKeyboardFacingLeft:    g.fillRect (area.withHeight (1.0f)); break;
475             case verticalKeyboardFacingRight:   g.fillRect (area.removeFromBottom (1.0f)); break;
476             default: break;
477         }
478 
479         if (midiNoteNumber == rangeEnd)
480         {
481             switch (orientation)
482             {
483                 case horizontalKeyboard:            g.fillRect (area.expanded (1.0f, 0).removeFromRight (1.0f)); break;
484                 case verticalKeyboardFacingLeft:    g.fillRect (area.expanded (0, 1.0f).removeFromBottom (1.0f)); break;
485                 case verticalKeyboardFacingRight:   g.fillRect (area.expanded (0, 1.0f).removeFromTop (1.0f)); break;
486                 default: break;
487             }
488         }
489     }
490 }
491 
drawBlackNote(int,Graphics & g,Rectangle<float> area,bool isDown,bool isOver,Colour noteFillColour)492 void MidiKeyboardComponent::drawBlackNote (int /*midiNoteNumber*/, Graphics& g, Rectangle<float> area,
493                                            bool isDown, bool isOver, Colour noteFillColour)
494 {
495     auto c = noteFillColour;
496 
497     if (isDown)  c = c.overlaidWith (findColour (keyDownOverlayColourId));
498     if (isOver)  c = c.overlaidWith (findColour (mouseOverKeyOverlayColourId));
499 
500     g.setColour (c);
501     g.fillRect (area);
502 
503     if (isDown)
504     {
505         g.setColour (noteFillColour);
506         g.drawRect (area);
507     }
508     else
509     {
510         g.setColour (c.brighter());
511         auto sideIndent = 1.0f / 8.0f;
512         auto topIndent = 7.0f / 8.0f;
513         auto w = area.getWidth();
514         auto h = area.getHeight();
515 
516         switch (orientation)
517         {
518             case horizontalKeyboard:            g.fillRect (area.reduced (w * sideIndent, 0).removeFromTop   (h * topIndent)); break;
519             case verticalKeyboardFacingLeft:    g.fillRect (area.reduced (0, h * sideIndent).removeFromRight (w * topIndent)); break;
520             case verticalKeyboardFacingRight:   g.fillRect (area.reduced (0, h * sideIndent).removeFromLeft  (w * topIndent)); break;
521             default: break;
522         }
523     }
524 }
525 
setOctaveForMiddleC(int octaveNum)526 void MidiKeyboardComponent::setOctaveForMiddleC (int octaveNum)
527 {
528     octaveNumForMiddleC = octaveNum;
529     repaint();
530 }
531 
getWhiteNoteText(int midiNoteNumber)532 String MidiKeyboardComponent::getWhiteNoteText (int midiNoteNumber)
533 {
534     if (midiNoteNumber % 12 == 0)
535         return MidiMessage::getMidiNoteName (midiNoteNumber, true, true, octaveNumForMiddleC);
536 
537     return {};
538 }
539 
drawUpDownButton(Graphics & g,int w,int h,bool mouseOver,bool buttonDown,bool movesOctavesUp)540 void MidiKeyboardComponent::drawUpDownButton (Graphics& g, int w, int h,
541                                               bool mouseOver,
542                                               bool buttonDown,
543                                               bool movesOctavesUp)
544 {
545     g.fillAll (findColour (upDownButtonBackgroundColourId));
546 
547     float angle = 0;
548 
549     switch (orientation)
550     {
551         case horizontalKeyboard:            angle = movesOctavesUp ? 0.0f  : 0.5f;  break;
552         case verticalKeyboardFacingLeft:    angle = movesOctavesUp ? 0.25f : 0.75f; break;
553         case verticalKeyboardFacingRight:   angle = movesOctavesUp ? 0.75f : 0.25f; break;
554         default:                            jassertfalse; break;
555     }
556 
557     Path path;
558     path.addTriangle (0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.5f);
559     path.applyTransform (AffineTransform::rotation (MathConstants<float>::twoPi * angle, 0.5f, 0.5f));
560 
561     g.setColour (findColour (upDownButtonArrowColourId)
562                   .withAlpha (buttonDown ? 1.0f : (mouseOver ? 0.6f : 0.4f)));
563 
564     g.fillPath (path, path.getTransformToScaleToFit (1.0f, 1.0f, (float) w - 2.0f, (float) h - 2.0f, true));
565 }
566 
setBlackNoteLengthProportion(float ratio)567 void MidiKeyboardComponent::setBlackNoteLengthProportion (float ratio) noexcept
568 {
569     jassert (ratio >= 0.0f && ratio <= 1.0f);
570 
571     if (blackNoteLengthRatio != ratio)
572     {
573         blackNoteLengthRatio = ratio;
574         resized();
575     }
576 }
577 
getBlackNoteLength() const578 float MidiKeyboardComponent::getBlackNoteLength() const noexcept
579 {
580     auto whiteNoteLength = orientation == horizontalKeyboard ? getHeight() : getWidth();
581     return (float) whiteNoteLength * blackNoteLengthRatio;
582 }
583 
setBlackNoteWidthProportion(float ratio)584 void MidiKeyboardComponent::setBlackNoteWidthProportion (float ratio) noexcept
585 {
586     jassert (ratio >= 0.0f && ratio <= 1.0f);
587 
588     if (blackNoteWidthRatio != ratio)
589     {
590         blackNoteWidthRatio = ratio;
591         resized();
592     }
593 }
594 
resized()595 void MidiKeyboardComponent::resized()
596 {
597     auto w = getWidth();
598     auto h = getHeight();
599 
600     if (w > 0 && h > 0)
601     {
602         if (orientation != horizontalKeyboard)
603             std::swap (w, h);
604 
605         auto kx2 = getKeyPos (rangeEnd).getEnd();
606 
607         if ((int) firstKey != rangeStart)
608         {
609             auto kx1 = getKeyPos (rangeStart).getStart();
610 
611             if (kx2 - kx1 <= (float) w)
612             {
613                 firstKey = (float) rangeStart;
614                 sendChangeMessage();
615                 repaint();
616             }
617         }
618 
619         scrollDown->setVisible (canScroll && firstKey > (float) rangeStart);
620 
621         xOffset = 0;
622 
623         if (canScroll)
624         {
625             auto scrollButtonW = jmin (scrollButtonWidth, w / 2);
626             auto r = getLocalBounds();
627 
628             if (orientation == horizontalKeyboard)
629             {
630                 scrollDown->setBounds (r.removeFromLeft  (scrollButtonW));
631                 scrollUp  ->setBounds (r.removeFromRight (scrollButtonW));
632             }
633             else if (orientation == verticalKeyboardFacingLeft)
634             {
635                 scrollDown->setBounds (r.removeFromTop    (scrollButtonW));
636                 scrollUp  ->setBounds (r.removeFromBottom (scrollButtonW));
637             }
638             else
639             {
640                 scrollDown->setBounds (r.removeFromBottom (scrollButtonW));
641                 scrollUp  ->setBounds (r.removeFromTop    (scrollButtonW));
642             }
643 
644             auto endOfLastKey = getKeyPos (rangeEnd).getEnd();
645 
646             float mousePositionVelocity;
647             auto spaceAvailable = w;
648             auto lastStartKey = remappedXYToNote ({ endOfLastKey - (float) spaceAvailable, 0 }, mousePositionVelocity) + 1;
649 
650             if (lastStartKey >= 0 && ((int) firstKey) > lastStartKey)
651             {
652                 firstKey = (float) jlimit (rangeStart, rangeEnd, lastStartKey);
653                 sendChangeMessage();
654             }
655 
656             xOffset = getKeyPos ((int) firstKey).getStart();
657         }
658         else
659         {
660             firstKey = (float) rangeStart;
661         }
662 
663         scrollUp->setVisible (canScroll && getKeyPos (rangeEnd).getStart() > (float) w);
664         repaint();
665     }
666 }
667 
668 //==============================================================================
handleNoteOn(MidiKeyboardState *,int,int,float)669 void MidiKeyboardComponent::handleNoteOn (MidiKeyboardState*, int /*midiChannel*/, int /*midiNoteNumber*/, float /*velocity*/)
670 {
671     shouldCheckState = true; // (probably being called from the audio thread, so avoid blocking in here)
672 }
673 
handleNoteOff(MidiKeyboardState *,int,int,float)674 void MidiKeyboardComponent::handleNoteOff (MidiKeyboardState*, int /*midiChannel*/, int /*midiNoteNumber*/, float /*velocity*/)
675 {
676     shouldCheckState = true; // (probably being called from the audio thread, so avoid blocking in here)
677 }
678 
679 //==============================================================================
resetAnyKeysInUse()680 void MidiKeyboardComponent::resetAnyKeysInUse()
681 {
682     if (! keysPressed.isZero())
683     {
684         for (int i = 128; --i >= 0;)
685             if (keysPressed[i])
686                 state.noteOff (midiChannel, i, 0.0f);
687 
688         keysPressed.clear();
689     }
690 
691     for (int i = mouseDownNotes.size(); --i >= 0;)
692     {
693         auto noteDown = mouseDownNotes.getUnchecked(i);
694 
695         if (noteDown >= 0)
696         {
697             state.noteOff (midiChannel, noteDown, 0.0f);
698             mouseDownNotes.set (i, -1);
699         }
700 
701         mouseOverNotes.set (i, -1);
702     }
703 }
704 
updateNoteUnderMouse(const MouseEvent & e,bool isDown)705 void MidiKeyboardComponent::updateNoteUnderMouse (const MouseEvent& e, bool isDown)
706 {
707     updateNoteUnderMouse (e.getEventRelativeTo (this).position, isDown, e.source.getIndex());
708 }
709 
updateNoteUnderMouse(Point<float> pos,bool isDown,int fingerNum)710 void MidiKeyboardComponent::updateNoteUnderMouse (Point<float> pos, bool isDown, int fingerNum)
711 {
712     float mousePositionVelocity = 0.0f;
713     auto newNote = xyToNote (pos, mousePositionVelocity);
714     auto oldNote = mouseOverNotes.getUnchecked (fingerNum);
715     auto oldNoteDown = mouseDownNotes.getUnchecked (fingerNum);
716     auto eventVelocity = useMousePositionForVelocity ? mousePositionVelocity * velocity : velocity;
717 
718     if (oldNote != newNote)
719     {
720         repaintNote (oldNote);
721         repaintNote (newNote);
722         mouseOverNotes.set (fingerNum, newNote);
723     }
724 
725     if (isDown)
726     {
727         if (newNote != oldNoteDown)
728         {
729             if (oldNoteDown >= 0)
730             {
731                 mouseDownNotes.set (fingerNum, -1);
732 
733                 if (! mouseDownNotes.contains (oldNoteDown))
734                     state.noteOff (midiChannel, oldNoteDown, eventVelocity);
735             }
736 
737             if (newNote >= 0 && ! mouseDownNotes.contains (newNote))
738             {
739                 state.noteOn (midiChannel, newNote, eventVelocity);
740                 mouseDownNotes.set (fingerNum, newNote);
741             }
742         }
743     }
744     else if (oldNoteDown >= 0)
745     {
746         mouseDownNotes.set (fingerNum, -1);
747 
748         if (! mouseDownNotes.contains (oldNoteDown))
749             state.noteOff (midiChannel, oldNoteDown, eventVelocity);
750     }
751 }
752 
mouseMove(const MouseEvent & e)753 void MidiKeyboardComponent::mouseMove (const MouseEvent& e)
754 {
755     updateNoteUnderMouse (e, false);
756 }
757 
mouseDrag(const MouseEvent & e)758 void MidiKeyboardComponent::mouseDrag (const MouseEvent& e)
759 {
760     float mousePositionVelocity;
761     auto newNote = xyToNote (e.position, mousePositionVelocity);
762 
763     if (newNote >= 0 && mouseDraggedToKey (newNote, e))
764         updateNoteUnderMouse (e, true);
765 }
766 
mouseDownOnKey(int,const MouseEvent &)767 bool MidiKeyboardComponent::mouseDownOnKey    (int, const MouseEvent&)  { return true; }
mouseDraggedToKey(int,const MouseEvent &)768 bool MidiKeyboardComponent::mouseDraggedToKey (int, const MouseEvent&)  { return true; }
mouseUpOnKey(int,const MouseEvent &)769 void MidiKeyboardComponent::mouseUpOnKey      (int, const MouseEvent&)  {}
770 
mouseDown(const MouseEvent & e)771 void MidiKeyboardComponent::mouseDown (const MouseEvent& e)
772 {
773     float mousePositionVelocity;
774     auto newNote = xyToNote (e.position, mousePositionVelocity);
775 
776     if (newNote >= 0 && mouseDownOnKey (newNote, e))
777         updateNoteUnderMouse (e, true);
778 }
779 
mouseUp(const MouseEvent & e)780 void MidiKeyboardComponent::mouseUp (const MouseEvent& e)
781 {
782     updateNoteUnderMouse (e, false);
783 
784     float mousePositionVelocity;
785     auto note = xyToNote (e.position, mousePositionVelocity);
786 
787     if (note >= 0)
788         mouseUpOnKey (note, e);
789 }
790 
mouseEnter(const MouseEvent & e)791 void MidiKeyboardComponent::mouseEnter (const MouseEvent& e)
792 {
793     updateNoteUnderMouse (e, false);
794 }
795 
mouseExit(const MouseEvent & e)796 void MidiKeyboardComponent::mouseExit (const MouseEvent& e)
797 {
798     updateNoteUnderMouse (e, false);
799 }
800 
mouseWheelMove(const MouseEvent &,const MouseWheelDetails & wheel)801 void MidiKeyboardComponent::mouseWheelMove (const MouseEvent&, const MouseWheelDetails& wheel)
802 {
803     auto amount = (orientation == horizontalKeyboard && wheel.deltaX != 0)
804                        ? wheel.deltaX : (orientation == verticalKeyboardFacingLeft ? wheel.deltaY
805                                                                                    : -wheel.deltaY);
806 
807     setLowestVisibleKeyFloat (firstKey - amount * keyWidth);
808 }
809 
timerCallback()810 void MidiKeyboardComponent::timerCallback()
811 {
812     if (shouldCheckState)
813     {
814         shouldCheckState = false;
815 
816         for (int i = rangeStart; i <= rangeEnd; ++i)
817         {
818             bool isOn = state.isNoteOnForChannels (midiInChannelMask, i);
819 
820             if (keysCurrentlyDrawnDown[i] != isOn)
821             {
822                 keysCurrentlyDrawnDown.setBit (i, isOn);
823                 repaintNote (i);
824             }
825         }
826     }
827 }
828 
829 //==============================================================================
clearKeyMappings()830 void MidiKeyboardComponent::clearKeyMappings()
831 {
832     resetAnyKeysInUse();
833     keyPressNotes.clear();
834     keyPresses.clear();
835 }
836 
setKeyPressForNote(const KeyPress & key,int midiNoteOffsetFromC)837 void MidiKeyboardComponent::setKeyPressForNote (const KeyPress& key, int midiNoteOffsetFromC)
838 {
839     removeKeyPressForNote (midiNoteOffsetFromC);
840 
841     keyPressNotes.add (midiNoteOffsetFromC);
842     keyPresses.add (key);
843 }
844 
removeKeyPressForNote(int midiNoteOffsetFromC)845 void MidiKeyboardComponent::removeKeyPressForNote (int midiNoteOffsetFromC)
846 {
847     for (int i = keyPressNotes.size(); --i >= 0;)
848     {
849         if (keyPressNotes.getUnchecked (i) == midiNoteOffsetFromC)
850         {
851             keyPressNotes.remove (i);
852             keyPresses.remove (i);
853         }
854     }
855 }
856 
setKeyPressBaseOctave(int newOctaveNumber)857 void MidiKeyboardComponent::setKeyPressBaseOctave (int newOctaveNumber)
858 {
859     jassert (newOctaveNumber >= 0 && newOctaveNumber <= 10);
860 
861     keyMappingOctave = newOctaveNumber;
862 }
863 
keyStateChanged(bool)864 bool MidiKeyboardComponent::keyStateChanged (bool /*isKeyDown*/)
865 {
866     bool keyPressUsed = false;
867 
868     for (int i = keyPresses.size(); --i >= 0;)
869     {
870         auto note = 12 * keyMappingOctave + keyPressNotes.getUnchecked (i);
871 
872         if (keyPresses.getReference(i).isCurrentlyDown())
873         {
874             if (! keysPressed[note])
875             {
876                 keysPressed.setBit (note);
877                 state.noteOn (midiChannel, note, velocity);
878                 keyPressUsed = true;
879             }
880         }
881         else
882         {
883             if (keysPressed[note])
884             {
885                 keysPressed.clearBit (note);
886                 state.noteOff (midiChannel, note, 0.0f);
887                 keyPressUsed = true;
888             }
889         }
890     }
891 
892     return keyPressUsed;
893 }
894 
keyPressed(const KeyPress & key)895 bool MidiKeyboardComponent::keyPressed (const KeyPress& key)
896 {
897     return keyPresses.contains (key);
898 }
899 
focusLost(FocusChangeType)900 void MidiKeyboardComponent::focusLost (FocusChangeType)
901 {
902     resetAnyKeysInUse();
903 }
904 
905 } // namespace juce
906