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.

487 lines
16KB

  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. //==============================================================================
  21. MidiKeyboardComponent::MidiKeyboardComponent (MidiKeyboardState& stateToUse, Orientation orientationToUse)
  22. : KeyboardComponentBase (orientationToUse), state (stateToUse)
  23. {
  24. state.addListener (this);
  25. // initialise with a default set of qwerty key-mappings..
  26. int note = 0;
  27. for (char c : "awsedftgyhujkolp;")
  28. setKeyPressForNote ({ c, 0, 0 }, note++);
  29. mouseOverNotes.insertMultiple (0, -1, 32);
  30. mouseDownNotes.insertMultiple (0, -1, 32);
  31. colourChanged();
  32. setWantsKeyboardFocus (true);
  33. startTimerHz (20);
  34. }
  35. MidiKeyboardComponent::~MidiKeyboardComponent()
  36. {
  37. state.removeListener (this);
  38. }
  39. //==============================================================================
  40. void MidiKeyboardComponent::setVelocity (float v, bool useMousePosition)
  41. {
  42. velocity = v;
  43. useMousePositionForVelocity = useMousePosition;
  44. }
  45. //==============================================================================
  46. void MidiKeyboardComponent::setMidiChannel (int midiChannelNumber)
  47. {
  48. jassert (midiChannelNumber > 0 && midiChannelNumber <= 16);
  49. if (midiChannel != midiChannelNumber)
  50. {
  51. resetAnyKeysInUse();
  52. midiChannel = jlimit (1, 16, midiChannelNumber);
  53. }
  54. }
  55. void MidiKeyboardComponent::setMidiChannelsToDisplay (int midiChannelMask)
  56. {
  57. midiInChannelMask = midiChannelMask;
  58. noPendingUpdates.store (false);
  59. }
  60. //==============================================================================
  61. void MidiKeyboardComponent::clearKeyMappings()
  62. {
  63. resetAnyKeysInUse();
  64. keyPressNotes.clear();
  65. keyPresses.clear();
  66. }
  67. void MidiKeyboardComponent::setKeyPressForNote (const KeyPress& key, int midiNoteOffsetFromC)
  68. {
  69. removeKeyPressForNote (midiNoteOffsetFromC);
  70. keyPressNotes.add (midiNoteOffsetFromC);
  71. keyPresses.add (key);
  72. }
  73. void MidiKeyboardComponent::removeKeyPressForNote (int midiNoteOffsetFromC)
  74. {
  75. for (int i = keyPressNotes.size(); --i >= 0;)
  76. {
  77. if (keyPressNotes.getUnchecked (i) == midiNoteOffsetFromC)
  78. {
  79. keyPressNotes.remove (i);
  80. keyPresses.remove (i);
  81. }
  82. }
  83. }
  84. void MidiKeyboardComponent::setKeyPressBaseOctave (int newOctaveNumber)
  85. {
  86. jassert (newOctaveNumber >= 0 && newOctaveNumber <= 10);
  87. keyMappingOctave = newOctaveNumber;
  88. }
  89. //==============================================================================
  90. void MidiKeyboardComponent::resetAnyKeysInUse()
  91. {
  92. if (! keysPressed.isZero())
  93. {
  94. for (int i = 128; --i >= 0;)
  95. if (keysPressed[i])
  96. state.noteOff (midiChannel, i, 0.0f);
  97. keysPressed.clear();
  98. }
  99. for (int i = mouseDownNotes.size(); --i >= 0;)
  100. {
  101. auto noteDown = mouseDownNotes.getUnchecked (i);
  102. if (noteDown >= 0)
  103. {
  104. state.noteOff (midiChannel, noteDown, 0.0f);
  105. mouseDownNotes.set (i, -1);
  106. }
  107. mouseOverNotes.set (i, -1);
  108. }
  109. }
  110. void MidiKeyboardComponent::updateNoteUnderMouse (const MouseEvent& e, bool isDown)
  111. {
  112. updateNoteUnderMouse (e.getEventRelativeTo (this).position, isDown, e.source.getIndex());
  113. }
  114. void MidiKeyboardComponent::updateNoteUnderMouse (Point<float> pos, bool isDown, int fingerNum)
  115. {
  116. const auto noteInfo = getNoteAndVelocityAtPosition (pos);
  117. const auto newNote = noteInfo.note;
  118. const auto oldNote = mouseOverNotes.getUnchecked (fingerNum);
  119. const auto oldNoteDown = mouseDownNotes.getUnchecked (fingerNum);
  120. const auto eventVelocity = useMousePositionForVelocity ? noteInfo.velocity * velocity : velocity;
  121. if (oldNote != newNote)
  122. {
  123. repaintNote (oldNote);
  124. repaintNote (newNote);
  125. mouseOverNotes.set (fingerNum, newNote);
  126. }
  127. if (isDown)
  128. {
  129. if (newNote != oldNoteDown)
  130. {
  131. if (oldNoteDown >= 0)
  132. {
  133. mouseDownNotes.set (fingerNum, -1);
  134. if (! mouseDownNotes.contains (oldNoteDown))
  135. state.noteOff (midiChannel, oldNoteDown, eventVelocity);
  136. }
  137. if (newNote >= 0 && ! mouseDownNotes.contains (newNote))
  138. {
  139. state.noteOn (midiChannel, newNote, eventVelocity);
  140. mouseDownNotes.set (fingerNum, newNote);
  141. }
  142. }
  143. }
  144. else if (oldNoteDown >= 0)
  145. {
  146. mouseDownNotes.set (fingerNum, -1);
  147. if (! mouseDownNotes.contains (oldNoteDown))
  148. state.noteOff (midiChannel, oldNoteDown, eventVelocity);
  149. }
  150. }
  151. void MidiKeyboardComponent::repaintNote (int noteNum)
  152. {
  153. if (getRangeStart() <= noteNum && noteNum <= getRangeEnd())
  154. repaint (getRectangleForKey (noteNum).getSmallestIntegerContainer());
  155. }
  156. void MidiKeyboardComponent::mouseMove (const MouseEvent& e)
  157. {
  158. updateNoteUnderMouse (e, false);
  159. }
  160. void MidiKeyboardComponent::mouseDrag (const MouseEvent& e)
  161. {
  162. auto newNote = getNoteAndVelocityAtPosition (e.position).note;
  163. if (newNote >= 0 && mouseDraggedToKey (newNote, e))
  164. updateNoteUnderMouse (e, true);
  165. }
  166. void MidiKeyboardComponent::mouseDown (const MouseEvent& e)
  167. {
  168. auto newNote = getNoteAndVelocityAtPosition (e.position).note;
  169. if (newNote >= 0 && mouseDownOnKey (newNote, e))
  170. updateNoteUnderMouse (e, true);
  171. }
  172. void MidiKeyboardComponent::mouseUp (const MouseEvent& e)
  173. {
  174. updateNoteUnderMouse (e, false);
  175. auto note = getNoteAndVelocityAtPosition (e.position).note;
  176. if (note >= 0)
  177. mouseUpOnKey (note, e);
  178. }
  179. void MidiKeyboardComponent::mouseEnter (const MouseEvent& e)
  180. {
  181. updateNoteUnderMouse (e, false);
  182. }
  183. void MidiKeyboardComponent::mouseExit (const MouseEvent& e)
  184. {
  185. updateNoteUnderMouse (e, false);
  186. }
  187. void MidiKeyboardComponent::timerCallback()
  188. {
  189. if (noPendingUpdates.exchange (true))
  190. return;
  191. for (auto i = getRangeStart(); i <= getRangeEnd(); ++i)
  192. {
  193. const auto isOn = state.isNoteOnForChannels (midiInChannelMask, i);
  194. if (keysCurrentlyDrawnDown[i] != isOn)
  195. {
  196. keysCurrentlyDrawnDown.setBit (i, isOn);
  197. repaintNote (i);
  198. }
  199. }
  200. }
  201. bool MidiKeyboardComponent::keyStateChanged (bool /*isKeyDown*/)
  202. {
  203. bool keyPressUsed = false;
  204. for (int i = keyPresses.size(); --i >= 0;)
  205. {
  206. auto note = 12 * keyMappingOctave + keyPressNotes.getUnchecked (i);
  207. if (keyPresses.getReference(i).isCurrentlyDown())
  208. {
  209. if (! keysPressed[note])
  210. {
  211. keysPressed.setBit (note);
  212. state.noteOn (midiChannel, note, velocity);
  213. keyPressUsed = true;
  214. }
  215. }
  216. else
  217. {
  218. if (keysPressed[note])
  219. {
  220. keysPressed.clearBit (note);
  221. state.noteOff (midiChannel, note, 0.0f);
  222. keyPressUsed = true;
  223. }
  224. }
  225. }
  226. return keyPressUsed;
  227. }
  228. bool MidiKeyboardComponent::keyPressed (const KeyPress& key)
  229. {
  230. return keyPresses.contains (key);
  231. }
  232. void MidiKeyboardComponent::focusLost (FocusChangeType)
  233. {
  234. resetAnyKeysInUse();
  235. }
  236. //==============================================================================
  237. void MidiKeyboardComponent::drawKeyboardBackground (Graphics& g, Rectangle<float> area)
  238. {
  239. g.fillAll (findColour (whiteNoteColourId));
  240. auto width = area.getWidth();
  241. auto height = area.getHeight();
  242. auto currentOrientation = getOrientation();
  243. Point<float> shadowGradientStart, shadowGradientEnd;
  244. if (currentOrientation == verticalKeyboardFacingLeft)
  245. {
  246. shadowGradientStart.x = width - 1.0f;
  247. shadowGradientEnd.x = width - 5.0f;
  248. }
  249. else if (currentOrientation == verticalKeyboardFacingRight)
  250. {
  251. shadowGradientEnd.x = 5.0f;
  252. }
  253. else
  254. {
  255. shadowGradientEnd.y = 5.0f;
  256. }
  257. auto keyboardWidth = getRectangleForKey (getRangeEnd()).getRight();
  258. auto shadowColour = findColour (shadowColourId);
  259. if (! shadowColour.isTransparent())
  260. {
  261. g.setGradientFill ({ shadowColour, shadowGradientStart,
  262. shadowColour.withAlpha (0.0f), shadowGradientEnd,
  263. false });
  264. switch (currentOrientation)
  265. {
  266. case horizontalKeyboard: g.fillRect (0.0f, 0.0f, keyboardWidth, 5.0f); break;
  267. case verticalKeyboardFacingLeft: g.fillRect (width - 5.0f, 0.0f, 5.0f, keyboardWidth); break;
  268. case verticalKeyboardFacingRight: g.fillRect (0.0f, 0.0f, 5.0f, keyboardWidth); break;
  269. default: break;
  270. }
  271. }
  272. auto lineColour = findColour (keySeparatorLineColourId);
  273. if (! lineColour.isTransparent())
  274. {
  275. g.setColour (lineColour);
  276. switch (currentOrientation)
  277. {
  278. case horizontalKeyboard: g.fillRect (0.0f, height - 1.0f, keyboardWidth, 1.0f); break;
  279. case verticalKeyboardFacingLeft: g.fillRect (0.0f, 0.0f, 1.0f, keyboardWidth); break;
  280. case verticalKeyboardFacingRight: g.fillRect (width - 1.0f, 0.0f, 1.0f, keyboardWidth); break;
  281. default: break;
  282. }
  283. }
  284. }
  285. void MidiKeyboardComponent::drawWhiteNote (int midiNoteNumber, Graphics& g, Rectangle<float> area,
  286. bool isDown, bool isOver, Colour lineColour, Colour textColour)
  287. {
  288. auto c = Colours::transparentWhite;
  289. if (isDown) c = findColour (keyDownOverlayColourId);
  290. if (isOver) c = c.overlaidWith (findColour (mouseOverKeyOverlayColourId));
  291. g.setColour (c);
  292. g.fillRect (area);
  293. const auto currentOrientation = getOrientation();
  294. auto text = getWhiteNoteText (midiNoteNumber);
  295. if (text.isNotEmpty())
  296. {
  297. auto fontHeight = jmin (12.0f, getKeyWidth() * 0.9f);
  298. g.setColour (textColour);
  299. g.setFont (Font (fontHeight).withHorizontalScale (0.8f));
  300. switch (currentOrientation)
  301. {
  302. case horizontalKeyboard: g.drawText (text, area.withTrimmedLeft (1.0f).withTrimmedBottom (2.0f), Justification::centredBottom, false); break;
  303. case verticalKeyboardFacingLeft: g.drawText (text, area.reduced (2.0f), Justification::centredLeft, false); break;
  304. case verticalKeyboardFacingRight: g.drawText (text, area.reduced (2.0f), Justification::centredRight, false); break;
  305. default: break;
  306. }
  307. }
  308. if (! lineColour.isTransparent())
  309. {
  310. g.setColour (lineColour);
  311. switch (currentOrientation)
  312. {
  313. case horizontalKeyboard: g.fillRect (area.withWidth (1.0f)); break;
  314. case verticalKeyboardFacingLeft: g.fillRect (area.withHeight (1.0f)); break;
  315. case verticalKeyboardFacingRight: g.fillRect (area.removeFromBottom (1.0f)); break;
  316. default: break;
  317. }
  318. if (midiNoteNumber == getRangeEnd())
  319. {
  320. switch (currentOrientation)
  321. {
  322. case horizontalKeyboard: g.fillRect (area.expanded (1.0f, 0).removeFromRight (1.0f)); break;
  323. case verticalKeyboardFacingLeft: g.fillRect (area.expanded (0, 1.0f).removeFromBottom (1.0f)); break;
  324. case verticalKeyboardFacingRight: g.fillRect (area.expanded (0, 1.0f).removeFromTop (1.0f)); break;
  325. default: break;
  326. }
  327. }
  328. }
  329. }
  330. void MidiKeyboardComponent::drawBlackNote (int /*midiNoteNumber*/, Graphics& g, Rectangle<float> area,
  331. bool isDown, bool isOver, Colour noteFillColour)
  332. {
  333. auto c = noteFillColour;
  334. if (isDown) c = c.overlaidWith (findColour (keyDownOverlayColourId));
  335. if (isOver) c = c.overlaidWith (findColour (mouseOverKeyOverlayColourId));
  336. g.setColour (c);
  337. g.fillRect (area);
  338. if (isDown)
  339. {
  340. g.setColour (noteFillColour);
  341. g.drawRect (area);
  342. }
  343. else
  344. {
  345. g.setColour (c.brighter());
  346. auto sideIndent = 1.0f / 8.0f;
  347. auto topIndent = 7.0f / 8.0f;
  348. auto w = area.getWidth();
  349. auto h = area.getHeight();
  350. switch (getOrientation())
  351. {
  352. case horizontalKeyboard: g.fillRect (area.reduced (w * sideIndent, 0).removeFromTop (h * topIndent)); break;
  353. case verticalKeyboardFacingLeft: g.fillRect (area.reduced (0, h * sideIndent).removeFromRight (w * topIndent)); break;
  354. case verticalKeyboardFacingRight: g.fillRect (area.reduced (0, h * sideIndent).removeFromLeft (w * topIndent)); break;
  355. default: break;
  356. }
  357. }
  358. }
  359. String MidiKeyboardComponent::getWhiteNoteText (int midiNoteNumber)
  360. {
  361. if (midiNoteNumber % 12 == 0)
  362. return MidiMessage::getMidiNoteName (midiNoteNumber, true, true, getOctaveForMiddleC());
  363. return {};
  364. }
  365. void MidiKeyboardComponent::colourChanged()
  366. {
  367. setOpaque (findColour (whiteNoteColourId).isOpaque());
  368. repaint();
  369. }
  370. //==============================================================================
  371. void MidiKeyboardComponent::drawWhiteKey (int midiNoteNumber, Graphics& g, Rectangle<float> area)
  372. {
  373. drawWhiteNote (midiNoteNumber, g, area, state.isNoteOnForChannels (midiInChannelMask, midiNoteNumber),
  374. mouseOverNotes.contains (midiNoteNumber), findColour (keySeparatorLineColourId), findColour (textLabelColourId));
  375. }
  376. void MidiKeyboardComponent::drawBlackKey (int midiNoteNumber, Graphics& g, Rectangle<float> area)
  377. {
  378. drawBlackNote (midiNoteNumber, g, area, state.isNoteOnForChannels (midiInChannelMask, midiNoteNumber),
  379. mouseOverNotes.contains (midiNoteNumber), findColour (blackNoteColourId));
  380. }
  381. //==============================================================================
  382. void MidiKeyboardComponent::handleNoteOn (MidiKeyboardState*, int /*midiChannel*/, int /*midiNoteNumber*/, float /*velocity*/)
  383. {
  384. noPendingUpdates.store (false);
  385. }
  386. void MidiKeyboardComponent::handleNoteOff (MidiKeyboardState*, int /*midiChannel*/, int /*midiNoteNumber*/, float /*velocity*/)
  387. {
  388. noPendingUpdates.store (false);
  389. }
  390. //==============================================================================
  391. bool MidiKeyboardComponent::mouseDownOnKey ([[maybe_unused]] int midiNoteNumber, [[maybe_unused]] const MouseEvent& e) { return true; }
  392. bool MidiKeyboardComponent::mouseDraggedToKey ([[maybe_unused]] int midiNoteNumber, [[maybe_unused]] const MouseEvent& e) { return true; }
  393. void MidiKeyboardComponent::mouseUpOnKey ([[maybe_unused]] int midiNoteNumber, [[maybe_unused]] const MouseEvent& e) {}
  394. } // namespace juce