The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

463 lines
14KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2022 - Raw Material Software Limited
  5. JUCE is an open source library subject to commercial or open-source
  6. licensing.
  7. By using JUCE, you agree to the terms of both the JUCE 7 End-User License
  8. Agreement and JUCE Privacy Policy.
  9. End User License Agreement: www.juce.com/juce-7-licence
  10. Privacy Policy: www.juce.com/juce-privacy-policy
  11. Or: You may also use this code under the terms of the GPL v3 (see
  12. www.gnu.org/licenses).
  13. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  14. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  15. DISCLAIMED.
  16. ==============================================================================
  17. */
  18. namespace juce
  19. {
  20. constexpr uint8 whiteNotes[] = { 0, 2, 4, 5, 7, 9, 11 };
  21. constexpr uint8 blackNotes[] = { 1, 3, 6, 8, 10 };
  22. //==============================================================================
  23. struct KeyboardComponentBase::UpDownButton : public Button
  24. {
  25. UpDownButton (KeyboardComponentBase& c, int d)
  26. : Button ({}), owner (c), delta (d)
  27. {
  28. }
  29. void clicked() override
  30. {
  31. auto note = owner.getLowestVisibleKey();
  32. note = delta < 0 ? (note - 1) / 12 : note / 12 + 1;
  33. owner.setLowestVisibleKey (note * 12);
  34. }
  35. using Button::clicked;
  36. void paintButton (Graphics& g, bool shouldDrawButtonAsHighlighted, bool shouldDrawButtonAsDown) override
  37. {
  38. owner.drawUpDownButton (g, getWidth(), getHeight(),
  39. shouldDrawButtonAsHighlighted, shouldDrawButtonAsDown,
  40. delta > 0);
  41. }
  42. private:
  43. KeyboardComponentBase& owner;
  44. int delta;
  45. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UpDownButton)
  46. };
  47. //==============================================================================
  48. KeyboardComponentBase::KeyboardComponentBase (Orientation o) : orientation (o)
  49. {
  50. scrollDown = std::make_unique<UpDownButton> (*this, -1);
  51. scrollUp = std::make_unique<UpDownButton> (*this, 1);
  52. addChildComponent (*scrollDown);
  53. addChildComponent (*scrollUp);
  54. colourChanged();
  55. }
  56. //==============================================================================
  57. void KeyboardComponentBase::setKeyWidth (float widthInPixels)
  58. {
  59. jassert (widthInPixels > 0);
  60. if (keyWidth != widthInPixels) // Prevent infinite recursion if the width is being computed in a 'resized()' callback
  61. {
  62. keyWidth = widthInPixels;
  63. resized();
  64. }
  65. }
  66. void KeyboardComponentBase::setScrollButtonWidth (int widthInPixels)
  67. {
  68. jassert (widthInPixels > 0);
  69. if (scrollButtonWidth != widthInPixels)
  70. {
  71. scrollButtonWidth = widthInPixels;
  72. resized();
  73. }
  74. }
  75. void KeyboardComponentBase::setOrientation (Orientation newOrientation)
  76. {
  77. if (orientation != newOrientation)
  78. {
  79. orientation = newOrientation;
  80. resized();
  81. }
  82. }
  83. void KeyboardComponentBase::setAvailableRange (int lowestNote, int highestNote)
  84. {
  85. jassert (lowestNote >= 0 && lowestNote <= 127);
  86. jassert (highestNote >= 0 && highestNote <= 127);
  87. jassert (lowestNote <= highestNote);
  88. if (rangeStart != lowestNote || rangeEnd != highestNote)
  89. {
  90. rangeStart = jlimit (0, 127, lowestNote);
  91. rangeEnd = jlimit (0, 127, highestNote);
  92. firstKey = jlimit ((float) rangeStart, (float) rangeEnd, firstKey);
  93. resized();
  94. }
  95. }
  96. void KeyboardComponentBase::setLowestVisibleKey (int noteNumber)
  97. {
  98. setLowestVisibleKeyFloat ((float) noteNumber);
  99. }
  100. void KeyboardComponentBase::setLowestVisibleKeyFloat (float noteNumber)
  101. {
  102. noteNumber = jlimit ((float) rangeStart, (float) rangeEnd, noteNumber);
  103. if (noteNumber != firstKey)
  104. {
  105. bool hasMoved = (((int) firstKey) != (int) noteNumber);
  106. firstKey = noteNumber;
  107. if (hasMoved)
  108. sendChangeMessage();
  109. resized();
  110. }
  111. }
  112. float KeyboardComponentBase::getWhiteNoteLength() const noexcept
  113. {
  114. return (orientation == horizontalKeyboard) ? (float) getHeight() : (float) getWidth();
  115. }
  116. void KeyboardComponentBase::setBlackNoteLengthProportion (float ratio) noexcept
  117. {
  118. jassert (ratio >= 0.0f && ratio <= 1.0f);
  119. if (blackNoteLengthRatio != ratio)
  120. {
  121. blackNoteLengthRatio = ratio;
  122. resized();
  123. }
  124. }
  125. float KeyboardComponentBase::getBlackNoteLength() const noexcept
  126. {
  127. auto whiteNoteLength = orientation == horizontalKeyboard ? getHeight() : getWidth();
  128. return (float) whiteNoteLength * blackNoteLengthRatio;
  129. }
  130. void KeyboardComponentBase::setBlackNoteWidthProportion (float ratio) noexcept
  131. {
  132. jassert (ratio >= 0.0f && ratio <= 1.0f);
  133. if (blackNoteWidthRatio != ratio)
  134. {
  135. blackNoteWidthRatio = ratio;
  136. resized();
  137. }
  138. }
  139. void KeyboardComponentBase::setScrollButtonsVisible (bool newCanScroll)
  140. {
  141. if (canScroll != newCanScroll)
  142. {
  143. canScroll = newCanScroll;
  144. resized();
  145. }
  146. }
  147. //==============================================================================
  148. Range<float> KeyboardComponentBase::getKeyPos (int midiNoteNumber) const
  149. {
  150. return getKeyPosition (midiNoteNumber, keyWidth)
  151. - xOffset
  152. - getKeyPosition (rangeStart, keyWidth).getStart();
  153. }
  154. float KeyboardComponentBase::getKeyStartPosition (int midiNoteNumber) const
  155. {
  156. return getKeyPos (midiNoteNumber).getStart();
  157. }
  158. float KeyboardComponentBase::getTotalKeyboardWidth() const noexcept
  159. {
  160. return getKeyPos (rangeEnd).getEnd();
  161. }
  162. KeyboardComponentBase::NoteAndVelocity KeyboardComponentBase::getNoteAndVelocityAtPosition (Point<float> pos, bool children)
  163. {
  164. if (! reallyContains (pos, children))
  165. return { -1, 0.0f };
  166. auto p = pos;
  167. if (orientation != horizontalKeyboard)
  168. {
  169. p = { p.y, p.x };
  170. if (orientation == verticalKeyboardFacingLeft)
  171. p = { p.x, (float) getWidth() - p.y };
  172. else
  173. p = { (float) getHeight() - p.x, p.y };
  174. }
  175. return remappedXYToNote (p + Point<float> (xOffset, 0));
  176. }
  177. KeyboardComponentBase::NoteAndVelocity KeyboardComponentBase::remappedXYToNote (Point<float> pos) const
  178. {
  179. auto blackNoteLength = getBlackNoteLength();
  180. if (pos.getY() < blackNoteLength)
  181. {
  182. for (int octaveStart = 12 * (rangeStart / 12); octaveStart <= rangeEnd; octaveStart += 12)
  183. {
  184. for (int i = 0; i < 5; ++i)
  185. {
  186. auto note = octaveStart + blackNotes[i];
  187. if (rangeStart <= note && note <= rangeEnd)
  188. {
  189. if (getKeyPos (note).contains (pos.x - xOffset))
  190. {
  191. return { note, jmax (0.0f, pos.y / blackNoteLength) };
  192. }
  193. }
  194. }
  195. }
  196. }
  197. for (int octaveStart = 12 * (rangeStart / 12); octaveStart <= rangeEnd; octaveStart += 12)
  198. {
  199. for (int i = 0; i < 7; ++i)
  200. {
  201. auto note = octaveStart + whiteNotes[i];
  202. if (note >= rangeStart && note <= rangeEnd)
  203. {
  204. if (getKeyPos (note).contains (pos.x - xOffset))
  205. {
  206. auto whiteNoteLength = (orientation == horizontalKeyboard) ? getHeight() : getWidth();
  207. return { note, jmax (0.0f, pos.y / (float) whiteNoteLength) };
  208. }
  209. }
  210. }
  211. }
  212. return { -1, 0 };
  213. }
  214. Rectangle<float> KeyboardComponentBase::getRectangleForKey (int note) const
  215. {
  216. jassert (note >= rangeStart && note <= rangeEnd);
  217. auto pos = getKeyPos (note);
  218. auto x = pos.getStart();
  219. auto w = pos.getLength();
  220. if (MidiMessage::isMidiNoteBlack (note))
  221. {
  222. auto blackNoteLength = getBlackNoteLength();
  223. switch (orientation)
  224. {
  225. case horizontalKeyboard: return { x, 0, w, blackNoteLength };
  226. case verticalKeyboardFacingLeft: return { (float) getWidth() - blackNoteLength, x, blackNoteLength, w };
  227. case verticalKeyboardFacingRight: return { 0, (float) getHeight() - x - w, blackNoteLength, w };
  228. default: jassertfalse; break;
  229. }
  230. }
  231. else
  232. {
  233. switch (orientation)
  234. {
  235. case horizontalKeyboard: return { x, 0, w, (float) getHeight() };
  236. case verticalKeyboardFacingLeft: return { 0, x, (float) getWidth(), w };
  237. case verticalKeyboardFacingRight: return { 0, (float) getHeight() - x - w, (float) getWidth(), w };
  238. default: jassertfalse; break;
  239. }
  240. }
  241. return {};
  242. }
  243. //==============================================================================
  244. void KeyboardComponentBase::setOctaveForMiddleC (int octaveNum)
  245. {
  246. octaveNumForMiddleC = octaveNum;
  247. repaint();
  248. }
  249. //==============================================================================
  250. void KeyboardComponentBase::drawUpDownButton (Graphics& g, int w, int h, bool mouseOver, bool buttonDown, bool movesOctavesUp)
  251. {
  252. g.fillAll (findColour (upDownButtonBackgroundColourId));
  253. float angle = 0;
  254. switch (getOrientation())
  255. {
  256. case horizontalKeyboard: angle = movesOctavesUp ? 0.0f : 0.5f; break;
  257. case verticalKeyboardFacingLeft: angle = movesOctavesUp ? 0.25f : 0.75f; break;
  258. case verticalKeyboardFacingRight: angle = movesOctavesUp ? 0.75f : 0.25f; break;
  259. default: jassertfalse; break;
  260. }
  261. Path path;
  262. path.addTriangle (0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.5f);
  263. path.applyTransform (AffineTransform::rotation (MathConstants<float>::twoPi * angle, 0.5f, 0.5f));
  264. g.setColour (findColour (upDownButtonArrowColourId)
  265. .withAlpha (buttonDown ? 1.0f : (mouseOver ? 0.6f : 0.4f)));
  266. g.fillPath (path, path.getTransformToScaleToFit (1.0f, 1.0f, (float) w - 2.0f, (float) h - 2.0f, true));
  267. }
  268. Range<float> KeyboardComponentBase::getKeyPosition (int midiNoteNumber, float targetKeyWidth) const
  269. {
  270. auto ratio = getBlackNoteWidthProportion();
  271. static const float notePos[] = { 0.0f, 1 - ratio * 0.6f,
  272. 1.0f, 2 - ratio * 0.4f,
  273. 2.0f,
  274. 3.0f, 4 - ratio * 0.7f,
  275. 4.0f, 5 - ratio * 0.5f,
  276. 5.0f, 6 - ratio * 0.3f,
  277. 6.0f };
  278. auto octave = midiNoteNumber / 12;
  279. auto note = midiNoteNumber % 12;
  280. auto start = (float) octave * 7.0f * targetKeyWidth + notePos[note] * targetKeyWidth;
  281. auto width = MidiMessage::isMidiNoteBlack (note) ? blackNoteWidthRatio * targetKeyWidth : targetKeyWidth;
  282. return { start, start + width };
  283. }
  284. //==============================================================================
  285. void KeyboardComponentBase::paint (Graphics& g)
  286. {
  287. drawKeyboardBackground (g, getLocalBounds().toFloat());
  288. for (int octaveBase = 0; octaveBase < 128; octaveBase += 12)
  289. {
  290. for (auto noteNum : whiteNotes)
  291. {
  292. const auto key = octaveBase + noteNum;
  293. if (rangeStart <= key && key <= rangeEnd)
  294. drawWhiteKey (key, g, getRectangleForKey (key));
  295. }
  296. for (auto noteNum : blackNotes)
  297. {
  298. const auto key = octaveBase + noteNum;
  299. if (rangeStart <= key && key <= rangeEnd)
  300. drawBlackKey (key, g, getRectangleForKey (key));
  301. }
  302. }
  303. }
  304. void KeyboardComponentBase::resized()
  305. {
  306. auto w = getWidth();
  307. auto h = getHeight();
  308. if (w > 0 && h > 0)
  309. {
  310. if (orientation != horizontalKeyboard)
  311. std::swap (w, h);
  312. auto kx2 = getKeyPos (rangeEnd).getEnd();
  313. if ((int) firstKey != rangeStart)
  314. {
  315. auto kx1 = getKeyPos (rangeStart).getStart();
  316. if (kx2 - kx1 <= (float) w)
  317. {
  318. firstKey = (float) rangeStart;
  319. sendChangeMessage();
  320. repaint();
  321. }
  322. }
  323. scrollDown->setVisible (canScroll && firstKey > (float) rangeStart);
  324. xOffset = 0;
  325. if (canScroll)
  326. {
  327. auto scrollButtonW = jmin (scrollButtonWidth, w / 2);
  328. auto r = getLocalBounds();
  329. if (orientation == horizontalKeyboard)
  330. {
  331. scrollDown->setBounds (r.removeFromLeft (scrollButtonW));
  332. scrollUp ->setBounds (r.removeFromRight (scrollButtonW));
  333. }
  334. else if (orientation == verticalKeyboardFacingLeft)
  335. {
  336. scrollDown->setBounds (r.removeFromTop (scrollButtonW));
  337. scrollUp ->setBounds (r.removeFromBottom (scrollButtonW));
  338. }
  339. else
  340. {
  341. scrollDown->setBounds (r.removeFromBottom (scrollButtonW));
  342. scrollUp ->setBounds (r.removeFromTop (scrollButtonW));
  343. }
  344. auto endOfLastKey = getKeyPos (rangeEnd).getEnd();
  345. auto spaceAvailable = w;
  346. auto lastStartKey = remappedXYToNote ({ endOfLastKey - (float) spaceAvailable, 0 }).note + 1;
  347. if (lastStartKey >= 0 && ((int) firstKey) > lastStartKey)
  348. {
  349. firstKey = (float) jlimit (rangeStart, rangeEnd, lastStartKey);
  350. sendChangeMessage();
  351. }
  352. xOffset = getKeyPos ((int) firstKey).getStart();
  353. }
  354. else
  355. {
  356. firstKey = (float) rangeStart;
  357. }
  358. scrollUp->setVisible (canScroll && getKeyPos (rangeEnd).getStart() > (float) w);
  359. repaint();
  360. }
  361. }
  362. //==============================================================================
  363. void KeyboardComponentBase::mouseWheelMove (const MouseEvent&, const MouseWheelDetails& wheel)
  364. {
  365. auto amount = (orientation == horizontalKeyboard && wheel.deltaX != 0)
  366. ? wheel.deltaX : (orientation == verticalKeyboardFacingLeft ? wheel.deltaY
  367. : -wheel.deltaY);
  368. setLowestVisibleKeyFloat (firstKey - amount * keyWidth);
  369. }
  370. } // namespace juce