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.

475 lines
15KB

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